ContainerImage.Pinniped/internal/federationdomain/endpoints/login/post_login_handler.go
Ryan Richard 86c791b8a6 reorganize federation domain packages to be more intuitive
Co-authored-by: Benjamin A. Petersen <ben@benjaminapetersen.me>
2023-09-11 11:11:52 -07:00

103 lines
5.7 KiB
Go

// Copyright 2022-2023 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package login
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/oidc"
"go.pinniped.dev/internal/httputil/httperr"
"go.pinniped.dev/internal/plog"
)
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)
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.
plog.Error("error finding upstream provider", err)
return httperr.Wrap(http.StatusUnprocessableEntity, "error finding upstream provider", err)
}
// Get the original params that were used at the authorization endpoint.
downstreamAuthParams, err := url.ParseQuery(decodedState.AuthParams)
if err != nil {
// This shouldn't really happen because the authorization endpoint encoded these query params correctly.
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 {
// This shouldn't really happen because the authorization endpoint has already validated these params
// by calling NewAuthorizeRequest() itself.
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)
// Get the username and password form params from the POST body.
submittedUsername := r.PostFormValue(usernameParamName)
submittedPassword := r.PostFormValue(passwordParamName)
// Treat blank username or password as a bad username/password combination, as opposed to an internal error.
if submittedUsername == "" || submittedPassword == "" {
// User forgot to enter one of the required fields.
// 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, ShowBadUserPassErr)
}
// Attempt to authenticate the user with the upstream IDP.
authenticateResponse, authenticated, err := ldapUpstream.Provider.AuthenticateUser(r.Context(), submittedUsername, submittedPassword, authorizeRequester.GetGrantedScopes())
if err != nil {
plog.WarningErr("unexpected error during upstream LDAP authentication", err, "upstreamName", ldapUpstream.Provider.GetName())
// There was some problem during authentication with the upstream, aside from bad username/password.
// 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, ShowInternalError)
}
if !authenticated {
// The upstream did not accept the username/password combination.
// 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, ShowBadUserPassErr)
}
// We had previously interrupted the regular steps of the OIDC authcode flow to show the login page UI.
// Now the upstream IDP has authenticated the user, so now we're back into the regular OIDC authcode flow steps.
// Both success and error responses from this point onwards should look like the usual fosite redirect
// responses, and a happy redirect response will include a downstream authcode.
subject := downstreamsession.DownstreamSubjectFromUpstreamLDAP(ldapUpstream.Provider, authenticateResponse)
upstreamUsername := authenticateResponse.User.GetName()
upstreamGroups := authenticateResponse.User.GetGroups()
username, groups, err := downstreamsession.ApplyIdentityTransformations(r.Context(), ldapUpstream.Transforms, upstreamUsername, upstreamGroups)
if err != nil {
oidc.WriteAuthorizeError(r, w, oauthHelper, authorizeRequester,
fosite.ErrAccessDenied.WithHintf("Reason: %s.", err.Error()), false,
)
return nil
}
customSessionData := downstreamsession.MakeDownstreamLDAPOrADCustomSessionData(ldapUpstream.Provider, ldapUpstream.SessionProviderType, authenticateResponse, username, upstreamUsername, upstreamGroups)
openIDSession := downstreamsession.MakeDownstreamSession(subject, username, groups,
authorizeRequester.GetGrantedScopes(), authorizeRequester.GetClient().GetID(), customSessionData, map[string]interface{}{})
oidc.PerformAuthcodeRedirect(r, w, oauthHelper, authorizeRequester, openIDSession, false)
return nil
}
}