Files
pinniped/internal/federationdomain/endpoints/callback/callback_handler.go
Ryan Richard 1bc13e94f7 Refactor to extract interface for upstream IDP interactions
Create an interface to abstract the upstream IDP from the
authorize, IDP discovery, callback, choose IDP, and login
endpoints. This commit does not refactor the token endpoint,
which will be refactored in a similar way in the next commit.
2024-02-20 09:26:34 -08:00

107 lines
3.9 KiB
Go

// Copyright 2020-2024 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
}
oidcIdentityProvider, err := upstreamIDPs.FindUpstreamIDPByDisplayName(state.UpstreamName)
if err != nil || oidcIdentityProvider == nil {
plog.Warning("upstream provider not found")
return httperr.New(http.StatusUnprocessableEntity, "upstream provider not found")
}
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)
identity, err := oidcIdentityProvider.HandleCallback(r.Context(), authcode(r), state.PKCECode, state.Nonce, redirectURI)
if err != nil {
return err
}
session := downstreamsession.MakeDownstreamSession(
identity,
authorizeRequester.GetGrantedScopes(),
authorizeRequester.GetClient().GetID(),
)
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))
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
}