e2bdab9e2d
To make the subject of the downstream ID token more unique when there are multiple IDPs. It is possible to define two IDPs in a FederationDomain using the same identity provider CR, in which case the only thing that would make the subject claim different is adding the IDP display name into the values of the subject claim.
135 lines
5.2 KiB
Go
135 lines
5.2 KiB
Go
// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
// Package callback provides a handler for the OIDC callback endpoint.
|
|
package callback
|
|
|
|
import (
|
|
"net/http"
|
|
"net/url"
|
|
|
|
"github.com/ory/fosite"
|
|
|
|
"go.pinniped.dev/internal/federationdomain/downstreamsession"
|
|
"go.pinniped.dev/internal/federationdomain/federationdomainproviders"
|
|
"go.pinniped.dev/internal/federationdomain/formposthtml"
|
|
"go.pinniped.dev/internal/federationdomain/oidc"
|
|
"go.pinniped.dev/internal/httputil/httperr"
|
|
"go.pinniped.dev/internal/httputil/securityheader"
|
|
"go.pinniped.dev/internal/plog"
|
|
)
|
|
|
|
func NewHandler(
|
|
upstreamIDPs federationdomainproviders.FederationDomainIdentityProvidersFinderI,
|
|
oauthHelper fosite.OAuth2Provider,
|
|
stateDecoder, cookieDecoder oidc.Decoder,
|
|
redirectURI string,
|
|
) http.Handler {
|
|
handler := httperr.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
|
state, err := validateRequest(r, stateDecoder, cookieDecoder)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
resolvedOIDCIdentityProvider, _, err := upstreamIDPs.FindUpstreamIDPByDisplayName(state.UpstreamName)
|
|
if err != nil || resolvedOIDCIdentityProvider == nil {
|
|
plog.Warning("upstream provider not found")
|
|
return httperr.New(http.StatusUnprocessableEntity, "upstream provider not found")
|
|
}
|
|
upstreamIDPConfig := resolvedOIDCIdentityProvider.Provider
|
|
|
|
downstreamAuthParams, err := url.ParseQuery(state.AuthParams)
|
|
if err != nil {
|
|
plog.Error("error reading state downstream auth params", err)
|
|
return httperr.New(http.StatusBadRequest, "error reading state downstream auth params")
|
|
}
|
|
|
|
// Recreate enough of the original authorize request so we can pass it to NewAuthorizeRequest().
|
|
reconstitutedAuthRequest := &http.Request{Form: downstreamAuthParams}
|
|
authorizeRequester, err := oauthHelper.NewAuthorizeRequest(r.Context(), reconstitutedAuthRequest)
|
|
if err != nil {
|
|
plog.Error("error using state downstream auth params", err,
|
|
"fositeErr", oidc.FositeErrorForLog(err))
|
|
return httperr.New(http.StatusBadRequest, "error using state downstream auth params")
|
|
}
|
|
|
|
// Automatically grant certain scopes, but only if they were requested.
|
|
// This is instead of asking the user to approve these scopes. Note that `NewAuthorizeRequest` would have returned
|
|
// 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)
|
|
|
|
token, err := upstreamIDPConfig.ExchangeAuthcodeAndValidateTokens(
|
|
r.Context(),
|
|
authcode(r),
|
|
state.PKCECode,
|
|
state.Nonce,
|
|
redirectURI,
|
|
)
|
|
if err != nil {
|
|
plog.WarningErr("error exchanging and validating upstream tokens", err, "upstreamName", upstreamIDPConfig.GetName())
|
|
return httperr.New(http.StatusBadGateway, "error exchanging and validating upstream tokens")
|
|
}
|
|
|
|
subject, upstreamUsername, upstreamGroups, err := downstreamsession.GetDownstreamIdentityFromUpstreamIDToken(
|
|
upstreamIDPConfig, token.IDToken.Claims, resolvedOIDCIdentityProvider.DisplayName,
|
|
)
|
|
if err != nil {
|
|
return httperr.Wrap(http.StatusUnprocessableEntity, err.Error(), err)
|
|
}
|
|
|
|
username, groups, err := downstreamsession.ApplyIdentityTransformations(
|
|
r.Context(), resolvedOIDCIdentityProvider.Transforms, upstreamUsername, upstreamGroups,
|
|
)
|
|
if err != nil {
|
|
return httperr.Wrap(http.StatusUnprocessableEntity, err.Error(), err)
|
|
}
|
|
|
|
additionalClaims := downstreamsession.MapAdditionalClaimsFromUpstreamIDToken(upstreamIDPConfig, token.IDToken.Claims)
|
|
|
|
customSessionData, err := downstreamsession.MakeDownstreamOIDCCustomSessionData(
|
|
upstreamIDPConfig, token, username, upstreamUsername, upstreamGroups,
|
|
)
|
|
if err != nil {
|
|
return httperr.Wrap(http.StatusUnprocessableEntity, err.Error(), err)
|
|
}
|
|
|
|
openIDSession := downstreamsession.MakeDownstreamSession(subject, username, groups,
|
|
authorizeRequester.GetGrantedScopes(), authorizeRequester.GetClient().GetID(), customSessionData, additionalClaims)
|
|
|
|
authorizeResponder, err := oauthHelper.NewAuthorizeResponse(r.Context(), authorizeRequester, openIDSession)
|
|
if err != nil {
|
|
plog.WarningErr("error while generating and saving authcode", err,
|
|
"upstreamName", upstreamIDPConfig.GetName(), "fositeErr", oidc.FositeErrorForLog(err))
|
|
return httperr.Wrap(http.StatusInternalServerError, "error while generating and saving authcode", err)
|
|
}
|
|
|
|
oauthHelper.WriteAuthorizeResponse(r.Context(), w, authorizeRequester, authorizeResponder)
|
|
|
|
return nil
|
|
})
|
|
return securityheader.WrapWithCustomCSP(handler, formposthtml.ContentSecurityPolicy())
|
|
}
|
|
|
|
func authcode(r *http.Request) string {
|
|
return r.FormValue("code")
|
|
}
|
|
|
|
func validateRequest(r *http.Request, stateDecoder, cookieDecoder oidc.Decoder) (*oidc.UpstreamStateParamData, error) {
|
|
if r.Method != http.MethodGet {
|
|
return nil, httperr.Newf(http.StatusMethodNotAllowed, "%s (try GET)", r.Method)
|
|
}
|
|
|
|
_, decodedState, err := oidc.ReadStateParamAndValidateCSRFCookie(r, cookieDecoder, stateDecoder)
|
|
if err != nil {
|
|
plog.InfoErr("state or CSRF error", err)
|
|
return nil, err
|
|
}
|
|
|
|
if authcode(r) == "" {
|
|
plog.Info("code param not found")
|
|
return nil, httperr.New(http.StatusBadRequest, "code param not found")
|
|
}
|
|
|
|
return decodedState, nil
|
|
}
|