2023-01-17 23:54:16 +00:00
// Copyright 2021-2023 the Pinniped contributors. All Rights Reserved.
2021-06-30 22:02:14 +00:00
// SPDX-License-Identifier: Apache-2.0
// Package downstreamsession provides some shared helpers for creating downstream OIDC sessions.
package downstreamsession
import (
2023-05-08 21:07:38 +00:00
"context"
2022-01-11 19:00:54 +00:00
"errors"
2021-08-12 17:00:18 +00:00
"fmt"
"net/url"
2021-06-30 22:02:14 +00:00
"time"
"github.com/ory/fosite"
"github.com/ory/fosite/handler/openid"
"github.com/ory/fosite/token/jwt"
2022-01-18 23:34:19 +00:00
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2022-06-22 15:21:16 +00:00
"k8s.io/utils/strings/slices"
2021-06-30 22:02:14 +00:00
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"
2022-04-29 23:01:51 +00:00
"go.pinniped.dev/internal/authenticators"
2021-08-18 19:06:46 +00:00
"go.pinniped.dev/internal/constable"
2023-06-22 22:12:33 +00:00
"go.pinniped.dev/internal/federationdomain/oidc"
"go.pinniped.dev/internal/federationdomain/upstreamprovider"
2023-05-08 21:07:38 +00:00
"go.pinniped.dev/internal/idtransform"
2021-08-12 17:00:18 +00:00
"go.pinniped.dev/internal/plog"
2021-10-06 22:28:13 +00:00
"go.pinniped.dev/internal/psession"
2022-01-11 19:00:54 +00:00
"go.pinniped.dev/pkg/oidcclient/oidctypes"
2021-08-12 17:00:18 +00:00
)
const (
// The name of the email claim from https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
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
emailClaimName = oidcapi . ScopeEmail
2021-08-12 17:00:18 +00:00
// The name of the email_verified claim from https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
emailVerifiedClaimName = "email_verified"
2021-08-18 19:06:46 +00:00
requiredClaimMissingErr = constable . Error ( "required claim in upstream ID token missing" )
requiredClaimInvalidFormatErr = constable . Error ( "required claim in upstream ID token has invalid format" )
requiredClaimEmptyErr = constable . Error ( "required claim in upstream ID token is empty" )
emailVerifiedClaimInvalidFormatErr = constable . Error ( "email_verified claim in upstream ID token has invalid format" )
emailVerifiedClaimFalseErr = constable . Error ( "email_verified claim in upstream ID token has false value" )
2023-05-08 21:07:38 +00:00
idTransformUnexpectedErr = constable . Error ( "configured identity transformation or policy resulted in unexpected error" )
idTransformPolicyErr = constable . Error ( "configured identity policy rejected this authentication" )
2021-06-30 22:02:14 +00:00
)
// MakeDownstreamSession creates a downstream OIDC session.
2022-08-09 23:07:23 +00:00
func MakeDownstreamSession (
subject string ,
username string ,
groups [ ] string ,
grantedScopes [ ] string ,
clientID string ,
custom * psession . CustomSessionData ,
2022-09-20 21:54:10 +00:00
additionalClaims map [ string ] interface { } ,
2022-08-09 23:07:23 +00:00
) * psession . PinnipedSession {
2021-06-30 22:02:14 +00:00
now := time . Now ( ) . UTC ( )
2021-10-06 22:28:13 +00:00
openIDSession := & psession . PinnipedSession {
Fosite : & openid . DefaultSession {
Claims : & jwt . IDTokenClaims {
Subject : subject ,
RequestedAt : now ,
AuthTime : now ,
} ,
2021-06-30 22:02:14 +00:00
} ,
2021-10-08 22:48:21 +00:00
Custom : custom ,
2021-06-30 22:02:14 +00:00
}
if groups == nil {
groups = [ ] string { }
}
2022-08-09 23:07:23 +00:00
extras := map [ string ] interface { } { }
extras [ oidcapi . IDTokenClaimAuthorizedParty ] = clientID
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 slices . Contains ( grantedScopes , oidcapi . ScopeUsername ) {
2022-08-09 23:07:23 +00:00
extras [ oidcapi . IDTokenClaimUsername ] = username
2022-06-15 15:00:17 +00:00
}
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 slices . Contains ( grantedScopes , oidcapi . ScopeGroups ) {
2022-08-09 23:07:23 +00:00
extras [ oidcapi . IDTokenClaimGroups ] = groups
2021-06-30 22:02:14 +00:00
}
2022-09-20 21:54:10 +00:00
if len ( additionalClaims ) > 0 {
extras [ oidcapi . IDTokenClaimAdditionalClaims ] = additionalClaims
}
2022-08-09 23:07:23 +00:00
openIDSession . IDTokenClaims ( ) . Extra = extras
2021-06-30 22:02:14 +00:00
return openIDSession
}
2022-04-29 23:01:51 +00:00
func MakeDownstreamLDAPOrADCustomSessionData (
2023-05-08 21:07:38 +00:00
ldapUpstream upstreamprovider . UpstreamLDAPIdentityProviderI ,
2022-04-29 23:01:51 +00:00
idpType psession . ProviderType ,
authenticateResponse * authenticators . Response ,
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 string ,
2023-05-08 21:07:38 +00:00
untransformedUpstreamUsername string ,
untransformedUpstreamGroups [ ] string ,
2022-04-29 23:01:51 +00:00
) * psession . CustomSessionData {
customSessionData := & psession . CustomSessionData {
2023-05-08 21:07:38 +00:00
Username : username ,
UpstreamUsername : untransformedUpstreamUsername ,
UpstreamGroups : untransformedUpstreamGroups ,
ProviderUID : ldapUpstream . GetResourceUID ( ) ,
ProviderName : ldapUpstream . GetName ( ) ,
ProviderType : idpType ,
2022-04-29 23:01:51 +00:00
}
if idpType == psession . ProviderTypeLDAP {
customSessionData . LDAP = & psession . LDAPSessionData {
UserDN : authenticateResponse . DN ,
ExtraRefreshAttributes : authenticateResponse . ExtraRefreshAttributes ,
}
}
if idpType == psession . ProviderTypeActiveDirectory {
customSessionData . ActiveDirectory = & psession . ActiveDirectorySessionData {
UserDN : authenticateResponse . DN ,
ExtraRefreshAttributes : authenticateResponse . ExtraRefreshAttributes ,
}
}
return customSessionData
}
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
func MakeDownstreamOIDCCustomSessionData (
2023-05-08 21:07:38 +00:00
oidcUpstream upstreamprovider . UpstreamOIDCIdentityProviderI ,
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
token * oidctypes . Token ,
username string ,
2023-05-08 21:07:38 +00:00
untransformedUpstreamUsername string ,
untransformedUpstreamGroups [ ] string ,
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
) ( * psession . CustomSessionData , error ) {
upstreamSubject , err := ExtractStringClaimValue ( oidcapi . IDTokenClaimSubject , oidcUpstream . GetName ( ) , token . IDToken . Claims )
2022-01-11 19:00:54 +00:00
if err != nil {
return nil , err
}
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
upstreamIssuer , err := ExtractStringClaimValue ( oidcapi . IDTokenClaimIssuer , oidcUpstream . GetName ( ) , token . IDToken . Claims )
2022-01-11 19:00:54 +00:00
if err != nil {
return nil , err
}
customSessionData := & psession . CustomSessionData {
2023-05-08 21:07:38 +00:00
Username : username ,
UpstreamUsername : untransformedUpstreamUsername ,
UpstreamGroups : untransformedUpstreamGroups ,
ProviderUID : oidcUpstream . GetResourceUID ( ) ,
ProviderName : oidcUpstream . GetName ( ) ,
ProviderType : psession . ProviderTypeOIDC ,
2022-01-11 19:00:54 +00:00
OIDC : & psession . OIDCSessionData {
UpstreamIssuer : upstreamIssuer ,
UpstreamSubject : upstreamSubject ,
} ,
}
2022-01-11 23:40:38 +00:00
const pleaseCheck = "please check configuration of OIDCIdentityProvider and the client in the " +
"upstream provider's API/UI and try to get a refresh token if possible"
logKV := [ ] interface { } {
"upstreamName" , oidcUpstream . GetName ( ) ,
"scopes" , oidcUpstream . GetScopes ( ) ,
"additionalParams" , oidcUpstream . GetAdditionalAuthcodeParams ( ) ,
}
2022-01-11 19:00:54 +00:00
hasRefreshToken := token . RefreshToken != nil && token . RefreshToken . Token != ""
hasAccessToken := token . AccessToken != nil && token . AccessToken . Token != ""
switch {
case hasRefreshToken : // we prefer refresh tokens, so check for this first
customSessionData . OIDC . UpstreamRefreshToken = token . RefreshToken . Token
2022-01-11 23:40:38 +00:00
case hasAccessToken : // as a fallback, we can use the access token as long as there is a userinfo endpoint
if ! oidcUpstream . HasUserInfoURL ( ) {
plog . Warning ( "access token was returned by upstream provider during login without a refresh token " +
"and there was no userinfo endpoint available on the provider. " + pleaseCheck , logKV ... )
return nil , errors . New ( "access token was returned by upstream provider but there was no userinfo endpoint" )
}
plog . Info ( "refresh token not returned by upstream provider during login, using access token instead. " + pleaseCheck , logKV ... )
2022-01-11 19:00:54 +00:00
customSessionData . OIDC . UpstreamAccessToken = token . AccessToken . Token
2022-01-18 23:34:19 +00:00
// When we are in a flow where we will be performing access token based refresh, issue a warning to the client if the access
// token lifetime is very short, since that would mean that the user's session is very short.
// The warnings are stored here and will be processed by the token handler.
threeHoursFromNow := metav1 . NewTime ( time . Now ( ) . Add ( 3 * time . Hour ) )
if ! token . AccessToken . Expiry . IsZero ( ) && token . AccessToken . Expiry . Before ( & threeHoursFromNow ) {
customSessionData . Warnings = append ( customSessionData . Warnings , "Access token from identity provider has lifetime of less than 3 hours. Expect frequent prompts to log in." )
}
2022-01-11 19:00:54 +00:00
default :
2022-01-11 23:40:38 +00:00
plog . Warning ( "refresh token and access token not returned by upstream provider during login. " + pleaseCheck , logKV ... )
2022-01-11 19:00:54 +00:00
return nil , errors . New ( "neither access token nor refresh token returned by upstream provider" )
}
2022-01-11 23:40:38 +00:00
2022-01-11 19:00:54 +00:00
return customSessionData , nil
}
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
// AutoApproveScopes auto-grants the scopes which we support and for which we do not require end-user approval,
// if they were requested. This should only be called after it has been validated that the client is allowed to request
// the scopes that it requested (which is a check performed by fosite).
func AutoApproveScopes ( authorizeRequester fosite . AuthorizeRequester ) {
for _ , scope := range [ ] string {
oidcapi . ScopeOpenID ,
oidcapi . ScopeOfflineAccess ,
oidcapi . ScopeRequestAudience ,
oidcapi . ScopeUsername ,
oidcapi . ScopeGroups ,
} {
2022-06-15 15:00:17 +00:00
oidc . GrantScopeIfRequested ( authorizeRequester , scope )
}
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
// For backwards-compatibility with old pinniped CLI binaries which never request the username and groups scopes
// (because those scopes did not exist yet when those CLIs were released), grant/approve the username and groups
// scopes even if the CLI did not request them. Basically, pretend that the CLI requested them and auto-approve
// them. Newer versions of the CLI binaries will request these scopes, so after enough time has passed that
// we can assume the old versions of the CLI are no longer in use in the wild, then we can remove this code and
// just let the above logic handle all clients.
if authorizeRequester . GetClient ( ) . GetID ( ) == oidcapi . ClientIDPinnipedCLI {
authorizeRequester . GrantScope ( oidcapi . ScopeUsername )
authorizeRequester . GrantScope ( oidcapi . ScopeGroups )
}
2021-06-30 22:02:14 +00:00
}
2021-08-12 17:00:18 +00:00
2021-08-17 20:14:09 +00:00
// GetDownstreamIdentityFromUpstreamIDToken returns the mapped subject, username, and group names, in that order.
func GetDownstreamIdentityFromUpstreamIDToken (
2023-05-08 21:07:38 +00:00
upstreamIDPConfig upstreamprovider . UpstreamOIDCIdentityProviderI ,
2021-08-12 17:00:18 +00:00
idTokenClaims map [ string ] interface { } ,
2021-08-17 20:14:09 +00:00
) ( string , string , [ ] string , error ) {
subject , username , err := getSubjectAndUsernameFromUpstreamIDToken ( upstreamIDPConfig , idTokenClaims )
if err != nil {
return "" , "" , nil , err
2021-08-12 17:00:18 +00:00
}
2022-01-15 00:38:21 +00:00
groups , err := GetGroupsFromUpstreamIDToken ( upstreamIDPConfig , idTokenClaims )
2021-08-17 20:14:09 +00:00
if err != nil {
return "" , "" , nil , err
2021-08-12 17:00:18 +00:00
}
2021-08-17 20:14:09 +00:00
return subject , username , groups , err
}
2021-08-12 17:00:18 +00:00
2022-09-20 21:54:10 +00:00
// MapAdditionalClaimsFromUpstreamIDToken returns the additionalClaims mapped from the upstream token, if any.
func MapAdditionalClaimsFromUpstreamIDToken (
2023-05-08 21:07:38 +00:00
upstreamIDPConfig upstreamprovider . UpstreamOIDCIdentityProviderI ,
2022-09-20 21:54:10 +00:00
idTokenClaims map [ string ] interface { } ,
) map [ string ] interface { } {
mapped := make ( map [ string ] interface { } , len ( upstreamIDPConfig . GetAdditionalClaimMappings ( ) ) )
for downstreamClaimName , upstreamClaimName := range upstreamIDPConfig . GetAdditionalClaimMappings ( ) {
upstreamClaimValue , ok := idTokenClaims [ upstreamClaimName ]
if ! ok {
plog . Warning (
"additionalClaims mapping claim in upstream ID token missing" ,
"upstreamName" , upstreamIDPConfig . GetName ( ) ,
"claimName" , upstreamClaimName ,
)
} else {
mapped [ downstreamClaimName ] = upstreamClaimValue
}
}
return mapped
}
2023-05-08 21:07:38 +00:00
func ApplyIdentityTransformations (
ctx context . Context ,
identityTransforms * idtransform . TransformationPipeline ,
username string ,
groups [ ] string ,
) ( string , [ ] string , error ) {
transformationResult , err := identityTransforms . Evaluate ( ctx , username , groups )
if err != nil {
plog . Error ( "unexpected identity transformation error during authentication" , err , "inputUsername" , username )
return "" , nil , idTransformUnexpectedErr
}
if ! transformationResult . AuthenticationAllowed {
plog . Debug ( "authentication rejected by configured policy" , "inputUsername" , username , "inputGroups" , groups )
return "" , nil , idTransformPolicyErr
}
plog . Debug ( "identity transformation successfully applied during authentication" ,
"originalUsername" , username ,
"newUsername" , transformationResult . Username ,
"originalGroups" , groups ,
"newGroups" , transformationResult . Groups ,
)
return transformationResult . Username , transformationResult . Groups , nil
}
2021-08-17 20:14:09 +00:00
func getSubjectAndUsernameFromUpstreamIDToken (
2023-05-08 21:07:38 +00:00
upstreamIDPConfig upstreamprovider . UpstreamOIDCIdentityProviderI ,
2021-08-17 20:14:09 +00:00
idTokenClaims map [ string ] interface { } ,
) ( string , string , error ) {
// The spec says the "sub" claim is only unique per issuer,
// so we will prepend the issuer string to make it globally unique.
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
upstreamIssuer , err := ExtractStringClaimValue ( oidcapi . IDTokenClaimIssuer , upstreamIDPConfig . GetName ( ) , idTokenClaims )
2021-08-17 20:14:09 +00:00
if err != nil {
return "" , "" , err
}
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
upstreamSubject , err := ExtractStringClaimValue ( oidcapi . IDTokenClaimSubject , upstreamIDPConfig . GetName ( ) , idTokenClaims )
2021-08-17 20:14:09 +00:00
if err != nil {
return "" , "" , err
}
2021-12-16 20:53:49 +00:00
subject := downstreamSubjectFromUpstreamOIDC ( upstreamIssuer , upstreamSubject )
2021-08-12 17:00:18 +00:00
usernameClaimName := upstreamIDPConfig . GetUsernameClaim ( )
if usernameClaimName == "" {
return subject , subject , nil
}
// If the upstream username claim is configured to be the special "email" claim and the upstream "email_verified"
// claim is present, then validate that the "email_verified" claim is true.
emailVerifiedAsInterface , ok := idTokenClaims [ emailVerifiedClaimName ]
if usernameClaimName == emailClaimName && ok {
emailVerified , ok := emailVerifiedAsInterface . ( bool )
if ! ok {
plog . Warning (
"username claim configured as \"email\" and upstream email_verified claim is not a boolean" ,
"upstreamName" , upstreamIDPConfig . GetName ( ) ,
"configuredUsernameClaim" , usernameClaimName ,
"emailVerifiedClaim" , emailVerifiedAsInterface ,
)
2021-08-18 19:06:46 +00:00
return "" , "" , emailVerifiedClaimInvalidFormatErr
2021-08-12 17:00:18 +00:00
}
if ! emailVerified {
plog . Warning (
"username claim configured as \"email\" and upstream email_verified claim has false value" ,
"upstreamName" , upstreamIDPConfig . GetName ( ) ,
"configuredUsernameClaim" , usernameClaimName ,
)
2021-08-18 19:06:46 +00:00
return "" , "" , emailVerifiedClaimFalseErr
2021-08-12 17:00:18 +00:00
}
}
2022-01-07 23:04:58 +00:00
username , err := ExtractStringClaimValue ( usernameClaimName , upstreamIDPConfig . GetName ( ) , idTokenClaims )
2021-08-17 20:14:09 +00:00
if err != nil {
return "" , "" , err
}
return subject , username , nil
}
2022-01-07 23:04:58 +00:00
func ExtractStringClaimValue ( claimName string , upstreamIDPName string , idTokenClaims map [ string ] interface { } ) ( string , error ) {
2021-08-17 20:14:09 +00:00
value , ok := idTokenClaims [ claimName ]
2021-08-12 17:00:18 +00:00
if ! ok {
plog . Warning (
2021-08-17 20:14:09 +00:00
"required claim in upstream ID token missing" ,
"upstreamName" , upstreamIDPName ,
"claimName" , claimName ,
2021-08-12 17:00:18 +00:00
)
2021-08-18 19:06:46 +00:00
return "" , requiredClaimMissingErr
2021-08-12 17:00:18 +00:00
}
2021-08-17 20:14:09 +00:00
valueAsString , ok := value . ( string )
2021-08-12 17:00:18 +00:00
if ! ok {
plog . Warning (
2021-08-17 20:14:09 +00:00
"required claim in upstream ID token is not a string value" ,
"upstreamName" , upstreamIDPName ,
"claimName" , claimName ,
2021-08-12 17:00:18 +00:00
)
2021-08-18 19:06:46 +00:00
return "" , requiredClaimInvalidFormatErr
2021-08-12 17:00:18 +00:00
}
2021-08-17 20:14:09 +00:00
if valueAsString == "" {
plog . Warning (
"required claim in upstream ID token has an empty string value" ,
"upstreamName" , upstreamIDPName ,
"claimName" , claimName ,
)
2021-08-18 19:06:46 +00:00
return "" , requiredClaimEmptyErr
2021-08-17 20:14:09 +00:00
}
return valueAsString , nil
2021-08-12 17:00:18 +00:00
}
2023-05-08 21:07:38 +00:00
func DownstreamSubjectFromUpstreamLDAP ( ldapUpstream upstreamprovider . UpstreamLDAPIdentityProviderI , authenticateResponse * authenticators . Response ) string {
2022-04-29 23:01:51 +00:00
ldapURL := * ldapUpstream . GetURL ( )
return DownstreamLDAPSubject ( authenticateResponse . User . GetUID ( ) , ldapURL )
}
2021-10-25 21:25:43 +00:00
func DownstreamLDAPSubject ( uid string , ldapURL url . URL ) string {
q := ldapURL . Query ( )
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
q . Set ( oidcapi . IDTokenClaimSubject , uid )
2021-10-25 21:25:43 +00:00
ldapURL . RawQuery = q . Encode ( )
return ldapURL . String ( )
}
2021-12-16 20:53:49 +00:00
func downstreamSubjectFromUpstreamOIDC ( upstreamIssuerAsString string , upstreamSubject string ) string {
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
return fmt . Sprintf ( "%s?%s=%s" , upstreamIssuerAsString , oidcapi . IDTokenClaimSubject , url . QueryEscape ( upstreamSubject ) )
2021-08-12 17:00:18 +00:00
}
2022-01-15 00:38:21 +00:00
// GetGroupsFromUpstreamIDToken returns mapped group names coerced into a slice of strings.
// It returns nil when there is no configured groups claim name, or then when the configured claim name is not found
// in the provided map of claims. It returns an error when the claim exists but its value cannot be parsed.
func GetGroupsFromUpstreamIDToken (
2023-05-08 21:07:38 +00:00
upstreamIDPConfig upstreamprovider . UpstreamOIDCIdentityProviderI ,
2021-08-12 17:00:18 +00:00
idTokenClaims map [ string ] interface { } ,
) ( [ ] string , error ) {
groupsClaimName := upstreamIDPConfig . GetGroupsClaim ( )
if groupsClaimName == "" {
return nil , nil
}
groupsAsInterface , ok := idTokenClaims [ groupsClaimName ]
if ! ok {
plog . Warning (
"no groups claim in upstream ID token" ,
"upstreamName" , upstreamIDPConfig . GetName ( ) ,
"configuredGroupsClaim" , groupsClaimName ,
)
return nil , nil // the upstream IDP may have omitted the claim if the user has no groups
}
groupsAsArray , okAsArray := extractGroups ( groupsAsInterface )
if ! okAsArray {
plog . Warning (
"groups claim in upstream ID token has invalid format" ,
"upstreamName" , upstreamIDPConfig . GetName ( ) ,
"configuredGroupsClaim" , groupsClaimName ,
)
2021-08-18 19:06:46 +00:00
return nil , requiredClaimInvalidFormatErr
2021-08-12 17:00:18 +00:00
}
return groupsAsArray , nil
}
func extractGroups ( groupsAsInterface interface { } ) ( [ ] string , bool ) {
groupsAsString , okAsString := groupsAsInterface . ( string )
if okAsString {
return [ ] string { groupsAsString } , true
}
groupsAsStringArray , okAsStringArray := groupsAsInterface . ( [ ] string )
if okAsStringArray {
return groupsAsStringArray , true
}
groupsAsInterfaceArray , okAsArray := groupsAsInterface . ( [ ] interface { } )
if ! okAsArray {
return nil , false
}
var groupsAsStrings [ ] string
for _ , groupAsInterface := range groupsAsInterfaceArray {
groupAsString , okAsString := groupAsInterface . ( string )
if ! okAsString {
return nil , false
}
if groupAsString != "" {
groupsAsStrings = append ( groupsAsStrings , groupAsString )
}
}
return groupsAsStrings , true
}