// Copyright 2021-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 // Package downstreamsession provides some shared helpers for creating downstream OIDC sessions. package downstreamsession import ( "context" "fmt" "slices" "time" "github.com/ory/fosite" "github.com/ory/fosite/handler/openid" fositejwt "github.com/ory/fosite/token/jwt" oidcapi "go.pinniped.dev/generated/latest/apis/supervisor/oidc" "go.pinniped.dev/internal/auditevent" "go.pinniped.dev/internal/constable" "go.pinniped.dev/internal/federationdomain/oidc" "go.pinniped.dev/internal/federationdomain/resolvedprovider" "go.pinniped.dev/internal/idtransform" "go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/psession" ) const idTransformUnexpectedErr = constable.Error("configured identity transformation or policy resulted in unexpected error") // SessionConfig is everything that is needed to start a new downstream Pinniped session, including the upstream and // downstream identities of the user. All fields are required. type SessionConfig struct { UpstreamIdentity *resolvedprovider.Identity UpstreamLoginExtras *resolvedprovider.IdentityLoginExtras // The ID of the client who started the new downstream session. ClientID string // The scopes that were granted for the new downstream session. GrantedScopes []string // The identity provider used to authenticate the user. IdentityProvider resolvedprovider.FederationDomainResolvedIdentityProvider // The fosite Requester that is starting this session. SessionIDGetter plog.SessionIDGetter } // NewPinnipedSession applies the configured FederationDomain identity transformations // and creates a downstream Pinniped session. func NewPinnipedSession( ctx context.Context, auditLogger plog.AuditLogger, c *SessionConfig, ) (*psession.PinnipedSession, error) { now := time.Now().UTC() auditLogger.Audit(auditevent.IdentityFromUpstreamIDP, &plog.AuditParams{ ReqCtx: ctx, PIIKeysAndValues: []any{ "upstreamUsername", c.UpstreamIdentity.UpstreamUsername, "upstreamGroups", c.UpstreamIdentity.UpstreamGroups, }, KeysAndValues: []any{ "upstreamIDPDisplayName", c.IdentityProvider.GetDisplayName(), "upstreamIDPType", c.IdentityProvider.GetSessionProviderType(), "upstreamIDPResourceName", c.IdentityProvider.GetProvider().GetResourceName(), "upstreamIDPResourceUID", c.IdentityProvider.GetProvider().GetResourceUID(), }, }) downstreamUsername, downstreamGroups, err := applyIdentityTransformations(ctx, c.IdentityProvider.GetTransforms(), c.UpstreamIdentity.UpstreamUsername, c.UpstreamIdentity.UpstreamGroups) if err != nil { auditLogger.Audit(auditevent.AuthenticationRejectedByTransforms, &plog.AuditParams{ ReqCtx: ctx, KeysAndValues: []any{"reason", err}, }) return nil, err } customSessionData := &psession.CustomSessionData{ Username: downstreamUsername, UpstreamUsername: c.UpstreamIdentity.UpstreamUsername, UpstreamGroups: c.UpstreamIdentity.UpstreamGroups, ProviderUID: c.IdentityProvider.GetProvider().GetResourceUID(), ProviderName: c.IdentityProvider.GetProvider().GetResourceName(), ProviderType: c.IdentityProvider.GetSessionProviderType(), Warnings: c.UpstreamLoginExtras.Warnings, } c.IdentityProvider.ApplyIDPSpecificSessionDataToSession(customSessionData, c.UpstreamIdentity.IDPSpecificSessionData) pinnipedSession := &psession.PinnipedSession{ Fosite: &openid.DefaultSession{ Claims: &fositejwt.IDTokenClaims{ Subject: c.UpstreamIdentity.DownstreamSubject, RequestedAt: now, AuthTime: now, }, }, Custom: customSessionData, } extras := map[string]any{} extras[oidcapi.IDTokenClaimAuthorizedParty] = c.ClientID if slices.Contains(c.GrantedScopes, oidcapi.ScopeUsername) { extras[oidcapi.IDTokenClaimUsername] = downstreamUsername } if slices.Contains(c.GrantedScopes, oidcapi.ScopeGroups) { if downstreamGroups == nil { downstreamGroups = []string{} } extras[oidcapi.IDTokenClaimGroups] = downstreamGroups } if len(c.UpstreamLoginExtras.DownstreamAdditionalClaims) > 0 { extras[oidcapi.IDTokenClaimAdditionalClaims] = c.UpstreamLoginExtras.DownstreamAdditionalClaims } pinnipedSession.IDTokenClaims().Extra = extras auditLogger.Audit(auditevent.SessionStarted, &plog.AuditParams{ ReqCtx: ctx, Session: c.SessionIDGetter, PIIKeysAndValues: []any{ "username", downstreamUsername, "groups", downstreamGroups, "subject", c.UpstreamIdentity.DownstreamSubject, "additionalClaims", c.UpstreamLoginExtras.DownstreamAdditionalClaims, }, KeysAndValues: []any{ "warnings", c.UpstreamLoginExtras.Warnings, }, }) return pinnipedSession, nil } // AutoApproveScopes auto-grants the scopes which we support and for which we do not require end-user approval, // if they were requested. This should only be called after it has been validated that the client is allowed to request // the scopes that it requested (which is a check performed by fosite). func AutoApproveScopes(authorizeRequester fosite.AuthorizeRequester) { for _, scope := range []string{ oidcapi.ScopeOpenID, oidcapi.ScopeOfflineAccess, oidcapi.ScopeRequestAudience, oidcapi.ScopeUsername, oidcapi.ScopeGroups, } { oidc.GrantScopeIfRequested(authorizeRequester, scope) } // For backwards-compatibility with old pinniped CLI binaries which never request the username and groups scopes // (because those scopes did not exist yet when those CLIs were released), grant/approve the username and groups // scopes even if the CLI did not request them. Basically, pretend that the CLI requested them and auto-approve // them. Newer versions of the CLI binaries will request these scopes, so after enough time has passed that // we can assume the old versions of the CLI are no longer in use in the wild, then we can remove this code and // just let the above logic handle all clients. if authorizeRequester.GetClient().GetID() == oidcapi.ClientIDPinnipedCLI { authorizeRequester.GrantScope(oidcapi.ScopeUsername) authorizeRequester.GrantScope(oidcapi.ScopeGroups) } } // applyIdentityTransformations applies an identity transformation pipeline to an upstream identity to transform // or potentially reject the identity. func applyIdentityTransformations( ctx context.Context, transforms *idtransform.TransformationPipeline, username string, groups []string, ) (string, []string, error) { transformationResult, err := transforms.Evaluate(ctx, username, groups) if err != nil { plog.Error("unexpected identity transformation error during authentication", err, "inputUsername", username) return "", nil, idTransformUnexpectedErr } if !transformationResult.AuthenticationAllowed { plog.Debug("authentication rejected by configured policy", "inputUsername", username, "inputGroups", groups) return "", nil, fmt.Errorf("configured identity policy rejected this authentication: %s", transformationResult.RejectedAuthenticationMessage) } plog.Debug("identity transformation successfully applied during authentication", "originalUsername", username, "newUsername", transformationResult.Username, "originalGroups", groups, "newGroups", transformationResult.Groups, ) return transformationResult.Username, transformationResult.Groups, nil }