22fbced863
- For backwards compatibility with older Pinniped CLIs, the pinniped-cli client does not need to request the username or groups scopes for them to be granted. For dynamic clients, the usual OAuth2 rules apply: the client must be allowed to request the scopes according to its configuration, and the client must actually request the scopes in the authorization request. - If the username scope was not granted, then there will be no username in the ID token, and the cluster-scoped token exchange will fail since there would be no username in the resulting cluster-scoped ID token. - The OIDC well-known discovery endpoint lists the username and groups scopes in the scopes_supported list, and lists the username and groups claims in the claims_supported list. - Add username and groups scopes to the default list of scopes put into kubeconfig files by "pinniped get kubeconfig" CLI command, and the default list of scopes used by "pinniped login oidc" when no list of scopes is specified in the kubeconfig file - The warning header about group memberships changing during upstream refresh will only be sent to the pinniped-cli client, since it is only intended for kubectl and it could leak the username to the client (which may not have the username scope granted) through the warning message text. - Add the user's username to the session storage as a new field, so that during upstream refresh we can compare the original username from the initial authorization to the refreshed username, even in the case when the username scope was not granted (and therefore the username is not stored in the ID token claims of the session storage) - Bump the Supervisor session storage format version from 2 to 3 due to the username field being added to the session struct - Extract commonly used string constants related to OIDC flows to api package. - Change some import names to make them consistent: - Always import github.com/coreos/go-oidc/v3/oidc as "coreosoidc" - Always import go.pinniped.dev/generated/latest/apis/supervisor/oidc as "oidcapi" - Always import go.pinniped.dev/internal/oidc as "oidc"
92 lines
5.0 KiB
Go
92 lines
5.0 KiB
Go
// Copyright 2022 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/httputil/httperr"
|
|
"go.pinniped.dev/internal/oidc"
|
|
"go.pinniped.dev/internal/oidc/downstreamsession"
|
|
"go.pinniped.dev/internal/plog"
|
|
)
|
|
|
|
func NewPostHandler(issuerURL string, upstreamIDPs oidc.UpstreamIdentityProvidersLister, 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, idpType, err := oidc.FindUpstreamIDPByNameAndType(upstreamIDPs, decodedState.UpstreamName, decodedState.UpstreamType)
|
|
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.
|
|
username := r.PostFormValue(usernameParamName)
|
|
password := r.PostFormValue(passwordParamName)
|
|
|
|
// Treat blank username or password as a bad username/password combination, as opposed to an internal error.
|
|
if username == "" || password == "" {
|
|
// 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.AuthenticateUser(r.Context(), username, password, authorizeRequester.GetGrantedScopes())
|
|
if err != nil {
|
|
plog.WarningErr("unexpected error during upstream LDAP authentication", err, "upstreamName", ldapUpstream.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, authenticateResponse)
|
|
username = authenticateResponse.User.GetName()
|
|
groups := authenticateResponse.User.GetGroups()
|
|
customSessionData := downstreamsession.MakeDownstreamLDAPOrADCustomSessionData(ldapUpstream, idpType, authenticateResponse, username)
|
|
openIDSession := downstreamsession.MakeDownstreamSession(subject, username, groups, authorizeRequester.GetGrantedScopes(), customSessionData)
|
|
oidc.PerformAuthcodeRedirect(r, w, oauthHelper, authorizeRequester, openIDSession, false)
|
|
|
|
return nil
|
|
}
|
|
}
|