mirror of
https://github.com/vmware-tanzu/pinniped.git
synced 2026-09-26 18:04:24 +00:00
Implement upstream LDAP support in auth_handler.go
- When the upstream IDP is an LDAP IDP and the user's LDAP username and password are received as new custom headers, then authenticate the user and, if authentication was successful, return a redirect with an authcode. Handle errors according to the OAuth/OIDC specs. - Still does not support having multiple upstream IDPs defined at the same time, which was an existing limitation of this endpoint. - Does not yet include the actual LDAP authentication, which is hidden behind an interface from the point of view of auth_handler.go - Move the oidctestutil package to the testutil directory. - Add an interface for Fosite storage to avoid a cyclical test dependency. - Add GetURL() to the UpstreamLDAPIdentityProviderI interface. - Extract test helpers to be shared between callback_handler_test.go and auth_handler_test.go because the authcode and fosite storage assertions should be identical. - Backfill Content-Type assertions in callback_handler_test.go. Signed-off-by: Andrew Keesler <akeesler@vmware.com>
This commit is contained in:
committed by
Andrew Keesler
parent
064e3144a2
commit
f6ded84f07
+212
-105
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/ory/fosite"
|
||||
"github.com/ory/fosite/handler/openid"
|
||||
"github.com/ory/fosite/token/jwt"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"go.pinniped.dev/internal/httputil/httperr"
|
||||
@@ -25,11 +26,16 @@ import (
|
||||
"go.pinniped.dev/pkg/oidcclient/pkce"
|
||||
)
|
||||
|
||||
const (
|
||||
CustomUsernameHeaderName = "X-Pinniped-Upstream-Username"
|
||||
CustomPasswordHeaderName = "X-Pinniped-Upstream-Password" //nolint:gosec // this is not a credential
|
||||
)
|
||||
|
||||
func NewHandler(
|
||||
downstreamIssuer string,
|
||||
idpLister oidc.UpstreamIdentityProvidersLister,
|
||||
oauthHelperWithNullStorage fosite.OAuth2Provider,
|
||||
oauthHelperWithRealStorage fosite.OAuth2Provider,
|
||||
oauthHelperWithoutStorage fosite.OAuth2Provider,
|
||||
oauthHelperWithStorage fosite.OAuth2Provider,
|
||||
generateCSRF func() (csrftoken.CSRFToken, error),
|
||||
generatePKCE func() (pkce.Code, error),
|
||||
generateNonce func() (nonce.Nonce, error),
|
||||
@@ -44,109 +50,203 @@ func NewHandler(
|
||||
return httperr.Newf(http.StatusMethodNotAllowed, "%s (try GET or POST)", r.Method)
|
||||
}
|
||||
|
||||
csrfFromCookie := readCSRFCookie(r, cookieCodec)
|
||||
|
||||
authorizeRequester, err := oauthHelperWithNullStorage.NewAuthorizeRequest(r.Context(), r)
|
||||
if err != nil {
|
||||
plog.Info("authorize request error", oidc.FositeErrorForLog(err)...)
|
||||
oauthHelperWithNullStorage.WriteAuthorizeError(w, authorizeRequester, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
upstreamIDP, err := chooseUpstreamIDP(idpLister)
|
||||
oidcUpstream, ldapUpstream, err := chooseUpstreamIDP(idpLister)
|
||||
if err != nil {
|
||||
plog.WarningErr("authorize upstream config", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Grant the openid scope (for now) if they asked for it so that `NewAuthorizeResponse` will perform its OIDC validations.
|
||||
oidc.GrantScopeIfRequested(authorizeRequester, coreosoidc.ScopeOpenID)
|
||||
// There don't seem to be any validations inside `NewAuthorizeResponse` related to the offline_access scope
|
||||
// at this time, however we will temporarily grant the scope just in case that changes in a future release of fosite.
|
||||
oidc.GrantScopeIfRequested(authorizeRequester, coreosoidc.ScopeOfflineAccess)
|
||||
|
||||
// Grant the pinniped:request-audience scope if requested.
|
||||
oidc.GrantScopeIfRequested(authorizeRequester, "pinniped:request-audience")
|
||||
|
||||
now := time.Now()
|
||||
_, err = oauthHelperWithNullStorage.NewAuthorizeResponse(r.Context(), authorizeRequester, &openid.DefaultSession{
|
||||
Claims: &jwt.IDTokenClaims{
|
||||
// Temporary claim values to allow `NewAuthorizeResponse` to perform other OIDC validations.
|
||||
Subject: "none",
|
||||
AuthTime: now,
|
||||
RequestedAt: now,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
plog.Info("authorize response error", oidc.FositeErrorForLog(err)...)
|
||||
oauthHelperWithNullStorage.WriteAuthorizeError(w, authorizeRequester, err)
|
||||
return nil
|
||||
if oidcUpstream != nil {
|
||||
return handleAuthRequestForOIDCUpstream(r, w,
|
||||
oauthHelperWithoutStorage,
|
||||
generateCSRF, generateNonce, generatePKCE,
|
||||
oidcUpstream,
|
||||
downstreamIssuer,
|
||||
upstreamStateEncoder,
|
||||
cookieCodec,
|
||||
)
|
||||
}
|
||||
|
||||
csrfValue, nonceValue, pkceValue, err := generateValues(generateCSRF, generateNonce, generatePKCE)
|
||||
if err != nil {
|
||||
plog.Error("authorize generate error", err)
|
||||
return err
|
||||
}
|
||||
if csrfFromCookie != "" {
|
||||
csrfValue = csrfFromCookie
|
||||
}
|
||||
|
||||
upstreamOAuthConfig := oauth2.Config{
|
||||
ClientID: upstreamIDP.GetClientID(),
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: upstreamIDP.GetAuthorizationURL().String(),
|
||||
},
|
||||
RedirectURL: fmt.Sprintf("%s/callback", downstreamIssuer),
|
||||
Scopes: upstreamIDP.GetScopes(),
|
||||
}
|
||||
|
||||
encodedStateParamValue, err := upstreamStateParam(
|
||||
authorizeRequester,
|
||||
upstreamIDP.GetName(),
|
||||
nonceValue,
|
||||
csrfValue,
|
||||
pkceValue,
|
||||
upstreamStateEncoder,
|
||||
return handleAuthRequestForLDAPUpstream(r, w,
|
||||
oauthHelperWithStorage,
|
||||
ldapUpstream,
|
||||
)
|
||||
if err != nil {
|
||||
plog.Error("authorize upstream state param error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if csrfFromCookie == "" {
|
||||
// We did not receive an incoming CSRF cookie, so write a new one.
|
||||
err := addCSRFSetCookieHeader(w, csrfValue, cookieCodec)
|
||||
if err != nil {
|
||||
plog.Error("error setting CSRF cookie", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
authCodeOptions := []oauth2.AuthCodeOption{
|
||||
oauth2.AccessTypeOffline,
|
||||
nonceValue.Param(),
|
||||
pkceValue.Challenge(),
|
||||
pkceValue.Method(),
|
||||
}
|
||||
|
||||
promptParam := r.Form.Get("prompt")
|
||||
if promptParam != "" && oidc.ScopeWasRequested(authorizeRequester, coreosoidc.ScopeOpenID) {
|
||||
authCodeOptions = append(authCodeOptions, oauth2.SetAuthURLParam("prompt", promptParam))
|
||||
}
|
||||
|
||||
http.Redirect(w, r,
|
||||
upstreamOAuthConfig.AuthCodeURL(
|
||||
encodedStateParamValue,
|
||||
authCodeOptions...,
|
||||
),
|
||||
302,
|
||||
)
|
||||
|
||||
return nil
|
||||
}))
|
||||
}
|
||||
|
||||
func handleAuthRequestForLDAPUpstream(
|
||||
r *http.Request,
|
||||
w http.ResponseWriter,
|
||||
oauthHelper fosite.OAuth2Provider,
|
||||
ldapUpstream provider.UpstreamLDAPIdentityProviderI,
|
||||
) error {
|
||||
authorizeRequester, created := newAuthorizeRequest(r, w, oauthHelper)
|
||||
if !created {
|
||||
return nil
|
||||
}
|
||||
|
||||
username := r.Header.Get(CustomUsernameHeaderName)
|
||||
password := r.Header.Get(CustomPasswordHeaderName)
|
||||
if username == "" || password == "" {
|
||||
// Return an error according to OIDC spec 3.1.2.6 (second paragraph).
|
||||
err := errors.WithStack(fosite.ErrAccessDenied.WithHintf("Missing or blank username or password."))
|
||||
oauthHelper.WriteAuthorizeError(w, authorizeRequester, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
authenticateResponse, authenticated, err := ldapUpstream.AuthenticateUser(r.Context(), username, password)
|
||||
if err != nil {
|
||||
plog.WarningErr("unexpected error during upstream authentication", err, "upstreamName", ldapUpstream.GetName())
|
||||
return httperr.New(http.StatusBadGateway, "unexpected error during upstream authentication")
|
||||
}
|
||||
if !authenticated {
|
||||
// Return an error according to OIDC spec 3.1.2.6 (second paragraph).
|
||||
err = errors.WithStack(fosite.ErrAccessDenied.WithHintf("Username/password not accepted by LDAP provider."))
|
||||
oauthHelper.WriteAuthorizeError(w, authorizeRequester, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
subject := fmt.Sprintf("%s?%s=%s", ldapUpstream.GetURL(), oidc.IDTokenSubjectClaim, authenticateResponse.User.GetUID())
|
||||
now := time.Now().UTC()
|
||||
openIDSession := &openid.DefaultSession{
|
||||
Claims: &jwt.IDTokenClaims{
|
||||
Subject: subject,
|
||||
RequestedAt: now,
|
||||
AuthTime: now,
|
||||
},
|
||||
}
|
||||
openIDSession.Claims.Extra = map[string]interface{}{
|
||||
oidc.DownstreamUsernameClaim: authenticateResponse.User.GetName(),
|
||||
oidc.DownstreamGroupsClaim: authenticateResponse.User.GetGroups(),
|
||||
}
|
||||
|
||||
authorizeResponder, err := oauthHelper.NewAuthorizeResponse(r.Context(), authorizeRequester, openIDSession)
|
||||
if err != nil {
|
||||
plog.Info("authorize response error", oidc.FositeErrorForLog(err)...)
|
||||
oauthHelper.WriteAuthorizeError(w, authorizeRequester, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
oauthHelper.WriteAuthorizeResponse(w, authorizeRequester, authorizeResponder)
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleAuthRequestForOIDCUpstream(
|
||||
r *http.Request,
|
||||
w http.ResponseWriter,
|
||||
oauthHelper fosite.OAuth2Provider,
|
||||
generateCSRF func() (csrftoken.CSRFToken, error),
|
||||
generateNonce func() (nonce.Nonce, error),
|
||||
generatePKCE func() (pkce.Code, error),
|
||||
oidcUpstream provider.UpstreamOIDCIdentityProviderI,
|
||||
downstreamIssuer string,
|
||||
upstreamStateEncoder oidc.Encoder,
|
||||
cookieCodec oidc.Codec,
|
||||
) error {
|
||||
authorizeRequester, created := newAuthorizeRequest(r, w, oauthHelper)
|
||||
if !created {
|
||||
return nil
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
_, err := oauthHelper.NewAuthorizeResponse(r.Context(), authorizeRequester, &openid.DefaultSession{
|
||||
Claims: &jwt.IDTokenClaims{
|
||||
// Temporary claim values to allow `NewAuthorizeResponse` to perform other OIDC validations.
|
||||
Subject: "none",
|
||||
AuthTime: now,
|
||||
RequestedAt: now,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
plog.Info("authorize response error", oidc.FositeErrorForLog(err)...)
|
||||
oauthHelper.WriteAuthorizeError(w, authorizeRequester, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
csrfValue, nonceValue, pkceValue, err := generateValues(generateCSRF, generateNonce, generatePKCE)
|
||||
if err != nil {
|
||||
plog.Error("authorize generate error", err)
|
||||
return err
|
||||
}
|
||||
csrfFromCookie := readCSRFCookie(r, cookieCodec)
|
||||
if csrfFromCookie != "" {
|
||||
csrfValue = csrfFromCookie
|
||||
}
|
||||
|
||||
upstreamOAuthConfig := oauth2.Config{
|
||||
ClientID: oidcUpstream.GetClientID(),
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: oidcUpstream.GetAuthorizationURL().String(),
|
||||
},
|
||||
RedirectURL: fmt.Sprintf("%s/callback", downstreamIssuer),
|
||||
Scopes: oidcUpstream.GetScopes(),
|
||||
}
|
||||
|
||||
encodedStateParamValue, err := upstreamStateParam(
|
||||
authorizeRequester,
|
||||
oidcUpstream.GetName(),
|
||||
nonceValue,
|
||||
csrfValue,
|
||||
pkceValue,
|
||||
upstreamStateEncoder,
|
||||
)
|
||||
if err != nil {
|
||||
plog.Error("authorize upstream state param error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if csrfFromCookie == "" {
|
||||
// We did not receive an incoming CSRF cookie, so write a new one.
|
||||
err := addCSRFSetCookieHeader(w, csrfValue, cookieCodec)
|
||||
if err != nil {
|
||||
plog.Error("error setting CSRF cookie", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
authCodeOptions := []oauth2.AuthCodeOption{
|
||||
oauth2.AccessTypeOffline,
|
||||
nonceValue.Param(),
|
||||
pkceValue.Challenge(),
|
||||
pkceValue.Method(),
|
||||
}
|
||||
|
||||
promptParam := r.Form.Get("prompt")
|
||||
if promptParam != "" && oidc.ScopeWasRequested(authorizeRequester, coreosoidc.ScopeOpenID) {
|
||||
authCodeOptions = append(authCodeOptions, oauth2.SetAuthURLParam("prompt", promptParam))
|
||||
}
|
||||
|
||||
http.Redirect(w, r,
|
||||
upstreamOAuthConfig.AuthCodeURL(
|
||||
encodedStateParamValue,
|
||||
authCodeOptions...,
|
||||
),
|
||||
302,
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func newAuthorizeRequest(r *http.Request, w http.ResponseWriter, oauthHelper fosite.OAuth2Provider) (fosite.AuthorizeRequester, bool) {
|
||||
authorizeRequester, err := oauthHelper.NewAuthorizeRequest(r.Context(), r)
|
||||
if err != nil {
|
||||
plog.Info("authorize request error", oidc.FositeErrorForLog(err)...)
|
||||
oauthHelper.WriteAuthorizeError(w, authorizeRequester, err)
|
||||
return nil, false
|
||||
}
|
||||
grantScopes(authorizeRequester)
|
||||
return authorizeRequester, true
|
||||
}
|
||||
|
||||
func grantScopes(authorizeRequester fosite.AuthorizeRequester) {
|
||||
// Grant the openid scope (for now) if they asked for it so that `NewAuthorizeResponse` will perform its OIDC validations.
|
||||
oidc.GrantScopeIfRequested(authorizeRequester, coreosoidc.ScopeOpenID)
|
||||
// There don't seem to be any validations inside `NewAuthorizeResponse` related to the offline_access scope
|
||||
// at this time, however we will temporarily grant the scope just in case that changes in a future release of fosite.
|
||||
oidc.GrantScopeIfRequested(authorizeRequester, coreosoidc.ScopeOfflineAccess)
|
||||
// Grant the pinniped:request-audience scope if requested.
|
||||
oidc.GrantScopeIfRequested(authorizeRequester, "pinniped:request-audience")
|
||||
}
|
||||
|
||||
func readCSRFCookie(r *http.Request, codec oidc.Decoder) csrftoken.CSRFToken {
|
||||
receivedCSRFCookie, err := r.Cookie(oidc.CSRFCookieName)
|
||||
if err != nil {
|
||||
@@ -166,27 +266,34 @@ func readCSRFCookie(r *http.Request, codec oidc.Decoder) csrftoken.CSRFToken {
|
||||
return csrfFromCookie
|
||||
}
|
||||
|
||||
func chooseUpstreamIDP(idpLister oidc.UpstreamOIDCIdentityProvidersLister) (provider.UpstreamOIDCIdentityProviderI, error) {
|
||||
allUpstreamIDPs := idpLister.GetOIDCIdentityProviders()
|
||||
if len(allUpstreamIDPs) == 0 {
|
||||
return nil, httperr.New(
|
||||
// Select either an OIDC or an LDAP IDP, or return an error.
|
||||
func chooseUpstreamIDP(idpLister oidc.UpstreamIdentityProvidersLister) (provider.UpstreamOIDCIdentityProviderI, provider.UpstreamLDAPIdentityProviderI, error) {
|
||||
oidcUpstreams := idpLister.GetOIDCIdentityProviders()
|
||||
ldapUpstreams := idpLister.GetLDAPIdentityProviders()
|
||||
switch {
|
||||
case len(oidcUpstreams)+len(ldapUpstreams) == 0:
|
||||
return nil, nil, httperr.New(
|
||||
http.StatusUnprocessableEntity,
|
||||
"No upstream providers are configured",
|
||||
)
|
||||
} else if len(allUpstreamIDPs) > 1 {
|
||||
case len(oidcUpstreams)+len(ldapUpstreams) > 1:
|
||||
var upstreamIDPNames []string
|
||||
for _, idp := range allUpstreamIDPs {
|
||||
for _, idp := range oidcUpstreams {
|
||||
upstreamIDPNames = append(upstreamIDPNames, idp.GetName())
|
||||
}
|
||||
for _, idp := range ldapUpstreams {
|
||||
upstreamIDPNames = append(upstreamIDPNames, idp.GetName())
|
||||
}
|
||||
|
||||
plog.Warning("Too many upstream providers are configured (found: %s)", upstreamIDPNames)
|
||||
|
||||
return nil, httperr.New(
|
||||
return nil, nil, httperr.New(
|
||||
http.StatusUnprocessableEntity,
|
||||
"Too many upstream providers are configured (support for multiple upstreams is not yet implemented)",
|
||||
)
|
||||
case len(oidcUpstreams) == 1:
|
||||
return oidcUpstreams[0], nil, nil
|
||||
default:
|
||||
return nil, ldapUpstreams[0], nil
|
||||
}
|
||||
return allUpstreamIDPs[0], nil
|
||||
}
|
||||
|
||||
func generateValues(
|
||||
|
||||
Reference in New Issue
Block a user