Files
pinniped/internal/federationdomain/resolvedprovider/resolved_provider.go
Ryan Richard 0d31e955ae Don't skip upstream group memberships when groups scope is not granted
Background: For dynamic clients, the groups scope is not always allowed
and/or requested by the client, so it will not always be granted by the
Supervisor for an authorization request.

Previously, when the groups scope was not granted, we would skip
searching for upstream groups in some scenarios.

This commit changes the behavior of authorization flows so that even
when the groups scope is not granted we still search for the upstream
group memberships as configured, and we pass the upstream group
memberships into any configured identity transformations. The identity
transformations could potentially reject the user's authentication based
on their upstream group membership.

When the groups scope is not granted, we don't include the groups in
the final Supervisor-issued ID token. This behavior is not changed.
2024-02-21 13:12:18 -08:00

174 lines
9.3 KiB
Go

// Copyright 2023-2024 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package resolvedprovider
import (
"context"
"net/http"
"github.com/ory/fosite"
"go.pinniped.dev/generated/latest/apis/supervisor/idpdiscovery/v1alpha1"
"go.pinniped.dev/internal/federationdomain/upstreamprovider"
"go.pinniped.dev/internal/idtransform"
"go.pinniped.dev/internal/psession"
"go.pinniped.dev/pkg/oidcclient/nonce"
"go.pinniped.dev/pkg/oidcclient/pkce"
)
// Identity is the information that an identity provider must determine from the upstream IDP during login.
// This information will also be passed back to the identity provider interface during a refresh flow to
// represent the user's previous identity from their original login or most recent refresh, to aid in
// validating the refresh.
type Identity struct {
// The username extracted from the upstream identity provider, before identity
// transformations are applied to determine the final downstream username.
// Must not be empty.
UpstreamUsername string
// The group names extracted from the upstream identity provider, before identity transformations
// are applied to determine the final downstream group names. nil or an empty list means that the
// user belongs to no upstream groups (before applying identity transformations to determine their
// downstream group memberships).
UpstreamGroups []string
// The downstream subject determined for this user in an identity provider-specific way.
// Must not be empty.
DownstreamSubject string
// The portion of the user's session data which is specific to the upstream identity provider type.
// Refer to the fields of psession.CustomSessionData whose types are specific to an identity provider type.
// Must not be nil.
IDPSpecificSessionData interface{}
}
// IdentityLoginExtras are additional information that an identity provider may choose to determine
// during login. This information will not be passed into the
// FederationDomainResolvedIdentityProvider.UpstreamRefresh function, so it does not impact upstream
// refreshes. Its fields are optional and may be nil.
type IdentityLoginExtras struct {
// The downstream additional claims determined for this user in an identity provider-specific way, if any.
DownstreamAdditionalClaims map[string]interface{}
// Login warnings to show the user after they exchange their downstream authcode, if any.
Warnings []string
}
// RefreshedIdentity represents the parts of an identity that an identity provider may update
// from the upstream IDP during a refresh. It will be returned when performing an upstream refresh.
type RefreshedIdentity struct {
// The username extracted from the upstream identity provider, before identity
// transformations are applied to determine the final downstream username.
// Must not be empty.
UpstreamUsername string
// The group names extracted from the upstream identity provider, before identity
// transformations are applied to determine the final downstream group names.
// If refreshing the groups was not possible, then set this to nil, and the user's old groups
// from their session will be used again. Returning an empty list of groups will mean
// that the user's upstream group membership will be updated to make them belong to no
// upstream groups (before applying identity transformations to determine their downstream
// group memberships).
UpstreamGroups []string
// The portion of the user's session data which is specific to the upstream identity provider type.
// Refer to the fields of psession.CustomSessionData whose types are specific to an identity provider type.
// Set this to be the potentially updated IDP-specific session data. If no updates were required, then
// set this to nil.
IDPSpecificSessionData interface{}
}
// UpstreamAuthorizeRequestState is the state capturing the downstream authorization request, used as a parameter to
// FederationDomainResolvedIdentityProvider.UpstreamAuthorizeRedirectURL to help formulate the upstream authorization
// request. It includes the state param that should be sent in the upstream authorization request. It also includes
// the information needed to create the PKCE and nonce parameters for the upstream authorization request. If the
// upstream authorization request does not allow PKCE, then implementations of
// FederationDomainResolvedIdentityProvider.UpstreamAuthorizeRedirectURL may choose to ignore that struct field.
type UpstreamAuthorizeRequestState struct {
EncodedStateParam string
PKCE pkce.Code
Nonce nonce.Nonce
}
type FederationDomainResolvedIdentityProvider interface {
// GetDisplayName returns the display name of this identity provider, as configured in the FederationDomain.
GetDisplayName() string
// GetProvider returns a representation of the upstream identity provider custom resource related to this
// identity provider for the FederationDomain, e.g. the OIDCIdentityProvider.
GetProvider() upstreamprovider.UpstreamIdentityProviderI
// GetSessionProviderType returns the type of session created by this identity provider.
GetSessionProviderType() psession.ProviderType
// GetIDPDiscoveryType returns the type of this identity provider, as shown by the IDP discovery endpoint.
GetIDPDiscoveryType() v1alpha1.IDPType
// GetIDPDiscoveryFlows returns the supported flows of this identity provider,
// as shown by the IDP discovery endpoint.
GetIDPDiscoveryFlows() []v1alpha1.IDPFlow
// GetTransforms returns the compiled version of the identity transformations and policies configured on the
// FederationDomain for this identity provider.
GetTransforms() *idtransform.TransformationPipeline
// CloneIDPSpecificSessionDataFromSession should reach into the provided session and return a clone
// of the field which is specific to the upstream identity provider type. If the session's field is
// nil, then return nil.
// Refer to the fields of psession.CustomSessionData whose types are specific to an identity provider type.
CloneIDPSpecificSessionDataFromSession(session *psession.CustomSessionData) interface{}
// ApplyIDPSpecificSessionDataToSession assigns the IDP-specific portion of the session data into a session.
// The IDP-specific session data provided to this function will be from an Identity that was returned by
// one of the other functions of this interface, so an implementation of this function can make safe
// assumptions about the type of idpSpecificSessionData for casting, based upon how it chooses to return
// IDPSpecificSessionData in Identity structs. If the given session already has any IDP-specific session
// data, it should be overwritten by this function.
ApplyIDPSpecificSessionDataToSession(session *psession.CustomSessionData, idpSpecificSessionData interface{})
// UpstreamAuthorizeRedirectURL returns the URL to which the user's browser can be redirected to continue
// the downstream browser-based authorization flow. Returned errors should be of type fosite.RFC6749Error.
UpstreamAuthorizeRedirectURL(state *UpstreamAuthorizeRequestState, downstreamIssuerURL string) (string, error)
// LoginFromCallback handles an OAuth-style callback in a browser-based flow. This function should complete
// the authorization with the upstream identity provider using the authCode, extract their upstream
// identity, and transform it into their downstream identity. If the upstream does not allow PKCE, then
// the pkce parameter can be ignored.
// Returned errors should be from the httperr package.
LoginFromCallback(ctx context.Context, authCode string, pkce pkce.Code, nonce nonce.Nonce, redirectURI string) (*Identity, *IdentityLoginExtras, error)
// Login performs auth using a username and password that was submitted by the client, without a web browser.
// This function should authenticate the user with the upstream identity provider, extract their upstream
// identity, and transform it into their downstream identity.
// Returned errors should be of type fosite.RFC6749Error.
Login(ctx context.Context, submittedUsername string, submittedPassword string) (*Identity, *IdentityLoginExtras, error)
// UpstreamRefresh performs a refresh with the upstream provider.
// The user's previous identity information is provided as a parameter.
// Implementations may use this information to assist in refreshes, but mutations to this argument will be ignored.
// If possible, implementations should update the user's upstream group memberships by fetching them from the
// upstream provider during the refresh, and returning them.
// Returned errors should be of type fosite.RFC6749Error.
UpstreamRefresh(ctx context.Context, identity *Identity) (refreshedIdentity *RefreshedIdentity, err error)
}
// ErrMissingUpstreamSessionInternalError returns a common type of error that can happen during a login or refresh.
func ErrMissingUpstreamSessionInternalError() *fosite.RFC6749Error {
return &fosite.RFC6749Error{
ErrorField: "error",
DescriptionField: "There was an internal server error.",
HintField: "Required upstream data not found in session.",
CodeField: http.StatusInternalServerError,
}
}
// ErrUpstreamRefreshError returns a common type of error that can happen during a refresh.
func ErrUpstreamRefreshError() *fosite.RFC6749Error {
return &fosite.RFC6749Error{
ErrorField: "error",
DescriptionField: "Error during upstream refresh.",
CodeField: http.StatusUnauthorized,
}
}