diff --git a/.github/workflows/artifacts-fork.yaml b/.github/workflows/artifacts-fork.yaml new file mode 100644 index 00000000000..2cb749132af --- /dev/null +++ b/.github/workflows/artifacts-fork.yaml @@ -0,0 +1,159 @@ +name: Fork Artifacts + +on: + push: + branches: + - master + tags: + - '*' + pull_request: + +jobs: + cf-images: + name: Cloudfoundry images + runs-on: ubuntu-latest + strategy: + matrix: + variant: + - alpine + + 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 }}-cf + type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }},suffix=-${{ matrix.variant }}-cf + labels: | + org.opencontainers.image.documentation=https://dexidp.io/docs/ + + - 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 + provenance: false + sbom: false + 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' + + 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/cmd/dex/config.go b/cmd/dex/config.go index dd6d2e2ab94..9734af0ea95 100644 --- a/cmd/dex/config.go +++ b/cmd/dex/config.go @@ -149,6 +149,8 @@ type OAuth2 struct { AlwaysShowLoginScreen bool `json:"alwaysShowLoginScreen"` // This is the connector that can be used for password grant PasswordConnector string `json:"passwordConnector"` + // List of additional scope prefixes to allow + AllowedScopePrefixes []string `json:"allowedScopePrefixes"` } // Web is the config format for the HTTP server. diff --git a/cmd/dex/serve.go b/cmd/dex/serve.go index 8a69c7ee3eb..0601feb18f5 100644 --- a/cmd/dex/serve.go +++ b/cmd/dex/serve.go @@ -280,6 +280,9 @@ func runServe(options serveOptions) error { if len(c.Web.AllowedOrigins) > 0 { logger.Info("config allowed origins", "origins", c.Web.AllowedOrigins) } + if len(c.OAuth2.AllowedScopePrefixes) > 0 { + logger.Info("config allowed scope prefixes", "scopes", strings.Join(c.OAuth2.AllowedScopePrefixes, ",")) + } // explicitly convert to UTC. now := func() time.Time { return time.Now().UTC() } @@ -295,6 +298,7 @@ func runServe(options serveOptions) error { Headers: c.Web.Headers.ToHTTPHeader(), AllowedOrigins: c.Web.AllowedOrigins, AllowedHeaders: c.Web.AllowedHeaders, + AllowedScopePrefixes: c.OAuth2.AllowedScopePrefixes, Issuer: c.Issuer, Storage: s, Web: c.Frontend, diff --git a/connector/connector.go b/connector/connector.go index d812390f0c8..c5b447008b3 100644 --- a/connector/connector.go +++ b/connector/connector.go @@ -103,3 +103,8 @@ type RefreshConnector interface { type TokenIdentityConnector interface { TokenIdentity(ctx context.Context, subjectTokenType, subjectToken string) (Identity, error) } + +// PayloadExtender allows connectors to enhance the payload before signing +type PayloadExtender interface { + ExtendPayload(scopes []string, payload []byte, connectorData []byte) ([]byte, error) +} diff --git a/connector/hsdp/README.md b/connector/hsdp/README.md new file mode 100644 index 00000000000..75eb9c10a23 --- /dev/null +++ b/connector/hsdp/README.md @@ -0,0 +1,121 @@ +# HSP IAM connector + +This connector allows you to use the HSP IAM service as an identity provider for your Cloud Foundry applications. + +## Configuration + +There are a few steps required to configure the HSP IAM Dex connector, specifically for CODE1 integration. In the below +example we'll assume you are going to install Dex on the following URL: + +`https://dex.example.com` + +### 1. Create HSP IAM OAuth2 OAuth2 + +Create an OAuth2 Client in your HSP IAM Organization. Set the `RedirectURI` to the Dex callback URL: + +`https://dex.example.com/callback` + +Add the following scopes, also include these as default scopes: + - auth_iam_introspect + - auth_iam_organization + - openid + - profile + - email + - name + +The `ClientId` and `ClientSecret` are required in the config step below + +### 2. Open a SNOW ticket to allow-list the Dex callback URL and to request the SAML2 login URL + +Open a General service request in SNOW to allow-list the Dex callback URL. This is required to allow the Dex callback URL to be used in the HSP IAM service. +The RedirectURI pattern to allow-list should be this: + +```https://dex.example.com/*?*``` + +Note the `*?*` at the end. This is required to allow the HSP IAM service to pass the OAuth2 code back to Dex. + +In the same SNOW ticket also request the IAM team to share the `CODE1 SAML2 Login URL`. This URL is the value to use for saml2LoginURL in the config below. +It should look like something like this: + +```https://iam-integration.iam-region.philips-healthsuite.com/authorize/saml2/login?idp_id=https://sts.windows.net/1a407a2d-7675-4d17-8692-b3ac285306e4/&client_id=sp-philips-hspiam-region&api-version=1``` + +### 3. Create one or more static clients in Dex + +Create one ore more static clients in Dex. These clients are used in your app +to integrated with Dex itself. Example: + +```yaml +config: + staticClients: + - id: example-app + secret: SecretHere + name: 'Example App' + # Where the app will be running. + redirectURIs: + - 'https://your-app.example.com/callback' +``` + +### 4. Create a hsdp connector in Dex + +```yaml +config: + connectors: + - type: hsdp + id: hsdp + name: HSP IAM Code1 + config: + trustedOrgID: 8a67a785-73bb-46d5-b73f-d951a6d3cb43 + audienceTrustMap: + example-app: 8a67a785-73bb-46d5-b73f-d951a6d3cb43 + 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: ClientId # The OAuth2 Client ID from step 1 + clientSecret: ClientSecret # The OAuth2 Client Secret from step 1 + iamURL: 'https://iam-client-test.us-east.philips-healthsuite.com' + idmURL: 'https://idm-client-test.us-east.philips-healthsuite.com' + redirectURI: https://dex.example.com/callback + getUserInfo: true + userNameKey: sub + scopes: + - auth_iam_introspect + - auth_iam_organization + - openid + - profile + - email + - name +``` + +#### argument description + +| Argument | Type | Description | +|--------------------|-------------------------------------|----------------------------------------------------------------------------------------------------------------------------| +| `trustedOrgID` | string | The default HSP IAM Organization ID to trust. This is the Organization ID of the HSP IAM Org. | +| `audienceTrustMap` | map(string) | A mapping of static clients to trusted Organization ID. Use this to override the default `trustedOrgId` for a given client | +| `issuer` | string | The HSP IAM OAuth2 issuer URL. | +| `insecureIssuer` | string | The HSP IAM OAuth2 issuer URL for introspection. | +| `saml2LoginURL` | string | The HSP IAM SAML2 login URL. | +| `clientID` | string | The OAuth2 Client ID from step 1. | +| `clientSecret` | string | The OAuth2 Client Secret from step 1. | +| `iamURL` | string | The HSP IAM URL. | +| `idmURL` | string | The HSP IDM URL. | +| `redirectURI` | string | The Dex redirect URI. | +| `getUserInfo` | bool | Whether to get user info. | +| `userNameKey` | bool | The key to use for the user name. | +| `scopes` | list(string) The scopes to request. | + + +You are now set. Dex will integrate with HSP IAM Code1 and your apps can now +integrate with Dex through OIDC. All roles assigned in the trusted HSP IAM Org will +be exposed as `claims` to your app. + +## Custom scopes + +The connector supports custom scopes. To use them, you need to create a custom scope in the HSP IAM service and then add it to the `scopes` array in the `manifest.yml` file. + +| Scope | Description | +|----------------------|--------------------------------------------| +| `hsp:iam:introspect` | Returns introspect response as a claim. | +| `hsp:iam:token` | Returns a HSP IAM access token as a claim. | + +> All the above-mentioned scopes are optional but must be specified in the `allowed_scopes` settings for them to become available. diff --git a/connector/hsdp/extend_payload.go b/connector/hsdp/extend_payload.go new file mode 100644 index 00000000000..999396f5f21 --- /dev/null +++ b/connector/hsdp/extend_payload.go @@ -0,0 +1,91 @@ +package hsdp + +import ( + "encoding/json" + "fmt" + "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 + } + + 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 + } + + extendedPayload, err := json.Marshal(originalClaims) + if err != nil { + return payload, err + } + return extendedPayload, nil +} diff --git a/connector/hsdp/hsdp.go b/connector/hsdp/hsdp.go new file mode 100644 index 00000000000..dca3f93466f --- /dev/null +++ b/connector/hsdp/hsdp.go @@ -0,0 +1,472 @@ +// 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"` + 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 + +// ConnectorData stores information for sessions authenticated by this connector +type ConnectorData struct { + RefreshToken []byte + AccessToken []byte + Assertion []byte + Groups []string + TrustedIDPOrg string + AudienceTrustMap AudienceTrustMap + 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, + 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 +} + +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 00000000000..1b74e06efaf --- /dev/null +++ b/connector/hsdp/hsdp_test.go @@ -0,0 +1,299 @@ +package hsdp_test + +import ( + "bytes" + "crypto/rand" + "crypto/rsa" + "encoding/base64" + "encoding/binary" + "encoding/json" + "errors" + "fmt" + "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) { + logger := slog.Default() + conn, err := config.Open("id", logger) + 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 00000000000..89b6e52297c --- /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 890cc8dfe58..34975f39498 100644 --- a/go.mod +++ b/go.mod @@ -26,6 +26,7 @@ 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.85.0 github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.19.1 github.com/russellhaering/goxmldsig v1.4.0 @@ -40,6 +41,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,18 +55,25 @@ 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 github.com/davecgh/go-spew v1.1.1 // indirect + github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect 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.17.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-nih-signer v1.5.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 @@ -96,7 +107,7 @@ require ( golang.org/x/mod v0.17.0 // indirect golang.org/x/sys v0.22.0 // indirect golang.org/x/text v0.16.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240725223205-93522f1f2a9f // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240722135656-d784300faade // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240730163845-b1a4ccb954bf // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index da52911df8b..239036b7bb4 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= @@ -63,6 +65,8 @@ github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8 github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= +github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-asn1-ber/asn1-ber v1.5.5 h1:MNHlNMBDgEKD4TcKr36vQN68BA00aDfjIt3/bD50WnA= @@ -78,6 +82,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.17.0 h1:SmVVlfAOtlZncTxRuinDPomC2DkXJ4E5T9gDA0AIH74= +github.com/go-playground/validator/v10 v10.17.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= 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 +97,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 +122,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 +184,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 +200,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.85.0 h1:+sJQcYPlknBH2X5mJwPaYhvOyBcm7MZ5/3OFKz6vMMY= +github.com/philips-software/go-hsdp-api v0.85.0/go.mod h1:UMwbi/28tlAPjL2JDT0YzNOnY+ESj8GUzKO7HQwoKr8= +github.com/philips-software/go-nih-signer v1.5.0 h1:qMQ2uArwgnSCbZV7GvuiN4cyHLFXqpVFojQKIiF24tE= +github.com/philips-software/go-nih-signer v1.5.0/go.mod h1:lJQZASlfNi8XiJjEIz/xjdumb7wWU8RuSETQjlaU1f4= 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= @@ -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= @@ -375,8 +399,8 @@ google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCID google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto/googleapis/api v0.0.0-20240725223205-93522f1f2a9f h1:b1Ln/PG8orm0SsBbHZWke8dDp2lrCD4jSmfglFpTZbk= -google.golang.org/genproto/googleapis/api v0.0.0-20240725223205-93522f1f2a9f/go.mod h1:AHT0dDg3SoMOgZGnZk29b5xTbPHMoEC8qthmBLJCpys= +google.golang.org/genproto/googleapis/api v0.0.0-20240722135656-d784300faade h1:WxZOF2yayUHpHSbUE6NMzumUzBxYc3YGwo0YHnbzsJY= +google.golang.org/genproto/googleapis/api v0.0.0-20240722135656-d784300faade/go.mod h1:mw8MG/Qz5wfgYr6VqVCiZcHe/GJEfI+oGGDCohaVgB0= google.golang.org/genproto/googleapis/rpc v0.0.0-20240730163845-b1a4ccb954bf h1:liao9UHurZLtiEwBgT9LMOnKYsHze6eA6w1KQCMVN2Q= google.golang.org/genproto/googleapis/rpc v0.0.0-20240730163845-b1a4ccb954bf/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= @@ -403,6 +427,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/handlers.go b/server/handlers.go index 6521bf6a939..87981f98bd2 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -728,14 +728,14 @@ func (s *Server) sendCodeResponse(w http.ResponseWriter, r *http.Request, authRe implicitOrHybrid = true var err error - accessToken, _, err = s.newAccessToken(r.Context(), authReq.ClientID, authReq.Claims, authReq.Scopes, authReq.Nonce, authReq.ConnectorID) + accessToken, _, err = s.newAccessToken(r.Context(), authReq.ClientID, authReq.Claims, authReq.Scopes, authReq.Nonce, authReq.ConnectorID, authReq.ConnectorData) if err != nil { s.logger.ErrorContext(r.Context(), "failed to create new access token", "err", err) s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError) return } - idToken, idTokenExpiry, err = s.newIDToken(r.Context(), authReq.ClientID, authReq.Claims, authReq.Scopes, authReq.Nonce, accessToken, code.ID, authReq.ConnectorID) + idToken, idTokenExpiry, err = s.newIDToken(r.Context(), authReq.ClientID, authReq.Claims, authReq.Scopes, authReq.Nonce, accessToken, code.ID, authReq.ConnectorID, authReq.ConnectorData) if err != nil { s.logger.ErrorContext(r.Context(), "failed to create ID token", "err", err) s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError) @@ -943,14 +943,14 @@ func (s *Server) handleAuthCode(w http.ResponseWriter, r *http.Request, client s } func (s *Server) exchangeAuthCode(ctx context.Context, w http.ResponseWriter, authCode storage.AuthCode, client storage.Client) (*accessTokenResponse, error) { - accessToken, _, err := s.newAccessToken(ctx, client.ID, authCode.Claims, authCode.Scopes, authCode.Nonce, authCode.ConnectorID) + accessToken, _, err := s.newAccessToken(ctx, client.ID, authCode.Claims, authCode.Scopes, authCode.Nonce, authCode.ConnectorID, authCode.ConnectorData) if err != nil { s.logger.ErrorContext(ctx, "failed to create new access token", "err", err) s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError) return nil, err } - idToken, expiry, err := s.newIDToken(ctx, client.ID, authCode.Claims, authCode.Scopes, authCode.Nonce, accessToken, authCode.ID, authCode.ConnectorID) + idToken, expiry, err := s.newIDToken(ctx, client.ID, authCode.Claims, authCode.Scopes, authCode.Nonce, accessToken, authCode.ID, authCode.ConnectorID, authCode.ConnectorData) if err != nil { s.logger.ErrorContext(ctx, "failed to create ID token", "err", err) s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError) @@ -1143,7 +1143,16 @@ func (s *Server) handlePasswordGrant(w http.ResponseWriter, r *http.Request, cli default: peerID, ok := parseCrossClientScope(scope) if !ok { - unrecognized = append(unrecognized, scope) + var recognized bool + for _, prefix := range s.allowedScopePrefixes { + if strings.HasPrefix(scope, prefix) { + recognized = true + break + } + } + if !recognized { + unrecognized = append(unrecognized, scope) + } continue } @@ -1208,14 +1217,14 @@ func (s *Server) handlePasswordGrant(w http.ResponseWriter, r *http.Request, cli Groups: identity.Groups, } - accessToken, _, err := s.newAccessToken(r.Context(), client.ID, claims, scopes, nonce, connID) + accessToken, _, err := s.newAccessToken(r.Context(), client.ID, claims, scopes, nonce, connID, identity.ConnectorData) if err != nil { s.logger.ErrorContext(r.Context(), "password grant failed to create new access token", "err", err) s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError) return } - idToken, expiry, err := s.newIDToken(r.Context(), client.ID, claims, scopes, nonce, accessToken, "", connID) + idToken, expiry, err := s.newIDToken(r.Context(), client.ID, claims, scopes, nonce, accessToken, "", connID, identity.ConnectorData) if err != nil { s.logger.ErrorContext(r.Context(), "password grant failed to create new ID token", "err", err) s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError) @@ -1412,9 +1421,9 @@ func (s *Server) handleTokenExchange(w http.ResponseWriter, r *http.Request, cli var expiry time.Time switch requestedTokenType { case tokenTypeID: - resp.AccessToken, expiry, err = s.newIDToken(r.Context(), client.ID, claims, scopes, "", "", "", connID) + resp.AccessToken, expiry, err = s.newIDToken(r.Context(), client.ID, claims, scopes, "", "", "", connID, identity.ConnectorData) case tokenTypeAccess: - resp.AccessToken, expiry, err = s.newAccessToken(r.Context(), client.ID, claims, scopes, "", connID) + resp.AccessToken, expiry, err = s.newAccessToken(r.Context(), client.ID, claims, scopes, "", connID, identity.ConnectorData) default: s.tokenErrHelper(w, errRequestNotSupported, "Invalid requested_token_type.", http.StatusBadRequest) return diff --git a/server/introspectionhandler_test.go b/server/introspectionhandler_test.go index 695bbad8e67..fee6d51b6a0 100644 --- a/server/introspectionhandler_test.go +++ b/server/introspectionhandler_test.go @@ -265,7 +265,7 @@ func TestHandleIntrospect(t *testing.T) { Email: "jane.doe@example.com", EmailVerified: true, Groups: []string{"a", "b"}, - }, []string{"openid", "email", "profile", "groups"}, "foo", "", "", "test") + }, []string{"openid", "email", "profile", "groups"}, "foo", "", "", "test", nil) require.NoError(t, err) activeRefreshToken, err := internal.Marshal(&internal.RefreshToken{RefreshId: "test", Token: "bar"}) diff --git a/server/oauth2.go b/server/oauth2.go index ec972beab13..3eb3e182d0e 100644 --- a/server/oauth2.go +++ b/server/oauth2.go @@ -93,6 +93,7 @@ func tokenErr(w http.ResponseWriter, typ, description string, statusCode int) er return nil } +// nolint const ( errInvalidRequest = "invalid_request" errUnauthorizedClient = "unauthorized_client" @@ -303,8 +304,8 @@ type federatedIDClaims struct { UserID string `json:"user_id,omitempty"` } -func (s *Server) newAccessToken(ctx context.Context, clientID string, claims storage.Claims, scopes []string, nonce, connID string) (accessToken string, expiry time.Time, err error) { - return s.newIDToken(ctx, clientID, claims, scopes, nonce, storage.NewID(), "", connID) +func (s *Server) newAccessToken(ctx context.Context, clientID string, claims storage.Claims, scopes []string, nonce, connID string, connectorData []byte) (accessToken string, expiry time.Time, err error) { + return s.newIDToken(ctx, clientID, claims, scopes, nonce, storage.NewID(), "", connID, connectorData) } func getClientID(aud audience, azp string) (string, error) { @@ -350,13 +351,19 @@ func genSubject(userID string, connID string) (string, error) { return internal.Marshal(sub) } -func (s *Server) newIDToken(ctx context.Context, clientID string, claims storage.Claims, scopes []string, nonce, accessToken, code, connID string) (idToken string, expiry time.Time, err error) { +func (s *Server) newIDToken(ctx context.Context, clientID string, claims storage.Claims, scopes []string, nonce, accessToken, code, connID string, connectorData []byte) (idToken string, expiry time.Time, err error) { keys, err := s.storage.GetKeys() if err != nil { s.logger.ErrorContext(ctx, "failed to get keys", "err", err) return "", expiry, err } + conn, err := s.getConnector(connID) + if err != nil { + s.logger.Error("Failed to get connector", "id", connID, "error", err) + return "", expiry, err + } + signingKey := keys.SigningKey if signingKey == nil { return "", expiry, fmt.Errorf("no key to sign payload with") @@ -445,6 +452,17 @@ func (s *Server) newIDToken(ctx context.Context, clientID string, claims storage return "", expiry, fmt.Errorf("could not serialize claims: %v", err) } + switch c := conn.Connector.(type) { + case connector.PayloadExtender: + extendedPayload, err := c.ExtendPayload(scopes, payload, connectorData) + if err != nil { + s.logger.Warn("failed to enhance payload", "error", err) + break + } + payload = extendedPayload + default: + } + if idToken, err = signPayload(signingKey, signingAlg, payload); err != nil { return "", expiry, fmt.Errorf("failed to sign payload: %v", err) } @@ -533,7 +551,16 @@ func (s *Server) parseAuthorizationRequest(r *http.Request) (*storage.AuthReques default: peerID, ok := parseCrossClientScope(scope) if !ok { - unrecognized = append(unrecognized, scope) + var recognized bool + for _, prefix := range s.allowedScopePrefixes { + if strings.HasPrefix(scope, prefix) { + recognized = true + break + } + } + if !recognized { + unrecognized = append(unrecognized, scope) + } continue } diff --git a/server/refreshhandlers.go b/server/refreshhandlers.go index 391d552251d..69c5a2c9f6c 100644 --- a/server/refreshhandlers.go +++ b/server/refreshhandlers.go @@ -364,14 +364,14 @@ func (s *Server) handleRefreshToken(w http.ResponseWriter, r *http.Request, clie Groups: ident.Groups, } - accessToken, _, err := s.newAccessToken(r.Context(), client.ID, claims, rCtx.scopes, rCtx.storageToken.Nonce, rCtx.storageToken.ConnectorID) + accessToken, _, err := s.newAccessToken(r.Context(), client.ID, claims, rCtx.scopes, rCtx.storageToken.Nonce, rCtx.storageToken.ConnectorID, rCtx.connectorData) if err != nil { s.logger.ErrorContext(r.Context(), "failed to create new access token", "err", err) s.refreshTokenErrHelper(w, newInternalServerError()) return } - idToken, expiry, err := s.newIDToken(r.Context(), client.ID, claims, rCtx.scopes, rCtx.storageToken.Nonce, accessToken, "", rCtx.storageToken.ConnectorID) + idToken, expiry, err := s.newIDToken(r.Context(), client.ID, claims, rCtx.scopes, rCtx.storageToken.Nonce, accessToken, "", rCtx.storageToken.ConnectorID, rCtx.connectorData) if err != nil { s.logger.ErrorContext(r.Context(), "failed to create ID token", "err", err) s.refreshTokenErrHelper(w, newInternalServerError()) diff --git a/server/server.go b/server/server.go index 1cf71c5038b..12ecdd98182 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" @@ -119,7 +120,8 @@ type Config struct { PrometheusRegistry *prometheus.Registry - HealthChecker gosundheit.Health + HealthChecker gosundheit.Health + AllowedScopePrefixes []string } // WebConfig holds the server's frontend templates and asset configuration. @@ -188,6 +190,8 @@ type Server struct { supportedGrantTypes []string + allowedScopePrefixes []string + now func() time.Time idTokensValidFor time.Duration @@ -303,6 +307,7 @@ func newServer(ctx context.Context, c Config, rotationStrategy rotationStrategy) storage: newKeyCacher(c.Storage, now), supportedResponseTypes: supportedRes, supportedGrantTypes: supportedGrants, + allowedScopePrefixes: c.AllowedScopePrefixes, idTokensValidFor: value(c.IDTokensValidFor, 24*time.Hour), authRequestsValidFor: value(c.AuthRequestsValidFor, 24*time.Hour), deviceRequestsValidFor: value(c.DeviceRequestsValidFor, 5*time.Minute), @@ -631,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) },