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

151 lines
5.8 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"
"time"
"github.com/ory/fosite"
"github.com/ory/fosite/handler/openid"
"github.com/ory/fosite/token/jwt"
"k8s.io/utils/strings/slices"
oidcapi "go.pinniped.dev/generated/latest/apis/supervisor/oidc"
"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
}
// NewPinnipedSession applies the configured FederationDomain identity transformations
// and creates a downstream Pinniped session.
func NewPinnipedSession(
ctx context.Context,
idp resolvedprovider.FederationDomainResolvedIdentityProvider,
c *SessionConfig,
) (*psession.PinnipedSession, error) {
now := time.Now().UTC()
downstreamUsername, downstreamGroups, err := applyIdentityTransformations(ctx,
idp.GetTransforms(), c.UpstreamIdentity.UpstreamUsername, c.UpstreamIdentity.UpstreamGroups)
if err != nil {
return nil, err
}
customSessionData := &psession.CustomSessionData{
Username: downstreamUsername,
UpstreamUsername: c.UpstreamIdentity.UpstreamUsername,
UpstreamGroups: c.UpstreamIdentity.UpstreamGroups,
ProviderUID: idp.GetProvider().GetResourceUID(),
ProviderName: idp.GetProvider().GetName(),
ProviderType: idp.GetSessionProviderType(),
Warnings: c.UpstreamLoginExtras.Warnings,
}
idp.ApplyIDPSpecificSessionDataToSession(customSessionData, c.UpstreamIdentity.IDPSpecificSessionData)
pinnipedSession := &psession.PinnipedSession{
Fosite: &openid.DefaultSession{
Claims: &jwt.IDTokenClaims{
Subject: c.UpstreamIdentity.DownstreamSubject,
RequestedAt: now,
AuthTime: now,
},
},
Custom: customSessionData,
}
extras := map[string]interface{}{}
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
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
}