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.
110 lines
5.7 KiB
Go
110 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, ldapUpstream.DisplayName,
|
|
)
|
|
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
|
|
}
|
|
}
|