2023-01-17 23:54:16 +00:00
|
|
|
// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
|
2020-11-04 00:17:38 +00:00
|
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
|
|
|
// Package auth provides a handler for the OIDC authorization endpoint.
|
|
|
|
package auth
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"net/http"
|
2022-05-06 19:00:46 +00:00
|
|
|
"net/url"
|
2020-11-06 22:44:58 +00:00
|
|
|
"time"
|
2020-11-04 20:19:07 +00:00
|
|
|
|
2020-11-04 23:04:50 +00:00
|
|
|
"github.com/ory/fosite"
|
2020-11-06 22:44:58 +00:00
|
|
|
"github.com/ory/fosite/handler/openid"
|
|
|
|
"github.com/ory/fosite/token/jwt"
|
2020-11-11 01:58:00 +00:00
|
|
|
"golang.org/x/oauth2"
|
|
|
|
|
Create username scope, required for clients to get username in ID token
- 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"
2022-08-08 23:29:22 +00:00
|
|
|
oidcapi "go.pinniped.dev/generated/latest/apis/supervisor/oidc"
|
2020-11-04 00:17:38 +00:00
|
|
|
"go.pinniped.dev/internal/httputil/httperr"
|
2020-12-14 23:28:32 +00:00
|
|
|
"go.pinniped.dev/internal/httputil/securityheader"
|
2020-11-13 23:59:51 +00:00
|
|
|
"go.pinniped.dev/internal/oidc"
|
2020-11-11 01:58:00 +00:00
|
|
|
"go.pinniped.dev/internal/oidc/csrftoken"
|
2021-06-30 22:02:14 +00:00
|
|
|
"go.pinniped.dev/internal/oidc/downstreamsession"
|
2022-04-29 23:01:51 +00:00
|
|
|
"go.pinniped.dev/internal/oidc/login"
|
2020-11-04 00:17:38 +00:00
|
|
|
"go.pinniped.dev/internal/oidc/provider"
|
2022-06-02 16:23:34 +00:00
|
|
|
"go.pinniped.dev/internal/oidc/provider/formposthtml"
|
2020-11-11 01:58:00 +00:00
|
|
|
"go.pinniped.dev/internal/plog"
|
2021-10-06 22:28:13 +00:00
|
|
|
"go.pinniped.dev/internal/psession"
|
2020-11-17 18:46:54 +00:00
|
|
|
"go.pinniped.dev/pkg/oidcclient/nonce"
|
|
|
|
"go.pinniped.dev/pkg/oidcclient/pkce"
|
2020-11-11 01:58:00 +00:00
|
|
|
)
|
|
|
|
|
2021-10-08 22:48:21 +00:00
|
|
|
const (
|
|
|
|
promptParamName = "prompt"
|
|
|
|
promptParamNone = "none"
|
|
|
|
)
|
2021-10-06 23:30:30 +00:00
|
|
|
|
2020-11-04 00:17:38 +00:00
|
|
|
func NewHandler(
|
2020-11-19 16:35:23 +00:00
|
|
|
downstreamIssuer string,
|
2021-04-07 23:12:13 +00:00
|
|
|
idpLister oidc.UpstreamIdentityProvidersLister,
|
2021-04-09 00:28:01 +00:00
|
|
|
oauthHelperWithoutStorage fosite.OAuth2Provider,
|
|
|
|
oauthHelperWithStorage fosite.OAuth2Provider,
|
2020-11-11 01:58:00 +00:00
|
|
|
generateCSRF func() (csrftoken.CSRFToken, error),
|
2020-11-04 00:17:38 +00:00
|
|
|
generatePKCE func() (pkce.Code, error),
|
|
|
|
generateNonce func() (nonce.Nonce, error),
|
2020-11-16 19:41:00 +00:00
|
|
|
upstreamStateEncoder oidc.Encoder,
|
|
|
|
cookieCodec oidc.Codec,
|
2020-11-04 00:17:38 +00:00
|
|
|
) http.Handler {
|
2022-06-02 16:23:34 +00:00
|
|
|
handler := httperr.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
2020-11-04 00:17:38 +00:00
|
|
|
if r.Method != http.MethodPost && r.Method != http.MethodGet {
|
|
|
|
// https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
|
|
|
|
// Authorization Servers MUST support the use of the HTTP GET and POST methods defined in
|
|
|
|
// RFC 2616 [RFC2616] at the Authorization Endpoint.
|
|
|
|
return httperr.Newf(http.StatusMethodNotAllowed, "%s (try GET or POST)", r.Method)
|
|
|
|
}
|
|
|
|
|
Create username scope, required for clients to get username in ID token
- 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"
2022-08-08 23:29:22 +00:00
|
|
|
// Note that the client might have used oidcapi.AuthorizeUpstreamIDPNameParamName and
|
|
|
|
// oidcapi.AuthorizeUpstreamIDPTypeParamName query params to request a certain upstream IDP.
|
2022-05-06 19:00:46 +00:00
|
|
|
// The Pinniped CLI has been sending these params since v0.9.0.
|
|
|
|
// Currently, these are ignored because the Supervisor does not yet support logins when multiple IDPs
|
|
|
|
// are configured. However, these params should be honored in the future when choosing an upstream
|
Create username scope, required for clients to get username in ID token
- 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"
2022-08-08 23:29:22 +00:00
|
|
|
// here, e.g. by calling oidcapi.FindUpstreamIDPByNameAndType() when the params are present.
|
2021-10-08 22:48:21 +00:00
|
|
|
oidcUpstream, ldapUpstream, idpType, err := chooseUpstreamIDP(idpLister)
|
2020-11-04 20:19:07 +00:00
|
|
|
if err != nil {
|
2020-11-11 20:29:14 +00:00
|
|
|
plog.WarningErr("authorize upstream config", err)
|
2020-11-04 23:04:50 +00:00
|
|
|
return err
|
2020-11-04 20:19:07 +00:00
|
|
|
}
|
|
|
|
|
2021-10-08 22:48:21 +00:00
|
|
|
if idpType == psession.ProviderTypeOIDC {
|
Create username scope, required for clients to get username in ID token
- 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"
2022-08-08 23:29:22 +00:00
|
|
|
if len(r.Header.Values(oidcapi.AuthorizeUsernameHeaderName)) > 0 ||
|
|
|
|
len(r.Header.Values(oidcapi.AuthorizePasswordHeaderName)) > 0 {
|
2021-08-12 17:00:18 +00:00
|
|
|
// The client set a username header, so they are trying to log in with a username/password.
|
|
|
|
return handleAuthRequestForOIDCUpstreamPasswordGrant(r, w, oauthHelperWithStorage, oidcUpstream)
|
|
|
|
}
|
2022-05-06 19:00:46 +00:00
|
|
|
return handleAuthRequestForOIDCUpstreamBrowserFlow(r, w,
|
2021-04-09 00:28:01 +00:00
|
|
|
oauthHelperWithoutStorage,
|
|
|
|
generateCSRF, generateNonce, generatePKCE,
|
|
|
|
oidcUpstream,
|
|
|
|
downstreamIssuer,
|
|
|
|
upstreamStateEncoder,
|
|
|
|
cookieCodec,
|
|
|
|
)
|
2020-11-04 00:17:38 +00:00
|
|
|
}
|
2022-04-26 19:51:56 +00:00
|
|
|
|
2022-05-06 19:00:46 +00:00
|
|
|
// We know it's an AD/LDAP upstream.
|
Create username scope, required for clients to get username in ID token
- 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"
2022-08-08 23:29:22 +00:00
|
|
|
if len(r.Header.Values(oidcapi.AuthorizeUsernameHeaderName)) > 0 ||
|
|
|
|
len(r.Header.Values(oidcapi.AuthorizePasswordHeaderName)) > 0 {
|
2022-04-26 19:51:56 +00:00
|
|
|
// The client set a username header, so they are trying to log in with a username/password.
|
|
|
|
return handleAuthRequestForLDAPUpstreamCLIFlow(r, w,
|
|
|
|
oauthHelperWithStorage,
|
|
|
|
ldapUpstream,
|
|
|
|
idpType,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
return handleAuthRequestForLDAPUpstreamBrowserFlow(
|
|
|
|
r,
|
|
|
|
w,
|
|
|
|
oauthHelperWithoutStorage,
|
|
|
|
generateCSRF,
|
|
|
|
generateNonce,
|
|
|
|
generatePKCE,
|
2021-04-09 00:28:01 +00:00
|
|
|
ldapUpstream,
|
2021-10-08 22:48:21 +00:00
|
|
|
idpType,
|
2022-04-26 19:51:56 +00:00
|
|
|
downstreamIssuer,
|
|
|
|
upstreamStateEncoder,
|
|
|
|
cookieCodec,
|
2021-04-09 00:28:01 +00:00
|
|
|
)
|
2022-06-02 16:23:34 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
// During a response_mode=form_post auth request using the browser flow, the custom form_post html page may
|
|
|
|
// be used to post certain errors back to the CLI from this handler's response, so allow the form_post
|
|
|
|
// page's CSS and JS to run.
|
|
|
|
return securityheader.WrapWithCustomCSP(handler, formposthtml.ContentSecurityPolicy())
|
2021-04-09 00:28:01 +00:00
|
|
|
}
|
2020-11-04 00:17:38 +00:00
|
|
|
|
2022-04-26 19:51:56 +00:00
|
|
|
func handleAuthRequestForLDAPUpstreamCLIFlow(
|
2021-04-09 00:28:01 +00:00
|
|
|
r *http.Request,
|
|
|
|
w http.ResponseWriter,
|
|
|
|
oauthHelper fosite.OAuth2Provider,
|
|
|
|
ldapUpstream provider.UpstreamLDAPIdentityProviderI,
|
2021-10-08 22:48:21 +00:00
|
|
|
idpType psession.ProviderType,
|
2021-04-09 00:28:01 +00:00
|
|
|
) error {
|
2021-12-10 22:22:36 +00:00
|
|
|
authorizeRequester, created := newAuthorizeRequest(r, w, oauthHelper, true)
|
2021-04-09 00:28:01 +00:00
|
|
|
if !created {
|
|
|
|
return nil
|
|
|
|
}
|
2020-11-04 00:17:38 +00:00
|
|
|
|
2022-12-14 00:18:51 +00:00
|
|
|
if !requireStaticClientForUsernameAndPasswordHeaders(r, w, oauthHelper, authorizeRequester) {
|
2022-07-14 16:51:11 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2021-08-12 17:00:18 +00:00
|
|
|
username, password, hadUsernamePasswordValues := requireNonEmptyUsernameAndPasswordHeaders(r, w, oauthHelper, authorizeRequester)
|
|
|
|
if !hadUsernamePasswordValues {
|
2021-04-09 00:28:01 +00:00
|
|
|
return nil
|
|
|
|
}
|
2020-11-04 00:17:38 +00:00
|
|
|
|
2022-06-22 17:58:08 +00:00
|
|
|
authenticateResponse, authenticated, err := ldapUpstream.AuthenticateUser(r.Context(), username, password, authorizeRequester.GetGrantedScopes())
|
2021-04-09 00:28:01 +00:00
|
|
|
if err != nil {
|
2021-04-13 23:22:13 +00:00
|
|
|
plog.WarningErr("unexpected error during upstream LDAP authentication", err, "upstreamName", ldapUpstream.GetName())
|
2021-04-09 00:28:01 +00:00
|
|
|
return httperr.New(http.StatusBadGateway, "unexpected error during upstream authentication")
|
|
|
|
}
|
|
|
|
if !authenticated {
|
2022-12-14 00:18:51 +00:00
|
|
|
oidc.WriteAuthorizeError(r, w, oauthHelper, authorizeRequester,
|
2021-12-10 22:22:36 +00:00
|
|
|
fosite.ErrAccessDenied.WithHintf("Username/password not accepted by LDAP provider."), true)
|
2022-04-29 23:01:51 +00:00
|
|
|
return nil
|
2021-04-09 00:28:01 +00:00
|
|
|
}
|
|
|
|
|
2022-04-29 23:01:51 +00:00
|
|
|
subject := downstreamsession.DownstreamSubjectFromUpstreamLDAP(ldapUpstream, authenticateResponse)
|
2021-08-16 22:17:30 +00:00
|
|
|
username = authenticateResponse.User.GetName()
|
|
|
|
groups := authenticateResponse.User.GetGroups()
|
Create username scope, required for clients to get username in ID token
- 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"
2022-08-08 23:29:22 +00:00
|
|
|
customSessionData := downstreamsession.MakeDownstreamLDAPOrADCustomSessionData(ldapUpstream, idpType, authenticateResponse, username)
|
2022-08-09 23:07:23 +00:00
|
|
|
openIDSession := downstreamsession.MakeDownstreamSession(subject, username, groups,
|
2022-09-20 21:54:10 +00:00
|
|
|
authorizeRequester.GetGrantedScopes(), authorizeRequester.GetClient().GetID(), customSessionData, map[string]interface{}{})
|
2022-04-29 23:01:51 +00:00
|
|
|
oidc.PerformAuthcodeRedirect(r, w, oauthHelper, authorizeRequester, openIDSession, true)
|
2021-10-22 20:57:30 +00:00
|
|
|
|
2022-04-29 23:01:51 +00:00
|
|
|
return nil
|
2021-04-09 00:28:01 +00:00
|
|
|
}
|
|
|
|
|
2022-04-26 19:51:56 +00:00
|
|
|
func handleAuthRequestForLDAPUpstreamBrowserFlow(
|
|
|
|
r *http.Request,
|
|
|
|
w http.ResponseWriter,
|
|
|
|
oauthHelper fosite.OAuth2Provider,
|
|
|
|
generateCSRF func() (csrftoken.CSRFToken, error),
|
|
|
|
generateNonce func() (nonce.Nonce, error),
|
|
|
|
generatePKCE func() (pkce.Code, error),
|
|
|
|
ldapUpstream provider.UpstreamLDAPIdentityProviderI,
|
|
|
|
idpType psession.ProviderType,
|
|
|
|
downstreamIssuer string,
|
|
|
|
upstreamStateEncoder oidc.Encoder,
|
|
|
|
cookieCodec oidc.Codec,
|
|
|
|
) error {
|
2022-05-19 23:02:08 +00:00
|
|
|
authRequestState, err := handleBrowserFlowAuthRequest(
|
2022-04-27 15:51:37 +00:00
|
|
|
r,
|
|
|
|
w,
|
|
|
|
oauthHelper,
|
|
|
|
generateCSRF,
|
|
|
|
generateNonce,
|
|
|
|
generatePKCE,
|
2022-04-26 19:51:56 +00:00
|
|
|
ldapUpstream.GetName(),
|
2022-04-27 15:51:37 +00:00
|
|
|
idpType,
|
|
|
|
cookieCodec,
|
2022-04-26 19:51:56 +00:00
|
|
|
upstreamStateEncoder,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2022-05-19 23:02:08 +00:00
|
|
|
if authRequestState == nil {
|
|
|
|
// There was an error but handleBrowserFlowAuthRequest() already took care of writing the response for it.
|
2022-04-27 15:51:37 +00:00
|
|
|
return nil
|
2022-04-26 19:51:56 +00:00
|
|
|
}
|
|
|
|
|
2022-05-19 23:02:08 +00:00
|
|
|
return login.RedirectToLoginPage(r, w, downstreamIssuer, authRequestState.encodedStateParam, login.ShowNoError)
|
2022-04-26 19:51:56 +00:00
|
|
|
}
|
|
|
|
|
2021-08-12 17:00:18 +00:00
|
|
|
func handleAuthRequestForOIDCUpstreamPasswordGrant(
|
|
|
|
r *http.Request,
|
|
|
|
w http.ResponseWriter,
|
|
|
|
oauthHelper fosite.OAuth2Provider,
|
|
|
|
oidcUpstream provider.UpstreamOIDCIdentityProviderI,
|
|
|
|
) error {
|
2021-12-10 22:22:36 +00:00
|
|
|
authorizeRequester, created := newAuthorizeRequest(r, w, oauthHelper, true)
|
2021-08-12 17:00:18 +00:00
|
|
|
if !created {
|
2021-04-09 00:28:01 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2022-12-14 00:18:51 +00:00
|
|
|
if !requireStaticClientForUsernameAndPasswordHeaders(r, w, oauthHelper, authorizeRequester) {
|
2022-07-14 16:51:11 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2021-08-12 17:00:18 +00:00
|
|
|
username, password, hadUsernamePasswordValues := requireNonEmptyUsernameAndPasswordHeaders(r, w, oauthHelper, authorizeRequester)
|
|
|
|
if !hadUsernamePasswordValues {
|
|
|
|
return nil
|
|
|
|
}
|
2021-04-09 00:28:01 +00:00
|
|
|
|
2021-08-16 21:27:40 +00:00
|
|
|
if !oidcUpstream.AllowsPasswordGrant() {
|
|
|
|
// Return a user-friendly error for this case which is entirely within our control.
|
2022-12-14 00:18:51 +00:00
|
|
|
oidc.WriteAuthorizeError(r, w, oauthHelper, authorizeRequester,
|
2021-08-18 19:06:46 +00:00
|
|
|
fosite.ErrAccessDenied.WithHint(
|
2021-12-10 22:22:36 +00:00
|
|
|
"Resource owner password credentials grant is not allowed for this upstream provider according to its configuration."), true)
|
2022-04-29 23:01:51 +00:00
|
|
|
return nil
|
2021-08-16 21:27:40 +00:00
|
|
|
}
|
|
|
|
|
2021-08-12 17:00:18 +00:00
|
|
|
token, err := oidcUpstream.PasswordCredentialsGrantAndValidateTokens(r.Context(), username, password)
|
2021-04-09 00:28:01 +00:00
|
|
|
if err != nil {
|
2021-08-16 21:27:40 +00:00
|
|
|
// Upstream password grant errors can be generic errors (e.g. a network failure) or can be oauth2.RetrieveError errors
|
|
|
|
// which represent the http response from the upstream server. These could be a 5XX or some other unexpected error,
|
|
|
|
// or could be a 400 with a JSON body as described by https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
|
|
|
|
// which notes that wrong resource owner credentials should result in an "invalid_grant" error.
|
|
|
|
// However, the exact response is undefined in the sense that there is no such thing as a password grant in
|
|
|
|
// the OIDC spec, so we don't try too hard to read the upstream errors in this case. (E.g. Dex departs from the
|
|
|
|
// spec and returns something other than an "invalid_grant" error for bad resource owner credentials.)
|
2022-12-14 00:18:51 +00:00
|
|
|
oidc.WriteAuthorizeError(r, w, oauthHelper, authorizeRequester,
|
2021-12-10 22:22:36 +00:00
|
|
|
fosite.ErrAccessDenied.WithDebug(err.Error()), true) // WithDebug hides the error from the client
|
2022-04-29 23:01:51 +00:00
|
|
|
return nil
|
2021-04-09 00:28:01 +00:00
|
|
|
}
|
|
|
|
|
2021-08-17 20:14:09 +00:00
|
|
|
subject, username, groups, err := downstreamsession.GetDownstreamIdentityFromUpstreamIDToken(oidcUpstream, token.IDToken.Claims)
|
2021-08-12 17:00:18 +00:00
|
|
|
if err != nil {
|
2021-08-18 19:06:46 +00:00
|
|
|
// Return a user-friendly error for this case which is entirely within our control.
|
2022-12-14 00:18:51 +00:00
|
|
|
oidc.WriteAuthorizeError(r, w, oauthHelper, authorizeRequester,
|
2021-12-10 22:22:36 +00:00
|
|
|
fosite.ErrAccessDenied.WithHintf("Reason: %s.", err.Error()), true,
|
2021-08-18 19:06:46 +00:00
|
|
|
)
|
2022-04-29 23:01:51 +00:00
|
|
|
return nil
|
2021-08-12 17:00:18 +00:00
|
|
|
}
|
2022-01-11 19:00:54 +00:00
|
|
|
|
2022-09-20 21:54:10 +00:00
|
|
|
additionalClaims := downstreamsession.MapAdditionalClaimsFromUpstreamIDToken(oidcUpstream, token.IDToken.Claims)
|
|
|
|
|
Create username scope, required for clients to get username in ID token
- 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"
2022-08-08 23:29:22 +00:00
|
|
|
customSessionData, err := downstreamsession.MakeDownstreamOIDCCustomSessionData(oidcUpstream, token, username)
|
2022-01-07 23:04:58 +00:00
|
|
|
if err != nil {
|
2022-12-14 00:18:51 +00:00
|
|
|
oidc.WriteAuthorizeError(r, w, oauthHelper, authorizeRequester,
|
2022-01-07 23:04:58 +00:00
|
|
|
fosite.ErrAccessDenied.WithHintf("Reason: %s.", err.Error()), true,
|
|
|
|
)
|
2022-04-29 23:01:51 +00:00
|
|
|
return nil
|
2022-01-07 23:04:58 +00:00
|
|
|
}
|
2021-06-30 22:02:14 +00:00
|
|
|
|
2022-08-09 23:07:23 +00:00
|
|
|
openIDSession := downstreamsession.MakeDownstreamSession(subject, username, groups,
|
2022-09-20 21:54:10 +00:00
|
|
|
authorizeRequester.GetGrantedScopes(), authorizeRequester.GetClient().GetID(), customSessionData, additionalClaims)
|
2022-04-29 23:01:51 +00:00
|
|
|
|
|
|
|
oidc.PerformAuthcodeRedirect(r, w, oauthHelper, authorizeRequester, openIDSession, true)
|
|
|
|
|
|
|
|
return nil
|
2021-04-09 00:28:01 +00:00
|
|
|
}
|
|
|
|
|
2022-05-06 19:00:46 +00:00
|
|
|
func handleAuthRequestForOIDCUpstreamBrowserFlow(
|
2021-04-09 00:28:01 +00:00
|
|
|
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 {
|
2022-05-19 23:02:08 +00:00
|
|
|
authRequestState, err := handleBrowserFlowAuthRequest(
|
2022-04-27 15:51:37 +00:00
|
|
|
r,
|
|
|
|
w,
|
|
|
|
oauthHelper,
|
|
|
|
generateCSRF,
|
|
|
|
generateNonce,
|
|
|
|
generatePKCE,
|
|
|
|
oidcUpstream.GetName(),
|
|
|
|
psession.ProviderTypeOIDC,
|
|
|
|
cookieCodec,
|
|
|
|
upstreamStateEncoder,
|
|
|
|
)
|
2021-04-09 00:28:01 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2022-05-19 23:02:08 +00:00
|
|
|
if authRequestState == nil {
|
|
|
|
// There was an error but handleBrowserFlowAuthRequest() already took care of writing the response for it.
|
2022-04-27 15:51:37 +00:00
|
|
|
return nil
|
2021-04-09 00:28:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
upstreamOAuthConfig := oauth2.Config{
|
|
|
|
ClientID: oidcUpstream.GetClientID(),
|
|
|
|
Endpoint: oauth2.Endpoint{
|
|
|
|
AuthURL: oidcUpstream.GetAuthorizationURL().String(),
|
|
|
|
},
|
|
|
|
RedirectURL: fmt.Sprintf("%s/callback", downstreamIssuer),
|
|
|
|
Scopes: oidcUpstream.GetScopes(),
|
|
|
|
}
|
|
|
|
|
|
|
|
authCodeOptions := []oauth2.AuthCodeOption{
|
2022-05-19 23:02:08 +00:00
|
|
|
authRequestState.nonce.Param(),
|
|
|
|
authRequestState.pkce.Challenge(),
|
|
|
|
authRequestState.pkce.Method(),
|
2021-04-09 00:28:01 +00:00
|
|
|
}
|
2020-11-11 01:58:00 +00:00
|
|
|
|
2021-10-08 22:48:21 +00:00
|
|
|
for key, val := range oidcUpstream.GetAdditionalAuthcodeParams() {
|
|
|
|
authCodeOptions = append(authCodeOptions, oauth2.SetAuthURLParam(key, val))
|
|
|
|
}
|
|
|
|
|
2021-04-09 00:28:01 +00:00
|
|
|
http.Redirect(w, r,
|
|
|
|
upstreamOAuthConfig.AuthCodeURL(
|
2022-05-19 23:02:08 +00:00
|
|
|
authRequestState.encodedStateParam,
|
2021-04-09 00:28:01 +00:00
|
|
|
authCodeOptions...,
|
|
|
|
),
|
2021-12-10 22:22:36 +00:00
|
|
|
http.StatusSeeOther, // match fosite and https://tools.ietf.org/id/draft-ietf-oauth-security-topics-18.html#section-4.11
|
2021-04-09 00:28:01 +00:00
|
|
|
)
|
2020-12-12 01:13:27 +00:00
|
|
|
|
2021-04-09 00:28:01 +00:00
|
|
|
return nil
|
|
|
|
}
|
2020-11-04 00:17:38 +00:00
|
|
|
|
2022-12-14 00:18:51 +00:00
|
|
|
func requireStaticClientForUsernameAndPasswordHeaders(r *http.Request, w http.ResponseWriter, oauthHelper fosite.OAuth2Provider, authorizeRequester fosite.AuthorizeRequester) bool {
|
Create username scope, required for clients to get username in ID token
- 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"
2022-08-08 23:29:22 +00:00
|
|
|
isStaticClient := authorizeRequester.GetClient().GetID() == oidcapi.ClientIDPinnipedCLI
|
2022-07-14 16:51:11 +00:00
|
|
|
if !isStaticClient {
|
2022-12-14 00:18:51 +00:00
|
|
|
oidc.WriteAuthorizeError(r, w, oauthHelper, authorizeRequester,
|
2022-07-14 16:51:11 +00:00
|
|
|
fosite.ErrAccessDenied.WithHintf("This client is not allowed to submit username or password headers to this endpoint."), true)
|
|
|
|
}
|
|
|
|
return isStaticClient
|
|
|
|
}
|
|
|
|
|
2021-08-16 21:27:40 +00:00
|
|
|
func requireNonEmptyUsernameAndPasswordHeaders(r *http.Request, w http.ResponseWriter, oauthHelper fosite.OAuth2Provider, authorizeRequester fosite.AuthorizeRequester) (string, string, bool) {
|
Create username scope, required for clients to get username in ID token
- 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"
2022-08-08 23:29:22 +00:00
|
|
|
username := r.Header.Get(oidcapi.AuthorizeUsernameHeaderName)
|
|
|
|
password := r.Header.Get(oidcapi.AuthorizePasswordHeaderName)
|
2021-08-16 21:27:40 +00:00
|
|
|
if username == "" || password == "" {
|
2022-12-14 00:18:51 +00:00
|
|
|
oidc.WriteAuthorizeError(r, w, oauthHelper, authorizeRequester,
|
2021-12-10 22:22:36 +00:00
|
|
|
fosite.ErrAccessDenied.WithHintf("Missing or blank username or password."), true)
|
2021-08-16 21:27:40 +00:00
|
|
|
return "", "", false
|
|
|
|
}
|
|
|
|
return username, password, true
|
|
|
|
}
|
|
|
|
|
2021-12-10 22:22:36 +00:00
|
|
|
func newAuthorizeRequest(r *http.Request, w http.ResponseWriter, oauthHelper fosite.OAuth2Provider, isBrowserless bool) (fosite.AuthorizeRequester, bool) {
|
2021-04-09 00:28:01 +00:00
|
|
|
authorizeRequester, err := oauthHelper.NewAuthorizeRequest(r.Context(), r)
|
|
|
|
if err != nil {
|
2022-12-14 00:18:51 +00:00
|
|
|
oidc.WriteAuthorizeError(r, w, oauthHelper, authorizeRequester, err, isBrowserless)
|
2021-04-09 00:28:01 +00:00
|
|
|
return nil, false
|
|
|
|
}
|
|
|
|
|
Create username scope, required for clients to get username in ID token
- 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"
2022-08-08 23:29:22 +00:00
|
|
|
// Automatically grant certain scopes, but only if they were requested.
|
2021-04-09 00:28:01 +00:00
|
|
|
// Grant the openid scope (for now) if they asked for it so that `NewAuthorizeResponse` will perform its OIDC validations.
|
|
|
|
// 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.
|
2022-07-14 16:51:11 +00:00
|
|
|
// 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.
|
Create username scope, required for clients to get username in ID token
- 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"
2022-08-08 23:29:22 +00:00
|
|
|
downstreamsession.AutoApproveScopes(authorizeRequester)
|
2021-06-30 22:02:14 +00:00
|
|
|
|
|
|
|
return authorizeRequester, true
|
2020-11-04 00:17:38 +00:00
|
|
|
}
|
|
|
|
|
2020-12-10 01:20:57 +00:00
|
|
|
func readCSRFCookie(r *http.Request, codec oidc.Decoder) csrftoken.CSRFToken {
|
2020-11-16 16:47:49 +00:00
|
|
|
receivedCSRFCookie, err := r.Cookie(oidc.CSRFCookieName)
|
2020-11-12 23:36:59 +00:00
|
|
|
if err != nil {
|
|
|
|
// Error means that the cookie was not found
|
2020-11-20 21:56:35 +00:00
|
|
|
return ""
|
2020-11-12 23:36:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
var csrfFromCookie csrftoken.CSRFToken
|
2020-11-16 16:47:49 +00:00
|
|
|
err = codec.Decode(oidc.CSRFCookieEncodingName, receivedCSRFCookie.Value, &csrfFromCookie)
|
2020-11-12 23:36:59 +00:00
|
|
|
if err != nil {
|
2020-11-20 21:56:35 +00:00
|
|
|
// We can ignore any errors and just make a new cookie. Hopefully this will
|
|
|
|
// make the user experience better if, for example, the server rotated
|
|
|
|
// cookie signing keys and then a user submitted a very old cookie.
|
|
|
|
return ""
|
2020-11-12 23:36:59 +00:00
|
|
|
}
|
|
|
|
|
2020-11-20 21:56:35 +00:00
|
|
|
return csrfFromCookie
|
2020-11-12 23:36:59 +00:00
|
|
|
}
|
|
|
|
|
2022-04-29 23:01:51 +00:00
|
|
|
// chooseUpstreamIDP selects either an OIDC, an LDAP, or an AD IDP, or returns an error.
|
|
|
|
// Note that AD and LDAP IDPs both return the same interface type, but different ProviderTypes values.
|
2021-10-08 22:48:21 +00:00
|
|
|
func chooseUpstreamIDP(idpLister oidc.UpstreamIdentityProvidersLister) (provider.UpstreamOIDCIdentityProviderI, provider.UpstreamLDAPIdentityProviderI, psession.ProviderType, error) {
|
2021-04-09 00:28:01 +00:00
|
|
|
oidcUpstreams := idpLister.GetOIDCIdentityProviders()
|
|
|
|
ldapUpstreams := idpLister.GetLDAPIdentityProviders()
|
2021-07-02 22:30:27 +00:00
|
|
|
adUpstreams := idpLister.GetActiveDirectoryIdentityProviders()
|
2021-04-09 00:28:01 +00:00
|
|
|
switch {
|
2021-07-02 22:30:27 +00:00
|
|
|
case len(oidcUpstreams)+len(ldapUpstreams)+len(adUpstreams) == 0:
|
2021-10-08 22:48:21 +00:00
|
|
|
return nil, nil, "", httperr.New(
|
2020-11-04 00:17:38 +00:00
|
|
|
http.StatusUnprocessableEntity,
|
|
|
|
"No upstream providers are configured",
|
|
|
|
)
|
2021-07-02 22:30:27 +00:00
|
|
|
case len(oidcUpstreams)+len(ldapUpstreams)+len(adUpstreams) > 1:
|
2020-12-15 18:49:13 +00:00
|
|
|
var upstreamIDPNames []string
|
2021-04-09 00:28:01 +00:00
|
|
|
for _, idp := range oidcUpstreams {
|
|
|
|
upstreamIDPNames = append(upstreamIDPNames, idp.GetName())
|
|
|
|
}
|
|
|
|
for _, idp := range ldapUpstreams {
|
2020-12-15 18:49:13 +00:00
|
|
|
upstreamIDPNames = append(upstreamIDPNames, idp.GetName())
|
|
|
|
}
|
2021-07-02 22:30:27 +00:00
|
|
|
for _, idp := range adUpstreams {
|
|
|
|
upstreamIDPNames = append(upstreamIDPNames, idp.GetName())
|
|
|
|
}
|
2020-12-15 18:49:13 +00:00
|
|
|
plog.Warning("Too many upstream providers are configured (found: %s)", upstreamIDPNames)
|
2021-10-08 22:48:21 +00:00
|
|
|
return nil, nil, "", httperr.New(
|
2020-11-04 00:17:38 +00:00
|
|
|
http.StatusUnprocessableEntity,
|
|
|
|
"Too many upstream providers are configured (support for multiple upstreams is not yet implemented)",
|
|
|
|
)
|
2021-04-09 00:28:01 +00:00
|
|
|
case len(oidcUpstreams) == 1:
|
2021-10-08 22:48:21 +00:00
|
|
|
return oidcUpstreams[0], nil, psession.ProviderTypeOIDC, nil
|
2021-07-02 22:30:27 +00:00
|
|
|
case len(adUpstreams) == 1:
|
2021-10-08 22:48:21 +00:00
|
|
|
return nil, adUpstreams[0], psession.ProviderTypeActiveDirectory, nil
|
2021-04-09 00:28:01 +00:00
|
|
|
default:
|
2021-10-08 22:48:21 +00:00
|
|
|
return nil, ldapUpstreams[0], psession.ProviderTypeLDAP, nil
|
2020-11-04 00:17:38 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-19 23:02:08 +00:00
|
|
|
type browserFlowAuthRequestState struct {
|
|
|
|
encodedStateParam string
|
|
|
|
pkce pkce.Code
|
|
|
|
nonce nonce.Nonce
|
|
|
|
}
|
|
|
|
|
|
|
|
// handleBrowserFlowAuthRequest performs the shared validations and setup between browser based
|
|
|
|
// auth requests regardless of IDP type-- LDAP, Active Directory and OIDC.
|
2022-04-27 15:51:37 +00:00
|
|
|
// It generates the state param, sets the CSRF cookie, and validates the prompt param.
|
2022-05-19 23:02:08 +00:00
|
|
|
// It returns an error when it encounters an error without handling it, leaving it to
|
|
|
|
// the caller to decide how to handle it.
|
|
|
|
// It returns nil with no error when it encounters an error and also has already handled writing
|
|
|
|
// the error response to the ResponseWriter, in which case the caller should not also try to
|
|
|
|
// write the error response.
|
|
|
|
func handleBrowserFlowAuthRequest(
|
2022-04-27 15:51:37 +00:00
|
|
|
r *http.Request,
|
|
|
|
w http.ResponseWriter,
|
|
|
|
oauthHelper fosite.OAuth2Provider,
|
|
|
|
generateCSRF func() (csrftoken.CSRFToken, error),
|
|
|
|
generateNonce func() (nonce.Nonce, error),
|
|
|
|
generatePKCE func() (pkce.Code, error),
|
|
|
|
upstreamName string,
|
|
|
|
idpType psession.ProviderType,
|
|
|
|
cookieCodec oidc.Codec,
|
|
|
|
upstreamStateEncoder oidc.Encoder,
|
2022-05-19 23:02:08 +00:00
|
|
|
) (*browserFlowAuthRequestState, error) {
|
2022-04-27 15:51:37 +00:00
|
|
|
authorizeRequester, created := newAuthorizeRequest(r, w, oauthHelper, false)
|
|
|
|
if !created {
|
2022-05-19 23:02:08 +00:00
|
|
|
return nil, nil // already wrote the error response, don't return error
|
2022-04-27 15:51:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
now := time.Now()
|
|
|
|
_, err := oauthHelper.NewAuthorizeResponse(r.Context(), authorizeRequester, &psession.PinnipedSession{
|
|
|
|
Fosite: &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 {
|
2022-12-14 00:18:51 +00:00
|
|
|
oidc.WriteAuthorizeError(r, w, oauthHelper, authorizeRequester, err, false)
|
2022-05-19 23:02:08 +00:00
|
|
|
return nil, nil // already wrote the error response, don't return error
|
2022-04-27 15:51:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
csrfValue, nonceValue, pkceValue, err := generateValues(generateCSRF, generateNonce, generatePKCE)
|
|
|
|
if err != nil {
|
|
|
|
plog.Error("authorize generate error", err)
|
2022-05-19 23:02:08 +00:00
|
|
|
return nil, err
|
2022-04-27 15:51:37 +00:00
|
|
|
}
|
|
|
|
csrfFromCookie := readCSRFCookie(r, cookieCodec)
|
|
|
|
if csrfFromCookie != "" {
|
|
|
|
csrfValue = csrfFromCookie
|
|
|
|
}
|
|
|
|
|
|
|
|
encodedStateParamValue, err := upstreamStateParam(
|
|
|
|
authorizeRequester,
|
|
|
|
upstreamName,
|
|
|
|
string(idpType),
|
|
|
|
nonceValue,
|
|
|
|
csrfValue,
|
|
|
|
pkceValue,
|
|
|
|
upstreamStateEncoder,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
plog.Error("authorize upstream state param error", err)
|
2022-05-19 23:02:08 +00:00
|
|
|
return nil, err
|
2022-04-27 15:51:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
promptParam := r.Form.Get(promptParamName)
|
Create username scope, required for clients to get username in ID token
- 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"
2022-08-08 23:29:22 +00:00
|
|
|
if promptParam == promptParamNone && oidc.ScopeWasRequested(authorizeRequester, oidcapi.ScopeOpenID) {
|
2022-12-14 00:18:51 +00:00
|
|
|
oidc.WriteAuthorizeError(r, w, oauthHelper, authorizeRequester, fosite.ErrLoginRequired, false)
|
2022-05-19 23:02:08 +00:00
|
|
|
return nil, nil // already wrote the error response, don't return error
|
2022-04-27 15:51:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
2022-05-19 23:02:08 +00:00
|
|
|
return nil, err
|
2022-04-27 15:51:37 +00:00
|
|
|
}
|
|
|
|
}
|
2022-05-19 23:02:08 +00:00
|
|
|
|
|
|
|
return &browserFlowAuthRequestState{
|
|
|
|
encodedStateParam: encodedStateParamValue,
|
|
|
|
pkce: pkceValue,
|
|
|
|
nonce: nonceValue,
|
|
|
|
}, nil
|
2022-04-27 15:51:37 +00:00
|
|
|
}
|
|
|
|
|
2020-11-11 01:58:00 +00:00
|
|
|
func generateValues(
|
|
|
|
generateCSRF func() (csrftoken.CSRFToken, error),
|
2020-11-04 00:17:38 +00:00
|
|
|
generateNonce func() (nonce.Nonce, error),
|
|
|
|
generatePKCE func() (pkce.Code, error),
|
2020-11-11 01:58:00 +00:00
|
|
|
) (csrftoken.CSRFToken, nonce.Nonce, pkce.Code, error) {
|
|
|
|
csrfValue, err := generateCSRF()
|
2020-11-04 00:17:38 +00:00
|
|
|
if err != nil {
|
2020-11-11 01:58:00 +00:00
|
|
|
return "", "", "", httperr.Wrap(http.StatusInternalServerError, "error generating CSRF token", err)
|
2020-11-04 00:17:38 +00:00
|
|
|
}
|
|
|
|
nonceValue, err := generateNonce()
|
|
|
|
if err != nil {
|
|
|
|
return "", "", "", httperr.Wrap(http.StatusInternalServerError, "error generating nonce param", err)
|
|
|
|
}
|
|
|
|
pkceValue, err := generatePKCE()
|
|
|
|
if err != nil {
|
|
|
|
return "", "", "", httperr.Wrap(http.StatusInternalServerError, "error generating PKCE param", err)
|
|
|
|
}
|
2020-11-11 01:58:00 +00:00
|
|
|
return csrfValue, nonceValue, pkceValue, nil
|
2020-11-04 00:17:38 +00:00
|
|
|
}
|
2020-11-10 18:33:52 +00:00
|
|
|
|
2020-11-11 20:29:14 +00:00
|
|
|
func upstreamStateParam(
|
|
|
|
authorizeRequester fosite.AuthorizeRequester,
|
2020-11-20 21:14:45 +00:00
|
|
|
upstreamName string,
|
2022-04-26 19:51:56 +00:00
|
|
|
upstreamType string,
|
2020-11-11 20:29:14 +00:00
|
|
|
nonceValue nonce.Nonce,
|
|
|
|
csrfValue csrftoken.CSRFToken,
|
|
|
|
pkceValue pkce.Code,
|
2020-11-16 19:41:00 +00:00
|
|
|
encoder oidc.Encoder,
|
2020-11-11 20:29:14 +00:00
|
|
|
) (string, error) {
|
2020-11-16 19:41:00 +00:00
|
|
|
stateParamData := oidc.UpstreamStateParamData{
|
Create username scope, required for clients to get username in ID token
- 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"
2022-08-08 23:29:22 +00:00
|
|
|
// The auth params might have included oidcapi.AuthorizeUpstreamIDPNameParamName and
|
|
|
|
// oidcapi.AuthorizeUpstreamIDPTypeParamName, but those can be ignored by other handlers
|
2022-05-06 19:00:46 +00:00
|
|
|
// that are reading from the encoded upstream state param being built here.
|
|
|
|
// The UpstreamName and UpstreamType struct fields can be used instead.
|
|
|
|
// Remove those params here to avoid potential confusion about which should be used later.
|
|
|
|
AuthParams: removeCustomIDPParams(authorizeRequester.GetRequestForm()).Encode(),
|
2020-11-20 21:14:45 +00:00
|
|
|
UpstreamName: upstreamName,
|
2022-04-26 19:51:56 +00:00
|
|
|
UpstreamType: upstreamType,
|
2020-11-16 19:41:00 +00:00
|
|
|
Nonce: nonceValue,
|
|
|
|
CSRFToken: csrfValue,
|
|
|
|
PKCECode: pkceValue,
|
|
|
|
FormatVersion: oidc.UpstreamStateParamFormatVersion,
|
2020-11-11 20:29:14 +00:00
|
|
|
}
|
2020-11-16 19:41:00 +00:00
|
|
|
encodedStateParamValue, err := encoder.Encode(oidc.UpstreamStateParamEncodingName, stateParamData)
|
2020-11-11 20:29:14 +00:00
|
|
|
if err != nil {
|
|
|
|
return "", httperr.Wrap(http.StatusInternalServerError, "error encoding upstream state param", err)
|
|
|
|
}
|
|
|
|
return encodedStateParamValue, nil
|
|
|
|
}
|
|
|
|
|
2022-05-06 19:00:46 +00:00
|
|
|
func removeCustomIDPParams(params url.Values) url.Values {
|
|
|
|
p := url.Values{}
|
|
|
|
// Copy all params.
|
|
|
|
for k, v := range params {
|
|
|
|
p[k] = v
|
|
|
|
}
|
|
|
|
// Remove the unnecessary params.
|
Create username scope, required for clients to get username in ID token
- 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"
2022-08-08 23:29:22 +00:00
|
|
|
delete(p, oidcapi.AuthorizeUpstreamIDPNameParamName)
|
|
|
|
delete(p, oidcapi.AuthorizeUpstreamIDPTypeParamName)
|
2022-05-06 19:00:46 +00:00
|
|
|
return p
|
|
|
|
}
|
|
|
|
|
2020-12-10 01:20:57 +00:00
|
|
|
func addCSRFSetCookieHeader(w http.ResponseWriter, csrfValue csrftoken.CSRFToken, codec oidc.Encoder) error {
|
2020-11-16 16:47:49 +00:00
|
|
|
encodedCSRFValue, err := codec.Encode(oidc.CSRFCookieEncodingName, csrfValue)
|
2020-11-12 23:36:59 +00:00
|
|
|
if err != nil {
|
|
|
|
return httperr.Wrap(http.StatusInternalServerError, "error encoding CSRF cookie", err)
|
|
|
|
}
|
|
|
|
|
2020-11-11 20:29:14 +00:00
|
|
|
http.SetCookie(w, &http.Cookie{
|
2020-11-16 16:47:49 +00:00
|
|
|
Name: oidc.CSRFCookieName,
|
2020-11-12 23:36:59 +00:00
|
|
|
Value: encodedCSRFValue,
|
2020-11-11 20:29:14 +00:00
|
|
|
HttpOnly: true,
|
2020-12-04 03:23:58 +00:00
|
|
|
SameSite: http.SameSiteLaxMode,
|
2020-11-11 20:29:14 +00:00
|
|
|
Secure: true,
|
2020-12-01 23:01:22 +00:00
|
|
|
Path: "/",
|
2020-11-11 20:29:14 +00:00
|
|
|
})
|
2020-11-12 23:36:59 +00:00
|
|
|
|
|
|
|
return nil
|
2020-11-11 20:29:14 +00:00
|
|
|
}
|