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"
This commit is contained in:
parent
6b29082c27
commit
22fbced863
@ -15,11 +15,68 @@ const (
|
|||||||
// or an LDAPIdentityProvider.
|
// or an LDAPIdentityProvider.
|
||||||
AuthorizePasswordHeaderName = "Pinniped-Password" //nolint:gosec // this is not a credential
|
AuthorizePasswordHeaderName = "Pinniped-Password" //nolint:gosec // this is not a credential
|
||||||
|
|
||||||
// AuthorizeUpstreamIDPNameParamName is the name of the HTTP request parameter which can be used to help select which
|
// AuthorizeUpstreamIDPNameParamName is the name of the HTTP request parameter which can be used to help select
|
||||||
// identity provider should be used for authentication by sending the name of the desired identity provider.
|
// which identity provider should be used for authentication by sending the name of the desired identity provider.
|
||||||
AuthorizeUpstreamIDPNameParamName = "pinniped_idp_name"
|
AuthorizeUpstreamIDPNameParamName = "pinniped_idp_name"
|
||||||
|
|
||||||
// AuthorizeUpstreamIDPTypeParamName is the name of the HTTP request parameter which can be used to help select which
|
// AuthorizeUpstreamIDPTypeParamName is the name of the HTTP request parameter which can be used to help select
|
||||||
// identity provider should be used for authentication by sending the type of the desired identity provider.
|
// which identity provider should be used for authentication by sending the type of the desired identity provider.
|
||||||
AuthorizeUpstreamIDPTypeParamName = "pinniped_idp_type"
|
AuthorizeUpstreamIDPTypeParamName = "pinniped_idp_type"
|
||||||
|
|
||||||
|
// IDTokenClaimIssuer is name of the issuer claim defined by the OIDC spec.
|
||||||
|
IDTokenClaimIssuer = "iss"
|
||||||
|
|
||||||
|
// IDTokenClaimSubject is name of the subject claim defined by the OIDC spec.
|
||||||
|
IDTokenClaimSubject = "sub"
|
||||||
|
|
||||||
|
// IDTokenClaimAuthorizedParty is name of the authorized party claim defined by the OIDC spec.
|
||||||
|
IDTokenClaimAuthorizedParty = "azp"
|
||||||
|
|
||||||
|
// IDTokenClaimUsername is the name of a custom claim in the downstream ID token whose value will contain the user's
|
||||||
|
// username which was mapped from the upstream identity provider.
|
||||||
|
IDTokenClaimUsername = "username"
|
||||||
|
|
||||||
|
// IDTokenClaimGroups is the name of a custom claim in the downstream ID token whose value will contain the user's
|
||||||
|
// group names which were mapped from the upstream identity provider.
|
||||||
|
IDTokenClaimGroups = "groups"
|
||||||
|
|
||||||
|
// GrantTypeAuthorizationCode is the name of the grant type for authorization code flows defined by the OIDC spec.
|
||||||
|
GrantTypeAuthorizationCode = "authorization_code"
|
||||||
|
|
||||||
|
// GrantTypeRefreshToken is the name of the grant type for refresh flow defined by the OIDC spec.
|
||||||
|
GrantTypeRefreshToken = "refresh_token"
|
||||||
|
|
||||||
|
// GrantTypeTokenExchange is the name of a custom grant type for RFC8693 token exchanges.
|
||||||
|
GrantTypeTokenExchange = "urn:ietf:params:oauth:grant-type:token-exchange" //nolint:gosec // this is not a credential
|
||||||
|
|
||||||
|
// ScopeOpenID is name of the openid scope defined by the OIDC spec.
|
||||||
|
ScopeOpenID = "openid"
|
||||||
|
|
||||||
|
// ScopeOfflineAccess is name of the offline access scope defined by the OIDC spec, used for requesting refresh
|
||||||
|
// tokens.
|
||||||
|
ScopeOfflineAccess = "offline_access"
|
||||||
|
|
||||||
|
// ScopeEmail is name of the email scope defined by the OIDC spec.
|
||||||
|
ScopeEmail = "email"
|
||||||
|
|
||||||
|
// ScopeProfile is name of the profile scope defined by the OIDC spec.
|
||||||
|
ScopeProfile = "profile"
|
||||||
|
|
||||||
|
// ScopeUsername is the name of a custom scope that determines whether the username claim will be returned inside
|
||||||
|
// ID tokens.
|
||||||
|
ScopeUsername = "username"
|
||||||
|
|
||||||
|
// ScopeGroups is the name of a custom scope that determines whether the groups claim will be returned inside
|
||||||
|
// ID tokens.
|
||||||
|
ScopeGroups = "groups"
|
||||||
|
|
||||||
|
// ScopeRequestAudience is the name of a custom scope that determines whether a RFC8693 token exchange is allowed to
|
||||||
|
// be used to request a different audience.
|
||||||
|
ScopeRequestAudience = "pinniped:request-audience"
|
||||||
|
|
||||||
|
// ClientIDPinnipedCLI is the client ID of the statically defined public OIDC client which is used by the CLI.
|
||||||
|
ClientIDPinnipedCLI = "pinniped-cli"
|
||||||
|
|
||||||
|
// ClientIDRequiredOIDCClientPrefix is the required prefix for the metadata.name of OIDCClient CRs.
|
||||||
|
ClientIDRequiredOIDCClientPrefix = "client.oauth.pinniped.dev-"
|
||||||
)
|
)
|
||||||
|
@ -17,7 +17,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/coreos/go-oidc/v3/oidc"
|
coreosoidc "github.com/coreos/go-oidc/v3/oidc"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
|
clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
|
||||||
@ -28,6 +28,7 @@ import (
|
|||||||
conciergev1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1"
|
conciergev1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1"
|
||||||
configv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/config/v1alpha1"
|
configv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/config/v1alpha1"
|
||||||
idpdiscoveryv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idpdiscovery/v1alpha1"
|
idpdiscoveryv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idpdiscovery/v1alpha1"
|
||||||
|
oidcapi "go.pinniped.dev/generated/latest/apis/supervisor/oidc"
|
||||||
conciergeclientset "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned"
|
conciergeclientset "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned"
|
||||||
"go.pinniped.dev/internal/groupsuffix"
|
"go.pinniped.dev/internal/groupsuffix"
|
||||||
"go.pinniped.dev/internal/net/phttp"
|
"go.pinniped.dev/internal/net/phttp"
|
||||||
@ -126,9 +127,9 @@ func kubeconfigCommand(deps kubeconfigDeps) *cobra.Command {
|
|||||||
f.Var(&flags.concierge.mode, "concierge-mode", "Concierge mode of operation")
|
f.Var(&flags.concierge.mode, "concierge-mode", "Concierge mode of operation")
|
||||||
|
|
||||||
f.StringVar(&flags.oidc.issuer, "oidc-issuer", "", "OpenID Connect issuer URL (default: autodiscover)")
|
f.StringVar(&flags.oidc.issuer, "oidc-issuer", "", "OpenID Connect issuer URL (default: autodiscover)")
|
||||||
f.StringVar(&flags.oidc.clientID, "oidc-client-id", "pinniped-cli", "OpenID Connect client ID (default: autodiscover)")
|
f.StringVar(&flags.oidc.clientID, "oidc-client-id", oidcapi.ClientIDPinnipedCLI, "OpenID Connect client ID (default: autodiscover)")
|
||||||
f.Uint16Var(&flags.oidc.listenPort, "oidc-listen-port", 0, "TCP port for localhost listener (authorization code flow only)")
|
f.Uint16Var(&flags.oidc.listenPort, "oidc-listen-port", 0, "TCP port for localhost listener (authorization code flow only)")
|
||||||
f.StringSliceVar(&flags.oidc.scopes, "oidc-scopes", []string{oidc.ScopeOfflineAccess, oidc.ScopeOpenID, "pinniped:request-audience"}, "OpenID Connect scopes to request during login")
|
f.StringSliceVar(&flags.oidc.scopes, "oidc-scopes", []string{oidcapi.ScopeOfflineAccess, oidcapi.ScopeOpenID, oidcapi.ScopeRequestAudience, oidcapi.ScopeUsername, oidcapi.ScopeGroups}, "OpenID Connect scopes to request during login")
|
||||||
f.BoolVar(&flags.oidc.skipBrowser, "oidc-skip-browser", false, "During OpenID Connect login, skip opening the browser (just print the URL)")
|
f.BoolVar(&flags.oidc.skipBrowser, "oidc-skip-browser", false, "During OpenID Connect login, skip opening the browser (just print the URL)")
|
||||||
f.BoolVar(&flags.oidc.skipListen, "oidc-skip-listen", false, "During OpenID Connect login, skip starting a localhost callback listener (manual copy/paste flow only)")
|
f.BoolVar(&flags.oidc.skipListen, "oidc-skip-listen", false, "During OpenID Connect login, skip starting a localhost callback listener (manual copy/paste flow only)")
|
||||||
f.StringVar(&flags.oidc.sessionCachePath, "oidc-session-cache", "", "Path to OpenID Connect session cache file")
|
f.StringVar(&flags.oidc.sessionCachePath, "oidc-session-cache", "", "Path to OpenID Connect session cache file")
|
||||||
@ -784,7 +785,7 @@ func newDiscoveryHTTPClient(caBundleFlag caBundleFlag) (*http.Client, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func discoverIDPsDiscoveryEndpointURL(ctx context.Context, issuer string, httpClient *http.Client) (string, error) {
|
func discoverIDPsDiscoveryEndpointURL(ctx context.Context, issuer string, httpClient *http.Client) (string, error) {
|
||||||
discoveredProvider, err := oidc.NewProvider(oidc.ClientContext(ctx, httpClient), issuer)
|
discoveredProvider, err := coreosoidc.NewProvider(coreosoidc.ClientContext(ctx, httpClient), issuer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("while fetching OIDC discovery data from issuer: %w", err)
|
return "", fmt.Errorf("while fetching OIDC discovery data from issuer: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -142,7 +142,7 @@ func TestGetKubeconfig(t *testing.T) {
|
|||||||
--oidc-issuer string OpenID Connect issuer URL (default: autodiscover)
|
--oidc-issuer string OpenID Connect issuer URL (default: autodiscover)
|
||||||
--oidc-listen-port uint16 TCP port for localhost listener (authorization code flow only)
|
--oidc-listen-port uint16 TCP port for localhost listener (authorization code flow only)
|
||||||
--oidc-request-audience string Request a token with an alternate audience using RFC8693 token exchange
|
--oidc-request-audience string Request a token with an alternate audience using RFC8693 token exchange
|
||||||
--oidc-scopes strings OpenID Connect scopes to request during login (default [offline_access,openid,pinniped:request-audience])
|
--oidc-scopes strings OpenID Connect scopes to request during login (default [offline_access,openid,pinniped:request-audience,username,groups])
|
||||||
--oidc-session-cache string Path to OpenID Connect session cache file
|
--oidc-session-cache string Path to OpenID Connect session cache file
|
||||||
--oidc-skip-browser During OpenID Connect login, skip opening the browser (just print the URL)
|
--oidc-skip-browser During OpenID Connect login, skip opening the browser (just print the URL)
|
||||||
-o, --output string Output file path (default: stdout)
|
-o, --output string Output file path (default: stdout)
|
||||||
@ -1290,7 +1290,7 @@ func TestGetKubeconfig(t *testing.T) {
|
|||||||
- oidc
|
- oidc
|
||||||
- --issuer=%s
|
- --issuer=%s
|
||||||
- --client-id=pinniped-cli
|
- --client-id=pinniped-cli
|
||||||
- --scopes=offline_access,openid,pinniped:request-audience
|
- --scopes=offline_access,openid,pinniped:request-audience,username,groups
|
||||||
- --ca-bundle-data=%s
|
- --ca-bundle-data=%s
|
||||||
- --upstream-identity-provider-name=some-ldap-idp
|
- --upstream-identity-provider-name=some-ldap-idp
|
||||||
- --upstream-identity-provider-type=ldap
|
- --upstream-identity-provider-type=ldap
|
||||||
@ -1496,7 +1496,7 @@ func TestGetKubeconfig(t *testing.T) {
|
|||||||
- --concierge-ca-bundle-data=ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ==
|
- --concierge-ca-bundle-data=ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ==
|
||||||
- --issuer=%s
|
- --issuer=%s
|
||||||
- --client-id=pinniped-cli
|
- --client-id=pinniped-cli
|
||||||
- --scopes=offline_access,openid,pinniped:request-audience
|
- --scopes=offline_access,openid,pinniped:request-audience,username,groups
|
||||||
- --ca-bundle-data=%s
|
- --ca-bundle-data=%s
|
||||||
- --request-audience=test-audience
|
- --request-audience=test-audience
|
||||||
command: '.../path/to/pinniped'
|
command: '.../path/to/pinniped'
|
||||||
@ -1577,7 +1577,7 @@ func TestGetKubeconfig(t *testing.T) {
|
|||||||
- --credential-cache=/path/to/cache/dir/credentials.yaml
|
- --credential-cache=/path/to/cache/dir/credentials.yaml
|
||||||
- --issuer=%s
|
- --issuer=%s
|
||||||
- --client-id=pinniped-cli
|
- --client-id=pinniped-cli
|
||||||
- --scopes=offline_access,openid,pinniped:request-audience
|
- --scopes=offline_access,openid,pinniped:request-audience,username,groups
|
||||||
- --skip-browser
|
- --skip-browser
|
||||||
- --skip-listen
|
- --skip-listen
|
||||||
- --listen-port=1234
|
- --listen-port=1234
|
||||||
@ -1695,7 +1695,7 @@ func TestGetKubeconfig(t *testing.T) {
|
|||||||
- --concierge-ca-bundle-data=%s
|
- --concierge-ca-bundle-data=%s
|
||||||
- --issuer=%s
|
- --issuer=%s
|
||||||
- --client-id=pinniped-cli
|
- --client-id=pinniped-cli
|
||||||
- --scopes=offline_access,openid,pinniped:request-audience
|
- --scopes=offline_access,openid,pinniped:request-audience,username,groups
|
||||||
- --ca-bundle-data=%s
|
- --ca-bundle-data=%s
|
||||||
- --request-audience=test-audience
|
- --request-audience=test-audience
|
||||||
command: '.../path/to/pinniped'
|
command: '.../path/to/pinniped'
|
||||||
@ -1804,7 +1804,7 @@ func TestGetKubeconfig(t *testing.T) {
|
|||||||
- --concierge-ca-bundle-data=dGVzdC1jb25jaWVyZ2UtY2E=
|
- --concierge-ca-bundle-data=dGVzdC1jb25jaWVyZ2UtY2E=
|
||||||
- --issuer=%s
|
- --issuer=%s
|
||||||
- --client-id=pinniped-cli
|
- --client-id=pinniped-cli
|
||||||
- --scopes=offline_access,openid,pinniped:request-audience
|
- --scopes=offline_access,openid,pinniped:request-audience,username,groups
|
||||||
- --ca-bundle-data=%s
|
- --ca-bundle-data=%s
|
||||||
- --request-audience=test-audience
|
- --request-audience=test-audience
|
||||||
command: '.../path/to/pinniped'
|
command: '.../path/to/pinniped'
|
||||||
@ -1881,7 +1881,7 @@ func TestGetKubeconfig(t *testing.T) {
|
|||||||
- --concierge-ca-bundle-data=ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ==
|
- --concierge-ca-bundle-data=ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ==
|
||||||
- --issuer=%s
|
- --issuer=%s
|
||||||
- --client-id=pinniped-cli
|
- --client-id=pinniped-cli
|
||||||
- --scopes=offline_access,openid,pinniped:request-audience
|
- --scopes=offline_access,openid,pinniped:request-audience,username,groups
|
||||||
- --ca-bundle-data=%s
|
- --ca-bundle-data=%s
|
||||||
- --request-audience=test-audience
|
- --request-audience=test-audience
|
||||||
- --upstream-identity-provider-name=some-ldap-idp
|
- --upstream-identity-provider-name=some-ldap-idp
|
||||||
@ -1960,7 +1960,7 @@ func TestGetKubeconfig(t *testing.T) {
|
|||||||
- --concierge-ca-bundle-data=ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ==
|
- --concierge-ca-bundle-data=ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ==
|
||||||
- --issuer=%s
|
- --issuer=%s
|
||||||
- --client-id=pinniped-cli
|
- --client-id=pinniped-cli
|
||||||
- --scopes=offline_access,openid,pinniped:request-audience
|
- --scopes=offline_access,openid,pinniped:request-audience,username,groups
|
||||||
- --ca-bundle-data=%s
|
- --ca-bundle-data=%s
|
||||||
- --request-audience=test-audience
|
- --request-audience=test-audience
|
||||||
- --upstream-identity-provider-name=some-oidc-idp
|
- --upstream-identity-provider-name=some-oidc-idp
|
||||||
@ -2037,7 +2037,7 @@ func TestGetKubeconfig(t *testing.T) {
|
|||||||
- --concierge-ca-bundle-data=ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ==
|
- --concierge-ca-bundle-data=ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ==
|
||||||
- --issuer=%s
|
- --issuer=%s
|
||||||
- --client-id=pinniped-cli
|
- --client-id=pinniped-cli
|
||||||
- --scopes=offline_access,openid,pinniped:request-audience
|
- --scopes=offline_access,openid,pinniped:request-audience,username,groups
|
||||||
- --ca-bundle-data=%s
|
- --ca-bundle-data=%s
|
||||||
- --request-audience=test-audience
|
- --request-audience=test-audience
|
||||||
command: '.../path/to/pinniped'
|
command: '.../path/to/pinniped'
|
||||||
@ -2110,7 +2110,7 @@ func TestGetKubeconfig(t *testing.T) {
|
|||||||
- --concierge-ca-bundle-data=ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ==
|
- --concierge-ca-bundle-data=ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ==
|
||||||
- --issuer=%s
|
- --issuer=%s
|
||||||
- --client-id=pinniped-cli
|
- --client-id=pinniped-cli
|
||||||
- --scopes=offline_access,openid,pinniped:request-audience
|
- --scopes=offline_access,openid,pinniped:request-audience,username,groups
|
||||||
- --ca-bundle-data=%s
|
- --ca-bundle-data=%s
|
||||||
- --request-audience=test-audience
|
- --request-audience=test-audience
|
||||||
command: '.../path/to/pinniped'
|
command: '.../path/to/pinniped'
|
||||||
@ -2190,7 +2190,7 @@ func TestGetKubeconfig(t *testing.T) {
|
|||||||
- --concierge-ca-bundle-data=ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ==
|
- --concierge-ca-bundle-data=ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ==
|
||||||
- --issuer=%s
|
- --issuer=%s
|
||||||
- --client-id=pinniped-cli
|
- --client-id=pinniped-cli
|
||||||
- --scopes=offline_access,openid,pinniped:request-audience
|
- --scopes=offline_access,openid,pinniped:request-audience,username,groups
|
||||||
- --ca-bundle-data=%s
|
- --ca-bundle-data=%s
|
||||||
- --request-audience=test-audience
|
- --request-audience=test-audience
|
||||||
command: '.../path/to/pinniped'
|
command: '.../path/to/pinniped'
|
||||||
@ -2265,7 +2265,7 @@ func TestGetKubeconfig(t *testing.T) {
|
|||||||
- --concierge-ca-bundle-data=ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ==
|
- --concierge-ca-bundle-data=ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ==
|
||||||
- --issuer=%s
|
- --issuer=%s
|
||||||
- --client-id=pinniped-cli
|
- --client-id=pinniped-cli
|
||||||
- --scopes=offline_access,openid,pinniped:request-audience
|
- --scopes=offline_access,openid,pinniped:request-audience,username,groups
|
||||||
- --ca-bundle-data=%s
|
- --ca-bundle-data=%s
|
||||||
- --request-audience=test-audience
|
- --request-audience=test-audience
|
||||||
- --upstream-identity-provider-name=some-oidc-idp
|
- --upstream-identity-provider-name=some-oidc-idp
|
||||||
@ -2348,7 +2348,7 @@ func TestGetKubeconfig(t *testing.T) {
|
|||||||
- --concierge-ca-bundle-data=ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ==
|
- --concierge-ca-bundle-data=ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ==
|
||||||
- --issuer=%s
|
- --issuer=%s
|
||||||
- --client-id=pinniped-cli
|
- --client-id=pinniped-cli
|
||||||
- --scopes=offline_access,openid,pinniped:request-audience
|
- --scopes=offline_access,openid,pinniped:request-audience,username,groups
|
||||||
- --ca-bundle-data=%s
|
- --ca-bundle-data=%s
|
||||||
- --request-audience=test-audience
|
- --request-audience=test-audience
|
||||||
- --upstream-identity-provider-name=some-oidc-idp
|
- --upstream-identity-provider-name=some-oidc-idp
|
||||||
@ -2408,7 +2408,7 @@ func TestGetKubeconfig(t *testing.T) {
|
|||||||
- oidc
|
- oidc
|
||||||
- --issuer=%s
|
- --issuer=%s
|
||||||
- --client-id=pinniped-cli
|
- --client-id=pinniped-cli
|
||||||
- --scopes=offline_access,openid,pinniped:request-audience
|
- --scopes=offline_access,openid,pinniped:request-audience,username,groups
|
||||||
- --ca-bundle-data=%s
|
- --ca-bundle-data=%s
|
||||||
- --upstream-identity-provider-name=some-ldap-idp
|
- --upstream-identity-provider-name=some-ldap-idp
|
||||||
- --upstream-identity-provider-type=ldap
|
- --upstream-identity-provider-type=ldap
|
||||||
@ -2469,7 +2469,7 @@ func TestGetKubeconfig(t *testing.T) {
|
|||||||
- oidc
|
- oidc
|
||||||
- --issuer=%s
|
- --issuer=%s
|
||||||
- --client-id=pinniped-cli
|
- --client-id=pinniped-cli
|
||||||
- --scopes=offline_access,openid,pinniped:request-audience
|
- --scopes=offline_access,openid,pinniped:request-audience,username,groups
|
||||||
- --ca-bundle-data=%s
|
- --ca-bundle-data=%s
|
||||||
- --upstream-identity-provider-name=some-ldap-idp
|
- --upstream-identity-provider-name=some-ldap-idp
|
||||||
- --upstream-identity-provider-type=ldap
|
- --upstream-identity-provider-type=ldap
|
||||||
@ -2530,7 +2530,7 @@ func TestGetKubeconfig(t *testing.T) {
|
|||||||
- oidc
|
- oidc
|
||||||
- --issuer=%s
|
- --issuer=%s
|
||||||
- --client-id=pinniped-cli
|
- --client-id=pinniped-cli
|
||||||
- --scopes=offline_access,openid,pinniped:request-audience
|
- --scopes=offline_access,openid,pinniped:request-audience,username,groups
|
||||||
- --ca-bundle-data=%s
|
- --ca-bundle-data=%s
|
||||||
- --upstream-identity-provider-name=some-ldap-idp
|
- --upstream-identity-provider-name=some-ldap-idp
|
||||||
- --upstream-identity-provider-type=ldap
|
- --upstream-identity-provider-type=ldap
|
||||||
@ -2592,7 +2592,7 @@ func TestGetKubeconfig(t *testing.T) {
|
|||||||
- oidc
|
- oidc
|
||||||
- --issuer=%s
|
- --issuer=%s
|
||||||
- --client-id=pinniped-cli
|
- --client-id=pinniped-cli
|
||||||
- --scopes=offline_access,openid,pinniped:request-audience
|
- --scopes=offline_access,openid,pinniped:request-audience,username,groups
|
||||||
- --ca-bundle-data=%s
|
- --ca-bundle-data=%s
|
||||||
- --upstream-identity-provider-name=some-ldap-idp
|
- --upstream-identity-provider-name=some-ldap-idp
|
||||||
- --upstream-identity-provider-type=ldap
|
- --upstream-identity-provider-type=ldap
|
||||||
@ -2654,7 +2654,7 @@ func TestGetKubeconfig(t *testing.T) {
|
|||||||
- oidc
|
- oidc
|
||||||
- --issuer=%s
|
- --issuer=%s
|
||||||
- --client-id=pinniped-cli
|
- --client-id=pinniped-cli
|
||||||
- --scopes=offline_access,openid,pinniped:request-audience
|
- --scopes=offline_access,openid,pinniped:request-audience,username,groups
|
||||||
- --ca-bundle-data=%s
|
- --ca-bundle-data=%s
|
||||||
- --upstream-identity-provider-name=some-ldap-idp
|
- --upstream-identity-provider-name=some-ldap-idp
|
||||||
- --upstream-identity-provider-type=ldap
|
- --upstream-identity-provider-type=ldap
|
||||||
@ -2715,7 +2715,7 @@ func TestGetKubeconfig(t *testing.T) {
|
|||||||
- oidc
|
- oidc
|
||||||
- --issuer=%s
|
- --issuer=%s
|
||||||
- --client-id=pinniped-cli
|
- --client-id=pinniped-cli
|
||||||
- --scopes=offline_access,openid,pinniped:request-audience
|
- --scopes=offline_access,openid,pinniped:request-audience,username,groups
|
||||||
- --ca-bundle-data=%s
|
- --ca-bundle-data=%s
|
||||||
- --upstream-identity-provider-name=some-ldap-idp
|
- --upstream-identity-provider-name=some-ldap-idp
|
||||||
- --upstream-identity-provider-type=ldap
|
- --upstream-identity-provider-type=ldap
|
||||||
@ -2775,7 +2775,7 @@ func TestGetKubeconfig(t *testing.T) {
|
|||||||
- oidc
|
- oidc
|
||||||
- --issuer=%s
|
- --issuer=%s
|
||||||
- --client-id=pinniped-cli
|
- --client-id=pinniped-cli
|
||||||
- --scopes=offline_access,openid,pinniped:request-audience
|
- --scopes=offline_access,openid,pinniped:request-audience,username,groups
|
||||||
- --ca-bundle-data=%s
|
- --ca-bundle-data=%s
|
||||||
- --upstream-identity-provider-name=some-ldap-idp
|
- --upstream-identity-provider-name=some-ldap-idp
|
||||||
- --upstream-identity-provider-type=ldap
|
- --upstream-identity-provider-type=ldap
|
||||||
|
@ -16,12 +16,12 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/coreos/go-oidc/v3/oidc"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
clientauthv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
|
clientauthv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
|
||||||
|
|
||||||
idpdiscoveryv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idpdiscovery/v1alpha1"
|
idpdiscoveryv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idpdiscovery/v1alpha1"
|
||||||
|
oidcapi "go.pinniped.dev/generated/latest/apis/supervisor/oidc"
|
||||||
"go.pinniped.dev/internal/execcredcache"
|
"go.pinniped.dev/internal/execcredcache"
|
||||||
"go.pinniped.dev/internal/groupsuffix"
|
"go.pinniped.dev/internal/groupsuffix"
|
||||||
"go.pinniped.dev/internal/net/phttp"
|
"go.pinniped.dev/internal/net/phttp"
|
||||||
@ -98,9 +98,9 @@ func oidcLoginCommand(deps oidcLoginCommandDeps) *cobra.Command {
|
|||||||
conciergeNamespace string // unused now
|
conciergeNamespace string // unused now
|
||||||
)
|
)
|
||||||
cmd.Flags().StringVar(&flags.issuer, "issuer", "", "OpenID Connect issuer URL")
|
cmd.Flags().StringVar(&flags.issuer, "issuer", "", "OpenID Connect issuer URL")
|
||||||
cmd.Flags().StringVar(&flags.clientID, "client-id", "pinniped-cli", "OpenID Connect client ID")
|
cmd.Flags().StringVar(&flags.clientID, "client-id", oidcapi.ClientIDPinnipedCLI, "OpenID Connect client ID")
|
||||||
cmd.Flags().Uint16Var(&flags.listenPort, "listen-port", 0, "TCP port for localhost listener (authorization code flow only)")
|
cmd.Flags().Uint16Var(&flags.listenPort, "listen-port", 0, "TCP port for localhost listener (authorization code flow only)")
|
||||||
cmd.Flags().StringSliceVar(&flags.scopes, "scopes", []string{oidc.ScopeOfflineAccess, oidc.ScopeOpenID, "pinniped:request-audience"}, "OIDC scopes to request during login")
|
cmd.Flags().StringSliceVar(&flags.scopes, "scopes", []string{oidcapi.ScopeOfflineAccess, oidcapi.ScopeOpenID, oidcapi.ScopeRequestAudience, oidcapi.ScopeUsername, oidcapi.ScopeGroups}, "OIDC scopes to request during login")
|
||||||
cmd.Flags().BoolVar(&flags.skipBrowser, "skip-browser", false, "Skip opening the browser (just print the URL)")
|
cmd.Flags().BoolVar(&flags.skipBrowser, "skip-browser", false, "Skip opening the browser (just print the URL)")
|
||||||
cmd.Flags().BoolVar(&flags.skipListen, "skip-listen", false, "Skip starting a localhost callback listener (manual copy/paste flow only)")
|
cmd.Flags().BoolVar(&flags.skipListen, "skip-listen", false, "Skip starting a localhost callback listener (manual copy/paste flow only)")
|
||||||
cmd.Flags().StringVar(&flags.sessionCachePath, "session-cache", filepath.Join(mustGetConfigDir(), "sessions.yaml"), "Path to session cache file")
|
cmd.Flags().StringVar(&flags.sessionCachePath, "session-cache", filepath.Join(mustGetConfigDir(), "sessions.yaml"), "Path to session cache file")
|
||||||
|
@ -80,7 +80,7 @@ func TestLoginOIDCCommand(t *testing.T) {
|
|||||||
--issuer string OpenID Connect issuer URL
|
--issuer string OpenID Connect issuer URL
|
||||||
--listen-port uint16 TCP port for localhost listener (authorization code flow only)
|
--listen-port uint16 TCP port for localhost listener (authorization code flow only)
|
||||||
--request-audience string Request a token with an alternate audience using RFC8693 token exchange
|
--request-audience string Request a token with an alternate audience using RFC8693 token exchange
|
||||||
--scopes strings OIDC scopes to request during login (default [offline_access,openid,pinniped:request-audience])
|
--scopes strings OIDC scopes to request during login (default [offline_access,openid,pinniped:request-audience,username,groups])
|
||||||
--session-cache string Path to session cache file (default "` + cfgDir + `/sessions.yaml")
|
--session-cache string Path to session cache file (default "` + cfgDir + `/sessions.yaml")
|
||||||
--skip-browser Skip opening the browser (just print the URL)
|
--skip-browser Skip opening the browser (just print the URL)
|
||||||
--upstream-identity-provider-flow string The type of client flow to use with the upstream identity provider during login with a Supervisor (e.g. 'browser_authcode', 'cli_password')
|
--upstream-identity-provider-flow string The type of client flow to use with the upstream identity provider during login with a Supervisor (e.g. 'browser_authcode', 'cli_password')
|
||||||
|
@ -15,11 +15,68 @@ const (
|
|||||||
// or an LDAPIdentityProvider.
|
// or an LDAPIdentityProvider.
|
||||||
AuthorizePasswordHeaderName = "Pinniped-Password" //nolint:gosec // this is not a credential
|
AuthorizePasswordHeaderName = "Pinniped-Password" //nolint:gosec // this is not a credential
|
||||||
|
|
||||||
// AuthorizeUpstreamIDPNameParamName is the name of the HTTP request parameter which can be used to help select which
|
// AuthorizeUpstreamIDPNameParamName is the name of the HTTP request parameter which can be used to help select
|
||||||
// identity provider should be used for authentication by sending the name of the desired identity provider.
|
// which identity provider should be used for authentication by sending the name of the desired identity provider.
|
||||||
AuthorizeUpstreamIDPNameParamName = "pinniped_idp_name"
|
AuthorizeUpstreamIDPNameParamName = "pinniped_idp_name"
|
||||||
|
|
||||||
// AuthorizeUpstreamIDPTypeParamName is the name of the HTTP request parameter which can be used to help select which
|
// AuthorizeUpstreamIDPTypeParamName is the name of the HTTP request parameter which can be used to help select
|
||||||
// identity provider should be used for authentication by sending the type of the desired identity provider.
|
// which identity provider should be used for authentication by sending the type of the desired identity provider.
|
||||||
AuthorizeUpstreamIDPTypeParamName = "pinniped_idp_type"
|
AuthorizeUpstreamIDPTypeParamName = "pinniped_idp_type"
|
||||||
|
|
||||||
|
// IDTokenClaimIssuer is name of the issuer claim defined by the OIDC spec.
|
||||||
|
IDTokenClaimIssuer = "iss"
|
||||||
|
|
||||||
|
// IDTokenClaimSubject is name of the subject claim defined by the OIDC spec.
|
||||||
|
IDTokenClaimSubject = "sub"
|
||||||
|
|
||||||
|
// IDTokenClaimAuthorizedParty is name of the authorized party claim defined by the OIDC spec.
|
||||||
|
IDTokenClaimAuthorizedParty = "azp"
|
||||||
|
|
||||||
|
// IDTokenClaimUsername is the name of a custom claim in the downstream ID token whose value will contain the user's
|
||||||
|
// username which was mapped from the upstream identity provider.
|
||||||
|
IDTokenClaimUsername = "username"
|
||||||
|
|
||||||
|
// IDTokenClaimGroups is the name of a custom claim in the downstream ID token whose value will contain the user's
|
||||||
|
// group names which were mapped from the upstream identity provider.
|
||||||
|
IDTokenClaimGroups = "groups"
|
||||||
|
|
||||||
|
// GrantTypeAuthorizationCode is the name of the grant type for authorization code flows defined by the OIDC spec.
|
||||||
|
GrantTypeAuthorizationCode = "authorization_code"
|
||||||
|
|
||||||
|
// GrantTypeRefreshToken is the name of the grant type for refresh flow defined by the OIDC spec.
|
||||||
|
GrantTypeRefreshToken = "refresh_token"
|
||||||
|
|
||||||
|
// GrantTypeTokenExchange is the name of a custom grant type for RFC8693 token exchanges.
|
||||||
|
GrantTypeTokenExchange = "urn:ietf:params:oauth:grant-type:token-exchange" //nolint:gosec // this is not a credential
|
||||||
|
|
||||||
|
// ScopeOpenID is name of the openid scope defined by the OIDC spec.
|
||||||
|
ScopeOpenID = "openid"
|
||||||
|
|
||||||
|
// ScopeOfflineAccess is name of the offline access scope defined by the OIDC spec, used for requesting refresh
|
||||||
|
// tokens.
|
||||||
|
ScopeOfflineAccess = "offline_access"
|
||||||
|
|
||||||
|
// ScopeEmail is name of the email scope defined by the OIDC spec.
|
||||||
|
ScopeEmail = "email"
|
||||||
|
|
||||||
|
// ScopeProfile is name of the profile scope defined by the OIDC spec.
|
||||||
|
ScopeProfile = "profile"
|
||||||
|
|
||||||
|
// ScopeUsername is the name of a custom scope that determines whether the username claim will be returned inside
|
||||||
|
// ID tokens.
|
||||||
|
ScopeUsername = "username"
|
||||||
|
|
||||||
|
// ScopeGroups is the name of a custom scope that determines whether the groups claim will be returned inside
|
||||||
|
// ID tokens.
|
||||||
|
ScopeGroups = "groups"
|
||||||
|
|
||||||
|
// ScopeRequestAudience is the name of a custom scope that determines whether a RFC8693 token exchange is allowed to
|
||||||
|
// be used to request a different audience.
|
||||||
|
ScopeRequestAudience = "pinniped:request-audience"
|
||||||
|
|
||||||
|
// ClientIDPinnipedCLI is the client ID of the statically defined public OIDC client which is used by the CLI.
|
||||||
|
ClientIDPinnipedCLI = "pinniped-cli"
|
||||||
|
|
||||||
|
// ClientIDRequiredOIDCClientPrefix is the required prefix for the metadata.name of OIDCClient CRs.
|
||||||
|
ClientIDRequiredOIDCClientPrefix = "client.oauth.pinniped.dev-"
|
||||||
)
|
)
|
||||||
|
@ -15,11 +15,68 @@ const (
|
|||||||
// or an LDAPIdentityProvider.
|
// or an LDAPIdentityProvider.
|
||||||
AuthorizePasswordHeaderName = "Pinniped-Password" //nolint:gosec // this is not a credential
|
AuthorizePasswordHeaderName = "Pinniped-Password" //nolint:gosec // this is not a credential
|
||||||
|
|
||||||
// AuthorizeUpstreamIDPNameParamName is the name of the HTTP request parameter which can be used to help select which
|
// AuthorizeUpstreamIDPNameParamName is the name of the HTTP request parameter which can be used to help select
|
||||||
// identity provider should be used for authentication by sending the name of the desired identity provider.
|
// which identity provider should be used for authentication by sending the name of the desired identity provider.
|
||||||
AuthorizeUpstreamIDPNameParamName = "pinniped_idp_name"
|
AuthorizeUpstreamIDPNameParamName = "pinniped_idp_name"
|
||||||
|
|
||||||
// AuthorizeUpstreamIDPTypeParamName is the name of the HTTP request parameter which can be used to help select which
|
// AuthorizeUpstreamIDPTypeParamName is the name of the HTTP request parameter which can be used to help select
|
||||||
// identity provider should be used for authentication by sending the type of the desired identity provider.
|
// which identity provider should be used for authentication by sending the type of the desired identity provider.
|
||||||
AuthorizeUpstreamIDPTypeParamName = "pinniped_idp_type"
|
AuthorizeUpstreamIDPTypeParamName = "pinniped_idp_type"
|
||||||
|
|
||||||
|
// IDTokenClaimIssuer is name of the issuer claim defined by the OIDC spec.
|
||||||
|
IDTokenClaimIssuer = "iss"
|
||||||
|
|
||||||
|
// IDTokenClaimSubject is name of the subject claim defined by the OIDC spec.
|
||||||
|
IDTokenClaimSubject = "sub"
|
||||||
|
|
||||||
|
// IDTokenClaimAuthorizedParty is name of the authorized party claim defined by the OIDC spec.
|
||||||
|
IDTokenClaimAuthorizedParty = "azp"
|
||||||
|
|
||||||
|
// IDTokenClaimUsername is the name of a custom claim in the downstream ID token whose value will contain the user's
|
||||||
|
// username which was mapped from the upstream identity provider.
|
||||||
|
IDTokenClaimUsername = "username"
|
||||||
|
|
||||||
|
// IDTokenClaimGroups is the name of a custom claim in the downstream ID token whose value will contain the user's
|
||||||
|
// group names which were mapped from the upstream identity provider.
|
||||||
|
IDTokenClaimGroups = "groups"
|
||||||
|
|
||||||
|
// GrantTypeAuthorizationCode is the name of the grant type for authorization code flows defined by the OIDC spec.
|
||||||
|
GrantTypeAuthorizationCode = "authorization_code"
|
||||||
|
|
||||||
|
// GrantTypeRefreshToken is the name of the grant type for refresh flow defined by the OIDC spec.
|
||||||
|
GrantTypeRefreshToken = "refresh_token"
|
||||||
|
|
||||||
|
// GrantTypeTokenExchange is the name of a custom grant type for RFC8693 token exchanges.
|
||||||
|
GrantTypeTokenExchange = "urn:ietf:params:oauth:grant-type:token-exchange" //nolint:gosec // this is not a credential
|
||||||
|
|
||||||
|
// ScopeOpenID is name of the openid scope defined by the OIDC spec.
|
||||||
|
ScopeOpenID = "openid"
|
||||||
|
|
||||||
|
// ScopeOfflineAccess is name of the offline access scope defined by the OIDC spec, used for requesting refresh
|
||||||
|
// tokens.
|
||||||
|
ScopeOfflineAccess = "offline_access"
|
||||||
|
|
||||||
|
// ScopeEmail is name of the email scope defined by the OIDC spec.
|
||||||
|
ScopeEmail = "email"
|
||||||
|
|
||||||
|
// ScopeProfile is name of the profile scope defined by the OIDC spec.
|
||||||
|
ScopeProfile = "profile"
|
||||||
|
|
||||||
|
// ScopeUsername is the name of a custom scope that determines whether the username claim will be returned inside
|
||||||
|
// ID tokens.
|
||||||
|
ScopeUsername = "username"
|
||||||
|
|
||||||
|
// ScopeGroups is the name of a custom scope that determines whether the groups claim will be returned inside
|
||||||
|
// ID tokens.
|
||||||
|
ScopeGroups = "groups"
|
||||||
|
|
||||||
|
// ScopeRequestAudience is the name of a custom scope that determines whether a RFC8693 token exchange is allowed to
|
||||||
|
// be used to request a different audience.
|
||||||
|
ScopeRequestAudience = "pinniped:request-audience"
|
||||||
|
|
||||||
|
// ClientIDPinnipedCLI is the client ID of the statically defined public OIDC client which is used by the CLI.
|
||||||
|
ClientIDPinnipedCLI = "pinniped-cli"
|
||||||
|
|
||||||
|
// ClientIDRequiredOIDCClientPrefix is the required prefix for the metadata.name of OIDCClient CRs.
|
||||||
|
ClientIDRequiredOIDCClientPrefix = "client.oauth.pinniped.dev-"
|
||||||
)
|
)
|
||||||
|
@ -15,11 +15,68 @@ const (
|
|||||||
// or an LDAPIdentityProvider.
|
// or an LDAPIdentityProvider.
|
||||||
AuthorizePasswordHeaderName = "Pinniped-Password" //nolint:gosec // this is not a credential
|
AuthorizePasswordHeaderName = "Pinniped-Password" //nolint:gosec // this is not a credential
|
||||||
|
|
||||||
// AuthorizeUpstreamIDPNameParamName is the name of the HTTP request parameter which can be used to help select which
|
// AuthorizeUpstreamIDPNameParamName is the name of the HTTP request parameter which can be used to help select
|
||||||
// identity provider should be used for authentication by sending the name of the desired identity provider.
|
// which identity provider should be used for authentication by sending the name of the desired identity provider.
|
||||||
AuthorizeUpstreamIDPNameParamName = "pinniped_idp_name"
|
AuthorizeUpstreamIDPNameParamName = "pinniped_idp_name"
|
||||||
|
|
||||||
// AuthorizeUpstreamIDPTypeParamName is the name of the HTTP request parameter which can be used to help select which
|
// AuthorizeUpstreamIDPTypeParamName is the name of the HTTP request parameter which can be used to help select
|
||||||
// identity provider should be used for authentication by sending the type of the desired identity provider.
|
// which identity provider should be used for authentication by sending the type of the desired identity provider.
|
||||||
AuthorizeUpstreamIDPTypeParamName = "pinniped_idp_type"
|
AuthorizeUpstreamIDPTypeParamName = "pinniped_idp_type"
|
||||||
|
|
||||||
|
// IDTokenClaimIssuer is name of the issuer claim defined by the OIDC spec.
|
||||||
|
IDTokenClaimIssuer = "iss"
|
||||||
|
|
||||||
|
// IDTokenClaimSubject is name of the subject claim defined by the OIDC spec.
|
||||||
|
IDTokenClaimSubject = "sub"
|
||||||
|
|
||||||
|
// IDTokenClaimAuthorizedParty is name of the authorized party claim defined by the OIDC spec.
|
||||||
|
IDTokenClaimAuthorizedParty = "azp"
|
||||||
|
|
||||||
|
// IDTokenClaimUsername is the name of a custom claim in the downstream ID token whose value will contain the user's
|
||||||
|
// username which was mapped from the upstream identity provider.
|
||||||
|
IDTokenClaimUsername = "username"
|
||||||
|
|
||||||
|
// IDTokenClaimGroups is the name of a custom claim in the downstream ID token whose value will contain the user's
|
||||||
|
// group names which were mapped from the upstream identity provider.
|
||||||
|
IDTokenClaimGroups = "groups"
|
||||||
|
|
||||||
|
// GrantTypeAuthorizationCode is the name of the grant type for authorization code flows defined by the OIDC spec.
|
||||||
|
GrantTypeAuthorizationCode = "authorization_code"
|
||||||
|
|
||||||
|
// GrantTypeRefreshToken is the name of the grant type for refresh flow defined by the OIDC spec.
|
||||||
|
GrantTypeRefreshToken = "refresh_token"
|
||||||
|
|
||||||
|
// GrantTypeTokenExchange is the name of a custom grant type for RFC8693 token exchanges.
|
||||||
|
GrantTypeTokenExchange = "urn:ietf:params:oauth:grant-type:token-exchange" //nolint:gosec // this is not a credential
|
||||||
|
|
||||||
|
// ScopeOpenID is name of the openid scope defined by the OIDC spec.
|
||||||
|
ScopeOpenID = "openid"
|
||||||
|
|
||||||
|
// ScopeOfflineAccess is name of the offline access scope defined by the OIDC spec, used for requesting refresh
|
||||||
|
// tokens.
|
||||||
|
ScopeOfflineAccess = "offline_access"
|
||||||
|
|
||||||
|
// ScopeEmail is name of the email scope defined by the OIDC spec.
|
||||||
|
ScopeEmail = "email"
|
||||||
|
|
||||||
|
// ScopeProfile is name of the profile scope defined by the OIDC spec.
|
||||||
|
ScopeProfile = "profile"
|
||||||
|
|
||||||
|
// ScopeUsername is the name of a custom scope that determines whether the username claim will be returned inside
|
||||||
|
// ID tokens.
|
||||||
|
ScopeUsername = "username"
|
||||||
|
|
||||||
|
// ScopeGroups is the name of a custom scope that determines whether the groups claim will be returned inside
|
||||||
|
// ID tokens.
|
||||||
|
ScopeGroups = "groups"
|
||||||
|
|
||||||
|
// ScopeRequestAudience is the name of a custom scope that determines whether a RFC8693 token exchange is allowed to
|
||||||
|
// be used to request a different audience.
|
||||||
|
ScopeRequestAudience = "pinniped:request-audience"
|
||||||
|
|
||||||
|
// ClientIDPinnipedCLI is the client ID of the statically defined public OIDC client which is used by the CLI.
|
||||||
|
ClientIDPinnipedCLI = "pinniped-cli"
|
||||||
|
|
||||||
|
// ClientIDRequiredOIDCClientPrefix is the required prefix for the metadata.name of OIDCClient CRs.
|
||||||
|
ClientIDRequiredOIDCClientPrefix = "client.oauth.pinniped.dev-"
|
||||||
)
|
)
|
||||||
|
@ -15,11 +15,68 @@ const (
|
|||||||
// or an LDAPIdentityProvider.
|
// or an LDAPIdentityProvider.
|
||||||
AuthorizePasswordHeaderName = "Pinniped-Password" //nolint:gosec // this is not a credential
|
AuthorizePasswordHeaderName = "Pinniped-Password" //nolint:gosec // this is not a credential
|
||||||
|
|
||||||
// AuthorizeUpstreamIDPNameParamName is the name of the HTTP request parameter which can be used to help select which
|
// AuthorizeUpstreamIDPNameParamName is the name of the HTTP request parameter which can be used to help select
|
||||||
// identity provider should be used for authentication by sending the name of the desired identity provider.
|
// which identity provider should be used for authentication by sending the name of the desired identity provider.
|
||||||
AuthorizeUpstreamIDPNameParamName = "pinniped_idp_name"
|
AuthorizeUpstreamIDPNameParamName = "pinniped_idp_name"
|
||||||
|
|
||||||
// AuthorizeUpstreamIDPTypeParamName is the name of the HTTP request parameter which can be used to help select which
|
// AuthorizeUpstreamIDPTypeParamName is the name of the HTTP request parameter which can be used to help select
|
||||||
// identity provider should be used for authentication by sending the type of the desired identity provider.
|
// which identity provider should be used for authentication by sending the type of the desired identity provider.
|
||||||
AuthorizeUpstreamIDPTypeParamName = "pinniped_idp_type"
|
AuthorizeUpstreamIDPTypeParamName = "pinniped_idp_type"
|
||||||
|
|
||||||
|
// IDTokenClaimIssuer is name of the issuer claim defined by the OIDC spec.
|
||||||
|
IDTokenClaimIssuer = "iss"
|
||||||
|
|
||||||
|
// IDTokenClaimSubject is name of the subject claim defined by the OIDC spec.
|
||||||
|
IDTokenClaimSubject = "sub"
|
||||||
|
|
||||||
|
// IDTokenClaimAuthorizedParty is name of the authorized party claim defined by the OIDC spec.
|
||||||
|
IDTokenClaimAuthorizedParty = "azp"
|
||||||
|
|
||||||
|
// IDTokenClaimUsername is the name of a custom claim in the downstream ID token whose value will contain the user's
|
||||||
|
// username which was mapped from the upstream identity provider.
|
||||||
|
IDTokenClaimUsername = "username"
|
||||||
|
|
||||||
|
// IDTokenClaimGroups is the name of a custom claim in the downstream ID token whose value will contain the user's
|
||||||
|
// group names which were mapped from the upstream identity provider.
|
||||||
|
IDTokenClaimGroups = "groups"
|
||||||
|
|
||||||
|
// GrantTypeAuthorizationCode is the name of the grant type for authorization code flows defined by the OIDC spec.
|
||||||
|
GrantTypeAuthorizationCode = "authorization_code"
|
||||||
|
|
||||||
|
// GrantTypeRefreshToken is the name of the grant type for refresh flow defined by the OIDC spec.
|
||||||
|
GrantTypeRefreshToken = "refresh_token"
|
||||||
|
|
||||||
|
// GrantTypeTokenExchange is the name of a custom grant type for RFC8693 token exchanges.
|
||||||
|
GrantTypeTokenExchange = "urn:ietf:params:oauth:grant-type:token-exchange" //nolint:gosec // this is not a credential
|
||||||
|
|
||||||
|
// ScopeOpenID is name of the openid scope defined by the OIDC spec.
|
||||||
|
ScopeOpenID = "openid"
|
||||||
|
|
||||||
|
// ScopeOfflineAccess is name of the offline access scope defined by the OIDC spec, used for requesting refresh
|
||||||
|
// tokens.
|
||||||
|
ScopeOfflineAccess = "offline_access"
|
||||||
|
|
||||||
|
// ScopeEmail is name of the email scope defined by the OIDC spec.
|
||||||
|
ScopeEmail = "email"
|
||||||
|
|
||||||
|
// ScopeProfile is name of the profile scope defined by the OIDC spec.
|
||||||
|
ScopeProfile = "profile"
|
||||||
|
|
||||||
|
// ScopeUsername is the name of a custom scope that determines whether the username claim will be returned inside
|
||||||
|
// ID tokens.
|
||||||
|
ScopeUsername = "username"
|
||||||
|
|
||||||
|
// ScopeGroups is the name of a custom scope that determines whether the groups claim will be returned inside
|
||||||
|
// ID tokens.
|
||||||
|
ScopeGroups = "groups"
|
||||||
|
|
||||||
|
// ScopeRequestAudience is the name of a custom scope that determines whether a RFC8693 token exchange is allowed to
|
||||||
|
// be used to request a different audience.
|
||||||
|
ScopeRequestAudience = "pinniped:request-audience"
|
||||||
|
|
||||||
|
// ClientIDPinnipedCLI is the client ID of the statically defined public OIDC client which is used by the CLI.
|
||||||
|
ClientIDPinnipedCLI = "pinniped-cli"
|
||||||
|
|
||||||
|
// ClientIDRequiredOIDCClientPrefix is the required prefix for the metadata.name of OIDCClient CRs.
|
||||||
|
ClientIDRequiredOIDCClientPrefix = "client.oauth.pinniped.dev-"
|
||||||
)
|
)
|
||||||
|
@ -15,11 +15,68 @@ const (
|
|||||||
// or an LDAPIdentityProvider.
|
// or an LDAPIdentityProvider.
|
||||||
AuthorizePasswordHeaderName = "Pinniped-Password" //nolint:gosec // this is not a credential
|
AuthorizePasswordHeaderName = "Pinniped-Password" //nolint:gosec // this is not a credential
|
||||||
|
|
||||||
// AuthorizeUpstreamIDPNameParamName is the name of the HTTP request parameter which can be used to help select which
|
// AuthorizeUpstreamIDPNameParamName is the name of the HTTP request parameter which can be used to help select
|
||||||
// identity provider should be used for authentication by sending the name of the desired identity provider.
|
// which identity provider should be used for authentication by sending the name of the desired identity provider.
|
||||||
AuthorizeUpstreamIDPNameParamName = "pinniped_idp_name"
|
AuthorizeUpstreamIDPNameParamName = "pinniped_idp_name"
|
||||||
|
|
||||||
// AuthorizeUpstreamIDPTypeParamName is the name of the HTTP request parameter which can be used to help select which
|
// AuthorizeUpstreamIDPTypeParamName is the name of the HTTP request parameter which can be used to help select
|
||||||
// identity provider should be used for authentication by sending the type of the desired identity provider.
|
// which identity provider should be used for authentication by sending the type of the desired identity provider.
|
||||||
AuthorizeUpstreamIDPTypeParamName = "pinniped_idp_type"
|
AuthorizeUpstreamIDPTypeParamName = "pinniped_idp_type"
|
||||||
|
|
||||||
|
// IDTokenClaimIssuer is name of the issuer claim defined by the OIDC spec.
|
||||||
|
IDTokenClaimIssuer = "iss"
|
||||||
|
|
||||||
|
// IDTokenClaimSubject is name of the subject claim defined by the OIDC spec.
|
||||||
|
IDTokenClaimSubject = "sub"
|
||||||
|
|
||||||
|
// IDTokenClaimAuthorizedParty is name of the authorized party claim defined by the OIDC spec.
|
||||||
|
IDTokenClaimAuthorizedParty = "azp"
|
||||||
|
|
||||||
|
// IDTokenClaimUsername is the name of a custom claim in the downstream ID token whose value will contain the user's
|
||||||
|
// username which was mapped from the upstream identity provider.
|
||||||
|
IDTokenClaimUsername = "username"
|
||||||
|
|
||||||
|
// IDTokenClaimGroups is the name of a custom claim in the downstream ID token whose value will contain the user's
|
||||||
|
// group names which were mapped from the upstream identity provider.
|
||||||
|
IDTokenClaimGroups = "groups"
|
||||||
|
|
||||||
|
// GrantTypeAuthorizationCode is the name of the grant type for authorization code flows defined by the OIDC spec.
|
||||||
|
GrantTypeAuthorizationCode = "authorization_code"
|
||||||
|
|
||||||
|
// GrantTypeRefreshToken is the name of the grant type for refresh flow defined by the OIDC spec.
|
||||||
|
GrantTypeRefreshToken = "refresh_token"
|
||||||
|
|
||||||
|
// GrantTypeTokenExchange is the name of a custom grant type for RFC8693 token exchanges.
|
||||||
|
GrantTypeTokenExchange = "urn:ietf:params:oauth:grant-type:token-exchange" //nolint:gosec // this is not a credential
|
||||||
|
|
||||||
|
// ScopeOpenID is name of the openid scope defined by the OIDC spec.
|
||||||
|
ScopeOpenID = "openid"
|
||||||
|
|
||||||
|
// ScopeOfflineAccess is name of the offline access scope defined by the OIDC spec, used for requesting refresh
|
||||||
|
// tokens.
|
||||||
|
ScopeOfflineAccess = "offline_access"
|
||||||
|
|
||||||
|
// ScopeEmail is name of the email scope defined by the OIDC spec.
|
||||||
|
ScopeEmail = "email"
|
||||||
|
|
||||||
|
// ScopeProfile is name of the profile scope defined by the OIDC spec.
|
||||||
|
ScopeProfile = "profile"
|
||||||
|
|
||||||
|
// ScopeUsername is the name of a custom scope that determines whether the username claim will be returned inside
|
||||||
|
// ID tokens.
|
||||||
|
ScopeUsername = "username"
|
||||||
|
|
||||||
|
// ScopeGroups is the name of a custom scope that determines whether the groups claim will be returned inside
|
||||||
|
// ID tokens.
|
||||||
|
ScopeGroups = "groups"
|
||||||
|
|
||||||
|
// ScopeRequestAudience is the name of a custom scope that determines whether a RFC8693 token exchange is allowed to
|
||||||
|
// be used to request a different audience.
|
||||||
|
ScopeRequestAudience = "pinniped:request-audience"
|
||||||
|
|
||||||
|
// ClientIDPinnipedCLI is the client ID of the statically defined public OIDC client which is used by the CLI.
|
||||||
|
ClientIDPinnipedCLI = "pinniped-cli"
|
||||||
|
|
||||||
|
// ClientIDRequiredOIDCClientPrefix is the required prefix for the metadata.name of OIDCClient CRs.
|
||||||
|
ClientIDRequiredOIDCClientPrefix = "client.oauth.pinniped.dev-"
|
||||||
)
|
)
|
||||||
|
@ -15,11 +15,68 @@ const (
|
|||||||
// or an LDAPIdentityProvider.
|
// or an LDAPIdentityProvider.
|
||||||
AuthorizePasswordHeaderName = "Pinniped-Password" //nolint:gosec // this is not a credential
|
AuthorizePasswordHeaderName = "Pinniped-Password" //nolint:gosec // this is not a credential
|
||||||
|
|
||||||
// AuthorizeUpstreamIDPNameParamName is the name of the HTTP request parameter which can be used to help select which
|
// AuthorizeUpstreamIDPNameParamName is the name of the HTTP request parameter which can be used to help select
|
||||||
// identity provider should be used for authentication by sending the name of the desired identity provider.
|
// which identity provider should be used for authentication by sending the name of the desired identity provider.
|
||||||
AuthorizeUpstreamIDPNameParamName = "pinniped_idp_name"
|
AuthorizeUpstreamIDPNameParamName = "pinniped_idp_name"
|
||||||
|
|
||||||
// AuthorizeUpstreamIDPTypeParamName is the name of the HTTP request parameter which can be used to help select which
|
// AuthorizeUpstreamIDPTypeParamName is the name of the HTTP request parameter which can be used to help select
|
||||||
// identity provider should be used for authentication by sending the type of the desired identity provider.
|
// which identity provider should be used for authentication by sending the type of the desired identity provider.
|
||||||
AuthorizeUpstreamIDPTypeParamName = "pinniped_idp_type"
|
AuthorizeUpstreamIDPTypeParamName = "pinniped_idp_type"
|
||||||
|
|
||||||
|
// IDTokenClaimIssuer is name of the issuer claim defined by the OIDC spec.
|
||||||
|
IDTokenClaimIssuer = "iss"
|
||||||
|
|
||||||
|
// IDTokenClaimSubject is name of the subject claim defined by the OIDC spec.
|
||||||
|
IDTokenClaimSubject = "sub"
|
||||||
|
|
||||||
|
// IDTokenClaimAuthorizedParty is name of the authorized party claim defined by the OIDC spec.
|
||||||
|
IDTokenClaimAuthorizedParty = "azp"
|
||||||
|
|
||||||
|
// IDTokenClaimUsername is the name of a custom claim in the downstream ID token whose value will contain the user's
|
||||||
|
// username which was mapped from the upstream identity provider.
|
||||||
|
IDTokenClaimUsername = "username"
|
||||||
|
|
||||||
|
// IDTokenClaimGroups is the name of a custom claim in the downstream ID token whose value will contain the user's
|
||||||
|
// group names which were mapped from the upstream identity provider.
|
||||||
|
IDTokenClaimGroups = "groups"
|
||||||
|
|
||||||
|
// GrantTypeAuthorizationCode is the name of the grant type for authorization code flows defined by the OIDC spec.
|
||||||
|
GrantTypeAuthorizationCode = "authorization_code"
|
||||||
|
|
||||||
|
// GrantTypeRefreshToken is the name of the grant type for refresh flow defined by the OIDC spec.
|
||||||
|
GrantTypeRefreshToken = "refresh_token"
|
||||||
|
|
||||||
|
// GrantTypeTokenExchange is the name of a custom grant type for RFC8693 token exchanges.
|
||||||
|
GrantTypeTokenExchange = "urn:ietf:params:oauth:grant-type:token-exchange" //nolint:gosec // this is not a credential
|
||||||
|
|
||||||
|
// ScopeOpenID is name of the openid scope defined by the OIDC spec.
|
||||||
|
ScopeOpenID = "openid"
|
||||||
|
|
||||||
|
// ScopeOfflineAccess is name of the offline access scope defined by the OIDC spec, used for requesting refresh
|
||||||
|
// tokens.
|
||||||
|
ScopeOfflineAccess = "offline_access"
|
||||||
|
|
||||||
|
// ScopeEmail is name of the email scope defined by the OIDC spec.
|
||||||
|
ScopeEmail = "email"
|
||||||
|
|
||||||
|
// ScopeProfile is name of the profile scope defined by the OIDC spec.
|
||||||
|
ScopeProfile = "profile"
|
||||||
|
|
||||||
|
// ScopeUsername is the name of a custom scope that determines whether the username claim will be returned inside
|
||||||
|
// ID tokens.
|
||||||
|
ScopeUsername = "username"
|
||||||
|
|
||||||
|
// ScopeGroups is the name of a custom scope that determines whether the groups claim will be returned inside
|
||||||
|
// ID tokens.
|
||||||
|
ScopeGroups = "groups"
|
||||||
|
|
||||||
|
// ScopeRequestAudience is the name of a custom scope that determines whether a RFC8693 token exchange is allowed to
|
||||||
|
// be used to request a different audience.
|
||||||
|
ScopeRequestAudience = "pinniped:request-audience"
|
||||||
|
|
||||||
|
// ClientIDPinnipedCLI is the client ID of the statically defined public OIDC client which is used by the CLI.
|
||||||
|
ClientIDPinnipedCLI = "pinniped-cli"
|
||||||
|
|
||||||
|
// ClientIDRequiredOIDCClientPrefix is the required prefix for the metadata.name of OIDCClient CRs.
|
||||||
|
ClientIDRequiredOIDCClientPrefix = "client.oauth.pinniped.dev-"
|
||||||
)
|
)
|
||||||
|
@ -15,11 +15,68 @@ const (
|
|||||||
// or an LDAPIdentityProvider.
|
// or an LDAPIdentityProvider.
|
||||||
AuthorizePasswordHeaderName = "Pinniped-Password" //nolint:gosec // this is not a credential
|
AuthorizePasswordHeaderName = "Pinniped-Password" //nolint:gosec // this is not a credential
|
||||||
|
|
||||||
// AuthorizeUpstreamIDPNameParamName is the name of the HTTP request parameter which can be used to help select which
|
// AuthorizeUpstreamIDPNameParamName is the name of the HTTP request parameter which can be used to help select
|
||||||
// identity provider should be used for authentication by sending the name of the desired identity provider.
|
// which identity provider should be used for authentication by sending the name of the desired identity provider.
|
||||||
AuthorizeUpstreamIDPNameParamName = "pinniped_idp_name"
|
AuthorizeUpstreamIDPNameParamName = "pinniped_idp_name"
|
||||||
|
|
||||||
// AuthorizeUpstreamIDPTypeParamName is the name of the HTTP request parameter which can be used to help select which
|
// AuthorizeUpstreamIDPTypeParamName is the name of the HTTP request parameter which can be used to help select
|
||||||
// identity provider should be used for authentication by sending the type of the desired identity provider.
|
// which identity provider should be used for authentication by sending the type of the desired identity provider.
|
||||||
AuthorizeUpstreamIDPTypeParamName = "pinniped_idp_type"
|
AuthorizeUpstreamIDPTypeParamName = "pinniped_idp_type"
|
||||||
|
|
||||||
|
// IDTokenClaimIssuer is name of the issuer claim defined by the OIDC spec.
|
||||||
|
IDTokenClaimIssuer = "iss"
|
||||||
|
|
||||||
|
// IDTokenClaimSubject is name of the subject claim defined by the OIDC spec.
|
||||||
|
IDTokenClaimSubject = "sub"
|
||||||
|
|
||||||
|
// IDTokenClaimAuthorizedParty is name of the authorized party claim defined by the OIDC spec.
|
||||||
|
IDTokenClaimAuthorizedParty = "azp"
|
||||||
|
|
||||||
|
// IDTokenClaimUsername is the name of a custom claim in the downstream ID token whose value will contain the user's
|
||||||
|
// username which was mapped from the upstream identity provider.
|
||||||
|
IDTokenClaimUsername = "username"
|
||||||
|
|
||||||
|
// IDTokenClaimGroups is the name of a custom claim in the downstream ID token whose value will contain the user's
|
||||||
|
// group names which were mapped from the upstream identity provider.
|
||||||
|
IDTokenClaimGroups = "groups"
|
||||||
|
|
||||||
|
// GrantTypeAuthorizationCode is the name of the grant type for authorization code flows defined by the OIDC spec.
|
||||||
|
GrantTypeAuthorizationCode = "authorization_code"
|
||||||
|
|
||||||
|
// GrantTypeRefreshToken is the name of the grant type for refresh flow defined by the OIDC spec.
|
||||||
|
GrantTypeRefreshToken = "refresh_token"
|
||||||
|
|
||||||
|
// GrantTypeTokenExchange is the name of a custom grant type for RFC8693 token exchanges.
|
||||||
|
GrantTypeTokenExchange = "urn:ietf:params:oauth:grant-type:token-exchange" //nolint:gosec // this is not a credential
|
||||||
|
|
||||||
|
// ScopeOpenID is name of the openid scope defined by the OIDC spec.
|
||||||
|
ScopeOpenID = "openid"
|
||||||
|
|
||||||
|
// ScopeOfflineAccess is name of the offline access scope defined by the OIDC spec, used for requesting refresh
|
||||||
|
// tokens.
|
||||||
|
ScopeOfflineAccess = "offline_access"
|
||||||
|
|
||||||
|
// ScopeEmail is name of the email scope defined by the OIDC spec.
|
||||||
|
ScopeEmail = "email"
|
||||||
|
|
||||||
|
// ScopeProfile is name of the profile scope defined by the OIDC spec.
|
||||||
|
ScopeProfile = "profile"
|
||||||
|
|
||||||
|
// ScopeUsername is the name of a custom scope that determines whether the username claim will be returned inside
|
||||||
|
// ID tokens.
|
||||||
|
ScopeUsername = "username"
|
||||||
|
|
||||||
|
// ScopeGroups is the name of a custom scope that determines whether the groups claim will be returned inside
|
||||||
|
// ID tokens.
|
||||||
|
ScopeGroups = "groups"
|
||||||
|
|
||||||
|
// ScopeRequestAudience is the name of a custom scope that determines whether a RFC8693 token exchange is allowed to
|
||||||
|
// be used to request a different audience.
|
||||||
|
ScopeRequestAudience = "pinniped:request-audience"
|
||||||
|
|
||||||
|
// ClientIDPinnipedCLI is the client ID of the statically defined public OIDC client which is used by the CLI.
|
||||||
|
ClientIDPinnipedCLI = "pinniped-cli"
|
||||||
|
|
||||||
|
// ClientIDRequiredOIDCClientPrefix is the required prefix for the metadata.name of OIDCClient CRs.
|
||||||
|
ClientIDRequiredOIDCClientPrefix = "client.oauth.pinniped.dev-"
|
||||||
)
|
)
|
||||||
|
@ -15,11 +15,68 @@ const (
|
|||||||
// or an LDAPIdentityProvider.
|
// or an LDAPIdentityProvider.
|
||||||
AuthorizePasswordHeaderName = "Pinniped-Password" //nolint:gosec // this is not a credential
|
AuthorizePasswordHeaderName = "Pinniped-Password" //nolint:gosec // this is not a credential
|
||||||
|
|
||||||
// AuthorizeUpstreamIDPNameParamName is the name of the HTTP request parameter which can be used to help select which
|
// AuthorizeUpstreamIDPNameParamName is the name of the HTTP request parameter which can be used to help select
|
||||||
// identity provider should be used for authentication by sending the name of the desired identity provider.
|
// which identity provider should be used for authentication by sending the name of the desired identity provider.
|
||||||
AuthorizeUpstreamIDPNameParamName = "pinniped_idp_name"
|
AuthorizeUpstreamIDPNameParamName = "pinniped_idp_name"
|
||||||
|
|
||||||
// AuthorizeUpstreamIDPTypeParamName is the name of the HTTP request parameter which can be used to help select which
|
// AuthorizeUpstreamIDPTypeParamName is the name of the HTTP request parameter which can be used to help select
|
||||||
// identity provider should be used for authentication by sending the type of the desired identity provider.
|
// which identity provider should be used for authentication by sending the type of the desired identity provider.
|
||||||
AuthorizeUpstreamIDPTypeParamName = "pinniped_idp_type"
|
AuthorizeUpstreamIDPTypeParamName = "pinniped_idp_type"
|
||||||
|
|
||||||
|
// IDTokenClaimIssuer is name of the issuer claim defined by the OIDC spec.
|
||||||
|
IDTokenClaimIssuer = "iss"
|
||||||
|
|
||||||
|
// IDTokenClaimSubject is name of the subject claim defined by the OIDC spec.
|
||||||
|
IDTokenClaimSubject = "sub"
|
||||||
|
|
||||||
|
// IDTokenClaimAuthorizedParty is name of the authorized party claim defined by the OIDC spec.
|
||||||
|
IDTokenClaimAuthorizedParty = "azp"
|
||||||
|
|
||||||
|
// IDTokenClaimUsername is the name of a custom claim in the downstream ID token whose value will contain the user's
|
||||||
|
// username which was mapped from the upstream identity provider.
|
||||||
|
IDTokenClaimUsername = "username"
|
||||||
|
|
||||||
|
// IDTokenClaimGroups is the name of a custom claim in the downstream ID token whose value will contain the user's
|
||||||
|
// group names which were mapped from the upstream identity provider.
|
||||||
|
IDTokenClaimGroups = "groups"
|
||||||
|
|
||||||
|
// GrantTypeAuthorizationCode is the name of the grant type for authorization code flows defined by the OIDC spec.
|
||||||
|
GrantTypeAuthorizationCode = "authorization_code"
|
||||||
|
|
||||||
|
// GrantTypeRefreshToken is the name of the grant type for refresh flow defined by the OIDC spec.
|
||||||
|
GrantTypeRefreshToken = "refresh_token"
|
||||||
|
|
||||||
|
// GrantTypeTokenExchange is the name of a custom grant type for RFC8693 token exchanges.
|
||||||
|
GrantTypeTokenExchange = "urn:ietf:params:oauth:grant-type:token-exchange" //nolint:gosec // this is not a credential
|
||||||
|
|
||||||
|
// ScopeOpenID is name of the openid scope defined by the OIDC spec.
|
||||||
|
ScopeOpenID = "openid"
|
||||||
|
|
||||||
|
// ScopeOfflineAccess is name of the offline access scope defined by the OIDC spec, used for requesting refresh
|
||||||
|
// tokens.
|
||||||
|
ScopeOfflineAccess = "offline_access"
|
||||||
|
|
||||||
|
// ScopeEmail is name of the email scope defined by the OIDC spec.
|
||||||
|
ScopeEmail = "email"
|
||||||
|
|
||||||
|
// ScopeProfile is name of the profile scope defined by the OIDC spec.
|
||||||
|
ScopeProfile = "profile"
|
||||||
|
|
||||||
|
// ScopeUsername is the name of a custom scope that determines whether the username claim will be returned inside
|
||||||
|
// ID tokens.
|
||||||
|
ScopeUsername = "username"
|
||||||
|
|
||||||
|
// ScopeGroups is the name of a custom scope that determines whether the groups claim will be returned inside
|
||||||
|
// ID tokens.
|
||||||
|
ScopeGroups = "groups"
|
||||||
|
|
||||||
|
// ScopeRequestAudience is the name of a custom scope that determines whether a RFC8693 token exchange is allowed to
|
||||||
|
// be used to request a different audience.
|
||||||
|
ScopeRequestAudience = "pinniped:request-audience"
|
||||||
|
|
||||||
|
// ClientIDPinnipedCLI is the client ID of the statically defined public OIDC client which is used by the CLI.
|
||||||
|
ClientIDPinnipedCLI = "pinniped-cli"
|
||||||
|
|
||||||
|
// ClientIDRequiredOIDCClientPrefix is the required prefix for the metadata.name of OIDCClient CRs.
|
||||||
|
ClientIDRequiredOIDCClientPrefix = "client.oauth.pinniped.dev-"
|
||||||
)
|
)
|
||||||
|
@ -15,11 +15,68 @@ const (
|
|||||||
// or an LDAPIdentityProvider.
|
// or an LDAPIdentityProvider.
|
||||||
AuthorizePasswordHeaderName = "Pinniped-Password" //nolint:gosec // this is not a credential
|
AuthorizePasswordHeaderName = "Pinniped-Password" //nolint:gosec // this is not a credential
|
||||||
|
|
||||||
// AuthorizeUpstreamIDPNameParamName is the name of the HTTP request parameter which can be used to help select which
|
// AuthorizeUpstreamIDPNameParamName is the name of the HTTP request parameter which can be used to help select
|
||||||
// identity provider should be used for authentication by sending the name of the desired identity provider.
|
// which identity provider should be used for authentication by sending the name of the desired identity provider.
|
||||||
AuthorizeUpstreamIDPNameParamName = "pinniped_idp_name"
|
AuthorizeUpstreamIDPNameParamName = "pinniped_idp_name"
|
||||||
|
|
||||||
// AuthorizeUpstreamIDPTypeParamName is the name of the HTTP request parameter which can be used to help select which
|
// AuthorizeUpstreamIDPTypeParamName is the name of the HTTP request parameter which can be used to help select
|
||||||
// identity provider should be used for authentication by sending the type of the desired identity provider.
|
// which identity provider should be used for authentication by sending the type of the desired identity provider.
|
||||||
AuthorizeUpstreamIDPTypeParamName = "pinniped_idp_type"
|
AuthorizeUpstreamIDPTypeParamName = "pinniped_idp_type"
|
||||||
|
|
||||||
|
// IDTokenClaimIssuer is name of the issuer claim defined by the OIDC spec.
|
||||||
|
IDTokenClaimIssuer = "iss"
|
||||||
|
|
||||||
|
// IDTokenClaimSubject is name of the subject claim defined by the OIDC spec.
|
||||||
|
IDTokenClaimSubject = "sub"
|
||||||
|
|
||||||
|
// IDTokenClaimAuthorizedParty is name of the authorized party claim defined by the OIDC spec.
|
||||||
|
IDTokenClaimAuthorizedParty = "azp"
|
||||||
|
|
||||||
|
// IDTokenClaimUsername is the name of a custom claim in the downstream ID token whose value will contain the user's
|
||||||
|
// username which was mapped from the upstream identity provider.
|
||||||
|
IDTokenClaimUsername = "username"
|
||||||
|
|
||||||
|
// IDTokenClaimGroups is the name of a custom claim in the downstream ID token whose value will contain the user's
|
||||||
|
// group names which were mapped from the upstream identity provider.
|
||||||
|
IDTokenClaimGroups = "groups"
|
||||||
|
|
||||||
|
// GrantTypeAuthorizationCode is the name of the grant type for authorization code flows defined by the OIDC spec.
|
||||||
|
GrantTypeAuthorizationCode = "authorization_code"
|
||||||
|
|
||||||
|
// GrantTypeRefreshToken is the name of the grant type for refresh flow defined by the OIDC spec.
|
||||||
|
GrantTypeRefreshToken = "refresh_token"
|
||||||
|
|
||||||
|
// GrantTypeTokenExchange is the name of a custom grant type for RFC8693 token exchanges.
|
||||||
|
GrantTypeTokenExchange = "urn:ietf:params:oauth:grant-type:token-exchange" //nolint:gosec // this is not a credential
|
||||||
|
|
||||||
|
// ScopeOpenID is name of the openid scope defined by the OIDC spec.
|
||||||
|
ScopeOpenID = "openid"
|
||||||
|
|
||||||
|
// ScopeOfflineAccess is name of the offline access scope defined by the OIDC spec, used for requesting refresh
|
||||||
|
// tokens.
|
||||||
|
ScopeOfflineAccess = "offline_access"
|
||||||
|
|
||||||
|
// ScopeEmail is name of the email scope defined by the OIDC spec.
|
||||||
|
ScopeEmail = "email"
|
||||||
|
|
||||||
|
// ScopeProfile is name of the profile scope defined by the OIDC spec.
|
||||||
|
ScopeProfile = "profile"
|
||||||
|
|
||||||
|
// ScopeUsername is the name of a custom scope that determines whether the username claim will be returned inside
|
||||||
|
// ID tokens.
|
||||||
|
ScopeUsername = "username"
|
||||||
|
|
||||||
|
// ScopeGroups is the name of a custom scope that determines whether the groups claim will be returned inside
|
||||||
|
// ID tokens.
|
||||||
|
ScopeGroups = "groups"
|
||||||
|
|
||||||
|
// ScopeRequestAudience is the name of a custom scope that determines whether a RFC8693 token exchange is allowed to
|
||||||
|
// be used to request a different audience.
|
||||||
|
ScopeRequestAudience = "pinniped:request-audience"
|
||||||
|
|
||||||
|
// ClientIDPinnipedCLI is the client ID of the statically defined public OIDC client which is used by the CLI.
|
||||||
|
ClientIDPinnipedCLI = "pinniped-cli"
|
||||||
|
|
||||||
|
// ClientIDRequiredOIDCClientPrefix is the required prefix for the metadata.name of OIDCClient CRs.
|
||||||
|
ClientIDRequiredOIDCClientPrefix = "client.oauth.pinniped.dev-"
|
||||||
)
|
)
|
||||||
|
@ -21,6 +21,7 @@ import (
|
|||||||
"k8s.io/klog/v2"
|
"k8s.io/klog/v2"
|
||||||
|
|
||||||
auth1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1"
|
auth1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1"
|
||||||
|
oidcapi "go.pinniped.dev/generated/latest/apis/supervisor/oidc"
|
||||||
authinformers "go.pinniped.dev/generated/latest/client/concierge/informers/externalversions/authentication/v1alpha1"
|
authinformers "go.pinniped.dev/generated/latest/client/concierge/informers/externalversions/authentication/v1alpha1"
|
||||||
pinnipedcontroller "go.pinniped.dev/internal/controller"
|
pinnipedcontroller "go.pinniped.dev/internal/controller"
|
||||||
pinnipedauthenticator "go.pinniped.dev/internal/controller/authenticator"
|
pinnipedauthenticator "go.pinniped.dev/internal/controller/authenticator"
|
||||||
@ -32,8 +33,8 @@ import (
|
|||||||
// These default values come from the way that the Supervisor issues and signs tokens. We make these
|
// These default values come from the way that the Supervisor issues and signs tokens. We make these
|
||||||
// the defaults for a JWTAuthenticator so that they can easily integrate with the Supervisor.
|
// the defaults for a JWTAuthenticator so that they can easily integrate with the Supervisor.
|
||||||
const (
|
const (
|
||||||
defaultUsernameClaim = "username"
|
defaultUsernameClaim = oidcapi.IDTokenClaimUsername
|
||||||
defaultGroupsClaim = "groups"
|
defaultGroupsClaim = oidcapi.IDTokenClaimGroups
|
||||||
)
|
)
|
||||||
|
|
||||||
// defaultSupportedSigningAlgos returns the default signing algos that this JWTAuthenticator
|
// defaultSupportedSigningAlgos returns the default signing algos that this JWTAuthenticator
|
||||||
|
@ -15,6 +15,7 @@ import (
|
|||||||
corev1informers "k8s.io/client-go/informers/core/v1"
|
corev1informers "k8s.io/client-go/informers/core/v1"
|
||||||
|
|
||||||
"go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1"
|
"go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1"
|
||||||
|
oidcapi "go.pinniped.dev/generated/latest/apis/supervisor/oidc"
|
||||||
pinnipedclientset "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned"
|
pinnipedclientset "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned"
|
||||||
configInformers "go.pinniped.dev/generated/latest/client/supervisor/informers/externalversions/config/v1alpha1"
|
configInformers "go.pinniped.dev/generated/latest/client/supervisor/informers/externalversions/config/v1alpha1"
|
||||||
pinnipedcontroller "go.pinniped.dev/internal/controller"
|
pinnipedcontroller "go.pinniped.dev/internal/controller"
|
||||||
@ -27,7 +28,7 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
secretTypeToObserve = "storage.pinniped.dev/oidc-client-secret" //nolint:gosec // this is not a credential
|
secretTypeToObserve = "storage.pinniped.dev/oidc-client-secret" //nolint:gosec // this is not a credential
|
||||||
oidcClientPrefixToObserve = "client.oauth.pinniped.dev-" //nolint:gosec // this is not a credential
|
oidcClientPrefixToObserve = oidcapi.ClientIDRequiredOIDCClientPrefix
|
||||||
)
|
)
|
||||||
|
|
||||||
type oidcClientWatcherController struct {
|
type oidcClientWatcherController struct {
|
||||||
|
@ -14,7 +14,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/coreos/go-oidc/v3/oidc"
|
coreosoidc "github.com/coreos/go-oidc/v3/oidc"
|
||||||
"github.com/go-logr/logr"
|
"github.com/go-logr/logr"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
@ -26,6 +26,7 @@ import (
|
|||||||
corev1informers "k8s.io/client-go/informers/core/v1"
|
corev1informers "k8s.io/client-go/informers/core/v1"
|
||||||
|
|
||||||
"go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1"
|
"go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1"
|
||||||
|
oidcapi "go.pinniped.dev/generated/latest/apis/supervisor/oidc"
|
||||||
pinnipedclientset "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned"
|
pinnipedclientset "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned"
|
||||||
idpinformers "go.pinniped.dev/generated/latest/client/supervisor/informers/externalversions/idp/v1alpha1"
|
idpinformers "go.pinniped.dev/generated/latest/client/supervisor/informers/externalversions/idp/v1alpha1"
|
||||||
"go.pinniped.dev/internal/constable"
|
"go.pinniped.dev/internal/constable"
|
||||||
@ -97,11 +98,11 @@ type UpstreamOIDCIdentityProviderICache interface {
|
|||||||
type lruValidatorCache struct{ cache *cache.Expiring }
|
type lruValidatorCache struct{ cache *cache.Expiring }
|
||||||
|
|
||||||
type lruValidatorCacheEntry struct {
|
type lruValidatorCacheEntry struct {
|
||||||
provider *oidc.Provider
|
provider *coreosoidc.Provider
|
||||||
client *http.Client
|
client *http.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *lruValidatorCache) getProvider(spec *v1alpha1.OIDCIdentityProviderSpec) (*oidc.Provider, *http.Client) {
|
func (c *lruValidatorCache) getProvider(spec *v1alpha1.OIDCIdentityProviderSpec) (*coreosoidc.Provider, *http.Client) {
|
||||||
if result, ok := c.cache.Get(c.cacheKey(spec)); ok {
|
if result, ok := c.cache.Get(c.cacheKey(spec)); ok {
|
||||||
entry := result.(*lruValidatorCacheEntry)
|
entry := result.(*lruValidatorCacheEntry)
|
||||||
return entry.provider, entry.client
|
return entry.provider, entry.client
|
||||||
@ -109,7 +110,7 @@ func (c *lruValidatorCache) getProvider(spec *v1alpha1.OIDCIdentityProviderSpec)
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *lruValidatorCache) putProvider(spec *v1alpha1.OIDCIdentityProviderSpec, provider *oidc.Provider, client *http.Client) {
|
func (c *lruValidatorCache) putProvider(spec *v1alpha1.OIDCIdentityProviderSpec, provider *coreosoidc.Provider, client *http.Client) {
|
||||||
c.cache.Set(c.cacheKey(spec), &lruValidatorCacheEntry{provider: provider, client: client}, oidcValidatorCacheTTL)
|
c.cache.Set(c.cacheKey(spec), &lruValidatorCacheEntry{provider: provider, client: client}, oidcValidatorCacheTTL)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -129,8 +130,8 @@ type oidcWatcherController struct {
|
|||||||
oidcIdentityProviderInformer idpinformers.OIDCIdentityProviderInformer
|
oidcIdentityProviderInformer idpinformers.OIDCIdentityProviderInformer
|
||||||
secretInformer corev1informers.SecretInformer
|
secretInformer corev1informers.SecretInformer
|
||||||
validatorCache interface {
|
validatorCache interface {
|
||||||
getProvider(*v1alpha1.OIDCIdentityProviderSpec) (*oidc.Provider, *http.Client)
|
getProvider(*v1alpha1.OIDCIdentityProviderSpec) (*coreosoidc.Provider, *http.Client)
|
||||||
putProvider(*v1alpha1.OIDCIdentityProviderSpec, *oidc.Provider, *http.Client)
|
putProvider(*v1alpha1.OIDCIdentityProviderSpec, *coreosoidc.Provider, *http.Client)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -329,7 +330,7 @@ func (c *oidcWatcherController) validateIssuer(ctx context.Context, upstream *v1
|
|||||||
return issuerURLCondition
|
return issuerURLCondition
|
||||||
}
|
}
|
||||||
|
|
||||||
discoveredProvider, err = oidc.NewProvider(oidc.ClientContext(ctx, httpClient), upstream.Spec.Issuer)
|
discoveredProvider, err = coreosoidc.NewProvider(coreosoidc.ClientContext(ctx, httpClient), upstream.Spec.Issuer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.log.V(plog.KlogLevelTrace).WithValues(
|
c.log.V(plog.KlogLevelTrace).WithValues(
|
||||||
"namespace", upstream.Namespace,
|
"namespace", upstream.Namespace,
|
||||||
@ -457,12 +458,12 @@ func defaultClientShortTimeout(rootCAs *x509.CertPool) *http.Client {
|
|||||||
func computeScopes(additionalScopes []string) []string {
|
func computeScopes(additionalScopes []string) []string {
|
||||||
// If none are set then provide a reasonable default which only tries to use scopes defined in the OIDC spec.
|
// If none are set then provide a reasonable default which only tries to use scopes defined in the OIDC spec.
|
||||||
if len(additionalScopes) == 0 {
|
if len(additionalScopes) == 0 {
|
||||||
return []string{"openid", "offline_access", "email", "profile"}
|
return []string{oidcapi.ScopeOpenID, oidcapi.ScopeOfflineAccess, oidcapi.ScopeEmail, oidcapi.ScopeProfile}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, first compute the unique set of scopes, including "openid" (de-duplicate).
|
// Otherwise, first compute the unique set of scopes, including "openid" (de-duplicate).
|
||||||
set := sets.NewString()
|
set := sets.NewString()
|
||||||
set.Insert("openid")
|
set.Insert(oidcapi.ScopeOpenID)
|
||||||
for _, s := range additionalScopes {
|
for _, s := range additionalScopes {
|
||||||
set.Insert(s)
|
set.Insert(s)
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
coreosoidc "github.com/coreos/go-oidc/v3/oidc"
|
|
||||||
v1 "k8s.io/api/core/v1"
|
v1 "k8s.io/api/core/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/labels"
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
@ -17,8 +16,8 @@ import (
|
|||||||
"k8s.io/client-go/kubernetes"
|
"k8s.io/client-go/kubernetes"
|
||||||
"k8s.io/utils/clock"
|
"k8s.io/utils/clock"
|
||||||
clocktesting "k8s.io/utils/clock/testing"
|
clocktesting "k8s.io/utils/clock/testing"
|
||||||
"k8s.io/utils/strings/slices"
|
|
||||||
|
|
||||||
|
oidcapi "go.pinniped.dev/generated/latest/apis/supervisor/oidc"
|
||||||
pinnipedcontroller "go.pinniped.dev/internal/controller"
|
pinnipedcontroller "go.pinniped.dev/internal/controller"
|
||||||
"go.pinniped.dev/internal/controllerlib"
|
"go.pinniped.dev/internal/controllerlib"
|
||||||
"go.pinniped.dev/internal/crud"
|
"go.pinniped.dev/internal/crud"
|
||||||
@ -204,7 +203,7 @@ func (c *garbageCollectorController) maybeRevokeUpstreamOIDCToken(ctx context.Co
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
pinnipedSession := accessTokenSession.Request.Session.(*psession.PinnipedSession)
|
pinnipedSession := accessTokenSession.Request.Session.(*psession.PinnipedSession)
|
||||||
if slices.Contains(accessTokenSession.Request.GetGrantedScopes(), coreosoidc.ScopeOfflineAccess) {
|
if accessTokenSession.Request.GetGrantedScopes().Has(oidcapi.ScopeOfflineAccess) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return c.tryRevokeUpstreamOIDCToken(ctx, pinnipedSession.Custom, secret)
|
return c.tryRevokeUpstreamOIDCToken(ctx, pinnipedSession.Custom, secret)
|
||||||
|
@ -263,13 +263,14 @@ func TestGarbageCollectorControllerSync(t *testing.T) {
|
|||||||
when("there are valid, expired authcode secrets which contain upstream refresh tokens", func() {
|
when("there are valid, expired authcode secrets which contain upstream refresh tokens", func() {
|
||||||
it.Before(func() {
|
it.Before(func() {
|
||||||
activeOIDCAuthcodeSession := &authorizationcode.Session{
|
activeOIDCAuthcodeSession := &authorizationcode.Session{
|
||||||
Version: "2",
|
Version: "3",
|
||||||
Active: true,
|
Active: true,
|
||||||
Request: &fosite.Request{
|
Request: &fosite.Request{
|
||||||
ID: "request-id-1",
|
ID: "request-id-1",
|
||||||
Client: &clientregistry.Client{},
|
Client: &clientregistry.Client{},
|
||||||
Session: &psession.PinnipedSession{
|
Session: &psession.PinnipedSession{
|
||||||
Custom: &psession.CustomSessionData{
|
Custom: &psession.CustomSessionData{
|
||||||
|
Username: "should be ignored by garbage collector",
|
||||||
ProviderUID: "upstream-oidc-provider-uid",
|
ProviderUID: "upstream-oidc-provider-uid",
|
||||||
ProviderName: "upstream-oidc-provider-name",
|
ProviderName: "upstream-oidc-provider-name",
|
||||||
ProviderType: psession.ProviderTypeOIDC,
|
ProviderType: psession.ProviderTypeOIDC,
|
||||||
@ -307,13 +308,14 @@ func TestGarbageCollectorControllerSync(t *testing.T) {
|
|||||||
r.NoError(kubeClient.Tracker().Add(activeOIDCAuthcodeSessionSecret))
|
r.NoError(kubeClient.Tracker().Add(activeOIDCAuthcodeSessionSecret))
|
||||||
|
|
||||||
inactiveOIDCAuthcodeSession := &authorizationcode.Session{
|
inactiveOIDCAuthcodeSession := &authorizationcode.Session{
|
||||||
Version: "2",
|
Version: "3",
|
||||||
Active: false,
|
Active: false,
|
||||||
Request: &fosite.Request{
|
Request: &fosite.Request{
|
||||||
ID: "request-id-2",
|
ID: "request-id-2",
|
||||||
Client: &clientregistry.Client{},
|
Client: &clientregistry.Client{},
|
||||||
Session: &psession.PinnipedSession{
|
Session: &psession.PinnipedSession{
|
||||||
Custom: &psession.CustomSessionData{
|
Custom: &psession.CustomSessionData{
|
||||||
|
Username: "should be ignored by garbage collector",
|
||||||
ProviderUID: "upstream-oidc-provider-uid",
|
ProviderUID: "upstream-oidc-provider-uid",
|
||||||
ProviderName: "upstream-oidc-provider-name",
|
ProviderName: "upstream-oidc-provider-name",
|
||||||
ProviderType: psession.ProviderTypeOIDC,
|
ProviderType: psession.ProviderTypeOIDC,
|
||||||
@ -385,13 +387,14 @@ func TestGarbageCollectorControllerSync(t *testing.T) {
|
|||||||
when("there are valid, expired authcode secrets which contain upstream access tokens", func() {
|
when("there are valid, expired authcode secrets which contain upstream access tokens", func() {
|
||||||
it.Before(func() {
|
it.Before(func() {
|
||||||
activeOIDCAuthcodeSession := &authorizationcode.Session{
|
activeOIDCAuthcodeSession := &authorizationcode.Session{
|
||||||
Version: "2",
|
Version: "3",
|
||||||
Active: true,
|
Active: true,
|
||||||
Request: &fosite.Request{
|
Request: &fosite.Request{
|
||||||
ID: "request-id-1",
|
ID: "request-id-1",
|
||||||
Client: &clientregistry.Client{},
|
Client: &clientregistry.Client{},
|
||||||
Session: &psession.PinnipedSession{
|
Session: &psession.PinnipedSession{
|
||||||
Custom: &psession.CustomSessionData{
|
Custom: &psession.CustomSessionData{
|
||||||
|
Username: "should be ignored by garbage collector",
|
||||||
ProviderUID: "upstream-oidc-provider-uid",
|
ProviderUID: "upstream-oidc-provider-uid",
|
||||||
ProviderName: "upstream-oidc-provider-name",
|
ProviderName: "upstream-oidc-provider-name",
|
||||||
ProviderType: psession.ProviderTypeOIDC,
|
ProviderType: psession.ProviderTypeOIDC,
|
||||||
@ -429,13 +432,14 @@ func TestGarbageCollectorControllerSync(t *testing.T) {
|
|||||||
r.NoError(kubeClient.Tracker().Add(activeOIDCAuthcodeSessionSecret))
|
r.NoError(kubeClient.Tracker().Add(activeOIDCAuthcodeSessionSecret))
|
||||||
|
|
||||||
inactiveOIDCAuthcodeSession := &authorizationcode.Session{
|
inactiveOIDCAuthcodeSession := &authorizationcode.Session{
|
||||||
Version: "2",
|
Version: "3",
|
||||||
Active: false,
|
Active: false,
|
||||||
Request: &fosite.Request{
|
Request: &fosite.Request{
|
||||||
ID: "request-id-2",
|
ID: "request-id-2",
|
||||||
Client: &clientregistry.Client{},
|
Client: &clientregistry.Client{},
|
||||||
Session: &psession.PinnipedSession{
|
Session: &psession.PinnipedSession{
|
||||||
Custom: &psession.CustomSessionData{
|
Custom: &psession.CustomSessionData{
|
||||||
|
Username: "should be ignored by garbage collector",
|
||||||
ProviderUID: "upstream-oidc-provider-uid",
|
ProviderUID: "upstream-oidc-provider-uid",
|
||||||
ProviderName: "upstream-oidc-provider-name",
|
ProviderName: "upstream-oidc-provider-name",
|
||||||
ProviderType: psession.ProviderTypeOIDC,
|
ProviderType: psession.ProviderTypeOIDC,
|
||||||
@ -507,13 +511,14 @@ func TestGarbageCollectorControllerSync(t *testing.T) {
|
|||||||
when("there is an invalid, expired authcode secret", func() {
|
when("there is an invalid, expired authcode secret", func() {
|
||||||
it.Before(func() {
|
it.Before(func() {
|
||||||
invalidOIDCAuthcodeSession := &authorizationcode.Session{
|
invalidOIDCAuthcodeSession := &authorizationcode.Session{
|
||||||
Version: "2",
|
Version: "3",
|
||||||
Active: true,
|
Active: true,
|
||||||
Request: &fosite.Request{
|
Request: &fosite.Request{
|
||||||
ID: "", // it is invalid for there to be a missing request ID
|
ID: "", // it is invalid for there to be a missing request ID
|
||||||
Client: &clientregistry.Client{},
|
Client: &clientregistry.Client{},
|
||||||
Session: &psession.PinnipedSession{
|
Session: &psession.PinnipedSession{
|
||||||
Custom: &psession.CustomSessionData{
|
Custom: &psession.CustomSessionData{
|
||||||
|
Username: "should be ignored by garbage collector",
|
||||||
ProviderUID: "upstream-oidc-provider-uid",
|
ProviderUID: "upstream-oidc-provider-uid",
|
||||||
ProviderName: "upstream-oidc-provider-name",
|
ProviderName: "upstream-oidc-provider-name",
|
||||||
ProviderType: psession.ProviderTypeOIDC,
|
ProviderType: psession.ProviderTypeOIDC,
|
||||||
@ -575,13 +580,14 @@ func TestGarbageCollectorControllerSync(t *testing.T) {
|
|||||||
when("there is a valid, expired authcode secret but its upstream name does not match any existing upstream", func() {
|
when("there is a valid, expired authcode secret but its upstream name does not match any existing upstream", func() {
|
||||||
it.Before(func() {
|
it.Before(func() {
|
||||||
wrongProviderNameOIDCAuthcodeSession := &authorizationcode.Session{
|
wrongProviderNameOIDCAuthcodeSession := &authorizationcode.Session{
|
||||||
Version: "2",
|
Version: "3",
|
||||||
Active: true,
|
Active: true,
|
||||||
Request: &fosite.Request{
|
Request: &fosite.Request{
|
||||||
ID: "request-id-1",
|
ID: "request-id-1",
|
||||||
Client: &clientregistry.Client{},
|
Client: &clientregistry.Client{},
|
||||||
Session: &psession.PinnipedSession{
|
Session: &psession.PinnipedSession{
|
||||||
Custom: &psession.CustomSessionData{
|
Custom: &psession.CustomSessionData{
|
||||||
|
Username: "should be ignored by garbage collector",
|
||||||
ProviderUID: "upstream-oidc-provider-uid",
|
ProviderUID: "upstream-oidc-provider-uid",
|
||||||
ProviderName: "upstream-oidc-provider-name-will-not-match",
|
ProviderName: "upstream-oidc-provider-name-will-not-match",
|
||||||
ProviderType: psession.ProviderTypeOIDC,
|
ProviderType: psession.ProviderTypeOIDC,
|
||||||
@ -645,13 +651,14 @@ func TestGarbageCollectorControllerSync(t *testing.T) {
|
|||||||
when("there is a valid, expired authcode secret but its upstream UID does not match any existing upstream", func() {
|
when("there is a valid, expired authcode secret but its upstream UID does not match any existing upstream", func() {
|
||||||
it.Before(func() {
|
it.Before(func() {
|
||||||
wrongProviderNameOIDCAuthcodeSession := &authorizationcode.Session{
|
wrongProviderNameOIDCAuthcodeSession := &authorizationcode.Session{
|
||||||
Version: "2",
|
Version: "3",
|
||||||
Active: true,
|
Active: true,
|
||||||
Request: &fosite.Request{
|
Request: &fosite.Request{
|
||||||
ID: "request-id-1",
|
ID: "request-id-1",
|
||||||
Client: &clientregistry.Client{},
|
Client: &clientregistry.Client{},
|
||||||
Session: &psession.PinnipedSession{
|
Session: &psession.PinnipedSession{
|
||||||
Custom: &psession.CustomSessionData{
|
Custom: &psession.CustomSessionData{
|
||||||
|
Username: "should be ignored by garbage collector",
|
||||||
ProviderUID: "upstream-oidc-provider-uid-will-not-match",
|
ProviderUID: "upstream-oidc-provider-uid-will-not-match",
|
||||||
ProviderName: "upstream-oidc-provider-name",
|
ProviderName: "upstream-oidc-provider-name",
|
||||||
ProviderType: psession.ProviderTypeOIDC,
|
ProviderType: psession.ProviderTypeOIDC,
|
||||||
@ -715,13 +722,14 @@ func TestGarbageCollectorControllerSync(t *testing.T) {
|
|||||||
when("there is a valid, recently expired authcode secret but the upstream revocation fails", func() {
|
when("there is a valid, recently expired authcode secret but the upstream revocation fails", func() {
|
||||||
it.Before(func() {
|
it.Before(func() {
|
||||||
activeOIDCAuthcodeSession := &authorizationcode.Session{
|
activeOIDCAuthcodeSession := &authorizationcode.Session{
|
||||||
Version: "2",
|
Version: "3",
|
||||||
Active: true,
|
Active: true,
|
||||||
Request: &fosite.Request{
|
Request: &fosite.Request{
|
||||||
ID: "request-id-1",
|
ID: "request-id-1",
|
||||||
Client: &clientregistry.Client{},
|
Client: &clientregistry.Client{},
|
||||||
Session: &psession.PinnipedSession{
|
Session: &psession.PinnipedSession{
|
||||||
Custom: &psession.CustomSessionData{
|
Custom: &psession.CustomSessionData{
|
||||||
|
Username: "should be ignored by garbage collector",
|
||||||
ProviderUID: "upstream-oidc-provider-uid",
|
ProviderUID: "upstream-oidc-provider-uid",
|
||||||
ProviderName: "upstream-oidc-provider-name",
|
ProviderName: "upstream-oidc-provider-name",
|
||||||
ProviderType: psession.ProviderTypeOIDC,
|
ProviderType: psession.ProviderTypeOIDC,
|
||||||
@ -819,13 +827,14 @@ func TestGarbageCollectorControllerSync(t *testing.T) {
|
|||||||
when("there is a valid, long-since expired authcode secret but the upstream revocation fails", func() {
|
when("there is a valid, long-since expired authcode secret but the upstream revocation fails", func() {
|
||||||
it.Before(func() {
|
it.Before(func() {
|
||||||
activeOIDCAuthcodeSession := &authorizationcode.Session{
|
activeOIDCAuthcodeSession := &authorizationcode.Session{
|
||||||
Version: "2",
|
Version: "3",
|
||||||
Active: true,
|
Active: true,
|
||||||
Request: &fosite.Request{
|
Request: &fosite.Request{
|
||||||
ID: "request-id-1",
|
ID: "request-id-1",
|
||||||
Client: &clientregistry.Client{},
|
Client: &clientregistry.Client{},
|
||||||
Session: &psession.PinnipedSession{
|
Session: &psession.PinnipedSession{
|
||||||
Custom: &psession.CustomSessionData{
|
Custom: &psession.CustomSessionData{
|
||||||
|
Username: "should be ignored by garbage collector",
|
||||||
ProviderUID: "upstream-oidc-provider-uid",
|
ProviderUID: "upstream-oidc-provider-uid",
|
||||||
ProviderName: "upstream-oidc-provider-name",
|
ProviderName: "upstream-oidc-provider-name",
|
||||||
ProviderType: psession.ProviderTypeOIDC,
|
ProviderType: psession.ProviderTypeOIDC,
|
||||||
@ -897,13 +906,14 @@ func TestGarbageCollectorControllerSync(t *testing.T) {
|
|||||||
when("there are valid, expired access token secrets which contain upstream refresh tokens", func() {
|
when("there are valid, expired access token secrets which contain upstream refresh tokens", func() {
|
||||||
it.Before(func() {
|
it.Before(func() {
|
||||||
offlineAccessGrantedOIDCAccessTokenSession := &accesstoken.Session{
|
offlineAccessGrantedOIDCAccessTokenSession := &accesstoken.Session{
|
||||||
Version: "2",
|
Version: "3",
|
||||||
Request: &fosite.Request{
|
Request: &fosite.Request{
|
||||||
GrantedScope: fosite.Arguments{"scope1", "scope2", "offline_access"},
|
GrantedScope: fosite.Arguments{"scope1", "scope2", "offline_access"},
|
||||||
ID: "request-id-1",
|
ID: "request-id-1",
|
||||||
Client: &clientregistry.Client{},
|
Client: &clientregistry.Client{},
|
||||||
Session: &psession.PinnipedSession{
|
Session: &psession.PinnipedSession{
|
||||||
Custom: &psession.CustomSessionData{
|
Custom: &psession.CustomSessionData{
|
||||||
|
Username: "should be ignored by garbage collector",
|
||||||
ProviderUID: "upstream-oidc-provider-uid",
|
ProviderUID: "upstream-oidc-provider-uid",
|
||||||
ProviderName: "upstream-oidc-provider-name",
|
ProviderName: "upstream-oidc-provider-name",
|
||||||
ProviderType: psession.ProviderTypeOIDC,
|
ProviderType: psession.ProviderTypeOIDC,
|
||||||
@ -941,13 +951,14 @@ func TestGarbageCollectorControllerSync(t *testing.T) {
|
|||||||
r.NoError(kubeClient.Tracker().Add(offlineAccessGrantedOIDCAccessTokenSessionSecret))
|
r.NoError(kubeClient.Tracker().Add(offlineAccessGrantedOIDCAccessTokenSessionSecret))
|
||||||
|
|
||||||
offlineAccessNotGrantedOIDCAccessTokenSession := &accesstoken.Session{
|
offlineAccessNotGrantedOIDCAccessTokenSession := &accesstoken.Session{
|
||||||
Version: "2",
|
Version: "3",
|
||||||
Request: &fosite.Request{
|
Request: &fosite.Request{
|
||||||
GrantedScope: fosite.Arguments{"scope1", "scope2"},
|
GrantedScope: fosite.Arguments{"scope1", "scope2"},
|
||||||
ID: "request-id-2",
|
ID: "request-id-2",
|
||||||
Client: &clientregistry.Client{},
|
Client: &clientregistry.Client{},
|
||||||
Session: &psession.PinnipedSession{
|
Session: &psession.PinnipedSession{
|
||||||
Custom: &psession.CustomSessionData{
|
Custom: &psession.CustomSessionData{
|
||||||
|
Username: "should be ignored by garbage collector",
|
||||||
ProviderUID: "upstream-oidc-provider-uid",
|
ProviderUID: "upstream-oidc-provider-uid",
|
||||||
ProviderName: "upstream-oidc-provider-name",
|
ProviderName: "upstream-oidc-provider-name",
|
||||||
ProviderType: psession.ProviderTypeOIDC,
|
ProviderType: psession.ProviderTypeOIDC,
|
||||||
@ -1019,13 +1030,14 @@ func TestGarbageCollectorControllerSync(t *testing.T) {
|
|||||||
when("there are valid, expired access token secrets which contain upstream access tokens", func() {
|
when("there are valid, expired access token secrets which contain upstream access tokens", func() {
|
||||||
it.Before(func() {
|
it.Before(func() {
|
||||||
offlineAccessGrantedOIDCAccessTokenSession := &accesstoken.Session{
|
offlineAccessGrantedOIDCAccessTokenSession := &accesstoken.Session{
|
||||||
Version: "2",
|
Version: "3",
|
||||||
Request: &fosite.Request{
|
Request: &fosite.Request{
|
||||||
GrantedScope: fosite.Arguments{"scope1", "scope2", "offline_access"},
|
GrantedScope: fosite.Arguments{"scope1", "scope2", "offline_access"},
|
||||||
ID: "request-id-1",
|
ID: "request-id-1",
|
||||||
Client: &clientregistry.Client{},
|
Client: &clientregistry.Client{},
|
||||||
Session: &psession.PinnipedSession{
|
Session: &psession.PinnipedSession{
|
||||||
Custom: &psession.CustomSessionData{
|
Custom: &psession.CustomSessionData{
|
||||||
|
Username: "should be ignored by garbage collector",
|
||||||
ProviderUID: "upstream-oidc-provider-uid",
|
ProviderUID: "upstream-oidc-provider-uid",
|
||||||
ProviderName: "upstream-oidc-provider-name",
|
ProviderName: "upstream-oidc-provider-name",
|
||||||
ProviderType: psession.ProviderTypeOIDC,
|
ProviderType: psession.ProviderTypeOIDC,
|
||||||
@ -1063,13 +1075,14 @@ func TestGarbageCollectorControllerSync(t *testing.T) {
|
|||||||
r.NoError(kubeClient.Tracker().Add(offlineAccessGrantedOIDCAccessTokenSessionSecret))
|
r.NoError(kubeClient.Tracker().Add(offlineAccessGrantedOIDCAccessTokenSessionSecret))
|
||||||
|
|
||||||
offlineAccessNotGrantedOIDCAccessTokenSession := &accesstoken.Session{
|
offlineAccessNotGrantedOIDCAccessTokenSession := &accesstoken.Session{
|
||||||
Version: "2",
|
Version: "3",
|
||||||
Request: &fosite.Request{
|
Request: &fosite.Request{
|
||||||
GrantedScope: fosite.Arguments{"scope1", "scope2"},
|
GrantedScope: fosite.Arguments{"scope1", "scope2"},
|
||||||
ID: "request-id-2",
|
ID: "request-id-2",
|
||||||
Client: &clientregistry.Client{},
|
Client: &clientregistry.Client{},
|
||||||
Session: &psession.PinnipedSession{
|
Session: &psession.PinnipedSession{
|
||||||
Custom: &psession.CustomSessionData{
|
Custom: &psession.CustomSessionData{
|
||||||
|
Username: "should be ignored by garbage collector",
|
||||||
ProviderUID: "upstream-oidc-provider-uid",
|
ProviderUID: "upstream-oidc-provider-uid",
|
||||||
ProviderName: "upstream-oidc-provider-name",
|
ProviderName: "upstream-oidc-provider-name",
|
||||||
ProviderType: psession.ProviderTypeOIDC,
|
ProviderType: psession.ProviderTypeOIDC,
|
||||||
@ -1141,12 +1154,13 @@ func TestGarbageCollectorControllerSync(t *testing.T) {
|
|||||||
when("there are valid, expired refresh secrets which contain upstream refresh tokens", func() {
|
when("there are valid, expired refresh secrets which contain upstream refresh tokens", func() {
|
||||||
it.Before(func() {
|
it.Before(func() {
|
||||||
oidcRefreshSession := &refreshtoken.Session{
|
oidcRefreshSession := &refreshtoken.Session{
|
||||||
Version: "2",
|
Version: "3",
|
||||||
Request: &fosite.Request{
|
Request: &fosite.Request{
|
||||||
ID: "request-id-1",
|
ID: "request-id-1",
|
||||||
Client: &clientregistry.Client{},
|
Client: &clientregistry.Client{},
|
||||||
Session: &psession.PinnipedSession{
|
Session: &psession.PinnipedSession{
|
||||||
Custom: &psession.CustomSessionData{
|
Custom: &psession.CustomSessionData{
|
||||||
|
Username: "should be ignored by garbage collector",
|
||||||
ProviderUID: "upstream-oidc-provider-uid",
|
ProviderUID: "upstream-oidc-provider-uid",
|
||||||
ProviderName: "upstream-oidc-provider-name",
|
ProviderName: "upstream-oidc-provider-name",
|
||||||
ProviderType: psession.ProviderTypeOIDC,
|
ProviderType: psession.ProviderTypeOIDC,
|
||||||
@ -1217,12 +1231,13 @@ func TestGarbageCollectorControllerSync(t *testing.T) {
|
|||||||
when("there are valid, expired refresh secrets which contain upstream access tokens", func() {
|
when("there are valid, expired refresh secrets which contain upstream access tokens", func() {
|
||||||
it.Before(func() {
|
it.Before(func() {
|
||||||
oidcRefreshSession := &refreshtoken.Session{
|
oidcRefreshSession := &refreshtoken.Session{
|
||||||
Version: "2",
|
Version: "3",
|
||||||
Request: &fosite.Request{
|
Request: &fosite.Request{
|
||||||
ID: "request-id-1",
|
ID: "request-id-1",
|
||||||
Client: &clientregistry.Client{},
|
Client: &clientregistry.Client{},
|
||||||
Session: &psession.PinnipedSession{
|
Session: &psession.PinnipedSession{
|
||||||
Custom: &psession.CustomSessionData{
|
Custom: &psession.CustomSessionData{
|
||||||
|
Username: "should be ignored by garbage collector",
|
||||||
ProviderUID: "upstream-oidc-provider-uid",
|
ProviderUID: "upstream-oidc-provider-uid",
|
||||||
ProviderName: "upstream-oidc-provider-name",
|
ProviderName: "upstream-oidc-provider-name",
|
||||||
ProviderType: psession.ProviderTypeOIDC,
|
ProviderType: psession.ProviderTypeOIDC,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
package accesstoken
|
package accesstoken
|
||||||
@ -29,7 +29,8 @@ const (
|
|||||||
|
|
||||||
// Version 1 was the initial release of storage.
|
// Version 1 was the initial release of storage.
|
||||||
// Version 2 is when we switched to storing psession.PinnipedSession inside the fosite request.
|
// Version 2 is when we switched to storing psession.PinnipedSession inside the fosite request.
|
||||||
accessTokenStorageVersion = "2"
|
// Version 3 is when we added the Username field to the psession.CustomSessionData.
|
||||||
|
accessTokenStorageVersion = "3"
|
||||||
)
|
)
|
||||||
|
|
||||||
type RevocationStorage interface {
|
type RevocationStorage interface {
|
||||||
|
@ -53,7 +53,7 @@ func TestAccessTokenStorage(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Data: map[string][]byte{
|
Data: map[string][]byte{
|
||||||
"pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"custom":{"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"2"}`),
|
"pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"custom":{"username":"fake-username","providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"3"}`),
|
||||||
"pinniped-storage-version": []byte("1"),
|
"pinniped-storage-version": []byte("1"),
|
||||||
},
|
},
|
||||||
Type: "storage.pinniped.dev/access-token",
|
Type: "storage.pinniped.dev/access-token",
|
||||||
@ -122,7 +122,7 @@ func TestAccessTokenStorageRevocation(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Data: map[string][]byte{
|
Data: map[string][]byte{
|
||||||
"pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"custom":{"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"2"}`),
|
"pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"custom":{"username":"fake-username","providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"3"}`),
|
||||||
"pinniped-storage-version": []byte("1"),
|
"pinniped-storage-version": []byte("1"),
|
||||||
},
|
},
|
||||||
Type: "storage.pinniped.dev/access-token",
|
Type: "storage.pinniped.dev/access-token",
|
||||||
@ -195,7 +195,7 @@ func TestWrongVersion(t *testing.T) {
|
|||||||
|
|
||||||
_, err = storage.GetAccessTokenSession(ctx, "fancy-signature", nil)
|
_, err = storage.GetAccessTokenSession(ctx, "fancy-signature", nil)
|
||||||
|
|
||||||
require.EqualError(t, err, "access token request data has wrong version: access token session for fancy-signature has version not-the-right-version instead of 2")
|
require.EqualError(t, err, "access token request data has wrong version: access token session for fancy-signature has version not-the-right-version instead of 3")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNilSessionRequest(t *testing.T) {
|
func TestNilSessionRequest(t *testing.T) {
|
||||||
@ -213,7 +213,7 @@ func TestNilSessionRequest(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Data: map[string][]byte{
|
Data: map[string][]byte{
|
||||||
"pinniped-storage-data": []byte(`{"nonsense-key": "nonsense-value","version":"2"}`),
|
"pinniped-storage-data": []byte(`{"nonsense-key": "nonsense-value","version":"3"}`),
|
||||||
"pinniped-storage-version": []byte("1"),
|
"pinniped-storage-version": []byte("1"),
|
||||||
},
|
},
|
||||||
Type: "storage.pinniped.dev/access-token",
|
Type: "storage.pinniped.dev/access-token",
|
||||||
@ -297,13 +297,13 @@ func TestReadFromSecret(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Data: map[string][]byte{
|
Data: map[string][]byte{
|
||||||
"pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","session":{"fosite":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"custom":{"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token"}}}},"version":"2","active": true}`),
|
"pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","session":{"fosite":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"custom":{"username":"fake-username","providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token"}}}},"version":"3","active": true}`),
|
||||||
"pinniped-storage-version": []byte("1"),
|
"pinniped-storage-version": []byte("1"),
|
||||||
},
|
},
|
||||||
Type: "storage.pinniped.dev/access-token",
|
Type: "storage.pinniped.dev/access-token",
|
||||||
},
|
},
|
||||||
wantSession: &Session{
|
wantSession: &Session{
|
||||||
Version: "2",
|
Version: "3",
|
||||||
Request: &fosite.Request{
|
Request: &fosite.Request{
|
||||||
ID: "abcd-1",
|
ID: "abcd-1",
|
||||||
Client: &clientregistry.Client{},
|
Client: &clientregistry.Client{},
|
||||||
@ -313,6 +313,7 @@ func TestReadFromSecret(t *testing.T) {
|
|||||||
Subject: "panda",
|
Subject: "panda",
|
||||||
},
|
},
|
||||||
Custom: &psession.CustomSessionData{
|
Custom: &psession.CustomSessionData{
|
||||||
|
Username: "fake-username",
|
||||||
ProviderUID: "fake-provider-uid",
|
ProviderUID: "fake-provider-uid",
|
||||||
ProviderName: "fake-provider-name",
|
ProviderName: "fake-provider-name",
|
||||||
ProviderType: "fake-provider-type",
|
ProviderType: "fake-provider-type",
|
||||||
@ -335,7 +336,7 @@ func TestReadFromSecret(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Data: map[string][]byte{
|
Data: map[string][]byte{
|
||||||
"pinniped-storage-data": []byte(`{"request":{"id":"abcd-1"},"version":"2","active": true}`),
|
"pinniped-storage-data": []byte(`{"request":{"id":"abcd-1"},"version":"3","active": true}`),
|
||||||
"pinniped-storage-version": []byte("1"),
|
"pinniped-storage-version": []byte("1"),
|
||||||
},
|
},
|
||||||
Type: "storage.pinniped.dev/not-access-token",
|
Type: "storage.pinniped.dev/not-access-token",
|
||||||
@ -358,7 +359,7 @@ func TestReadFromSecret(t *testing.T) {
|
|||||||
},
|
},
|
||||||
Type: "storage.pinniped.dev/access-token",
|
Type: "storage.pinniped.dev/access-token",
|
||||||
},
|
},
|
||||||
wantErr: "access token request data has wrong version: access token session has version wrong-version-here instead of 2",
|
wantErr: "access token request data has wrong version: access token session has version wrong-version-here instead of 3",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "missing request",
|
name: "missing request",
|
||||||
@ -371,7 +372,7 @@ func TestReadFromSecret(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Data: map[string][]byte{
|
Data: map[string][]byte{
|
||||||
"pinniped-storage-data": []byte(`{"version":"2","active": true}`),
|
"pinniped-storage-data": []byte(`{"version":"3","active": true}`),
|
||||||
"pinniped-storage-version": []byte("1"),
|
"pinniped-storage-version": []byte("1"),
|
||||||
},
|
},
|
||||||
Type: "storage.pinniped.dev/access-token",
|
Type: "storage.pinniped.dev/access-token",
|
||||||
|
@ -30,7 +30,8 @@ const (
|
|||||||
|
|
||||||
// Version 1 was the initial release of storage.
|
// Version 1 was the initial release of storage.
|
||||||
// Version 2 is when we switched to storing psession.PinnipedSession inside the fosite request.
|
// Version 2 is when we switched to storing psession.PinnipedSession inside the fosite request.
|
||||||
authorizeCodeStorageVersion = "2"
|
// Version 3 is when we added the Username field to the psession.CustomSessionData.
|
||||||
|
authorizeCodeStorageVersion = "3"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ oauth2.AuthorizeCodeStorage = &authorizeCodeStorage{}
|
var _ oauth2.AuthorizeCodeStorage = &authorizeCodeStorage{}
|
||||||
@ -366,45 +367,43 @@ const ExpectedAuthorizeCodeSessionJSONFromFuzzing = `{
|
|||||||
"Subject": "\u0026¥潝邎Ȗ莅ŝǔ盕戙鵮碡ʯiŬŽ"
|
"Subject": "\u0026¥潝邎Ȗ莅ŝǔ盕戙鵮碡ʯiŬŽ"
|
||||||
},
|
},
|
||||||
"custom": {
|
"custom": {
|
||||||
"providerUID": "Ĝ眧Ĭ",
|
"username": "Ĝ眧Ĭ",
|
||||||
"providerName": "ʼn2ƋŢ觛ǂ焺nŐǛ",
|
"providerUID": "ʼn2ƋŢ觛ǂ焺nŐǛ",
|
||||||
"providerType": "ɥ闣ʬ橳(ý綃ʃʚƟ覣k眐4",
|
"providerName": "ɥ闣ʬ橳(ý綃ʃʚƟ覣k眐4",
|
||||||
|
"providerType": "ȣ掘ʃƸ澺淗a紽ǒ|鰽",
|
||||||
"warnings": [
|
"warnings": [
|
||||||
"掘ʃƸ澺淗a紽ǒ|鰽ŋ猊",
|
"t毇妬\u003e6鉢緋uƴŤȱʀļÂ",
|
||||||
"毇妬\u003e6鉢緋uƴŤȱʀļÂ?"
|
"虝27就伒犘c钡ɏȫ齁š"
|
||||||
],
|
],
|
||||||
"oidc": {
|
"oidc": {
|
||||||
"upstreamRefreshToken": "\u003cƬb",
|
"upstreamRefreshToken": "OpKȱ藚ɏ¬Ê蒭堜]ȗ韚ʫ繕ȫ碰+ʫ",
|
||||||
"upstreamAccessToken": "犘c钡ɏȫ",
|
"upstreamAccessToken": "k9帴",
|
||||||
"upstreamSubject": "鬌",
|
"upstreamSubject": "磊ůď逳鞪?3)藵睋邔\u0026Ű惫蜀Ģ",
|
||||||
"upstreamIssuer": "%OpKȱ藚ɏ¬Ê蒭堜"
|
"upstreamIssuer": "4İ"
|
||||||
},
|
},
|
||||||
"ldap": {
|
"ldap": {
|
||||||
"userDN": "ȗ韚ʫ繕ȫ碰+",
|
"userDN": "×",
|
||||||
"extraRefreshAttributes": {
|
"extraRefreshAttributes": {
|
||||||
"+î艔垎0": "ĝ",
|
"ʥ笿0D": "s"
|
||||||
"4İ": "墀jMʥ",
|
|
||||||
"k9帴": "磊ůď逳鞪?3)藵睋邔\u0026Ű惫蜀Ģ"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"activedirectory": {
|
"activedirectory": {
|
||||||
"userDN": "%Ä摱ìÓȐĨf跞@)¿,ɭS隑i",
|
"userDN": "ĝ",
|
||||||
"extraRefreshAttributes": {
|
"extraRefreshAttributes": {
|
||||||
" 皦pSǬŝ社Vƅȭǝ*擦28Dž": "vư",
|
"IȽ齤士bEǎ": "跞@)¿,ɭS隑ip偶宾儮猷V麹",
|
||||||
"艱iYn面@yȝƋ鬯犦獢9c5¤.岵": "浛a齙\\蹼偦歛"
|
"ȝƋ鬯犦獢9c5¤.岵": "浛a齙\\蹼偦歛"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"requestedAudience": [
|
"requestedAudience": [
|
||||||
"置b",
|
" 皦pSǬŝ社Vƅȭǝ*擦28Dž",
|
||||||
"筫MN\u0026錝D肁Ŷɽ蔒PR}Ųʓl{"
|
"vư"
|
||||||
],
|
],
|
||||||
"grantedAudience": [
|
"grantedAudience": [
|
||||||
"jÃ轘屔挝",
|
"置b",
|
||||||
"Œų崓ļ憽-蹐È_¸]fś",
|
"筫MN\u0026錝D肁Ŷɽ蔒PR}Ųʓl{"
|
||||||
"ɵʮGɃɫ囤1+,Ȳ"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"version": "2"
|
"version": "3"
|
||||||
}`
|
}`
|
||||||
|
@ -65,7 +65,7 @@ func TestAuthorizationCodeStorage(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Data: map[string][]byte{
|
Data: map[string][]byte{
|
||||||
"pinniped-storage-data": []byte(`{"active":true,"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"custom":{"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"2"}`),
|
"pinniped-storage-data": []byte(`{"active":true,"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"custom":{"username":"fake-username","providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"3"}`),
|
||||||
"pinniped-storage-version": []byte("1"),
|
"pinniped-storage-version": []byte("1"),
|
||||||
},
|
},
|
||||||
Type: "storage.pinniped.dev/authcode",
|
Type: "storage.pinniped.dev/authcode",
|
||||||
@ -84,7 +84,7 @@ func TestAuthorizationCodeStorage(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Data: map[string][]byte{
|
Data: map[string][]byte{
|
||||||
"pinniped-storage-data": []byte(`{"active":false,"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"custom":{"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"2"}`),
|
"pinniped-storage-data": []byte(`{"active":false,"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"custom":{"username":"fake-username","providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"3"}`),
|
||||||
"pinniped-storage-version": []byte("1"),
|
"pinniped-storage-version": []byte("1"),
|
||||||
},
|
},
|
||||||
Type: "storage.pinniped.dev/authcode",
|
Type: "storage.pinniped.dev/authcode",
|
||||||
@ -202,7 +202,7 @@ func TestWrongVersion(t *testing.T) {
|
|||||||
|
|
||||||
_, err = storage.GetAuthorizeCodeSession(ctx, "fancy-signature", nil)
|
_, err = storage.GetAuthorizeCodeSession(ctx, "fancy-signature", nil)
|
||||||
|
|
||||||
require.EqualError(t, err, "authorization request data has wrong version: authorization code session for fancy-signature has version not-the-right-version instead of 2")
|
require.EqualError(t, err, "authorization request data has wrong version: authorization code session for fancy-signature has version not-the-right-version instead of 3")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNilSessionRequest(t *testing.T) {
|
func TestNilSessionRequest(t *testing.T) {
|
||||||
@ -217,7 +217,7 @@ func TestNilSessionRequest(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Data: map[string][]byte{
|
Data: map[string][]byte{
|
||||||
"pinniped-storage-data": []byte(`{"nonsense-key": "nonsense-value", "version":"2", "active": true}`),
|
"pinniped-storage-data": []byte(`{"nonsense-key": "nonsense-value", "version":"3", "active": true}`),
|
||||||
"pinniped-storage-version": []byte("1"),
|
"pinniped-storage-version": []byte("1"),
|
||||||
},
|
},
|
||||||
Type: "storage.pinniped.dev/authcode",
|
Type: "storage.pinniped.dev/authcode",
|
||||||
@ -384,7 +384,7 @@ func TestFuzzAndJSONNewValidEmptyAuthorizeCodeSession(t *testing.T) {
|
|||||||
|
|
||||||
// set these to match CreateAuthorizeCodeSession so that .JSONEq works
|
// set these to match CreateAuthorizeCodeSession so that .JSONEq works
|
||||||
validSession.Active = true
|
validSession.Active = true
|
||||||
validSession.Version = "2"
|
validSession.Version = "3"
|
||||||
|
|
||||||
validSessionJSONBytes, err := json.MarshalIndent(validSession, "", "\t")
|
validSessionJSONBytes, err := json.MarshalIndent(validSession, "", "\t")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -419,13 +419,13 @@ func TestReadFromSecret(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Data: map[string][]byte{
|
Data: map[string][]byte{
|
||||||
"pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","session":{"fosite":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"custom":{"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token"}}}},"version":"2","active": true}`),
|
"pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","session":{"fosite":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"custom":{"username":"fake-username","providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token"}}}},"version":"3","active": true}`),
|
||||||
"pinniped-storage-version": []byte("1"),
|
"pinniped-storage-version": []byte("1"),
|
||||||
},
|
},
|
||||||
Type: "storage.pinniped.dev/authcode",
|
Type: "storage.pinniped.dev/authcode",
|
||||||
},
|
},
|
||||||
wantSession: &Session{
|
wantSession: &Session{
|
||||||
Version: "2",
|
Version: "3",
|
||||||
Active: true,
|
Active: true,
|
||||||
Request: &fosite.Request{
|
Request: &fosite.Request{
|
||||||
ID: "abcd-1",
|
ID: "abcd-1",
|
||||||
@ -436,6 +436,7 @@ func TestReadFromSecret(t *testing.T) {
|
|||||||
Subject: "panda",
|
Subject: "panda",
|
||||||
},
|
},
|
||||||
Custom: &psession.CustomSessionData{
|
Custom: &psession.CustomSessionData{
|
||||||
|
Username: "fake-username",
|
||||||
ProviderUID: "fake-provider-uid",
|
ProviderUID: "fake-provider-uid",
|
||||||
ProviderName: "fake-provider-name",
|
ProviderName: "fake-provider-name",
|
||||||
ProviderType: "fake-provider-type",
|
ProviderType: "fake-provider-type",
|
||||||
@ -458,7 +459,7 @@ func TestReadFromSecret(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Data: map[string][]byte{
|
Data: map[string][]byte{
|
||||||
"pinniped-storage-data": []byte(`{"request":{"id":"abcd-1"},"version":"2","active": true}`),
|
"pinniped-storage-data": []byte(`{"request":{"id":"abcd-1"},"version":"3","active": true}`),
|
||||||
"pinniped-storage-version": []byte("1"),
|
"pinniped-storage-version": []byte("1"),
|
||||||
},
|
},
|
||||||
Type: "storage.pinniped.dev/not-authcode",
|
Type: "storage.pinniped.dev/not-authcode",
|
||||||
@ -481,7 +482,7 @@ func TestReadFromSecret(t *testing.T) {
|
|||||||
},
|
},
|
||||||
Type: "storage.pinniped.dev/authcode",
|
Type: "storage.pinniped.dev/authcode",
|
||||||
},
|
},
|
||||||
wantErr: "authorization request data has wrong version: authorization code session has version wrong-version-here instead of 2",
|
wantErr: "authorization request data has wrong version: authorization code session has version wrong-version-here instead of 3",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "missing request",
|
name: "missing request",
|
||||||
@ -494,7 +495,7 @@ func TestReadFromSecret(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Data: map[string][]byte{
|
Data: map[string][]byte{
|
||||||
"pinniped-storage-data": []byte(`{"version":"2","active": true}`),
|
"pinniped-storage-data": []byte(`{"version":"3","active": true}`),
|
||||||
"pinniped-storage-version": []byte("1"),
|
"pinniped-storage-version": []byte("1"),
|
||||||
},
|
},
|
||||||
Type: "storage.pinniped.dev/authcode",
|
Type: "storage.pinniped.dev/authcode",
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
package openidconnect
|
package openidconnect
|
||||||
@ -30,7 +30,8 @@ const (
|
|||||||
|
|
||||||
// Version 1 was the initial release of storage.
|
// Version 1 was the initial release of storage.
|
||||||
// Version 2 is when we switched to storing psession.PinnipedSession inside the fosite request.
|
// Version 2 is when we switched to storing psession.PinnipedSession inside the fosite request.
|
||||||
oidcStorageVersion = "2"
|
// Version 3 is when we added the Username field to the psession.CustomSessionData.
|
||||||
|
oidcStorageVersion = "3"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ openid.OpenIDConnectRequestStorage = &openIDConnectRequestStorage{}
|
var _ openid.OpenIDConnectRequestStorage = &openIDConnectRequestStorage{}
|
||||||
|
@ -52,7 +52,7 @@ func TestOpenIdConnectStorage(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Data: map[string][]byte{
|
Data: map[string][]byte{
|
||||||
"pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"custom":{"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"2"}`),
|
"pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"custom":{"username":"fake-username","providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"3"}`),
|
||||||
"pinniped-storage-version": []byte("1"),
|
"pinniped-storage-version": []byte("1"),
|
||||||
},
|
},
|
||||||
Type: "storage.pinniped.dev/oidc",
|
Type: "storage.pinniped.dev/oidc",
|
||||||
@ -137,7 +137,7 @@ func TestWrongVersion(t *testing.T) {
|
|||||||
|
|
||||||
_, err = storage.GetOpenIDConnectSession(ctx, "fancy-code.fancy-signature", nil)
|
_, err = storage.GetOpenIDConnectSession(ctx, "fancy-code.fancy-signature", nil)
|
||||||
|
|
||||||
require.EqualError(t, err, "oidc request data has wrong version: oidc session for fancy-signature has version not-the-right-version instead of 2")
|
require.EqualError(t, err, "oidc request data has wrong version: oidc session for fancy-signature has version not-the-right-version instead of 3")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNilSessionRequest(t *testing.T) {
|
func TestNilSessionRequest(t *testing.T) {
|
||||||
@ -152,7 +152,7 @@ func TestNilSessionRequest(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Data: map[string][]byte{
|
Data: map[string][]byte{
|
||||||
"pinniped-storage-data": []byte(`{"nonsense-key": "nonsense-value","version":"2"}`),
|
"pinniped-storage-data": []byte(`{"nonsense-key": "nonsense-value","version":"3"}`),
|
||||||
"pinniped-storage-version": []byte("1"),
|
"pinniped-storage-version": []byte("1"),
|
||||||
},
|
},
|
||||||
Type: "storage.pinniped.dev/oidc",
|
Type: "storage.pinniped.dev/oidc",
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
package pkce
|
package pkce
|
||||||
@ -28,7 +28,8 @@ const (
|
|||||||
|
|
||||||
// Version 1 was the initial release of storage.
|
// Version 1 was the initial release of storage.
|
||||||
// Version 2 is when we switched to storing psession.PinnipedSession inside the fosite request.
|
// Version 2 is when we switched to storing psession.PinnipedSession inside the fosite request.
|
||||||
pkceStorageVersion = "2"
|
// Version 3 is when we added the Username field to the psession.CustomSessionData.
|
||||||
|
pkceStorageVersion = "3"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ pkce.PKCERequestStorage = &pkceStorage{}
|
var _ pkce.PKCERequestStorage = &pkceStorage{}
|
||||||
|
@ -52,7 +52,7 @@ func TestPKCEStorage(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Data: map[string][]byte{
|
Data: map[string][]byte{
|
||||||
"pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"custom":{"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"2"}`),
|
"pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"custom":{"username":"fake-username","providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"3"}`),
|
||||||
"pinniped-storage-version": []byte("1"),
|
"pinniped-storage-version": []byte("1"),
|
||||||
},
|
},
|
||||||
Type: "storage.pinniped.dev/pkce",
|
Type: "storage.pinniped.dev/pkce",
|
||||||
@ -140,7 +140,7 @@ func TestWrongVersion(t *testing.T) {
|
|||||||
|
|
||||||
_, err = storage.GetPKCERequestSession(ctx, "fancy-signature", nil)
|
_, err = storage.GetPKCERequestSession(ctx, "fancy-signature", nil)
|
||||||
|
|
||||||
require.EqualError(t, err, "pkce request data has wrong version: pkce session for fancy-signature has version not-the-right-version instead of 2")
|
require.EqualError(t, err, "pkce request data has wrong version: pkce session for fancy-signature has version not-the-right-version instead of 3")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNilSessionRequest(t *testing.T) {
|
func TestNilSessionRequest(t *testing.T) {
|
||||||
@ -158,7 +158,7 @@ func TestNilSessionRequest(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Data: map[string][]byte{
|
Data: map[string][]byte{
|
||||||
"pinniped-storage-data": []byte(`{"nonsense-key": "nonsense-value","version":"2"}`),
|
"pinniped-storage-data": []byte(`{"nonsense-key": "nonsense-value","version":"3"}`),
|
||||||
"pinniped-storage-version": []byte("1"),
|
"pinniped-storage-version": []byte("1"),
|
||||||
},
|
},
|
||||||
Type: "storage.pinniped.dev/pkce",
|
Type: "storage.pinniped.dev/pkce",
|
||||||
|
@ -29,7 +29,8 @@ const (
|
|||||||
|
|
||||||
// Version 1 was the initial release of storage.
|
// Version 1 was the initial release of storage.
|
||||||
// Version 2 is when we switched to storing psession.PinnipedSession inside the fosite request.
|
// Version 2 is when we switched to storing psession.PinnipedSession inside the fosite request.
|
||||||
refreshTokenStorageVersion = "2"
|
// Version 3 is when we added the Username field to the psession.CustomSessionData.
|
||||||
|
refreshTokenStorageVersion = "3"
|
||||||
)
|
)
|
||||||
|
|
||||||
type RevocationStorage interface {
|
type RevocationStorage interface {
|
||||||
|
@ -52,7 +52,7 @@ func TestRefreshTokenStorage(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Data: map[string][]byte{
|
Data: map[string][]byte{
|
||||||
"pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"custom":{"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"2"}`),
|
"pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"custom":{"username":"fake-username","providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"3"}`),
|
||||||
"pinniped-storage-version": []byte("1"),
|
"pinniped-storage-version": []byte("1"),
|
||||||
},
|
},
|
||||||
Type: "storage.pinniped.dev/refresh-token",
|
Type: "storage.pinniped.dev/refresh-token",
|
||||||
@ -122,7 +122,7 @@ func TestRefreshTokenStorageRevocation(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Data: map[string][]byte{
|
Data: map[string][]byte{
|
||||||
"pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"custom":{"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"2"}`),
|
"pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"custom":{"username":"fake-username","providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"3"}`),
|
||||||
"pinniped-storage-version": []byte("1"),
|
"pinniped-storage-version": []byte("1"),
|
||||||
},
|
},
|
||||||
Type: "storage.pinniped.dev/refresh-token",
|
Type: "storage.pinniped.dev/refresh-token",
|
||||||
@ -177,7 +177,7 @@ func TestRefreshTokenStorageRevokeRefreshTokenMaybeGracePeriod(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Data: map[string][]byte{
|
Data: map[string][]byte{
|
||||||
"pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"custom":{"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"2"}`),
|
"pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"custom":{"username":"fake-username","providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"3"}`),
|
||||||
"pinniped-storage-version": []byte("1"),
|
"pinniped-storage-version": []byte("1"),
|
||||||
},
|
},
|
||||||
Type: "storage.pinniped.dev/refresh-token",
|
Type: "storage.pinniped.dev/refresh-token",
|
||||||
@ -251,7 +251,7 @@ func TestWrongVersion(t *testing.T) {
|
|||||||
|
|
||||||
_, err = storage.GetRefreshTokenSession(ctx, "fancy-signature", nil)
|
_, err = storage.GetRefreshTokenSession(ctx, "fancy-signature", nil)
|
||||||
|
|
||||||
require.EqualError(t, err, "refresh token request data has wrong version: refresh token session for fancy-signature has version not-the-right-version instead of 2")
|
require.EqualError(t, err, "refresh token request data has wrong version: refresh token session for fancy-signature has version not-the-right-version instead of 3")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNilSessionRequest(t *testing.T) {
|
func TestNilSessionRequest(t *testing.T) {
|
||||||
@ -269,7 +269,7 @@ func TestNilSessionRequest(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Data: map[string][]byte{
|
Data: map[string][]byte{
|
||||||
"pinniped-storage-data": []byte(`{"nonsense-key": "nonsense-value","version":"2"}`),
|
"pinniped-storage-data": []byte(`{"nonsense-key": "nonsense-value","version":"3"}`),
|
||||||
"pinniped-storage-version": []byte("1"),
|
"pinniped-storage-version": []byte("1"),
|
||||||
},
|
},
|
||||||
Type: "storage.pinniped.dev/refresh-token",
|
Type: "storage.pinniped.dev/refresh-token",
|
||||||
@ -353,13 +353,13 @@ func TestReadFromSecret(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Data: map[string][]byte{
|
Data: map[string][]byte{
|
||||||
"pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","session":{"fosite":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"custom":{"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token"}}}},"version":"2","active": true}`),
|
"pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","session":{"fosite":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"custom":{"username":"fake-username","providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token"}}}},"version":"3","active": true}`),
|
||||||
"pinniped-storage-version": []byte("1"),
|
"pinniped-storage-version": []byte("1"),
|
||||||
},
|
},
|
||||||
Type: "storage.pinniped.dev/refresh-token",
|
Type: "storage.pinniped.dev/refresh-token",
|
||||||
},
|
},
|
||||||
wantSession: &Session{
|
wantSession: &Session{
|
||||||
Version: "2",
|
Version: "3",
|
||||||
Request: &fosite.Request{
|
Request: &fosite.Request{
|
||||||
ID: "abcd-1",
|
ID: "abcd-1",
|
||||||
Client: &clientregistry.Client{},
|
Client: &clientregistry.Client{},
|
||||||
@ -369,6 +369,7 @@ func TestReadFromSecret(t *testing.T) {
|
|||||||
Subject: "panda",
|
Subject: "panda",
|
||||||
},
|
},
|
||||||
Custom: &psession.CustomSessionData{
|
Custom: &psession.CustomSessionData{
|
||||||
|
Username: "fake-username",
|
||||||
ProviderUID: "fake-provider-uid",
|
ProviderUID: "fake-provider-uid",
|
||||||
ProviderName: "fake-provider-name",
|
ProviderName: "fake-provider-name",
|
||||||
ProviderType: "fake-provider-type",
|
ProviderType: "fake-provider-type",
|
||||||
@ -391,7 +392,7 @@ func TestReadFromSecret(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Data: map[string][]byte{
|
Data: map[string][]byte{
|
||||||
"pinniped-storage-data": []byte(`{"request":{"id":"abcd-1"},"version":"2","active": true}`),
|
"pinniped-storage-data": []byte(`{"request":{"id":"abcd-1"},"version":"3","active": true}`),
|
||||||
"pinniped-storage-version": []byte("1"),
|
"pinniped-storage-version": []byte("1"),
|
||||||
},
|
},
|
||||||
Type: "storage.pinniped.dev/not-refresh-token",
|
Type: "storage.pinniped.dev/not-refresh-token",
|
||||||
@ -414,7 +415,7 @@ func TestReadFromSecret(t *testing.T) {
|
|||||||
},
|
},
|
||||||
Type: "storage.pinniped.dev/refresh-token",
|
Type: "storage.pinniped.dev/refresh-token",
|
||||||
},
|
},
|
||||||
wantErr: "refresh token request data has wrong version: refresh token session has version wrong-version-here instead of 2",
|
wantErr: "refresh token request data has wrong version: refresh token session has version wrong-version-here instead of 3",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "missing request",
|
name: "missing request",
|
||||||
@ -427,7 +428,7 @@ func TestReadFromSecret(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Data: map[string][]byte{
|
Data: map[string][]byte{
|
||||||
"pinniped-storage-data": []byte(`{"version":"2","active": true}`),
|
"pinniped-storage-data": []byte(`{"version":"3","active": true}`),
|
||||||
"pinniped-storage-version": []byte("1"),
|
"pinniped-storage-version": []byte("1"),
|
||||||
},
|
},
|
||||||
Type: "storage.pinniped.dev/refresh-token",
|
Type: "storage.pinniped.dev/refresh-token",
|
||||||
|
@ -10,17 +10,15 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
coreosoidc "github.com/coreos/go-oidc/v3/oidc"
|
|
||||||
"github.com/ory/fosite"
|
"github.com/ory/fosite"
|
||||||
"github.com/ory/fosite/handler/openid"
|
"github.com/ory/fosite/handler/openid"
|
||||||
"github.com/ory/fosite/token/jwt"
|
"github.com/ory/fosite/token/jwt"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
|
|
||||||
supervisoroidc "go.pinniped.dev/generated/latest/apis/supervisor/oidc"
|
oidcapi "go.pinniped.dev/generated/latest/apis/supervisor/oidc"
|
||||||
"go.pinniped.dev/internal/httputil/httperr"
|
"go.pinniped.dev/internal/httputil/httperr"
|
||||||
"go.pinniped.dev/internal/httputil/securityheader"
|
"go.pinniped.dev/internal/httputil/securityheader"
|
||||||
"go.pinniped.dev/internal/oidc"
|
"go.pinniped.dev/internal/oidc"
|
||||||
"go.pinniped.dev/internal/oidc/clientregistry"
|
|
||||||
"go.pinniped.dev/internal/oidc/csrftoken"
|
"go.pinniped.dev/internal/oidc/csrftoken"
|
||||||
"go.pinniped.dev/internal/oidc/downstreamsession"
|
"go.pinniped.dev/internal/oidc/downstreamsession"
|
||||||
"go.pinniped.dev/internal/oidc/login"
|
"go.pinniped.dev/internal/oidc/login"
|
||||||
@ -56,12 +54,12 @@ func NewHandler(
|
|||||||
return httperr.Newf(http.StatusMethodNotAllowed, "%s (try GET or POST)", r.Method)
|
return httperr.Newf(http.StatusMethodNotAllowed, "%s (try GET or POST)", r.Method)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note that the client might have used supervisoroidc.AuthorizeUpstreamIDPNameParamName and
|
// Note that the client might have used oidcapi.AuthorizeUpstreamIDPNameParamName and
|
||||||
// supervisoroidc.AuthorizeUpstreamIDPTypeParamName query params to request a certain upstream IDP.
|
// oidcapi.AuthorizeUpstreamIDPTypeParamName query params to request a certain upstream IDP.
|
||||||
// The Pinniped CLI has been sending these params since v0.9.0.
|
// 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
|
// 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
|
// are configured. However, these params should be honored in the future when choosing an upstream
|
||||||
// here, e.g. by calling supervisoroidc.FindUpstreamIDPByNameAndType() when the params are present.
|
// here, e.g. by calling oidcapi.FindUpstreamIDPByNameAndType() when the params are present.
|
||||||
oidcUpstream, ldapUpstream, idpType, err := chooseUpstreamIDP(idpLister)
|
oidcUpstream, ldapUpstream, idpType, err := chooseUpstreamIDP(idpLister)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
plog.WarningErr("authorize upstream config", err)
|
plog.WarningErr("authorize upstream config", err)
|
||||||
@ -69,8 +67,8 @@ func NewHandler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if idpType == psession.ProviderTypeOIDC {
|
if idpType == psession.ProviderTypeOIDC {
|
||||||
if len(r.Header.Values(supervisoroidc.AuthorizeUsernameHeaderName)) > 0 ||
|
if len(r.Header.Values(oidcapi.AuthorizeUsernameHeaderName)) > 0 ||
|
||||||
len(r.Header.Values(supervisoroidc.AuthorizePasswordHeaderName)) > 0 {
|
len(r.Header.Values(oidcapi.AuthorizePasswordHeaderName)) > 0 {
|
||||||
// The client set a username header, so they are trying to log in with a username/password.
|
// The client set a username header, so they are trying to log in with a username/password.
|
||||||
return handleAuthRequestForOIDCUpstreamPasswordGrant(r, w, oauthHelperWithStorage, oidcUpstream)
|
return handleAuthRequestForOIDCUpstreamPasswordGrant(r, w, oauthHelperWithStorage, oidcUpstream)
|
||||||
}
|
}
|
||||||
@ -85,8 +83,8 @@ func NewHandler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// We know it's an AD/LDAP upstream.
|
// We know it's an AD/LDAP upstream.
|
||||||
if len(r.Header.Values(supervisoroidc.AuthorizeUsernameHeaderName)) > 0 ||
|
if len(r.Header.Values(oidcapi.AuthorizeUsernameHeaderName)) > 0 ||
|
||||||
len(r.Header.Values(supervisoroidc.AuthorizePasswordHeaderName)) > 0 {
|
len(r.Header.Values(oidcapi.AuthorizePasswordHeaderName)) > 0 {
|
||||||
// The client set a username header, so they are trying to log in with a username/password.
|
// The client set a username header, so they are trying to log in with a username/password.
|
||||||
return handleAuthRequestForLDAPUpstreamCLIFlow(r, w,
|
return handleAuthRequestForLDAPUpstreamCLIFlow(r, w,
|
||||||
oauthHelperWithStorage,
|
oauthHelperWithStorage,
|
||||||
@ -150,7 +148,7 @@ func handleAuthRequestForLDAPUpstreamCLIFlow(
|
|||||||
subject := downstreamsession.DownstreamSubjectFromUpstreamLDAP(ldapUpstream, authenticateResponse)
|
subject := downstreamsession.DownstreamSubjectFromUpstreamLDAP(ldapUpstream, authenticateResponse)
|
||||||
username = authenticateResponse.User.GetName()
|
username = authenticateResponse.User.GetName()
|
||||||
groups := authenticateResponse.User.GetGroups()
|
groups := authenticateResponse.User.GetGroups()
|
||||||
customSessionData := downstreamsession.MakeDownstreamLDAPOrADCustomSessionData(ldapUpstream, idpType, authenticateResponse)
|
customSessionData := downstreamsession.MakeDownstreamLDAPOrADCustomSessionData(ldapUpstream, idpType, authenticateResponse, username)
|
||||||
openIDSession := downstreamsession.MakeDownstreamSession(subject, username, groups, authorizeRequester.GetGrantedScopes(), customSessionData)
|
openIDSession := downstreamsession.MakeDownstreamSession(subject, username, groups, authorizeRequester.GetGrantedScopes(), customSessionData)
|
||||||
oidc.PerformAuthcodeRedirect(r, w, oauthHelper, authorizeRequester, openIDSession, true)
|
oidc.PerformAuthcodeRedirect(r, w, oauthHelper, authorizeRequester, openIDSession, true)
|
||||||
|
|
||||||
@ -244,7 +242,7 @@ func handleAuthRequestForOIDCUpstreamPasswordGrant(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
customSessionData, err := downstreamsession.MakeDownstreamOIDCCustomSessionData(oidcUpstream, token)
|
customSessionData, err := downstreamsession.MakeDownstreamOIDCCustomSessionData(oidcUpstream, token, username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
oidc.WriteAuthorizeError(w, oauthHelper, authorizeRequester,
|
oidc.WriteAuthorizeError(w, oauthHelper, authorizeRequester,
|
||||||
fosite.ErrAccessDenied.WithHintf("Reason: %s.", err.Error()), true,
|
fosite.ErrAccessDenied.WithHintf("Reason: %s.", err.Error()), true,
|
||||||
@ -322,7 +320,7 @@ func handleAuthRequestForOIDCUpstreamBrowserFlow(
|
|||||||
}
|
}
|
||||||
|
|
||||||
func requireStaticClientForUsernameAndPasswordHeaders(w http.ResponseWriter, oauthHelper fosite.OAuth2Provider, authorizeRequester fosite.AuthorizeRequester) bool {
|
func requireStaticClientForUsernameAndPasswordHeaders(w http.ResponseWriter, oauthHelper fosite.OAuth2Provider, authorizeRequester fosite.AuthorizeRequester) bool {
|
||||||
isStaticClient := authorizeRequester.GetClient().GetID() == clientregistry.PinnipedCLIClientID
|
isStaticClient := authorizeRequester.GetClient().GetID() == oidcapi.ClientIDPinnipedCLI
|
||||||
if !isStaticClient {
|
if !isStaticClient {
|
||||||
oidc.WriteAuthorizeError(w, oauthHelper, authorizeRequester,
|
oidc.WriteAuthorizeError(w, oauthHelper, authorizeRequester,
|
||||||
fosite.ErrAccessDenied.WithHintf("This client is not allowed to submit username or password headers to this endpoint."), true)
|
fosite.ErrAccessDenied.WithHintf("This client is not allowed to submit username or password headers to this endpoint."), true)
|
||||||
@ -331,8 +329,8 @@ func requireStaticClientForUsernameAndPasswordHeaders(w http.ResponseWriter, oau
|
|||||||
}
|
}
|
||||||
|
|
||||||
func requireNonEmptyUsernameAndPasswordHeaders(r *http.Request, w http.ResponseWriter, oauthHelper fosite.OAuth2Provider, authorizeRequester fosite.AuthorizeRequester) (string, string, bool) {
|
func requireNonEmptyUsernameAndPasswordHeaders(r *http.Request, w http.ResponseWriter, oauthHelper fosite.OAuth2Provider, authorizeRequester fosite.AuthorizeRequester) (string, string, bool) {
|
||||||
username := r.Header.Get(supervisoroidc.AuthorizeUsernameHeaderName)
|
username := r.Header.Get(oidcapi.AuthorizeUsernameHeaderName)
|
||||||
password := r.Header.Get(supervisoroidc.AuthorizePasswordHeaderName)
|
password := r.Header.Get(oidcapi.AuthorizePasswordHeaderName)
|
||||||
if username == "" || password == "" {
|
if username == "" || password == "" {
|
||||||
oidc.WriteAuthorizeError(w, oauthHelper, authorizeRequester,
|
oidc.WriteAuthorizeError(w, oauthHelper, authorizeRequester,
|
||||||
fosite.ErrAccessDenied.WithHintf("Missing or blank username or password."), true)
|
fosite.ErrAccessDenied.WithHintf("Missing or blank username or password."), true)
|
||||||
@ -348,13 +346,13 @@ func newAuthorizeRequest(r *http.Request, w http.ResponseWriter, oauthHelper fos
|
|||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Automatically grant the openid, offline_access, pinniped:request-audience, and groups scopes, but only if they were requested.
|
// Automatically grant certain scopes, but only if they were requested.
|
||||||
// Grant the openid scope (for now) if they asked for it so that `NewAuthorizeResponse` will perform its OIDC validations.
|
// 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
|
// 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.
|
// at this time, however we will temporarily grant the scope just in case that changes in a future release of fosite.
|
||||||
// This is instead of asking the user to approve these scopes. Note that `NewAuthorizeRequest` would have returned
|
// 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.
|
// an error if the client requested a scope that they are not allowed to request, so we don't need to worry about that here.
|
||||||
downstreamsession.GrantScopesIfRequested(authorizeRequester, []string{coreosoidc.ScopeOpenID, coreosoidc.ScopeOfflineAccess, oidc.RequestAudienceScope, oidc.DownstreamGroupsScope})
|
downstreamsession.AutoApproveScopes(authorizeRequester)
|
||||||
|
|
||||||
return authorizeRequester, true
|
return authorizeRequester, true
|
||||||
}
|
}
|
||||||
@ -487,7 +485,7 @@ func handleBrowserFlowAuthRequest(
|
|||||||
}
|
}
|
||||||
|
|
||||||
promptParam := r.Form.Get(promptParamName)
|
promptParam := r.Form.Get(promptParamName)
|
||||||
if promptParam == promptParamNone && oidc.ScopeWasRequested(authorizeRequester, coreosoidc.ScopeOpenID) {
|
if promptParam == promptParamNone && oidc.ScopeWasRequested(authorizeRequester, oidcapi.ScopeOpenID) {
|
||||||
oidc.WriteAuthorizeError(w, oauthHelper, authorizeRequester, fosite.ErrLoginRequired, false)
|
oidc.WriteAuthorizeError(w, oauthHelper, authorizeRequester, fosite.ErrLoginRequired, false)
|
||||||
return nil, nil // already wrote the error response, don't return error
|
return nil, nil // already wrote the error response, don't return error
|
||||||
}
|
}
|
||||||
@ -538,8 +536,8 @@ func upstreamStateParam(
|
|||||||
encoder oidc.Encoder,
|
encoder oidc.Encoder,
|
||||||
) (string, error) {
|
) (string, error) {
|
||||||
stateParamData := oidc.UpstreamStateParamData{
|
stateParamData := oidc.UpstreamStateParamData{
|
||||||
// The auth params might have included supervisoroidc.AuthorizeUpstreamIDPNameParamName and
|
// The auth params might have included oidcapi.AuthorizeUpstreamIDPNameParamName and
|
||||||
// supervisoroidc.AuthorizeUpstreamIDPTypeParamName, but those can be ignored by other handlers
|
// oidcapi.AuthorizeUpstreamIDPTypeParamName, but those can be ignored by other handlers
|
||||||
// that are reading from the encoded upstream state param being built here.
|
// that are reading from the encoded upstream state param being built here.
|
||||||
// The UpstreamName and UpstreamType struct fields can be used instead.
|
// The UpstreamName and UpstreamType struct fields can be used instead.
|
||||||
// Remove those params here to avoid potential confusion about which should be used later.
|
// Remove those params here to avoid potential confusion about which should be used later.
|
||||||
@ -565,8 +563,8 @@ func removeCustomIDPParams(params url.Values) url.Values {
|
|||||||
p[k] = v
|
p[k] = v
|
||||||
}
|
}
|
||||||
// Remove the unnecessary params.
|
// Remove the unnecessary params.
|
||||||
delete(p, supervisoroidc.AuthorizeUpstreamIDPNameParamName)
|
delete(p, oidcapi.AuthorizeUpstreamIDPNameParamName)
|
||||||
delete(p, supervisoroidc.AuthorizeUpstreamIDPTypeParamName)
|
delete(p, oidcapi.AuthorizeUpstreamIDPTypeParamName)
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,6 +34,7 @@ import (
|
|||||||
"go.pinniped.dev/internal/oidc"
|
"go.pinniped.dev/internal/oidc"
|
||||||
"go.pinniped.dev/internal/oidc/csrftoken"
|
"go.pinniped.dev/internal/oidc/csrftoken"
|
||||||
"go.pinniped.dev/internal/oidc/jwks"
|
"go.pinniped.dev/internal/oidc/jwks"
|
||||||
|
"go.pinniped.dev/internal/oidc/oidcclientvalidator"
|
||||||
"go.pinniped.dev/internal/oidc/provider"
|
"go.pinniped.dev/internal/oidc/provider"
|
||||||
"go.pinniped.dev/internal/psession"
|
"go.pinniped.dev/internal/psession"
|
||||||
"go.pinniped.dev/internal/testutil"
|
"go.pinniped.dev/internal/testutil"
|
||||||
@ -391,8 +392,8 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
return urlToReturn
|
return urlToReturn
|
||||||
}
|
}
|
||||||
|
|
||||||
happyDownstreamScopesRequested := []string{"openid", "profile", "email", "groups"}
|
happyDownstreamScopesRequested := []string{"openid", "profile", "email", "username", "groups"}
|
||||||
happyDownstreamScopesGranted := []string{"openid", "groups"}
|
happyDownstreamScopesGranted := []string{"openid", "username", "groups"}
|
||||||
|
|
||||||
happyGetRequestQueryMap := map[string]string{
|
happyGetRequestQueryMap := map[string]string{
|
||||||
"response_type": "code",
|
"response_type": "code",
|
||||||
@ -465,6 +466,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
expectedHappyActiveDirectoryUpstreamCustomSession := &psession.CustomSessionData{
|
expectedHappyActiveDirectoryUpstreamCustomSession := &psession.CustomSessionData{
|
||||||
|
Username: happyLDAPUsernameFromAuthenticator,
|
||||||
ProviderUID: activeDirectoryUpstreamResourceUID,
|
ProviderUID: activeDirectoryUpstreamResourceUID,
|
||||||
ProviderName: activeDirectoryUpstreamName,
|
ProviderName: activeDirectoryUpstreamName,
|
||||||
ProviderType: psession.ProviderTypeActiveDirectory,
|
ProviderType: psession.ProviderTypeActiveDirectory,
|
||||||
@ -477,6 +479,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
expectedHappyLDAPUpstreamCustomSession := &psession.CustomSessionData{
|
expectedHappyLDAPUpstreamCustomSession := &psession.CustomSessionData{
|
||||||
|
Username: happyLDAPUsernameFromAuthenticator,
|
||||||
ProviderUID: ldapUpstreamResourceUID,
|
ProviderUID: ldapUpstreamResourceUID,
|
||||||
ProviderName: ldapUpstreamName,
|
ProviderName: ldapUpstreamName,
|
||||||
ProviderType: psession.ProviderTypeLDAP,
|
ProviderType: psession.ProviderTypeLDAP,
|
||||||
@ -489,6 +492,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
expectedHappyOIDCPasswordGrantCustomSession := &psession.CustomSessionData{
|
expectedHappyOIDCPasswordGrantCustomSession := &psession.CustomSessionData{
|
||||||
|
Username: oidcUpstreamUsername,
|
||||||
ProviderUID: oidcPasswordGrantUpstreamResourceUID,
|
ProviderUID: oidcPasswordGrantUpstreamResourceUID,
|
||||||
ProviderName: oidcPasswordGrantUpstreamName,
|
ProviderName: oidcPasswordGrantUpstreamName,
|
||||||
ProviderType: psession.ProviderTypeOIDC,
|
ProviderType: psession.ProviderTypeOIDC,
|
||||||
@ -499,7 +503,16 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
expectedHappyOIDCPasswordGrantCustomSessionWithUsername := func(wantUsername string) *psession.CustomSessionData {
|
||||||
|
copyOfCustomSession := *expectedHappyOIDCPasswordGrantCustomSession
|
||||||
|
copyOfOIDC := *(expectedHappyOIDCPasswordGrantCustomSession.OIDC)
|
||||||
|
copyOfCustomSession.OIDC = ©OfOIDC
|
||||||
|
copyOfCustomSession.Username = wantUsername
|
||||||
|
return ©OfCustomSession
|
||||||
|
}
|
||||||
|
|
||||||
expectedHappyOIDCPasswordGrantCustomSessionWithAccessToken := &psession.CustomSessionData{
|
expectedHappyOIDCPasswordGrantCustomSessionWithAccessToken := &psession.CustomSessionData{
|
||||||
|
Username: oidcUpstreamUsername,
|
||||||
ProviderUID: oidcPasswordGrantUpstreamResourceUID,
|
ProviderUID: oidcPasswordGrantUpstreamResourceUID,
|
||||||
ProviderName: oidcPasswordGrantUpstreamName,
|
ProviderName: oidcPasswordGrantUpstreamName,
|
||||||
ProviderType: psession.ProviderTypeOIDC,
|
ProviderType: psession.ProviderTypeOIDC,
|
||||||
@ -512,13 +525,14 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
|
|
||||||
addFullyCapableDynamicClientAndSecretToKubeResources := func(t *testing.T, supervisorClient *supervisorfake.Clientset, kubeClient *fake.Clientset) {
|
addFullyCapableDynamicClientAndSecretToKubeResources := func(t *testing.T, supervisorClient *supervisorfake.Clientset, kubeClient *fake.Clientset) {
|
||||||
oidcClient, secret := testutil.FullyCapableOIDCClientAndStorageSecret(t,
|
oidcClient, secret := testutil.FullyCapableOIDCClientAndStorageSecret(t,
|
||||||
"some-namespace", dynamicClientID, dynamicClientUID, downstreamRedirectURI, []string{testutil.HashedPassword1AtGoMinCost})
|
"some-namespace", dynamicClientID, dynamicClientUID, downstreamRedirectURI,
|
||||||
|
[]string{testutil.HashedPassword1AtGoMinCost}, oidcclientvalidator.Validate)
|
||||||
require.NoError(t, supervisorClient.Tracker().Add(oidcClient))
|
require.NoError(t, supervisorClient.Tracker().Add(oidcClient))
|
||||||
require.NoError(t, kubeClient.Tracker().Add(secret))
|
require.NoError(t, kubeClient.Tracker().Add(secret))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note that fosite puts the granted scopes as a param in the redirect URI even though the spec doesn't seem to require it
|
// Note that fosite puts the granted scopes as a param in the redirect URI even though the spec doesn't seem to require it
|
||||||
happyAuthcodeDownstreamRedirectLocationRegexp := downstreamRedirectURI + `\?code=([^&]+)&scope=openid\+groups&state=` + happyState
|
happyAuthcodeDownstreamRedirectLocationRegexp := downstreamRedirectURI + `\?code=([^&]+)&scope=openid\+username\+groups&state=` + happyState
|
||||||
|
|
||||||
incomingCookieCSRFValue := "csrf-value-from-cookie"
|
incomingCookieCSRFValue := "csrf-value-from-cookie"
|
||||||
encodedIncomingCookieCSRFValue, err := happyCookieEncoder.Encode("csrf", incomingCookieCSRFValue)
|
encodedIncomingCookieCSRFValue, err := happyCookieEncoder.Encode("csrf", incomingCookieCSRFValue)
|
||||||
@ -528,6 +542,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
name string
|
name string
|
||||||
|
|
||||||
idps *oidctestutil.UpstreamIDPListerBuilder
|
idps *oidctestutil.UpstreamIDPListerBuilder
|
||||||
|
kubeResources func(t *testing.T, supervisorClient *supervisorfake.Clientset, kubeClient *fake.Clientset)
|
||||||
generateCSRF func() (csrftoken.CSRFToken, error)
|
generateCSRF func() (csrftoken.CSRFToken, error)
|
||||||
generatePKCE func() (pkce.Code, error)
|
generatePKCE func() (pkce.Code, error)
|
||||||
generateNonce func() (nonce.Nonce, error)
|
generateNonce func() (nonce.Nonce, error)
|
||||||
@ -540,7 +555,6 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
csrfCookie string
|
csrfCookie string
|
||||||
customUsernameHeader *string // nil means do not send header, empty means send header with empty value
|
customUsernameHeader *string // nil means do not send header, empty means send header with empty value
|
||||||
customPasswordHeader *string // nil means do not send header, empty means send header with empty value
|
customPasswordHeader *string // nil means do not send header, empty means send header with empty value
|
||||||
kubeResources func(t *testing.T, supervisorClient *supervisorfake.Clientset, kubeClient *fake.Clientset)
|
|
||||||
|
|
||||||
wantStatus int
|
wantStatus int
|
||||||
wantContentType string
|
wantContentType string
|
||||||
@ -1122,7 +1136,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
|
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
|
||||||
wantStatus: http.StatusFound,
|
wantStatus: http.StatusFound,
|
||||||
wantContentType: htmlContentType,
|
wantContentType: htmlContentType,
|
||||||
wantRedirectLocationRegexp: downstreamRedirectURIWithDifferentPort + `\?code=([^&]+)&scope=openid\+groups&state=` + happyState,
|
wantRedirectLocationRegexp: downstreamRedirectURIWithDifferentPort + `\?code=([^&]+)&scope=openid\+username\+groups&state=` + happyState,
|
||||||
wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped,
|
wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped,
|
||||||
wantDownstreamIDTokenUsername: oidcUpstreamUsername,
|
wantDownstreamIDTokenUsername: oidcUpstreamUsername,
|
||||||
wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership,
|
wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership,
|
||||||
@ -1145,7 +1159,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
customPasswordHeader: pointer.StringPtr(happyLDAPPassword),
|
customPasswordHeader: pointer.StringPtr(happyLDAPPassword),
|
||||||
wantStatus: http.StatusFound,
|
wantStatus: http.StatusFound,
|
||||||
wantContentType: htmlContentType,
|
wantContentType: htmlContentType,
|
||||||
wantRedirectLocationRegexp: downstreamRedirectURIWithDifferentPort + `\?code=([^&]+)&scope=openid\+groups&state=` + happyState,
|
wantRedirectLocationRegexp: downstreamRedirectURIWithDifferentPort + `\?code=([^&]+)&scope=openid\+username\+groups&state=` + happyState,
|
||||||
wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID,
|
wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID,
|
||||||
wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator,
|
wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator,
|
||||||
wantDownstreamIDTokenGroups: happyLDAPGroups,
|
wantDownstreamIDTokenGroups: happyLDAPGroups,
|
||||||
@ -1219,6 +1233,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
|
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
|
||||||
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
|
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
|
||||||
wantDownstreamCustomSessionData: &psession.CustomSessionData{
|
wantDownstreamCustomSessionData: &psession.CustomSessionData{
|
||||||
|
Username: oidcUpstreamUsername,
|
||||||
ProviderUID: oidcPasswordGrantUpstreamResourceUID,
|
ProviderUID: oidcPasswordGrantUpstreamResourceUID,
|
||||||
ProviderName: oidcPasswordGrantUpstreamName,
|
ProviderName: oidcPasswordGrantUpstreamName,
|
||||||
ProviderType: psession.ProviderTypeOIDC,
|
ProviderType: psession.ProviderTypeOIDC,
|
||||||
@ -2373,13 +2388,13 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
|
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
|
||||||
wantStatus: http.StatusFound,
|
wantStatus: http.StatusFound,
|
||||||
wantContentType: htmlContentType,
|
wantContentType: htmlContentType,
|
||||||
wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=&state=` + happyState, // no scopes granted
|
wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=username\+groups&state=` + happyState, // username and groups scopes were not requested, but are granted anyway for backwards compatibility
|
||||||
wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped,
|
wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped,
|
||||||
wantDownstreamIDTokenUsername: oidcUpstreamUsername,
|
wantDownstreamIDTokenUsername: oidcUpstreamUsername, // username scope was not requested, but is granted anyway for backwards compatibility
|
||||||
wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership,
|
wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership, // groups scope was not requested, but is granted anyway for backwards compatibility
|
||||||
wantDownstreamRequestedScopes: []string{"email"}, // only email was requested
|
wantDownstreamRequestedScopes: []string{"email"}, // only email was requested
|
||||||
wantDownstreamRedirectURI: downstreamRedirectURI,
|
wantDownstreamRedirectURI: downstreamRedirectURI,
|
||||||
wantDownstreamGrantedScopes: []string{}, // no scopes granted
|
wantDownstreamGrantedScopes: []string{"username", "groups"}, // username and groups scopes were not requested, but are granted anyway for backwards compatibility
|
||||||
wantDownstreamNonce: downstreamNonce,
|
wantDownstreamNonce: downstreamNonce,
|
||||||
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
|
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
|
||||||
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
|
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
|
||||||
@ -2395,13 +2410,13 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
customPasswordHeader: pointer.StringPtr(happyLDAPPassword),
|
customPasswordHeader: pointer.StringPtr(happyLDAPPassword),
|
||||||
wantStatus: http.StatusFound,
|
wantStatus: http.StatusFound,
|
||||||
wantContentType: htmlContentType,
|
wantContentType: htmlContentType,
|
||||||
wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=&state=` + happyState, // no scopes granted
|
wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=username\+groups&state=` + happyState, // username and groups scopes were not requested, but are granted anyway for backwards compatibility
|
||||||
wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID,
|
wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID,
|
||||||
wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator,
|
wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator, // username scope was not requested, but is granted anyway for backwards compatibility
|
||||||
wantDownstreamIDTokenGroups: happyLDAPGroups,
|
wantDownstreamIDTokenGroups: happyLDAPGroups, // groups scope was not requested, but is granted anyway for backwards compatibility
|
||||||
wantDownstreamRequestedScopes: []string{"email"}, // only email was requested
|
wantDownstreamRequestedScopes: []string{"email"}, // only email was requested
|
||||||
wantDownstreamRedirectURI: downstreamRedirectURI,
|
wantDownstreamRedirectURI: downstreamRedirectURI,
|
||||||
wantDownstreamGrantedScopes: []string{}, // no scopes granted
|
wantDownstreamGrantedScopes: []string{"username", "groups"}, // username and groups scopes were not requested, but are granted anyway for backwards compatibility
|
||||||
wantDownstreamNonce: downstreamNonce,
|
wantDownstreamNonce: downstreamNonce,
|
||||||
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
|
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
|
||||||
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
|
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
|
||||||
@ -2429,7 +2444,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
wantDownstreamNonce: downstreamNonce,
|
wantDownstreamNonce: downstreamNonce,
|
||||||
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
|
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
|
||||||
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
|
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
|
||||||
wantDownstreamCustomSessionData: expectedHappyOIDCPasswordGrantCustomSession,
|
wantDownstreamCustomSessionData: expectedHappyOIDCPasswordGrantCustomSessionWithUsername(oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "OIDC upstream password grant: upstream IDP configures username claim as special claim `email` and `email_verified` upstream claim is missing",
|
name: "OIDC upstream password grant: upstream IDP configures username claim as special claim `email` and `email_verified` upstream claim is missing",
|
||||||
@ -2455,7 +2470,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
wantDownstreamNonce: downstreamNonce,
|
wantDownstreamNonce: downstreamNonce,
|
||||||
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
|
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
|
||||||
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
|
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
|
||||||
wantDownstreamCustomSessionData: expectedHappyOIDCPasswordGrantCustomSession,
|
wantDownstreamCustomSessionData: expectedHappyOIDCPasswordGrantCustomSessionWithUsername("joe@whitehouse.gov"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "OIDC upstream password grant: upstream IDP configures username claim as special claim `email` and `email_verified` upstream claim is present with true value",
|
name: "OIDC upstream password grant: upstream IDP configures username claim as special claim `email` and `email_verified` upstream claim is present with true value",
|
||||||
@ -2482,7 +2497,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
wantDownstreamNonce: downstreamNonce,
|
wantDownstreamNonce: downstreamNonce,
|
||||||
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
|
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
|
||||||
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
|
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
|
||||||
wantDownstreamCustomSessionData: expectedHappyOIDCPasswordGrantCustomSession,
|
wantDownstreamCustomSessionData: expectedHappyOIDCPasswordGrantCustomSessionWithUsername("joe@whitehouse.gov"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "OIDC upstream password grant: upstream IDP configures username claim as anything other than special claim `email` and `email_verified` upstream claim is present with false value",
|
name: "OIDC upstream password grant: upstream IDP configures username claim as anything other than special claim `email` and `email_verified` upstream claim is present with false value",
|
||||||
@ -2510,7 +2525,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
wantDownstreamNonce: downstreamNonce,
|
wantDownstreamNonce: downstreamNonce,
|
||||||
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
|
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
|
||||||
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
|
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
|
||||||
wantDownstreamCustomSessionData: expectedHappyOIDCPasswordGrantCustomSession,
|
wantDownstreamCustomSessionData: expectedHappyOIDCPasswordGrantCustomSessionWithUsername("joe"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "OIDC upstream password grant: upstream IDP configures username claim as special claim `email` and `email_verified` upstream claim is present with illegal value",
|
name: "OIDC upstream password grant: upstream IDP configures username claim as special claim `email` and `email_verified` upstream claim is present with illegal value",
|
||||||
@ -2570,7 +2585,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
wantDownstreamNonce: downstreamNonce,
|
wantDownstreamNonce: downstreamNonce,
|
||||||
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
|
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
|
||||||
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
|
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
|
||||||
wantDownstreamCustomSessionData: expectedHappyOIDCPasswordGrantCustomSession,
|
wantDownstreamCustomSessionData: expectedHappyOIDCPasswordGrantCustomSessionWithUsername(oidcUpstreamSubject),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "OIDC upstream password grant: upstream IDP's configured groups claim in the ID token has a non-array value",
|
name: "OIDC upstream password grant: upstream IDP's configured groups claim in the ID token has a non-array value",
|
||||||
|
@ -8,7 +8,6 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
coreosoidc "github.com/coreos/go-oidc/v3/oidc"
|
|
||||||
"github.com/ory/fosite"
|
"github.com/ory/fosite"
|
||||||
|
|
||||||
"go.pinniped.dev/internal/httputil/httperr"
|
"go.pinniped.dev/internal/httputil/httperr"
|
||||||
@ -53,10 +52,10 @@ func NewHandler(
|
|||||||
return httperr.New(http.StatusBadRequest, "error using state downstream auth params")
|
return httperr.New(http.StatusBadRequest, "error using state downstream auth params")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Automatically grant the openid, offline_access, pinniped:request-audience, and groups scopes, but only if they were requested.
|
// Automatically grant certain scopes, but only if they were requested.
|
||||||
// This is instead of asking the user to approve these scopes. Note that `NewAuthorizeRequest` would have returned
|
// 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.
|
// an error if the client requested a scope that they are not allowed to request, so we don't need to worry about that here.
|
||||||
downstreamsession.GrantScopesIfRequested(authorizeRequester, []string{coreosoidc.ScopeOpenID, coreosoidc.ScopeOfflineAccess, oidc.RequestAudienceScope, oidc.DownstreamGroupsScope})
|
downstreamsession.AutoApproveScopes(authorizeRequester)
|
||||||
|
|
||||||
token, err := upstreamIDPConfig.ExchangeAuthcodeAndValidateTokens(
|
token, err := upstreamIDPConfig.ExchangeAuthcodeAndValidateTokens(
|
||||||
r.Context(),
|
r.Context(),
|
||||||
@ -75,7 +74,7 @@ func NewHandler(
|
|||||||
return httperr.Wrap(http.StatusUnprocessableEntity, err.Error(), err)
|
return httperr.Wrap(http.StatusUnprocessableEntity, err.Error(), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
customSessionData, err := downstreamsession.MakeDownstreamOIDCCustomSessionData(upstreamIDPConfig, token)
|
customSessionData, err := downstreamsession.MakeDownstreamOIDCCustomSessionData(upstreamIDPConfig, token, username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperr.Wrap(http.StatusUnprocessableEntity, err.Error(), err)
|
return httperr.Wrap(http.StatusUnprocessableEntity, err.Error(), err)
|
||||||
}
|
}
|
||||||
|
@ -19,9 +19,11 @@ import (
|
|||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/client-go/kubernetes/fake"
|
"k8s.io/client-go/kubernetes/fake"
|
||||||
|
|
||||||
|
configv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1"
|
||||||
supervisorfake "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned/fake"
|
supervisorfake "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned/fake"
|
||||||
"go.pinniped.dev/internal/oidc"
|
"go.pinniped.dev/internal/oidc"
|
||||||
"go.pinniped.dev/internal/oidc/jwks"
|
"go.pinniped.dev/internal/oidc/jwks"
|
||||||
|
"go.pinniped.dev/internal/oidc/oidcclientvalidator"
|
||||||
"go.pinniped.dev/internal/psession"
|
"go.pinniped.dev/internal/psession"
|
||||||
"go.pinniped.dev/internal/testutil"
|
"go.pinniped.dev/internal/testutil"
|
||||||
"go.pinniped.dev/internal/testutil/oidctestutil"
|
"go.pinniped.dev/internal/testutil/oidctestutil"
|
||||||
@ -66,8 +68,8 @@ const (
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
oidcUpstreamGroupMembership = []string{"test-pinniped-group-0", "test-pinniped-group-1"}
|
oidcUpstreamGroupMembership = []string{"test-pinniped-group-0", "test-pinniped-group-1"}
|
||||||
happyDownstreamScopesRequested = []string{"openid", "groups"}
|
happyDownstreamScopesRequested = []string{"openid", "username", "groups"}
|
||||||
happyDownstreamScopesGranted = []string{"openid", "groups"}
|
happyDownstreamScopesGranted = []string{"openid", "username", "groups"}
|
||||||
|
|
||||||
happyDownstreamRequestParamsQuery = url.Values{
|
happyDownstreamRequestParamsQuery = url.Values{
|
||||||
"response_type": []string{"code"},
|
"response_type": []string{"code"},
|
||||||
@ -81,11 +83,13 @@ var (
|
|||||||
}
|
}
|
||||||
happyDownstreamRequestParams = happyDownstreamRequestParamsQuery.Encode()
|
happyDownstreamRequestParams = happyDownstreamRequestParamsQuery.Encode()
|
||||||
|
|
||||||
happyDownstreamRequestParamsForDynamicClient = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery,
|
happyDownstreamRequestParamsQueryForDynamicClient = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery,
|
||||||
map[string]string{"client_id": downstreamDynamicClientID},
|
map[string]string{"client_id": downstreamDynamicClientID},
|
||||||
).Encode()
|
)
|
||||||
|
happyDownstreamRequestParamsForDynamicClient = happyDownstreamRequestParamsQueryForDynamicClient.Encode()
|
||||||
|
|
||||||
happyDownstreamCustomSessionData = &psession.CustomSessionData{
|
happyDownstreamCustomSessionData = &psession.CustomSessionData{
|
||||||
|
Username: oidcUpstreamUsername,
|
||||||
ProviderUID: happyUpstreamIDPResourceUID,
|
ProviderUID: happyUpstreamIDPResourceUID,
|
||||||
ProviderName: happyUpstreamIDPName,
|
ProviderName: happyUpstreamIDPName,
|
||||||
ProviderType: psession.ProviderTypeOIDC,
|
ProviderType: psession.ProviderTypeOIDC,
|
||||||
@ -95,7 +99,15 @@ var (
|
|||||||
UpstreamSubject: oidcUpstreamSubject,
|
UpstreamSubject: oidcUpstreamSubject,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
happyDownstreamCustomSessionDataWithUsername = func(wantUsername string) *psession.CustomSessionData {
|
||||||
|
copyOfCustomSession := *happyDownstreamCustomSessionData
|
||||||
|
copyOfOIDC := *(happyDownstreamCustomSessionData.OIDC)
|
||||||
|
copyOfCustomSession.OIDC = ©OfOIDC
|
||||||
|
copyOfCustomSession.Username = wantUsername
|
||||||
|
return ©OfCustomSession
|
||||||
|
}
|
||||||
happyDownstreamAccessTokenCustomSessionData = &psession.CustomSessionData{
|
happyDownstreamAccessTokenCustomSessionData = &psession.CustomSessionData{
|
||||||
|
Username: oidcUpstreamUsername,
|
||||||
ProviderUID: happyUpstreamIDPResourceUID,
|
ProviderUID: happyUpstreamIDPResourceUID,
|
||||||
ProviderName: happyUpstreamIDPName,
|
ProviderName: happyUpstreamIDPName,
|
||||||
ProviderType: psession.ProviderTypeOIDC,
|
ProviderType: psession.ProviderTypeOIDC,
|
||||||
@ -143,11 +155,12 @@ func TestCallbackEndpoint(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Note that fosite puts the granted scopes as a param in the redirect URI even though the spec doesn't seem to require it
|
// Note that fosite puts the granted scopes as a param in the redirect URI even though the spec doesn't seem to require it
|
||||||
happyDownstreamRedirectLocationRegexp := downstreamRedirectURI + `\?code=([^&]+)&scope=openid\+groups&state=` + happyDownstreamState
|
happyDownstreamRedirectLocationRegexp := downstreamRedirectURI + `\?code=([^&]+)&scope=openid\+username\+groups&state=` + happyDownstreamState
|
||||||
|
|
||||||
addFullyCapableDynamicClientAndSecretToKubeResources := func(t *testing.T, supervisorClient *supervisorfake.Clientset, kubeClient *fake.Clientset) {
|
addFullyCapableDynamicClientAndSecretToKubeResources := func(t *testing.T, supervisorClient *supervisorfake.Clientset, kubeClient *fake.Clientset) {
|
||||||
oidcClient, secret := testutil.FullyCapableOIDCClientAndStorageSecret(t,
|
oidcClient, secret := testutil.FullyCapableOIDCClientAndStorageSecret(t,
|
||||||
"some-namespace", downstreamDynamicClientID, downstreamDynamicClientUID, downstreamRedirectURI, []string{testutil.HashedPassword1AtGoMinCost})
|
"some-namespace", downstreamDynamicClientID, downstreamDynamicClientUID, downstreamRedirectURI,
|
||||||
|
[]string{testutil.HashedPassword1AtGoMinCost}, oidcclientvalidator.Validate)
|
||||||
require.NoError(t, supervisorClient.Tracker().Add(oidcClient))
|
require.NoError(t, supervisorClient.Tracker().Add(oidcClient))
|
||||||
require.NoError(t, kubeClient.Tracker().Add(secret))
|
require.NoError(t, kubeClient.Tracker().Add(secret))
|
||||||
}
|
}
|
||||||
@ -284,7 +297,7 @@ func TestCallbackEndpoint(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "form_post happy path with no groups scope requested",
|
name: "form_post happy path without username or groups scopes requested",
|
||||||
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()),
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()),
|
||||||
method: http.MethodGet,
|
method: http.MethodGet,
|
||||||
path: newRequestPath().WithState(
|
path: newRequestPath().WithState(
|
||||||
@ -305,7 +318,9 @@ func TestCallbackEndpoint(t *testing.T) {
|
|||||||
wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped,
|
wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped,
|
||||||
wantDownstreamIDTokenUsername: oidcUpstreamUsername,
|
wantDownstreamIDTokenUsername: oidcUpstreamUsername,
|
||||||
wantDownstreamRequestedScopes: []string{"openid"},
|
wantDownstreamRequestedScopes: []string{"openid"},
|
||||||
wantDownstreamGrantedScopes: []string{"openid"},
|
wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership,
|
||||||
|
// username and groups scopes were not requested but are granted anyway for the pinniped-cli client for backwards compatibility
|
||||||
|
wantDownstreamGrantedScopes: []string{"openid", "username", "groups"},
|
||||||
wantDownstreamNonce: downstreamNonce,
|
wantDownstreamNonce: downstreamNonce,
|
||||||
wantDownstreamClientID: downstreamPinnipedClientID,
|
wantDownstreamClientID: downstreamPinnipedClientID,
|
||||||
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
|
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
|
||||||
@ -335,6 +350,7 @@ func TestCallbackEndpoint(t *testing.T) {
|
|||||||
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
|
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
|
||||||
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
|
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
|
||||||
wantDownstreamCustomSessionData: &psession.CustomSessionData{
|
wantDownstreamCustomSessionData: &psession.CustomSessionData{
|
||||||
|
Username: oidcUpstreamUsername,
|
||||||
ProviderUID: happyUpstreamIDPResourceUID,
|
ProviderUID: happyUpstreamIDPResourceUID,
|
||||||
ProviderName: happyUpstreamIDPName,
|
ProviderName: happyUpstreamIDPName,
|
||||||
ProviderType: psession.ProviderTypeOIDC,
|
ProviderType: psession.ProviderTypeOIDC,
|
||||||
@ -370,7 +386,7 @@ func TestCallbackEndpoint(t *testing.T) {
|
|||||||
wantDownstreamClientID: downstreamPinnipedClientID,
|
wantDownstreamClientID: downstreamPinnipedClientID,
|
||||||
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
|
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
|
||||||
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
|
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
|
||||||
wantDownstreamCustomSessionData: happyDownstreamCustomSessionData,
|
wantDownstreamCustomSessionData: happyDownstreamCustomSessionDataWithUsername(oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped),
|
||||||
wantAuthcodeExchangeCall: &expectedAuthcodeExchange{
|
wantAuthcodeExchangeCall: &expectedAuthcodeExchange{
|
||||||
performedByUpstreamName: happyUpstreamIDPName,
|
performedByUpstreamName: happyUpstreamIDPName,
|
||||||
args: happyExchangeAndValidateTokensArgs,
|
args: happyExchangeAndValidateTokensArgs,
|
||||||
@ -396,7 +412,7 @@ func TestCallbackEndpoint(t *testing.T) {
|
|||||||
wantDownstreamClientID: downstreamPinnipedClientID,
|
wantDownstreamClientID: downstreamPinnipedClientID,
|
||||||
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
|
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
|
||||||
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
|
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
|
||||||
wantDownstreamCustomSessionData: happyDownstreamCustomSessionData,
|
wantDownstreamCustomSessionData: happyDownstreamCustomSessionDataWithUsername("joe@whitehouse.gov"),
|
||||||
wantAuthcodeExchangeCall: &expectedAuthcodeExchange{
|
wantAuthcodeExchangeCall: &expectedAuthcodeExchange{
|
||||||
performedByUpstreamName: happyUpstreamIDPName,
|
performedByUpstreamName: happyUpstreamIDPName,
|
||||||
args: happyExchangeAndValidateTokensArgs,
|
args: happyExchangeAndValidateTokensArgs,
|
||||||
@ -424,7 +440,7 @@ func TestCallbackEndpoint(t *testing.T) {
|
|||||||
wantDownstreamClientID: downstreamPinnipedClientID,
|
wantDownstreamClientID: downstreamPinnipedClientID,
|
||||||
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
|
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
|
||||||
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
|
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
|
||||||
wantDownstreamCustomSessionData: happyDownstreamCustomSessionData,
|
wantDownstreamCustomSessionData: happyDownstreamCustomSessionDataWithUsername("joe@whitehouse.gov"),
|
||||||
wantAuthcodeExchangeCall: &expectedAuthcodeExchange{
|
wantAuthcodeExchangeCall: &expectedAuthcodeExchange{
|
||||||
performedByUpstreamName: happyUpstreamIDPName,
|
performedByUpstreamName: happyUpstreamIDPName,
|
||||||
args: happyExchangeAndValidateTokensArgs,
|
args: happyExchangeAndValidateTokensArgs,
|
||||||
@ -453,7 +469,7 @@ func TestCallbackEndpoint(t *testing.T) {
|
|||||||
wantDownstreamClientID: downstreamPinnipedClientID,
|
wantDownstreamClientID: downstreamPinnipedClientID,
|
||||||
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
|
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
|
||||||
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
|
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
|
||||||
wantDownstreamCustomSessionData: happyDownstreamCustomSessionData,
|
wantDownstreamCustomSessionData: happyDownstreamCustomSessionDataWithUsername("joe"),
|
||||||
wantAuthcodeExchangeCall: &expectedAuthcodeExchange{
|
wantAuthcodeExchangeCall: &expectedAuthcodeExchange{
|
||||||
performedByUpstreamName: happyUpstreamIDPName,
|
performedByUpstreamName: happyUpstreamIDPName,
|
||||||
args: happyExchangeAndValidateTokensArgs,
|
args: happyExchangeAndValidateTokensArgs,
|
||||||
@ -584,7 +600,7 @@ func TestCallbackEndpoint(t *testing.T) {
|
|||||||
wantDownstreamClientID: downstreamPinnipedClientID,
|
wantDownstreamClientID: downstreamPinnipedClientID,
|
||||||
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
|
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
|
||||||
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
|
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
|
||||||
wantDownstreamCustomSessionData: happyDownstreamCustomSessionData,
|
wantDownstreamCustomSessionData: happyDownstreamCustomSessionDataWithUsername(oidcUpstreamSubject),
|
||||||
wantAuthcodeExchangeCall: &expectedAuthcodeExchange{
|
wantAuthcodeExchangeCall: &expectedAuthcodeExchange{
|
||||||
performedByUpstreamName: happyUpstreamIDPName,
|
performedByUpstreamName: happyUpstreamIDPName,
|
||||||
args: happyExchangeAndValidateTokensArgs,
|
args: happyExchangeAndValidateTokensArgs,
|
||||||
@ -642,6 +658,152 @@ func TestCallbackEndpoint(t *testing.T) {
|
|||||||
args: happyExchangeAndValidateTokensArgs,
|
args: happyExchangeAndValidateTokensArgs,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "using dynamic client which is allowed to request username scope, but does not actually request username scope in authorize request, does not get username in ID token",
|
||||||
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()),
|
||||||
|
kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources,
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: newRequestPath().WithState(
|
||||||
|
happyUpstreamStateParamForDynamicClient().
|
||||||
|
WithAuthorizeRequestParams(shallowCopyAndModifyQuery(happyDownstreamRequestParamsQueryForDynamicClient,
|
||||||
|
map[string]string{"scope": "openid groups offline_access"}).Encode()).
|
||||||
|
Build(t, happyStateCodec),
|
||||||
|
).String(),
|
||||||
|
csrfCookie: happyCSRFCookie,
|
||||||
|
wantStatus: http.StatusSeeOther,
|
||||||
|
wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=openid\+offline_access\+groups&state=` + happyDownstreamState,
|
||||||
|
wantBody: "",
|
||||||
|
wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped,
|
||||||
|
wantDownstreamIDTokenUsername: "", // username scope was not requested
|
||||||
|
wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership,
|
||||||
|
wantDownstreamRequestedScopes: []string{"openid", "groups", "offline_access"},
|
||||||
|
wantDownstreamGrantedScopes: []string{"openid", "groups", "offline_access"},
|
||||||
|
wantDownstreamNonce: downstreamNonce,
|
||||||
|
wantDownstreamClientID: downstreamDynamicClientID,
|
||||||
|
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
|
||||||
|
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
|
||||||
|
wantDownstreamCustomSessionData: happyDownstreamCustomSessionData,
|
||||||
|
wantAuthcodeExchangeCall: &expectedAuthcodeExchange{
|
||||||
|
performedByUpstreamName: happyUpstreamIDPName,
|
||||||
|
args: happyExchangeAndValidateTokensArgs,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "using dynamic client which is allowed to request groups scope, but does not actually request groups scope in authorize request, does not get groups in ID token",
|
||||||
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()),
|
||||||
|
kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources,
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: newRequestPath().WithState(
|
||||||
|
happyUpstreamStateParamForDynamicClient().
|
||||||
|
WithAuthorizeRequestParams(shallowCopyAndModifyQuery(happyDownstreamRequestParamsQueryForDynamicClient,
|
||||||
|
map[string]string{"scope": "openid username offline_access"}).Encode()).
|
||||||
|
Build(t, happyStateCodec),
|
||||||
|
).String(),
|
||||||
|
csrfCookie: happyCSRFCookie,
|
||||||
|
wantStatus: http.StatusSeeOther,
|
||||||
|
wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=openid\+offline_access\+username&state=` + happyDownstreamState,
|
||||||
|
wantBody: "",
|
||||||
|
wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped,
|
||||||
|
wantDownstreamIDTokenUsername: oidcUpstreamUsername,
|
||||||
|
wantDownstreamIDTokenGroups: nil, // groups scope was not requested
|
||||||
|
wantDownstreamRequestedScopes: []string{"openid", "username", "offline_access"},
|
||||||
|
wantDownstreamGrantedScopes: []string{"openid", "username", "offline_access"},
|
||||||
|
wantDownstreamNonce: downstreamNonce,
|
||||||
|
wantDownstreamClientID: downstreamDynamicClientID,
|
||||||
|
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
|
||||||
|
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
|
||||||
|
wantDownstreamCustomSessionData: happyDownstreamCustomSessionData,
|
||||||
|
wantAuthcodeExchangeCall: &expectedAuthcodeExchange{
|
||||||
|
performedByUpstreamName: happyUpstreamIDPName,
|
||||||
|
args: happyExchangeAndValidateTokensArgs,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "using dynamic client which is not allowed to request username scope, and does not actually request username scope in authorize request, does not get username in ID token",
|
||||||
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()),
|
||||||
|
kubeResources: func(t *testing.T, supervisorClient *supervisorfake.Clientset, kubeClient *fake.Clientset) {
|
||||||
|
oidcClient, secret := testutil.OIDCClientAndStorageSecret(t,
|
||||||
|
"some-namespace", downstreamDynamicClientID, downstreamDynamicClientUID,
|
||||||
|
[]configv1alpha1.GrantType{"authorization_code", "refresh_token"}, // token exchange not allowed (required to exclude username scope)
|
||||||
|
[]configv1alpha1.Scope{"openid", "offline_access", "groups"}, // username not allowed
|
||||||
|
downstreamRedirectURI, []string{testutil.HashedPassword1AtGoMinCost}, oidcclientvalidator.Validate)
|
||||||
|
require.NoError(t, supervisorClient.Tracker().Add(oidcClient))
|
||||||
|
require.NoError(t, kubeClient.Tracker().Add(secret))
|
||||||
|
},
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: newRequestPath().WithState(
|
||||||
|
happyUpstreamStateParam().WithAuthorizeRequestParams(
|
||||||
|
shallowCopyAndModifyQuery(
|
||||||
|
happyDownstreamRequestParamsQuery,
|
||||||
|
map[string]string{
|
||||||
|
"client_id": downstreamDynamicClientID,
|
||||||
|
"scope": "openid offline_access groups",
|
||||||
|
},
|
||||||
|
).Encode(),
|
||||||
|
).Build(t, happyStateCodec),
|
||||||
|
).String(),
|
||||||
|
csrfCookie: happyCSRFCookie,
|
||||||
|
wantStatus: http.StatusSeeOther,
|
||||||
|
wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=openid\+offline_access\+groups&state=` + happyDownstreamState,
|
||||||
|
wantBody: "",
|
||||||
|
wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped,
|
||||||
|
wantDownstreamIDTokenUsername: "", // username scope was not requested
|
||||||
|
wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership,
|
||||||
|
wantDownstreamRequestedScopes: []string{"openid", "groups", "offline_access"},
|
||||||
|
wantDownstreamGrantedScopes: []string{"openid", "groups", "offline_access"},
|
||||||
|
wantDownstreamNonce: downstreamNonce,
|
||||||
|
wantDownstreamClientID: downstreamDynamicClientID,
|
||||||
|
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
|
||||||
|
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
|
||||||
|
wantDownstreamCustomSessionData: happyDownstreamCustomSessionData,
|
||||||
|
wantAuthcodeExchangeCall: &expectedAuthcodeExchange{
|
||||||
|
performedByUpstreamName: happyUpstreamIDPName,
|
||||||
|
args: happyExchangeAndValidateTokensArgs,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "using dynamic client which is not allowed to request groups scope, and does not actually request groups scope in authorize request, does not get groups in ID token",
|
||||||
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()),
|
||||||
|
kubeResources: func(t *testing.T, supervisorClient *supervisorfake.Clientset, kubeClient *fake.Clientset) {
|
||||||
|
oidcClient, secret := testutil.OIDCClientAndStorageSecret(t,
|
||||||
|
"some-namespace", downstreamDynamicClientID, downstreamDynamicClientUID,
|
||||||
|
[]configv1alpha1.GrantType{"authorization_code", "refresh_token"}, // token exchange not allowed (required to exclude groups scope)
|
||||||
|
[]configv1alpha1.Scope{"openid", "offline_access", "username"}, // groups not allowed
|
||||||
|
downstreamRedirectURI, []string{testutil.HashedPassword1AtGoMinCost}, oidcclientvalidator.Validate)
|
||||||
|
require.NoError(t, supervisorClient.Tracker().Add(oidcClient))
|
||||||
|
require.NoError(t, kubeClient.Tracker().Add(secret))
|
||||||
|
},
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: newRequestPath().WithState(
|
||||||
|
happyUpstreamStateParam().WithAuthorizeRequestParams(
|
||||||
|
shallowCopyAndModifyQuery(
|
||||||
|
happyDownstreamRequestParamsQuery,
|
||||||
|
map[string]string{
|
||||||
|
"client_id": downstreamDynamicClientID,
|
||||||
|
"scope": "openid offline_access username",
|
||||||
|
},
|
||||||
|
).Encode(),
|
||||||
|
).Build(t, happyStateCodec),
|
||||||
|
).String(),
|
||||||
|
csrfCookie: happyCSRFCookie,
|
||||||
|
wantStatus: http.StatusSeeOther,
|
||||||
|
wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=openid\+offline_access\+username&state=` + happyDownstreamState,
|
||||||
|
wantBody: "",
|
||||||
|
wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped,
|
||||||
|
wantDownstreamIDTokenUsername: oidcUpstreamUsername,
|
||||||
|
wantDownstreamIDTokenGroups: nil, // groups scope was not requested
|
||||||
|
wantDownstreamRequestedScopes: []string{"openid", "username", "offline_access"},
|
||||||
|
wantDownstreamGrantedScopes: []string{"openid", "username", "offline_access"},
|
||||||
|
wantDownstreamNonce: downstreamNonce,
|
||||||
|
wantDownstreamClientID: downstreamDynamicClientID,
|
||||||
|
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
|
||||||
|
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
|
||||||
|
wantDownstreamCustomSessionData: happyDownstreamCustomSessionData,
|
||||||
|
wantAuthcodeExchangeCall: &expectedAuthcodeExchange{
|
||||||
|
performedByUpstreamName: happyUpstreamIDPName,
|
||||||
|
args: happyExchangeAndValidateTokensArgs,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
// Pre-upstream-exchange verification
|
// Pre-upstream-exchange verification
|
||||||
{
|
{
|
||||||
@ -718,7 +880,8 @@ func TestCallbackEndpoint(t *testing.T) {
|
|||||||
method: http.MethodGet,
|
method: http.MethodGet,
|
||||||
path: newRequestPath().WithState(
|
path: newRequestPath().WithState(
|
||||||
happyUpstreamStateParam().
|
happyUpstreamStateParam().
|
||||||
WithAuthorizeRequestParams(shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery, map[string]string{"prompt": "none login"}).Encode()).
|
WithAuthorizeRequestParams(shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery,
|
||||||
|
map[string]string{"prompt": "none login"}).Encode()).
|
||||||
Build(t, happyStateCodec),
|
Build(t, happyStateCodec),
|
||||||
).String(),
|
).String(),
|
||||||
csrfCookie: happyCSRFCookie,
|
csrfCookie: happyCSRFCookie,
|
||||||
@ -759,7 +922,8 @@ func TestCallbackEndpoint(t *testing.T) {
|
|||||||
method: http.MethodGet,
|
method: http.MethodGet,
|
||||||
path: newRequestPath().WithState(
|
path: newRequestPath().WithState(
|
||||||
happyUpstreamStateParam().
|
happyUpstreamStateParam().
|
||||||
WithAuthorizeRequestParams(shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery, map[string]string{"client_id": ""}).Encode()).
|
WithAuthorizeRequestParams(shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery,
|
||||||
|
map[string]string{"client_id": ""}).Encode()).
|
||||||
Build(t, happyStateCodec),
|
Build(t, happyStateCodec),
|
||||||
).String(),
|
).String(),
|
||||||
csrfCookie: happyCSRFCookie,
|
csrfCookie: happyCSRFCookie,
|
||||||
@ -773,7 +937,8 @@ func TestCallbackEndpoint(t *testing.T) {
|
|||||||
method: http.MethodGet,
|
method: http.MethodGet,
|
||||||
path: newRequestPath().WithState(
|
path: newRequestPath().WithState(
|
||||||
happyUpstreamStateParam().
|
happyUpstreamStateParam().
|
||||||
WithAuthorizeRequestParams(shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery, map[string]string{"client_id": "bogus"}).Encode()).
|
WithAuthorizeRequestParams(shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery,
|
||||||
|
map[string]string{"client_id": "bogus"}).Encode()).
|
||||||
Build(t, happyStateCodec),
|
Build(t, happyStateCodec),
|
||||||
).String(),
|
).String(),
|
||||||
csrfCookie: happyCSRFCookie,
|
csrfCookie: happyCSRFCookie,
|
||||||
@ -803,6 +968,64 @@ func TestCallbackEndpoint(t *testing.T) {
|
|||||||
wantContentType: htmlContentType,
|
wantContentType: htmlContentType,
|
||||||
wantBody: "Bad Request: error using state downstream auth params\n",
|
wantBody: "Bad Request: error using state downstream auth params\n",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "using dynamic client which is not allowed to request username scope in authorize request but requests it anyway",
|
||||||
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()),
|
||||||
|
kubeResources: func(t *testing.T, supervisorClient *supervisorfake.Clientset, kubeClient *fake.Clientset) {
|
||||||
|
oidcClient, secret := testutil.OIDCClientAndStorageSecret(t,
|
||||||
|
"some-namespace", downstreamDynamicClientID, downstreamDynamicClientUID,
|
||||||
|
[]configv1alpha1.GrantType{"authorization_code", "refresh_token"}, // token exchange not allowed (required to exclude username scope)
|
||||||
|
[]configv1alpha1.Scope{"openid", "offline_access", "groups"}, // username not allowed
|
||||||
|
downstreamRedirectURI, []string{testutil.HashedPassword1AtGoMinCost}, oidcclientvalidator.Validate)
|
||||||
|
require.NoError(t, supervisorClient.Tracker().Add(oidcClient))
|
||||||
|
require.NoError(t, kubeClient.Tracker().Add(secret))
|
||||||
|
},
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: newRequestPath().WithState(
|
||||||
|
happyUpstreamStateParam().WithAuthorizeRequestParams(
|
||||||
|
shallowCopyAndModifyQuery(
|
||||||
|
happyDownstreamRequestParamsQuery,
|
||||||
|
map[string]string{
|
||||||
|
"client_id": downstreamDynamicClientID,
|
||||||
|
"scope": "openid username",
|
||||||
|
},
|
||||||
|
).Encode(),
|
||||||
|
).Build(t, happyStateCodec),
|
||||||
|
).String(),
|
||||||
|
csrfCookie: happyCSRFCookie,
|
||||||
|
wantStatus: http.StatusBadRequest,
|
||||||
|
wantContentType: htmlContentType,
|
||||||
|
wantBody: "Bad Request: error using state downstream auth params\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "using dynamic client which is not allowed to request groups scope in authorize request but requests it anyway",
|
||||||
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()),
|
||||||
|
kubeResources: func(t *testing.T, supervisorClient *supervisorfake.Clientset, kubeClient *fake.Clientset) {
|
||||||
|
oidcClient, secret := testutil.OIDCClientAndStorageSecret(t,
|
||||||
|
"some-namespace", downstreamDynamicClientID, downstreamDynamicClientUID,
|
||||||
|
[]configv1alpha1.GrantType{"authorization_code", "refresh_token"}, // token exchange not allowed (required to exclude groups scope)
|
||||||
|
[]configv1alpha1.Scope{"openid", "offline_access", "username"}, // groups not allowed
|
||||||
|
downstreamRedirectURI, []string{testutil.HashedPassword1AtGoMinCost}, oidcclientvalidator.Validate)
|
||||||
|
require.NoError(t, supervisorClient.Tracker().Add(oidcClient))
|
||||||
|
require.NoError(t, kubeClient.Tracker().Add(secret))
|
||||||
|
},
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: newRequestPath().WithState(
|
||||||
|
happyUpstreamStateParam().WithAuthorizeRequestParams(
|
||||||
|
shallowCopyAndModifyQuery(
|
||||||
|
happyDownstreamRequestParamsQuery,
|
||||||
|
map[string]string{
|
||||||
|
"client_id": downstreamDynamicClientID,
|
||||||
|
"scope": "openid groups",
|
||||||
|
},
|
||||||
|
).Encode(),
|
||||||
|
).Build(t, happyStateCodec),
|
||||||
|
).String(),
|
||||||
|
csrfCookie: happyCSRFCookie,
|
||||||
|
wantStatus: http.StatusBadRequest,
|
||||||
|
wantContentType: htmlContentType,
|
||||||
|
wantBody: "Bad Request: error using state downstream auth params\n",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "state's downstream auth params does not contain openid scope",
|
name: "state's downstream auth params does not contain openid scope",
|
||||||
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()),
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()),
|
||||||
@ -810,16 +1033,17 @@ func TestCallbackEndpoint(t *testing.T) {
|
|||||||
path: newRequestPath().
|
path: newRequestPath().
|
||||||
WithState(
|
WithState(
|
||||||
happyUpstreamStateParam().
|
happyUpstreamStateParam().
|
||||||
WithAuthorizeRequestParams(shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery, map[string]string{"scope": "profile email groups"}).Encode()).
|
WithAuthorizeRequestParams(shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery,
|
||||||
|
map[string]string{"scope": "profile username email groups"}).Encode()).
|
||||||
Build(t, happyStateCodec),
|
Build(t, happyStateCodec),
|
||||||
).String(),
|
).String(),
|
||||||
csrfCookie: happyCSRFCookie,
|
csrfCookie: happyCSRFCookie,
|
||||||
wantStatus: http.StatusSeeOther,
|
wantStatus: http.StatusSeeOther,
|
||||||
wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=groups&state=` + happyDownstreamState,
|
wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=username\+groups&state=` + happyDownstreamState,
|
||||||
wantDownstreamIDTokenUsername: oidcUpstreamUsername,
|
wantDownstreamIDTokenUsername: oidcUpstreamUsername,
|
||||||
wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped,
|
wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped,
|
||||||
wantDownstreamRequestedScopes: []string{"profile", "email", "groups"},
|
wantDownstreamRequestedScopes: []string{"profile", "email", "username", "groups"},
|
||||||
wantDownstreamGrantedScopes: []string{"groups"},
|
wantDownstreamGrantedScopes: []string{"username", "groups"},
|
||||||
wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership,
|
wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership,
|
||||||
wantDownstreamNonce: downstreamNonce,
|
wantDownstreamNonce: downstreamNonce,
|
||||||
wantDownstreamClientID: downstreamPinnipedClientID,
|
wantDownstreamClientID: downstreamPinnipedClientID,
|
||||||
@ -832,22 +1056,25 @@ func TestCallbackEndpoint(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "state's downstream auth params does not contain openid or groups scope",
|
name: "state's downstream auth params does not contain openid, username, or groups scope",
|
||||||
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()),
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()),
|
||||||
method: http.MethodGet,
|
method: http.MethodGet,
|
||||||
path: newRequestPath().
|
path: newRequestPath().
|
||||||
WithState(
|
WithState(
|
||||||
happyUpstreamStateParam().
|
happyUpstreamStateParam().
|
||||||
WithAuthorizeRequestParams(shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery, map[string]string{"scope": "profile email"}).Encode()).
|
WithAuthorizeRequestParams(shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery,
|
||||||
|
map[string]string{"scope": "profile email"}).Encode()).
|
||||||
Build(t, happyStateCodec),
|
Build(t, happyStateCodec),
|
||||||
).String(),
|
).String(),
|
||||||
csrfCookie: happyCSRFCookie,
|
csrfCookie: happyCSRFCookie,
|
||||||
wantStatus: http.StatusSeeOther,
|
wantStatus: http.StatusSeeOther,
|
||||||
wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=&state=` + happyDownstreamState,
|
wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=username\+groups&state=` + happyDownstreamState,
|
||||||
wantDownstreamIDTokenUsername: oidcUpstreamUsername,
|
wantDownstreamIDTokenUsername: oidcUpstreamUsername,
|
||||||
|
wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership,
|
||||||
wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped,
|
wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped,
|
||||||
wantDownstreamRequestedScopes: []string{"profile", "email"},
|
wantDownstreamRequestedScopes: []string{"profile", "email"},
|
||||||
wantDownstreamGrantedScopes: []string{},
|
// username and groups scopes were not requested but are granted anyway for the pinniped-cli client for backwards compatibility
|
||||||
|
wantDownstreamGrantedScopes: []string{"username", "groups"},
|
||||||
wantDownstreamNonce: downstreamNonce,
|
wantDownstreamNonce: downstreamNonce,
|
||||||
wantDownstreamClientID: downstreamPinnipedClientID,
|
wantDownstreamClientID: downstreamPinnipedClientID,
|
||||||
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
|
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
|
||||||
@ -865,16 +1092,17 @@ func TestCallbackEndpoint(t *testing.T) {
|
|||||||
path: newRequestPath().
|
path: newRequestPath().
|
||||||
WithState(
|
WithState(
|
||||||
happyUpstreamStateParam().
|
happyUpstreamStateParam().
|
||||||
WithAuthorizeRequestParams(shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery, map[string]string{"scope": "openid offline_access groups"}).Encode()).
|
WithAuthorizeRequestParams(shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery,
|
||||||
|
map[string]string{"scope": "openid offline_access username groups"}).Encode()).
|
||||||
Build(t, happyStateCodec),
|
Build(t, happyStateCodec),
|
||||||
).String(),
|
).String(),
|
||||||
csrfCookie: happyCSRFCookie,
|
csrfCookie: happyCSRFCookie,
|
||||||
wantStatus: http.StatusSeeOther,
|
wantStatus: http.StatusSeeOther,
|
||||||
wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=openid\+offline_access\+groups&state=` + happyDownstreamState,
|
wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=openid\+offline_access\+username\+groups&state=` + happyDownstreamState,
|
||||||
wantDownstreamIDTokenUsername: oidcUpstreamUsername,
|
wantDownstreamIDTokenUsername: oidcUpstreamUsername,
|
||||||
wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped,
|
wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped,
|
||||||
wantDownstreamRequestedScopes: []string{"openid", "offline_access", "groups"},
|
wantDownstreamRequestedScopes: []string{"openid", "offline_access", "username", "groups"},
|
||||||
wantDownstreamGrantedScopes: []string{"openid", "offline_access", "groups"},
|
wantDownstreamGrantedScopes: []string{"openid", "offline_access", "username", "groups"},
|
||||||
wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership,
|
wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership,
|
||||||
wantDownstreamNonce: downstreamNonce,
|
wantDownstreamNonce: downstreamNonce,
|
||||||
wantDownstreamClientID: downstreamPinnipedClientID,
|
wantDownstreamClientID: downstreamPinnipedClientID,
|
||||||
|
@ -10,25 +10,19 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/coreos/go-oidc/v3/oidc"
|
coreosoidc "github.com/coreos/go-oidc/v3/oidc"
|
||||||
"github.com/ory/fosite"
|
"github.com/ory/fosite"
|
||||||
"k8s.io/apimachinery/pkg/api/errors"
|
"k8s.io/apimachinery/pkg/api/errors"
|
||||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
|
||||||
configv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1"
|
configv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1"
|
||||||
|
oidcapi "go.pinniped.dev/generated/latest/apis/supervisor/oidc"
|
||||||
supervisorclient "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned/typed/config/v1alpha1"
|
supervisorclient "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned/typed/config/v1alpha1"
|
||||||
"go.pinniped.dev/internal/oidc/oidcclientvalidator"
|
"go.pinniped.dev/internal/oidc/oidcclientvalidator"
|
||||||
"go.pinniped.dev/internal/oidcclientsecretstorage"
|
"go.pinniped.dev/internal/oidcclientsecretstorage"
|
||||||
"go.pinniped.dev/internal/plog"
|
"go.pinniped.dev/internal/plog"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
// PinnipedCLIClientID is the client ID of the statically defined public OIDC client which is used by the CLI.
|
|
||||||
PinnipedCLIClientID = "pinniped-cli"
|
|
||||||
|
|
||||||
requiredOIDCClientPrefix = "client.oauth.pinniped.dev-"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Client represents a Pinniped OAuth/OIDC client. It can be the static pinniped-cli client
|
// Client represents a Pinniped OAuth/OIDC client. It can be the static pinniped-cli client
|
||||||
// or a dynamic client defined by an OIDCClient CR.
|
// or a dynamic client defined by an OIDCClient CR.
|
||||||
type Client struct {
|
type Client struct {
|
||||||
@ -43,7 +37,7 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (c *Client) GetResponseModes() []fosite.ResponseModeType {
|
func (c *Client) GetResponseModes() []fosite.ResponseModeType {
|
||||||
if c.ID == PinnipedCLIClientID {
|
if c.ID == oidcapi.ClientIDPinnipedCLI {
|
||||||
// The pinniped-cli client supports "" (unspecified), "query", and "form_post" response modes.
|
// The pinniped-cli client supports "" (unspecified), "query", and "form_post" response modes.
|
||||||
return []fosite.ResponseModeType{fosite.ResponseModeDefault, fosite.ResponseModeQuery, fosite.ResponseModeFormPost}
|
return []fosite.ResponseModeType{fosite.ResponseModeDefault, fosite.ResponseModeQuery, fosite.ResponseModeFormPost}
|
||||||
}
|
}
|
||||||
@ -78,12 +72,12 @@ func NewClientManager(
|
|||||||
// Other errors returned are plain errors, because fosite will wrap them into a new ErrInvalidClient error and
|
// Other errors returned are plain errors, because fosite will wrap them into a new ErrInvalidClient error and
|
||||||
// use the plain error's text as that error's debug message (see client_authentication.go in fosite).
|
// use the plain error's text as that error's debug message (see client_authentication.go in fosite).
|
||||||
func (m *ClientManager) GetClient(ctx context.Context, id string) (fosite.Client, error) {
|
func (m *ClientManager) GetClient(ctx context.Context, id string) (fosite.Client, error) {
|
||||||
if id == PinnipedCLIClientID {
|
if id == oidcapi.ClientIDPinnipedCLI {
|
||||||
// Return the static client. No lookups needed.
|
// Return the static client. No lookups needed.
|
||||||
return PinnipedCLI(), nil
|
return PinnipedCLI(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if !strings.HasPrefix(id, requiredOIDCClientPrefix) {
|
if !strings.HasPrefix(id, oidcapi.ClientIDRequiredOIDCClientPrefix) {
|
||||||
// It shouldn't really be possible to find this OIDCClient because the OIDCClient CRD validates the name prefix
|
// It shouldn't really be possible to find this OIDCClient because the OIDCClient CRD validates the name prefix
|
||||||
// upon create, but just in case, don't even try to lookup clients which lack the required name prefix.
|
// upon create, but just in case, don't even try to lookup clients which lack the required name prefix.
|
||||||
return nil, fosite.ErrNotFound.WithDescription("no such client")
|
return nil, fosite.ErrNotFound.WithDescription("no such client")
|
||||||
@ -143,22 +137,23 @@ func PinnipedCLI() *Client {
|
|||||||
return &Client{
|
return &Client{
|
||||||
DefaultOpenIDConnectClient: fosite.DefaultOpenIDConnectClient{
|
DefaultOpenIDConnectClient: fosite.DefaultOpenIDConnectClient{
|
||||||
DefaultClient: &fosite.DefaultClient{
|
DefaultClient: &fosite.DefaultClient{
|
||||||
ID: PinnipedCLIClientID,
|
ID: oidcapi.ClientIDPinnipedCLI,
|
||||||
Secret: nil,
|
Secret: nil,
|
||||||
RedirectURIs: []string{"http://127.0.0.1/callback"},
|
RedirectURIs: []string{"http://127.0.0.1/callback"},
|
||||||
GrantTypes: fosite.Arguments{
|
GrantTypes: fosite.Arguments{
|
||||||
"authorization_code",
|
oidcapi.GrantTypeAuthorizationCode,
|
||||||
"refresh_token",
|
oidcapi.GrantTypeRefreshToken,
|
||||||
"urn:ietf:params:oauth:grant-type:token-exchange",
|
oidcapi.GrantTypeTokenExchange,
|
||||||
},
|
},
|
||||||
ResponseTypes: []string{"code"},
|
ResponseTypes: []string{"code"},
|
||||||
Scopes: fosite.Arguments{
|
Scopes: fosite.Arguments{
|
||||||
oidc.ScopeOpenID,
|
oidcapi.ScopeOpenID,
|
||||||
oidc.ScopeOfflineAccess,
|
oidcapi.ScopeOfflineAccess,
|
||||||
"profile",
|
oidcapi.ScopeProfile,
|
||||||
"email",
|
oidcapi.ScopeEmail,
|
||||||
"pinniped:request-audience",
|
oidcapi.ScopeRequestAudience,
|
||||||
"groups",
|
oidcapi.ScopeUsername,
|
||||||
|
oidcapi.ScopeGroups,
|
||||||
},
|
},
|
||||||
Audience: nil,
|
Audience: nil,
|
||||||
Public: true,
|
Public: true,
|
||||||
@ -167,7 +162,7 @@ func PinnipedCLI() *Client {
|
|||||||
JSONWebKeys: nil,
|
JSONWebKeys: nil,
|
||||||
JSONWebKeysURI: "",
|
JSONWebKeysURI: "",
|
||||||
RequestObjectSigningAlgorithm: "",
|
RequestObjectSigningAlgorithm: "",
|
||||||
TokenEndpointAuthSigningAlgorithm: oidc.RS256,
|
TokenEndpointAuthSigningAlgorithm: coreosoidc.RS256,
|
||||||
TokenEndpointAuthMethod: "none",
|
TokenEndpointAuthMethod: "none",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -194,7 +189,7 @@ func oidcClientCRToFositeClient(oidcClient *configv1alpha1.OIDCClient, clientSec
|
|||||||
JSONWebKeys: nil,
|
JSONWebKeys: nil,
|
||||||
JSONWebKeysURI: "",
|
JSONWebKeysURI: "",
|
||||||
RequestObjectSigningAlgorithm: "",
|
RequestObjectSigningAlgorithm: "",
|
||||||
TokenEndpointAuthSigningAlgorithm: oidc.RS256,
|
TokenEndpointAuthSigningAlgorithm: coreosoidc.RS256,
|
||||||
TokenEndpointAuthMethod: "client_secret_basic",
|
TokenEndpointAuthMethod: "client_secret_basic",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -269,7 +269,7 @@ func requireEqualsPinnipedCLI(t *testing.T, c *Client) {
|
|||||||
require.Equal(t, []string{"http://127.0.0.1/callback"}, c.GetRedirectURIs())
|
require.Equal(t, []string{"http://127.0.0.1/callback"}, c.GetRedirectURIs())
|
||||||
require.Equal(t, fosite.Arguments{"authorization_code", "refresh_token", "urn:ietf:params:oauth:grant-type:token-exchange"}, c.GetGrantTypes())
|
require.Equal(t, fosite.Arguments{"authorization_code", "refresh_token", "urn:ietf:params:oauth:grant-type:token-exchange"}, c.GetGrantTypes())
|
||||||
require.Equal(t, fosite.Arguments{"code"}, c.GetResponseTypes())
|
require.Equal(t, fosite.Arguments{"code"}, c.GetResponseTypes())
|
||||||
require.Equal(t, fosite.Arguments{oidc.ScopeOpenID, oidc.ScopeOfflineAccess, "profile", "email", "pinniped:request-audience", "groups"}, c.GetScopes())
|
require.Equal(t, fosite.Arguments{oidc.ScopeOpenID, oidc.ScopeOfflineAccess, "profile", "email", "pinniped:request-audience", "username", "groups"}, c.GetScopes())
|
||||||
require.True(t, c.IsPublic())
|
require.True(t, c.IsPublic())
|
||||||
require.Nil(t, c.GetAudience())
|
require.Nil(t, c.GetAudience())
|
||||||
require.Nil(t, c.GetRequestURIs())
|
require.Nil(t, c.GetRequestURIs())
|
||||||
@ -302,6 +302,7 @@ func requireEqualsPinnipedCLI(t *testing.T, c *Client) {
|
|||||||
"profile",
|
"profile",
|
||||||
"email",
|
"email",
|
||||||
"pinniped:request-audience",
|
"pinniped:request-audience",
|
||||||
|
"username",
|
||||||
"groups"
|
"groups"
|
||||||
],
|
],
|
||||||
"audience": null,
|
"audience": null,
|
||||||
|
@ -10,6 +10,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"go.pinniped.dev/generated/latest/apis/supervisor/idpdiscovery/v1alpha1"
|
"go.pinniped.dev/generated/latest/apis/supervisor/idpdiscovery/v1alpha1"
|
||||||
|
oidcapi "go.pinniped.dev/generated/latest/apis/supervisor/oidc"
|
||||||
"go.pinniped.dev/internal/oidc"
|
"go.pinniped.dev/internal/oidc"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -68,8 +69,8 @@ func NewHandler(issuerURL string) http.Handler {
|
|||||||
IDTokenSigningAlgValuesSupported: []string{"ES256"},
|
IDTokenSigningAlgValuesSupported: []string{"ES256"},
|
||||||
TokenEndpointAuthMethodsSupported: []string{"client_secret_basic"},
|
TokenEndpointAuthMethodsSupported: []string{"client_secret_basic"},
|
||||||
CodeChallengeMethodsSupported: []string{"S256"},
|
CodeChallengeMethodsSupported: []string{"S256"},
|
||||||
ScopesSupported: []string{"openid", "offline"},
|
ScopesSupported: []string{oidcapi.ScopeOpenID, oidcapi.ScopeOfflineAccess, oidcapi.ScopeRequestAudience, oidcapi.ScopeUsername, oidcapi.ScopeGroups},
|
||||||
ClaimsSupported: []string{"groups"},
|
ClaimsSupported: []string{oidcapi.IDTokenClaimUsername, oidcapi.IDTokenClaimGroups},
|
||||||
}
|
}
|
||||||
|
|
||||||
var b bytes.Buffer
|
var b bytes.Buffer
|
||||||
|
@ -45,9 +45,9 @@ func TestDiscovery(t *testing.T) {
|
|||||||
"subject_types_supported": ["public"],
|
"subject_types_supported": ["public"],
|
||||||
"id_token_signing_alg_values_supported": ["ES256"],
|
"id_token_signing_alg_values_supported": ["ES256"],
|
||||||
"token_endpoint_auth_methods_supported": ["client_secret_basic"],
|
"token_endpoint_auth_methods_supported": ["client_secret_basic"],
|
||||||
"scopes_supported": ["openid", "offline"],
|
"scopes_supported": ["openid", "offline_access", "pinniped:request-audience", "username", "groups"],
|
||||||
"code_challenge_methods_supported": ["S256"],
|
"code_challenge_methods_supported": ["S256"],
|
||||||
"claims_supported": ["groups"],
|
"claims_supported": ["username", "groups"],
|
||||||
"discovery.supervisor.pinniped.dev/v1alpha1": {
|
"discovery.supervisor.pinniped.dev/v1alpha1": {
|
||||||
"pinniped_identity_providers_endpoint": "https://some-issuer.com/some/path/v1alpha1/pinniped_identity_providers"
|
"pinniped_identity_providers_endpoint": "https://some-issuer.com/some/path/v1alpha1/pinniped_identity_providers"
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ import (
|
|||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/utils/strings/slices"
|
"k8s.io/utils/strings/slices"
|
||||||
|
|
||||||
|
oidcapi "go.pinniped.dev/generated/latest/apis/supervisor/oidc"
|
||||||
"go.pinniped.dev/internal/authenticators"
|
"go.pinniped.dev/internal/authenticators"
|
||||||
"go.pinniped.dev/internal/constable"
|
"go.pinniped.dev/internal/constable"
|
||||||
"go.pinniped.dev/internal/oidc"
|
"go.pinniped.dev/internal/oidc"
|
||||||
@ -27,7 +28,7 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
// The name of the email claim from https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
|
// The name of the email claim from https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
|
||||||
emailClaimName = "email"
|
emailClaimName = oidcapi.ScopeEmail
|
||||||
|
|
||||||
// The name of the email_verified claim from https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
|
// The name of the email_verified claim from https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
|
||||||
emailVerifiedClaimName = "email_verified"
|
emailVerifiedClaimName = "email_verified"
|
||||||
@ -55,11 +56,12 @@ func MakeDownstreamSession(subject string, username string, groups []string, gra
|
|||||||
if groups == nil {
|
if groups == nil {
|
||||||
groups = []string{}
|
groups = []string{}
|
||||||
}
|
}
|
||||||
openIDSession.IDTokenClaims().Extra = map[string]interface{}{
|
openIDSession.IDTokenClaims().Extra = map[string]interface{}{}
|
||||||
oidc.DownstreamUsernameClaim: username,
|
if slices.Contains(grantedScopes, oidcapi.ScopeUsername) {
|
||||||
|
openIDSession.IDTokenClaims().Extra[oidcapi.IDTokenClaimUsername] = username
|
||||||
}
|
}
|
||||||
if slices.Contains(grantedScopes, oidc.DownstreamGroupsScope) {
|
if slices.Contains(grantedScopes, oidcapi.ScopeGroups) {
|
||||||
openIDSession.IDTokenClaims().Extra[oidc.DownstreamGroupsClaim] = groups
|
openIDSession.IDTokenClaims().Extra[oidcapi.IDTokenClaimGroups] = groups
|
||||||
}
|
}
|
||||||
return openIDSession
|
return openIDSession
|
||||||
}
|
}
|
||||||
@ -68,8 +70,10 @@ func MakeDownstreamLDAPOrADCustomSessionData(
|
|||||||
ldapUpstream provider.UpstreamLDAPIdentityProviderI,
|
ldapUpstream provider.UpstreamLDAPIdentityProviderI,
|
||||||
idpType psession.ProviderType,
|
idpType psession.ProviderType,
|
||||||
authenticateResponse *authenticators.Response,
|
authenticateResponse *authenticators.Response,
|
||||||
|
username string,
|
||||||
) *psession.CustomSessionData {
|
) *psession.CustomSessionData {
|
||||||
customSessionData := &psession.CustomSessionData{
|
customSessionData := &psession.CustomSessionData{
|
||||||
|
Username: username,
|
||||||
ProviderUID: ldapUpstream.GetResourceUID(),
|
ProviderUID: ldapUpstream.GetResourceUID(),
|
||||||
ProviderName: ldapUpstream.GetName(),
|
ProviderName: ldapUpstream.GetName(),
|
||||||
ProviderType: idpType,
|
ProviderType: idpType,
|
||||||
@ -92,17 +96,22 @@ func MakeDownstreamLDAPOrADCustomSessionData(
|
|||||||
return customSessionData
|
return customSessionData
|
||||||
}
|
}
|
||||||
|
|
||||||
func MakeDownstreamOIDCCustomSessionData(oidcUpstream provider.UpstreamOIDCIdentityProviderI, token *oidctypes.Token) (*psession.CustomSessionData, error) {
|
func MakeDownstreamOIDCCustomSessionData(
|
||||||
upstreamSubject, err := ExtractStringClaimValue(oidc.IDTokenSubjectClaim, oidcUpstream.GetName(), token.IDToken.Claims)
|
oidcUpstream provider.UpstreamOIDCIdentityProviderI,
|
||||||
|
token *oidctypes.Token,
|
||||||
|
username string,
|
||||||
|
) (*psession.CustomSessionData, error) {
|
||||||
|
upstreamSubject, err := ExtractStringClaimValue(oidcapi.IDTokenClaimSubject, oidcUpstream.GetName(), token.IDToken.Claims)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
upstreamIssuer, err := ExtractStringClaimValue(oidc.IDTokenIssuerClaim, oidcUpstream.GetName(), token.IDToken.Claims)
|
upstreamIssuer, err := ExtractStringClaimValue(oidcapi.IDTokenClaimIssuer, oidcUpstream.GetName(), token.IDToken.Claims)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
customSessionData := &psession.CustomSessionData{
|
customSessionData := &psession.CustomSessionData{
|
||||||
|
Username: username,
|
||||||
ProviderUID: oidcUpstream.GetResourceUID(),
|
ProviderUID: oidcUpstream.GetResourceUID(),
|
||||||
ProviderName: oidcUpstream.GetName(),
|
ProviderName: oidcUpstream.GetName(),
|
||||||
ProviderType: psession.ProviderTypeOIDC,
|
ProviderType: psession.ProviderTypeOIDC,
|
||||||
@ -148,11 +157,30 @@ func MakeDownstreamOIDCCustomSessionData(oidcUpstream provider.UpstreamOIDCIdent
|
|||||||
return customSessionData, nil
|
return customSessionData, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GrantScopesIfRequested auto-grants the scopes for which we do not require end-user approval, if they were requested.
|
// AutoApproveScopes auto-grants the scopes which we support and for which we do not require end-user approval,
|
||||||
func GrantScopesIfRequested(authorizeRequester fosite.AuthorizeRequester, scopes []string) {
|
// if they were requested. This should only be called after it has been validated that the client is allowed to request
|
||||||
for _, scope := range scopes {
|
// 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,
|
||||||
|
} {
|
||||||
oidc.GrantScopeIfRequested(authorizeRequester, scope)
|
oidc.GrantScopeIfRequested(authorizeRequester, scope)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDownstreamIdentityFromUpstreamIDToken returns the mapped subject, username, and group names, in that order.
|
// GetDownstreamIdentityFromUpstreamIDToken returns the mapped subject, username, and group names, in that order.
|
||||||
@ -179,11 +207,11 @@ func getSubjectAndUsernameFromUpstreamIDToken(
|
|||||||
) (string, string, error) {
|
) (string, string, error) {
|
||||||
// The spec says the "sub" claim is only unique per issuer,
|
// The spec says the "sub" claim is only unique per issuer,
|
||||||
// so we will prepend the issuer string to make it globally unique.
|
// so we will prepend the issuer string to make it globally unique.
|
||||||
upstreamIssuer, err := ExtractStringClaimValue(oidc.IDTokenIssuerClaim, upstreamIDPConfig.GetName(), idTokenClaims)
|
upstreamIssuer, err := ExtractStringClaimValue(oidcapi.IDTokenClaimIssuer, upstreamIDPConfig.GetName(), idTokenClaims)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
upstreamSubject, err := ExtractStringClaimValue(oidc.IDTokenSubjectClaim, upstreamIDPConfig.GetName(), idTokenClaims)
|
upstreamSubject, err := ExtractStringClaimValue(oidcapi.IDTokenClaimSubject, upstreamIDPConfig.GetName(), idTokenClaims)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
@ -266,13 +294,13 @@ func DownstreamSubjectFromUpstreamLDAP(ldapUpstream provider.UpstreamLDAPIdentit
|
|||||||
|
|
||||||
func DownstreamLDAPSubject(uid string, ldapURL url.URL) string {
|
func DownstreamLDAPSubject(uid string, ldapURL url.URL) string {
|
||||||
q := ldapURL.Query()
|
q := ldapURL.Query()
|
||||||
q.Set(oidc.IDTokenSubjectClaim, uid)
|
q.Set(oidcapi.IDTokenClaimSubject, uid)
|
||||||
ldapURL.RawQuery = q.Encode()
|
ldapURL.RawQuery = q.Encode()
|
||||||
return ldapURL.String()
|
return ldapURL.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func downstreamSubjectFromUpstreamOIDC(upstreamIssuerAsString string, upstreamSubject string) string {
|
func downstreamSubjectFromUpstreamOIDC(upstreamIssuerAsString string, upstreamSubject string) string {
|
||||||
return fmt.Sprintf("%s?%s=%s", upstreamIssuerAsString, oidc.IDTokenSubjectClaim, url.QueryEscape(upstreamSubject))
|
return fmt.Sprintf("%s?%s=%s", upstreamIssuerAsString, oidcapi.IDTokenClaimSubject, url.QueryEscape(upstreamSubject))
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetGroupsFromUpstreamIDToken returns mapped group names coerced into a slice of strings.
|
// GetGroupsFromUpstreamIDToken returns mapped group names coerced into a slice of strings.
|
||||||
|
@ -7,7 +7,6 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
coreosoidc "github.com/coreos/go-oidc/v3/oidc"
|
|
||||||
"github.com/ory/fosite"
|
"github.com/ory/fosite"
|
||||||
|
|
||||||
"go.pinniped.dev/internal/httputil/httperr"
|
"go.pinniped.dev/internal/httputil/httperr"
|
||||||
@ -46,10 +45,10 @@ func NewPostHandler(issuerURL string, upstreamIDPs oidc.UpstreamIdentityProvider
|
|||||||
return httperr.New(http.StatusBadRequest, "error using state downstream auth params")
|
return httperr.New(http.StatusBadRequest, "error using state downstream auth params")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Automatically grant the openid, offline_access, pinniped:request-audience and groups scopes, but only if they were requested.
|
// Automatically grant certain scopes, but only if they were requested.
|
||||||
// This is instead of asking the user to approve these scopes. Note that `NewAuthorizeRequest` would have returned
|
// 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.
|
// an error if the client requested a scope that they are not allowed to request, so we don't need to worry about that here.
|
||||||
downstreamsession.GrantScopesIfRequested(authorizeRequester, []string{coreosoidc.ScopeOpenID, coreosoidc.ScopeOfflineAccess, oidc.RequestAudienceScope, oidc.DownstreamGroupsScope})
|
downstreamsession.AutoApproveScopes(authorizeRequester)
|
||||||
|
|
||||||
// Get the username and password form params from the POST body.
|
// Get the username and password form params from the POST body.
|
||||||
username := r.PostFormValue(usernameParamName)
|
username := r.PostFormValue(usernameParamName)
|
||||||
@ -83,7 +82,7 @@ func NewPostHandler(issuerURL string, upstreamIDPs oidc.UpstreamIdentityProvider
|
|||||||
subject := downstreamsession.DownstreamSubjectFromUpstreamLDAP(ldapUpstream, authenticateResponse)
|
subject := downstreamsession.DownstreamSubjectFromUpstreamLDAP(ldapUpstream, authenticateResponse)
|
||||||
username = authenticateResponse.User.GetName()
|
username = authenticateResponse.User.GetName()
|
||||||
groups := authenticateResponse.User.GetGroups()
|
groups := authenticateResponse.User.GetGroups()
|
||||||
customSessionData := downstreamsession.MakeDownstreamLDAPOrADCustomSessionData(ldapUpstream, idpType, authenticateResponse)
|
customSessionData := downstreamsession.MakeDownstreamLDAPOrADCustomSessionData(ldapUpstream, idpType, authenticateResponse, username)
|
||||||
openIDSession := downstreamsession.MakeDownstreamSession(subject, username, groups, authorizeRequester.GetGrantedScopes(), customSessionData)
|
openIDSession := downstreamsession.MakeDownstreamSession(subject, username, groups, authorizeRequester.GetGrantedScopes(), customSessionData)
|
||||||
oidc.PerformAuthcodeRedirect(r, w, oauthHelper, authorizeRequester, openIDSession, false)
|
oidc.PerformAuthcodeRedirect(r, w, oauthHelper, authorizeRequester, openIDSession, false)
|
||||||
|
|
||||||
|
@ -17,10 +17,12 @@ import (
|
|||||||
"k8s.io/apiserver/pkg/authentication/user"
|
"k8s.io/apiserver/pkg/authentication/user"
|
||||||
"k8s.io/client-go/kubernetes/fake"
|
"k8s.io/client-go/kubernetes/fake"
|
||||||
|
|
||||||
|
configv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1"
|
||||||
supervisorfake "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned/fake"
|
supervisorfake "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned/fake"
|
||||||
"go.pinniped.dev/internal/authenticators"
|
"go.pinniped.dev/internal/authenticators"
|
||||||
"go.pinniped.dev/internal/oidc"
|
"go.pinniped.dev/internal/oidc"
|
||||||
"go.pinniped.dev/internal/oidc/jwks"
|
"go.pinniped.dev/internal/oidc/jwks"
|
||||||
|
"go.pinniped.dev/internal/oidc/oidcclientvalidator"
|
||||||
"go.pinniped.dev/internal/psession"
|
"go.pinniped.dev/internal/psession"
|
||||||
"go.pinniped.dev/internal/testutil"
|
"go.pinniped.dev/internal/testutil"
|
||||||
"go.pinniped.dev/internal/testutil/oidctestutil"
|
"go.pinniped.dev/internal/testutil/oidctestutil"
|
||||||
@ -86,8 +88,8 @@ func TestPostLoginEndpoint(t *testing.T) {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
happyDownstreamScopesRequested := []string{"openid", "groups"}
|
happyDownstreamScopesRequested := []string{"openid", "username", "groups"}
|
||||||
happyDownstreamScopesGranted := []string{"openid", "groups"}
|
happyDownstreamScopesGranted := []string{"openid", "username", "groups"}
|
||||||
|
|
||||||
happyDownstreamRequestParamsQuery := url.Values{
|
happyDownstreamRequestParamsQuery := url.Values{
|
||||||
"response_type": []string{"code"},
|
"response_type": []string{"code"},
|
||||||
@ -192,6 +194,7 @@ func TestPostLoginEndpoint(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
expectedHappyActiveDirectoryUpstreamCustomSession := &psession.CustomSessionData{
|
expectedHappyActiveDirectoryUpstreamCustomSession := &psession.CustomSessionData{
|
||||||
|
Username: happyLDAPUsernameFromAuthenticator,
|
||||||
ProviderUID: activeDirectoryUpstreamResourceUID,
|
ProviderUID: activeDirectoryUpstreamResourceUID,
|
||||||
ProviderName: activeDirectoryUpstreamName,
|
ProviderName: activeDirectoryUpstreamName,
|
||||||
ProviderType: psession.ProviderTypeActiveDirectory,
|
ProviderType: psession.ProviderTypeActiveDirectory,
|
||||||
@ -204,6 +207,7 @@ func TestPostLoginEndpoint(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
expectedHappyLDAPUpstreamCustomSession := &psession.CustomSessionData{
|
expectedHappyLDAPUpstreamCustomSession := &psession.CustomSessionData{
|
||||||
|
Username: happyLDAPUsernameFromAuthenticator,
|
||||||
ProviderUID: ldapUpstreamResourceUID,
|
ProviderUID: ldapUpstreamResourceUID,
|
||||||
ProviderName: ldapUpstreamName,
|
ProviderName: ldapUpstreamName,
|
||||||
ProviderType: psession.ProviderTypeLDAP,
|
ProviderType: psession.ProviderTypeLDAP,
|
||||||
@ -216,7 +220,7 @@ func TestPostLoginEndpoint(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Note that fosite puts the granted scopes as a param in the redirect URI even though the spec doesn't seem to require it
|
// Note that fosite puts the granted scopes as a param in the redirect URI even though the spec doesn't seem to require it
|
||||||
happyAuthcodeDownstreamRedirectLocationRegexp := downstreamRedirectURI + `\?code=([^&]+)&scope=openid\+groups&state=` + happyDownstreamState
|
happyAuthcodeDownstreamRedirectLocationRegexp := downstreamRedirectURI + `\?code=([^&]+)&scope=openid\+username\+groups&state=` + happyDownstreamState
|
||||||
|
|
||||||
happyUsernamePasswordFormParams := url.Values{userParam: []string{happyLDAPUsername}, passParam: []string{happyLDAPPassword}}
|
happyUsernamePasswordFormParams := url.Values{userParam: []string{happyLDAPUsername}, passParam: []string{happyLDAPPassword}}
|
||||||
|
|
||||||
@ -237,7 +241,8 @@ func TestPostLoginEndpoint(t *testing.T) {
|
|||||||
|
|
||||||
addFullyCapableDynamicClientAndSecretToKubeResources := func(t *testing.T, supervisorClient *supervisorfake.Clientset, kubeClient *fake.Clientset) {
|
addFullyCapableDynamicClientAndSecretToKubeResources := func(t *testing.T, supervisorClient *supervisorfake.Clientset, kubeClient *fake.Clientset) {
|
||||||
oidcClient, secret := testutil.FullyCapableOIDCClientAndStorageSecret(t,
|
oidcClient, secret := testutil.FullyCapableOIDCClientAndStorageSecret(t,
|
||||||
"some-namespace", downstreamDynamicClientID, downstreamDynamicClientUID, downstreamRedirectURI, []string{testutil.HashedPassword1AtGoMinCost})
|
"some-namespace", downstreamDynamicClientID, downstreamDynamicClientUID, downstreamRedirectURI,
|
||||||
|
[]string{testutil.HashedPassword1AtGoMinCost}, oidcclientvalidator.Validate)
|
||||||
require.NoError(t, supervisorClient.Tracker().Add(oidcClient))
|
require.NoError(t, supervisorClient.Tracker().Add(oidcClient))
|
||||||
require.NoError(t, kubeClient.Tracker().Add(secret))
|
require.NoError(t, kubeClient.Tracker().Add(secret))
|
||||||
}
|
}
|
||||||
@ -413,7 +418,7 @@ func TestPostLoginEndpoint(t *testing.T) {
|
|||||||
wantStatus: http.StatusSeeOther,
|
wantStatus: http.StatusSeeOther,
|
||||||
wantContentType: htmlContentType,
|
wantContentType: htmlContentType,
|
||||||
wantBodyString: "",
|
wantBodyString: "",
|
||||||
wantRedirectLocationRegexp: "http://127.0.0.1:4242/callback" + `\?code=([^&]+)&scope=openid\+groups&state=` + happyDownstreamState,
|
wantRedirectLocationRegexp: "http://127.0.0.1:4242/callback" + `\?code=([^&]+)&scope=openid\+username\+groups&state=` + happyDownstreamState,
|
||||||
wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID,
|
wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID,
|
||||||
wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator,
|
wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator,
|
||||||
wantDownstreamIDTokenGroups: happyLDAPGroups,
|
wantDownstreamIDTokenGroups: happyLDAPGroups,
|
||||||
@ -439,7 +444,7 @@ func TestPostLoginEndpoint(t *testing.T) {
|
|||||||
wantStatus: http.StatusSeeOther,
|
wantStatus: http.StatusSeeOther,
|
||||||
wantContentType: htmlContentType,
|
wantContentType: htmlContentType,
|
||||||
wantBodyString: "",
|
wantBodyString: "",
|
||||||
wantRedirectLocationRegexp: "http://127.0.0.1:4242/callback" + `\?code=([^&]+)&scope=openid\+groups&state=` + happyDownstreamState,
|
wantRedirectLocationRegexp: "http://127.0.0.1:4242/callback" + `\?code=([^&]+)&scope=openid\+username\+groups&state=` + happyDownstreamState,
|
||||||
wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID,
|
wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID,
|
||||||
wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator,
|
wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator,
|
||||||
wantDownstreamIDTokenGroups: happyLDAPGroups,
|
wantDownstreamIDTokenGroups: happyLDAPGroups,
|
||||||
@ -464,13 +469,14 @@ func TestPostLoginEndpoint(t *testing.T) {
|
|||||||
wantStatus: http.StatusSeeOther,
|
wantStatus: http.StatusSeeOther,
|
||||||
wantContentType: htmlContentType,
|
wantContentType: htmlContentType,
|
||||||
wantBodyString: "",
|
wantBodyString: "",
|
||||||
wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=openid\+offline_access\+pinniped%3Arequest-audience&state=` + happyDownstreamState,
|
// username and groups scopes were not requested but are granted anyway for the pinniped-cli client for backwards compatibility
|
||||||
|
wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=openid\+offline_access\+pinniped%3Arequest-audience\+username\+groups&state=` + happyDownstreamState,
|
||||||
wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID,
|
wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID,
|
||||||
wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator,
|
wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator,
|
||||||
wantDownstreamIDTokenGroups: happyLDAPGroups,
|
wantDownstreamIDTokenGroups: happyLDAPGroups,
|
||||||
wantDownstreamRequestedScopes: []string{"openid", "offline_access", "pinniped:request-audience"},
|
wantDownstreamRequestedScopes: []string{"openid", "offline_access", "pinniped:request-audience"},
|
||||||
wantDownstreamRedirectURI: downstreamRedirectURI,
|
wantDownstreamRedirectURI: downstreamRedirectURI,
|
||||||
wantDownstreamGrantedScopes: []string{"openid", "offline_access", "pinniped:request-audience"},
|
wantDownstreamGrantedScopes: []string{"openid", "offline_access", "pinniped:request-audience", "username", "groups"},
|
||||||
wantDownstreamNonce: downstreamNonce,
|
wantDownstreamNonce: downstreamNonce,
|
||||||
wantDownstreamClient: downstreamPinnipedCLIClientID,
|
wantDownstreamClient: downstreamPinnipedCLIClientID,
|
||||||
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
|
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
|
||||||
@ -478,7 +484,7 @@ func TestPostLoginEndpoint(t *testing.T) {
|
|||||||
wantDownstreamCustomSessionData: expectedHappyLDAPUpstreamCustomSession,
|
wantDownstreamCustomSessionData: expectedHappyLDAPUpstreamCustomSession,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "happy LDAP login when there are additional allowed downstream requested scopes with dynamic client",
|
name: "happy LDAP login when there are additional allowed downstream requested scopes with dynamic client, when dynamic client is allowed to request username and groups but does not request them",
|
||||||
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
|
||||||
kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources,
|
kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources,
|
||||||
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
|
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
|
||||||
@ -492,8 +498,8 @@ func TestPostLoginEndpoint(t *testing.T) {
|
|||||||
wantBodyString: "",
|
wantBodyString: "",
|
||||||
wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=openid\+offline_access\+pinniped%3Arequest-audience&state=` + happyDownstreamState,
|
wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=openid\+offline_access\+pinniped%3Arequest-audience&state=` + happyDownstreamState,
|
||||||
wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID,
|
wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID,
|
||||||
wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator,
|
wantDownstreamIDTokenUsername: "", // username scope was not requested, so there should be no username in the ID token
|
||||||
wantDownstreamIDTokenGroups: happyLDAPGroups,
|
wantDownstreamIDTokenGroups: []string{}, // groups scope was not requested, so there should be no groups in the ID token
|
||||||
wantDownstreamRequestedScopes: []string{"openid", "offline_access", "pinniped:request-audience"},
|
wantDownstreamRequestedScopes: []string{"openid", "offline_access", "pinniped:request-audience"},
|
||||||
wantDownstreamRedirectURI: downstreamRedirectURI,
|
wantDownstreamRedirectURI: downstreamRedirectURI,
|
||||||
wantDownstreamGrantedScopes: []string{"openid", "offline_access", "pinniped:request-audience"},
|
wantDownstreamGrantedScopes: []string{"openid", "offline_access", "pinniped:request-audience"},
|
||||||
@ -503,6 +509,74 @@ func TestPostLoginEndpoint(t *testing.T) {
|
|||||||
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
|
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
|
||||||
wantDownstreamCustomSessionData: expectedHappyLDAPUpstreamCustomSession,
|
wantDownstreamCustomSessionData: expectedHappyLDAPUpstreamCustomSession,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "happy LDAP login when there are additional allowed downstream requested scopes with dynamic client, when dynamic client is not allowed to request username and does not request username",
|
||||||
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
|
||||||
|
kubeResources: func(t *testing.T, supervisorClient *supervisorfake.Clientset, kubeClient *fake.Clientset) {
|
||||||
|
oidcClient, secret := testutil.OIDCClientAndStorageSecret(t,
|
||||||
|
"some-namespace", downstreamDynamicClientID, downstreamDynamicClientUID,
|
||||||
|
[]configv1alpha1.GrantType{"authorization_code", "refresh_token"}, // token exchange not allowed (required to exclude username scope)
|
||||||
|
[]configv1alpha1.Scope{"openid", "offline_access", "groups"}, // username not allowed
|
||||||
|
downstreamRedirectURI, []string{testutil.HashedPassword1AtGoMinCost}, oidcclientvalidator.Validate)
|
||||||
|
require.NoError(t, supervisorClient.Tracker().Add(oidcClient))
|
||||||
|
require.NoError(t, kubeClient.Tracker().Add(secret))
|
||||||
|
},
|
||||||
|
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
|
||||||
|
data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQueryForDynamicClient,
|
||||||
|
map[string]string{"scope": "openid groups offline_access"},
|
||||||
|
).Encode()
|
||||||
|
}),
|
||||||
|
formParams: happyUsernamePasswordFormParams,
|
||||||
|
wantStatus: http.StatusSeeOther,
|
||||||
|
wantContentType: htmlContentType,
|
||||||
|
wantBodyString: "",
|
||||||
|
wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=openid\+offline_access\+groups&state=` + happyDownstreamState,
|
||||||
|
wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID,
|
||||||
|
wantDownstreamIDTokenUsername: "", // username scope was not requested, so there should be no username in the ID token
|
||||||
|
wantDownstreamIDTokenGroups: happyLDAPGroups,
|
||||||
|
wantDownstreamRequestedScopes: []string{"openid", "offline_access", "groups"},
|
||||||
|
wantDownstreamRedirectURI: downstreamRedirectURI,
|
||||||
|
wantDownstreamGrantedScopes: []string{"openid", "offline_access", "groups"},
|
||||||
|
wantDownstreamNonce: downstreamNonce,
|
||||||
|
wantDownstreamClient: downstreamDynamicClientID,
|
||||||
|
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
|
||||||
|
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
|
||||||
|
wantDownstreamCustomSessionData: expectedHappyLDAPUpstreamCustomSession,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "happy LDAP login when there are additional allowed downstream requested scopes with dynamic client, when dynamic client is not allowed to request groups and does not request groups",
|
||||||
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
|
||||||
|
kubeResources: func(t *testing.T, supervisorClient *supervisorfake.Clientset, kubeClient *fake.Clientset) {
|
||||||
|
oidcClient, secret := testutil.OIDCClientAndStorageSecret(t,
|
||||||
|
"some-namespace", downstreamDynamicClientID, downstreamDynamicClientUID,
|
||||||
|
[]configv1alpha1.GrantType{"authorization_code", "refresh_token"}, // token exchange not allowed (required to exclude groups scope)
|
||||||
|
[]configv1alpha1.Scope{"openid", "offline_access", "username"}, // groups not allowed
|
||||||
|
downstreamRedirectURI, []string{testutil.HashedPassword1AtGoMinCost}, oidcclientvalidator.Validate)
|
||||||
|
require.NoError(t, supervisorClient.Tracker().Add(oidcClient))
|
||||||
|
require.NoError(t, kubeClient.Tracker().Add(secret))
|
||||||
|
},
|
||||||
|
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
|
||||||
|
data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQueryForDynamicClient,
|
||||||
|
map[string]string{"scope": "openid username offline_access"},
|
||||||
|
).Encode()
|
||||||
|
}),
|
||||||
|
formParams: happyUsernamePasswordFormParams,
|
||||||
|
wantStatus: http.StatusSeeOther,
|
||||||
|
wantContentType: htmlContentType,
|
||||||
|
wantBodyString: "",
|
||||||
|
wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=openid\+offline_access\+username&state=` + happyDownstreamState,
|
||||||
|
wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID,
|
||||||
|
wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator,
|
||||||
|
wantDownstreamIDTokenGroups: []string{}, // groups scope was not requested, so there should be no groups in the ID token
|
||||||
|
wantDownstreamRequestedScopes: []string{"openid", "offline_access", "username"},
|
||||||
|
wantDownstreamRedirectURI: downstreamRedirectURI,
|
||||||
|
wantDownstreamGrantedScopes: []string{"openid", "offline_access", "username"},
|
||||||
|
wantDownstreamNonce: downstreamNonce,
|
||||||
|
wantDownstreamClient: downstreamDynamicClientID,
|
||||||
|
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
|
||||||
|
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
|
||||||
|
wantDownstreamCustomSessionData: expectedHappyLDAPUpstreamCustomSession,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "happy LDAP when downstream OIDC validations are skipped because the openid scope was not requested",
|
name: "happy LDAP when downstream OIDC validations are skipped because the openid scope was not requested",
|
||||||
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
|
||||||
@ -519,13 +593,14 @@ func TestPostLoginEndpoint(t *testing.T) {
|
|||||||
wantStatus: http.StatusSeeOther,
|
wantStatus: http.StatusSeeOther,
|
||||||
wantContentType: htmlContentType,
|
wantContentType: htmlContentType,
|
||||||
wantBodyString: "",
|
wantBodyString: "",
|
||||||
wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=&state=` + happyDownstreamState, // no scopes granted
|
// username and groups scopes were not requested but are granted anyway for the pinniped-cli client for backwards compatibility
|
||||||
|
wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=username\+groups&state=` + happyDownstreamState,
|
||||||
wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID,
|
wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID,
|
||||||
wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator,
|
wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator,
|
||||||
wantDownstreamIDTokenGroups: happyLDAPGroups,
|
wantDownstreamIDTokenGroups: happyLDAPGroups,
|
||||||
wantDownstreamRequestedScopes: []string{"email"}, // only email was requested
|
wantDownstreamRequestedScopes: []string{"email"}, // only email was requested
|
||||||
wantDownstreamRedirectURI: downstreamRedirectURI,
|
wantDownstreamRedirectURI: downstreamRedirectURI,
|
||||||
wantDownstreamGrantedScopes: []string{}, // no scopes granted
|
wantDownstreamGrantedScopes: []string{"username", "groups"},
|
||||||
wantDownstreamNonce: downstreamNonce,
|
wantDownstreamNonce: downstreamNonce,
|
||||||
wantDownstreamClient: downstreamPinnipedCLIClientID,
|
wantDownstreamClient: downstreamPinnipedCLIClientID,
|
||||||
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
|
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
|
||||||
@ -533,7 +608,7 @@ func TestPostLoginEndpoint(t *testing.T) {
|
|||||||
wantDownstreamCustomSessionData: expectedHappyLDAPUpstreamCustomSession,
|
wantDownstreamCustomSessionData: expectedHappyLDAPUpstreamCustomSession,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "happy LDAP login when groups scope is not requested",
|
name: "happy LDAP login when username and groups scopes are not requested",
|
||||||
idps: oidctestutil.NewUpstreamIDPListerBuilder().
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().
|
||||||
WithLDAP(&upstreamLDAPIdentityProvider). // should pick this one
|
WithLDAP(&upstreamLDAPIdentityProvider). // should pick this one
|
||||||
WithActiveDirectory(&erroringUpstreamLDAPIdentityProvider),
|
WithActiveDirectory(&erroringUpstreamLDAPIdentityProvider),
|
||||||
@ -546,12 +621,14 @@ func TestPostLoginEndpoint(t *testing.T) {
|
|||||||
wantStatus: http.StatusSeeOther,
|
wantStatus: http.StatusSeeOther,
|
||||||
wantContentType: htmlContentType,
|
wantContentType: htmlContentType,
|
||||||
wantBodyString: "",
|
wantBodyString: "",
|
||||||
wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=openid&state=` + happyDownstreamState,
|
// username and groups scopes were not requested but are granted anyway for the pinniped-cli client for backwards compatibility
|
||||||
|
wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=openid\+username\+groups&state=` + happyDownstreamState,
|
||||||
wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID,
|
wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID,
|
||||||
wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator,
|
wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator,
|
||||||
|
wantDownstreamIDTokenGroups: happyLDAPGroups,
|
||||||
wantDownstreamRequestedScopes: []string{"openid"},
|
wantDownstreamRequestedScopes: []string{"openid"},
|
||||||
wantDownstreamRedirectURI: downstreamRedirectURI,
|
wantDownstreamRedirectURI: downstreamRedirectURI,
|
||||||
wantDownstreamGrantedScopes: []string{"openid"},
|
wantDownstreamGrantedScopes: []string{"openid", "username", "groups"},
|
||||||
wantDownstreamNonce: downstreamNonce,
|
wantDownstreamNonce: downstreamNonce,
|
||||||
wantDownstreamClient: downstreamPinnipedCLIClientID,
|
wantDownstreamClient: downstreamPinnipedCLIClientID,
|
||||||
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
|
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
|
||||||
@ -810,6 +887,46 @@ func TestPostLoginEndpoint(t *testing.T) {
|
|||||||
formParams: happyUsernamePasswordFormParams,
|
formParams: happyUsernamePasswordFormParams,
|
||||||
wantErr: "error using state downstream auth params",
|
wantErr: "error using state downstream auth params",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "using dynamic client which is not allowed to request username scope in authorize request but requests it anyway",
|
||||||
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
|
||||||
|
kubeResources: func(t *testing.T, supervisorClient *supervisorfake.Clientset, kubeClient *fake.Clientset) {
|
||||||
|
oidcClient, secret := testutil.OIDCClientAndStorageSecret(t,
|
||||||
|
"some-namespace", downstreamDynamicClientID, downstreamDynamicClientUID,
|
||||||
|
[]configv1alpha1.GrantType{"authorization_code", "refresh_token"}, // token exchange not allowed (required to exclude username scope)
|
||||||
|
[]configv1alpha1.Scope{"openid", "offline_access", "groups"}, // username not allowed
|
||||||
|
downstreamRedirectURI, []string{testutil.HashedPassword1AtGoMinCost}, oidcclientvalidator.Validate)
|
||||||
|
require.NoError(t, supervisorClient.Tracker().Add(oidcClient))
|
||||||
|
require.NoError(t, kubeClient.Tracker().Add(secret))
|
||||||
|
},
|
||||||
|
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
|
||||||
|
data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQueryForDynamicClient,
|
||||||
|
map[string]string{"scope": "openid username offline_access"},
|
||||||
|
).Encode()
|
||||||
|
}),
|
||||||
|
formParams: happyUsernamePasswordFormParams,
|
||||||
|
wantErr: "error using state downstream auth params",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "using dynamic client which is not allowed to request groups scope in authorize request but requests it anyway",
|
||||||
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
|
||||||
|
kubeResources: func(t *testing.T, supervisorClient *supervisorfake.Clientset, kubeClient *fake.Clientset) {
|
||||||
|
oidcClient, secret := testutil.OIDCClientAndStorageSecret(t,
|
||||||
|
"some-namespace", downstreamDynamicClientID, downstreamDynamicClientUID,
|
||||||
|
[]configv1alpha1.GrantType{"authorization_code", "refresh_token"}, // token exchange not allowed (required to exclude groups scope)
|
||||||
|
[]configv1alpha1.Scope{"openid", "offline_access", "username"}, // groups not allowed
|
||||||
|
downstreamRedirectURI, []string{testutil.HashedPassword1AtGoMinCost}, oidcclientvalidator.Validate)
|
||||||
|
require.NoError(t, supervisorClient.Tracker().Add(oidcClient))
|
||||||
|
require.NoError(t, kubeClient.Tracker().Add(secret))
|
||||||
|
},
|
||||||
|
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
|
||||||
|
data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQueryForDynamicClient,
|
||||||
|
map[string]string{"scope": "openid groups offline_access"},
|
||||||
|
).Encode()
|
||||||
|
}),
|
||||||
|
formParams: happyUsernamePasswordFormParams,
|
||||||
|
wantErr: "error using state downstream auth params",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "downstream scopes do not match what is configured for client with dynamic client",
|
name: "downstream scopes do not match what is configured for client with dynamic client",
|
||||||
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
|
||||||
|
@ -11,13 +11,13 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
coreosoidc "github.com/coreos/go-oidc/v3/oidc"
|
|
||||||
"github.com/felixge/httpsnoop"
|
"github.com/felixge/httpsnoop"
|
||||||
"github.com/ory/fosite"
|
"github.com/ory/fosite"
|
||||||
"github.com/ory/fosite/compose"
|
"github.com/ory/fosite/compose"
|
||||||
errorsx "github.com/pkg/errors"
|
errorsx "github.com/pkg/errors"
|
||||||
|
|
||||||
"go.pinniped.dev/generated/latest/apis/supervisor/idpdiscovery/v1alpha1"
|
"go.pinniped.dev/generated/latest/apis/supervisor/idpdiscovery/v1alpha1"
|
||||||
|
oidcapi "go.pinniped.dev/generated/latest/apis/supervisor/oidc"
|
||||||
"go.pinniped.dev/internal/httputil/httperr"
|
"go.pinniped.dev/internal/httputil/httperr"
|
||||||
"go.pinniped.dev/internal/oidc/csrftoken"
|
"go.pinniped.dev/internal/oidc/csrftoken"
|
||||||
"go.pinniped.dev/internal/oidc/jwks"
|
"go.pinniped.dev/internal/oidc/jwks"
|
||||||
@ -40,16 +40,17 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// Just in case we need to make a breaking change to the format of the upstream state param,
|
// UpstreamStateParamFormatVersion exists just in case we need to make a breaking change to the format of the
|
||||||
// we are including a format version number. This gives the opportunity for a future version of Pinniped
|
// upstream state param, we are including a format version number. This gives the opportunity for a future version
|
||||||
// to have the consumer of this format decide to reject versions that it doesn't understand.
|
// of Pinniped to have the consumer of this format decide to reject versions that it doesn't understand.
|
||||||
//
|
//
|
||||||
// Version 1 was the original version.
|
// Version 1 was the original version.
|
||||||
// Version 2 added the UpstreamType field to the UpstreamStateParamData struct.
|
// Version 2 added the UpstreamType field to the UpstreamStateParamData struct.
|
||||||
UpstreamStateParamFormatVersion = "2"
|
UpstreamStateParamFormatVersion = "2"
|
||||||
|
|
||||||
// The `name` passed to the encoder for encoding the upstream state param value. This name is short
|
// UpstreamStateParamEncodingName is the `name` passed to the encoder for encoding the upstream state param value.
|
||||||
// because it will be encoded into the upstream state param value and we're trying to keep that small.
|
// This name is short because it will be encoded into the upstream state param value, and we're trying to keep that
|
||||||
|
// small.
|
||||||
UpstreamStateParamEncodingName = "s"
|
UpstreamStateParamEncodingName = "s"
|
||||||
|
|
||||||
// CSRFCookieName is the name of the browser cookie which shall hold our CSRF value.
|
// CSRFCookieName is the name of the browser cookie which shall hold our CSRF value.
|
||||||
@ -61,29 +62,6 @@ const (
|
|||||||
// cookie contents.
|
// cookie contents.
|
||||||
CSRFCookieEncodingName = "csrf"
|
CSRFCookieEncodingName = "csrf"
|
||||||
|
|
||||||
// The name of the issuer claim specified in the OIDC spec.
|
|
||||||
IDTokenIssuerClaim = "iss"
|
|
||||||
|
|
||||||
// The name of the subject claim specified in the OIDC spec.
|
|
||||||
IDTokenSubjectClaim = "sub"
|
|
||||||
|
|
||||||
// DownstreamUsernameClaim is a custom claim in the downstream ID token
|
|
||||||
// whose value is mapped from a claim in the upstream token.
|
|
||||||
// By default the value is the same as the downstream subject claim's.
|
|
||||||
DownstreamUsernameClaim = "username"
|
|
||||||
|
|
||||||
// DownstreamGroupsClaim is what we will use to encode the groups in the downstream OIDC ID token
|
|
||||||
// information.
|
|
||||||
DownstreamGroupsClaim = "groups"
|
|
||||||
|
|
||||||
// DownstreamGroupsScope is a custom scope that determines whether the
|
|
||||||
// groups claim will be returned in ID tokens.
|
|
||||||
DownstreamGroupsScope = "groups"
|
|
||||||
|
|
||||||
// RequestAudienceScope is a custom scope that determines whether a RFC8693 token
|
|
||||||
// exchange is allowed to request a different audience.
|
|
||||||
RequestAudienceScope = "pinniped:request-audience"
|
|
||||||
|
|
||||||
// CSRFCookieLifespan is the length of time that the CSRF cookie is valid. After this time, the
|
// CSRFCookieLifespan is the length of time that the CSRF cookie is valid. After this time, the
|
||||||
// Supervisor's authorization endpoint should give the browser a new CSRF cookie. We set it to
|
// Supervisor's authorization endpoint should give the browser a new CSRF cookie. We set it to
|
||||||
// a week so that it is unlikely to expire during a login.
|
// a week so that it is unlikely to expire during a login.
|
||||||
@ -229,7 +207,7 @@ func FositeOauth2Helper(
|
|||||||
EnforcePKCE: true,
|
EnforcePKCE: true,
|
||||||
|
|
||||||
// "offline_access" as per https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess
|
// "offline_access" as per https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess
|
||||||
RefreshTokenScopes: []string{coreosoidc.ScopeOfflineAccess},
|
RefreshTokenScopes: []string{oidcapi.ScopeOfflineAccess},
|
||||||
|
|
||||||
// The default is to support all prompt values from the spec.
|
// The default is to support all prompt values from the spec.
|
||||||
// See https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
|
// See https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
|
||||||
|
@ -7,11 +7,11 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/coreos/go-oidc/v3/oidc"
|
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
v1 "k8s.io/api/core/v1"
|
v1 "k8s.io/api/core/v1"
|
||||||
|
|
||||||
"go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1"
|
"go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1"
|
||||||
|
oidcapi "go.pinniped.dev/generated/latest/apis/supervisor/oidc"
|
||||||
"go.pinniped.dev/internal/oidcclientsecretstorage"
|
"go.pinniped.dev/internal/oidcclientsecretstorage"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -27,16 +27,6 @@ const (
|
|||||||
reasonNoClientSecretFound = "NoClientSecretFound"
|
reasonNoClientSecretFound = "NoClientSecretFound"
|
||||||
reasonInvalidClientSecretFound = "InvalidClientSecretFound"
|
reasonInvalidClientSecretFound = "InvalidClientSecretFound"
|
||||||
|
|
||||||
authorizationCodeGrantTypeName = "authorization_code"
|
|
||||||
refreshTokenGrantTypeName = "refresh_token"
|
|
||||||
tokenExchangeGrantTypeName = "urn:ietf:params:oauth:grant-type:token-exchange" //nolint:gosec // this is not a credential
|
|
||||||
|
|
||||||
openidScopeName = oidc.ScopeOpenID
|
|
||||||
offlineAccessScopeName = oidc.ScopeOfflineAccess
|
|
||||||
requestAudienceScopeName = "pinniped:request-audience"
|
|
||||||
usernameScopeName = "username"
|
|
||||||
groupsScopeName = "groups"
|
|
||||||
|
|
||||||
allowedGrantTypesFieldName = "allowedGrantTypes"
|
allowedGrantTypesFieldName = "allowedGrantTypes"
|
||||||
allowedScopesFieldName = "allowedScopes"
|
allowedScopesFieldName = "allowedScopes"
|
||||||
)
|
)
|
||||||
@ -67,21 +57,21 @@ func Validate(oidcClient *v1alpha1.OIDCClient, secret *v1.Secret, minBcryptCost
|
|||||||
func validateAllowedScopes(oidcClient *v1alpha1.OIDCClient, conditions []*v1alpha1.Condition) []*v1alpha1.Condition {
|
func validateAllowedScopes(oidcClient *v1alpha1.OIDCClient, conditions []*v1alpha1.Condition) []*v1alpha1.Condition {
|
||||||
m := make([]string, 0, 4)
|
m := make([]string, 0, 4)
|
||||||
|
|
||||||
if !allowedScopesContains(oidcClient, openidScopeName) {
|
if !allowedScopesContains(oidcClient, oidcapi.ScopeOpenID) {
|
||||||
m = append(m, fmt.Sprintf("%q must always be included in %q", openidScopeName, allowedScopesFieldName))
|
m = append(m, fmt.Sprintf("%q must always be included in %q", oidcapi.ScopeOpenID, allowedScopesFieldName))
|
||||||
}
|
}
|
||||||
if allowedGrantTypesContains(oidcClient, refreshTokenGrantTypeName) && !allowedScopesContains(oidcClient, offlineAccessScopeName) {
|
if allowedGrantTypesContains(oidcClient, oidcapi.GrantTypeRefreshToken) && !allowedScopesContains(oidcClient, oidcapi.ScopeOfflineAccess) {
|
||||||
m = append(m, fmt.Sprintf("%q must be included in %q when %q is included in %q",
|
m = append(m, fmt.Sprintf("%q must be included in %q when %q is included in %q",
|
||||||
offlineAccessScopeName, allowedScopesFieldName, refreshTokenGrantTypeName, allowedGrantTypesFieldName))
|
oidcapi.ScopeOfflineAccess, allowedScopesFieldName, oidcapi.GrantTypeRefreshToken, allowedGrantTypesFieldName))
|
||||||
}
|
}
|
||||||
if allowedScopesContains(oidcClient, requestAudienceScopeName) &&
|
if allowedScopesContains(oidcClient, oidcapi.ScopeRequestAudience) &&
|
||||||
(!allowedScopesContains(oidcClient, usernameScopeName) || !allowedScopesContains(oidcClient, groupsScopeName)) {
|
(!allowedScopesContains(oidcClient, oidcapi.ScopeUsername) || !allowedScopesContains(oidcClient, oidcapi.ScopeGroups)) {
|
||||||
m = append(m, fmt.Sprintf("%q and %q must be included in %q when %q is included in %q",
|
m = append(m, fmt.Sprintf("%q and %q must be included in %q when %q is included in %q",
|
||||||
usernameScopeName, groupsScopeName, allowedScopesFieldName, requestAudienceScopeName, allowedScopesFieldName))
|
oidcapi.ScopeUsername, oidcapi.ScopeGroups, allowedScopesFieldName, oidcapi.ScopeRequestAudience, allowedScopesFieldName))
|
||||||
}
|
}
|
||||||
if allowedGrantTypesContains(oidcClient, tokenExchangeGrantTypeName) && !allowedScopesContains(oidcClient, requestAudienceScopeName) {
|
if allowedGrantTypesContains(oidcClient, oidcapi.GrantTypeTokenExchange) && !allowedScopesContains(oidcClient, oidcapi.ScopeRequestAudience) {
|
||||||
m = append(m, fmt.Sprintf("%q must be included in %q when %q is included in %q",
|
m = append(m, fmt.Sprintf("%q must be included in %q when %q is included in %q",
|
||||||
requestAudienceScopeName, allowedScopesFieldName, tokenExchangeGrantTypeName, allowedGrantTypesFieldName))
|
oidcapi.ScopeRequestAudience, allowedScopesFieldName, oidcapi.GrantTypeTokenExchange, allowedGrantTypesFieldName))
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(m) == 0 {
|
if len(m) == 0 {
|
||||||
@ -107,17 +97,17 @@ func validateAllowedScopes(oidcClient *v1alpha1.OIDCClient, conditions []*v1alph
|
|||||||
func validateAllowedGrantTypes(oidcClient *v1alpha1.OIDCClient, conditions []*v1alpha1.Condition) []*v1alpha1.Condition {
|
func validateAllowedGrantTypes(oidcClient *v1alpha1.OIDCClient, conditions []*v1alpha1.Condition) []*v1alpha1.Condition {
|
||||||
m := make([]string, 0, 3)
|
m := make([]string, 0, 3)
|
||||||
|
|
||||||
if !allowedGrantTypesContains(oidcClient, authorizationCodeGrantTypeName) {
|
if !allowedGrantTypesContains(oidcClient, oidcapi.GrantTypeAuthorizationCode) {
|
||||||
m = append(m, fmt.Sprintf("%q must always be included in %q",
|
m = append(m, fmt.Sprintf("%q must always be included in %q",
|
||||||
authorizationCodeGrantTypeName, allowedGrantTypesFieldName))
|
oidcapi.GrantTypeAuthorizationCode, allowedGrantTypesFieldName))
|
||||||
}
|
}
|
||||||
if allowedScopesContains(oidcClient, offlineAccessScopeName) && !allowedGrantTypesContains(oidcClient, refreshTokenGrantTypeName) {
|
if allowedScopesContains(oidcClient, oidcapi.ScopeOfflineAccess) && !allowedGrantTypesContains(oidcClient, oidcapi.GrantTypeRefreshToken) {
|
||||||
m = append(m, fmt.Sprintf("%q must be included in %q when %q is included in %q",
|
m = append(m, fmt.Sprintf("%q must be included in %q when %q is included in %q",
|
||||||
refreshTokenGrantTypeName, allowedGrantTypesFieldName, offlineAccessScopeName, allowedScopesFieldName))
|
oidcapi.GrantTypeRefreshToken, allowedGrantTypesFieldName, oidcapi.ScopeOfflineAccess, allowedScopesFieldName))
|
||||||
}
|
}
|
||||||
if allowedScopesContains(oidcClient, requestAudienceScopeName) && !allowedGrantTypesContains(oidcClient, tokenExchangeGrantTypeName) {
|
if allowedScopesContains(oidcClient, oidcapi.ScopeRequestAudience) && !allowedGrantTypesContains(oidcClient, oidcapi.GrantTypeTokenExchange) {
|
||||||
m = append(m, fmt.Sprintf("%q must be included in %q when %q is included in %q",
|
m = append(m, fmt.Sprintf("%q must be included in %q when %q is included in %q",
|
||||||
tokenExchangeGrantTypeName, allowedGrantTypesFieldName, requestAudienceScopeName, allowedScopesFieldName))
|
oidcapi.GrantTypeTokenExchange, allowedGrantTypesFieldName, oidcapi.ScopeRequestAudience, allowedScopesFieldName))
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(m) == 0 {
|
if len(m) == 0 {
|
||||||
|
@ -171,7 +171,7 @@ func TestManager(t *testing.T) {
|
|||||||
r.NoError(err)
|
r.NoError(err)
|
||||||
actualLocationQueryParams := parsedLocation.Query()
|
actualLocationQueryParams := parsedLocation.Query()
|
||||||
r.Contains(actualLocationQueryParams, "code")
|
r.Contains(actualLocationQueryParams, "code")
|
||||||
r.Equal("openid", actualLocationQueryParams.Get("scope"))
|
r.Equal("openid username groups", actualLocationQueryParams.Get("scope"))
|
||||||
r.Equal("some-state-value-with-enough-bytes-to-exceed-min-allowed", actualLocationQueryParams.Get("state"))
|
r.Equal("some-state-value-with-enough-bytes-to-exceed-min-allowed", actualLocationQueryParams.Get("state"))
|
||||||
|
|
||||||
// Make sure that we wired up the callback endpoint to use kube storage for fosite sessions.
|
// Make sure that we wired up the callback endpoint to use kube storage for fosite sessions.
|
||||||
@ -343,7 +343,7 @@ func TestManager(t *testing.T) {
|
|||||||
|
|
||||||
authRequestParams := "?" + url.Values{
|
authRequestParams := "?" + url.Values{
|
||||||
"response_type": []string{"code"},
|
"response_type": []string{"code"},
|
||||||
"scope": []string{"openid profile email"},
|
"scope": []string{"openid profile email username groups"},
|
||||||
"client_id": []string{downstreamClientID},
|
"client_id": []string{downstreamClientID},
|
||||||
"state": []string{"some-state-value-with-enough-bytes-to-exceed-min-allowed"},
|
"state": []string{"some-state-value-with-enough-bytes-to-exceed-min-allowed"},
|
||||||
"nonce": []string{"some-nonce-value-with-enough-bytes-to-exceed-min-allowed"},
|
"nonce": []string{"some-nonce-value-with-enough-bytes-to-exceed-min-allowed"},
|
||||||
|
@ -17,6 +17,7 @@ import (
|
|||||||
"k8s.io/apiserver/pkg/warning"
|
"k8s.io/apiserver/pkg/warning"
|
||||||
"k8s.io/utils/strings/slices"
|
"k8s.io/utils/strings/slices"
|
||||||
|
|
||||||
|
oidcapi "go.pinniped.dev/generated/latest/apis/supervisor/oidc"
|
||||||
"go.pinniped.dev/internal/httputil/httperr"
|
"go.pinniped.dev/internal/httputil/httperr"
|
||||||
"go.pinniped.dev/internal/oidc"
|
"go.pinniped.dev/internal/oidc"
|
||||||
"go.pinniped.dev/internal/oidc/downstreamsession"
|
"go.pinniped.dev/internal/oidc/downstreamsession"
|
||||||
@ -39,7 +40,7 @@ func NewHandler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if we are performing a refresh grant.
|
// Check if we are performing a refresh grant.
|
||||||
if accessRequest.GetGrantTypes().ExactOne("refresh_token") {
|
if accessRequest.GetGrantTypes().ExactOne(oidcapi.GrantTypeRefreshToken) {
|
||||||
// The above call to NewAccessRequest has loaded the session from storage into the accessRequest variable.
|
// The above call to NewAccessRequest has loaded the session from storage into the accessRequest variable.
|
||||||
// The session, requested scopes, and requested audience from the original authorize request was retrieved
|
// The session, requested scopes, and requested audience from the original authorize request was retrieved
|
||||||
// from the Kube storage layer and added to the accessRequest. Additionally, the audience and scopes may
|
// from the Kube storage layer and added to the accessRequest. Additionally, the audience and scopes may
|
||||||
@ -54,7 +55,7 @@ func NewHandler(
|
|||||||
|
|
||||||
// When we are in the authorization code flow, check if we have any warnings that previous handlers want us
|
// When we are in the authorization code flow, check if we have any warnings that previous handlers want us
|
||||||
// to send to the client to be printed on the CLI.
|
// to send to the client to be printed on the CLI.
|
||||||
if accessRequest.GetGrantTypes().ExactOne("authorization_code") {
|
if accessRequest.GetGrantTypes().ExactOne(oidcapi.GrantTypeAuthorizationCode) {
|
||||||
storedSession := accessRequest.GetSession().(*psession.PinnipedSession)
|
storedSession := accessRequest.GetSession().(*psession.PinnipedSession)
|
||||||
customSessionData := storedSession.Custom
|
customSessionData := storedSession.Custom
|
||||||
if customSessionData != nil {
|
if customSessionData != nil {
|
||||||
@ -108,20 +109,27 @@ func upstreamRefresh(ctx context.Context, accessRequest fosite.AccessRequester,
|
|||||||
}
|
}
|
||||||
|
|
||||||
grantedScopes := accessRequest.GetGrantedScopes()
|
grantedScopes := accessRequest.GetGrantedScopes()
|
||||||
|
clientID := accessRequest.GetClient().GetID()
|
||||||
|
|
||||||
switch customSessionData.ProviderType {
|
switch customSessionData.ProviderType {
|
||||||
case psession.ProviderTypeOIDC:
|
case psession.ProviderTypeOIDC:
|
||||||
return upstreamOIDCRefresh(ctx, session, providerCache, grantedScopes)
|
return upstreamOIDCRefresh(ctx, session, providerCache, grantedScopes, clientID)
|
||||||
case psession.ProviderTypeLDAP:
|
case psession.ProviderTypeLDAP:
|
||||||
return upstreamLDAPRefresh(ctx, providerCache, session, grantedScopes)
|
return upstreamLDAPRefresh(ctx, providerCache, session, grantedScopes, clientID)
|
||||||
case psession.ProviderTypeActiveDirectory:
|
case psession.ProviderTypeActiveDirectory:
|
||||||
return upstreamLDAPRefresh(ctx, providerCache, session, grantedScopes)
|
return upstreamLDAPRefresh(ctx, providerCache, session, grantedScopes, clientID)
|
||||||
default:
|
default:
|
||||||
return errorsx.WithStack(errMissingUpstreamSessionInternalError())
|
return errorsx.WithStack(errMissingUpstreamSessionInternalError())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func upstreamOIDCRefresh(ctx context.Context, session *psession.PinnipedSession, providerCache oidc.UpstreamIdentityProvidersLister, grantedScopes []string) error {
|
func upstreamOIDCRefresh(
|
||||||
|
ctx context.Context,
|
||||||
|
session *psession.PinnipedSession,
|
||||||
|
providerCache oidc.UpstreamIdentityProvidersLister,
|
||||||
|
grantedScopes []string,
|
||||||
|
clientID string,
|
||||||
|
) error {
|
||||||
s := session.Custom
|
s := session.Custom
|
||||||
if s.OIDC == nil {
|
if s.OIDC == nil {
|
||||||
return errorsx.WithStack(errMissingUpstreamSessionInternalError())
|
return errorsx.WithStack(errMissingUpstreamSessionInternalError())
|
||||||
@ -180,7 +188,7 @@ func upstreamOIDCRefresh(ctx context.Context, session *psession.PinnipedSession,
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
groupsScope := slices.Contains(grantedScopes, oidc.DownstreamGroupsScope)
|
groupsScope := slices.Contains(grantedScopes, oidcapi.ScopeGroups)
|
||||||
if groupsScope { //nolint:nestif
|
if groupsScope { //nolint:nestif
|
||||||
// If possible, update the user's group memberships. The configured groups claim name (if there is one) may or
|
// If possible, update the user's group memberships. The configured groups claim name (if there is one) may or
|
||||||
// may not be included in the newly fetched and merged claims. It could be missing due to a misconfiguration of the
|
// may not be included in the newly fetched and merged claims. It could be missing due to a misconfiguration of the
|
||||||
@ -204,8 +212,8 @@ func upstreamOIDCRefresh(ctx context.Context, session *psession.PinnipedSession,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
warnIfGroupsChanged(ctx, oldGroups, refreshedGroups, username)
|
warnIfGroupsChanged(ctx, oldGroups, refreshedGroups, username, clientID)
|
||||||
session.Fosite.Claims.Extra[oidc.DownstreamGroupsClaim] = refreshedGroups
|
session.Fosite.Claims.Extra[oidcapi.IDTokenClaimGroups] = refreshedGroups
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -240,7 +248,7 @@ func validateIdentityUnchangedSinceInitialLogin(mergedClaims map[string]interfac
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
newSub, hasSub := getString(mergedClaims, oidc.IDTokenSubjectClaim)
|
newSub, hasSub := getString(mergedClaims, oidcapi.IDTokenClaimSubject)
|
||||||
if !hasSub {
|
if !hasSub {
|
||||||
return errUpstreamRefreshError().WithHintf(
|
return errUpstreamRefreshError().WithHintf(
|
||||||
"Upstream refresh failed.").WithTrace(errors.New("subject in upstream refresh not found")).
|
"Upstream refresh failed.").WithTrace(errors.New("subject in upstream refresh not found")).
|
||||||
@ -253,7 +261,10 @@ func validateIdentityUnchangedSinceInitialLogin(mergedClaims map[string]interfac
|
|||||||
}
|
}
|
||||||
|
|
||||||
newUsername, hasUsername := getString(mergedClaims, usernameClaimName)
|
newUsername, hasUsername := getString(mergedClaims, usernameClaimName)
|
||||||
oldUsername := session.Fosite.Claims.Extra[oidc.DownstreamUsernameClaim]
|
oldUsername, err := getDownstreamUsernameFromPinnipedSession(session)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
// It's possible that a username wasn't returned by the upstream provider during refresh,
|
// It's possible that a username wasn't returned by the upstream provider during refresh,
|
||||||
// but if it is, verify that it hasn't changed.
|
// but if it is, verify that it hasn't changed.
|
||||||
if hasUsername && oldUsername != newUsername {
|
if hasUsername && oldUsername != newUsername {
|
||||||
@ -262,7 +273,7 @@ func validateIdentityUnchangedSinceInitialLogin(mergedClaims map[string]interfac
|
|||||||
WithDebugf("provider name: %q, provider type: %q", s.ProviderName, s.ProviderType)
|
WithDebugf("provider name: %q, provider type: %q", s.ProviderName, s.ProviderType)
|
||||||
}
|
}
|
||||||
|
|
||||||
newIssuer, hasIssuer := getString(mergedClaims, oidc.IDTokenIssuerClaim)
|
newIssuer, hasIssuer := getString(mergedClaims, oidcapi.IDTokenClaimIssuer)
|
||||||
// It's possible that an issuer wasn't returned by the upstream provider during refresh,
|
// It's possible that an issuer wasn't returned by the upstream provider during refresh,
|
||||||
// but if it is, verify that it hasn't changed.
|
// but if it is, verify that it hasn't changed.
|
||||||
if hasIssuer && s.OIDC.UpstreamIssuer != newIssuer {
|
if hasIssuer && s.OIDC.UpstreamIssuer != newIssuer {
|
||||||
@ -297,14 +308,20 @@ func findOIDCProviderByNameAndValidateUID(
|
|||||||
WithDebugf("provider name: %q, provider type: %q", s.ProviderName, s.ProviderType))
|
WithDebugf("provider name: %q, provider type: %q", s.ProviderName, s.ProviderType))
|
||||||
}
|
}
|
||||||
|
|
||||||
func upstreamLDAPRefresh(ctx context.Context, providerCache oidc.UpstreamIdentityProvidersLister, session *psession.PinnipedSession, grantedScopes []string) error {
|
func upstreamLDAPRefresh(
|
||||||
|
ctx context.Context,
|
||||||
|
providerCache oidc.UpstreamIdentityProvidersLister,
|
||||||
|
session *psession.PinnipedSession,
|
||||||
|
grantedScopes []string,
|
||||||
|
clientID string,
|
||||||
|
) error {
|
||||||
username, err := getDownstreamUsernameFromPinnipedSession(session)
|
username, err := getDownstreamUsernameFromPinnipedSession(session)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
subject := session.Fosite.Claims.Subject
|
subject := session.Fosite.Claims.Subject
|
||||||
var oldGroups []string
|
var oldGroups []string
|
||||||
if slices.Contains(grantedScopes, oidc.DownstreamGroupsScope) {
|
if slices.Contains(grantedScopes, oidcapi.ScopeGroups) {
|
||||||
oldGroups, err = getDownstreamGroupsFromPinnipedSession(session)
|
oldGroups, err = getDownstreamGroupsFromPinnipedSession(session)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -349,12 +366,11 @@ func upstreamLDAPRefresh(ctx context.Context, providerCache oidc.UpstreamIdentit
|
|||||||
"Upstream refresh failed.").WithTrace(err).
|
"Upstream refresh failed.").WithTrace(err).
|
||||||
WithDebugf("provider name: %q, provider type: %q", s.ProviderName, s.ProviderType)
|
WithDebugf("provider name: %q, provider type: %q", s.ProviderName, s.ProviderType)
|
||||||
}
|
}
|
||||||
groupsScope := slices.Contains(grantedScopes, oidc.DownstreamGroupsScope)
|
groupsScope := slices.Contains(grantedScopes, oidcapi.ScopeGroups)
|
||||||
if groupsScope {
|
if groupsScope {
|
||||||
|
warnIfGroupsChanged(ctx, oldGroups, groups, username, clientID)
|
||||||
// Replace the old value with the new value.
|
// Replace the old value with the new value.
|
||||||
session.Fosite.Claims.Extra[oidc.DownstreamGroupsClaim] = groups
|
session.Fosite.Claims.Extra[oidcapi.IDTokenClaimGroups] = groups
|
||||||
|
|
||||||
warnIfGroupsChanged(ctx, oldGroups, groups, username)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -391,16 +407,8 @@ func findLDAPProviderByNameAndValidateUID(
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getDownstreamUsernameFromPinnipedSession(session *psession.PinnipedSession) (string, error) {
|
func getDownstreamUsernameFromPinnipedSession(session *psession.PinnipedSession) (string, error) {
|
||||||
extra := session.Fosite.Claims.Extra
|
downstreamUsername := session.Custom.Username
|
||||||
if extra == nil {
|
if len(downstreamUsername) == 0 {
|
||||||
return "", errorsx.WithStack(errMissingUpstreamSessionInternalError())
|
|
||||||
}
|
|
||||||
downstreamUsernameInterface := extra[oidc.DownstreamUsernameClaim]
|
|
||||||
if downstreamUsernameInterface == nil {
|
|
||||||
return "", errorsx.WithStack(errMissingUpstreamSessionInternalError())
|
|
||||||
}
|
|
||||||
downstreamUsername, ok := downstreamUsernameInterface.(string)
|
|
||||||
if !ok || len(downstreamUsername) == 0 {
|
|
||||||
return "", errorsx.WithStack(errMissingUpstreamSessionInternalError())
|
return "", errorsx.WithStack(errMissingUpstreamSessionInternalError())
|
||||||
}
|
}
|
||||||
return downstreamUsername, nil
|
return downstreamUsername, nil
|
||||||
@ -411,7 +419,7 @@ func getDownstreamGroupsFromPinnipedSession(session *psession.PinnipedSession) (
|
|||||||
if extra == nil {
|
if extra == nil {
|
||||||
return nil, errorsx.WithStack(errMissingUpstreamSessionInternalError())
|
return nil, errorsx.WithStack(errMissingUpstreamSessionInternalError())
|
||||||
}
|
}
|
||||||
downstreamGroupsInterface := extra[oidc.DownstreamGroupsClaim]
|
downstreamGroupsInterface := extra[oidcapi.IDTokenClaimGroups]
|
||||||
if downstreamGroupsInterface == nil {
|
if downstreamGroupsInterface == nil {
|
||||||
return nil, errorsx.WithStack(errMissingUpstreamSessionInternalError())
|
return nil, errorsx.WithStack(errMissingUpstreamSessionInternalError())
|
||||||
}
|
}
|
||||||
@ -431,8 +439,17 @@ func getDownstreamGroupsFromPinnipedSession(session *psession.PinnipedSession) (
|
|||||||
return downstreamGroups, nil
|
return downstreamGroups, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func warnIfGroupsChanged(ctx context.Context, oldGroups, newGroups []string, username string) {
|
func warnIfGroupsChanged(ctx context.Context, oldGroups, newGroups []string, username string, clientID string) {
|
||||||
|
if clientID != oidcapi.ClientIDPinnipedCLI {
|
||||||
|
// Only send these warnings to the CLI client. They are intended for kubectl to print to the screen.
|
||||||
|
// A webapp using a dynamic client wouldn't know to look for these special warning headers, and
|
||||||
|
// if the dynamic client lacked the username scope, then these warning messages would be leaking
|
||||||
|
// the user's username to the client within the text of the warning.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
added, removed := diffSortedGroups(oldGroups, newGroups)
|
added, removed := diffSortedGroups(oldGroups, newGroups)
|
||||||
|
|
||||||
if len(added) > 0 {
|
if len(added) > 0 {
|
||||||
warning.AddWarning(ctx, "", fmt.Sprintf("User %q has been added to the following groups: %q", username, added))
|
warning.AddWarning(ctx, "", fmt.Sprintf("User %q has been added to the following groups: %q", username, added))
|
||||||
}
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -8,21 +8,19 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/coreos/go-oidc/v3/oidc"
|
|
||||||
"github.com/ory/fosite"
|
"github.com/ory/fosite"
|
||||||
"github.com/ory/fosite/compose"
|
"github.com/ory/fosite/compose"
|
||||||
"github.com/ory/fosite/handler/oauth2"
|
"github.com/ory/fosite/handler/oauth2"
|
||||||
"github.com/ory/fosite/handler/openid"
|
"github.com/ory/fosite/handler/openid"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
"go.pinniped.dev/internal/oidc/clientregistry"
|
oidcapi "go.pinniped.dev/generated/latest/apis/supervisor/oidc"
|
||||||
|
"go.pinniped.dev/internal/psession"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
tokenExchangeGrantType = "urn:ietf:params:oauth:grant-type:token-exchange" //nolint: gosec
|
|
||||||
tokenTypeAccessToken = "urn:ietf:params:oauth:token-type:access_token" //nolint: gosec
|
tokenTypeAccessToken = "urn:ietf:params:oauth:token-type:access_token" //nolint: gosec
|
||||||
tokenTypeJWT = "urn:ietf:params:oauth:token-type:jwt" //nolint: gosec
|
tokenTypeJWT = "urn:ietf:params:oauth:token-type:jwt" //nolint: gosec
|
||||||
pinnipedTokenExchangeScope = "pinniped:request-audience" //nolint: gosec
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type stsParams struct {
|
type stsParams struct {
|
||||||
@ -78,17 +76,22 @@ func (t *TokenExchangeHandler) PopulateTokenEndpointResponse(ctx context.Context
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check that the client is allowed to perform this grant type.
|
// Check that the client is allowed to perform this grant type.
|
||||||
if !requester.GetClient().GetGrantTypes().Has(tokenExchangeGrantType) {
|
if !requester.GetClient().GetGrantTypes().Has(oidcapi.GrantTypeTokenExchange) {
|
||||||
// This error message is trying to be similar to the analogous one in fosite's flow_authorize_code_token.go.
|
// This error message is trying to be similar to the analogous one in fosite's flow_authorize_code_token.go.
|
||||||
return errors.WithStack(fosite.ErrUnauthorizedClient.WithHintf(`The OAuth 2.0 Client is not allowed to use token exchange grant "%s".`, tokenExchangeGrantType))
|
return errors.WithStack(fosite.ErrUnauthorizedClient.WithHintf(`The OAuth 2.0 Client is not allowed to use token exchange grant "%s".`, oidcapi.GrantTypeTokenExchange))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Require that the incoming access token has the pinniped:request-audience and OpenID scopes.
|
// Require that the incoming access token has the pinniped:request-audience and OpenID scopes.
|
||||||
if !originalRequester.GetGrantedScopes().Has(pinnipedTokenExchangeScope) {
|
if !originalRequester.GetGrantedScopes().Has(oidcapi.ScopeRequestAudience) {
|
||||||
return errors.WithStack(fosite.ErrAccessDenied.WithHintf("missing the %q scope", pinnipedTokenExchangeScope))
|
return errors.WithStack(fosite.ErrAccessDenied.WithHintf("missing the %q scope", oidcapi.ScopeRequestAudience))
|
||||||
}
|
}
|
||||||
if !originalRequester.GetGrantedScopes().Has(oidc.ScopeOpenID) {
|
if !originalRequester.GetGrantedScopes().Has(oidcapi.ScopeOpenID) {
|
||||||
return errors.WithStack(fosite.ErrAccessDenied.WithHintf("missing the %q scope", oidc.ScopeOpenID))
|
return errors.WithStack(fosite.ErrAccessDenied.WithHintf("missing the %q scope", oidcapi.ScopeOpenID))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that the stored session meets the minimum requirements for token exchange.
|
||||||
|
if err := t.validateSession(originalRequester); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the original authorize request information, along with the requested audience, to mint a new JWT.
|
// Use the original authorize request information, along with the requested audience, to mint a new JWT.
|
||||||
@ -110,6 +113,22 @@ func (t *TokenExchangeHandler) mintJWT(ctx context.Context, requester fosite.Req
|
|||||||
return t.idTokenStrategy.GenerateIDToken(ctx, downscoped)
|
return t.idTokenStrategy.GenerateIDToken(ctx, downscoped)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *TokenExchangeHandler) validateSession(requester fosite.Requester) error {
|
||||||
|
pSession, ok := requester.GetSession().(*psession.PinnipedSession)
|
||||||
|
if !ok {
|
||||||
|
// This shouldn't really happen.
|
||||||
|
return fosite.ErrServerError.WithHint("Invalid session storage.")
|
||||||
|
}
|
||||||
|
username, ok := pSession.IDTokenClaims().Extra[oidcapi.IDTokenClaimUsername].(string)
|
||||||
|
if !ok || username == "" {
|
||||||
|
// No username was stored in the session's ID token claims (or the stored username was not a string, which
|
||||||
|
// shouldn't really happen). Usernames will not be stored in the session's ID token claims when the username
|
||||||
|
// scope was not requested/granted, but otherwise they should be stored.
|
||||||
|
return fosite.ErrAccessDenied.WithHintf("No username found in session. Ensure that the %q scope was requested and granted at the authorization endpoint.", oidcapi.ScopeUsername)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (t *TokenExchangeHandler) validateParams(params url.Values) (*stsParams, error) {
|
func (t *TokenExchangeHandler) validateParams(params url.Values) (*stsParams, error) {
|
||||||
var result stsParams
|
var result stsParams
|
||||||
|
|
||||||
@ -157,8 +176,8 @@ func (t *TokenExchangeHandler) validateParams(params url.Values) (*stsParams, er
|
|||||||
if strings.Contains(result.requestedAudience, ".pinniped.dev") {
|
if strings.Contains(result.requestedAudience, ".pinniped.dev") {
|
||||||
return nil, fosite.ErrInvalidRequest.WithHintf("requested audience cannot contain '.pinniped.dev'")
|
return nil, fosite.ErrInvalidRequest.WithHintf("requested audience cannot contain '.pinniped.dev'")
|
||||||
}
|
}
|
||||||
if result.requestedAudience == clientregistry.PinnipedCLIClientID {
|
if result.requestedAudience == oidcapi.ClientIDPinnipedCLI {
|
||||||
return nil, fosite.ErrInvalidRequest.WithHintf("requested audience cannot equal '%s'", clientregistry.PinnipedCLIClientID)
|
return nil, fosite.ErrInvalidRequest.WithHintf("requested audience cannot equal '%s'", oidcapi.ClientIDPinnipedCLI)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &result, nil
|
return &result, nil
|
||||||
@ -181,5 +200,5 @@ func (t *TokenExchangeHandler) CanSkipClientAuth(_ fosite.AccessRequester) bool
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *TokenExchangeHandler) CanHandleTokenEndpointRequest(requester fosite.AccessRequester) bool {
|
func (t *TokenExchangeHandler) CanHandleTokenEndpointRequest(requester fosite.AccessRequester) bool {
|
||||||
return requester.GetGrantTypes().ExactOne(tokenExchangeGrantType)
|
return requester.GetGrantTypes().ExactOne(oidcapi.GrantTypeTokenExchange)
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,11 @@ var _ openid.Session = &PinnipedSession{}
|
|||||||
// CustomSessionData is the custom session data needed by Pinniped. It should be treated as a union type,
|
// CustomSessionData is the custom session data needed by Pinniped. It should be treated as a union type,
|
||||||
// where the value of ProviderType decides which other fields to use.
|
// where the value of ProviderType decides which other fields to use.
|
||||||
type CustomSessionData struct {
|
type CustomSessionData struct {
|
||||||
|
// Username will contain the downstream username determined during initial authorization. We store this
|
||||||
|
// so that we can validate that it does not change upon refresh. This should normally never be empty, since
|
||||||
|
// all users must have a username.
|
||||||
|
Username string `json:"username"`
|
||||||
|
|
||||||
// The Kubernetes resource UID of the identity provider CRD for the upstream IDP used to start this session.
|
// The Kubernetes resource UID of the identity provider CRD for the upstream IDP used to start this session.
|
||||||
// This should be validated again upon downstream refresh to make sure that we are not refreshing against
|
// This should be validated again upon downstream refresh to make sure that we are not refreshing against
|
||||||
// a different identity provider CRD which just happens to have the same name.
|
// a different identity provider CRD which just happens to have the same name.
|
||||||
|
@ -7,6 +7,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/types"
|
"k8s.io/apimachinery/pkg/types"
|
||||||
@ -41,19 +43,30 @@ func allDynamicClientScopes() []configv1alpha1.Scope {
|
|||||||
return scopes
|
return scopes
|
||||||
}
|
}
|
||||||
|
|
||||||
// fullyCapableOIDCClient returns an OIDC client which is allowed to use all grant types and all scopes that
|
func newOIDCClient(
|
||||||
// are supported by the Supervisor for dynamic clients.
|
namespace string,
|
||||||
func fullyCapableOIDCClient(namespace string, clientID string, clientUID string, redirectURI string) *configv1alpha1.OIDCClient {
|
clientID string,
|
||||||
|
clientUID string,
|
||||||
|
redirectURI string,
|
||||||
|
allowedGrantTypes []configv1alpha1.GrantType,
|
||||||
|
allowedScopes []configv1alpha1.Scope,
|
||||||
|
) *configv1alpha1.OIDCClient {
|
||||||
return &configv1alpha1.OIDCClient{
|
return &configv1alpha1.OIDCClient{
|
||||||
ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: clientID, Generation: 1, UID: types.UID(clientUID)},
|
ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: clientID, Generation: 1, UID: types.UID(clientUID)},
|
||||||
Spec: configv1alpha1.OIDCClientSpec{
|
Spec: configv1alpha1.OIDCClientSpec{
|
||||||
AllowedGrantTypes: []configv1alpha1.GrantType{"authorization_code", "urn:ietf:params:oauth:grant-type:token-exchange", "refresh_token"},
|
AllowedGrantTypes: allowedGrantTypes,
|
||||||
AllowedScopes: allDynamicClientScopes(),
|
AllowedScopes: allowedScopes,
|
||||||
AllowedRedirectURIs: []configv1alpha1.RedirectURI{configv1alpha1.RedirectURI(redirectURI)},
|
AllowedRedirectURIs: []configv1alpha1.RedirectURI{configv1alpha1.RedirectURI(redirectURI)},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OIDCClientValidatorFunc is an interface-like type that allows these test helpers to avoid having a direct dependency
|
||||||
|
// on the production code, to avoid circular module dependencies. Implemented by oidcclientvalidator.Validate.
|
||||||
|
type OIDCClientValidatorFunc func(oidcClient *configv1alpha1.OIDCClient, secret *corev1.Secret, minBcryptCost int) (bool, []*configv1alpha1.Condition, []string)
|
||||||
|
|
||||||
|
// FullyCapableOIDCClientAndStorageSecret returns an OIDC client which is allowed to use all grant types and all scopes
|
||||||
|
// that are supported by the Supervisor for dynamic clients, along with a corresponding client secret storage Secret.
|
||||||
func FullyCapableOIDCClientAndStorageSecret(
|
func FullyCapableOIDCClientAndStorageSecret(
|
||||||
t *testing.T,
|
t *testing.T,
|
||||||
namespace string,
|
namespace string,
|
||||||
@ -61,7 +74,38 @@ func FullyCapableOIDCClientAndStorageSecret(
|
|||||||
clientUID string,
|
clientUID string,
|
||||||
redirectURI string,
|
redirectURI string,
|
||||||
hashes []string,
|
hashes []string,
|
||||||
|
validateFunc OIDCClientValidatorFunc,
|
||||||
) (*configv1alpha1.OIDCClient, *corev1.Secret) {
|
) (*configv1alpha1.OIDCClient, *corev1.Secret) {
|
||||||
return fullyCapableOIDCClient(namespace, clientID, clientUID, redirectURI),
|
allScopes := allDynamicClientScopes()
|
||||||
OIDCClientSecretStorageSecretForUID(t, namespace, clientUID, hashes)
|
|
||||||
|
allGrantTypes := []configv1alpha1.GrantType{
|
||||||
|
"authorization_code", "urn:ietf:params:oauth:grant-type:token-exchange", "refresh_token",
|
||||||
|
}
|
||||||
|
|
||||||
|
return OIDCClientAndStorageSecret(t, namespace, clientID, clientUID, allGrantTypes, allScopes, redirectURI, hashes, validateFunc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// OIDCClientAndStorageSecret returns an OIDC client which is allowed to use the specified grant types and scopes,
|
||||||
|
// along with a corresponding client secret storage Secret. It also validates the client to make sure that the specified
|
||||||
|
// combination of grant types and scopes is considered valid before returning the client.
|
||||||
|
func OIDCClientAndStorageSecret(
|
||||||
|
t *testing.T,
|
||||||
|
namespace string,
|
||||||
|
clientID string,
|
||||||
|
clientUID string,
|
||||||
|
allowedGrantTypes []configv1alpha1.GrantType,
|
||||||
|
allowedScopes []configv1alpha1.Scope,
|
||||||
|
redirectURI string,
|
||||||
|
hashes []string,
|
||||||
|
validateFunc OIDCClientValidatorFunc,
|
||||||
|
) (*configv1alpha1.OIDCClient, *corev1.Secret) {
|
||||||
|
oidcClient := newOIDCClient(namespace, clientID, clientUID, redirectURI, allowedGrantTypes, allowedScopes)
|
||||||
|
secret := OIDCClientSecretStorageSecretForUID(t, namespace, clientUID, hashes)
|
||||||
|
|
||||||
|
// If a test made an invalid OIDCClient then inform the author of the test, so they can fix the test case.
|
||||||
|
// This is an easy mistake to make when writing tests because there are lots of validations on OIDCClients.
|
||||||
|
valid, conditions, _ := validateFunc(oidcClient, secret, bcrypt.MinCost)
|
||||||
|
require.True(t, valid, "Test's OIDCClient should have been valid. See conditions for errors: %s", conditions)
|
||||||
|
|
||||||
|
return oidcClient, secret
|
||||||
}
|
}
|
||||||
|
@ -1064,14 +1064,22 @@ func validateAuthcodeStorage(
|
|||||||
|
|
||||||
// Check the user's identity, which are put into the downstream ID token's subject, username and groups claims.
|
// Check the user's identity, which are put into the downstream ID token's subject, username and groups claims.
|
||||||
require.Equal(t, wantDownstreamIDTokenSubject, actualClaims.Subject)
|
require.Equal(t, wantDownstreamIDTokenSubject, actualClaims.Subject)
|
||||||
|
wantDownstreamIDTokenUsernameClaimToExist := 1
|
||||||
|
if wantDownstreamIDTokenUsername == "" {
|
||||||
|
wantDownstreamIDTokenUsernameClaimToExist = 0
|
||||||
|
require.NotContains(t, actualClaims.Extra, "username")
|
||||||
|
} else {
|
||||||
require.Equal(t, wantDownstreamIDTokenUsername, actualClaims.Extra["username"])
|
require.Equal(t, wantDownstreamIDTokenUsername, actualClaims.Extra["username"])
|
||||||
|
}
|
||||||
if slices.Contains(wantDownstreamGrantedScopes, "groups") {
|
if slices.Contains(wantDownstreamGrantedScopes, "groups") {
|
||||||
require.Len(t, actualClaims.Extra, 2)
|
require.Len(t, actualClaims.Extra, wantDownstreamIDTokenUsernameClaimToExist+1)
|
||||||
actualDownstreamIDTokenGroups := actualClaims.Extra["groups"]
|
actualDownstreamIDTokenGroups := actualClaims.Extra["groups"]
|
||||||
require.NotNil(t, actualDownstreamIDTokenGroups)
|
require.NotNil(t, actualDownstreamIDTokenGroups)
|
||||||
require.ElementsMatch(t, wantDownstreamIDTokenGroups, actualDownstreamIDTokenGroups)
|
require.ElementsMatch(t, wantDownstreamIDTokenGroups, actualDownstreamIDTokenGroups)
|
||||||
} else {
|
} else {
|
||||||
require.Len(t, actualClaims.Extra, 1)
|
require.Emptyf(t, wantDownstreamIDTokenGroups, "test case did not want the groups scope to be granted, "+
|
||||||
|
"but wanted something in the groups claim, which doesn't make sense. please review the test case's expectations.")
|
||||||
|
require.Len(t, actualClaims.Extra, wantDownstreamIDTokenUsernameClaimToExist)
|
||||||
actualDownstreamIDTokenGroups := actualClaims.Extra["groups"]
|
actualDownstreamIDTokenGroups := actualClaims.Extra["groups"]
|
||||||
require.Nil(t, actualDownstreamIDTokenGroups)
|
require.Nil(t, actualDownstreamIDTokenGroups)
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,7 @@ func NewFakePinnipedSession() *psession.PinnipedSession {
|
|||||||
Subject: "panda",
|
Subject: "panda",
|
||||||
},
|
},
|
||||||
Custom: &psession.CustomSessionData{
|
Custom: &psession.CustomSessionData{
|
||||||
|
Username: "fake-username",
|
||||||
ProviderUID: "fake-provider-uid",
|
ProviderUID: "fake-provider-uid",
|
||||||
ProviderType: "fake-provider-type",
|
ProviderType: "fake-provider-type",
|
||||||
ProviderName: "fake-provider-name",
|
ProviderName: "fake-provider-name",
|
||||||
|
@ -23,10 +23,10 @@ import (
|
|||||||
"k8s.io/utils/strings/slices"
|
"k8s.io/utils/strings/slices"
|
||||||
"k8s.io/utils/trace"
|
"k8s.io/utils/trace"
|
||||||
|
|
||||||
|
oidcapi "go.pinniped.dev/generated/latest/apis/supervisor/oidc"
|
||||||
"go.pinniped.dev/internal/authenticators"
|
"go.pinniped.dev/internal/authenticators"
|
||||||
"go.pinniped.dev/internal/crypto/ptls"
|
"go.pinniped.dev/internal/crypto/ptls"
|
||||||
"go.pinniped.dev/internal/endpointaddr"
|
"go.pinniped.dev/internal/endpointaddr"
|
||||||
"go.pinniped.dev/internal/oidc"
|
|
||||||
"go.pinniped.dev/internal/oidc/downstreamsession"
|
"go.pinniped.dev/internal/oidc/downstreamsession"
|
||||||
"go.pinniped.dev/internal/oidc/provider"
|
"go.pinniped.dev/internal/oidc/provider"
|
||||||
"go.pinniped.dev/internal/plog"
|
"go.pinniped.dev/internal/plog"
|
||||||
@ -241,7 +241,7 @@ func (p *Provider) PerformRefresh(ctx context.Context, storedRefreshAttributes p
|
|||||||
return storedRefreshAttributes.Groups, nil
|
return storedRefreshAttributes.Groups, nil
|
||||||
}
|
}
|
||||||
// if we were not granted the groups scope, we should not search for groups or return any.
|
// if we were not granted the groups scope, we should not search for groups or return any.
|
||||||
if !slices.Contains(storedRefreshAttributes.GrantedScopes, oidc.DownstreamGroupsScope) {
|
if !slices.Contains(storedRefreshAttributes.GrantedScopes, oidcapi.ScopeGroups) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -593,7 +593,7 @@ func (p *Provider) searchAndBindUser(conn Conn, username string, grantedScopes [
|
|||||||
}
|
}
|
||||||
|
|
||||||
var mappedGroupNames []string
|
var mappedGroupNames []string
|
||||||
if slices.Contains(grantedScopes, oidc.DownstreamGroupsScope) {
|
if slices.Contains(grantedScopes, oidcapi.ScopeGroups) {
|
||||||
mappedGroupNames, err = p.searchGroupsForUserDN(conn, userEntry.DN)
|
mappedGroupNames, err = p.searchGroupsForUserDN(conn, userEntry.DN)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -20,8 +20,8 @@ import (
|
|||||||
"k8s.io/apimachinery/pkg/types"
|
"k8s.io/apimachinery/pkg/types"
|
||||||
"k8s.io/apimachinery/pkg/util/sets"
|
"k8s.io/apimachinery/pkg/util/sets"
|
||||||
|
|
||||||
|
oidcapi "go.pinniped.dev/generated/latest/apis/supervisor/oidc"
|
||||||
"go.pinniped.dev/internal/httputil/httperr"
|
"go.pinniped.dev/internal/httputil/httperr"
|
||||||
"go.pinniped.dev/internal/oidc"
|
|
||||||
"go.pinniped.dev/internal/oidc/provider"
|
"go.pinniped.dev/internal/oidc/provider"
|
||||||
"go.pinniped.dev/internal/plog"
|
"go.pinniped.dev/internal/plog"
|
||||||
"go.pinniped.dev/pkg/oidcclient/nonce"
|
"go.pinniped.dev/pkg/oidcclient/nonce"
|
||||||
@ -286,7 +286,7 @@ func (p *ProviderConfig) ValidateTokenAndMergeWithUserInfo(ctx context.Context,
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
idTokenSubject, _ := validatedClaims[oidc.IDTokenSubjectClaim].(string)
|
idTokenSubject, _ := validatedClaims[oidcapi.IDTokenClaimSubject].(string)
|
||||||
|
|
||||||
if len(idTokenSubject) > 0 || !requireIDToken {
|
if len(idTokenSubject) > 0 || !requireIDToken {
|
||||||
// only fetch userinfo if the ID token has a subject or if we are ignoring the id token completely.
|
// only fetch userinfo if the ID token has a subject or if we are ignoring the id token completely.
|
||||||
@ -346,7 +346,7 @@ func (p *ProviderConfig) validateIDToken(ctx context.Context, tok *oauth2.Token,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *ProviderConfig) maybeFetchUserInfoAndMergeClaims(ctx context.Context, tok *oauth2.Token, claims map[string]interface{}, requireIDToken bool, requireUserInfo bool) error {
|
func (p *ProviderConfig) maybeFetchUserInfoAndMergeClaims(ctx context.Context, tok *oauth2.Token, claims map[string]interface{}, requireIDToken bool, requireUserInfo bool) error {
|
||||||
idTokenSubject, _ := claims[oidc.IDTokenSubjectClaim].(string)
|
idTokenSubject, _ := claims[oidcapi.IDTokenClaimSubject].(string)
|
||||||
|
|
||||||
userInfo, err := p.maybeFetchUserInfo(ctx, tok, requireUserInfo)
|
userInfo, err := p.maybeFetchUserInfo(ctx, tok, requireUserInfo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -371,7 +371,7 @@ func (p *ProviderConfig) maybeFetchUserInfoAndMergeClaims(ctx context.Context, t
|
|||||||
}
|
}
|
||||||
|
|
||||||
// keep track of the issuer from the ID token
|
// keep track of the issuer from the ID token
|
||||||
idTokenIssuer := claims["iss"]
|
idTokenIssuer := claims[oidcapi.IDTokenClaimIssuer]
|
||||||
|
|
||||||
// merge existing claims with user info claims
|
// merge existing claims with user info claims
|
||||||
if err := userInfo.Claims(&claims); err != nil {
|
if err := userInfo.Claims(&claims); err != nil {
|
||||||
@ -381,9 +381,9 @@ func (p *ProviderConfig) maybeFetchUserInfoAndMergeClaims(ctx context.Context, t
|
|||||||
// "If signed, the UserInfo Response SHOULD contain the Claims iss (issuer) and aud (audience) as members. The iss value SHOULD be the OP's Issuer Identifier URL."
|
// "If signed, the UserInfo Response SHOULD contain the Claims iss (issuer) and aud (audience) as members. The iss value SHOULD be the OP's Issuer Identifier URL."
|
||||||
// See https://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse
|
// See https://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse
|
||||||
// So we just ignore it and use it the version from the id token, which has stronger guarantees.
|
// So we just ignore it and use it the version from the id token, which has stronger guarantees.
|
||||||
delete(claims, "iss")
|
delete(claims, oidcapi.IDTokenClaimIssuer)
|
||||||
if idTokenIssuer != nil {
|
if idTokenIssuer != nil {
|
||||||
claims["iss"] = idTokenIssuer
|
claims[oidcapi.IDTokenClaimIssuer] = idTokenIssuer
|
||||||
}
|
}
|
||||||
|
|
||||||
maybeLogClaims("claims from ID token and userinfo", p.Name, claims)
|
maybeLogClaims("claims from ID token and userinfo", p.Name, claims)
|
||||||
|
@ -21,14 +21,14 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/coreos/go-oidc/v3/oidc"
|
coreosoidc "github.com/coreos/go-oidc/v3/oidc"
|
||||||
"github.com/go-logr/logr"
|
"github.com/go-logr/logr"
|
||||||
"github.com/pkg/browser"
|
"github.com/pkg/browser"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
"golang.org/x/term"
|
"golang.org/x/term"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
|
||||||
supervisoroidc "go.pinniped.dev/generated/latest/apis/supervisor/oidc"
|
oidcapi "go.pinniped.dev/generated/latest/apis/supervisor/oidc"
|
||||||
"go.pinniped.dev/internal/httputil/httperr"
|
"go.pinniped.dev/internal/httputil/httperr"
|
||||||
"go.pinniped.dev/internal/httputil/securityheader"
|
"go.pinniped.dev/internal/httputil/securityheader"
|
||||||
"go.pinniped.dev/internal/net/phttp"
|
"go.pinniped.dev/internal/net/phttp"
|
||||||
@ -91,7 +91,7 @@ type handlerState struct {
|
|||||||
callbackPath string
|
callbackPath string
|
||||||
|
|
||||||
// Generated parameters of a login flow.
|
// Generated parameters of a login flow.
|
||||||
provider *oidc.Provider
|
provider *coreosoidc.Provider
|
||||||
oauth2Config *oauth2.Config
|
oauth2Config *oauth2.Config
|
||||||
useFormPost bool
|
useFormPost bool
|
||||||
state state.State
|
state state.State
|
||||||
@ -106,8 +106,8 @@ type handlerState struct {
|
|||||||
getEnv func(key string) string
|
getEnv func(key string) string
|
||||||
listen func(string, string) (net.Listener, error)
|
listen func(string, string) (net.Listener, error)
|
||||||
isTTY func(int) bool
|
isTTY func(int) bool
|
||||||
getProvider func(*oauth2.Config, *oidc.Provider, *http.Client) provider.UpstreamOIDCIdentityProviderI
|
getProvider func(*oauth2.Config, *coreosoidc.Provider, *http.Client) provider.UpstreamOIDCIdentityProviderI
|
||||||
validateIDToken func(ctx context.Context, provider *oidc.Provider, audience string, token string) (*oidc.IDToken, error)
|
validateIDToken func(ctx context.Context, provider *coreosoidc.Provider, audience string, token string) (*coreosoidc.IDToken, error)
|
||||||
promptForValue func(ctx context.Context, promptLabel string) (string, error)
|
promptForValue func(ctx context.Context, promptLabel string) (string, error)
|
||||||
promptForSecret func(promptLabel string) (string, error)
|
promptForSecret func(promptLabel string) (string, error)
|
||||||
|
|
||||||
@ -268,7 +268,7 @@ func Login(issuer string, clientID string, opts ...Option) (*oidctypes.Token, er
|
|||||||
issuer: issuer,
|
issuer: issuer,
|
||||||
clientID: clientID,
|
clientID: clientID,
|
||||||
listenAddr: "localhost:0",
|
listenAddr: "localhost:0",
|
||||||
scopes: []string{oidc.ScopeOfflineAccess, oidc.ScopeOpenID, "email", "profile"},
|
scopes: []string{oidcapi.ScopeOfflineAccess, oidcapi.ScopeOpenID, oidcapi.ScopeEmail, oidcapi.ScopeProfile},
|
||||||
cache: &nopCache{},
|
cache: &nopCache{},
|
||||||
callbackPath: "/callback",
|
callbackPath: "/callback",
|
||||||
ctx: context.Background(),
|
ctx: context.Background(),
|
||||||
@ -285,8 +285,8 @@ func Login(issuer string, clientID string, opts ...Option) (*oidctypes.Token, er
|
|||||||
listen: net.Listen,
|
listen: net.Listen,
|
||||||
isTTY: term.IsTerminal,
|
isTTY: term.IsTerminal,
|
||||||
getProvider: upstreamoidc.New,
|
getProvider: upstreamoidc.New,
|
||||||
validateIDToken: func(ctx context.Context, provider *oidc.Provider, audience string, token string) (*oidc.IDToken, error) {
|
validateIDToken: func(ctx context.Context, provider *coreosoidc.Provider, audience string, token string) (*coreosoidc.IDToken, error) {
|
||||||
return provider.Verifier(&oidc.Config{ClientID: audience}).Verify(ctx, token)
|
return provider.Verifier(&coreosoidc.Config{ClientID: audience}).Verify(ctx, token)
|
||||||
},
|
},
|
||||||
promptForValue: promptForValue,
|
promptForValue: promptForValue,
|
||||||
promptForSecret: promptForSecret,
|
promptForSecret: promptForSecret,
|
||||||
@ -305,7 +305,7 @@ func Login(issuer string, clientID string, opts ...Option) (*oidctypes.Token, er
|
|||||||
// Always set a long, but non-infinite timeout for this operation.
|
// Always set a long, but non-infinite timeout for this operation.
|
||||||
ctx, cancel := context.WithTimeout(h.ctx, overallTimeout)
|
ctx, cancel := context.WithTimeout(h.ctx, overallTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
ctx = oidc.ClientContext(ctx, h.httpClient)
|
ctx = coreosoidc.ClientContext(ctx, h.httpClient)
|
||||||
h.ctx = ctx
|
h.ctx = ctx
|
||||||
|
|
||||||
// Initialize login parameters.
|
// Initialize login parameters.
|
||||||
@ -386,10 +386,10 @@ func (h *handlerState) baseLogin() (*oidctypes.Token, error) {
|
|||||||
}
|
}
|
||||||
if h.upstreamIdentityProviderName != "" {
|
if h.upstreamIdentityProviderName != "" {
|
||||||
authorizeOptions = append(authorizeOptions,
|
authorizeOptions = append(authorizeOptions,
|
||||||
oauth2.SetAuthURLParam(supervisoroidc.AuthorizeUpstreamIDPNameParamName, h.upstreamIdentityProviderName),
|
oauth2.SetAuthURLParam(oidcapi.AuthorizeUpstreamIDPNameParamName, h.upstreamIdentityProviderName),
|
||||||
)
|
)
|
||||||
authorizeOptions = append(authorizeOptions,
|
authorizeOptions = append(authorizeOptions,
|
||||||
oauth2.SetAuthURLParam(supervisoroidc.AuthorizeUpstreamIDPTypeParamName, h.upstreamIdentityProviderType),
|
oauth2.SetAuthURLParam(oidcapi.AuthorizeUpstreamIDPTypeParamName, h.upstreamIdentityProviderType),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -447,8 +447,8 @@ func (h *handlerState) cliBasedAuth(authorizeOptions *[]oauth2.AuthCodeOption) (
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("could not build authorize request: %w", err)
|
return nil, fmt.Errorf("could not build authorize request: %w", err)
|
||||||
}
|
}
|
||||||
authReq.Header.Set(supervisoroidc.AuthorizeUsernameHeaderName, username)
|
authReq.Header.Set(oidcapi.AuthorizeUsernameHeaderName, username)
|
||||||
authReq.Header.Set(supervisoroidc.AuthorizePasswordHeaderName, password)
|
authReq.Header.Set(oidcapi.AuthorizePasswordHeaderName, password)
|
||||||
authRes, err := h.httpClient.Do(authReq)
|
authRes, err := h.httpClient.Do(authReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("authorization response error: %w", err)
|
return nil, fmt.Errorf("authorization response error: %w", err)
|
||||||
@ -710,7 +710,7 @@ func (h *handlerState) initOIDCDiscovery() error {
|
|||||||
|
|
||||||
h.logger.V(plog.KlogLevelDebug).Info("Pinniped: Performing OIDC discovery", "issuer", h.issuer)
|
h.logger.V(plog.KlogLevelDebug).Info("Pinniped: Performing OIDC discovery", "issuer", h.issuer)
|
||||||
var err error
|
var err error
|
||||||
h.provider, err = oidc.NewProvider(h.ctx, h.issuer)
|
h.provider, err = coreosoidc.NewProvider(h.ctx, h.issuer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("could not perform OIDC discovery for %q: %w", h.issuer, err)
|
return fmt.Errorf("could not perform OIDC discovery for %q: %w", h.issuer, err)
|
||||||
}
|
}
|
||||||
@ -775,7 +775,7 @@ func (h *handlerState) tokenExchangeRFC8693(baseToken *oidctypes.Token) (*oidcty
|
|||||||
// Form the HTTP POST request with the parameters specified by RFC8693.
|
// Form the HTTP POST request with the parameters specified by RFC8693.
|
||||||
reqBody := strings.NewReader(url.Values{
|
reqBody := strings.NewReader(url.Values{
|
||||||
"client_id": []string{h.clientID},
|
"client_id": []string{h.clientID},
|
||||||
"grant_type": []string{"urn:ietf:params:oauth:grant-type:token-exchange"},
|
"grant_type": []string{oidcapi.GrantTypeTokenExchange},
|
||||||
"audience": []string{h.requestedAudience},
|
"audience": []string{h.requestedAudience},
|
||||||
"subject_token": []string{baseToken.AccessToken.Token},
|
"subject_token": []string{baseToken.AccessToken.Token},
|
||||||
"subject_token_type": []string{"urn:ietf:params:oauth:token-type:access_token"},
|
"subject_token_type": []string{"urn:ietf:params:oauth:token-type:access_token"},
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
// Package nonce implements
|
// Package nonce implements helpers for OIDC nonce parameter handling.
|
||||||
package nonce
|
package nonce
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -11,7 +11,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
"github.com/coreos/go-oidc/v3/oidc"
|
coreosoidc "github.com/coreos/go-oidc/v3/oidc"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -36,11 +36,11 @@ func (n *Nonce) String() string {
|
|||||||
|
|
||||||
// Param returns the OAuth2 auth code parameter for sending the nonce during the authorization request.
|
// Param returns the OAuth2 auth code parameter for sending the nonce during the authorization request.
|
||||||
func (n *Nonce) Param() oauth2.AuthCodeOption {
|
func (n *Nonce) Param() oauth2.AuthCodeOption {
|
||||||
return oidc.Nonce(string(*n))
|
return coreosoidc.Nonce(string(*n))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate the returned ID token). Returns true iff the nonce matches or the returned JWT does not have a nonce.
|
// Validate the returned ID token). Returns true iff the nonce matches or the returned JWT does not have a nonce.
|
||||||
func (n *Nonce) Validate(token *oidc.IDToken) error {
|
func (n *Nonce) Validate(token *coreosoidc.IDToken) error {
|
||||||
if subtle.ConstantTimeCompare([]byte(token.Nonce), []byte(*n)) != 1 {
|
if subtle.ConstantTimeCompare([]byte(token.Nonce), []byte(*n)) != 1 {
|
||||||
return InvalidNonceError{Expected: *n, Got: Nonce(token.Nonce)}
|
return InvalidNonceError{Expected: *n, Got: Nonce(token.Nonce)}
|
||||||
}
|
}
|
||||||
|
@ -188,7 +188,7 @@ The Supervisor's endpoints are:
|
|||||||
- And a number of endpoints for each FederationDomain that is configured by the user.
|
- And a number of endpoints for each FederationDomain that is configured by the user.
|
||||||
|
|
||||||
Each FederationDomain's endpoints are mounted under the path of the FederationDomain's `spec.issuer`,
|
Each FederationDomain's endpoints are mounted under the path of the FederationDomain's `spec.issuer`,
|
||||||
if the issuer as a path specified in its URL. If the issuer has no path, then they are mounted under `/`.
|
if the `spec.issuer` URL has a path component specified. If the issuer has no path, then they are mounted under `/`.
|
||||||
These per-FederationDomain endpoint are all mounted by the code in
|
These per-FederationDomain endpoint are all mounted by the code in
|
||||||
[internal/oidc/provider/manager/manager.go](https://github.com/vmware-tanzu/pinniped/blob/main/internal/oidc/provider/manager/manager.go).
|
[internal/oidc/provider/manager/manager.go](https://github.com/vmware-tanzu/pinniped/blob/main/internal/oidc/provider/manager/manager.go).
|
||||||
|
|
||||||
@ -202,6 +202,10 @@ The per-FederationDomain endpoints are:
|
|||||||
See [internal/oidc/auth/auth_handler.go](https://github.com/vmware-tanzu/pinniped/blob/main/internal/oidc/auth/auth_handler.go).
|
See [internal/oidc/auth/auth_handler.go](https://github.com/vmware-tanzu/pinniped/blob/main/internal/oidc/auth/auth_handler.go).
|
||||||
- `<issuer_path>/oauth2/token` is the standard OIDC token endpoint.
|
- `<issuer_path>/oauth2/token` is the standard OIDC token endpoint.
|
||||||
See [internal/oidc/token/token_handler.go](https://github.com/vmware-tanzu/pinniped/blob/main/internal/oidc/token/token_handler.go).
|
See [internal/oidc/token/token_handler.go](https://github.com/vmware-tanzu/pinniped/blob/main/internal/oidc/token/token_handler.go).
|
||||||
|
The token endpoint can handle the standard OIDC `authorization_code` and `refresh_token` grant types, and has also been
|
||||||
|
extended in [internal/oidc/token_exchange.go](https://github.com/vmware-tanzu/pinniped/blob/main/internal/oidc/token_exchange.go)
|
||||||
|
to handle an additional grant type for [RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693) token exchanges to
|
||||||
|
reduce the applicable scope (technically, the `aud` claim) of ID tokens.
|
||||||
- `<issuer_path>/callback` is a special endpoint that is used as the redirect URL when performing an OIDC authcode flow against an upstream OIDC identity provider as configured by an OIDCIdentityProvider custom resource.
|
- `<issuer_path>/callback` is a special endpoint that is used as the redirect URL when performing an OIDC authcode flow against an upstream OIDC identity provider as configured by an OIDCIdentityProvider custom resource.
|
||||||
See [internal/oidc/callback/callback_handler.go](https://github.com/vmware-tanzu/pinniped/blob/main/internal/oidc/callback/callback_handler.go).
|
See [internal/oidc/callback/callback_handler.go](https://github.com/vmware-tanzu/pinniped/blob/main/internal/oidc/callback/callback_handler.go).
|
||||||
- `<issuer_path>/v1alpha1/pinniped_identity_providers` is a custom discovery endpoint for clients to learn about available upstream identity providers.
|
- `<issuer_path>/v1alpha1/pinniped_identity_providers` is a custom discovery endpoint for clients to learn about available upstream identity providers.
|
||||||
|
@ -24,7 +24,6 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
coreosoidc "github.com/coreos/go-oidc/v3/oidc"
|
|
||||||
"github.com/creack/pty"
|
"github.com/creack/pty"
|
||||||
"github.com/sclevine/agouti"
|
"github.com/sclevine/agouti"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@ -40,7 +39,6 @@ import (
|
|||||||
"go.pinniped.dev/internal/certauthority"
|
"go.pinniped.dev/internal/certauthority"
|
||||||
"go.pinniped.dev/internal/crud"
|
"go.pinniped.dev/internal/crud"
|
||||||
"go.pinniped.dev/internal/here"
|
"go.pinniped.dev/internal/here"
|
||||||
"go.pinniped.dev/internal/oidc"
|
|
||||||
"go.pinniped.dev/internal/testutil"
|
"go.pinniped.dev/internal/testutil"
|
||||||
"go.pinniped.dev/pkg/oidcclient"
|
"go.pinniped.dev/pkg/oidcclient"
|
||||||
"go.pinniped.dev/pkg/oidcclient/filesession"
|
"go.pinniped.dev/pkg/oidcclient/filesession"
|
||||||
@ -53,6 +51,8 @@ import (
|
|||||||
func TestE2EFullIntegration_Browser(t *testing.T) {
|
func TestE2EFullIntegration_Browser(t *testing.T) {
|
||||||
env := testlib.IntegrationEnv(t)
|
env := testlib.IntegrationEnv(t)
|
||||||
|
|
||||||
|
allScopes := []string{"openid", "offline_access", "pinniped:request-audience", "username", "groups"}
|
||||||
|
|
||||||
// Avoid allowing PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW to interfere with these tests.
|
// Avoid allowing PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW to interfere with these tests.
|
||||||
originalFlowEnvVarValue, flowOverrideEnvVarSet := os.LookupEnv("PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW")
|
originalFlowEnvVarValue, flowOverrideEnvVarSet := os.LookupEnv("PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW")
|
||||||
if flowOverrideEnvVarSet {
|
if flowOverrideEnvVarSet {
|
||||||
@ -170,7 +170,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
|
|||||||
"--oidc-skip-browser",
|
"--oidc-skip-browser",
|
||||||
"--oidc-ca-bundle", testCABundlePath,
|
"--oidc-ca-bundle", testCABundlePath,
|
||||||
"--oidc-session-cache", sessionCachePath,
|
"--oidc-session-cache", sessionCachePath,
|
||||||
"--oidc-scopes", "offline_access,openid,pinniped:request-audience,groups",
|
// use default for --oidc-scopes, which is to request all relevant scopes
|
||||||
})
|
})
|
||||||
|
|
||||||
// Run "kubectl get namespaces" which should trigger a browser login via the plugin.
|
// Run "kubectl get namespaces" which should trigger a browser login via the plugin.
|
||||||
@ -193,11 +193,12 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
|
|||||||
|
|
||||||
requireKubectlGetNamespaceOutput(t, env, waitForKubectlOutput(t, kubectlOutputChan))
|
requireKubectlGetNamespaceOutput(t, env, waitForKubectlOutput(t, kubectlOutputChan))
|
||||||
|
|
||||||
requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, kubeconfigPath, sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, []string{coreosoidc.ScopeOfflineAccess, coreosoidc.ScopeOpenID, "pinniped:request-audience", "groups"})
|
requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, kubeconfigPath, sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, allScopes)
|
||||||
})
|
})
|
||||||
|
|
||||||
// If scopes aren't specified, we don't request the groups scope, which means we won't get any groups back in our token.
|
// If the username and groups scope are not requested by the CLI, then the CLI still gets them, to allow for
|
||||||
t.Run("with Supervisor OIDC upstream IDP and browser flow, scopes not specified", func(t *testing.T) {
|
// backwards compatibility with old CLIs that did not request those scopes because they did not exist yet.
|
||||||
|
t.Run("with Supervisor OIDC upstream IDP and browser flow, downstream username and groups scopes not requested", func(t *testing.T) {
|
||||||
testCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
testCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||||
t.Cleanup(cancel)
|
t.Cleanup(cancel)
|
||||||
|
|
||||||
@ -249,6 +250,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
|
|||||||
"--oidc-skip-browser",
|
"--oidc-skip-browser",
|
||||||
"--oidc-ca-bundle", testCABundlePath,
|
"--oidc-ca-bundle", testCABundlePath,
|
||||||
"--oidc-session-cache", sessionCachePath,
|
"--oidc-session-cache", sessionCachePath,
|
||||||
|
"--oidc-scopes", "offline_access,openid,pinniped:request-audience", // does not request username or groups
|
||||||
})
|
})
|
||||||
|
|
||||||
// Run "kubectl get namespaces" which should trigger a browser login via the plugin.
|
// Run "kubectl get namespaces" which should trigger a browser login via the plugin.
|
||||||
@ -271,7 +273,12 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
|
|||||||
|
|
||||||
requireKubectlGetNamespaceOutput(t, env, waitForKubectlOutput(t, kubectlOutputChan))
|
requireKubectlGetNamespaceOutput(t, env, waitForKubectlOutput(t, kubectlOutputChan))
|
||||||
|
|
||||||
requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, kubeconfigPath, sessionCachePath, pinnipedExe, expectedUsername, []string{}, []string{coreosoidc.ScopeOfflineAccess, coreosoidc.ScopeOpenID, "pinniped:request-audience"})
|
// Note that the list of scopes param here is used to form the cache key for looking up local session storage.
|
||||||
|
// The scopes portion of the cache key is made up of the requested scopes from the CLI flag, not the granted
|
||||||
|
// scopes returned by the Supervisor, so list the requested scopes from the CLI flag here. This helper will
|
||||||
|
// assert that the expected username and groups claims/values are in the downstream ID token.
|
||||||
|
requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, kubeconfigPath, sessionCachePath,
|
||||||
|
pinnipedExe, expectedUsername, []string{}, []string{"offline_access", "openid", "pinniped:request-audience"})
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("with Supervisor OIDC upstream IDP and manual authcode copy-paste from browser flow", func(t *testing.T) {
|
t.Run("with Supervisor OIDC upstream IDP and manual authcode copy-paste from browser flow", func(t *testing.T) {
|
||||||
@ -328,7 +335,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
|
|||||||
"--oidc-skip-listen",
|
"--oidc-skip-listen",
|
||||||
"--oidc-ca-bundle", testCABundlePath,
|
"--oidc-ca-bundle", testCABundlePath,
|
||||||
"--oidc-session-cache", sessionCachePath,
|
"--oidc-session-cache", sessionCachePath,
|
||||||
"--oidc-scopes", "offline_access,openid,pinniped:request-audience,groups",
|
// use default for --oidc-scopes, which is to request all relevant scopes
|
||||||
})
|
})
|
||||||
|
|
||||||
// Run "kubectl get namespaces" which should trigger a browser login via the plugin.
|
// Run "kubectl get namespaces" which should trigger a browser login via the plugin.
|
||||||
@ -382,7 +389,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
|
|||||||
|
|
||||||
t.Logf("first kubectl command took %s", time.Since(start).String())
|
t.Logf("first kubectl command took %s", time.Since(start).String())
|
||||||
|
|
||||||
requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, kubeconfigPath, sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, []string{coreosoidc.ScopeOfflineAccess, coreosoidc.ScopeOpenID, "pinniped:request-audience", "groups"})
|
requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, kubeconfigPath, sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, allScopes)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("access token based refresh with Supervisor OIDC upstream IDP and manual authcode copy-paste from browser flow", func(t *testing.T) {
|
t.Run("access token based refresh with Supervisor OIDC upstream IDP and manual authcode copy-paste from browser flow", func(t *testing.T) {
|
||||||
@ -447,7 +454,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
|
|||||||
"--oidc-skip-listen",
|
"--oidc-skip-listen",
|
||||||
"--oidc-ca-bundle", testCABundlePath,
|
"--oidc-ca-bundle", testCABundlePath,
|
||||||
"--oidc-session-cache", sessionCachePath,
|
"--oidc-session-cache", sessionCachePath,
|
||||||
"--oidc-scopes", "offline_access,openid,pinniped:request-audience,groups",
|
// use default for --oidc-scopes, which is to request all relevant scopes
|
||||||
})
|
})
|
||||||
|
|
||||||
// Run "kubectl get namespaces" which should trigger a browser login via the plugin.
|
// Run "kubectl get namespaces" which should trigger a browser login via the plugin.
|
||||||
@ -518,7 +525,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
|
|||||||
|
|
||||||
t.Logf("first kubectl command took %s", time.Since(start).String())
|
t.Logf("first kubectl command took %s", time.Since(start).String())
|
||||||
|
|
||||||
requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, kubeconfigPath, sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, []string{coreosoidc.ScopeOfflineAccess, coreosoidc.ScopeOpenID, "pinniped:request-audience", "groups"})
|
requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, kubeconfigPath, sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, allScopes)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("with Supervisor OIDC upstream IDP and CLI password flow without web browser", func(t *testing.T) {
|
t.Run("with Supervisor OIDC upstream IDP and CLI password flow without web browser", func(t *testing.T) {
|
||||||
@ -574,7 +581,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
|
|||||||
"--upstream-identity-provider-flow", "cli_password", // create a kubeconfig configured to use the cli_password flow
|
"--upstream-identity-provider-flow", "cli_password", // create a kubeconfig configured to use the cli_password flow
|
||||||
"--oidc-ca-bundle", testCABundlePath,
|
"--oidc-ca-bundle", testCABundlePath,
|
||||||
"--oidc-session-cache", sessionCachePath,
|
"--oidc-session-cache", sessionCachePath,
|
||||||
"--oidc-scopes", "offline_access,openid,pinniped:request-audience,groups",
|
// use default for --oidc-scopes, which is to request all relevant scopes
|
||||||
})
|
})
|
||||||
|
|
||||||
// Run "kubectl get namespaces" which should trigger a browser-less CLI prompt login via the plugin.
|
// Run "kubectl get namespaces" which should trigger a browser-less CLI prompt login via the plugin.
|
||||||
@ -601,7 +608,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
|
|||||||
|
|
||||||
t.Logf("first kubectl command took %s", time.Since(start).String())
|
t.Logf("first kubectl command took %s", time.Since(start).String())
|
||||||
|
|
||||||
requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, kubeconfigPath, sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, []string{coreosoidc.ScopeOfflineAccess, coreosoidc.ScopeOpenID, "pinniped:request-audience", "groups"})
|
requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, kubeconfigPath, sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, allScopes)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("with Supervisor OIDC upstream IDP and CLI password flow when OIDCIdentityProvider disallows it", func(t *testing.T) {
|
t.Run("with Supervisor OIDC upstream IDP and CLI password flow when OIDCIdentityProvider disallows it", func(t *testing.T) {
|
||||||
@ -648,7 +655,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
|
|||||||
"--upstream-identity-provider-flow", "cli_password",
|
"--upstream-identity-provider-flow", "cli_password",
|
||||||
"--oidc-ca-bundle", testCABundlePath,
|
"--oidc-ca-bundle", testCABundlePath,
|
||||||
"--oidc-session-cache", sessionCachePath,
|
"--oidc-session-cache", sessionCachePath,
|
||||||
"--oidc-scopes", "offline_access,openid,pinniped:request-audience,groups",
|
// use default for --oidc-scopes, which is to request all relevant scopes
|
||||||
})
|
})
|
||||||
|
|
||||||
// Run "kubectl get --raw /healthz" which should trigger a browser-less CLI prompt login via the plugin.
|
// Run "kubectl get --raw /healthz" which should trigger a browser-less CLI prompt login via the plugin.
|
||||||
@ -710,7 +717,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
|
|||||||
"--concierge-authenticator-type", "jwt",
|
"--concierge-authenticator-type", "jwt",
|
||||||
"--concierge-authenticator-name", authenticator.Name,
|
"--concierge-authenticator-name", authenticator.Name,
|
||||||
"--oidc-session-cache", sessionCachePath,
|
"--oidc-session-cache", sessionCachePath,
|
||||||
"--oidc-scopes", "offline_access,openid,pinniped:request-audience,groups",
|
// use default for --oidc-scopes, which is to request all relevant scopes
|
||||||
})
|
})
|
||||||
|
|
||||||
// Run "kubectl get namespaces" which should trigger an LDAP-style login CLI prompt via the plugin.
|
// Run "kubectl get namespaces" which should trigger an LDAP-style login CLI prompt via the plugin.
|
||||||
@ -737,7 +744,66 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
|
|||||||
|
|
||||||
t.Logf("first kubectl command took %s", time.Since(start).String())
|
t.Logf("first kubectl command took %s", time.Since(start).String())
|
||||||
|
|
||||||
requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, kubeconfigPath, sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, []string{coreosoidc.ScopeOfflineAccess, coreosoidc.ScopeOpenID, "pinniped:request-audience", "groups"})
|
requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, kubeconfigPath, sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, allScopes)
|
||||||
|
})
|
||||||
|
|
||||||
|
// If the username and groups scope are not requested by the CLI, then the CLI still gets them, to allow for
|
||||||
|
// backwards compatibility with old CLIs that did not request those scopes because they did not exist yet.
|
||||||
|
t.Run("with Supervisor LDAP upstream IDP using username and password prompts, downstream username and groups scopes not requested", func(t *testing.T) {
|
||||||
|
testlib.SkipTestWhenLDAPIsUnavailable(t, env)
|
||||||
|
|
||||||
|
testCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||||
|
t.Cleanup(cancel)
|
||||||
|
|
||||||
|
tempDir := testutil.TempDir(t) // per-test tmp dir to avoid sharing files between tests
|
||||||
|
|
||||||
|
expectedUsername := env.SupervisorUpstreamLDAP.TestUserMailAttributeValue
|
||||||
|
expectedGroups := env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs
|
||||||
|
|
||||||
|
setupClusterForEndToEndLDAPTest(t, expectedUsername, env)
|
||||||
|
|
||||||
|
// Use a specific session cache for this test.
|
||||||
|
sessionCachePath := tempDir + "/test-sessions.yaml"
|
||||||
|
|
||||||
|
kubeconfigPath := runPinnipedGetKubeconfig(t, env, pinnipedExe, tempDir, []string{
|
||||||
|
"get", "kubeconfig",
|
||||||
|
"--concierge-api-group-suffix", env.APIGroupSuffix,
|
||||||
|
"--concierge-authenticator-type", "jwt",
|
||||||
|
"--concierge-authenticator-name", authenticator.Name,
|
||||||
|
"--oidc-session-cache", sessionCachePath,
|
||||||
|
"--oidc-scopes", "offline_access,openid,pinniped:request-audience", // does not request username or groups
|
||||||
|
})
|
||||||
|
|
||||||
|
// Run "kubectl get namespaces" which should trigger an LDAP-style login CLI prompt via the plugin.
|
||||||
|
start := time.Now()
|
||||||
|
kubectlCmd := exec.CommandContext(testCtx, "kubectl", "get", "namespace", "--kubeconfig", kubeconfigPath)
|
||||||
|
kubectlCmd.Env = append(os.Environ(), env.ProxyEnv()...)
|
||||||
|
ptyFile, err := pty.Start(kubectlCmd)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Wait for the subprocess to print the username prompt, then type the user's username.
|
||||||
|
readFromFileUntilStringIsSeen(t, ptyFile, "Username: ")
|
||||||
|
_, err = ptyFile.WriteString(expectedUsername + "\n")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Wait for the subprocess to print the password prompt, then type the user's password.
|
||||||
|
readFromFileUntilStringIsSeen(t, ptyFile, "Password: ")
|
||||||
|
_, err = ptyFile.WriteString(env.SupervisorUpstreamLDAP.TestUserPassword + "\n")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Read all output from the subprocess until EOF.
|
||||||
|
// Ignore any errors returned because there is always an error on linux.
|
||||||
|
kubectlOutputBytes, _ := ioutil.ReadAll(ptyFile)
|
||||||
|
requireKubectlGetNamespaceOutput(t, env, string(kubectlOutputBytes))
|
||||||
|
|
||||||
|
t.Logf("first kubectl command took %s", time.Since(start).String())
|
||||||
|
|
||||||
|
// Note that the list of scopes param here is used to form the cache key for looking up local session storage.
|
||||||
|
// The scopes portion of the cache key is made up of the requested scopes from the CLI flag, not the granted
|
||||||
|
// scopes returned by the Supervisor, so list the requested scopes from the CLI flag here. This helper will
|
||||||
|
// assert that the expected username and groups claims/values are in the downstream ID token.
|
||||||
|
requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, kubeconfigPath, sessionCachePath,
|
||||||
|
pinnipedExe, expectedUsername, expectedGroups, []string{"offline_access", "openid", "pinniped:request-audience"})
|
||||||
})
|
})
|
||||||
|
|
||||||
// Add an LDAP upstream IDP and try using it to authenticate during kubectl commands
|
// Add an LDAP upstream IDP and try using it to authenticate during kubectl commands
|
||||||
@ -764,7 +830,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
|
|||||||
"--concierge-authenticator-type", "jwt",
|
"--concierge-authenticator-type", "jwt",
|
||||||
"--concierge-authenticator-name", authenticator.Name,
|
"--concierge-authenticator-name", authenticator.Name,
|
||||||
"--oidc-session-cache", sessionCachePath,
|
"--oidc-session-cache", sessionCachePath,
|
||||||
"--oidc-scopes", "offline_access,openid,pinniped:request-audience,groups",
|
// use default for --oidc-scopes, which is to request all relevant scopes
|
||||||
})
|
})
|
||||||
|
|
||||||
// Set up the username and password env vars to avoid the interactive prompts.
|
// Set up the username and password env vars to avoid the interactive prompts.
|
||||||
@ -803,7 +869,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
|
|||||||
require.NoError(t, os.Unsetenv(usernameEnvVar))
|
require.NoError(t, os.Unsetenv(usernameEnvVar))
|
||||||
require.NoError(t, os.Unsetenv(passwordEnvVar))
|
require.NoError(t, os.Unsetenv(passwordEnvVar))
|
||||||
|
|
||||||
requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, kubeconfigPath, sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, []string{coreosoidc.ScopeOfflineAccess, coreosoidc.ScopeOpenID, "pinniped:request-audience", "groups"})
|
requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, kubeconfigPath, sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, allScopes)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Add an Active Directory upstream IDP and try using it to authenticate during kubectl commands
|
// Add an Active Directory upstream IDP and try using it to authenticate during kubectl commands
|
||||||
@ -830,7 +896,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
|
|||||||
"--concierge-authenticator-type", "jwt",
|
"--concierge-authenticator-type", "jwt",
|
||||||
"--concierge-authenticator-name", authenticator.Name,
|
"--concierge-authenticator-name", authenticator.Name,
|
||||||
"--oidc-session-cache", sessionCachePath,
|
"--oidc-session-cache", sessionCachePath,
|
||||||
"--oidc-scopes", "offline_access,openid,pinniped:request-audience,groups",
|
// use default for --oidc-scopes, which is to request all relevant scopes
|
||||||
})
|
})
|
||||||
|
|
||||||
// Run "kubectl get namespaces" which should trigger an LDAP-style login CLI prompt via the plugin.
|
// Run "kubectl get namespaces" which should trigger an LDAP-style login CLI prompt via the plugin.
|
||||||
@ -857,7 +923,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
|
|||||||
|
|
||||||
t.Logf("first kubectl command took %s", time.Since(start).String())
|
t.Logf("first kubectl command took %s", time.Since(start).String())
|
||||||
|
|
||||||
requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, kubeconfigPath, sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, []string{coreosoidc.ScopeOfflineAccess, coreosoidc.ScopeOpenID, "pinniped:request-audience", "groups"})
|
requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, kubeconfigPath, sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, allScopes)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Add an ActiveDirectory upstream IDP and try using it to authenticate during kubectl commands
|
// Add an ActiveDirectory upstream IDP and try using it to authenticate during kubectl commands
|
||||||
@ -884,7 +950,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
|
|||||||
"--concierge-authenticator-type", "jwt",
|
"--concierge-authenticator-type", "jwt",
|
||||||
"--concierge-authenticator-name", authenticator.Name,
|
"--concierge-authenticator-name", authenticator.Name,
|
||||||
"--oidc-session-cache", sessionCachePath,
|
"--oidc-session-cache", sessionCachePath,
|
||||||
"--oidc-scopes", "offline_access,openid,pinniped:request-audience,groups",
|
// use default for --oidc-scopes, which is to request all relevant scopes
|
||||||
})
|
})
|
||||||
|
|
||||||
// Set up the username and password env vars to avoid the interactive prompts.
|
// Set up the username and password env vars to avoid the interactive prompts.
|
||||||
@ -923,7 +989,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
|
|||||||
require.NoError(t, os.Unsetenv(usernameEnvVar))
|
require.NoError(t, os.Unsetenv(usernameEnvVar))
|
||||||
require.NoError(t, os.Unsetenv(passwordEnvVar))
|
require.NoError(t, os.Unsetenv(passwordEnvVar))
|
||||||
|
|
||||||
requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, kubeconfigPath, sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, []string{coreosoidc.ScopeOfflineAccess, coreosoidc.ScopeOpenID, "pinniped:request-audience", "groups"})
|
requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, kubeconfigPath, sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, allScopes)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Add an LDAP upstream IDP and try using it to authenticate during kubectl commands, using the browser flow.
|
// Add an LDAP upstream IDP and try using it to authenticate during kubectl commands, using the browser flow.
|
||||||
@ -955,7 +1021,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
|
|||||||
"--oidc-ca-bundle", testCABundlePath,
|
"--oidc-ca-bundle", testCABundlePath,
|
||||||
"--upstream-identity-provider-flow", "browser_authcode",
|
"--upstream-identity-provider-flow", "browser_authcode",
|
||||||
"--oidc-session-cache", sessionCachePath,
|
"--oidc-session-cache", sessionCachePath,
|
||||||
"--oidc-scopes", "offline_access,openid,pinniped:request-audience,groups",
|
// use default for --oidc-scopes, which is to request all relevant scopes
|
||||||
})
|
})
|
||||||
|
|
||||||
// Run "kubectl get namespaces" which should trigger a browser login via the plugin.
|
// Run "kubectl get namespaces" which should trigger a browser login via the plugin.
|
||||||
@ -973,7 +1039,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
|
|||||||
|
|
||||||
requireKubectlGetNamespaceOutput(t, env, waitForKubectlOutput(t, kubectlOutputChan))
|
requireKubectlGetNamespaceOutput(t, env, waitForKubectlOutput(t, kubectlOutputChan))
|
||||||
|
|
||||||
requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, kubeconfigPath, sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, []string{coreosoidc.ScopeOfflineAccess, coreosoidc.ScopeOpenID, "pinniped:request-audience", "groups"})
|
requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, kubeconfigPath, sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, allScopes)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Add an Active Directory upstream IDP and try using it to authenticate during kubectl commands, using the browser flow.
|
// Add an Active Directory upstream IDP and try using it to authenticate during kubectl commands, using the browser flow.
|
||||||
@ -1005,7 +1071,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
|
|||||||
"--oidc-ca-bundle", testCABundlePath,
|
"--oidc-ca-bundle", testCABundlePath,
|
||||||
"--upstream-identity-provider-flow", "browser_authcode",
|
"--upstream-identity-provider-flow", "browser_authcode",
|
||||||
"--oidc-session-cache", sessionCachePath,
|
"--oidc-session-cache", sessionCachePath,
|
||||||
"--oidc-scopes", "offline_access,openid,pinniped:request-audience,groups",
|
// use default for --oidc-scopes, which is to request all relevant scopes
|
||||||
})
|
})
|
||||||
|
|
||||||
// Run "kubectl get namespaces" which should trigger a browser login via the plugin.
|
// Run "kubectl get namespaces" which should trigger a browser login via the plugin.
|
||||||
@ -1023,7 +1089,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
|
|||||||
|
|
||||||
requireKubectlGetNamespaceOutput(t, env, waitForKubectlOutput(t, kubectlOutputChan))
|
requireKubectlGetNamespaceOutput(t, env, waitForKubectlOutput(t, kubectlOutputChan))
|
||||||
|
|
||||||
requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, kubeconfigPath, sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, []string{coreosoidc.ScopeOfflineAccess, coreosoidc.ScopeOpenID, "pinniped:request-audience", "groups"})
|
requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, kubeconfigPath, sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, allScopes)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Add an LDAP upstream IDP and try using it to authenticate during kubectl commands, using the env var to choose the browser flow.
|
// Add an LDAP upstream IDP and try using it to authenticate during kubectl commands, using the env var to choose the browser flow.
|
||||||
@ -1055,7 +1121,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
|
|||||||
"--oidc-ca-bundle", testCABundlePath,
|
"--oidc-ca-bundle", testCABundlePath,
|
||||||
"--upstream-identity-provider-flow", "cli_password", // put cli_password in the kubeconfig, so we can override it with the env var
|
"--upstream-identity-provider-flow", "cli_password", // put cli_password in the kubeconfig, so we can override it with the env var
|
||||||
"--oidc-session-cache", sessionCachePath,
|
"--oidc-session-cache", sessionCachePath,
|
||||||
"--oidc-scopes", "offline_access,openid,pinniped:request-audience,groups",
|
// use default for --oidc-scopes, which is to request all relevant scopes
|
||||||
})
|
})
|
||||||
|
|
||||||
// Override the --upstream-identity-provider-flow flag from the kubeconfig using the env var.
|
// Override the --upstream-identity-provider-flow flag from the kubeconfig using the env var.
|
||||||
@ -1079,7 +1145,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
|
|||||||
|
|
||||||
requireKubectlGetNamespaceOutput(t, env, waitForKubectlOutput(t, kubectlOutputChan))
|
requireKubectlGetNamespaceOutput(t, env, waitForKubectlOutput(t, kubectlOutputChan))
|
||||||
|
|
||||||
requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, kubeconfigPath, sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, []string{coreosoidc.ScopeOfflineAccess, coreosoidc.ScopeOpenID, "pinniped:request-audience", "groups"})
|
requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, kubeconfigPath, sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, allScopes)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1337,17 +1403,17 @@ func requireUserCanUseKubectlWithoutAuthenticatingAgain(
|
|||||||
requireGCAnnotationsOnSessionStorage(ctx, t, env.SupervisorNamespace, startTime, token)
|
requireGCAnnotationsOnSessionStorage(ctx, t, env.SupervisorNamespace, startTime, token)
|
||||||
|
|
||||||
idTokenClaims := token.IDToken.Claims
|
idTokenClaims := token.IDToken.Claims
|
||||||
require.Equal(t, expectedUsername, idTokenClaims[oidc.DownstreamUsernameClaim])
|
require.Equal(t, expectedUsername, idTokenClaims["username"])
|
||||||
|
|
||||||
if expectedGroups == nil {
|
if expectedGroups == nil {
|
||||||
require.Nil(t, idTokenClaims[oidc.DownstreamGroupsClaim])
|
require.Nil(t, idTokenClaims["groups"])
|
||||||
} else {
|
} else {
|
||||||
// The groups claim in the file ends up as an []interface{}, so adjust our expectation to match.
|
// The groups claim in the file ends up as an []interface{}, so adjust our expectation to match.
|
||||||
expectedGroupsAsEmptyInterfaces := make([]interface{}, 0, len(expectedGroups))
|
expectedGroupsAsEmptyInterfaces := make([]interface{}, 0, len(expectedGroups))
|
||||||
for _, g := range expectedGroups {
|
for _, g := range expectedGroups {
|
||||||
expectedGroupsAsEmptyInterfaces = append(expectedGroupsAsEmptyInterfaces, g)
|
expectedGroupsAsEmptyInterfaces = append(expectedGroupsAsEmptyInterfaces, g)
|
||||||
}
|
}
|
||||||
require.ElementsMatch(t, expectedGroupsAsEmptyInterfaces, idTokenClaims[oidc.DownstreamGroupsClaim])
|
require.ElementsMatch(t, expectedGroupsAsEmptyInterfaces, idTokenClaims["groups"])
|
||||||
}
|
}
|
||||||
|
|
||||||
expectedGroupsPlusAuthenticated := append([]string{}, expectedGroups...)
|
expectedGroupsPlusAuthenticated := append([]string{}, expectedGroups...)
|
||||||
|
@ -502,11 +502,11 @@ func requireWellKnownEndpointIsWorking(t *testing.T, supervisorScheme, superviso
|
|||||||
"token_endpoint": "%s/oauth2/token",
|
"token_endpoint": "%s/oauth2/token",
|
||||||
"token_endpoint_auth_methods_supported": ["client_secret_basic"],
|
"token_endpoint_auth_methods_supported": ["client_secret_basic"],
|
||||||
"jwks_uri": "%s/jwks.json",
|
"jwks_uri": "%s/jwks.json",
|
||||||
"scopes_supported": ["openid", "offline"],
|
"scopes_supported": ["openid", "offline_access", "pinniped:request-audience", "username", "groups"],
|
||||||
"response_types_supported": ["code"],
|
"response_types_supported": ["code"],
|
||||||
"response_modes_supported": ["query", "form_post"],
|
"response_modes_supported": ["query", "form_post"],
|
||||||
"code_challenge_methods_supported": ["S256"],
|
"code_challenge_methods_supported": ["S256"],
|
||||||
"claims_supported": ["groups"],
|
"claims_supported": ["username", "groups"],
|
||||||
"discovery.supervisor.pinniped.dev/v1alpha1": {"pinniped_identity_providers_endpoint": "%s/v1alpha1/pinniped_identity_providers"},
|
"discovery.supervisor.pinniped.dev/v1alpha1": {"pinniped_identity_providers_endpoint": "%s/v1alpha1/pinniped_identity_providers"},
|
||||||
"subject_types_supported": ["public"],
|
"subject_types_supported": ["public"],
|
||||||
"id_token_signing_alg_values_supported": ["ES256"]
|
"id_token_signing_alg_values_supported": ["ES256"]
|
||||||
|
@ -207,6 +207,9 @@ func TestSupervisorLogin_Browser(t *testing.T) {
|
|||||||
|
|
||||||
// The scopes to request from the authorization endpoint. Defaults will be used when not specified.
|
// The scopes to request from the authorization endpoint. Defaults will be used when not specified.
|
||||||
downstreamScopes []string
|
downstreamScopes []string
|
||||||
|
// The scopes to want granted from the authorization endpoint. Defaults to the downstreamScopes value when not,
|
||||||
|
// specified, i.e. by default it expects that all requested scopes were granted.
|
||||||
|
wantDownstreamScopes []string
|
||||||
|
|
||||||
// When we want the localhost callback to have never happened, then the flow will stop there. The login was
|
// When we want the localhost callback to have never happened, then the flow will stop there. The login was
|
||||||
// unable to finish so there is nothing to assert about what should have happened with the callback, and there
|
// unable to finish so there is nothing to assert about what should have happened with the callback, and there
|
||||||
@ -218,6 +221,7 @@ func TestSupervisorLogin_Browser(t *testing.T) {
|
|||||||
// The expected ID token subject claim value as a regexp, for the original ID token and the refreshed ID token.
|
// The expected ID token subject claim value as a regexp, for the original ID token and the refreshed ID token.
|
||||||
wantDownstreamIDTokenSubjectToMatch string
|
wantDownstreamIDTokenSubjectToMatch string
|
||||||
// The expected ID token username claim value as a regexp, for the original ID token and the refreshed ID token.
|
// The expected ID token username claim value as a regexp, for the original ID token and the refreshed ID token.
|
||||||
|
// This function should return an empty string when there should be no username claim in the ID tokens.
|
||||||
wantDownstreamIDTokenUsernameToMatch func(username string) string
|
wantDownstreamIDTokenUsernameToMatch func(username string) string
|
||||||
// The expected ID token groups claim value, for the original ID token and the refreshed ID token.
|
// The expected ID token groups claim value, for the original ID token and the refreshed ID token.
|
||||||
wantDownstreamIDTokenGroups []string
|
wantDownstreamIDTokenGroups []string
|
||||||
@ -240,7 +244,8 @@ func TestSupervisorLogin_Browser(t *testing.T) {
|
|||||||
wantTokenExchangeResponse func(t *testing.T, status int, body string)
|
wantTokenExchangeResponse func(t *testing.T, status int, body string)
|
||||||
|
|
||||||
// Optionally edit the refresh session data between the initial login and the first refresh,
|
// Optionally edit the refresh session data between the initial login and the first refresh,
|
||||||
// which is still expected to succeed after these edits.
|
// which is still expected to succeed after these edits. Returns the group memberships expected after the
|
||||||
|
// refresh is performed.
|
||||||
editRefreshSessionDataWithoutBreaking func(t *testing.T, sessionData *psession.PinnipedSession, idpName, username string) []string
|
editRefreshSessionDataWithoutBreaking func(t *testing.T, sessionData *psession.PinnipedSession, idpName, username string) []string
|
||||||
// Optionally either revoke the user's session on the upstream provider, or manipulate the user's session
|
// Optionally either revoke the user's session on the upstream provider, or manipulate the user's session
|
||||||
// data in such a way that it should cause the next upstream refresh attempt to fail.
|
// data in such a way that it should cause the next upstream refresh attempt to fail.
|
||||||
@ -278,8 +283,8 @@ func TestSupervisorLogin_Browser(t *testing.T) {
|
|||||||
},
|
},
|
||||||
requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowOIDC,
|
requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowOIDC,
|
||||||
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _, _ string) {
|
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _, _ string) {
|
||||||
fositeSessionData := pinnipedSession.Fosite
|
customSessionData := pinnipedSession.Custom
|
||||||
fositeSessionData.Claims.Extra["username"] = "some-incorrect-username"
|
customSessionData.Username = "some-incorrect-username"
|
||||||
},
|
},
|
||||||
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+",
|
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+",
|
||||||
wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Username) + "$" },
|
wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Username) + "$" },
|
||||||
@ -321,8 +326,8 @@ func TestSupervisorLogin_Browser(t *testing.T) {
|
|||||||
},
|
},
|
||||||
requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowOIDC,
|
requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowOIDC,
|
||||||
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _, _ string) {
|
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _, _ string) {
|
||||||
fositeSessionData := pinnipedSession.Fosite
|
customSessionData := pinnipedSession.Custom
|
||||||
fositeSessionData.Claims.Extra["username"] = "some-incorrect-username"
|
customSessionData.Username = "some-incorrect-username"
|
||||||
},
|
},
|
||||||
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+",
|
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+",
|
||||||
wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Username) + "$" },
|
wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Username) + "$" },
|
||||||
@ -400,13 +405,14 @@ func TestSupervisorLogin_Browser(t *testing.T) {
|
|||||||
wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs,
|
wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "ldap without requesting groups scope",
|
name: "ldap without requesting username and groups scope gets them anyway for pinniped-cli for backwards compatibility with old CLIs",
|
||||||
maybeSkip: skipLDAPTests,
|
maybeSkip: skipLDAPTests,
|
||||||
createIDP: func(t *testing.T) string {
|
createIDP: func(t *testing.T) string {
|
||||||
idp, _ := createLDAPIdentityProvider(t, nil)
|
idp, _ := createLDAPIdentityProvider(t, nil)
|
||||||
return idp.Name
|
return idp.Name
|
||||||
},
|
},
|
||||||
downstreamScopes: []string{"openid", "pinniped:request-audience", "offline_access"},
|
downstreamScopes: []string{"openid", "pinniped:request-audience", "offline_access"},
|
||||||
|
wantDownstreamScopes: []string{"openid", "pinniped:request-audience", "offline_access", "username", "groups"},
|
||||||
requestAuthorization: func(t *testing.T, _, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) {
|
requestAuthorization: func(t *testing.T, _, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) {
|
||||||
requestAuthorizationUsingCLIPasswordFlow(t,
|
requestAuthorizationUsingCLIPasswordFlow(t,
|
||||||
downstreamAuthorizeURL,
|
downstreamAuthorizeURL,
|
||||||
@ -426,10 +432,10 @@ func TestSupervisorLogin_Browser(t *testing.T) {
|
|||||||
wantDownstreamIDTokenUsernameToMatch: func(_ string) string {
|
wantDownstreamIDTokenUsernameToMatch: func(_ string) string {
|
||||||
return "^" + regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserMailAttributeValue) + "$"
|
return "^" + regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserMailAttributeValue) + "$"
|
||||||
},
|
},
|
||||||
wantDownstreamIDTokenGroups: []string{},
|
wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "oidc without requesting groups scope",
|
name: "oidc without requesting username and groups scope gets them anyway for pinniped-cli for backwards compatibility with old CLIs",
|
||||||
maybeSkip: skipNever,
|
maybeSkip: skipNever,
|
||||||
createIDP: func(t *testing.T) string {
|
createIDP: func(t *testing.T) string {
|
||||||
spec := basicOIDCIdentityProviderSpec()
|
spec := basicOIDCIdentityProviderSpec()
|
||||||
@ -443,10 +449,11 @@ func TestSupervisorLogin_Browser(t *testing.T) {
|
|||||||
return testlib.CreateTestOIDCIdentityProvider(t, spec, idpv1alpha1.PhaseReady).Name
|
return testlib.CreateTestOIDCIdentityProvider(t, spec, idpv1alpha1.PhaseReady).Name
|
||||||
},
|
},
|
||||||
downstreamScopes: []string{"openid", "pinniped:request-audience", "offline_access"},
|
downstreamScopes: []string{"openid", "pinniped:request-audience", "offline_access"},
|
||||||
|
wantDownstreamScopes: []string{"openid", "pinniped:request-audience", "offline_access", "username", "groups"},
|
||||||
requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowOIDC,
|
requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowOIDC,
|
||||||
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+",
|
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+",
|
||||||
wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Username) + "$" },
|
wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Username) + "$" },
|
||||||
wantDownstreamIDTokenGroups: nil,
|
wantDownstreamIDTokenGroups: env.SupervisorUpstreamOIDC.ExpectedGroups,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "ldap with browser flow",
|
name: "ldap with browser flow",
|
||||||
@ -649,8 +656,7 @@ func TestSupervisorLogin_Browser(t *testing.T) {
|
|||||||
customSessionData := pinnipedSession.Custom
|
customSessionData := pinnipedSession.Custom
|
||||||
require.Equal(t, psession.ProviderTypeLDAP, customSessionData.ProviderType)
|
require.Equal(t, psession.ProviderTypeLDAP, customSessionData.ProviderType)
|
||||||
require.NotEmpty(t, customSessionData.LDAP.UserDN)
|
require.NotEmpty(t, customSessionData.LDAP.UserDN)
|
||||||
fositeSessionData := pinnipedSession.Fosite
|
customSessionData.Username = "not-the-same"
|
||||||
fositeSessionData.Claims.Extra["username"] = "not-the-same"
|
|
||||||
},
|
},
|
||||||
// the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute
|
// the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute
|
||||||
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(
|
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(
|
||||||
@ -829,8 +835,7 @@ func TestSupervisorLogin_Browser(t *testing.T) {
|
|||||||
customSessionData := pinnipedSession.Custom
|
customSessionData := pinnipedSession.Custom
|
||||||
require.Equal(t, psession.ProviderTypeActiveDirectory, customSessionData.ProviderType)
|
require.Equal(t, psession.ProviderTypeActiveDirectory, customSessionData.ProviderType)
|
||||||
require.NotEmpty(t, customSessionData.ActiveDirectory.UserDN)
|
require.NotEmpty(t, customSessionData.ActiveDirectory.UserDN)
|
||||||
fositeSessionData := pinnipedSession.Fosite
|
customSessionData.Username = "not-the-same"
|
||||||
fositeSessionData.Claims.Extra["username"] = "not-the-same"
|
|
||||||
},
|
},
|
||||||
// the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute
|
// the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute
|
||||||
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(
|
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(
|
||||||
@ -1284,7 +1289,7 @@ func TestSupervisorLogin_Browser(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "oidc upstream with downstream dynamic client happy path",
|
name: "oidc upstream with downstream dynamic client happy path, requesting all scopes",
|
||||||
maybeSkip: skipNever,
|
maybeSkip: skipNever,
|
||||||
createIDP: func(t *testing.T) string {
|
createIDP: func(t *testing.T) string {
|
||||||
return testlib.CreateTestOIDCIdentityProvider(t, basicOIDCIdentityProviderSpec(), idpv1alpha1.PhaseReady).Name
|
return testlib.CreateTestOIDCIdentityProvider(t, basicOIDCIdentityProviderSpec(), idpv1alpha1.PhaseReady).Name
|
||||||
@ -1301,9 +1306,10 @@ func TestSupervisorLogin_Browser(t *testing.T) {
|
|||||||
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+",
|
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+",
|
||||||
// the ID token Username should include the upstream user ID after the upstream issuer name
|
// the ID token Username should include the upstream user ID after the upstream issuer name
|
||||||
wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+" },
|
wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+" },
|
||||||
|
wantDownstreamIDTokenGroups: env.SupervisorUpstreamOIDC.ExpectedGroups,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "ldap upstream with downstream dynamic client happy path",
|
name: "ldap upstream with downstream dynamic client happy path, requesting all scopes",
|
||||||
maybeSkip: skipLDAPTests,
|
maybeSkip: skipLDAPTests,
|
||||||
createIDP: func(t *testing.T) string {
|
createIDP: func(t *testing.T) string {
|
||||||
idp, _ := createLDAPIdentityProvider(t, nil)
|
idp, _ := createLDAPIdentityProvider(t, nil)
|
||||||
@ -1334,6 +1340,237 @@ func TestSupervisorLogin_Browser(t *testing.T) {
|
|||||||
},
|
},
|
||||||
wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs,
|
wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "ldap upstream with downstream dynamic client when dynamic client is not allowed to use the token exchange grant type, causes token exchange error",
|
||||||
|
maybeSkip: skipLDAPTests,
|
||||||
|
createIDP: func(t *testing.T) string {
|
||||||
|
idp, _ := createLDAPIdentityProvider(t, nil)
|
||||||
|
return idp.Name
|
||||||
|
},
|
||||||
|
createOIDCClient: func(t *testing.T, callbackURL string) (string, string) {
|
||||||
|
return testlib.CreateOIDCClient(t, configv1alpha1.OIDCClientSpec{
|
||||||
|
AllowedRedirectURIs: []configv1alpha1.RedirectURI{configv1alpha1.RedirectURI(callbackURL)},
|
||||||
|
AllowedGrantTypes: []configv1alpha1.GrantType{"authorization_code", "refresh_token"}, // token exchange grant type not allowed
|
||||||
|
AllowedScopes: []configv1alpha1.Scope{"openid", "offline_access", "username", "groups"}, // a validation requires that we also disallow the pinniped:request-audience scope
|
||||||
|
}, configv1alpha1.PhaseReady)
|
||||||
|
},
|
||||||
|
testUser: func(t *testing.T) (string, string) {
|
||||||
|
// return the username and password of the existing user that we want to use for this test
|
||||||
|
return env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login
|
||||||
|
env.SupervisorUpstreamLDAP.TestUserPassword // password to present to server during login
|
||||||
|
},
|
||||||
|
downstreamScopes: []string{"openid", "offline_access", "username", "groups"}, // does not request (or expect) pinniped:request-audience token exchange scope
|
||||||
|
requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowLDAP,
|
||||||
|
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
|
||||||
|
wantDownstreamIDTokenUsernameToMatch: func(_ string) string {
|
||||||
|
return "^" + regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserMailAttributeValue) + "$"
|
||||||
|
},
|
||||||
|
wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs,
|
||||||
|
wantTokenExchangeResponse: func(t *testing.T, status int, body string) { // can't do token exchanges without the token exchange grant type
|
||||||
|
require.Equal(t, http.StatusBadRequest, status)
|
||||||
|
require.Equal(t,
|
||||||
|
`{"error":"unauthorized_client","error_description":"The client is not authorized to request a token using this method. `+
|
||||||
|
`The OAuth 2.0 Client is not allowed to use token exchange grant 'urn:ietf:params:oauth:grant-type:token-exchange'."}`,
|
||||||
|
body)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ldap upstream with downstream dynamic client when dynamic client that does not request the pinniped:request-audience scope, causes token exchange error",
|
||||||
|
maybeSkip: skipLDAPTests,
|
||||||
|
createIDP: func(t *testing.T) string {
|
||||||
|
idp, _ := createLDAPIdentityProvider(t, nil)
|
||||||
|
return idp.Name
|
||||||
|
},
|
||||||
|
createOIDCClient: func(t *testing.T, callbackURL string) (string, string) {
|
||||||
|
return testlib.CreateOIDCClient(t, configv1alpha1.OIDCClientSpec{
|
||||||
|
AllowedRedirectURIs: []configv1alpha1.RedirectURI{configv1alpha1.RedirectURI(callbackURL)},
|
||||||
|
AllowedGrantTypes: []configv1alpha1.GrantType{"authorization_code", "urn:ietf:params:oauth:grant-type:token-exchange", "refresh_token"},
|
||||||
|
AllowedScopes: []configv1alpha1.Scope{"openid", "offline_access", "pinniped:request-audience", "username", "groups"},
|
||||||
|
}, configv1alpha1.PhaseReady)
|
||||||
|
},
|
||||||
|
testUser: func(t *testing.T) (string, string) {
|
||||||
|
// return the username and password of the existing user that we want to use for this test
|
||||||
|
return env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login
|
||||||
|
env.SupervisorUpstreamLDAP.TestUserPassword // password to present to server during login
|
||||||
|
},
|
||||||
|
downstreamScopes: []string{"openid", "offline_access", "username", "groups"}, // does not request (or expect) pinniped:request-audience token exchange scope
|
||||||
|
requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowLDAP,
|
||||||
|
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
|
||||||
|
wantDownstreamIDTokenUsernameToMatch: func(_ string) string {
|
||||||
|
return "^" + regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserMailAttributeValue) + "$"
|
||||||
|
},
|
||||||
|
wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs,
|
||||||
|
wantTokenExchangeResponse: func(t *testing.T, status int, body string) { // can't do token exchanges without the pinniped:request-audience token exchange scope
|
||||||
|
require.Equal(t, http.StatusForbidden, status)
|
||||||
|
require.Equal(t,
|
||||||
|
`{"error":"access_denied","error_description":"The resource owner or authorization server denied the request. `+
|
||||||
|
`missing the 'pinniped:request-audience' scope"}`,
|
||||||
|
body)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ldap upstream with downstream dynamic client when dynamic client is not allowed to request username but requests username anyway, causes authorization error",
|
||||||
|
maybeSkip: skipLDAPTests,
|
||||||
|
createIDP: func(t *testing.T) string {
|
||||||
|
idp, _ := createLDAPIdentityProvider(t, nil)
|
||||||
|
return idp.Name
|
||||||
|
},
|
||||||
|
createOIDCClient: func(t *testing.T, callbackURL string) (string, string) {
|
||||||
|
return testlib.CreateOIDCClient(t, configv1alpha1.OIDCClientSpec{
|
||||||
|
AllowedRedirectURIs: []configv1alpha1.RedirectURI{configv1alpha1.RedirectURI(callbackURL)},
|
||||||
|
AllowedGrantTypes: []configv1alpha1.GrantType{"authorization_code", "refresh_token"}, // token exchange not allowed (required to exclude groups scope)
|
||||||
|
AllowedScopes: []configv1alpha1.Scope{"openid", "offline_access", "groups"}, // username not allowed
|
||||||
|
}, configv1alpha1.PhaseReady)
|
||||||
|
},
|
||||||
|
testUser: func(t *testing.T) (string, string) {
|
||||||
|
// return the username and password of the existing user that we want to use for this test
|
||||||
|
return env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login
|
||||||
|
env.SupervisorUpstreamLDAP.TestUserPassword // password to present to server during login
|
||||||
|
},
|
||||||
|
downstreamScopes: []string{"openid", "offline_access", "username"}, // request username, even though the client is not allowed to request it
|
||||||
|
// Should have been immediately redirected back to the local callback server with an error in this case,
|
||||||
|
// since we requested a scope that the client is not allowed to request. The login UI page is never shown.
|
||||||
|
requestAuthorization: requestAuthorizationAndExpectImmediateRedirectToCallback,
|
||||||
|
wantAuthorizationErrorDescription: "The requested scope is invalid, unknown, or malformed. The OAuth 2.0 Client is not allowed to request scope 'username'.",
|
||||||
|
wantAuthorizationErrorType: "invalid_scope",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ldap upstream with downstream dynamic client when dynamic client is not allowed to request groups but requests groups anyway, causes authorization error",
|
||||||
|
maybeSkip: skipLDAPTests,
|
||||||
|
createIDP: func(t *testing.T) string {
|
||||||
|
idp, _ := createLDAPIdentityProvider(t, nil)
|
||||||
|
return idp.Name
|
||||||
|
},
|
||||||
|
createOIDCClient: func(t *testing.T, callbackURL string) (string, string) {
|
||||||
|
return testlib.CreateOIDCClient(t, configv1alpha1.OIDCClientSpec{
|
||||||
|
AllowedRedirectURIs: []configv1alpha1.RedirectURI{configv1alpha1.RedirectURI(callbackURL)},
|
||||||
|
AllowedGrantTypes: []configv1alpha1.GrantType{"authorization_code", "refresh_token"}, // token exchange not allowed (required to exclude groups scope)
|
||||||
|
AllowedScopes: []configv1alpha1.Scope{"openid", "offline_access", "username"}, // groups not allowed
|
||||||
|
}, configv1alpha1.PhaseReady)
|
||||||
|
},
|
||||||
|
testUser: func(t *testing.T) (string, string) {
|
||||||
|
// return the username and password of the existing user that we want to use for this test
|
||||||
|
return env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login
|
||||||
|
env.SupervisorUpstreamLDAP.TestUserPassword // password to present to server during login
|
||||||
|
},
|
||||||
|
downstreamScopes: []string{"openid", "offline_access", "groups"}, // request groups, even though the client is not allowed to request it
|
||||||
|
// Should have been immediately redirected back to the local callback server with an error in this case,
|
||||||
|
// since we requested a scope that the client is not allowed to request. The login UI page is never shown.
|
||||||
|
requestAuthorization: requestAuthorizationAndExpectImmediateRedirectToCallback,
|
||||||
|
wantAuthorizationErrorDescription: "The requested scope is invalid, unknown, or malformed. The OAuth 2.0 Client is not allowed to request scope 'groups'.",
|
||||||
|
wantAuthorizationErrorType: "invalid_scope",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ldap upstream with downstream dynamic client when dynamic client does not request groups happy path",
|
||||||
|
maybeSkip: skipLDAPTests,
|
||||||
|
createIDP: func(t *testing.T) string {
|
||||||
|
idp, _ := createLDAPIdentityProvider(t, nil)
|
||||||
|
return idp.Name
|
||||||
|
},
|
||||||
|
createOIDCClient: func(t *testing.T, callbackURL string) (string, string) {
|
||||||
|
return testlib.CreateOIDCClient(t, configv1alpha1.OIDCClientSpec{
|
||||||
|
AllowedRedirectURIs: []configv1alpha1.RedirectURI{configv1alpha1.RedirectURI(callbackURL)},
|
||||||
|
AllowedGrantTypes: []configv1alpha1.GrantType{"authorization_code", "urn:ietf:params:oauth:grant-type:token-exchange", "refresh_token"},
|
||||||
|
AllowedScopes: []configv1alpha1.Scope{"openid", "offline_access", "pinniped:request-audience", "username", "groups"},
|
||||||
|
}, configv1alpha1.PhaseReady)
|
||||||
|
},
|
||||||
|
testUser: func(t *testing.T) (string, string) {
|
||||||
|
// return the username and password of the existing user that we want to use for this test
|
||||||
|
return env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login
|
||||||
|
env.SupervisorUpstreamLDAP.TestUserPassword // password to present to server during login
|
||||||
|
},
|
||||||
|
downstreamScopes: []string{"openid", "pinniped:request-audience", "offline_access", "username"}, // do not request (or expect) groups
|
||||||
|
requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowLDAP,
|
||||||
|
// the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute
|
||||||
|
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(
|
||||||
|
"ldaps://"+env.SupervisorUpstreamLDAP.Host+
|
||||||
|
"?base="+url.QueryEscape(env.SupervisorUpstreamLDAP.UserSearchBase)+
|
||||||
|
"&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)),
|
||||||
|
) + "$",
|
||||||
|
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
|
||||||
|
wantDownstreamIDTokenUsernameToMatch: func(_ string) string {
|
||||||
|
return "^" + regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserMailAttributeValue) + "$"
|
||||||
|
},
|
||||||
|
wantDownstreamIDTokenGroups: nil, // did not request groups, so should not have got any groups
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ldap upstream with downstream dynamic client when dynamic client does not request username, is allowed to auth but cannot do token exchange",
|
||||||
|
maybeSkip: skipLDAPTests,
|
||||||
|
createIDP: func(t *testing.T) string {
|
||||||
|
idp, _ := createLDAPIdentityProvider(t, nil)
|
||||||
|
return idp.Name
|
||||||
|
},
|
||||||
|
createOIDCClient: func(t *testing.T, callbackURL string) (string, string) {
|
||||||
|
return testlib.CreateOIDCClient(t, configv1alpha1.OIDCClientSpec{
|
||||||
|
AllowedRedirectURIs: []configv1alpha1.RedirectURI{configv1alpha1.RedirectURI(callbackURL)},
|
||||||
|
AllowedGrantTypes: []configv1alpha1.GrantType{"authorization_code", "urn:ietf:params:oauth:grant-type:token-exchange", "refresh_token"},
|
||||||
|
AllowedScopes: []configv1alpha1.Scope{"openid", "offline_access", "pinniped:request-audience", "username", "groups"},
|
||||||
|
}, configv1alpha1.PhaseReady)
|
||||||
|
},
|
||||||
|
testUser: func(t *testing.T) (string, string) {
|
||||||
|
// return the username and password of the existing user that we want to use for this test
|
||||||
|
return env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login
|
||||||
|
env.SupervisorUpstreamLDAP.TestUserPassword // password to present to server during login
|
||||||
|
},
|
||||||
|
downstreamScopes: []string{"openid", "pinniped:request-audience", "offline_access", "groups"}, // do not request (or expect) username
|
||||||
|
requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowLDAP,
|
||||||
|
// the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute
|
||||||
|
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(
|
||||||
|
"ldaps://"+env.SupervisorUpstreamLDAP.Host+
|
||||||
|
"?base="+url.QueryEscape(env.SupervisorUpstreamLDAP.UserSearchBase)+
|
||||||
|
"&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)),
|
||||||
|
) + "$",
|
||||||
|
wantDownstreamIDTokenUsernameToMatch: func(_ string) string {
|
||||||
|
return "" // username should not exist as a claim since we did not request it
|
||||||
|
},
|
||||||
|
wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs,
|
||||||
|
wantTokenExchangeResponse: func(t *testing.T, status int, body string) { // can't do token exchanges without a username
|
||||||
|
require.Equal(t, http.StatusForbidden, status)
|
||||||
|
require.Equal(t,
|
||||||
|
`{"error":"access_denied","error_description":"The resource owner or authorization server denied the request. `+
|
||||||
|
`No username found in session. Ensure that the 'username' scope was requested and granted at the authorization endpoint."}`,
|
||||||
|
body)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ldap upstream with downstream dynamic client when dynamic client is not allowed to request username or groups and does not request them, is allowed to auth but cannot do token exchange",
|
||||||
|
maybeSkip: skipLDAPTests,
|
||||||
|
createIDP: func(t *testing.T) string {
|
||||||
|
idp, _ := createLDAPIdentityProvider(t, nil)
|
||||||
|
return idp.Name
|
||||||
|
},
|
||||||
|
createOIDCClient: func(t *testing.T, callbackURL string) (string, string) {
|
||||||
|
return testlib.CreateOIDCClient(t, configv1alpha1.OIDCClientSpec{
|
||||||
|
AllowedRedirectURIs: []configv1alpha1.RedirectURI{configv1alpha1.RedirectURI(callbackURL)},
|
||||||
|
AllowedGrantTypes: []configv1alpha1.GrantType{"authorization_code", "refresh_token"},
|
||||||
|
AllowedScopes: []configv1alpha1.Scope{"openid", "offline_access"}, // validations require that when username/groups are excluded, then token exchange must also not be allowed
|
||||||
|
}, configv1alpha1.PhaseReady)
|
||||||
|
},
|
||||||
|
testUser: func(t *testing.T) (string, string) {
|
||||||
|
// return the username and password of the existing user that we want to use for this test
|
||||||
|
return env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login
|
||||||
|
env.SupervisorUpstreamLDAP.TestUserPassword // password to present to server during login
|
||||||
|
},
|
||||||
|
downstreamScopes: []string{"openid", "offline_access"}, // do not request (or expect) pinniped:request-audience or username or groups
|
||||||
|
requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowLDAP,
|
||||||
|
// the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute
|
||||||
|
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(
|
||||||
|
"ldaps://"+env.SupervisorUpstreamLDAP.Host+
|
||||||
|
"?base="+url.QueryEscape(env.SupervisorUpstreamLDAP.UserSearchBase)+
|
||||||
|
"&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)),
|
||||||
|
) + "$",
|
||||||
|
wantDownstreamIDTokenUsernameToMatch: func(_ string) string {
|
||||||
|
return "" // username should not exist as a claim since we did not request it
|
||||||
|
},
|
||||||
|
wantDownstreamIDTokenGroups: nil, // did not request groups, so should not have got any groups
|
||||||
|
wantTokenExchangeResponse: func(t *testing.T, status int, body string) { // can't do token exchanges without the token exchange grant type
|
||||||
|
require.Equal(t, http.StatusBadRequest, status)
|
||||||
|
require.Equal(t,
|
||||||
|
`{"error":"unauthorized_client","error_description":"The client is not authorized to request a token using this method. `+
|
||||||
|
`The OAuth 2.0 Client is not allowed to use token exchange grant 'urn:ietf:params:oauth:grant-type:token-exchange'."}`,
|
||||||
|
body)
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "active directory with all default options with downstream dynamic client happy path",
|
name: "active directory with all default options with downstream dynamic client happy path",
|
||||||
maybeSkip: skipActiveDirectoryTests,
|
maybeSkip: skipActiveDirectoryTests,
|
||||||
@ -1411,6 +1648,7 @@ func TestSupervisorLogin_Browser(t *testing.T) {
|
|||||||
tt.createOIDCClient,
|
tt.createOIDCClient,
|
||||||
tt.downstreamScopes,
|
tt.downstreamScopes,
|
||||||
tt.requestTokenExchangeAud,
|
tt.requestTokenExchangeAud,
|
||||||
|
tt.wantDownstreamScopes,
|
||||||
tt.wantLocalhostCallbackToNeverHappen,
|
tt.wantLocalhostCallbackToNeverHappen,
|
||||||
tt.wantDownstreamIDTokenSubjectToMatch,
|
tt.wantDownstreamIDTokenSubjectToMatch,
|
||||||
tt.wantDownstreamIDTokenUsernameToMatch,
|
tt.wantDownstreamIDTokenUsernameToMatch,
|
||||||
@ -1552,6 +1790,7 @@ func testSupervisorLogin(
|
|||||||
createOIDCClient func(t *testing.T, callbackURL string) (string, string),
|
createOIDCClient func(t *testing.T, callbackURL string) (string, string),
|
||||||
downstreamScopes []string,
|
downstreamScopes []string,
|
||||||
requestTokenExchangeAud string,
|
requestTokenExchangeAud string,
|
||||||
|
wantDownstreamScopes []string,
|
||||||
wantLocalhostCallbackToNeverHappen bool,
|
wantLocalhostCallbackToNeverHappen bool,
|
||||||
wantDownstreamIDTokenSubjectToMatch string,
|
wantDownstreamIDTokenSubjectToMatch string,
|
||||||
wantDownstreamIDTokenUsernameToMatch func(username string) string,
|
wantDownstreamIDTokenUsernameToMatch func(username string) string,
|
||||||
@ -1672,7 +1911,13 @@ func testSupervisorLogin(
|
|||||||
}, 30*time.Second, 200*time.Millisecond)
|
}, 30*time.Second, 200*time.Millisecond)
|
||||||
|
|
||||||
if downstreamScopes == nil {
|
if downstreamScopes == nil {
|
||||||
downstreamScopes = []string{"openid", "pinniped:request-audience", "offline_access", "groups"}
|
// By default, tests will request all the relevant groups.
|
||||||
|
downstreamScopes = []string{"openid", "pinniped:request-audience", "offline_access", "username", "groups"}
|
||||||
|
}
|
||||||
|
if wantDownstreamScopes == nil {
|
||||||
|
// By default, tests will want that all requested scopes were granted.
|
||||||
|
wantDownstreamScopes = make([]string, len(downstreamScopes))
|
||||||
|
copy(wantDownstreamScopes, downstreamScopes)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the OAuth2 configuration.
|
// Create the OAuth2 configuration.
|
||||||
@ -1728,14 +1973,14 @@ func testSupervisorLogin(
|
|||||||
if wantAuthorizationErrorType != "" {
|
if wantAuthorizationErrorType != "" {
|
||||||
errorDescription := callback.URL.Query().Get("error_description")
|
errorDescription := callback.URL.Query().Get("error_description")
|
||||||
errorType := callback.URL.Query().Get("error")
|
errorType := callback.URL.Query().Get("error")
|
||||||
require.Equal(t, errorDescription, wantAuthorizationErrorDescription)
|
require.Equal(t, wantAuthorizationErrorDescription, errorDescription)
|
||||||
require.Equal(t, errorType, wantAuthorizationErrorType)
|
require.Equal(t, wantAuthorizationErrorType, errorType)
|
||||||
// The authorization has failed, so can't continue the login flow, making this the end of the test case.
|
// The authorization has failed, so can't continue the login flow, making this the end of the test case.
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
require.Equal(t, stateParam.String(), callback.URL.Query().Get("state"))
|
require.Equal(t, stateParam.String(), callback.URL.Query().Get("state"))
|
||||||
require.ElementsMatch(t, downstreamScopes, strings.Split(callback.URL.Query().Get("scope"), " "))
|
require.ElementsMatch(t, wantDownstreamScopes, strings.Split(callback.URL.Query().Get("scope"), " "))
|
||||||
authcode := callback.URL.Query().Get("code")
|
authcode := callback.URL.Query().Get("code")
|
||||||
require.NotEmpty(t, authcode)
|
require.NotEmpty(t, authcode)
|
||||||
|
|
||||||
@ -1750,8 +1995,14 @@ func testSupervisorLogin(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
expectedIDTokenClaims := []string{"iss", "exp", "sub", "aud", "auth_time", "iat", "jti", "nonce", "rat", "username"}
|
|
||||||
if slices.Contains(downstreamScopes, "groups") {
|
expectedIDTokenClaims := []string{"iss", "exp", "sub", "aud", "auth_time", "iat", "jti", "nonce", "rat"}
|
||||||
|
if slices.Contains(wantDownstreamScopes, "username") {
|
||||||
|
// If the test wants the username scope to have been granted, then also expect the claim in the ID token.
|
||||||
|
expectedIDTokenClaims = append(expectedIDTokenClaims, "username")
|
||||||
|
}
|
||||||
|
if slices.Contains(wantDownstreamScopes, "groups") {
|
||||||
|
// If the test wants the groups scope to have been granted, then also expect the claim in the ID token.
|
||||||
expectedIDTokenClaims = append(expectedIDTokenClaims, "groups")
|
expectedIDTokenClaims = append(expectedIDTokenClaims, "groups")
|
||||||
}
|
}
|
||||||
verifyTokenResponse(t,
|
verifyTokenResponse(t,
|
||||||
@ -1764,7 +2015,7 @@ func testSupervisorLogin(
|
|||||||
}
|
}
|
||||||
doTokenExchange(t, requestTokenExchangeAud, &downstreamOAuth2Config, tokenResponse, httpClient, discovery, wantTokenExchangeResponse)
|
doTokenExchange(t, requestTokenExchangeAud, &downstreamOAuth2Config, tokenResponse, httpClient, discovery, wantTokenExchangeResponse)
|
||||||
|
|
||||||
refreshedGroups := wantDownstreamIDTokenGroups
|
wantRefreshedGroups := wantDownstreamIDTokenGroups
|
||||||
if editRefreshSessionDataWithoutBreaking != nil {
|
if editRefreshSessionDataWithoutBreaking != nil {
|
||||||
latestRefreshToken := tokenResponse.RefreshToken
|
latestRefreshToken := tokenResponse.RefreshToken
|
||||||
signatureOfLatestRefreshToken := getFositeDataSignature(t, latestRefreshToken)
|
signatureOfLatestRefreshToken := getFositeDataSignature(t, latestRefreshToken)
|
||||||
@ -1780,7 +2031,7 @@ func testSupervisorLogin(
|
|||||||
pinnipedSession, ok := storedRefreshSession.GetSession().(*psession.PinnipedSession)
|
pinnipedSession, ok := storedRefreshSession.GetSession().(*psession.PinnipedSession)
|
||||||
require.True(t, ok, "should have been able to cast session data to PinnipedSession")
|
require.True(t, ok, "should have been able to cast session data to PinnipedSession")
|
||||||
|
|
||||||
refreshedGroups = editRefreshSessionDataWithoutBreaking(t, pinnipedSession, idpName, username)
|
wantRefreshedGroups = editRefreshSessionDataWithoutBreaking(t, pinnipedSession, idpName, username)
|
||||||
|
|
||||||
// Then save the mutated Secret back to Kubernetes.
|
// Then save the mutated Secret back to Kubernetes.
|
||||||
// There is no update function, so delete and create again at the same name.
|
// There is no update function, so delete and create again at the same name.
|
||||||
@ -1793,13 +2044,18 @@ func testSupervisorLogin(
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// When refreshing, expect to get an "at_hash" claim, but no "nonce" claim.
|
// When refreshing, expect to get an "at_hash" claim, but no "nonce" claim.
|
||||||
expectRefreshedIDTokenClaims := []string{"iss", "exp", "sub", "aud", "auth_time", "iat", "jti", "rat", "username", "at_hash"}
|
expectRefreshedIDTokenClaims := []string{"iss", "exp", "sub", "aud", "auth_time", "iat", "jti", "rat", "at_hash"}
|
||||||
if slices.Contains(downstreamScopes, "groups") {
|
if slices.Contains(wantDownstreamScopes, "username") {
|
||||||
|
// If the test wants the username scope to have been granted, then also expect the claim in the refreshed ID token.
|
||||||
|
expectRefreshedIDTokenClaims = append(expectRefreshedIDTokenClaims, "username")
|
||||||
|
}
|
||||||
|
if slices.Contains(wantDownstreamScopes, "groups") {
|
||||||
|
// If the test wants the groups scope to have been granted, then also expect the claim in the refreshed ID token.
|
||||||
expectRefreshedIDTokenClaims = append(expectRefreshedIDTokenClaims, "groups")
|
expectRefreshedIDTokenClaims = append(expectRefreshedIDTokenClaims, "groups")
|
||||||
}
|
}
|
||||||
verifyTokenResponse(t,
|
verifyTokenResponse(t,
|
||||||
refreshedTokenResponse, discovery, downstreamOAuth2Config, "",
|
refreshedTokenResponse, discovery, downstreamOAuth2Config, "",
|
||||||
expectRefreshedIDTokenClaims, wantDownstreamIDTokenSubjectToMatch, wantDownstreamIDTokenUsernameToMatch(username), refreshedGroups)
|
expectRefreshedIDTokenClaims, wantDownstreamIDTokenSubjectToMatch, wantDownstreamIDTokenUsernameToMatch(username), wantRefreshedGroups)
|
||||||
|
|
||||||
require.NotEqual(t, tokenResponse.AccessToken, refreshedTokenResponse.AccessToken)
|
require.NotEqual(t, tokenResponse.AccessToken, refreshedTokenResponse.AccessToken)
|
||||||
require.NotEqual(t, tokenResponse.RefreshToken, refreshedTokenResponse.RefreshToken)
|
require.NotEqual(t, tokenResponse.RefreshToken, refreshedTokenResponse.RefreshToken)
|
||||||
@ -1892,8 +2148,11 @@ func verifyTokenResponse(
|
|||||||
}
|
}
|
||||||
require.ElementsMatch(t, expectedIDTokenClaims, idTokenClaimNames)
|
require.ElementsMatch(t, expectedIDTokenClaims, idTokenClaimNames)
|
||||||
|
|
||||||
// Check username claim of the ID token.
|
// Check username claim of the ID token, if one is expected. Asserting on the lack of a username claim is
|
||||||
|
// handled above where the full list of claims are asserted.
|
||||||
|
if wantDownstreamIDTokenUsernameToMatch != "" {
|
||||||
require.Regexp(t, wantDownstreamIDTokenUsernameToMatch, idTokenClaims["username"].(string))
|
require.Regexp(t, wantDownstreamIDTokenUsernameToMatch, idTokenClaims["username"].(string))
|
||||||
|
}
|
||||||
|
|
||||||
// Check the groups claim.
|
// Check the groups claim.
|
||||||
require.ElementsMatch(t, wantDownstreamIDTokenGroups, idTokenClaims["groups"])
|
require.ElementsMatch(t, wantDownstreamIDTokenGroups, idTokenClaims["groups"])
|
||||||
@ -1912,6 +2171,21 @@ func verifyTokenResponse(
|
|||||||
require.True(t, strings.HasPrefix(tokenResponse.RefreshToken, "pin_rt_"), "token %q did not have expected prefix 'pin_rt_'", tokenResponse.RefreshToken)
|
require.True(t, strings.HasPrefix(tokenResponse.RefreshToken, "pin_rt_"), "token %q did not have expected prefix 'pin_rt_'", tokenResponse.RefreshToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func requestAuthorizationAndExpectImmediateRedirectToCallback(t *testing.T, _, downstreamAuthorizeURL, downstreamCallbackURL, _, _ string, _ *http.Client) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
// Open the web browser and navigate to the downstream authorize URL.
|
||||||
|
page := browsertest.Open(t)
|
||||||
|
t.Logf("opening browser to downstream authorize URL %s", testlib.MaskTokens(downstreamAuthorizeURL))
|
||||||
|
require.NoError(t, page.Navigate(downstreamAuthorizeURL))
|
||||||
|
|
||||||
|
// Expect that it immediately redirects back to the callback, which is what happens for certain types of errors
|
||||||
|
// where it is not worth redirecting to the login UI page.
|
||||||
|
t.Logf("waiting for redirect to callback")
|
||||||
|
callbackURLPattern := regexp.MustCompile(`\A` + regexp.QuoteMeta(downstreamCallbackURL) + `\?.+\z`)
|
||||||
|
browsertest.WaitForURL(t, page, callbackURLPattern)
|
||||||
|
}
|
||||||
|
|
||||||
func requestAuthorizationUsingBrowserAuthcodeFlowOIDC(t *testing.T, _, downstreamAuthorizeURL, downstreamCallbackURL, _, _ string, httpClient *http.Client) {
|
func requestAuthorizationUsingBrowserAuthcodeFlowOIDC(t *testing.T, _, downstreamAuthorizeURL, downstreamCallbackURL, _, _ string, httpClient *http.Client) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
env := testlib.IntegrationEnv(t)
|
env := testlib.IntegrationEnv(t)
|
||||||
|
@ -19,7 +19,6 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
coreosoidc "github.com/coreos/go-oidc/v3/oidc"
|
|
||||||
"github.com/creack/pty"
|
"github.com/creack/pty"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
authorizationv1 "k8s.io/api/authorization/v1"
|
authorizationv1 "k8s.io/api/authorization/v1"
|
||||||
@ -173,7 +172,7 @@ func TestSupervisorWarnings_Browser(t *testing.T) {
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
// construct the cache key
|
// construct the cache key
|
||||||
downstreamScopes := []string{coreosoidc.ScopeOfflineAccess, coreosoidc.ScopeOpenID, "pinniped:request-audience", "groups"}
|
downstreamScopes := []string{"offline_access", "openid", "pinniped:request-audience", "groups"}
|
||||||
sort.Strings(downstreamScopes)
|
sort.Strings(downstreamScopes)
|
||||||
sessionCacheKey := oidcclient.SessionCacheKey{
|
sessionCacheKey := oidcclient.SessionCacheKey{
|
||||||
Issuer: downstream.Spec.Issuer,
|
Issuer: downstream.Spec.Issuer,
|
||||||
@ -481,7 +480,7 @@ func TestSupervisorWarnings_Browser(t *testing.T) {
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
// construct the cache key
|
// construct the cache key
|
||||||
downstreamScopes := []string{coreosoidc.ScopeOfflineAccess, coreosoidc.ScopeOpenID, "pinniped:request-audience", "groups"}
|
downstreamScopes := []string{"offline_access", "openid", "pinniped:request-audience", "groups"}
|
||||||
sort.Strings(downstreamScopes)
|
sort.Strings(downstreamScopes)
|
||||||
sessionCacheKey := oidcclient.SessionCacheKey{
|
sessionCacheKey := oidcclient.SessionCacheKey{
|
||||||
Issuer: downstream.Spec.Issuer,
|
Issuer: downstream.Spec.Issuer,
|
||||||
|
Loading…
Reference in New Issue
Block a user