From 781f89bfa23fa2c4d9ecd5c3a733277765ef47ba Mon Sep 17 00:00:00 2001 From: Andy Lo-A-Foe Date: Mon, 2 Sep 2024 21:13:05 +0200 Subject: [PATCH] HSP connector Signed-off-by: Andy Lo-A-Foe --- .github/workflows/artifacts-fork.yaml | 89 +++++ .github/workflows/artifacts.yaml | 213 ------------ connector/hsdp/README.md | 60 ++++ connector/hsdp/extend_payload.go | 124 +++++++ connector/hsdp/hsdp.go | 478 ++++++++++++++++++++++++++ connector/hsdp/hsdp_test.go | 300 ++++++++++++++++ connector/hsdp/introspect.go | 66 ++++ go.mod | 11 + go.sum | 27 ++ server/server.go | 2 + 10 files changed, 1157 insertions(+), 213 deletions(-) create mode 100644 .github/workflows/artifacts-fork.yaml delete mode 100644 .github/workflows/artifacts.yaml create mode 100644 connector/hsdp/README.md create mode 100644 connector/hsdp/extend_payload.go create mode 100644 connector/hsdp/hsdp.go create mode 100644 connector/hsdp/hsdp_test.go create mode 100644 connector/hsdp/introspect.go diff --git a/.github/workflows/artifacts-fork.yaml b/.github/workflows/artifacts-fork.yaml new file mode 100644 index 0000000000..fca377ae90 --- /dev/null +++ b/.github/workflows/artifacts-fork.yaml @@ -0,0 +1,89 @@ +name: Fork Artifacts + +on: + push: + branches: + - master + tags: + - '*' + pull_request: + +jobs: + container-images: + name: Container images + runs-on: ubuntu-latest + strategy: + matrix: + variant: + - alpine + - distroless + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Gather metadata + id: meta + uses: docker/metadata-action@v4 + with: + images: | + ghcr.io/philips-forks/dex + flavor: | + latest = false + tags: | + type=ref,event=branch,enable=${{ matrix.variant == 'alpine' }} + type=ref,event=pr,enable=${{ matrix.variant == 'alpine' }} + type=semver,pattern={{raw}},enable=${{ matrix.variant == 'alpine' }} + type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && matrix.variant == 'alpine' }} + type=ref,event=branch,suffix=-${{ matrix.variant }} + type=ref,event=pr,suffix=-${{ matrix.variant }} + type=semver,pattern={{raw}},suffix=-${{ matrix.variant }} + type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }},suffix=-${{ matrix.variant }} + labels: | + org.opencontainers.image.documentation=https://dexidp.io/docs/ + + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + with: + platforms: all + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ github.token }} + if: github.event_name == 'push' + + - name: Build and push + uses: docker/build-push-action@v4 + with: + context: . + platforms: linux/amd64,linux/arm64 + # cache-from: type=gha + # cache-to: type=gha,mode=max + push: ${{ github.event_name == 'push' }} + tags: ${{ steps.meta.outputs.tags }} + build-args: | + BASE_IMAGE=${{ matrix.variant }} + VERSION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }} + COMMIT_HASH=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }} + BUILD_DATE=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.created'] }} + labels: ${{ steps.meta.outputs.labels }} + + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@0.10.0 + with: + image-ref: "ghcr.io/philips-forks/dex:${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }}" + format: "sarif" + output: "trivy-results.sarif" + if: github.event_name == 'push' + + - name: Upload Trivy scan results to GitHub Security tab + uses: github/codeql-action/upload-sarif@v2 + with: + sarif_file: "trivy-results.sarif" + if: github.event_name == 'push' diff --git a/.github/workflows/artifacts.yaml b/.github/workflows/artifacts.yaml deleted file mode 100644 index 81bc378654..0000000000 --- a/.github/workflows/artifacts.yaml +++ /dev/null @@ -1,213 +0,0 @@ -name: Artifacts - -on: - workflow_call: - inputs: - publish: - description: Publish artifacts to the artifact store - default: false - required: false - type: boolean - secrets: - DOCKER_USERNAME: - required: true - DOCKER_PASSWORD: - required: true - outputs: - container-image-name: - description: Container image name - value: ${{ jobs.container-images.outputs.name }} - container-image-digest: - description: Container image digest - value: ${{ jobs.container-images.outputs.digest }} - container-image-ref: - description: Container image ref - value: ${{ jobs.container-images.outputs.ref }} - -permissions: - contents: read - -jobs: - container-images: - name: Container images - runs-on: ubuntu-latest - strategy: - matrix: - variant: - - alpine - - distroless - - permissions: - attestations: write - contents: read - packages: write - id-token: write - security-events: write - - - outputs: - name: ${{ steps.image-name.outputs.value }} - digest: ${{ steps.build.outputs.digest }} - ref: ${{ steps.image-ref.outputs.value }} - - steps: - - name: Checkout repository - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - - - name: Set up QEMU - uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3.2.0 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db # v3.6.1 - - - name: Set up Syft - uses: anchore/sbom-action/download-syft@d94f46e13c6c62f59525ac9a1e147a99dc0b9bf5 # v0.17.0 - - - name: Install cosign - uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 # v3.5.0 - - - name: Set image name - id: image-name - run: echo "value=ghcr.io/${{ github.repository }}" >> "$GITHUB_OUTPUT" - - - name: Gather build metadata - id: meta - uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 # v5.5.1 - with: - images: | - ${{ steps.image-name.outputs.value }} - dexidp/dex - flavor: | - latest = false - tags: | - type=ref,event=branch,enable=${{ matrix.variant == 'alpine' }} - type=ref,event=pr,prefix=pr-,enable=${{ matrix.variant == 'alpine' }} - type=semver,pattern={{raw}},enable=${{ matrix.variant == 'alpine' }} - type=raw,value=latest,enable=${{ github.ref_name == github.event.repository.default_branch && matrix.variant == 'alpine' }} - type=ref,event=branch,suffix=-${{ matrix.variant }} - type=ref,event=pr,prefix=pr-,suffix=-${{ matrix.variant }} - type=semver,pattern={{raw}},suffix=-${{ matrix.variant }} - type=raw,value=latest,enable={{is_default_branch}},suffix=-${{ matrix.variant }} - labels: | - org.opencontainers.image.documentation=https://dexidp.io/docs/ - - # Multiple exporters are not supported yet - # See https://github.com/moby/buildkit/pull/2760 - - name: Determine build output - uses: haya14busa/action-cond@94f77f7a80cd666cb3155084e428254fea4281fd # v1.2.1 - id: build-output - with: - cond: ${{ inputs.publish }} - if_true: type=image,push=true - if_false: type=oci,dest=image.tar - - - name: Login to GitHub Container Registry - uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ github.token }} - if: inputs.publish - - - name: Login to Docker Hub - uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - if: inputs.publish - - - name: Build and push image - id: build - uses: docker/build-push-action@5176d81f87c23d6fc96624dfdbcd9f3830bbe445 # v6.5.0 - with: - context: . - platforms: linux/amd64,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x - tags: ${{ steps.meta.outputs.tags }} - build-args: | - BASE_IMAGE=${{ matrix.variant }} - VERSION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }} - COMMIT_HASH=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }} - BUILD_DATE=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.created'] }} - labels: ${{ steps.meta.outputs.labels }} - # cache-from: type=gha - # cache-to: type=gha,mode=max - outputs: ${{ steps.build-output.outputs.value }} - # push: ${{ inputs.publish }} - - - name: Sign the images with GitHub OIDC Token - run: | - cosign sign --yes ${{ steps.image-name.outputs.value }}@${{ steps.build.outputs.digest }} - if: inputs.publish - - - name: Set image ref - id: image-ref - run: echo "value=${{ steps.image-name.outputs.value }}@${{ steps.build.outputs.digest }}" >> "$GITHUB_OUTPUT" - - - name: Fetch image - run: skopeo --insecure-policy copy docker://${{ steps.image-ref.outputs.value }} oci-archive:image.tar - if: inputs.publish - - # Uncomment the following lines for debugging: - # - name: Upload image as artifact - # uses: actions/upload-artifact@v3 - # with: - # name: "[${{ github.job }}] OCI tarball" - # path: image.tar - - - name: Extract OCI tarball - run: | - mkdir -p image - tar -xf image.tar -C image - - # - name: List tags - # run: skopeo --insecure-policy list-tags oci:image - # - # # See https://github.com/anchore/syft/issues/1545 - # - name: Extract image from multi-arch image - # run: skopeo --override-os linux --override-arch amd64 --insecure-policy copy oci:image:${{ steps.image-name.outputs.value }}:${{ steps.meta.outputs.version }} docker-archive:docker.tar - # - # - name: Generate SBOM - # run: syft -o spdx-json=sbom-spdx.json docker-archive:docker.tar - # - # - name: Upload SBOM as artifact - # uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 - # with: - # name: "[${{ github.job }}] SBOM" - # path: sbom-spdx.json - # retention-days: 5 - - # TODO: uncomment when the action is working for non ghcr.io pushes. GH Issue: https://github.com/actions/attest-build-provenance/issues/80 - # - name: Generate build provenance attestation - # uses: actions/attest-build-provenance@210c1913531870065f03ce1f9440dd87bc0938cd # v1.4.0 - # with: - # subject-name: dexidp/dex - # subject-digest: ${{ steps.build.outputs.digest }} - # push-to-registry: true - - - name: Generate build provenance attestation - uses: actions/attest-build-provenance@210c1913531870065f03ce1f9440dd87bc0938cd # v1.4.0 - with: - subject-name: ghcr.io/dexidp/dex - subject-digest: ${{ steps.build.outputs.digest }} - push-to-registry: true - if: inputs.publish - - - name: Run Trivy vulnerability scanner - uses: aquasecurity/trivy-action@6e7b7d1fd3e4fef0c5fa8cce1229c54b2c9bd0d8 # 0.24.0 - with: - input: image - format: sarif - output: trivy-results.sarif - - - name: Upload Trivy scan results as artifact - uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4 - with: - name: "[${{ github.job }}] Trivy scan results" - path: trivy-results.sarif - retention-days: 5 - overwrite: true - - - name: Upload Trivy scan results to GitHub Security tab - uses: github/codeql-action/upload-sarif@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15 - with: - sarif_file: trivy-results.sarif diff --git a/connector/hsdp/README.md b/connector/hsdp/README.md new file mode 100644 index 0000000000..1195f1941d --- /dev/null +++ b/connector/hsdp/README.md @@ -0,0 +1,60 @@ +# hsdp connector + +This connector supports [HSP IAM](https://www.hsdp.io/documentation/identity-and-access-management-iam/getting-started) as an upstream IDP for Dex. + +# helm chart + +Dex is deployed using the [helm chart](https://artifacthub.io/packages/helm/dex/dex) from the [Artifact Hub](https://artifacthub.io/). + +# configuration + +When deploying Dex with the HSP IAM connector, you need to configure the connector in the Dex configuration file. +Helm chart users can configure the connector in the `values.yaml` file. + +Connector section example: + +```yaml + connectors: + - type: hsdp + id: hsdp + name: Philips Code1 + config: + trustedOrgID: 8a67a785-73bb-46d5-b73f-d951a6d3cb43 + tenantMap: + dae89cf0-888d-4a26-8c1d-578e97365efc: rpi5 + 8a67a785-73bb-46d5-b73f-d951a6d3cb43: starlift + issuer: 'https://iam-client-test.us-east.philips-healthsuite.com/authorize/oauth2/v2' + insecureIssuer: 'https://iam-client-test.us-east.philips-healthsuite.com/oauth2/access_token' + saml2LoginURL: 'https://iam-integration.us-east.philips-healthsuite.com/authorize/saml2/login?idp_id=https://sts.windows.net/1a407a2d-7675-4d17-8692-b3ac285306e4/&client_id=sp-philips-hspiam-useast-ct&api-version=1' + clientID: iamclient + clientSecret: SecretHere + iamURL: 'https://iam-client-test.us-east.philips-healthsuite.com' + idmURL: 'https://idm-client-test.us-east.philips-healthsuite.com' + redirectURI: https://dex.hsp.philips.com/callback + getUserInfo: true + userNameKey: sub + scopes: + - auth_iam_introspect + - auth_iam_organization + - openid + - profile + - email + - name + - federated:id +``` + +The following fields are supported: + +| Config field | Type | Description | +|----------------|-------------|----------------------------------------------------------------------------| +| trustedOrgID | string | The HSP IAM OrgID to determine claims | +| tenantMap | map(string) | Mapping of OrgIDs to tenant IDs (Observability | +| issuer | string | The issuer URL of the HSP IAM deployment | +| insecureIssuer | string | the issuer as returnd by HSP IAM. These are different in current IAM (bug) | +| saml2LoginURL | string | The SAML login URL given by HSP IAM for SSO login (code1) | +| clientID | string | An HSP IAM OAuth2 client ID | +| clientSecret | string | An HSP IAM OAuth2 client secret | +| redirectURI | string | The redirect URI of your Dex deployment. PAth should be `/callback` | +| getUserInfo | bool | Wether to inject complete userInfo as a claim in the JWT Token | +| userNameKey | string | The username key. Should be set to `sub` | +| scopes | string | The scopes to send to HSP IAM | diff --git a/connector/hsdp/extend_payload.go b/connector/hsdp/extend_payload.go new file mode 100644 index 0000000000..794572632e --- /dev/null +++ b/connector/hsdp/extend_payload.go @@ -0,0 +1,124 @@ +package hsdp + +import ( + "encoding/json" + "fmt" + "slices" + "strings" +) + +func (c *HSDPConnector) ExtendPayload(scopes []string, payload []byte, cdata []byte) ([]byte, error) { + var cd ConnectorData + var originalClaims map[string]interface{} + + trustedOrgID := c.trustedOrgID + + if err := json.Unmarshal(cdata, &cd); err != nil { + return payload, err + } + if err := json.Unmarshal(payload, &originalClaims); err != nil { + return payload, err + } + + c.logger.Info("ExtendPayload called", "sub", cd.Introspect.Sub, "user", cd.Introspect.Username) + + // Check if we have a trusted org mapping + aud := originalClaims["aud"].(string) + if orgID, ok := c.audienceTrustMap[aud]; ok { + c.logger.Info("Found trusted org mapping", "audience", aud, "org", orgID) + trustedOrgID = orgID + } + + // Service identities only support their managing org as the trusted org + // and token should expire when the service identity token expires + if cd.Introspect.IdentityType == "Service" { + trustedOrgID = cd.Introspect.Organizations.ManagingOrganization + originalClaims["exp"] = cd.Introspect.Expires + originalClaims["username"] = cd.Introspect.Sub + originalClaims["preferred_username"] = cd.Introspect.Sub + } + + for _, scope := range scopes { + // Experimental fill introspect body into claims + if scope == "hsp:iam:introspect" { + originalClaims["intr"] = cd.Introspect + } + // Experimental fill token into claims + if scope == "hsp:iam:token" { + originalClaims["tkn"] = string(cd.AccessToken) + } + } + originalClaims["idt"] = cd.Introspect.IdentityType + originalClaims["mid"] = cd.Introspect.Organizations.ManagingOrganization + originalClaims["tid"] = trustedOrgID + // Rewrite subject + var orgSubs []string + var orgGroups []string + for _, org := range cd.Introspect.Organizations.OrganizationList { + if org.OrganizationID == trustedOrgID { // Add groups from trusted IDP org + orgGroups = org.Groups + for _, group := range org.Groups { + if strings.HasPrefix(group, "sub-") { + orgSubs = append(orgSubs, fmt.Sprintf("sub:%s", strings.TrimPrefix(group, "sub-"))) + } + } + // Add roles + originalClaims["roles"] = org.Roles + // Add permissions + originalClaims["permissions"] = org.Permissions + } + } + // Rewrite name + if cd.User.GivenName != "" { + originalClaims["name"] = fmt.Sprintf("%s %s", cd.User.GivenName, cd.User.FamilyName) + } + // Inject username + if cd.Introspect.Username != "" { + originalClaims["username"] = cd.Introspect.Username + originalClaims["preferred_username"] = cd.Introspect.Username + } + if len(orgSubs) > 0 { + subs := strings.Join(orgSubs, ":") + origSub := originalClaims["sub"].(string) + originalClaims["sub"] = fmt.Sprintf("%s:id:%s", subs, origSub) + } + if len(orgGroups) > 0 || trustedOrgID != cd.TrustedIDPOrg { + originalClaims["groups"] = orgGroups + } + + // Custom claims for Observability + var readTenants []string + // Collect all orgs for which the user has LOG.READ permission + for _, org := range cd.Introspect.Organizations.OrganizationList { + if slices.Contains(org.Permissions, "LOG.READ") { + readTenants = append(readTenants, mapper(org.OrganizationID, c.tenantMap)) + } + } + if len(readTenants) > 0 { + originalClaims["ort"] = readTenants + } + + var writeTenants []string + // Collect all orgs for which the user has LOG.INDEXWRITE permission + for _, org := range cd.Introspect.Organizations.OrganizationList { + if slices.Contains(org.Permissions, "LOG.INDEXWRITE") { + writeTenants = append(writeTenants, mapper(org.OrganizationID, c.tenantMap)) + } + } + if len(writeTenants) > 0 { + originalClaims["owt"] = writeTenants + } + + extendedPayload, err := json.Marshal(originalClaims) + if err != nil { + return payload, err + } + return extendedPayload, nil +} + +func mapper(src string, data map[string]string) string { + if orgID, ok := data[src]; ok { + return orgID + } + return src +} diff --git a/connector/hsdp/hsdp.go b/connector/hsdp/hsdp.go new file mode 100644 index 0000000000..e1a2e2fad1 --- /dev/null +++ b/connector/hsdp/hsdp.go @@ -0,0 +1,478 @@ +// Package hsdp implements logging in through OpenID Connect providers. +// HSDP IAM is almost but not quite compatible with OIDC standards, hence this connector. +package hsdp + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "log/slog" + "net/http" + "net/url" + "strings" + "time" + + "github.com/philips-software/go-hsdp-api/iam" + + "github.com/coreos/go-oidc/v3/oidc" + "golang.org/x/oauth2" + + "github.com/dexidp/dex/connector" +) + +// Config holds configuration options for OpenID Connect logins. +type Config struct { + Issuer string `json:"issuer"` + InsecureIssuer string `json:"insecureIssuer"` + ClientID string `json:"clientID"` + ClientSecret string `json:"clientSecret"` + RedirectURI string `json:"redirectURI"` + TrustedOrgID string `json:"trustedOrgID"` + AudienceTrustMap AudienceTrustMap `json:"audienceTrustMap"` + TenantMap TenantMap `json:"tenantMap"` + SAML2LoginURL string `json:"saml2LoginURL"` + IAMURL string `json:"iamURL"` + IDMURL string `json:"idmURL"` + + // Extensions implemented by HSP IAM + Extension + + // Causes client_secret to be passed as POST parameters instead of basic + // auth. This is specifically "NOT RECOMMENDED" by the OAuth2 RFC, but some + // providers require it. + // + // https://tools.ietf.org/html/rfc6749#section-2.3.1 + BasicAuthUnsupported *bool `json:"basicAuthUnsupported"` + + Scopes []string `json:"scopes"` // defaults to "profile" and "email" + + TenantGroups []string `json:"tenantGroups"` + + // Optional list of whitelisted domains when using Google + // If this field is nonempty, only users from a listed domain will be allowed to log in + HostedDomains []string `json:"hostedDomains"` + + // Override the value of email_verified to true in the returned claims + InsecureSkipEmailVerified bool `json:"insecureSkipEmailVerified"` + + // InsecureEnableGroups enables groups claims. This is disabled by default until https://github.com/dexidp/dex/issues/1065 is resolved + InsecureEnableGroups bool `json:"insecureEnableGroups"` + + // PromptType will be used fot the prompt parameter (when offline_access, by default prompt=consent) + PromptType string `json:"promptType"` +} + +type Extension struct { + IntrospectionEndpoint string `json:"introspection_endpoint"` +} + +type AudienceTrustMap map[string]string + +type TenantMap map[string]string + +// ConnectorData stores information for sessions authenticated by this connector +type ConnectorData struct { + RefreshToken []byte + AccessToken []byte + Assertion []byte + Groups []string + TrustedIDPOrg string + AudienceTrustMap AudienceTrustMap + TenantMap TenantMap + Introspect iam.IntrospectResponse + User iam.Profile +} + +type caller uint + +const ( + createCaller caller = iota + refreshCaller + exchangeCaller +) + +// Open returns a connector which can be used to log in users through an upstream +// OpenID Connect provider. +func (c *Config) Open(id string, logger *slog.Logger) (conn connector.Connector, err error) { + parentContext, cancel := context.WithCancel(context.Background()) + + ctx := oidc.InsecureIssuerURLContext(parentContext, c.InsecureIssuer) + + provider, err := oidc.NewProvider(ctx, c.Issuer) + if err != nil { + cancel() + return nil, fmt.Errorf("failed to get provider: %v", err) + } + + endpoint := provider.Endpoint() + + // HSP IAM extension + if err := provider.Claims(&c.Extension); err != nil { + cancel() + return nil, fmt.Errorf("failed to get introspection endpoint: %v", err) + } + + if c.BasicAuthUnsupported != nil { + // Setting "basicAuthUnsupported" always overrides our detection. + if *c.BasicAuthUnsupported { + endpoint.AuthStyle = oauth2.AuthStyleInParams + } + } + + scopes := []string{oidc.ScopeOpenID} + if len(c.Scopes) > 0 { + filtered := removeElement(c.Scopes, "federated:id") // HSP IAM does not support scopes with colon + scopes = append(scopes, filtered...) + } else { + scopes = append(scopes, "profile", "email", "groups") + } + + // PromptType should be "consent" by default, if not set + if c.PromptType == "" { + c.PromptType = "consent" + } + + client, err := iam.NewClient(nil, &iam.Config{ + OAuth2ClientID: c.ClientID, + OAuth2Secret: c.ClientSecret, + IAMURL: c.IAMURL, + IDMURL: c.IDMURL, + }) + if err != nil { + return nil, fmt.Errorf("error creating HSP IAM client: %w", err) + } + + for a, t := range c.AudienceTrustMap { + logger.Info("audienceTrustMap", "source", a, "destination", t) + } + + clientID := c.ClientID + return &HSDPConnector{ + provider: provider, + client: client, + redirectURI: c.RedirectURI, + introspectURI: c.IntrospectionEndpoint, + trustedOrgID: c.TrustedOrgID, + audienceTrustMap: c.AudienceTrustMap, + tenantMap: c.TenantMap, + samlLoginURL: c.SAML2LoginURL, + clientID: c.ClientID, + clientSecret: c.ClientSecret, + oauth2Config: &oauth2.Config{ + ClientID: clientID, + ClientSecret: c.ClientSecret, + Endpoint: endpoint, + Scopes: scopes, + RedirectURL: c.RedirectURI, + }, + verifier: provider.Verifier( + &oidc.Config{ + ClientID: clientID, + SkipIssuerCheck: true, // Horribly broken currently + }, + ), + logger: logger, + cancel: cancel, + hostedDomains: c.HostedDomains, + insecureSkipEmailVerified: c.InsecureSkipEmailVerified, + promptType: c.PromptType, + tenantGroups: c.TenantGroups, + }, nil +} + +var ( + _ connector.CallbackConnector = (*HSDPConnector)(nil) + _ connector.RefreshConnector = (*HSDPConnector)(nil) +) + +type tokenResponse struct { + Scope string `json:"scope"` + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` + ExpiresIn int64 `json:"expires_in"` + TokenType string `json:"token_type"` + IDToken string `json:"id_token"` +} + +type HSDPConnector struct { + provider *oidc.Provider + client *iam.Client + redirectURI string + introspectURI string + trustedOrgID string + samlLoginURL string + clientID string + clientSecret string + region string + environment string + oauth2Config *oauth2.Config + verifier *oidc.IDTokenVerifier + cancel context.CancelFunc + logger *slog.Logger + hostedDomains []string + tenantGroups []string + insecureSkipEmailVerified bool + promptType string + audienceTrustMap AudienceTrustMap + tenantMap TenantMap +} + +func (c *HSDPConnector) isSAML() bool { + return len(c.samlLoginURL) > 0 +} + +func (c *HSDPConnector) Close() error { + c.cancel() + return nil +} + +func (c *HSDPConnector) LoginURL(s connector.Scopes, callbackURL, state string) (string, error) { + if c.redirectURI != callbackURL { + return "", fmt.Errorf("expected callback URL %q did not match the URL in the config %q", callbackURL, c.redirectURI) + } + + // SAML2 flow + if c.isSAML() { + cbu, _ := url.Parse(callbackURL) + values := cbu.Query() + values.Set("state", state) + cbu.RawQuery = values.Encode() + + u, err := url.Parse(c.samlLoginURL) + if err != nil { + return "", fmt.Errorf("invalid SAML2 login URL: %w", err) + } + values = u.Query() + values.Set("redirect_uri", cbu.String()) + u.RawQuery = values.Encode() + return u.String(), nil + } + + var opts []oauth2.AuthCodeOption + if len(c.hostedDomains) > 0 { + preferredDomain := c.hostedDomains[0] + if len(c.hostedDomains) > 1 { + preferredDomain = "*" + } + opts = append(opts, oauth2.SetAuthURLParam("hd", preferredDomain)) + } + + if s.OfflineAccess { + opts = append(opts, oauth2.AccessTypeOffline, oauth2.SetAuthURLParam("prompt", c.promptType)) + } + return c.oauth2Config.AuthCodeURL(state, opts...), nil +} + +type oauth2Error struct { + error string + errorDescription string +} + +func (e *oauth2Error) Error() string { + if e.errorDescription == "" { + return e.error + } + return e.error + ": " + e.errorDescription +} + +func (c *HSDPConnector) HandleCallback(s connector.Scopes, r *http.Request) (identity connector.Identity, err error) { + q := r.URL.Query() + if errType := q.Get("error"); errType != "" { + return identity, &oauth2Error{errType, q.Get("error_description")} + } + + // SAML2 flow + if c.isSAML() { + assertion := q.Get("assertion") + form := url.Values{} + form.Add("grant_type", "urn:ietf:params:oauth:grant-type:saml2-bearer") + form.Add("assertion", assertion) + requestBody := form.Encode() + req, _ := http.NewRequest(http.MethodPost, c.oauth2Config.Endpoint.TokenURL, io.NopCloser(strings.NewReader(requestBody))) + req.SetBasicAuth(c.clientID, c.clientSecret) + req.Header.Set("Accept", "application/json") + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("Api-Version", "2") + req.ContentLength = int64(len(requestBody)) + + resp, err := doRequest(r.Context(), req) + if err != nil { + return identity, err + } + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return identity, err + } + if resp.StatusCode != http.StatusOK { + return identity, fmt.Errorf("%s: %s", resp.Status, body) + } + + var tr tokenResponse + if err := json.Unmarshal(body, &tr); err != nil { + return identity, fmt.Errorf("hsdp: failed to token response: %v", err) + } + token := &oauth2.Token{ + AccessToken: tr.AccessToken, + TokenType: tr.TokenType, + RefreshToken: tr.RefreshToken, + Expiry: time.Unix(tr.ExpiresIn, 0), + } + return c.createIdentity(r.Context(), identity, token, r, createCaller) + } + + token, err := c.oauth2Config.Exchange(r.Context(), q.Get("code")) + if err != nil { + return identity, fmt.Errorf("oidc: failed to get token: %v", err) + } + + return c.createIdentity(r.Context(), identity, token, r, createCaller) +} + +// Refresh is used to refresh a session with the refresh token provided by the IdP +func (c *HSDPConnector) Refresh(ctx context.Context, s connector.Scopes, identity connector.Identity) (connector.Identity, error) { + cd := ConnectorData{} + err := json.Unmarshal(identity.ConnectorData, &cd) + if err != nil { + return identity, fmt.Errorf("oidc: failed to unmarshal connector data: %v", err) + } + + t := &oauth2.Token{ + RefreshToken: string(cd.RefreshToken), + Expiry: time.Now().Add(-time.Hour), + } + token, err := c.oauth2Config.TokenSource(ctx, t).Token() + if err != nil { + return identity, fmt.Errorf("oidc: failed to get refresh token: %v", err) + } + + return c.createIdentity(ctx, identity, token, nil, refreshCaller) +} + +func (c *HSDPConnector) TokenIdentity(ctx context.Context, subjectTokenType, subjectToken string) (connector.Identity, error) { + var identity connector.Identity + token := &oauth2.Token{ + AccessToken: subjectToken, + TokenType: "Bearer", + } + return c.createIdentity(ctx, identity, token, nil, exchangeCaller) +} + +func (c *HSDPConnector) createIdentity(ctx context.Context, identity connector.Identity, token *oauth2.Token, r *http.Request, caller caller) (connector.Identity, error) { + var claims map[string]interface{} + + cd := ConnectorData{} + + if caller == createCaller && c.isSAML() && r != nil { + // Save assertion + q := r.URL.Query() + assertion := q.Get("assertion") + cd.Assertion = []byte(assertion) + } + + // We immediately want to run getUserInfo if configured before we validate the claims + userInfo, err := c.provider.UserInfo(ctx, oauth2.StaticTokenSource(token)) + if err != nil { + return identity, fmt.Errorf("hsdp: error loading userinfo: %v", err) + } + if err := userInfo.Claims(&claims); err != nil { + return identity, fmt.Errorf("hsdp: failed to decode userinfo claims: %v", err) + } + // Introspect so we can get group assignments + introspectResponse, err := c.introspect(ctx, oauth2.StaticTokenSource(token)) + if err != nil { + return identity, fmt.Errorf("hsdp: introspect failed: %w", err) + } + + hasEmailScope := false + for _, s := range c.oauth2Config.Scopes { + if s == "email" { + hasEmailScope = true + break + } + } + + email, found := claims["email"].(string) + // For Service identities we take sub as email claim + if introspectResponse.IdentityType == "Service" { + email = introspectResponse.Sub + found = true + } + if !found && hasEmailScope { + return identity, errors.New("missing \"email\" claim") + } + + emailVerified := true + + if c.isSAML() { // For SAML2 we claim email verification for now + emailVerified = true + } + hostedDomain, _ := claims["hd"].(string) + + if len(c.hostedDomains) > 0 { + found := false + for _, domain := range c.hostedDomains { + if hostedDomain == domain { + found = true + break + } + } + if !found { + return identity, fmt.Errorf("hsdp: unexpected hd claim %v", hostedDomain) + } + } + + cd.RefreshToken = []byte(token.RefreshToken) + cd.AccessToken = []byte(token.AccessToken) + cd.Introspect = *introspectResponse + + // Get user info for profile details + user, _, err := c.client.WithToken(token.AccessToken).Users.LegacyGetUserByUUID(introspectResponse.Sub) + if err != nil { + // Should log here + } + if user != nil { + cd.User = *user + } + + identity = connector.Identity{ + UserID: introspectResponse.Sub, + Username: introspectResponse.Username, + Email: email, + EmailVerified: emailVerified, + } + + trustedOrgID := c.trustedOrgID // Default from config + + // HSP IAM groups from trustedOrgID + for _, org := range introspectResponse.Organizations.OrganizationList { + if org.OrganizationID == trustedOrgID { // Add groups from managing ORG + identity.Groups = append(identity.Groups, org.Groups...) + } + cd.Groups = identity.Groups + } + cd.TrustedIDPOrg = trustedOrgID + cd.AudienceTrustMap = c.audienceTrustMap + + // Attach connector data + connData, err := json.Marshal(&cd) + if err != nil { + return identity, fmt.Errorf("oidc: failed to encode connector data: %v", err) + } + identity.ConnectorData = connData + + return identity, nil +} + +// removeElement removes an element from a slice. It works for any ordered type (e.g., numbers, strings). +func removeElement[T comparable](slice []T, elementToRemove T) []T { + var newSlice []T + for _, item := range slice { + if item != elementToRemove { + newSlice = append(newSlice, item) + } + } + return newSlice +} diff --git a/connector/hsdp/hsdp_test.go b/connector/hsdp/hsdp_test.go new file mode 100644 index 0000000000..2467405b51 --- /dev/null +++ b/connector/hsdp/hsdp_test.go @@ -0,0 +1,300 @@ +package hsdp_test + +import ( + "bytes" + "crypto/rand" + "crypto/rsa" + "encoding/base64" + "encoding/binary" + "encoding/json" + "errors" + "fmt" + "io" + "log/slog" + "net/http" + "net/http/httptest" + "reflect" + "strings" + "testing" + "time" + + "github.com/dexidp/dex/connector/hsdp" + + "github.com/philips-software/go-hsdp-api/iam" + "gopkg.in/square/go-jose.v2" + + "github.com/dexidp/dex/connector" +) + +func TestHandleCallback(t *testing.T) { + t.Helper() + + tests := []struct { + name string + scopes []string + expectUserID string + expectUserName string + token map[string]interface{} + }{ + { + name: "simpleCase", + expectUserID: "subvalue", + expectUserName: "username", + token: map[string]interface{}{ + "sub": "subvalue", + "name": "namevalue", + "username": "username", + "email": "emailvalue", + "given_name": "givenname", + "family_name": "familyname", + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + testServer, iamServer, idmServer, err := setupServers(tc.token) + if err != nil { + t.Fatal("failed to setup test server", err) + } + defer testServer.Close() + defer iamServer.Close() + defer idmServer.Close() + + var scopes []string + if len(tc.scopes) > 0 { + scopes = tc.scopes + } else { + scopes = []string{"email", "groups"} + } + serverURL := testServer.URL + basicAuth := true + config := hsdp.Config{ + Issuer: serverURL, + ClientID: "clientID", + ClientSecret: "clientSecret", + Scopes: scopes, + IAMURL: iamServer.URL, + IDMURL: idmServer.URL, + RedirectURI: fmt.Sprintf("%s/callback", serverURL), + BasicAuthUnsupported: &basicAuth, + TenantGroups: []string{"logreaders"}, + AudienceTrustMap: map[string]string{ + "clientID": "tenantID", + }, + } + + conn, err := newConnector(config) + if err != nil { + t.Fatal("failed to create new connector", err) + } + + req, err := newRequestWithAuthCode(testServer.URL, "someCode") + if err != nil { + t.Fatal("failed to create request", err) + } + + identity, err := conn.HandleCallback(connector.Scopes{Groups: true}, req) + if err != nil { + t.Fatal("handle callback failed", err) + } + + if !reflect.DeepEqual(identity.UserID, tc.expectUserID) { + t.Errorf("Expected %+v to equal %+v", identity.UserID, tc.expectUserID) + } + if !reflect.DeepEqual(identity.Username, tc.expectUserName) { + t.Errorf("Expected %+v to equal %+v", identity.Username, tc.expectUserName) + } + if !reflect.DeepEqual(identity.EmailVerified, true) { + t.Errorf("Expected %+v to equal %+v", identity.EmailVerified, true) + } + }) + } +} + +func setupServers(tok map[string]interface{}) (dexmux *httptest.Server, iammux *httptest.Server, idmmux *httptest.Server, err error) { + key, err := rsa.GenerateKey(rand.Reader, 1024) + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to generate rsa key: %v", err) + } + + jwk := jose.JSONWebKey{ + Key: key, + KeyID: "keyId", + Algorithm: "RSA", + } + + // DEX Server + mux := http.NewServeMux() + + mux.HandleFunc("/keys", func(w http.ResponseWriter, r *http.Request) { + json.NewEncoder(w).Encode(&map[string]interface{}{ + "keys": []map[string]interface{}{{ + "alg": jwk.Algorithm, + "kty": jwk.Algorithm, + "kid": jwk.KeyID, + "n": n(&key.PublicKey), + "e": e(&key.PublicKey), + }}, + }) + }) + + mux.HandleFunc("/token", func(w http.ResponseWriter, r *http.Request) { + url := fmt.Sprintf("http://%s", r.Host) + tok["iss"] = url + tok["exp"] = time.Now().Add(time.Hour).Unix() + tok["aud"] = "clientID" + tok["user_name"] = "subvalue" + tok["name"] = "subvalue" + token, err := newToken(&jwk, tok) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + } + + w.Header().Add("Content-Type", "application/json") + json.NewEncoder(w).Encode(&map[string]string{ + "access_token": token, + "id_token": token, + "token_type": "Bearer", + }) + }) + + mux.HandleFunc("/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) { + url := fmt.Sprintf("http://%s", r.Host) + + json.NewEncoder(w).Encode(&map[string]string{ + "issuer": url, + "token_endpoint": fmt.Sprintf("%s/token", url), + "authorization_endpoint": fmt.Sprintf("%s/authorize", url), + "userinfo_endpoint": fmt.Sprintf("%s/userinfo", url), + "jwks_uri": fmt.Sprintf("%s/keys", url), + "introspection_endpoint": fmt.Sprintf("%s/introspect", url), + }) + }) + + mux.HandleFunc("/introspect", func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/json") + json.NewEncoder(w).Encode(&iam.IntrospectResponse{ + Active: true, + Username: tok["username"].(string), + Sub: tok["sub"].(string), + }) + }) + mux.HandleFunc("/userinfo", func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/json") + json.NewEncoder(w).Encode(tok) + }) + + up := struct { + Status string + }{ + Status: "OK", + } + + // IAM Server + iamMUX := http.NewServeMux() + iamMUX.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/json") + json.NewEncoder(w).Encode(up) + }) + + // IDM Server + idmMUX := http.NewServeMux() + idmMUX.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/json") + json.NewEncoder(w).Encode(up) + }) + + type exchange struct { + LoginID string `json:"loginId"` + Profile iam.Profile `json:"profile"` + } + responseStruct := struct { + Exchange exchange `json:"exchange"` + ResponseCode string `json:"responseCode"` + ResponseMessage string `json:"responseMessage"` + }{ + Exchange: exchange{ + LoginID: "rwanson", + Profile: iam.Profile{ + GivenName: "Ron", + FamilyName: "Swanson", + }, + }, + ResponseCode: "OK", + ResponseMessage: "OK", + } + + idmMUX.HandleFunc("/security/users/subvalue", func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/json") + json.NewEncoder(w).Encode(responseStruct) + }) + + return httptest.NewServer(mux), httptest.NewServer(iamMUX), httptest.NewServer(idmMUX), nil +} + +func newToken(key *jose.JSONWebKey, claims map[string]interface{}) (string, error) { + signingKey := jose.SigningKey{ + Key: key, + Algorithm: jose.RS256, + } + + signer, err := jose.NewSigner(signingKey, &jose.SignerOptions{}) + if err != nil { + return "", fmt.Errorf("failed to create new signer: %v", err) + } + + payload, err := json.Marshal(claims) + if err != nil { + return "", fmt.Errorf("failed to marshal claims: %v", err) + } + + signature, err := signer.Sign(payload) + if err != nil { + return "", fmt.Errorf("failed to sign: %v", err) + } + return signature.CompactSerialize() +} + +func newConnector(config hsdp.Config) (*hsdp.HSDPConnector, error) { + log := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{})) + conn, err := config.Open("id", log) + if err != nil { + return nil, fmt.Errorf("unable to open: %v", err) + } + + hsdpConn, ok := conn.(*hsdp.HSDPConnector) + if !ok { + return nil, errors.New("failed to convert to HSDPConnector") + } + + return hsdpConn, nil +} + +func newRequestWithAuthCode(serverURL string, code string) (*http.Request, error) { + req, err := http.NewRequest("GET", serverURL, nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %v", err) + } + + values := req.URL.Query() + values.Add("code", code) + req.URL.RawQuery = values.Encode() + + return req, nil +} + +func n(pub *rsa.PublicKey) string { + return encode(pub.N.Bytes()) +} + +func e(pub *rsa.PublicKey) string { + data := make([]byte, 8) + binary.BigEndian.PutUint64(data, uint64(pub.E)) + return encode(bytes.TrimLeft(data, "\x00")) +} + +func encode(payload []byte) string { + result := base64.URLEncoding.EncodeToString(payload) + return strings.TrimRight(result, "=") +} diff --git a/connector/hsdp/introspect.go b/connector/hsdp/introspect.go new file mode 100644 index 0000000000..89b6e52297 --- /dev/null +++ b/connector/hsdp/introspect.go @@ -0,0 +1,66 @@ +package hsdp + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "strings" + + "github.com/philips-software/go-hsdp-api/iam" + "golang.org/x/oauth2" +) + +func (c *HSDPConnector) introspect(ctx context.Context, tokenSource oauth2.TokenSource) (*iam.IntrospectResponse, error) { + if c.introspectURI == "" { + return nil, errors.New("hsdp: introspect endpoint is missing") + } + + req, err := http.NewRequest("POST", c.introspectURI, nil) + if err != nil { + return nil, fmt.Errorf("hsdp: create GET request: %v", err) + } + + token, err := tokenSource.Token() + if err != nil { + return nil, fmt.Errorf("hsdp: get access token: %v", err) + } + + form := url.Values{} + form.Add("token", token.AccessToken) + req.Body = io.NopCloser(strings.NewReader(form.Encode())) + req.ContentLength = int64(len(form.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("Api-Version", "4") + req.SetBasicAuth(c.oauth2Config.ClientID, c.oauth2Config.ClientSecret) + + resp, err := doRequest(ctx, req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("%s: %s", resp.Status, body) + } + + var introspectResponse iam.IntrospectResponse + if err := json.Unmarshal(body, &introspectResponse); err != nil { + return nil, fmt.Errorf("hsdp: failed to decode introspect: %v", err) + } + return &introspectResponse, nil +} + +func doRequest(ctx context.Context, req *http.Request) (*http.Response, error) { + client := http.DefaultClient + if c, ok := ctx.Value(oauth2.HTTPClient).(*http.Client); ok { + client = c + } + return client.Do(req.WithContext(ctx)) +} diff --git a/go.mod b/go.mod index 890cc8dfe5..16f35c89a8 100644 --- a/go.mod +++ b/go.mod @@ -26,9 +26,11 @@ require ( github.com/mattermost/xml-roundtrip-validator v0.1.0 github.com/mattn/go-sqlite3 v1.14.22 github.com/oklog/run v1.1.0 + github.com/philips-software/go-hsdp-api v0.81.0 github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.19.1 github.com/russellhaering/goxmldsig v1.4.0 + github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.8.1 github.com/stretchr/testify v1.9.0 go.etcd.io/etcd/client/pkg/v3 v3.5.15 @@ -40,6 +42,7 @@ require ( google.golang.org/api v0.190.0 google.golang.org/grpc v1.65.0 google.golang.org/protobuf v1.34.2 + gopkg.in/square/go-jose.v2 v2.6.0 ) require ( @@ -53,6 +56,7 @@ require ( github.com/agext/levenshtein v1.2.1 // indirect github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect github.com/beorn7/perks v1.0.1 // indirect + github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/coreos/go-semver v0.3.0 // indirect github.com/coreos/go-systemd/v22 v22.3.2 // indirect @@ -61,10 +65,15 @@ require ( github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/inflect v0.19.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.13.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/go-cmp v0.6.0 // indirect + github.com/google/go-querystring v1.1.0 // indirect github.com/google/s2a-go v0.1.8 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect github.com/googleapis/gax-go/v2 v2.13.0 // indirect @@ -73,9 +82,11 @@ require ( github.com/imdario/mergo v0.3.11 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jonboulle/clockwork v0.2.2 // indirect + github.com/leodido/go-urn v1.2.4 // indirect github.com/mitchellh/copystructure v1.0.0 // indirect github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect github.com/mitchellh/reflectwalk v1.0.0 // indirect + github.com/philips-software/go-hsdp-signer v1.4.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.5.0 // indirect github.com/prometheus/common v0.48.0 // indirect diff --git a/go.sum b/go.sum index da52911df8..bd1becadd9 100644 --- a/go.sum +++ b/go.sum @@ -37,6 +37,8 @@ github.com/beevik/etree v1.4.0 h1:oz1UedHRepuY3p4N5OjE0nK1WLCqtzHf25bxplKOHLs= github.com/beevik/etree v1.4.0/go.mod h1:cyWiXwGoasx60gHvtnEh5x8+uIjUVnjWqBvEnhnqKDA= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -78,6 +80,14 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4= github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.13.0 h1:cFRQdfaSMCOSfGCCLB20MHvuoHb/s5G8L5pu2ppK5AQ= +github.com/go-playground/validator/v10 v10.13.0/go.mod h1:dwu7+CG8/CtBiJFZDz4e+5Upb6OLw04gtBYw0mcG/z4= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= @@ -85,6 +95,8 @@ github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3a github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= @@ -108,9 +120,12 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM= github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -167,6 +182,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mattermost/xml-roundtrip-validator v0.1.0 h1:RXbVD2UAl7A7nOTR4u7E3ILa4IbtvKBHw64LDsmu9hU= @@ -181,6 +198,10 @@ github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/I github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= +github.com/philips-software/go-hsdp-api v0.81.0 h1:pw+946K5/yiam+PE5ctvvzaXFG5wnQ7RZPebKv1/L/s= +github.com/philips-software/go-hsdp-api v0.81.0/go.mod h1:A36oEWU86aIrM2nd9JUSmrdDDpA6lpkrL3XqKLVAhBQ= +github.com/philips-software/go-hsdp-signer v1.4.0 h1:yg7UILhmI4xJhr/tQiAiQwJL0EZFvLuMqpH2GZ9ygY4= +github.com/philips-software/go-hsdp-signer v1.4.0/go.mod h1:/QehZ/+Aks2t1TFpjhF/7ZSB8PJIIJHzLc03rOqwLw0= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -207,6 +228,8 @@ github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA= github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= @@ -229,6 +252,7 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= @@ -325,6 +349,7 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -403,6 +428,8 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= +gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/server/server.go b/server/server.go index 5c508b9234..12ecdd9818 100644 --- a/server/server.go +++ b/server/server.go @@ -37,6 +37,7 @@ import ( "github.com/dexidp/dex/connector/github" "github.com/dexidp/dex/connector/gitlab" "github.com/dexidp/dex/connector/google" + "github.com/dexidp/dex/connector/hsdp" "github.com/dexidp/dex/connector/keystone" "github.com/dexidp/dex/connector/ldap" "github.com/dexidp/dex/connector/linkedin" @@ -635,6 +636,7 @@ var ConnectorsConfig = map[string]func() ConnectorConfig{ "github": func() ConnectorConfig { return new(github.Config) }, "gitlab": func() ConnectorConfig { return new(gitlab.Config) }, "google": func() ConnectorConfig { return new(google.Config) }, + "hsdp": func() ConnectorConfig { return new(hsdp.Config) }, "oidc": func() ConnectorConfig { return new(oidc.Config) }, "oauth": func() ConnectorConfig { return new(oauth.Config) }, "saml": func() ConnectorConfig { return new(saml.Config) },