Skip to content

Commit

Permalink
Refactor to move invocation of identity transforms out of IDP interfaces
Browse files Browse the repository at this point in the history
Each endpoint handler is now responsible for applying the identity
transformations and creating most of the session data, rather than each
implementation of the upstream IDP interface. This shares code better,
and reduces the responsibilities of the implementations of the IDP
interface by letting them focus more on the upstream stuff.

Also refactor the parameters and return types of the IDP interfaces to
make them more clear, and because they can be more focused on upstream
identities (pre-identity transformation). This clarifies the
responsibilities of the implementations of the IDP interface.
  • Loading branch information
cfryanr committed Feb 17, 2024
1 parent 64be06a commit 035e48c
Show file tree
Hide file tree
Showing 9 changed files with 508 additions and 320 deletions.
69 changes: 53 additions & 16 deletions internal/federationdomain/downstreamsession/downstream_session.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,38 +25,75 @@ import (

const idTransformUnexpectedErr = constable.Error("configured identity transformation or policy resulted in unexpected error")

// MakeDownstreamSession creates a downstream OIDC session.
func MakeDownstreamSession(identity *resolvedprovider.Identity, grantedScopes []string, clientID string) *psession.PinnipedSession {
// 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 downstream username.
Username string
// The downstream groups.
Groups []string

// 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 creates a downstream Pinniped session.
func NewPinnipedSession(
idp resolvedprovider.FederationDomainResolvedIdentityProvider,
c *SessionConfig,
) *psession.PinnipedSession {
now := time.Now().UTC()
openIDSession := &psession.PinnipedSession{

customSessionData := &psession.CustomSessionData{
Username: c.Username,
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: identity.Subject,
Subject: c.UpstreamIdentity.DownstreamSubject,
RequestedAt: now,
AuthTime: now,
},
},
Custom: identity.SessionData,
Custom: customSessionData,
}

extras := map[string]interface{}{}
extras[oidcapi.IDTokenClaimAuthorizedParty] = clientID
if slices.Contains(grantedScopes, oidcapi.ScopeUsername) {
extras[oidcapi.IDTokenClaimUsername] = identity.SessionData.Username

extras[oidcapi.IDTokenClaimAuthorizedParty] = c.ClientID

if slices.Contains(c.GrantedScopes, oidcapi.ScopeUsername) {
extras[oidcapi.IDTokenClaimUsername] = c.Username
}
if slices.Contains(grantedScopes, oidcapi.ScopeGroups) {
groups := identity.Groups

if slices.Contains(c.GrantedScopes, oidcapi.ScopeGroups) {
groups := c.Groups
if groups == nil {
groups = []string{}
}
extras[oidcapi.IDTokenClaimGroups] = groups
}
if len(identity.AdditionalClaims) > 0 {
extras[oidcapi.IDTokenClaimAdditionalClaims] = identity.AdditionalClaims

if len(c.UpstreamLoginExtras.DownstreamAdditionalClaims) > 0 {
extras[oidcapi.IDTokenClaimAdditionalClaims] = c.UpstreamLoginExtras.DownstreamAdditionalClaims
}
openIDSession.IDTokenClaims().Extra = extras

return openIDSession
pinnipedSession.IDTokenClaims().Extra = extras

return pinnipedSession
}

// AutoApproveScopes auto-grants the scopes which we support and for which we do not require end-user approval,
Expand Down Expand Up @@ -89,11 +126,11 @@ func AutoApproveScopes(authorizeRequester fosite.AuthorizeRequester) {
// or potentially reject the identity.
func ApplyIdentityTransformations(
ctx context.Context,
identityTransforms *idtransform.TransformationPipeline,
transforms *idtransform.TransformationPipeline,
username string,
groups []string,
) (string, []string, error) {
transformationResult, err := identityTransforms.Evaluate(ctx, username, groups)
transformationResult, err := transforms.Evaluate(ctx, username, groups)
if err != nil {
plog.Error("unexpected identity transformation error during authentication", err, "inputUsername", username)
return "", nil, idTransformUnexpectedErr
Expand Down
39 changes: 25 additions & 14 deletions internal/federationdomain/endpoints/auth/auth_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ func (h *authorizeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}

upstreamProvider, err := chooseUpstreamIDP(idpNameQueryParamValue, h.idpFinder)
idp, err := chooseUpstreamIDP(idpNameQueryParamValue, h.idpFinder)
if err != nil {
oidc.WriteAuthorizeError(r, w,
h.oauthHelperWithoutStorage,
Expand All @@ -142,15 +142,15 @@ func (h *authorizeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}

h.authorize(w, r, requestedBrowserlessFlow, idpNameQueryParamValue, upstreamProvider)
h.authorize(w, r, requestedBrowserlessFlow, idpNameQueryParamValue, idp)
}

func (h *authorizeHandler) authorize(
w http.ResponseWriter,
r *http.Request,
requestedBrowserlessFlow bool,
idpNameQueryParamValue string,
upstreamProvider resolvedprovider.FederationDomainResolvedIdentityProvider,
idp resolvedprovider.FederationDomainResolvedIdentityProvider,
) {
// Browser flows do not need session storage at this step. For browser flows, the request parameters
// should be forwarded to the next step as upstream state parameters to avoid storing session state
Expand All @@ -177,9 +177,9 @@ func (h *authorizeHandler) authorize(
downstreamsession.AutoApproveScopes(authorizeRequester)

if requestedBrowserlessFlow {
err = h.authorizeWithoutBrowser(r, w, oauthHelper, authorizeRequester, upstreamProvider)
err = h.authorizeWithoutBrowser(r, w, oauthHelper, authorizeRequester, idp)
} else {
err = h.authorizeWithBrowser(r, w, oauthHelper, authorizeRequester, upstreamProvider)
err = h.authorizeWithBrowser(r, w, oauthHelper, authorizeRequester, idp)
}
if err != nil {
oidc.WriteAuthorizeError(r, w, oauthHelper, authorizeRequester, err, requestedBrowserlessFlow)
Expand All @@ -191,7 +191,7 @@ func (h *authorizeHandler) authorizeWithoutBrowser(
w http.ResponseWriter,
oauthHelper fosite.OAuth2Provider,
authorizeRequester fosite.AuthorizeRequester,
upstreamProvider resolvedprovider.FederationDomainResolvedIdentityProvider,
idp resolvedprovider.FederationDomainResolvedIdentityProvider,
) error {
if err := requireStaticClientForUsernameAndPasswordHeaders(authorizeRequester); err != nil {
return err
Expand All @@ -204,14 +204,25 @@ func (h *authorizeHandler) authorizeWithoutBrowser(

groupsWillBeIgnored := !slices.Contains(authorizeRequester.GetGrantedScopes(), oidcapi.ScopeGroups)

identity, err := upstreamProvider.Login(r.Context(), submittedUsername, submittedPassword, groupsWillBeIgnored)
identity, loginExtras, err := idp.Login(r.Context(), submittedUsername, submittedPassword, groupsWillBeIgnored)
if err != nil {
return err
}

session := downstreamsession.MakeDownstreamSession(
identity, authorizeRequester.GetGrantedScopes(), authorizeRequester.GetClient().GetID(),
)
username, groups, err := downstreamsession.ApplyIdentityTransformations(r.Context(),
idp.GetTransforms(), identity.UpstreamUsername, identity.UpstreamGroups)
if err != nil {
return fosite.ErrAccessDenied.WithHintf("Reason: %s.", err.Error())
}

session := downstreamsession.NewPinnipedSession(idp, &downstreamsession.SessionConfig{
UpstreamIdentity: identity,
UpstreamLoginExtras: loginExtras,
Username: username,
Groups: groups,
ClientID: authorizeRequester.GetClient().GetID(),
GrantedScopes: authorizeRequester.GetGrantedScopes(),
})

oidc.PerformAuthcodeRedirect(r, w, oauthHelper, authorizeRequester, session, true)

Expand All @@ -223,24 +234,24 @@ func (h *authorizeHandler) authorizeWithBrowser(
w http.ResponseWriter,
oauthHelper fosite.OAuth2Provider,
authorizeRequester fosite.AuthorizeRequester,
upstreamProvider resolvedprovider.FederationDomainResolvedIdentityProvider,
idp resolvedprovider.FederationDomainResolvedIdentityProvider,
) error {
authRequestState, err := generateUpstreamAuthorizeRequestState(r, w,
authorizeRequester,
oauthHelper,
h.generateCSRF,
h.generateNonce,
h.generatePKCE,
upstreamProvider.GetDisplayName(),
upstreamProvider.GetSessionProviderType(),
idp.GetDisplayName(),
idp.GetSessionProviderType(),
h.cookieCodec,
h.upstreamStateEncoder,
)
if err != nil {
return err
}

redirectURL, err := upstreamProvider.UpstreamAuthorizeRedirectURL(authRequestState, h.downstreamIssuerURL)
redirectURL, err := idp.UpstreamAuthorizeRedirectURL(authRequestState, h.downstreamIssuerURL)
if err != nil {
return err
}
Expand Down
26 changes: 18 additions & 8 deletions internal/federationdomain/endpoints/callback/callback_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ func NewHandler(
return err
}

oidcIdentityProvider, err := upstreamIDPs.FindUpstreamIDPByDisplayName(state.UpstreamName)
if err != nil || oidcIdentityProvider == nil {
idp, err := upstreamIDPs.FindUpstreamIDPByDisplayName(state.UpstreamName)
if err != nil || idp == nil {
plog.Warning("upstream provider not found")
return httperr.New(http.StatusUnprocessableEntity, "upstream provider not found")
}
Expand All @@ -57,21 +57,31 @@ func NewHandler(
// an error if the client requested a scope that they are not allowed to request, so we don't need to worry about that here.
downstreamsession.AutoApproveScopes(authorizeRequester)

identity, err := oidcIdentityProvider.HandleCallback(r.Context(), authcode(r), state.PKCECode, state.Nonce, redirectURI)
identity, loginExtras, err := idp.HandleCallback(r.Context(), authcode(r), state.PKCECode, state.Nonce, redirectURI)
if err != nil {
return err
}

session := downstreamsession.MakeDownstreamSession(
identity,
authorizeRequester.GetGrantedScopes(),
authorizeRequester.GetClient().GetID(),
username, groups, err := downstreamsession.ApplyIdentityTransformations(
r.Context(), idp.GetTransforms(), identity.UpstreamUsername, identity.UpstreamGroups,
)
if err != nil {
return httperr.Wrap(http.StatusUnprocessableEntity, err.Error(), err)
}

session := downstreamsession.NewPinnipedSession(idp, &downstreamsession.SessionConfig{
UpstreamIdentity: identity,
UpstreamLoginExtras: loginExtras,
Username: username,
Groups: groups,
ClientID: authorizeRequester.GetClient().GetID(),
GrantedScopes: authorizeRequester.GetGrantedScopes(),
})

authorizeResponder, err := oauthHelper.NewAuthorizeResponse(r.Context(), authorizeRequester, session)
if err != nil {
plog.WarningErr("error while generating and saving authcode", err,
"identityProviderDisplayName", oidcIdentityProvider.GetDisplayName(), "fositeErr", oidc.FositeErrorForLog(err))
"identityProviderDisplayName", idp.GetDisplayName(), "fositeErr", oidc.FositeErrorForLog(err))
return httperr.Wrap(http.StatusInternalServerError, "error while generating and saving authcode", err)
}

Expand Down
27 changes: 19 additions & 8 deletions internal/federationdomain/endpoints/login/post_login_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import (
func NewPostHandler(issuerURL string, upstreamIDPs federationdomainproviders.FederationDomainIdentityProvidersFinderI, oauthHelper fosite.OAuth2Provider) HandlerFunc {
return func(w http.ResponseWriter, r *http.Request, encodedState string, decodedState *oidc.UpstreamStateParamData) error {
// Note that the login handler prevents this handler from being called with OIDC upstreams.
ldapUpstream, err := upstreamIDPs.FindUpstreamIDPByDisplayName(decodedState.UpstreamName)
idp, err := upstreamIDPs.FindUpstreamIDPByDisplayName(decodedState.UpstreamName)
if err != nil {
// This shouldn't normally happen because the authorization endpoint ensured that this provider existed
// at that time. It would be possible in the unlikely event that the provider was deleted during the login.
Expand Down Expand Up @@ -70,7 +70,7 @@ func NewPostHandler(issuerURL string, upstreamIDPs federationdomainproviders.Fed
skipGroups := !slices.Contains(authorizeRequester.GetGrantedScopes(), oidcapi.ScopeGroups)

// Attempt to authenticate the user with the upstream IDP.
identity, err := ldapUpstream.Login(r.Context(), submittedUsername, submittedPassword, skipGroups)
identity, loginExtras, err := idp.Login(r.Context(), submittedUsername, submittedPassword, skipGroups)
if err != nil {
switch {
case errors.Is(err, resolvedldap.ErrUnexpectedUpstreamLDAPError):
Expand All @@ -82,17 +82,28 @@ func NewPostHandler(issuerURL string, upstreamIDPs federationdomainproviders.Fed
// The user may try to log in again if they'd like, so redirect back to the login page with an error.
return redirectToLoginPage(r, w, issuerURL, encodedState, loginurl.ShowBadUserPassErr)
default:
// Some other error happened, e.g. a configured identity transformation failed.
// Some other error happened.
oidc.WriteAuthorizeError(r, w, oauthHelper, authorizeRequester, err, false)
return nil
}
}

session := downstreamsession.MakeDownstreamSession(
identity,
authorizeRequester.GetGrantedScopes(),
authorizeRequester.GetClient().GetID(),
)
username, groups, err := downstreamsession.ApplyIdentityTransformations(r.Context(),
idp.GetTransforms(), identity.UpstreamUsername, identity.UpstreamGroups)
if err != nil {
err = fosite.ErrAccessDenied.WithHintf("Reason: %s.", err.Error())
oidc.WriteAuthorizeError(r, w, oauthHelper, authorizeRequester, err, false)
return nil
}

session := downstreamsession.NewPinnipedSession(idp, &downstreamsession.SessionConfig{
UpstreamIdentity: identity,
UpstreamLoginExtras: loginExtras,
Username: username,
Groups: groups,
ClientID: authorizeRequester.GetClient().GetID(),
GrantedScopes: authorizeRequester.GetGrantedScopes(),
})

oidc.PerformAuthcodeRedirect(r, w, oauthHelper, authorizeRequester, session, false)

Expand Down
Loading

0 comments on commit 035e48c

Please sign in to comment.