Files
pinniped/internal/federationdomain/downstreamsession/downstream_session.go

188 lines
7.2 KiB
Go

// 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
}