Merge pull request #858 from vmware-tanzu/upstream_refresh

For OIDCIdenitityProviders perform an upstream refresh during downstream refresh
This commit is contained in:
Mo Khan 2021-10-25 12:32:35 -04:00 committed by GitHub
commit bdb199c53a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
67 changed files with 3957 additions and 959 deletions

View File

@ -20,7 +20,7 @@ const (
PhaseError OIDCIdentityProviderPhase = "Error" PhaseError OIDCIdentityProviderPhase = "Error"
) )
// Status of an OIDC identity provider. // OIDCIdentityProviderStatus is the status of an OIDC identity provider.
type OIDCIdentityProviderStatus struct { type OIDCIdentityProviderStatus struct {
// Phase summarizes the overall status of the OIDCIdentityProvider. // Phase summarizes the overall status of the OIDCIdentityProvider.
// +kubebuilder:default=Pending // +kubebuilder:default=Pending
@ -38,15 +38,62 @@ type OIDCIdentityProviderStatus struct {
// OIDCAuthorizationConfig provides information about how to form the OAuth2 authorization // OIDCAuthorizationConfig provides information about how to form the OAuth2 authorization
// request parameters. // request parameters.
type OIDCAuthorizationConfig struct { type OIDCAuthorizationConfig struct {
// AdditionalScopes are the scopes in addition to "openid" that will be requested as part of the authorization // additionalScopes are the additional scopes that will be requested from your OIDC provider in the authorization
// request flow with an OIDC identity provider. // request during an OIDC Authorization Code Flow and in the token request during a Resource Owner Password Credentials
// In the case of a Resource Owner Password Credentials Grant flow, AdditionalScopes are the scopes // Grant. Note that the "openid" scope will always be requested regardless of the value in this setting, since it is
// in addition to "openid" that will be requested as part of the token request (see also the allowPasswordGrant field). // always required according to the OIDC spec. By default, when this field is not set, the Supervisor will request
// By default, only the "openid" scope will be requested. // the following scopes: "openid", "offline_access", "email", and "profile". See
// https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims for a description of the "profile" and "email"
// scopes. See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess for a description of the
// "offline_access" scope. This default value may change in future versions of Pinniped as the standard evolves,
// or as common patterns used by providers who implement the standard in the ecosystem evolve.
// By setting this list to anything other than an empty list, you are overriding the
// default value, so you may wish to include some of "offline_access", "email", and "profile" in your override list.
// If you do not want any of these scopes to be requested, you may set this list to contain only "openid".
// Some OIDC providers may also require a scope to get access to the user's group membership, in which case you
// may wish to include it in this list. Sometimes the scope to request the user's group membership is called
// "groups", but unfortunately this is not specified in the OIDC standard.
// Generally speaking, you should include any scopes required to cause the appropriate claims to be the returned by
// your OIDC provider in the ID token or userinfo endpoint results for those claims which you would like to use in
// the oidcClaims settings to determine the usernames and group memberships of your Kubernetes users. See
// your OIDC provider's documentation for more information about what scopes are available to request claims.
// Additionally, the Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor
// from these authorization flows. For most OIDC providers, the scope required to receive refresh tokens will be
// "offline_access". See the documentation of your OIDC provider's authorization and token endpoints for its
// requirements for what to include in the request in order to receive a refresh token in the response, if anything.
// Note that it may be safe to send "offline_access" even to providers which do not require it, since the provider
// may ignore scopes that it does not understand or require (see
// https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). In the unusual case that you must avoid sending the
// "offline_access" scope, then you must override the default value of this setting. This is required if your OIDC
// provider will reject the request when it includes "offline_access" (e.g. GitLab's OIDC provider).
// +optional // +optional
AdditionalScopes []string `json:"additionalScopes,omitempty"` AdditionalScopes []string `json:"additionalScopes,omitempty"`
// AllowPasswordGrant, when true, will allow the use of OAuth 2.0's Resource Owner Password Credentials Grant // additionalAuthorizeParameters are extra query parameters that should be included in the authorize request to your
// OIDC provider in the authorization request during an OIDC Authorization Code Flow. By default, no extra
// parameters are sent. The standard parameters that will be sent are "response_type", "scope", "client_id",
// "state", "nonce", "code_challenge", "code_challenge_method", and "redirect_uri". These parameters cannot be
// included in this setting. Additionally, the "hd" parameter cannot be included in this setting at this time.
// The "hd" parameter is used by Google's OIDC provider to provide a hint as to which "hosted domain" the user
// should use during login. However, Pinniped does not yet support validating the hosted domain in the resulting
// ID token, so it is not yet safe to use this feature of Google's OIDC provider with Pinniped.
// This setting does not influence the parameters sent to the token endpoint in the Resource Owner Password
// Credentials Grant. The Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the
// Supervisor from the authorization flows. Some OIDC providers may require a certain value for the "prompt"
// parameter in order to properly request refresh tokens. See the documentation of your OIDC provider's
// authorization endpoint for its requirements for what to include in the request in order to receive a refresh
// token in the response, if anything. If your provider requires the prompt parameter to request a refresh token,
// then include it here. Also note that most providers also require a certain scope to be requested in order to
// receive refresh tokens. See the additionalScopes setting for more information about using scopes to request
// refresh tokens.
// +optional
// +patchMergeKey=name
// +patchStrategy=merge
// +listType=map
// +listMapKey=name
AdditionalAuthorizeParameters []Parameter `json:"additionalAuthorizeParameters,omitempty"`
// allowPasswordGrant, when true, will allow the use of OAuth 2.0's Resource Owner Password Credentials Grant
// (see https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) to authenticate to the OIDC provider using a // (see https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) to authenticate to the OIDC provider using a
// username and password without a web browser, in addition to the usual browser-based OIDC Authorization Code Flow. // username and password without a web browser, in addition to the usual browser-based OIDC Authorization Code Flow.
// The Resource Owner Password Credentials Grant is not officially part of the OIDC specification, so it may not be // The Resource Owner Password Credentials Grant is not officially part of the OIDC specification, so it may not be
@ -61,20 +108,34 @@ type OIDCAuthorizationConfig struct {
// Credentials Grant means that the Pinniped CLI and Pinniped Supervisor will directly handle your end users' passwords // Credentials Grant means that the Pinniped CLI and Pinniped Supervisor will directly handle your end users' passwords
// (similar to LDAPIdentityProvider), and you will not be able to require multi-factor authentication or use the other // (similar to LDAPIdentityProvider), and you will not be able to require multi-factor authentication or use the other
// web-based login features of your OIDC provider during Resource Owner Password Credentials Grant logins. // web-based login features of your OIDC provider during Resource Owner Password Credentials Grant logins.
// AllowPasswordGrant defaults to false. // allowPasswordGrant defaults to false.
// +optional // +optional
AllowPasswordGrant bool `json:"allowPasswordGrant,omitempty"` AllowPasswordGrant bool `json:"allowPasswordGrant,omitempty"`
} }
// Parameter is a key/value pair which represents a parameter in an HTTP request.
type Parameter struct {
// The name of the parameter. Required.
// +kubebuilder:validation:MinLength=1
Name string `json:"name"`
// The value of the parameter.
// +optional
Value string `json:"value,omitempty"`
}
// OIDCClaims provides a mapping from upstream claims into identities. // OIDCClaims provides a mapping from upstream claims into identities.
type OIDCClaims struct { type OIDCClaims struct {
// Groups provides the name of the token claim that will be used to ascertain the groups to which // Groups provides the name of the ID token claim or userinfo endpoint response claim that will be used to ascertain
// an identity belongs. // the groups to which an identity belongs. By default, the identities will not include any group memberships when
// this setting is not configured.
// +optional // +optional
Groups string `json:"groups"` Groups string `json:"groups"`
// Username provides the name of the token claim that will be used to ascertain an identity's // Username provides the name of the ID token claim or userinfo endpoint response claim that will be used to
// username. // ascertain an identity's username. When not set, the username will be an automatically constructed unique string
// which will include the issuer URL of your OIDC provider along with the value of the "sub" (subject) claim from
// the ID token.
// +optional // +optional
Username string `json:"username"` Username string `json:"username"`
} }
@ -89,7 +150,7 @@ type OIDCClient struct {
SecretName string `json:"secretName"` SecretName string `json:"secretName"`
} }
// Spec for configuring an OIDC identity provider. // OIDCIdentityProviderSpec is the spec for configuring an OIDC identity provider.
type OIDCIdentityProviderSpec struct { type OIDCIdentityProviderSpec struct {
// Issuer is the issuer URL of this OIDC identity provider, i.e., where to fetch // Issuer is the issuer URL of this OIDC identity provider, i.e., where to fetch
// /.well-known/openid-configuration. // /.well-known/openid-configuration.
@ -135,7 +196,7 @@ type OIDCIdentityProvider struct {
Status OIDCIdentityProviderStatus `json:"status,omitempty"` Status OIDCIdentityProviderStatus `json:"status,omitempty"`
} }
// List of OIDCIdentityProvider objects. // OIDCIdentityProviderList lists OIDCIdentityProvider objects.
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type OIDCIdentityProviderList struct { type OIDCIdentityProviderList struct {
metav1.TypeMeta `json:",inline"` metav1.TypeMeta `json:",inline"`

View File

@ -56,19 +56,103 @@ spec:
the OAuth2 authorization request parameters to be used with this the OAuth2 authorization request parameters to be used with this
OIDC identity provider. OIDC identity provider.
properties: properties:
additionalAuthorizeParameters:
description: additionalAuthorizeParameters are extra query parameters
that should be included in the authorize request to your OIDC
provider in the authorization request during an OIDC Authorization
Code Flow. By default, no extra parameters are sent. The standard
parameters that will be sent are "response_type", "scope", "client_id",
"state", "nonce", "code_challenge", "code_challenge_method",
and "redirect_uri". These parameters cannot be included in this
setting. Additionally, the "hd" parameter cannot be included
in this setting at this time. The "hd" parameter is used by
Google's OIDC provider to provide a hint as to which "hosted
domain" the user should use during login. However, Pinniped
does not yet support validating the hosted domain in the resulting
ID token, so it is not yet safe to use this feature of Google's
OIDC provider with Pinniped. This setting does not influence
the parameters sent to the token endpoint in the Resource Owner
Password Credentials Grant. The Pinniped Supervisor requires
that your OIDC provider returns refresh tokens to the Supervisor
from the authorization flows. Some OIDC providers may require
a certain value for the "prompt" parameter in order to properly
request refresh tokens. See the documentation of your OIDC provider's
authorization endpoint for its requirements for what to include
in the request in order to receive a refresh token in the response,
if anything. If your provider requires the prompt parameter
to request a refresh token, then include it here. Also note
that most providers also require a certain scope to be requested
in order to receive refresh tokens. See the additionalScopes
setting for more information about using scopes to request refresh
tokens.
items:
description: Parameter is a key/value pair which represents
a parameter in an HTTP request.
properties:
name:
description: The name of the parameter. Required.
minLength: 1
type: string
value:
description: The value of the parameter.
type: string
required:
- name
type: object
type: array
x-kubernetes-list-map-keys:
- name
x-kubernetes-list-type: map
additionalScopes: additionalScopes:
description: AdditionalScopes are the scopes in addition to "openid" description: 'additionalScopes are the additional scopes that
that will be requested as part of the authorization request will be requested from your OIDC provider in the authorization
flow with an OIDC identity provider. In the case of a Resource request during an OIDC Authorization Code Flow and in the token
Owner Password Credentials Grant flow, AdditionalScopes are request during a Resource Owner Password Credentials Grant.
the scopes in addition to "openid" that will be requested as Note that the "openid" scope will always be requested regardless
part of the token request (see also the allowPasswordGrant field). of the value in this setting, since it is always required according
By default, only the "openid" scope will be requested. to the OIDC spec. By default, when this field is not set, the
Supervisor will request the following scopes: "openid", "offline_access",
"email", and "profile". See https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims
for a description of the "profile" and "email" scopes. See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess
for a description of the "offline_access" scope. This default
value may change in future versions of Pinniped as the standard
evolves, or as common patterns used by providers who implement
the standard in the ecosystem evolve. By setting this list to
anything other than an empty list, you are overriding the default
value, so you may wish to include some of "offline_access",
"email", and "profile" in your override list. If you do not
want any of these scopes to be requested, you may set this list
to contain only "openid". Some OIDC providers may also require
a scope to get access to the user''s group membership, in which
case you may wish to include it in this list. Sometimes the
scope to request the user''s group membership is called "groups",
but unfortunately this is not specified in the OIDC standard.
Generally speaking, you should include any scopes required to
cause the appropriate claims to be the returned by your OIDC
provider in the ID token or userinfo endpoint results for those
claims which you would like to use in the oidcClaims settings
to determine the usernames and group memberships of your Kubernetes
users. See your OIDC provider''s documentation for more information
about what scopes are available to request claims. Additionally,
the Pinniped Supervisor requires that your OIDC provider returns
refresh tokens to the Supervisor from these authorization flows.
For most OIDC providers, the scope required to receive refresh
tokens will be "offline_access". See the documentation of your
OIDC provider''s authorization and token endpoints for its requirements
for what to include in the request in order to receive a refresh
token in the response, if anything. Note that it may be safe
to send "offline_access" even to providers which do not require
it, since the provider may ignore scopes that it does not understand
or require (see https://datatracker.ietf.org/doc/html/rfc6749#section-3.3).
In the unusual case that you must avoid sending the "offline_access"
scope, then you must override the default value of this setting.
This is required if your OIDC provider will reject the request
when it includes "offline_access" (e.g. GitLab''s OIDC provider).'
items: items:
type: string type: string
type: array type: array
allowPasswordGrant: allowPasswordGrant:
description: AllowPasswordGrant, when true, will allow the use description: allowPasswordGrant, when true, will allow the use
of OAuth 2.0's Resource Owner Password Credentials Grant (see of OAuth 2.0's Resource Owner Password Credentials Grant (see
https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) to https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) to
authenticate to the OIDC provider using a username and password authenticate to the OIDC provider using a username and password
@ -94,7 +178,7 @@ spec:
your end users' passwords (similar to LDAPIdentityProvider), your end users' passwords (similar to LDAPIdentityProvider),
and you will not be able to require multi-factor authentication and you will not be able to require multi-factor authentication
or use the other web-based login features of your OIDC provider or use the other web-based login features of your OIDC provider
during Resource Owner Password Credentials Grant logins. AllowPasswordGrant during Resource Owner Password Credentials Grant logins. allowPasswordGrant
defaults to false. defaults to false.
type: boolean type: boolean
type: object type: object
@ -103,12 +187,19 @@ spec:
used when inspecting an identity from this OIDC identity provider. used when inspecting an identity from this OIDC identity provider.
properties: properties:
groups: groups:
description: Groups provides the name of the token claim that description: Groups provides the name of the ID token claim or
will be used to ascertain the groups to which an identity belongs. userinfo endpoint response claim that will be used to ascertain
the groups to which an identity belongs. By default, the identities
will not include any group memberships when this setting is
not configured.
type: string type: string
username: username:
description: Username provides the name of the token claim that description: Username provides the name of the ID token claim
will be used to ascertain an identity's username. or userinfo endpoint response claim that will be used to ascertain
an identity's username. When not set, the username will be an
automatically constructed unique string which will include the
issuer URL of your OIDC provider along with the value of the
"sub" (subject) claim from the ID token.
type: string type: string
type: object type: object
client: client:

View File

@ -1099,8 +1099,9 @@ OIDCAuthorizationConfig provides information about how to form the OAuth2 author
[cols="25a,75a", options="header"] [cols="25a,75a", options="header"]
|=== |===
| Field | Description | Field | Description
| *`additionalScopes`* __string array__ | AdditionalScopes are the scopes in addition to "openid" that will be requested as part of the authorization request flow with an OIDC identity provider. In the case of a Resource Owner Password Credentials Grant flow, AdditionalScopes are the scopes in addition to "openid" that will be requested as part of the token request (see also the allowPasswordGrant field). By default, only the "openid" scope will be requested. | *`additionalScopes`* __string array__ | additionalScopes are the additional scopes that will be requested from your OIDC provider in the authorization request during an OIDC Authorization Code Flow and in the token request during a Resource Owner Password Credentials Grant. Note that the "openid" scope will always be requested regardless of the value in this setting, since it is always required according to the OIDC spec. By default, when this field is not set, the Supervisor will request the following scopes: "openid", "offline_access", "email", and "profile". See https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims for a description of the "profile" and "email" scopes. See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess for a description of the "offline_access" scope. This default value may change in future versions of Pinniped as the standard evolves, or as common patterns used by providers who implement the standard in the ecosystem evolve. By setting this list to anything other than an empty list, you are overriding the default value, so you may wish to include some of "offline_access", "email", and "profile" in your override list. If you do not want any of these scopes to be requested, you may set this list to contain only "openid". Some OIDC providers may also require a scope to get access to the user's group membership, in which case you may wish to include it in this list. Sometimes the scope to request the user's group membership is called "groups", but unfortunately this is not specified in the OIDC standard. Generally speaking, you should include any scopes required to cause the appropriate claims to be the returned by your OIDC provider in the ID token or userinfo endpoint results for those claims which you would like to use in the oidcClaims settings to determine the usernames and group memberships of your Kubernetes users. See your OIDC provider's documentation for more information about what scopes are available to request claims. Additionally, the Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor from these authorization flows. For most OIDC providers, the scope required to receive refresh tokens will be "offline_access". See the documentation of your OIDC provider's authorization and token endpoints for its requirements for what to include in the request in order to receive a refresh token in the response, if anything. Note that it may be safe to send "offline_access" even to providers which do not require it, since the provider may ignore scopes that it does not understand or require (see https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). In the unusual case that you must avoid sending the "offline_access" scope, then you must override the default value of this setting. This is required if your OIDC provider will reject the request when it includes "offline_access" (e.g. GitLab's OIDC provider).
| *`allowPasswordGrant`* __boolean__ | AllowPasswordGrant, when true, will allow the use of OAuth 2.0's Resource Owner Password Credentials Grant (see https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) to authenticate to the OIDC provider using a username and password without a web browser, in addition to the usual browser-based OIDC Authorization Code Flow. The Resource Owner Password Credentials Grant is not officially part of the OIDC specification, so it may not be supported by your OIDC provider. If your OIDC provider supports returning ID tokens from a Resource Owner Password Credentials Grant token request, then you can choose to set this field to true. This will allow end users to choose to present their username and password to the kubectl CLI (using the Pinniped plugin) to authenticate to the cluster, without using a web browser to log in as is customary in OIDC Authorization Code Flow. This may be convenient for users, especially for identities from your OIDC provider which are not intended to represent a human actor, such as service accounts performing actions in a CI/CD environment. Even if your OIDC provider supports it, you may wish to disable this behavior by setting this field to false when you prefer to only allow users of this OIDCIdentityProvider to log in via the browser-based OIDC Authorization Code Flow. Using the Resource Owner Password Credentials Grant means that the Pinniped CLI and Pinniped Supervisor will directly handle your end users' passwords (similar to LDAPIdentityProvider), and you will not be able to require multi-factor authentication or use the other web-based login features of your OIDC provider during Resource Owner Password Credentials Grant logins. AllowPasswordGrant defaults to false. | *`additionalAuthorizeParameters`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-parameter[$$Parameter$$] array__ | additionalAuthorizeParameters are extra query parameters that should be included in the authorize request to your OIDC provider in the authorization request during an OIDC Authorization Code Flow. By default, no extra parameters are sent. The standard parameters that will be sent are "response_type", "scope", "client_id", "state", "nonce", "code_challenge", "code_challenge_method", and "redirect_uri". These parameters cannot be included in this setting. Additionally, the "hd" parameter cannot be included in this setting at this time. The "hd" parameter is used by Google's OIDC provider to provide a hint as to which "hosted domain" the user should use during login. However, Pinniped does not yet support validating the hosted domain in the resulting ID token, so it is not yet safe to use this feature of Google's OIDC provider with Pinniped. This setting does not influence the parameters sent to the token endpoint in the Resource Owner Password Credentials Grant. The Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor from the authorization flows. Some OIDC providers may require a certain value for the "prompt" parameter in order to properly request refresh tokens. See the documentation of your OIDC provider's authorization endpoint for its requirements for what to include in the request in order to receive a refresh token in the response, if anything. If your provider requires the prompt parameter to request a refresh token, then include it here. Also note that most providers also require a certain scope to be requested in order to receive refresh tokens. See the additionalScopes setting for more information about using scopes to request refresh tokens.
| *`allowPasswordGrant`* __boolean__ | allowPasswordGrant, when true, will allow the use of OAuth 2.0's Resource Owner Password Credentials Grant (see https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) to authenticate to the OIDC provider using a username and password without a web browser, in addition to the usual browser-based OIDC Authorization Code Flow. The Resource Owner Password Credentials Grant is not officially part of the OIDC specification, so it may not be supported by your OIDC provider. If your OIDC provider supports returning ID tokens from a Resource Owner Password Credentials Grant token request, then you can choose to set this field to true. This will allow end users to choose to present their username and password to the kubectl CLI (using the Pinniped plugin) to authenticate to the cluster, without using a web browser to log in as is customary in OIDC Authorization Code Flow. This may be convenient for users, especially for identities from your OIDC provider which are not intended to represent a human actor, such as service accounts performing actions in a CI/CD environment. Even if your OIDC provider supports it, you may wish to disable this behavior by setting this field to false when you prefer to only allow users of this OIDCIdentityProvider to log in via the browser-based OIDC Authorization Code Flow. Using the Resource Owner Password Credentials Grant means that the Pinniped CLI and Pinniped Supervisor will directly handle your end users' passwords (similar to LDAPIdentityProvider), and you will not be able to require multi-factor authentication or use the other web-based login features of your OIDC provider during Resource Owner Password Credentials Grant logins. allowPasswordGrant defaults to false.
|=== |===
@ -1117,8 +1118,8 @@ OIDCClaims provides a mapping from upstream claims into identities.
[cols="25a,75a", options="header"] [cols="25a,75a", options="header"]
|=== |===
| Field | Description | Field | Description
| *`groups`* __string__ | Groups provides the name of the token claim that will be used to ascertain the groups to which an identity belongs. | *`groups`* __string__ | Groups provides the name of the ID token claim or userinfo endpoint response claim that will be used to ascertain the groups to which an identity belongs. By default, the identities will not include any group memberships when this setting is not configured.
| *`username`* __string__ | Username provides the name of the token claim that will be used to ascertain an identity's username. | *`username`* __string__ | Username provides the name of the ID token claim or userinfo endpoint response claim that will be used to ascertain an identity's username. When not set, the username will be an automatically constructed unique string which will include the issuer URL of your OIDC provider along with the value of the "sub" (subject) claim from the ID token.
|=== |===
@ -1164,7 +1165,7 @@ OIDCIdentityProvider describes the configuration of an upstream OpenID Connect i
[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-oidcidentityproviderspec"] [id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-oidcidentityproviderspec"]
==== OIDCIdentityProviderSpec ==== OIDCIdentityProviderSpec
Spec for configuring an OIDC identity provider. OIDCIdentityProviderSpec is the spec for configuring an OIDC identity provider.
.Appears In: .Appears In:
**** ****
@ -1185,7 +1186,7 @@ Spec for configuring an OIDC identity provider.
[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-oidcidentityproviderstatus"] [id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-oidcidentityproviderstatus"]
==== OIDCIdentityProviderStatus ==== OIDCIdentityProviderStatus
Status of an OIDC identity provider. OIDCIdentityProviderStatus is the status of an OIDC identity provider.
.Appears In: .Appears In:
**** ****
@ -1200,6 +1201,24 @@ Status of an OIDC identity provider.
|=== |===
[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-parameter"]
==== Parameter
.Appears In:
****
- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-oidcauthorizationconfig[$$OIDCAuthorizationConfig$$]
****
[cols="25a,75a", options="header"]
|===
| Field | Description
| *`name`* __string__ | The name of the parameter. Required.
| *`value`* __string__ | The value of the parameter.
|===
[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-tlsspec"] [id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-tlsspec"]
==== TLSSpec ==== TLSSpec

View File

@ -20,7 +20,7 @@ const (
PhaseError OIDCIdentityProviderPhase = "Error" PhaseError OIDCIdentityProviderPhase = "Error"
) )
// Status of an OIDC identity provider. // OIDCIdentityProviderStatus is the status of an OIDC identity provider.
type OIDCIdentityProviderStatus struct { type OIDCIdentityProviderStatus struct {
// Phase summarizes the overall status of the OIDCIdentityProvider. // Phase summarizes the overall status of the OIDCIdentityProvider.
// +kubebuilder:default=Pending // +kubebuilder:default=Pending
@ -38,15 +38,62 @@ type OIDCIdentityProviderStatus struct {
// OIDCAuthorizationConfig provides information about how to form the OAuth2 authorization // OIDCAuthorizationConfig provides information about how to form the OAuth2 authorization
// request parameters. // request parameters.
type OIDCAuthorizationConfig struct { type OIDCAuthorizationConfig struct {
// AdditionalScopes are the scopes in addition to "openid" that will be requested as part of the authorization // additionalScopes are the additional scopes that will be requested from your OIDC provider in the authorization
// request flow with an OIDC identity provider. // request during an OIDC Authorization Code Flow and in the token request during a Resource Owner Password Credentials
// In the case of a Resource Owner Password Credentials Grant flow, AdditionalScopes are the scopes // Grant. Note that the "openid" scope will always be requested regardless of the value in this setting, since it is
// in addition to "openid" that will be requested as part of the token request (see also the allowPasswordGrant field). // always required according to the OIDC spec. By default, when this field is not set, the Supervisor will request
// By default, only the "openid" scope will be requested. // the following scopes: "openid", "offline_access", "email", and "profile". See
// https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims for a description of the "profile" and "email"
// scopes. See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess for a description of the
// "offline_access" scope. This default value may change in future versions of Pinniped as the standard evolves,
// or as common patterns used by providers who implement the standard in the ecosystem evolve.
// By setting this list to anything other than an empty list, you are overriding the
// default value, so you may wish to include some of "offline_access", "email", and "profile" in your override list.
// If you do not want any of these scopes to be requested, you may set this list to contain only "openid".
// Some OIDC providers may also require a scope to get access to the user's group membership, in which case you
// may wish to include it in this list. Sometimes the scope to request the user's group membership is called
// "groups", but unfortunately this is not specified in the OIDC standard.
// Generally speaking, you should include any scopes required to cause the appropriate claims to be the returned by
// your OIDC provider in the ID token or userinfo endpoint results for those claims which you would like to use in
// the oidcClaims settings to determine the usernames and group memberships of your Kubernetes users. See
// your OIDC provider's documentation for more information about what scopes are available to request claims.
// Additionally, the Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor
// from these authorization flows. For most OIDC providers, the scope required to receive refresh tokens will be
// "offline_access". See the documentation of your OIDC provider's authorization and token endpoints for its
// requirements for what to include in the request in order to receive a refresh token in the response, if anything.
// Note that it may be safe to send "offline_access" even to providers which do not require it, since the provider
// may ignore scopes that it does not understand or require (see
// https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). In the unusual case that you must avoid sending the
// "offline_access" scope, then you must override the default value of this setting. This is required if your OIDC
// provider will reject the request when it includes "offline_access" (e.g. GitLab's OIDC provider).
// +optional // +optional
AdditionalScopes []string `json:"additionalScopes,omitempty"` AdditionalScopes []string `json:"additionalScopes,omitempty"`
// AllowPasswordGrant, when true, will allow the use of OAuth 2.0's Resource Owner Password Credentials Grant // additionalAuthorizeParameters are extra query parameters that should be included in the authorize request to your
// OIDC provider in the authorization request during an OIDC Authorization Code Flow. By default, no extra
// parameters are sent. The standard parameters that will be sent are "response_type", "scope", "client_id",
// "state", "nonce", "code_challenge", "code_challenge_method", and "redirect_uri". These parameters cannot be
// included in this setting. Additionally, the "hd" parameter cannot be included in this setting at this time.
// The "hd" parameter is used by Google's OIDC provider to provide a hint as to which "hosted domain" the user
// should use during login. However, Pinniped does not yet support validating the hosted domain in the resulting
// ID token, so it is not yet safe to use this feature of Google's OIDC provider with Pinniped.
// This setting does not influence the parameters sent to the token endpoint in the Resource Owner Password
// Credentials Grant. The Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the
// Supervisor from the authorization flows. Some OIDC providers may require a certain value for the "prompt"
// parameter in order to properly request refresh tokens. See the documentation of your OIDC provider's
// authorization endpoint for its requirements for what to include in the request in order to receive a refresh
// token in the response, if anything. If your provider requires the prompt parameter to request a refresh token,
// then include it here. Also note that most providers also require a certain scope to be requested in order to
// receive refresh tokens. See the additionalScopes setting for more information about using scopes to request
// refresh tokens.
// +optional
// +patchMergeKey=name
// +patchStrategy=merge
// +listType=map
// +listMapKey=name
AdditionalAuthorizeParameters []Parameter `json:"additionalAuthorizeParameters,omitempty"`
// allowPasswordGrant, when true, will allow the use of OAuth 2.0's Resource Owner Password Credentials Grant
// (see https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) to authenticate to the OIDC provider using a // (see https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) to authenticate to the OIDC provider using a
// username and password without a web browser, in addition to the usual browser-based OIDC Authorization Code Flow. // username and password without a web browser, in addition to the usual browser-based OIDC Authorization Code Flow.
// The Resource Owner Password Credentials Grant is not officially part of the OIDC specification, so it may not be // The Resource Owner Password Credentials Grant is not officially part of the OIDC specification, so it may not be
@ -61,20 +108,34 @@ type OIDCAuthorizationConfig struct {
// Credentials Grant means that the Pinniped CLI and Pinniped Supervisor will directly handle your end users' passwords // Credentials Grant means that the Pinniped CLI and Pinniped Supervisor will directly handle your end users' passwords
// (similar to LDAPIdentityProvider), and you will not be able to require multi-factor authentication or use the other // (similar to LDAPIdentityProvider), and you will not be able to require multi-factor authentication or use the other
// web-based login features of your OIDC provider during Resource Owner Password Credentials Grant logins. // web-based login features of your OIDC provider during Resource Owner Password Credentials Grant logins.
// AllowPasswordGrant defaults to false. // allowPasswordGrant defaults to false.
// +optional // +optional
AllowPasswordGrant bool `json:"allowPasswordGrant,omitempty"` AllowPasswordGrant bool `json:"allowPasswordGrant,omitempty"`
} }
// Parameter is a key/value pair which represents a parameter in an HTTP request.
type Parameter struct {
// The name of the parameter. Required.
// +kubebuilder:validation:MinLength=1
Name string `json:"name"`
// The value of the parameter.
// +optional
Value string `json:"value,omitempty"`
}
// OIDCClaims provides a mapping from upstream claims into identities. // OIDCClaims provides a mapping from upstream claims into identities.
type OIDCClaims struct { type OIDCClaims struct {
// Groups provides the name of the token claim that will be used to ascertain the groups to which // Groups provides the name of the ID token claim or userinfo endpoint response claim that will be used to ascertain
// an identity belongs. // the groups to which an identity belongs. By default, the identities will not include any group memberships when
// this setting is not configured.
// +optional // +optional
Groups string `json:"groups"` Groups string `json:"groups"`
// Username provides the name of the token claim that will be used to ascertain an identity's // Username provides the name of the ID token claim or userinfo endpoint response claim that will be used to
// username. // ascertain an identity's username. When not set, the username will be an automatically constructed unique string
// which will include the issuer URL of your OIDC provider along with the value of the "sub" (subject) claim from
// the ID token.
// +optional // +optional
Username string `json:"username"` Username string `json:"username"`
} }
@ -89,7 +150,7 @@ type OIDCClient struct {
SecretName string `json:"secretName"` SecretName string `json:"secretName"`
} }
// Spec for configuring an OIDC identity provider. // OIDCIdentityProviderSpec is the spec for configuring an OIDC identity provider.
type OIDCIdentityProviderSpec struct { type OIDCIdentityProviderSpec struct {
// Issuer is the issuer URL of this OIDC identity provider, i.e., where to fetch // Issuer is the issuer URL of this OIDC identity provider, i.e., where to fetch
// /.well-known/openid-configuration. // /.well-known/openid-configuration.
@ -135,7 +196,7 @@ type OIDCIdentityProvider struct {
Status OIDCIdentityProviderStatus `json:"status,omitempty"` Status OIDCIdentityProviderStatus `json:"status,omitempty"`
} }
// List of OIDCIdentityProvider objects. // OIDCIdentityProviderList lists OIDCIdentityProvider objects.
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type OIDCIdentityProviderList struct { type OIDCIdentityProviderList struct {
metav1.TypeMeta `json:",inline"` metav1.TypeMeta `json:",inline"`

View File

@ -416,6 +416,11 @@ func (in *OIDCAuthorizationConfig) DeepCopyInto(out *OIDCAuthorizationConfig) {
*out = make([]string, len(*in)) *out = make([]string, len(*in))
copy(*out, *in) copy(*out, *in)
} }
if in.AdditionalAuthorizeParameters != nil {
in, out := &in.AdditionalAuthorizeParameters, &out.AdditionalAuthorizeParameters
*out = make([]Parameter, len(*in))
copy(*out, *in)
}
return return
} }
@ -569,6 +574,22 @@ func (in *OIDCIdentityProviderStatus) DeepCopy() *OIDCIdentityProviderStatus {
return out return out
} }
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Parameter) DeepCopyInto(out *Parameter) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Parameter.
func (in *Parameter) DeepCopy() *Parameter {
if in == nil {
return nil
}
out := new(Parameter)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TLSSpec) DeepCopyInto(out *TLSSpec) { func (in *TLSSpec) DeepCopyInto(out *TLSSpec) {
*out = *in *out = *in

View File

@ -56,19 +56,103 @@ spec:
the OAuth2 authorization request parameters to be used with this the OAuth2 authorization request parameters to be used with this
OIDC identity provider. OIDC identity provider.
properties: properties:
additionalAuthorizeParameters:
description: additionalAuthorizeParameters are extra query parameters
that should be included in the authorize request to your OIDC
provider in the authorization request during an OIDC Authorization
Code Flow. By default, no extra parameters are sent. The standard
parameters that will be sent are "response_type", "scope", "client_id",
"state", "nonce", "code_challenge", "code_challenge_method",
and "redirect_uri". These parameters cannot be included in this
setting. Additionally, the "hd" parameter cannot be included
in this setting at this time. The "hd" parameter is used by
Google's OIDC provider to provide a hint as to which "hosted
domain" the user should use during login. However, Pinniped
does not yet support validating the hosted domain in the resulting
ID token, so it is not yet safe to use this feature of Google's
OIDC provider with Pinniped. This setting does not influence
the parameters sent to the token endpoint in the Resource Owner
Password Credentials Grant. The Pinniped Supervisor requires
that your OIDC provider returns refresh tokens to the Supervisor
from the authorization flows. Some OIDC providers may require
a certain value for the "prompt" parameter in order to properly
request refresh tokens. See the documentation of your OIDC provider's
authorization endpoint for its requirements for what to include
in the request in order to receive a refresh token in the response,
if anything. If your provider requires the prompt parameter
to request a refresh token, then include it here. Also note
that most providers also require a certain scope to be requested
in order to receive refresh tokens. See the additionalScopes
setting for more information about using scopes to request refresh
tokens.
items:
description: Parameter is a key/value pair which represents
a parameter in an HTTP request.
properties:
name:
description: The name of the parameter. Required.
minLength: 1
type: string
value:
description: The value of the parameter.
type: string
required:
- name
type: object
type: array
x-kubernetes-list-map-keys:
- name
x-kubernetes-list-type: map
additionalScopes: additionalScopes:
description: AdditionalScopes are the scopes in addition to "openid" description: 'additionalScopes are the additional scopes that
that will be requested as part of the authorization request will be requested from your OIDC provider in the authorization
flow with an OIDC identity provider. In the case of a Resource request during an OIDC Authorization Code Flow and in the token
Owner Password Credentials Grant flow, AdditionalScopes are request during a Resource Owner Password Credentials Grant.
the scopes in addition to "openid" that will be requested as Note that the "openid" scope will always be requested regardless
part of the token request (see also the allowPasswordGrant field). of the value in this setting, since it is always required according
By default, only the "openid" scope will be requested. to the OIDC spec. By default, when this field is not set, the
Supervisor will request the following scopes: "openid", "offline_access",
"email", and "profile". See https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims
for a description of the "profile" and "email" scopes. See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess
for a description of the "offline_access" scope. This default
value may change in future versions of Pinniped as the standard
evolves, or as common patterns used by providers who implement
the standard in the ecosystem evolve. By setting this list to
anything other than an empty list, you are overriding the default
value, so you may wish to include some of "offline_access",
"email", and "profile" in your override list. If you do not
want any of these scopes to be requested, you may set this list
to contain only "openid". Some OIDC providers may also require
a scope to get access to the user''s group membership, in which
case you may wish to include it in this list. Sometimes the
scope to request the user''s group membership is called "groups",
but unfortunately this is not specified in the OIDC standard.
Generally speaking, you should include any scopes required to
cause the appropriate claims to be the returned by your OIDC
provider in the ID token or userinfo endpoint results for those
claims which you would like to use in the oidcClaims settings
to determine the usernames and group memberships of your Kubernetes
users. See your OIDC provider''s documentation for more information
about what scopes are available to request claims. Additionally,
the Pinniped Supervisor requires that your OIDC provider returns
refresh tokens to the Supervisor from these authorization flows.
For most OIDC providers, the scope required to receive refresh
tokens will be "offline_access". See the documentation of your
OIDC provider''s authorization and token endpoints for its requirements
for what to include in the request in order to receive a refresh
token in the response, if anything. Note that it may be safe
to send "offline_access" even to providers which do not require
it, since the provider may ignore scopes that it does not understand
or require (see https://datatracker.ietf.org/doc/html/rfc6749#section-3.3).
In the unusual case that you must avoid sending the "offline_access"
scope, then you must override the default value of this setting.
This is required if your OIDC provider will reject the request
when it includes "offline_access" (e.g. GitLab''s OIDC provider).'
items: items:
type: string type: string
type: array type: array
allowPasswordGrant: allowPasswordGrant:
description: AllowPasswordGrant, when true, will allow the use description: allowPasswordGrant, when true, will allow the use
of OAuth 2.0's Resource Owner Password Credentials Grant (see of OAuth 2.0's Resource Owner Password Credentials Grant (see
https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) to https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) to
authenticate to the OIDC provider using a username and password authenticate to the OIDC provider using a username and password
@ -94,7 +178,7 @@ spec:
your end users' passwords (similar to LDAPIdentityProvider), your end users' passwords (similar to LDAPIdentityProvider),
and you will not be able to require multi-factor authentication and you will not be able to require multi-factor authentication
or use the other web-based login features of your OIDC provider or use the other web-based login features of your OIDC provider
during Resource Owner Password Credentials Grant logins. AllowPasswordGrant during Resource Owner Password Credentials Grant logins. allowPasswordGrant
defaults to false. defaults to false.
type: boolean type: boolean
type: object type: object
@ -103,12 +187,19 @@ spec:
used when inspecting an identity from this OIDC identity provider. used when inspecting an identity from this OIDC identity provider.
properties: properties:
groups: groups:
description: Groups provides the name of the token claim that description: Groups provides the name of the ID token claim or
will be used to ascertain the groups to which an identity belongs. userinfo endpoint response claim that will be used to ascertain
the groups to which an identity belongs. By default, the identities
will not include any group memberships when this setting is
not configured.
type: string type: string
username: username:
description: Username provides the name of the token claim that description: Username provides the name of the ID token claim
will be used to ascertain an identity's username. or userinfo endpoint response claim that will be used to ascertain
an identity's username. When not set, the username will be an
automatically constructed unique string which will include the
issuer URL of your OIDC provider along with the value of the
"sub" (subject) claim from the ID token.
type: string type: string
type: object type: object
client: client:

View File

@ -1099,8 +1099,9 @@ OIDCAuthorizationConfig provides information about how to form the OAuth2 author
[cols="25a,75a", options="header"] [cols="25a,75a", options="header"]
|=== |===
| Field | Description | Field | Description
| *`additionalScopes`* __string array__ | AdditionalScopes are the scopes in addition to "openid" that will be requested as part of the authorization request flow with an OIDC identity provider. In the case of a Resource Owner Password Credentials Grant flow, AdditionalScopes are the scopes in addition to "openid" that will be requested as part of the token request (see also the allowPasswordGrant field). By default, only the "openid" scope will be requested. | *`additionalScopes`* __string array__ | additionalScopes are the additional scopes that will be requested from your OIDC provider in the authorization request during an OIDC Authorization Code Flow and in the token request during a Resource Owner Password Credentials Grant. Note that the "openid" scope will always be requested regardless of the value in this setting, since it is always required according to the OIDC spec. By default, when this field is not set, the Supervisor will request the following scopes: "openid", "offline_access", "email", and "profile". See https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims for a description of the "profile" and "email" scopes. See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess for a description of the "offline_access" scope. This default value may change in future versions of Pinniped as the standard evolves, or as common patterns used by providers who implement the standard in the ecosystem evolve. By setting this list to anything other than an empty list, you are overriding the default value, so you may wish to include some of "offline_access", "email", and "profile" in your override list. If you do not want any of these scopes to be requested, you may set this list to contain only "openid". Some OIDC providers may also require a scope to get access to the user's group membership, in which case you may wish to include it in this list. Sometimes the scope to request the user's group membership is called "groups", but unfortunately this is not specified in the OIDC standard. Generally speaking, you should include any scopes required to cause the appropriate claims to be the returned by your OIDC provider in the ID token or userinfo endpoint results for those claims which you would like to use in the oidcClaims settings to determine the usernames and group memberships of your Kubernetes users. See your OIDC provider's documentation for more information about what scopes are available to request claims. Additionally, the Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor from these authorization flows. For most OIDC providers, the scope required to receive refresh tokens will be "offline_access". See the documentation of your OIDC provider's authorization and token endpoints for its requirements for what to include in the request in order to receive a refresh token in the response, if anything. Note that it may be safe to send "offline_access" even to providers which do not require it, since the provider may ignore scopes that it does not understand or require (see https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). In the unusual case that you must avoid sending the "offline_access" scope, then you must override the default value of this setting. This is required if your OIDC provider will reject the request when it includes "offline_access" (e.g. GitLab's OIDC provider).
| *`allowPasswordGrant`* __boolean__ | AllowPasswordGrant, when true, will allow the use of OAuth 2.0's Resource Owner Password Credentials Grant (see https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) to authenticate to the OIDC provider using a username and password without a web browser, in addition to the usual browser-based OIDC Authorization Code Flow. The Resource Owner Password Credentials Grant is not officially part of the OIDC specification, so it may not be supported by your OIDC provider. If your OIDC provider supports returning ID tokens from a Resource Owner Password Credentials Grant token request, then you can choose to set this field to true. This will allow end users to choose to present their username and password to the kubectl CLI (using the Pinniped plugin) to authenticate to the cluster, without using a web browser to log in as is customary in OIDC Authorization Code Flow. This may be convenient for users, especially for identities from your OIDC provider which are not intended to represent a human actor, such as service accounts performing actions in a CI/CD environment. Even if your OIDC provider supports it, you may wish to disable this behavior by setting this field to false when you prefer to only allow users of this OIDCIdentityProvider to log in via the browser-based OIDC Authorization Code Flow. Using the Resource Owner Password Credentials Grant means that the Pinniped CLI and Pinniped Supervisor will directly handle your end users' passwords (similar to LDAPIdentityProvider), and you will not be able to require multi-factor authentication or use the other web-based login features of your OIDC provider during Resource Owner Password Credentials Grant logins. AllowPasswordGrant defaults to false. | *`additionalAuthorizeParameters`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-parameter[$$Parameter$$] array__ | additionalAuthorizeParameters are extra query parameters that should be included in the authorize request to your OIDC provider in the authorization request during an OIDC Authorization Code Flow. By default, no extra parameters are sent. The standard parameters that will be sent are "response_type", "scope", "client_id", "state", "nonce", "code_challenge", "code_challenge_method", and "redirect_uri". These parameters cannot be included in this setting. Additionally, the "hd" parameter cannot be included in this setting at this time. The "hd" parameter is used by Google's OIDC provider to provide a hint as to which "hosted domain" the user should use during login. However, Pinniped does not yet support validating the hosted domain in the resulting ID token, so it is not yet safe to use this feature of Google's OIDC provider with Pinniped. This setting does not influence the parameters sent to the token endpoint in the Resource Owner Password Credentials Grant. The Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor from the authorization flows. Some OIDC providers may require a certain value for the "prompt" parameter in order to properly request refresh tokens. See the documentation of your OIDC provider's authorization endpoint for its requirements for what to include in the request in order to receive a refresh token in the response, if anything. If your provider requires the prompt parameter to request a refresh token, then include it here. Also note that most providers also require a certain scope to be requested in order to receive refresh tokens. See the additionalScopes setting for more information about using scopes to request refresh tokens.
| *`allowPasswordGrant`* __boolean__ | allowPasswordGrant, when true, will allow the use of OAuth 2.0's Resource Owner Password Credentials Grant (see https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) to authenticate to the OIDC provider using a username and password without a web browser, in addition to the usual browser-based OIDC Authorization Code Flow. The Resource Owner Password Credentials Grant is not officially part of the OIDC specification, so it may not be supported by your OIDC provider. If your OIDC provider supports returning ID tokens from a Resource Owner Password Credentials Grant token request, then you can choose to set this field to true. This will allow end users to choose to present their username and password to the kubectl CLI (using the Pinniped plugin) to authenticate to the cluster, without using a web browser to log in as is customary in OIDC Authorization Code Flow. This may be convenient for users, especially for identities from your OIDC provider which are not intended to represent a human actor, such as service accounts performing actions in a CI/CD environment. Even if your OIDC provider supports it, you may wish to disable this behavior by setting this field to false when you prefer to only allow users of this OIDCIdentityProvider to log in via the browser-based OIDC Authorization Code Flow. Using the Resource Owner Password Credentials Grant means that the Pinniped CLI and Pinniped Supervisor will directly handle your end users' passwords (similar to LDAPIdentityProvider), and you will not be able to require multi-factor authentication or use the other web-based login features of your OIDC provider during Resource Owner Password Credentials Grant logins. allowPasswordGrant defaults to false.
|=== |===
@ -1117,8 +1118,8 @@ OIDCClaims provides a mapping from upstream claims into identities.
[cols="25a,75a", options="header"] [cols="25a,75a", options="header"]
|=== |===
| Field | Description | Field | Description
| *`groups`* __string__ | Groups provides the name of the token claim that will be used to ascertain the groups to which an identity belongs. | *`groups`* __string__ | Groups provides the name of the ID token claim or userinfo endpoint response claim that will be used to ascertain the groups to which an identity belongs. By default, the identities will not include any group memberships when this setting is not configured.
| *`username`* __string__ | Username provides the name of the token claim that will be used to ascertain an identity's username. | *`username`* __string__ | Username provides the name of the ID token claim or userinfo endpoint response claim that will be used to ascertain an identity's username. When not set, the username will be an automatically constructed unique string which will include the issuer URL of your OIDC provider along with the value of the "sub" (subject) claim from the ID token.
|=== |===
@ -1164,7 +1165,7 @@ OIDCIdentityProvider describes the configuration of an upstream OpenID Connect i
[id="{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-oidcidentityproviderspec"] [id="{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-oidcidentityproviderspec"]
==== OIDCIdentityProviderSpec ==== OIDCIdentityProviderSpec
Spec for configuring an OIDC identity provider. OIDCIdentityProviderSpec is the spec for configuring an OIDC identity provider.
.Appears In: .Appears In:
**** ****
@ -1185,7 +1186,7 @@ Spec for configuring an OIDC identity provider.
[id="{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-oidcidentityproviderstatus"] [id="{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-oidcidentityproviderstatus"]
==== OIDCIdentityProviderStatus ==== OIDCIdentityProviderStatus
Status of an OIDC identity provider. OIDCIdentityProviderStatus is the status of an OIDC identity provider.
.Appears In: .Appears In:
**** ****
@ -1200,6 +1201,24 @@ Status of an OIDC identity provider.
|=== |===
[id="{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-parameter"]
==== Parameter
.Appears In:
****
- xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-oidcauthorizationconfig[$$OIDCAuthorizationConfig$$]
****
[cols="25a,75a", options="header"]
|===
| Field | Description
| *`name`* __string__ | The name of the parameter. Required.
| *`value`* __string__ | The value of the parameter.
|===
[id="{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-tlsspec"] [id="{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-tlsspec"]
==== TLSSpec ==== TLSSpec

View File

@ -20,7 +20,7 @@ const (
PhaseError OIDCIdentityProviderPhase = "Error" PhaseError OIDCIdentityProviderPhase = "Error"
) )
// Status of an OIDC identity provider. // OIDCIdentityProviderStatus is the status of an OIDC identity provider.
type OIDCIdentityProviderStatus struct { type OIDCIdentityProviderStatus struct {
// Phase summarizes the overall status of the OIDCIdentityProvider. // Phase summarizes the overall status of the OIDCIdentityProvider.
// +kubebuilder:default=Pending // +kubebuilder:default=Pending
@ -38,15 +38,62 @@ type OIDCIdentityProviderStatus struct {
// OIDCAuthorizationConfig provides information about how to form the OAuth2 authorization // OIDCAuthorizationConfig provides information about how to form the OAuth2 authorization
// request parameters. // request parameters.
type OIDCAuthorizationConfig struct { type OIDCAuthorizationConfig struct {
// AdditionalScopes are the scopes in addition to "openid" that will be requested as part of the authorization // additionalScopes are the additional scopes that will be requested from your OIDC provider in the authorization
// request flow with an OIDC identity provider. // request during an OIDC Authorization Code Flow and in the token request during a Resource Owner Password Credentials
// In the case of a Resource Owner Password Credentials Grant flow, AdditionalScopes are the scopes // Grant. Note that the "openid" scope will always be requested regardless of the value in this setting, since it is
// in addition to "openid" that will be requested as part of the token request (see also the allowPasswordGrant field). // always required according to the OIDC spec. By default, when this field is not set, the Supervisor will request
// By default, only the "openid" scope will be requested. // the following scopes: "openid", "offline_access", "email", and "profile". See
// https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims for a description of the "profile" and "email"
// scopes. See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess for a description of the
// "offline_access" scope. This default value may change in future versions of Pinniped as the standard evolves,
// or as common patterns used by providers who implement the standard in the ecosystem evolve.
// By setting this list to anything other than an empty list, you are overriding the
// default value, so you may wish to include some of "offline_access", "email", and "profile" in your override list.
// If you do not want any of these scopes to be requested, you may set this list to contain only "openid".
// Some OIDC providers may also require a scope to get access to the user's group membership, in which case you
// may wish to include it in this list. Sometimes the scope to request the user's group membership is called
// "groups", but unfortunately this is not specified in the OIDC standard.
// Generally speaking, you should include any scopes required to cause the appropriate claims to be the returned by
// your OIDC provider in the ID token or userinfo endpoint results for those claims which you would like to use in
// the oidcClaims settings to determine the usernames and group memberships of your Kubernetes users. See
// your OIDC provider's documentation for more information about what scopes are available to request claims.
// Additionally, the Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor
// from these authorization flows. For most OIDC providers, the scope required to receive refresh tokens will be
// "offline_access". See the documentation of your OIDC provider's authorization and token endpoints for its
// requirements for what to include in the request in order to receive a refresh token in the response, if anything.
// Note that it may be safe to send "offline_access" even to providers which do not require it, since the provider
// may ignore scopes that it does not understand or require (see
// https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). In the unusual case that you must avoid sending the
// "offline_access" scope, then you must override the default value of this setting. This is required if your OIDC
// provider will reject the request when it includes "offline_access" (e.g. GitLab's OIDC provider).
// +optional // +optional
AdditionalScopes []string `json:"additionalScopes,omitempty"` AdditionalScopes []string `json:"additionalScopes,omitempty"`
// AllowPasswordGrant, when true, will allow the use of OAuth 2.0's Resource Owner Password Credentials Grant // additionalAuthorizeParameters are extra query parameters that should be included in the authorize request to your
// OIDC provider in the authorization request during an OIDC Authorization Code Flow. By default, no extra
// parameters are sent. The standard parameters that will be sent are "response_type", "scope", "client_id",
// "state", "nonce", "code_challenge", "code_challenge_method", and "redirect_uri". These parameters cannot be
// included in this setting. Additionally, the "hd" parameter cannot be included in this setting at this time.
// The "hd" parameter is used by Google's OIDC provider to provide a hint as to which "hosted domain" the user
// should use during login. However, Pinniped does not yet support validating the hosted domain in the resulting
// ID token, so it is not yet safe to use this feature of Google's OIDC provider with Pinniped.
// This setting does not influence the parameters sent to the token endpoint in the Resource Owner Password
// Credentials Grant. The Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the
// Supervisor from the authorization flows. Some OIDC providers may require a certain value for the "prompt"
// parameter in order to properly request refresh tokens. See the documentation of your OIDC provider's
// authorization endpoint for its requirements for what to include in the request in order to receive a refresh
// token in the response, if anything. If your provider requires the prompt parameter to request a refresh token,
// then include it here. Also note that most providers also require a certain scope to be requested in order to
// receive refresh tokens. See the additionalScopes setting for more information about using scopes to request
// refresh tokens.
// +optional
// +patchMergeKey=name
// +patchStrategy=merge
// +listType=map
// +listMapKey=name
AdditionalAuthorizeParameters []Parameter `json:"additionalAuthorizeParameters,omitempty"`
// allowPasswordGrant, when true, will allow the use of OAuth 2.0's Resource Owner Password Credentials Grant
// (see https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) to authenticate to the OIDC provider using a // (see https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) to authenticate to the OIDC provider using a
// username and password without a web browser, in addition to the usual browser-based OIDC Authorization Code Flow. // username and password without a web browser, in addition to the usual browser-based OIDC Authorization Code Flow.
// The Resource Owner Password Credentials Grant is not officially part of the OIDC specification, so it may not be // The Resource Owner Password Credentials Grant is not officially part of the OIDC specification, so it may not be
@ -61,20 +108,34 @@ type OIDCAuthorizationConfig struct {
// Credentials Grant means that the Pinniped CLI and Pinniped Supervisor will directly handle your end users' passwords // Credentials Grant means that the Pinniped CLI and Pinniped Supervisor will directly handle your end users' passwords
// (similar to LDAPIdentityProvider), and you will not be able to require multi-factor authentication or use the other // (similar to LDAPIdentityProvider), and you will not be able to require multi-factor authentication or use the other
// web-based login features of your OIDC provider during Resource Owner Password Credentials Grant logins. // web-based login features of your OIDC provider during Resource Owner Password Credentials Grant logins.
// AllowPasswordGrant defaults to false. // allowPasswordGrant defaults to false.
// +optional // +optional
AllowPasswordGrant bool `json:"allowPasswordGrant,omitempty"` AllowPasswordGrant bool `json:"allowPasswordGrant,omitempty"`
} }
// Parameter is a key/value pair which represents a parameter in an HTTP request.
type Parameter struct {
// The name of the parameter. Required.
// +kubebuilder:validation:MinLength=1
Name string `json:"name"`
// The value of the parameter.
// +optional
Value string `json:"value,omitempty"`
}
// OIDCClaims provides a mapping from upstream claims into identities. // OIDCClaims provides a mapping from upstream claims into identities.
type OIDCClaims struct { type OIDCClaims struct {
// Groups provides the name of the token claim that will be used to ascertain the groups to which // Groups provides the name of the ID token claim or userinfo endpoint response claim that will be used to ascertain
// an identity belongs. // the groups to which an identity belongs. By default, the identities will not include any group memberships when
// this setting is not configured.
// +optional // +optional
Groups string `json:"groups"` Groups string `json:"groups"`
// Username provides the name of the token claim that will be used to ascertain an identity's // Username provides the name of the ID token claim or userinfo endpoint response claim that will be used to
// username. // ascertain an identity's username. When not set, the username will be an automatically constructed unique string
// which will include the issuer URL of your OIDC provider along with the value of the "sub" (subject) claim from
// the ID token.
// +optional // +optional
Username string `json:"username"` Username string `json:"username"`
} }
@ -89,7 +150,7 @@ type OIDCClient struct {
SecretName string `json:"secretName"` SecretName string `json:"secretName"`
} }
// Spec for configuring an OIDC identity provider. // OIDCIdentityProviderSpec is the spec for configuring an OIDC identity provider.
type OIDCIdentityProviderSpec struct { type OIDCIdentityProviderSpec struct {
// Issuer is the issuer URL of this OIDC identity provider, i.e., where to fetch // Issuer is the issuer URL of this OIDC identity provider, i.e., where to fetch
// /.well-known/openid-configuration. // /.well-known/openid-configuration.
@ -135,7 +196,7 @@ type OIDCIdentityProvider struct {
Status OIDCIdentityProviderStatus `json:"status,omitempty"` Status OIDCIdentityProviderStatus `json:"status,omitempty"`
} }
// List of OIDCIdentityProvider objects. // OIDCIdentityProviderList lists OIDCIdentityProvider objects.
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type OIDCIdentityProviderList struct { type OIDCIdentityProviderList struct {
metav1.TypeMeta `json:",inline"` metav1.TypeMeta `json:",inline"`

View File

@ -416,6 +416,11 @@ func (in *OIDCAuthorizationConfig) DeepCopyInto(out *OIDCAuthorizationConfig) {
*out = make([]string, len(*in)) *out = make([]string, len(*in))
copy(*out, *in) copy(*out, *in)
} }
if in.AdditionalAuthorizeParameters != nil {
in, out := &in.AdditionalAuthorizeParameters, &out.AdditionalAuthorizeParameters
*out = make([]Parameter, len(*in))
copy(*out, *in)
}
return return
} }
@ -569,6 +574,22 @@ func (in *OIDCIdentityProviderStatus) DeepCopy() *OIDCIdentityProviderStatus {
return out return out
} }
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Parameter) DeepCopyInto(out *Parameter) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Parameter.
func (in *Parameter) DeepCopy() *Parameter {
if in == nil {
return nil
}
out := new(Parameter)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TLSSpec) DeepCopyInto(out *TLSSpec) { func (in *TLSSpec) DeepCopyInto(out *TLSSpec) {
*out = *in *out = *in

View File

@ -56,19 +56,103 @@ spec:
the OAuth2 authorization request parameters to be used with this the OAuth2 authorization request parameters to be used with this
OIDC identity provider. OIDC identity provider.
properties: properties:
additionalAuthorizeParameters:
description: additionalAuthorizeParameters are extra query parameters
that should be included in the authorize request to your OIDC
provider in the authorization request during an OIDC Authorization
Code Flow. By default, no extra parameters are sent. The standard
parameters that will be sent are "response_type", "scope", "client_id",
"state", "nonce", "code_challenge", "code_challenge_method",
and "redirect_uri". These parameters cannot be included in this
setting. Additionally, the "hd" parameter cannot be included
in this setting at this time. The "hd" parameter is used by
Google's OIDC provider to provide a hint as to which "hosted
domain" the user should use during login. However, Pinniped
does not yet support validating the hosted domain in the resulting
ID token, so it is not yet safe to use this feature of Google's
OIDC provider with Pinniped. This setting does not influence
the parameters sent to the token endpoint in the Resource Owner
Password Credentials Grant. The Pinniped Supervisor requires
that your OIDC provider returns refresh tokens to the Supervisor
from the authorization flows. Some OIDC providers may require
a certain value for the "prompt" parameter in order to properly
request refresh tokens. See the documentation of your OIDC provider's
authorization endpoint for its requirements for what to include
in the request in order to receive a refresh token in the response,
if anything. If your provider requires the prompt parameter
to request a refresh token, then include it here. Also note
that most providers also require a certain scope to be requested
in order to receive refresh tokens. See the additionalScopes
setting for more information about using scopes to request refresh
tokens.
items:
description: Parameter is a key/value pair which represents
a parameter in an HTTP request.
properties:
name:
description: The name of the parameter. Required.
minLength: 1
type: string
value:
description: The value of the parameter.
type: string
required:
- name
type: object
type: array
x-kubernetes-list-map-keys:
- name
x-kubernetes-list-type: map
additionalScopes: additionalScopes:
description: AdditionalScopes are the scopes in addition to "openid" description: 'additionalScopes are the additional scopes that
that will be requested as part of the authorization request will be requested from your OIDC provider in the authorization
flow with an OIDC identity provider. In the case of a Resource request during an OIDC Authorization Code Flow and in the token
Owner Password Credentials Grant flow, AdditionalScopes are request during a Resource Owner Password Credentials Grant.
the scopes in addition to "openid" that will be requested as Note that the "openid" scope will always be requested regardless
part of the token request (see also the allowPasswordGrant field). of the value in this setting, since it is always required according
By default, only the "openid" scope will be requested. to the OIDC spec. By default, when this field is not set, the
Supervisor will request the following scopes: "openid", "offline_access",
"email", and "profile". See https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims
for a description of the "profile" and "email" scopes. See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess
for a description of the "offline_access" scope. This default
value may change in future versions of Pinniped as the standard
evolves, or as common patterns used by providers who implement
the standard in the ecosystem evolve. By setting this list to
anything other than an empty list, you are overriding the default
value, so you may wish to include some of "offline_access",
"email", and "profile" in your override list. If you do not
want any of these scopes to be requested, you may set this list
to contain only "openid". Some OIDC providers may also require
a scope to get access to the user''s group membership, in which
case you may wish to include it in this list. Sometimes the
scope to request the user''s group membership is called "groups",
but unfortunately this is not specified in the OIDC standard.
Generally speaking, you should include any scopes required to
cause the appropriate claims to be the returned by your OIDC
provider in the ID token or userinfo endpoint results for those
claims which you would like to use in the oidcClaims settings
to determine the usernames and group memberships of your Kubernetes
users. See your OIDC provider''s documentation for more information
about what scopes are available to request claims. Additionally,
the Pinniped Supervisor requires that your OIDC provider returns
refresh tokens to the Supervisor from these authorization flows.
For most OIDC providers, the scope required to receive refresh
tokens will be "offline_access". See the documentation of your
OIDC provider''s authorization and token endpoints for its requirements
for what to include in the request in order to receive a refresh
token in the response, if anything. Note that it may be safe
to send "offline_access" even to providers which do not require
it, since the provider may ignore scopes that it does not understand
or require (see https://datatracker.ietf.org/doc/html/rfc6749#section-3.3).
In the unusual case that you must avoid sending the "offline_access"
scope, then you must override the default value of this setting.
This is required if your OIDC provider will reject the request
when it includes "offline_access" (e.g. GitLab''s OIDC provider).'
items: items:
type: string type: string
type: array type: array
allowPasswordGrant: allowPasswordGrant:
description: AllowPasswordGrant, when true, will allow the use description: allowPasswordGrant, when true, will allow the use
of OAuth 2.0's Resource Owner Password Credentials Grant (see of OAuth 2.0's Resource Owner Password Credentials Grant (see
https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) to https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) to
authenticate to the OIDC provider using a username and password authenticate to the OIDC provider using a username and password
@ -94,7 +178,7 @@ spec:
your end users' passwords (similar to LDAPIdentityProvider), your end users' passwords (similar to LDAPIdentityProvider),
and you will not be able to require multi-factor authentication and you will not be able to require multi-factor authentication
or use the other web-based login features of your OIDC provider or use the other web-based login features of your OIDC provider
during Resource Owner Password Credentials Grant logins. AllowPasswordGrant during Resource Owner Password Credentials Grant logins. allowPasswordGrant
defaults to false. defaults to false.
type: boolean type: boolean
type: object type: object
@ -103,12 +187,19 @@ spec:
used when inspecting an identity from this OIDC identity provider. used when inspecting an identity from this OIDC identity provider.
properties: properties:
groups: groups:
description: Groups provides the name of the token claim that description: Groups provides the name of the ID token claim or
will be used to ascertain the groups to which an identity belongs. userinfo endpoint response claim that will be used to ascertain
the groups to which an identity belongs. By default, the identities
will not include any group memberships when this setting is
not configured.
type: string type: string
username: username:
description: Username provides the name of the token claim that description: Username provides the name of the ID token claim
will be used to ascertain an identity's username. or userinfo endpoint response claim that will be used to ascertain
an identity's username. When not set, the username will be an
automatically constructed unique string which will include the
issuer URL of your OIDC provider along with the value of the
"sub" (subject) claim from the ID token.
type: string type: string
type: object type: object
client: client:

View File

@ -1099,8 +1099,9 @@ OIDCAuthorizationConfig provides information about how to form the OAuth2 author
[cols="25a,75a", options="header"] [cols="25a,75a", options="header"]
|=== |===
| Field | Description | Field | Description
| *`additionalScopes`* __string array__ | AdditionalScopes are the scopes in addition to "openid" that will be requested as part of the authorization request flow with an OIDC identity provider. In the case of a Resource Owner Password Credentials Grant flow, AdditionalScopes are the scopes in addition to "openid" that will be requested as part of the token request (see also the allowPasswordGrant field). By default, only the "openid" scope will be requested. | *`additionalScopes`* __string array__ | additionalScopes are the additional scopes that will be requested from your OIDC provider in the authorization request during an OIDC Authorization Code Flow and in the token request during a Resource Owner Password Credentials Grant. Note that the "openid" scope will always be requested regardless of the value in this setting, since it is always required according to the OIDC spec. By default, when this field is not set, the Supervisor will request the following scopes: "openid", "offline_access", "email", and "profile". See https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims for a description of the "profile" and "email" scopes. See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess for a description of the "offline_access" scope. This default value may change in future versions of Pinniped as the standard evolves, or as common patterns used by providers who implement the standard in the ecosystem evolve. By setting this list to anything other than an empty list, you are overriding the default value, so you may wish to include some of "offline_access", "email", and "profile" in your override list. If you do not want any of these scopes to be requested, you may set this list to contain only "openid". Some OIDC providers may also require a scope to get access to the user's group membership, in which case you may wish to include it in this list. Sometimes the scope to request the user's group membership is called "groups", but unfortunately this is not specified in the OIDC standard. Generally speaking, you should include any scopes required to cause the appropriate claims to be the returned by your OIDC provider in the ID token or userinfo endpoint results for those claims which you would like to use in the oidcClaims settings to determine the usernames and group memberships of your Kubernetes users. See your OIDC provider's documentation for more information about what scopes are available to request claims. Additionally, the Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor from these authorization flows. For most OIDC providers, the scope required to receive refresh tokens will be "offline_access". See the documentation of your OIDC provider's authorization and token endpoints for its requirements for what to include in the request in order to receive a refresh token in the response, if anything. Note that it may be safe to send "offline_access" even to providers which do not require it, since the provider may ignore scopes that it does not understand or require (see https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). In the unusual case that you must avoid sending the "offline_access" scope, then you must override the default value of this setting. This is required if your OIDC provider will reject the request when it includes "offline_access" (e.g. GitLab's OIDC provider).
| *`allowPasswordGrant`* __boolean__ | AllowPasswordGrant, when true, will allow the use of OAuth 2.0's Resource Owner Password Credentials Grant (see https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) to authenticate to the OIDC provider using a username and password without a web browser, in addition to the usual browser-based OIDC Authorization Code Flow. The Resource Owner Password Credentials Grant is not officially part of the OIDC specification, so it may not be supported by your OIDC provider. If your OIDC provider supports returning ID tokens from a Resource Owner Password Credentials Grant token request, then you can choose to set this field to true. This will allow end users to choose to present their username and password to the kubectl CLI (using the Pinniped plugin) to authenticate to the cluster, without using a web browser to log in as is customary in OIDC Authorization Code Flow. This may be convenient for users, especially for identities from your OIDC provider which are not intended to represent a human actor, such as service accounts performing actions in a CI/CD environment. Even if your OIDC provider supports it, you may wish to disable this behavior by setting this field to false when you prefer to only allow users of this OIDCIdentityProvider to log in via the browser-based OIDC Authorization Code Flow. Using the Resource Owner Password Credentials Grant means that the Pinniped CLI and Pinniped Supervisor will directly handle your end users' passwords (similar to LDAPIdentityProvider), and you will not be able to require multi-factor authentication or use the other web-based login features of your OIDC provider during Resource Owner Password Credentials Grant logins. AllowPasswordGrant defaults to false. | *`additionalAuthorizeParameters`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-parameter[$$Parameter$$] array__ | additionalAuthorizeParameters are extra query parameters that should be included in the authorize request to your OIDC provider in the authorization request during an OIDC Authorization Code Flow. By default, no extra parameters are sent. The standard parameters that will be sent are "response_type", "scope", "client_id", "state", "nonce", "code_challenge", "code_challenge_method", and "redirect_uri". These parameters cannot be included in this setting. Additionally, the "hd" parameter cannot be included in this setting at this time. The "hd" parameter is used by Google's OIDC provider to provide a hint as to which "hosted domain" the user should use during login. However, Pinniped does not yet support validating the hosted domain in the resulting ID token, so it is not yet safe to use this feature of Google's OIDC provider with Pinniped. This setting does not influence the parameters sent to the token endpoint in the Resource Owner Password Credentials Grant. The Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor from the authorization flows. Some OIDC providers may require a certain value for the "prompt" parameter in order to properly request refresh tokens. See the documentation of your OIDC provider's authorization endpoint for its requirements for what to include in the request in order to receive a refresh token in the response, if anything. If your provider requires the prompt parameter to request a refresh token, then include it here. Also note that most providers also require a certain scope to be requested in order to receive refresh tokens. See the additionalScopes setting for more information about using scopes to request refresh tokens.
| *`allowPasswordGrant`* __boolean__ | allowPasswordGrant, when true, will allow the use of OAuth 2.0's Resource Owner Password Credentials Grant (see https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) to authenticate to the OIDC provider using a username and password without a web browser, in addition to the usual browser-based OIDC Authorization Code Flow. The Resource Owner Password Credentials Grant is not officially part of the OIDC specification, so it may not be supported by your OIDC provider. If your OIDC provider supports returning ID tokens from a Resource Owner Password Credentials Grant token request, then you can choose to set this field to true. This will allow end users to choose to present their username and password to the kubectl CLI (using the Pinniped plugin) to authenticate to the cluster, without using a web browser to log in as is customary in OIDC Authorization Code Flow. This may be convenient for users, especially for identities from your OIDC provider which are not intended to represent a human actor, such as service accounts performing actions in a CI/CD environment. Even if your OIDC provider supports it, you may wish to disable this behavior by setting this field to false when you prefer to only allow users of this OIDCIdentityProvider to log in via the browser-based OIDC Authorization Code Flow. Using the Resource Owner Password Credentials Grant means that the Pinniped CLI and Pinniped Supervisor will directly handle your end users' passwords (similar to LDAPIdentityProvider), and you will not be able to require multi-factor authentication or use the other web-based login features of your OIDC provider during Resource Owner Password Credentials Grant logins. allowPasswordGrant defaults to false.
|=== |===
@ -1117,8 +1118,8 @@ OIDCClaims provides a mapping from upstream claims into identities.
[cols="25a,75a", options="header"] [cols="25a,75a", options="header"]
|=== |===
| Field | Description | Field | Description
| *`groups`* __string__ | Groups provides the name of the token claim that will be used to ascertain the groups to which an identity belongs. | *`groups`* __string__ | Groups provides the name of the ID token claim or userinfo endpoint response claim that will be used to ascertain the groups to which an identity belongs. By default, the identities will not include any group memberships when this setting is not configured.
| *`username`* __string__ | Username provides the name of the token claim that will be used to ascertain an identity's username. | *`username`* __string__ | Username provides the name of the ID token claim or userinfo endpoint response claim that will be used to ascertain an identity's username. When not set, the username will be an automatically constructed unique string which will include the issuer URL of your OIDC provider along with the value of the "sub" (subject) claim from the ID token.
|=== |===
@ -1164,7 +1165,7 @@ OIDCIdentityProvider describes the configuration of an upstream OpenID Connect i
[id="{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-oidcidentityproviderspec"] [id="{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-oidcidentityproviderspec"]
==== OIDCIdentityProviderSpec ==== OIDCIdentityProviderSpec
Spec for configuring an OIDC identity provider. OIDCIdentityProviderSpec is the spec for configuring an OIDC identity provider.
.Appears In: .Appears In:
**** ****
@ -1185,7 +1186,7 @@ Spec for configuring an OIDC identity provider.
[id="{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-oidcidentityproviderstatus"] [id="{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-oidcidentityproviderstatus"]
==== OIDCIdentityProviderStatus ==== OIDCIdentityProviderStatus
Status of an OIDC identity provider. OIDCIdentityProviderStatus is the status of an OIDC identity provider.
.Appears In: .Appears In:
**** ****
@ -1200,6 +1201,24 @@ Status of an OIDC identity provider.
|=== |===
[id="{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-parameter"]
==== Parameter
.Appears In:
****
- xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-oidcauthorizationconfig[$$OIDCAuthorizationConfig$$]
****
[cols="25a,75a", options="header"]
|===
| Field | Description
| *`name`* __string__ | The name of the parameter. Required.
| *`value`* __string__ | The value of the parameter.
|===
[id="{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-tlsspec"] [id="{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-tlsspec"]
==== TLSSpec ==== TLSSpec

View File

@ -20,7 +20,7 @@ const (
PhaseError OIDCIdentityProviderPhase = "Error" PhaseError OIDCIdentityProviderPhase = "Error"
) )
// Status of an OIDC identity provider. // OIDCIdentityProviderStatus is the status of an OIDC identity provider.
type OIDCIdentityProviderStatus struct { type OIDCIdentityProviderStatus struct {
// Phase summarizes the overall status of the OIDCIdentityProvider. // Phase summarizes the overall status of the OIDCIdentityProvider.
// +kubebuilder:default=Pending // +kubebuilder:default=Pending
@ -38,15 +38,62 @@ type OIDCIdentityProviderStatus struct {
// OIDCAuthorizationConfig provides information about how to form the OAuth2 authorization // OIDCAuthorizationConfig provides information about how to form the OAuth2 authorization
// request parameters. // request parameters.
type OIDCAuthorizationConfig struct { type OIDCAuthorizationConfig struct {
// AdditionalScopes are the scopes in addition to "openid" that will be requested as part of the authorization // additionalScopes are the additional scopes that will be requested from your OIDC provider in the authorization
// request flow with an OIDC identity provider. // request during an OIDC Authorization Code Flow and in the token request during a Resource Owner Password Credentials
// In the case of a Resource Owner Password Credentials Grant flow, AdditionalScopes are the scopes // Grant. Note that the "openid" scope will always be requested regardless of the value in this setting, since it is
// in addition to "openid" that will be requested as part of the token request (see also the allowPasswordGrant field). // always required according to the OIDC spec. By default, when this field is not set, the Supervisor will request
// By default, only the "openid" scope will be requested. // the following scopes: "openid", "offline_access", "email", and "profile". See
// https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims for a description of the "profile" and "email"
// scopes. See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess for a description of the
// "offline_access" scope. This default value may change in future versions of Pinniped as the standard evolves,
// or as common patterns used by providers who implement the standard in the ecosystem evolve.
// By setting this list to anything other than an empty list, you are overriding the
// default value, so you may wish to include some of "offline_access", "email", and "profile" in your override list.
// If you do not want any of these scopes to be requested, you may set this list to contain only "openid".
// Some OIDC providers may also require a scope to get access to the user's group membership, in which case you
// may wish to include it in this list. Sometimes the scope to request the user's group membership is called
// "groups", but unfortunately this is not specified in the OIDC standard.
// Generally speaking, you should include any scopes required to cause the appropriate claims to be the returned by
// your OIDC provider in the ID token or userinfo endpoint results for those claims which you would like to use in
// the oidcClaims settings to determine the usernames and group memberships of your Kubernetes users. See
// your OIDC provider's documentation for more information about what scopes are available to request claims.
// Additionally, the Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor
// from these authorization flows. For most OIDC providers, the scope required to receive refresh tokens will be
// "offline_access". See the documentation of your OIDC provider's authorization and token endpoints for its
// requirements for what to include in the request in order to receive a refresh token in the response, if anything.
// Note that it may be safe to send "offline_access" even to providers which do not require it, since the provider
// may ignore scopes that it does not understand or require (see
// https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). In the unusual case that you must avoid sending the
// "offline_access" scope, then you must override the default value of this setting. This is required if your OIDC
// provider will reject the request when it includes "offline_access" (e.g. GitLab's OIDC provider).
// +optional // +optional
AdditionalScopes []string `json:"additionalScopes,omitempty"` AdditionalScopes []string `json:"additionalScopes,omitempty"`
// AllowPasswordGrant, when true, will allow the use of OAuth 2.0's Resource Owner Password Credentials Grant // additionalAuthorizeParameters are extra query parameters that should be included in the authorize request to your
// OIDC provider in the authorization request during an OIDC Authorization Code Flow. By default, no extra
// parameters are sent. The standard parameters that will be sent are "response_type", "scope", "client_id",
// "state", "nonce", "code_challenge", "code_challenge_method", and "redirect_uri". These parameters cannot be
// included in this setting. Additionally, the "hd" parameter cannot be included in this setting at this time.
// The "hd" parameter is used by Google's OIDC provider to provide a hint as to which "hosted domain" the user
// should use during login. However, Pinniped does not yet support validating the hosted domain in the resulting
// ID token, so it is not yet safe to use this feature of Google's OIDC provider with Pinniped.
// This setting does not influence the parameters sent to the token endpoint in the Resource Owner Password
// Credentials Grant. The Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the
// Supervisor from the authorization flows. Some OIDC providers may require a certain value for the "prompt"
// parameter in order to properly request refresh tokens. See the documentation of your OIDC provider's
// authorization endpoint for its requirements for what to include in the request in order to receive a refresh
// token in the response, if anything. If your provider requires the prompt parameter to request a refresh token,
// then include it here. Also note that most providers also require a certain scope to be requested in order to
// receive refresh tokens. See the additionalScopes setting for more information about using scopes to request
// refresh tokens.
// +optional
// +patchMergeKey=name
// +patchStrategy=merge
// +listType=map
// +listMapKey=name
AdditionalAuthorizeParameters []Parameter `json:"additionalAuthorizeParameters,omitempty"`
// allowPasswordGrant, when true, will allow the use of OAuth 2.0's Resource Owner Password Credentials Grant
// (see https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) to authenticate to the OIDC provider using a // (see https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) to authenticate to the OIDC provider using a
// username and password without a web browser, in addition to the usual browser-based OIDC Authorization Code Flow. // username and password without a web browser, in addition to the usual browser-based OIDC Authorization Code Flow.
// The Resource Owner Password Credentials Grant is not officially part of the OIDC specification, so it may not be // The Resource Owner Password Credentials Grant is not officially part of the OIDC specification, so it may not be
@ -61,20 +108,34 @@ type OIDCAuthorizationConfig struct {
// Credentials Grant means that the Pinniped CLI and Pinniped Supervisor will directly handle your end users' passwords // Credentials Grant means that the Pinniped CLI and Pinniped Supervisor will directly handle your end users' passwords
// (similar to LDAPIdentityProvider), and you will not be able to require multi-factor authentication or use the other // (similar to LDAPIdentityProvider), and you will not be able to require multi-factor authentication or use the other
// web-based login features of your OIDC provider during Resource Owner Password Credentials Grant logins. // web-based login features of your OIDC provider during Resource Owner Password Credentials Grant logins.
// AllowPasswordGrant defaults to false. // allowPasswordGrant defaults to false.
// +optional // +optional
AllowPasswordGrant bool `json:"allowPasswordGrant,omitempty"` AllowPasswordGrant bool `json:"allowPasswordGrant,omitempty"`
} }
// Parameter is a key/value pair which represents a parameter in an HTTP request.
type Parameter struct {
// The name of the parameter. Required.
// +kubebuilder:validation:MinLength=1
Name string `json:"name"`
// The value of the parameter.
// +optional
Value string `json:"value,omitempty"`
}
// OIDCClaims provides a mapping from upstream claims into identities. // OIDCClaims provides a mapping from upstream claims into identities.
type OIDCClaims struct { type OIDCClaims struct {
// Groups provides the name of the token claim that will be used to ascertain the groups to which // Groups provides the name of the ID token claim or userinfo endpoint response claim that will be used to ascertain
// an identity belongs. // the groups to which an identity belongs. By default, the identities will not include any group memberships when
// this setting is not configured.
// +optional // +optional
Groups string `json:"groups"` Groups string `json:"groups"`
// Username provides the name of the token claim that will be used to ascertain an identity's // Username provides the name of the ID token claim or userinfo endpoint response claim that will be used to
// username. // ascertain an identity's username. When not set, the username will be an automatically constructed unique string
// which will include the issuer URL of your OIDC provider along with the value of the "sub" (subject) claim from
// the ID token.
// +optional // +optional
Username string `json:"username"` Username string `json:"username"`
} }
@ -89,7 +150,7 @@ type OIDCClient struct {
SecretName string `json:"secretName"` SecretName string `json:"secretName"`
} }
// Spec for configuring an OIDC identity provider. // OIDCIdentityProviderSpec is the spec for configuring an OIDC identity provider.
type OIDCIdentityProviderSpec struct { type OIDCIdentityProviderSpec struct {
// Issuer is the issuer URL of this OIDC identity provider, i.e., where to fetch // Issuer is the issuer URL of this OIDC identity provider, i.e., where to fetch
// /.well-known/openid-configuration. // /.well-known/openid-configuration.
@ -135,7 +196,7 @@ type OIDCIdentityProvider struct {
Status OIDCIdentityProviderStatus `json:"status,omitempty"` Status OIDCIdentityProviderStatus `json:"status,omitempty"`
} }
// List of OIDCIdentityProvider objects. // OIDCIdentityProviderList lists OIDCIdentityProvider objects.
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type OIDCIdentityProviderList struct { type OIDCIdentityProviderList struct {
metav1.TypeMeta `json:",inline"` metav1.TypeMeta `json:",inline"`

View File

@ -416,6 +416,11 @@ func (in *OIDCAuthorizationConfig) DeepCopyInto(out *OIDCAuthorizationConfig) {
*out = make([]string, len(*in)) *out = make([]string, len(*in))
copy(*out, *in) copy(*out, *in)
} }
if in.AdditionalAuthorizeParameters != nil {
in, out := &in.AdditionalAuthorizeParameters, &out.AdditionalAuthorizeParameters
*out = make([]Parameter, len(*in))
copy(*out, *in)
}
return return
} }
@ -569,6 +574,22 @@ func (in *OIDCIdentityProviderStatus) DeepCopy() *OIDCIdentityProviderStatus {
return out return out
} }
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Parameter) DeepCopyInto(out *Parameter) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Parameter.
func (in *Parameter) DeepCopy() *Parameter {
if in == nil {
return nil
}
out := new(Parameter)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TLSSpec) DeepCopyInto(out *TLSSpec) { func (in *TLSSpec) DeepCopyInto(out *TLSSpec) {
*out = *in *out = *in

View File

@ -56,19 +56,103 @@ spec:
the OAuth2 authorization request parameters to be used with this the OAuth2 authorization request parameters to be used with this
OIDC identity provider. OIDC identity provider.
properties: properties:
additionalAuthorizeParameters:
description: additionalAuthorizeParameters are extra query parameters
that should be included in the authorize request to your OIDC
provider in the authorization request during an OIDC Authorization
Code Flow. By default, no extra parameters are sent. The standard
parameters that will be sent are "response_type", "scope", "client_id",
"state", "nonce", "code_challenge", "code_challenge_method",
and "redirect_uri". These parameters cannot be included in this
setting. Additionally, the "hd" parameter cannot be included
in this setting at this time. The "hd" parameter is used by
Google's OIDC provider to provide a hint as to which "hosted
domain" the user should use during login. However, Pinniped
does not yet support validating the hosted domain in the resulting
ID token, so it is not yet safe to use this feature of Google's
OIDC provider with Pinniped. This setting does not influence
the parameters sent to the token endpoint in the Resource Owner
Password Credentials Grant. The Pinniped Supervisor requires
that your OIDC provider returns refresh tokens to the Supervisor
from the authorization flows. Some OIDC providers may require
a certain value for the "prompt" parameter in order to properly
request refresh tokens. See the documentation of your OIDC provider's
authorization endpoint for its requirements for what to include
in the request in order to receive a refresh token in the response,
if anything. If your provider requires the prompt parameter
to request a refresh token, then include it here. Also note
that most providers also require a certain scope to be requested
in order to receive refresh tokens. See the additionalScopes
setting for more information about using scopes to request refresh
tokens.
items:
description: Parameter is a key/value pair which represents
a parameter in an HTTP request.
properties:
name:
description: The name of the parameter. Required.
minLength: 1
type: string
value:
description: The value of the parameter.
type: string
required:
- name
type: object
type: array
x-kubernetes-list-map-keys:
- name
x-kubernetes-list-type: map
additionalScopes: additionalScopes:
description: AdditionalScopes are the scopes in addition to "openid" description: 'additionalScopes are the additional scopes that
that will be requested as part of the authorization request will be requested from your OIDC provider in the authorization
flow with an OIDC identity provider. In the case of a Resource request during an OIDC Authorization Code Flow and in the token
Owner Password Credentials Grant flow, AdditionalScopes are request during a Resource Owner Password Credentials Grant.
the scopes in addition to "openid" that will be requested as Note that the "openid" scope will always be requested regardless
part of the token request (see also the allowPasswordGrant field). of the value in this setting, since it is always required according
By default, only the "openid" scope will be requested. to the OIDC spec. By default, when this field is not set, the
Supervisor will request the following scopes: "openid", "offline_access",
"email", and "profile". See https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims
for a description of the "profile" and "email" scopes. See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess
for a description of the "offline_access" scope. This default
value may change in future versions of Pinniped as the standard
evolves, or as common patterns used by providers who implement
the standard in the ecosystem evolve. By setting this list to
anything other than an empty list, you are overriding the default
value, so you may wish to include some of "offline_access",
"email", and "profile" in your override list. If you do not
want any of these scopes to be requested, you may set this list
to contain only "openid". Some OIDC providers may also require
a scope to get access to the user''s group membership, in which
case you may wish to include it in this list. Sometimes the
scope to request the user''s group membership is called "groups",
but unfortunately this is not specified in the OIDC standard.
Generally speaking, you should include any scopes required to
cause the appropriate claims to be the returned by your OIDC
provider in the ID token or userinfo endpoint results for those
claims which you would like to use in the oidcClaims settings
to determine the usernames and group memberships of your Kubernetes
users. See your OIDC provider''s documentation for more information
about what scopes are available to request claims. Additionally,
the Pinniped Supervisor requires that your OIDC provider returns
refresh tokens to the Supervisor from these authorization flows.
For most OIDC providers, the scope required to receive refresh
tokens will be "offline_access". See the documentation of your
OIDC provider''s authorization and token endpoints for its requirements
for what to include in the request in order to receive a refresh
token in the response, if anything. Note that it may be safe
to send "offline_access" even to providers which do not require
it, since the provider may ignore scopes that it does not understand
or require (see https://datatracker.ietf.org/doc/html/rfc6749#section-3.3).
In the unusual case that you must avoid sending the "offline_access"
scope, then you must override the default value of this setting.
This is required if your OIDC provider will reject the request
when it includes "offline_access" (e.g. GitLab''s OIDC provider).'
items: items:
type: string type: string
type: array type: array
allowPasswordGrant: allowPasswordGrant:
description: AllowPasswordGrant, when true, will allow the use description: allowPasswordGrant, when true, will allow the use
of OAuth 2.0's Resource Owner Password Credentials Grant (see of OAuth 2.0's Resource Owner Password Credentials Grant (see
https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) to https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) to
authenticate to the OIDC provider using a username and password authenticate to the OIDC provider using a username and password
@ -94,7 +178,7 @@ spec:
your end users' passwords (similar to LDAPIdentityProvider), your end users' passwords (similar to LDAPIdentityProvider),
and you will not be able to require multi-factor authentication and you will not be able to require multi-factor authentication
or use the other web-based login features of your OIDC provider or use the other web-based login features of your OIDC provider
during Resource Owner Password Credentials Grant logins. AllowPasswordGrant during Resource Owner Password Credentials Grant logins. allowPasswordGrant
defaults to false. defaults to false.
type: boolean type: boolean
type: object type: object
@ -103,12 +187,19 @@ spec:
used when inspecting an identity from this OIDC identity provider. used when inspecting an identity from this OIDC identity provider.
properties: properties:
groups: groups:
description: Groups provides the name of the token claim that description: Groups provides the name of the ID token claim or
will be used to ascertain the groups to which an identity belongs. userinfo endpoint response claim that will be used to ascertain
the groups to which an identity belongs. By default, the identities
will not include any group memberships when this setting is
not configured.
type: string type: string
username: username:
description: Username provides the name of the token claim that description: Username provides the name of the ID token claim
will be used to ascertain an identity's username. or userinfo endpoint response claim that will be used to ascertain
an identity's username. When not set, the username will be an
automatically constructed unique string which will include the
issuer URL of your OIDC provider along with the value of the
"sub" (subject) claim from the ID token.
type: string type: string
type: object type: object
client: client:

View File

@ -1099,8 +1099,9 @@ OIDCAuthorizationConfig provides information about how to form the OAuth2 author
[cols="25a,75a", options="header"] [cols="25a,75a", options="header"]
|=== |===
| Field | Description | Field | Description
| *`additionalScopes`* __string array__ | AdditionalScopes are the scopes in addition to "openid" that will be requested as part of the authorization request flow with an OIDC identity provider. In the case of a Resource Owner Password Credentials Grant flow, AdditionalScopes are the scopes in addition to "openid" that will be requested as part of the token request (see also the allowPasswordGrant field). By default, only the "openid" scope will be requested. | *`additionalScopes`* __string array__ | additionalScopes are the additional scopes that will be requested from your OIDC provider in the authorization request during an OIDC Authorization Code Flow and in the token request during a Resource Owner Password Credentials Grant. Note that the "openid" scope will always be requested regardless of the value in this setting, since it is always required according to the OIDC spec. By default, when this field is not set, the Supervisor will request the following scopes: "openid", "offline_access", "email", and "profile". See https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims for a description of the "profile" and "email" scopes. See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess for a description of the "offline_access" scope. This default value may change in future versions of Pinniped as the standard evolves, or as common patterns used by providers who implement the standard in the ecosystem evolve. By setting this list to anything other than an empty list, you are overriding the default value, so you may wish to include some of "offline_access", "email", and "profile" in your override list. If you do not want any of these scopes to be requested, you may set this list to contain only "openid". Some OIDC providers may also require a scope to get access to the user's group membership, in which case you may wish to include it in this list. Sometimes the scope to request the user's group membership is called "groups", but unfortunately this is not specified in the OIDC standard. Generally speaking, you should include any scopes required to cause the appropriate claims to be the returned by your OIDC provider in the ID token or userinfo endpoint results for those claims which you would like to use in the oidcClaims settings to determine the usernames and group memberships of your Kubernetes users. See your OIDC provider's documentation for more information about what scopes are available to request claims. Additionally, the Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor from these authorization flows. For most OIDC providers, the scope required to receive refresh tokens will be "offline_access". See the documentation of your OIDC provider's authorization and token endpoints for its requirements for what to include in the request in order to receive a refresh token in the response, if anything. Note that it may be safe to send "offline_access" even to providers which do not require it, since the provider may ignore scopes that it does not understand or require (see https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). In the unusual case that you must avoid sending the "offline_access" scope, then you must override the default value of this setting. This is required if your OIDC provider will reject the request when it includes "offline_access" (e.g. GitLab's OIDC provider).
| *`allowPasswordGrant`* __boolean__ | AllowPasswordGrant, when true, will allow the use of OAuth 2.0's Resource Owner Password Credentials Grant (see https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) to authenticate to the OIDC provider using a username and password without a web browser, in addition to the usual browser-based OIDC Authorization Code Flow. The Resource Owner Password Credentials Grant is not officially part of the OIDC specification, so it may not be supported by your OIDC provider. If your OIDC provider supports returning ID tokens from a Resource Owner Password Credentials Grant token request, then you can choose to set this field to true. This will allow end users to choose to present their username and password to the kubectl CLI (using the Pinniped plugin) to authenticate to the cluster, without using a web browser to log in as is customary in OIDC Authorization Code Flow. This may be convenient for users, especially for identities from your OIDC provider which are not intended to represent a human actor, such as service accounts performing actions in a CI/CD environment. Even if your OIDC provider supports it, you may wish to disable this behavior by setting this field to false when you prefer to only allow users of this OIDCIdentityProvider to log in via the browser-based OIDC Authorization Code Flow. Using the Resource Owner Password Credentials Grant means that the Pinniped CLI and Pinniped Supervisor will directly handle your end users' passwords (similar to LDAPIdentityProvider), and you will not be able to require multi-factor authentication or use the other web-based login features of your OIDC provider during Resource Owner Password Credentials Grant logins. AllowPasswordGrant defaults to false. | *`additionalAuthorizeParameters`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-parameter[$$Parameter$$] array__ | additionalAuthorizeParameters are extra query parameters that should be included in the authorize request to your OIDC provider in the authorization request during an OIDC Authorization Code Flow. By default, no extra parameters are sent. The standard parameters that will be sent are "response_type", "scope", "client_id", "state", "nonce", "code_challenge", "code_challenge_method", and "redirect_uri". These parameters cannot be included in this setting. Additionally, the "hd" parameter cannot be included in this setting at this time. The "hd" parameter is used by Google's OIDC provider to provide a hint as to which "hosted domain" the user should use during login. However, Pinniped does not yet support validating the hosted domain in the resulting ID token, so it is not yet safe to use this feature of Google's OIDC provider with Pinniped. This setting does not influence the parameters sent to the token endpoint in the Resource Owner Password Credentials Grant. The Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor from the authorization flows. Some OIDC providers may require a certain value for the "prompt" parameter in order to properly request refresh tokens. See the documentation of your OIDC provider's authorization endpoint for its requirements for what to include in the request in order to receive a refresh token in the response, if anything. If your provider requires the prompt parameter to request a refresh token, then include it here. Also note that most providers also require a certain scope to be requested in order to receive refresh tokens. See the additionalScopes setting for more information about using scopes to request refresh tokens.
| *`allowPasswordGrant`* __boolean__ | allowPasswordGrant, when true, will allow the use of OAuth 2.0's Resource Owner Password Credentials Grant (see https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) to authenticate to the OIDC provider using a username and password without a web browser, in addition to the usual browser-based OIDC Authorization Code Flow. The Resource Owner Password Credentials Grant is not officially part of the OIDC specification, so it may not be supported by your OIDC provider. If your OIDC provider supports returning ID tokens from a Resource Owner Password Credentials Grant token request, then you can choose to set this field to true. This will allow end users to choose to present their username and password to the kubectl CLI (using the Pinniped plugin) to authenticate to the cluster, without using a web browser to log in as is customary in OIDC Authorization Code Flow. This may be convenient for users, especially for identities from your OIDC provider which are not intended to represent a human actor, such as service accounts performing actions in a CI/CD environment. Even if your OIDC provider supports it, you may wish to disable this behavior by setting this field to false when you prefer to only allow users of this OIDCIdentityProvider to log in via the browser-based OIDC Authorization Code Flow. Using the Resource Owner Password Credentials Grant means that the Pinniped CLI and Pinniped Supervisor will directly handle your end users' passwords (similar to LDAPIdentityProvider), and you will not be able to require multi-factor authentication or use the other web-based login features of your OIDC provider during Resource Owner Password Credentials Grant logins. allowPasswordGrant defaults to false.
|=== |===
@ -1117,8 +1118,8 @@ OIDCClaims provides a mapping from upstream claims into identities.
[cols="25a,75a", options="header"] [cols="25a,75a", options="header"]
|=== |===
| Field | Description | Field | Description
| *`groups`* __string__ | Groups provides the name of the token claim that will be used to ascertain the groups to which an identity belongs. | *`groups`* __string__ | Groups provides the name of the ID token claim or userinfo endpoint response claim that will be used to ascertain the groups to which an identity belongs. By default, the identities will not include any group memberships when this setting is not configured.
| *`username`* __string__ | Username provides the name of the token claim that will be used to ascertain an identity's username. | *`username`* __string__ | Username provides the name of the ID token claim or userinfo endpoint response claim that will be used to ascertain an identity's username. When not set, the username will be an automatically constructed unique string which will include the issuer URL of your OIDC provider along with the value of the "sub" (subject) claim from the ID token.
|=== |===
@ -1164,7 +1165,7 @@ OIDCIdentityProvider describes the configuration of an upstream OpenID Connect i
[id="{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-oidcidentityproviderspec"] [id="{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-oidcidentityproviderspec"]
==== OIDCIdentityProviderSpec ==== OIDCIdentityProviderSpec
Spec for configuring an OIDC identity provider. OIDCIdentityProviderSpec is the spec for configuring an OIDC identity provider.
.Appears In: .Appears In:
**** ****
@ -1185,7 +1186,7 @@ Spec for configuring an OIDC identity provider.
[id="{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-oidcidentityproviderstatus"] [id="{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-oidcidentityproviderstatus"]
==== OIDCIdentityProviderStatus ==== OIDCIdentityProviderStatus
Status of an OIDC identity provider. OIDCIdentityProviderStatus is the status of an OIDC identity provider.
.Appears In: .Appears In:
**** ****
@ -1200,6 +1201,24 @@ Status of an OIDC identity provider.
|=== |===
[id="{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-parameter"]
==== Parameter
.Appears In:
****
- xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-oidcauthorizationconfig[$$OIDCAuthorizationConfig$$]
****
[cols="25a,75a", options="header"]
|===
| Field | Description
| *`name`* __string__ | The name of the parameter. Required.
| *`value`* __string__ | The value of the parameter.
|===
[id="{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-tlsspec"] [id="{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-tlsspec"]
==== TLSSpec ==== TLSSpec

View File

@ -20,7 +20,7 @@ const (
PhaseError OIDCIdentityProviderPhase = "Error" PhaseError OIDCIdentityProviderPhase = "Error"
) )
// Status of an OIDC identity provider. // OIDCIdentityProviderStatus is the status of an OIDC identity provider.
type OIDCIdentityProviderStatus struct { type OIDCIdentityProviderStatus struct {
// Phase summarizes the overall status of the OIDCIdentityProvider. // Phase summarizes the overall status of the OIDCIdentityProvider.
// +kubebuilder:default=Pending // +kubebuilder:default=Pending
@ -38,15 +38,62 @@ type OIDCIdentityProviderStatus struct {
// OIDCAuthorizationConfig provides information about how to form the OAuth2 authorization // OIDCAuthorizationConfig provides information about how to form the OAuth2 authorization
// request parameters. // request parameters.
type OIDCAuthorizationConfig struct { type OIDCAuthorizationConfig struct {
// AdditionalScopes are the scopes in addition to "openid" that will be requested as part of the authorization // additionalScopes are the additional scopes that will be requested from your OIDC provider in the authorization
// request flow with an OIDC identity provider. // request during an OIDC Authorization Code Flow and in the token request during a Resource Owner Password Credentials
// In the case of a Resource Owner Password Credentials Grant flow, AdditionalScopes are the scopes // Grant. Note that the "openid" scope will always be requested regardless of the value in this setting, since it is
// in addition to "openid" that will be requested as part of the token request (see also the allowPasswordGrant field). // always required according to the OIDC spec. By default, when this field is not set, the Supervisor will request
// By default, only the "openid" scope will be requested. // the following scopes: "openid", "offline_access", "email", and "profile". See
// https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims for a description of the "profile" and "email"
// scopes. See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess for a description of the
// "offline_access" scope. This default value may change in future versions of Pinniped as the standard evolves,
// or as common patterns used by providers who implement the standard in the ecosystem evolve.
// By setting this list to anything other than an empty list, you are overriding the
// default value, so you may wish to include some of "offline_access", "email", and "profile" in your override list.
// If you do not want any of these scopes to be requested, you may set this list to contain only "openid".
// Some OIDC providers may also require a scope to get access to the user's group membership, in which case you
// may wish to include it in this list. Sometimes the scope to request the user's group membership is called
// "groups", but unfortunately this is not specified in the OIDC standard.
// Generally speaking, you should include any scopes required to cause the appropriate claims to be the returned by
// your OIDC provider in the ID token or userinfo endpoint results for those claims which you would like to use in
// the oidcClaims settings to determine the usernames and group memberships of your Kubernetes users. See
// your OIDC provider's documentation for more information about what scopes are available to request claims.
// Additionally, the Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor
// from these authorization flows. For most OIDC providers, the scope required to receive refresh tokens will be
// "offline_access". See the documentation of your OIDC provider's authorization and token endpoints for its
// requirements for what to include in the request in order to receive a refresh token in the response, if anything.
// Note that it may be safe to send "offline_access" even to providers which do not require it, since the provider
// may ignore scopes that it does not understand or require (see
// https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). In the unusual case that you must avoid sending the
// "offline_access" scope, then you must override the default value of this setting. This is required if your OIDC
// provider will reject the request when it includes "offline_access" (e.g. GitLab's OIDC provider).
// +optional // +optional
AdditionalScopes []string `json:"additionalScopes,omitempty"` AdditionalScopes []string `json:"additionalScopes,omitempty"`
// AllowPasswordGrant, when true, will allow the use of OAuth 2.0's Resource Owner Password Credentials Grant // additionalAuthorizeParameters are extra query parameters that should be included in the authorize request to your
// OIDC provider in the authorization request during an OIDC Authorization Code Flow. By default, no extra
// parameters are sent. The standard parameters that will be sent are "response_type", "scope", "client_id",
// "state", "nonce", "code_challenge", "code_challenge_method", and "redirect_uri". These parameters cannot be
// included in this setting. Additionally, the "hd" parameter cannot be included in this setting at this time.
// The "hd" parameter is used by Google's OIDC provider to provide a hint as to which "hosted domain" the user
// should use during login. However, Pinniped does not yet support validating the hosted domain in the resulting
// ID token, so it is not yet safe to use this feature of Google's OIDC provider with Pinniped.
// This setting does not influence the parameters sent to the token endpoint in the Resource Owner Password
// Credentials Grant. The Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the
// Supervisor from the authorization flows. Some OIDC providers may require a certain value for the "prompt"
// parameter in order to properly request refresh tokens. See the documentation of your OIDC provider's
// authorization endpoint for its requirements for what to include in the request in order to receive a refresh
// token in the response, if anything. If your provider requires the prompt parameter to request a refresh token,
// then include it here. Also note that most providers also require a certain scope to be requested in order to
// receive refresh tokens. See the additionalScopes setting for more information about using scopes to request
// refresh tokens.
// +optional
// +patchMergeKey=name
// +patchStrategy=merge
// +listType=map
// +listMapKey=name
AdditionalAuthorizeParameters []Parameter `json:"additionalAuthorizeParameters,omitempty"`
// allowPasswordGrant, when true, will allow the use of OAuth 2.0's Resource Owner Password Credentials Grant
// (see https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) to authenticate to the OIDC provider using a // (see https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) to authenticate to the OIDC provider using a
// username and password without a web browser, in addition to the usual browser-based OIDC Authorization Code Flow. // username and password without a web browser, in addition to the usual browser-based OIDC Authorization Code Flow.
// The Resource Owner Password Credentials Grant is not officially part of the OIDC specification, so it may not be // The Resource Owner Password Credentials Grant is not officially part of the OIDC specification, so it may not be
@ -61,20 +108,34 @@ type OIDCAuthorizationConfig struct {
// Credentials Grant means that the Pinniped CLI and Pinniped Supervisor will directly handle your end users' passwords // Credentials Grant means that the Pinniped CLI and Pinniped Supervisor will directly handle your end users' passwords
// (similar to LDAPIdentityProvider), and you will not be able to require multi-factor authentication or use the other // (similar to LDAPIdentityProvider), and you will not be able to require multi-factor authentication or use the other
// web-based login features of your OIDC provider during Resource Owner Password Credentials Grant logins. // web-based login features of your OIDC provider during Resource Owner Password Credentials Grant logins.
// AllowPasswordGrant defaults to false. // allowPasswordGrant defaults to false.
// +optional // +optional
AllowPasswordGrant bool `json:"allowPasswordGrant,omitempty"` AllowPasswordGrant bool `json:"allowPasswordGrant,omitempty"`
} }
// Parameter is a key/value pair which represents a parameter in an HTTP request.
type Parameter struct {
// The name of the parameter. Required.
// +kubebuilder:validation:MinLength=1
Name string `json:"name"`
// The value of the parameter.
// +optional
Value string `json:"value,omitempty"`
}
// OIDCClaims provides a mapping from upstream claims into identities. // OIDCClaims provides a mapping from upstream claims into identities.
type OIDCClaims struct { type OIDCClaims struct {
// Groups provides the name of the token claim that will be used to ascertain the groups to which // Groups provides the name of the ID token claim or userinfo endpoint response claim that will be used to ascertain
// an identity belongs. // the groups to which an identity belongs. By default, the identities will not include any group memberships when
// this setting is not configured.
// +optional // +optional
Groups string `json:"groups"` Groups string `json:"groups"`
// Username provides the name of the token claim that will be used to ascertain an identity's // Username provides the name of the ID token claim or userinfo endpoint response claim that will be used to
// username. // ascertain an identity's username. When not set, the username will be an automatically constructed unique string
// which will include the issuer URL of your OIDC provider along with the value of the "sub" (subject) claim from
// the ID token.
// +optional // +optional
Username string `json:"username"` Username string `json:"username"`
} }
@ -89,7 +150,7 @@ type OIDCClient struct {
SecretName string `json:"secretName"` SecretName string `json:"secretName"`
} }
// Spec for configuring an OIDC identity provider. // OIDCIdentityProviderSpec is the spec for configuring an OIDC identity provider.
type OIDCIdentityProviderSpec struct { type OIDCIdentityProviderSpec struct {
// Issuer is the issuer URL of this OIDC identity provider, i.e., where to fetch // Issuer is the issuer URL of this OIDC identity provider, i.e., where to fetch
// /.well-known/openid-configuration. // /.well-known/openid-configuration.
@ -135,7 +196,7 @@ type OIDCIdentityProvider struct {
Status OIDCIdentityProviderStatus `json:"status,omitempty"` Status OIDCIdentityProviderStatus `json:"status,omitempty"`
} }
// List of OIDCIdentityProvider objects. // OIDCIdentityProviderList lists OIDCIdentityProvider objects.
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type OIDCIdentityProviderList struct { type OIDCIdentityProviderList struct {
metav1.TypeMeta `json:",inline"` metav1.TypeMeta `json:",inline"`

View File

@ -416,6 +416,11 @@ func (in *OIDCAuthorizationConfig) DeepCopyInto(out *OIDCAuthorizationConfig) {
*out = make([]string, len(*in)) *out = make([]string, len(*in))
copy(*out, *in) copy(*out, *in)
} }
if in.AdditionalAuthorizeParameters != nil {
in, out := &in.AdditionalAuthorizeParameters, &out.AdditionalAuthorizeParameters
*out = make([]Parameter, len(*in))
copy(*out, *in)
}
return return
} }
@ -569,6 +574,22 @@ func (in *OIDCIdentityProviderStatus) DeepCopy() *OIDCIdentityProviderStatus {
return out return out
} }
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Parameter) DeepCopyInto(out *Parameter) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Parameter.
func (in *Parameter) DeepCopy() *Parameter {
if in == nil {
return nil
}
out := new(Parameter)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TLSSpec) DeepCopyInto(out *TLSSpec) { func (in *TLSSpec) DeepCopyInto(out *TLSSpec) {
*out = *in *out = *in

View File

@ -56,19 +56,103 @@ spec:
the OAuth2 authorization request parameters to be used with this the OAuth2 authorization request parameters to be used with this
OIDC identity provider. OIDC identity provider.
properties: properties:
additionalAuthorizeParameters:
description: additionalAuthorizeParameters are extra query parameters
that should be included in the authorize request to your OIDC
provider in the authorization request during an OIDC Authorization
Code Flow. By default, no extra parameters are sent. The standard
parameters that will be sent are "response_type", "scope", "client_id",
"state", "nonce", "code_challenge", "code_challenge_method",
and "redirect_uri". These parameters cannot be included in this
setting. Additionally, the "hd" parameter cannot be included
in this setting at this time. The "hd" parameter is used by
Google's OIDC provider to provide a hint as to which "hosted
domain" the user should use during login. However, Pinniped
does not yet support validating the hosted domain in the resulting
ID token, so it is not yet safe to use this feature of Google's
OIDC provider with Pinniped. This setting does not influence
the parameters sent to the token endpoint in the Resource Owner
Password Credentials Grant. The Pinniped Supervisor requires
that your OIDC provider returns refresh tokens to the Supervisor
from the authorization flows. Some OIDC providers may require
a certain value for the "prompt" parameter in order to properly
request refresh tokens. See the documentation of your OIDC provider's
authorization endpoint for its requirements for what to include
in the request in order to receive a refresh token in the response,
if anything. If your provider requires the prompt parameter
to request a refresh token, then include it here. Also note
that most providers also require a certain scope to be requested
in order to receive refresh tokens. See the additionalScopes
setting for more information about using scopes to request refresh
tokens.
items:
description: Parameter is a key/value pair which represents
a parameter in an HTTP request.
properties:
name:
description: The name of the parameter. Required.
minLength: 1
type: string
value:
description: The value of the parameter.
type: string
required:
- name
type: object
type: array
x-kubernetes-list-map-keys:
- name
x-kubernetes-list-type: map
additionalScopes: additionalScopes:
description: AdditionalScopes are the scopes in addition to "openid" description: 'additionalScopes are the additional scopes that
that will be requested as part of the authorization request will be requested from your OIDC provider in the authorization
flow with an OIDC identity provider. In the case of a Resource request during an OIDC Authorization Code Flow and in the token
Owner Password Credentials Grant flow, AdditionalScopes are request during a Resource Owner Password Credentials Grant.
the scopes in addition to "openid" that will be requested as Note that the "openid" scope will always be requested regardless
part of the token request (see also the allowPasswordGrant field). of the value in this setting, since it is always required according
By default, only the "openid" scope will be requested. to the OIDC spec. By default, when this field is not set, the
Supervisor will request the following scopes: "openid", "offline_access",
"email", and "profile". See https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims
for a description of the "profile" and "email" scopes. See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess
for a description of the "offline_access" scope. This default
value may change in future versions of Pinniped as the standard
evolves, or as common patterns used by providers who implement
the standard in the ecosystem evolve. By setting this list to
anything other than an empty list, you are overriding the default
value, so you may wish to include some of "offline_access",
"email", and "profile" in your override list. If you do not
want any of these scopes to be requested, you may set this list
to contain only "openid". Some OIDC providers may also require
a scope to get access to the user''s group membership, in which
case you may wish to include it in this list. Sometimes the
scope to request the user''s group membership is called "groups",
but unfortunately this is not specified in the OIDC standard.
Generally speaking, you should include any scopes required to
cause the appropriate claims to be the returned by your OIDC
provider in the ID token or userinfo endpoint results for those
claims which you would like to use in the oidcClaims settings
to determine the usernames and group memberships of your Kubernetes
users. See your OIDC provider''s documentation for more information
about what scopes are available to request claims. Additionally,
the Pinniped Supervisor requires that your OIDC provider returns
refresh tokens to the Supervisor from these authorization flows.
For most OIDC providers, the scope required to receive refresh
tokens will be "offline_access". See the documentation of your
OIDC provider''s authorization and token endpoints for its requirements
for what to include in the request in order to receive a refresh
token in the response, if anything. Note that it may be safe
to send "offline_access" even to providers which do not require
it, since the provider may ignore scopes that it does not understand
or require (see https://datatracker.ietf.org/doc/html/rfc6749#section-3.3).
In the unusual case that you must avoid sending the "offline_access"
scope, then you must override the default value of this setting.
This is required if your OIDC provider will reject the request
when it includes "offline_access" (e.g. GitLab''s OIDC provider).'
items: items:
type: string type: string
type: array type: array
allowPasswordGrant: allowPasswordGrant:
description: AllowPasswordGrant, when true, will allow the use description: allowPasswordGrant, when true, will allow the use
of OAuth 2.0's Resource Owner Password Credentials Grant (see of OAuth 2.0's Resource Owner Password Credentials Grant (see
https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) to https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) to
authenticate to the OIDC provider using a username and password authenticate to the OIDC provider using a username and password
@ -94,7 +178,7 @@ spec:
your end users' passwords (similar to LDAPIdentityProvider), your end users' passwords (similar to LDAPIdentityProvider),
and you will not be able to require multi-factor authentication and you will not be able to require multi-factor authentication
or use the other web-based login features of your OIDC provider or use the other web-based login features of your OIDC provider
during Resource Owner Password Credentials Grant logins. AllowPasswordGrant during Resource Owner Password Credentials Grant logins. allowPasswordGrant
defaults to false. defaults to false.
type: boolean type: boolean
type: object type: object
@ -103,12 +187,19 @@ spec:
used when inspecting an identity from this OIDC identity provider. used when inspecting an identity from this OIDC identity provider.
properties: properties:
groups: groups:
description: Groups provides the name of the token claim that description: Groups provides the name of the ID token claim or
will be used to ascertain the groups to which an identity belongs. userinfo endpoint response claim that will be used to ascertain
the groups to which an identity belongs. By default, the identities
will not include any group memberships when this setting is
not configured.
type: string type: string
username: username:
description: Username provides the name of the token claim that description: Username provides the name of the ID token claim
will be used to ascertain an identity's username. or userinfo endpoint response claim that will be used to ascertain
an identity's username. When not set, the username will be an
automatically constructed unique string which will include the
issuer URL of your OIDC provider along with the value of the
"sub" (subject) claim from the ID token.
type: string type: string
type: object type: object
client: client:

View File

@ -20,7 +20,7 @@ const (
PhaseError OIDCIdentityProviderPhase = "Error" PhaseError OIDCIdentityProviderPhase = "Error"
) )
// Status of an OIDC identity provider. // OIDCIdentityProviderStatus is the status of an OIDC identity provider.
type OIDCIdentityProviderStatus struct { type OIDCIdentityProviderStatus struct {
// Phase summarizes the overall status of the OIDCIdentityProvider. // Phase summarizes the overall status of the OIDCIdentityProvider.
// +kubebuilder:default=Pending // +kubebuilder:default=Pending
@ -38,15 +38,62 @@ type OIDCIdentityProviderStatus struct {
// OIDCAuthorizationConfig provides information about how to form the OAuth2 authorization // OIDCAuthorizationConfig provides information about how to form the OAuth2 authorization
// request parameters. // request parameters.
type OIDCAuthorizationConfig struct { type OIDCAuthorizationConfig struct {
// AdditionalScopes are the scopes in addition to "openid" that will be requested as part of the authorization // additionalScopes are the additional scopes that will be requested from your OIDC provider in the authorization
// request flow with an OIDC identity provider. // request during an OIDC Authorization Code Flow and in the token request during a Resource Owner Password Credentials
// In the case of a Resource Owner Password Credentials Grant flow, AdditionalScopes are the scopes // Grant. Note that the "openid" scope will always be requested regardless of the value in this setting, since it is
// in addition to "openid" that will be requested as part of the token request (see also the allowPasswordGrant field). // always required according to the OIDC spec. By default, when this field is not set, the Supervisor will request
// By default, only the "openid" scope will be requested. // the following scopes: "openid", "offline_access", "email", and "profile". See
// https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims for a description of the "profile" and "email"
// scopes. See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess for a description of the
// "offline_access" scope. This default value may change in future versions of Pinniped as the standard evolves,
// or as common patterns used by providers who implement the standard in the ecosystem evolve.
// By setting this list to anything other than an empty list, you are overriding the
// default value, so you may wish to include some of "offline_access", "email", and "profile" in your override list.
// If you do not want any of these scopes to be requested, you may set this list to contain only "openid".
// Some OIDC providers may also require a scope to get access to the user's group membership, in which case you
// may wish to include it in this list. Sometimes the scope to request the user's group membership is called
// "groups", but unfortunately this is not specified in the OIDC standard.
// Generally speaking, you should include any scopes required to cause the appropriate claims to be the returned by
// your OIDC provider in the ID token or userinfo endpoint results for those claims which you would like to use in
// the oidcClaims settings to determine the usernames and group memberships of your Kubernetes users. See
// your OIDC provider's documentation for more information about what scopes are available to request claims.
// Additionally, the Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor
// from these authorization flows. For most OIDC providers, the scope required to receive refresh tokens will be
// "offline_access". See the documentation of your OIDC provider's authorization and token endpoints for its
// requirements for what to include in the request in order to receive a refresh token in the response, if anything.
// Note that it may be safe to send "offline_access" even to providers which do not require it, since the provider
// may ignore scopes that it does not understand or require (see
// https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). In the unusual case that you must avoid sending the
// "offline_access" scope, then you must override the default value of this setting. This is required if your OIDC
// provider will reject the request when it includes "offline_access" (e.g. GitLab's OIDC provider).
// +optional // +optional
AdditionalScopes []string `json:"additionalScopes,omitempty"` AdditionalScopes []string `json:"additionalScopes,omitempty"`
// AllowPasswordGrant, when true, will allow the use of OAuth 2.0's Resource Owner Password Credentials Grant // additionalAuthorizeParameters are extra query parameters that should be included in the authorize request to your
// OIDC provider in the authorization request during an OIDC Authorization Code Flow. By default, no extra
// parameters are sent. The standard parameters that will be sent are "response_type", "scope", "client_id",
// "state", "nonce", "code_challenge", "code_challenge_method", and "redirect_uri". These parameters cannot be
// included in this setting. Additionally, the "hd" parameter cannot be included in this setting at this time.
// The "hd" parameter is used by Google's OIDC provider to provide a hint as to which "hosted domain" the user
// should use during login. However, Pinniped does not yet support validating the hosted domain in the resulting
// ID token, so it is not yet safe to use this feature of Google's OIDC provider with Pinniped.
// This setting does not influence the parameters sent to the token endpoint in the Resource Owner Password
// Credentials Grant. The Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the
// Supervisor from the authorization flows. Some OIDC providers may require a certain value for the "prompt"
// parameter in order to properly request refresh tokens. See the documentation of your OIDC provider's
// authorization endpoint for its requirements for what to include in the request in order to receive a refresh
// token in the response, if anything. If your provider requires the prompt parameter to request a refresh token,
// then include it here. Also note that most providers also require a certain scope to be requested in order to
// receive refresh tokens. See the additionalScopes setting for more information about using scopes to request
// refresh tokens.
// +optional
// +patchMergeKey=name
// +patchStrategy=merge
// +listType=map
// +listMapKey=name
AdditionalAuthorizeParameters []Parameter `json:"additionalAuthorizeParameters,omitempty"`
// allowPasswordGrant, when true, will allow the use of OAuth 2.0's Resource Owner Password Credentials Grant
// (see https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) to authenticate to the OIDC provider using a // (see https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) to authenticate to the OIDC provider using a
// username and password without a web browser, in addition to the usual browser-based OIDC Authorization Code Flow. // username and password without a web browser, in addition to the usual browser-based OIDC Authorization Code Flow.
// The Resource Owner Password Credentials Grant is not officially part of the OIDC specification, so it may not be // The Resource Owner Password Credentials Grant is not officially part of the OIDC specification, so it may not be
@ -61,20 +108,34 @@ type OIDCAuthorizationConfig struct {
// Credentials Grant means that the Pinniped CLI and Pinniped Supervisor will directly handle your end users' passwords // Credentials Grant means that the Pinniped CLI and Pinniped Supervisor will directly handle your end users' passwords
// (similar to LDAPIdentityProvider), and you will not be able to require multi-factor authentication or use the other // (similar to LDAPIdentityProvider), and you will not be able to require multi-factor authentication or use the other
// web-based login features of your OIDC provider during Resource Owner Password Credentials Grant logins. // web-based login features of your OIDC provider during Resource Owner Password Credentials Grant logins.
// AllowPasswordGrant defaults to false. // allowPasswordGrant defaults to false.
// +optional // +optional
AllowPasswordGrant bool `json:"allowPasswordGrant,omitempty"` AllowPasswordGrant bool `json:"allowPasswordGrant,omitempty"`
} }
// Parameter is a key/value pair which represents a parameter in an HTTP request.
type Parameter struct {
// The name of the parameter. Required.
// +kubebuilder:validation:MinLength=1
Name string `json:"name"`
// The value of the parameter.
// +optional
Value string `json:"value,omitempty"`
}
// OIDCClaims provides a mapping from upstream claims into identities. // OIDCClaims provides a mapping from upstream claims into identities.
type OIDCClaims struct { type OIDCClaims struct {
// Groups provides the name of the token claim that will be used to ascertain the groups to which // Groups provides the name of the ID token claim or userinfo endpoint response claim that will be used to ascertain
// an identity belongs. // the groups to which an identity belongs. By default, the identities will not include any group memberships when
// this setting is not configured.
// +optional // +optional
Groups string `json:"groups"` Groups string `json:"groups"`
// Username provides the name of the token claim that will be used to ascertain an identity's // Username provides the name of the ID token claim or userinfo endpoint response claim that will be used to
// username. // ascertain an identity's username. When not set, the username will be an automatically constructed unique string
// which will include the issuer URL of your OIDC provider along with the value of the "sub" (subject) claim from
// the ID token.
// +optional // +optional
Username string `json:"username"` Username string `json:"username"`
} }
@ -89,7 +150,7 @@ type OIDCClient struct {
SecretName string `json:"secretName"` SecretName string `json:"secretName"`
} }
// Spec for configuring an OIDC identity provider. // OIDCIdentityProviderSpec is the spec for configuring an OIDC identity provider.
type OIDCIdentityProviderSpec struct { type OIDCIdentityProviderSpec struct {
// Issuer is the issuer URL of this OIDC identity provider, i.e., where to fetch // Issuer is the issuer URL of this OIDC identity provider, i.e., where to fetch
// /.well-known/openid-configuration. // /.well-known/openid-configuration.
@ -135,7 +196,7 @@ type OIDCIdentityProvider struct {
Status OIDCIdentityProviderStatus `json:"status,omitempty"` Status OIDCIdentityProviderStatus `json:"status,omitempty"`
} }
// List of OIDCIdentityProvider objects. // OIDCIdentityProviderList lists OIDCIdentityProvider objects.
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type OIDCIdentityProviderList struct { type OIDCIdentityProviderList struct {
metav1.TypeMeta `json:",inline"` metav1.TypeMeta `json:",inline"`

View File

@ -416,6 +416,11 @@ func (in *OIDCAuthorizationConfig) DeepCopyInto(out *OIDCAuthorizationConfig) {
*out = make([]string, len(*in)) *out = make([]string, len(*in))
copy(*out, *in) copy(*out, *in)
} }
if in.AdditionalAuthorizeParameters != nil {
in, out := &in.AdditionalAuthorizeParameters, &out.AdditionalAuthorizeParameters
*out = make([]Parameter, len(*in))
copy(*out, *in)
}
return return
} }
@ -569,6 +574,22 @@ func (in *OIDCIdentityProviderStatus) DeepCopy() *OIDCIdentityProviderStatus {
return out return out
} }
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Parameter) DeepCopyInto(out *Parameter) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Parameter.
func (in *Parameter) DeepCopy() *Parameter {
if in == nil {
return nil
}
out := new(Parameter)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TLSSpec) DeepCopyInto(out *TLSSpec) { func (in *TLSSpec) DeepCopyInto(out *TLSSpec) {
*out = *in *out = *in

4
go.mod
View File

@ -17,7 +17,9 @@ require (
github.com/google/uuid v1.1.2 github.com/google/uuid v1.1.2
github.com/gorilla/securecookie v1.1.1 github.com/gorilla/securecookie v1.1.1
github.com/gorilla/websocket v1.4.2 github.com/gorilla/websocket v1.4.2
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826
github.com/ory/fosite v0.40.2 github.com/ory/fosite v0.40.2
github.com/ory/x v0.0.212
github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4 github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/sclevine/agouti v3.0.0+incompatible github.com/sclevine/agouti v3.0.0+incompatible
@ -96,12 +98,10 @@ require (
github.com/moby/spdystream v0.2.0 // indirect github.com/moby/spdystream v0.2.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/ory/go-acc v0.2.6 // indirect github.com/ory/go-acc v0.2.6 // indirect
github.com/ory/go-convenience v0.1.0 // indirect github.com/ory/go-convenience v0.1.0 // indirect
github.com/ory/viper v1.7.5 // indirect github.com/ory/viper v1.7.5 // indirect
github.com/ory/x v0.0.212 // indirect
github.com/pborman/uuid v1.2.0 // indirect github.com/pborman/uuid v1.2.0 // indirect
github.com/pelletier/go-toml v1.9.3 // indirect github.com/pelletier/go-toml v1.9.3 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect

View File

@ -372,7 +372,7 @@ export PINNIPED_TEST_CLI_OIDC_USERNAME=pinny@example.com
export PINNIPED_TEST_CLI_OIDC_PASSWORD=${dex_test_password} export PINNIPED_TEST_CLI_OIDC_PASSWORD=${dex_test_password}
export PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_ISSUER=https://dex.tools.svc.cluster.local/dex export PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_ISSUER=https://dex.tools.svc.cluster.local/dex
export PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_ISSUER_CA_BUNDLE="${test_ca_bundle_pem}" export PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_ISSUER_CA_BUNDLE="${test_ca_bundle_pem}"
export PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_ADDITIONAL_SCOPES=email export PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_ADDITIONAL_SCOPES="offline_access,email"
export PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_USERNAME_CLAIM=email export PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_USERNAME_CLAIM=email
export PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_GROUPS_CLAIM=groups export PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_GROUPS_CLAIM=groups
export PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_CLIENT_ID=pinniped-supervisor export PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_CLIENT_ID=pinniped-supervisor

View File

@ -301,8 +301,9 @@ func (c *activeDirectoryWatcherController) validateUpstream(ctx context.Context,
adUpstreamImpl := &activeDirectoryUpstreamGenericLDAPImpl{activeDirectoryIdentityProvider: *upstream} adUpstreamImpl := &activeDirectoryUpstreamGenericLDAPImpl{activeDirectoryIdentityProvider: *upstream}
config := &upstreamldap.ProviderConfig{ config := &upstreamldap.ProviderConfig{
Name: upstream.Name, Name: upstream.Name,
Host: spec.Host, ResourceUID: upstream.UID,
Host: spec.Host,
UserSearch: upstreamldap.UserSearchConfig{ UserSearch: upstreamldap.UserSearchConfig{
Base: spec.UserSearch.Base, Base: spec.UserSearch.Base,
Filter: adUpstreamImpl.Spec().UserSearch().Filter(), Filter: adUpstreamImpl.Spec().UserSearch().Filter(),

View File

@ -150,6 +150,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
const ( const (
testNamespace = "test-namespace" testNamespace = "test-namespace"
testResourceUID = "test-uid"
testName = "test-name" testName = "test-name"
testSecretName = "test-bind-secret" testSecretName = "test-bind-secret"
testBindUsername = "test-bind-username" testBindUsername = "test-bind-username"
@ -172,7 +173,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
testCABundleBase64Encoded := base64.StdEncoding.EncodeToString(testCABundle) testCABundleBase64Encoded := base64.StdEncoding.EncodeToString(testCABundle)
validUpstream := &v1alpha1.ActiveDirectoryIdentityProvider{ validUpstream := &v1alpha1.ActiveDirectoryIdentityProvider{
ObjectMeta: metav1.ObjectMeta{Name: testName, Namespace: testNamespace, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Name: testName, Namespace: testNamespace, Generation: 1234, UID: testResourceUID},
Spec: v1alpha1.ActiveDirectoryIdentityProviderSpec{ Spec: v1alpha1.ActiveDirectoryIdentityProviderSpec{
Host: testHost, Host: testHost,
TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testCABundleBase64Encoded}, TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testCABundleBase64Encoded},
@ -202,6 +203,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
providerConfigForValidUpstreamWithTLS := &upstreamldap.ProviderConfig{ providerConfigForValidUpstreamWithTLS := &upstreamldap.ProviderConfig{
Name: testName, Name: testName,
ResourceUID: testResourceUID,
Host: testHost, Host: testHost,
ConnectionProtocol: upstreamldap.TLS, ConnectionProtocol: upstreamldap.TLS,
CABundle: testCABundle, CABundle: testCABundle,
@ -364,7 +366,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
}, },
wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS}, wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS},
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234},
Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{
Phase: "Ready", Phase: "Ready",
Conditions: allConditionsTrue(1234, "4242"), Conditions: allConditionsTrue(1234, "4242"),
@ -379,7 +381,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantErr: controllerlib.ErrSyntheticRequeue.Error(),
wantResultingCache: []*upstreamldap.ProviderConfig{}, wantResultingCache: []*upstreamldap.ProviderConfig{},
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234},
Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{
Phase: "Error", Phase: "Error",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
@ -407,7 +409,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantErr: controllerlib.ErrSyntheticRequeue.Error(),
wantResultingCache: []*upstreamldap.ProviderConfig{}, wantResultingCache: []*upstreamldap.ProviderConfig{},
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234},
Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{
Phase: "Error", Phase: "Error",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
@ -434,7 +436,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantErr: controllerlib.ErrSyntheticRequeue.Error(),
wantResultingCache: []*upstreamldap.ProviderConfig{}, wantResultingCache: []*upstreamldap.ProviderConfig{},
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234},
Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{
Phase: "Error", Phase: "Error",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
@ -460,7 +462,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantErr: controllerlib.ErrSyntheticRequeue.Error(),
wantResultingCache: []*upstreamldap.ProviderConfig{}, wantResultingCache: []*upstreamldap.ProviderConfig{},
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234},
Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{
Phase: "Error", Phase: "Error",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
@ -486,7 +488,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantErr: controllerlib.ErrSyntheticRequeue.Error(),
wantResultingCache: []*upstreamldap.ProviderConfig{}, wantResultingCache: []*upstreamldap.ProviderConfig{},
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234},
Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{
Phase: "Error", Phase: "Error",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
@ -517,6 +519,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
wantResultingCache: []*upstreamldap.ProviderConfig{ wantResultingCache: []*upstreamldap.ProviderConfig{
{ {
Name: testName, Name: testName,
ResourceUID: testResourceUID,
Host: testHost, Host: testHost,
ConnectionProtocol: upstreamldap.TLS, ConnectionProtocol: upstreamldap.TLS,
CABundle: nil, CABundle: nil,
@ -537,7 +540,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
}, },
}, },
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234},
Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{
Phase: "Ready", Phase: "Ready",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
@ -572,6 +575,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
wantResultingCache: []*upstreamldap.ProviderConfig{ wantResultingCache: []*upstreamldap.ProviderConfig{
{ {
Name: testName, Name: testName,
ResourceUID: testResourceUID,
Host: testHost, Host: testHost,
ConnectionProtocol: upstreamldap.TLS, ConnectionProtocol: upstreamldap.TLS,
CABundle: nil, CABundle: nil,
@ -592,7 +596,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
}, },
}, },
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234},
Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{
Phase: "Ready", Phase: "Ready",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
@ -630,6 +634,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
wantResultingCache: []*upstreamldap.ProviderConfig{ wantResultingCache: []*upstreamldap.ProviderConfig{
{ {
Name: testName, Name: testName,
ResourceUID: testResourceUID,
Host: "ldap.example.com", Host: "ldap.example.com",
ConnectionProtocol: upstreamldap.StartTLS, // successfully fell back to using StartTLS ConnectionProtocol: upstreamldap.StartTLS, // successfully fell back to using StartTLS
CABundle: testCABundle, CABundle: testCABundle,
@ -650,7 +655,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
}, },
}, },
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234},
Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{
Phase: "Ready", Phase: "Ready",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
@ -688,6 +693,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
// even though the connection test failed, still loads into the cache because it is treated like a warning // even though the connection test failed, still loads into the cache because it is treated like a warning
{ {
Name: testName, Name: testName,
ResourceUID: testResourceUID,
Host: "ldap.example.com:5678", Host: "ldap.example.com:5678",
ConnectionProtocol: upstreamldap.TLS, // need to pick TLS or StartTLS to load into the cache when both fail, so choose TLS ConnectionProtocol: upstreamldap.TLS, // need to pick TLS or StartTLS to load into the cache when both fail, so choose TLS
CABundle: testCABundle, CABundle: testCABundle,
@ -709,7 +715,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
}, },
wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantErr: controllerlib.ErrSyntheticRequeue.Error(),
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234},
Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{
Phase: "Error", Phase: "Error",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
@ -745,6 +751,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
wantResultingCache: []*upstreamldap.ProviderConfig{ wantResultingCache: []*upstreamldap.ProviderConfig{
{ {
Name: testName, Name: testName,
ResourceUID: testResourceUID,
Host: testHost, Host: testHost,
ConnectionProtocol: upstreamldap.TLS, ConnectionProtocol: upstreamldap.TLS,
CABundle: nil, CABundle: nil,
@ -765,7 +772,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
}, },
}, },
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234},
Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{
Phase: "Ready", Phase: "Ready",
Conditions: allConditionsTrue(1234, "4242"), Conditions: allConditionsTrue(1234, "4242"),
@ -779,6 +786,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
upstream.Name = "other-upstream" upstream.Name = "other-upstream"
upstream.Generation = 42 upstream.Generation = 42
upstream.Spec.Bind.SecretName = "non-existent-secret" upstream.Spec.Bind.SecretName = "non-existent-secret"
upstream.UID = "other-uid"
})}, })},
inputSecrets: []runtime.Object{validBindUserSecret("4242")}, inputSecrets: []runtime.Object{validBindUserSecret("4242")},
setupMocks: func(conn *mockldapconn.MockConn) { setupMocks: func(conn *mockldapconn.MockConn) {
@ -790,7 +798,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS}, wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS},
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{ wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{
{ {
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: "other-upstream", Generation: 42}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: "other-upstream", Generation: 42, UID: "other-uid"},
Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{
Phase: "Error", Phase: "Error",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
@ -807,7 +815,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
}, },
}, },
{ {
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234},
Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{
Phase: "Ready", Phase: "Ready",
Conditions: allConditionsTrue(1234, "4242"), Conditions: allConditionsTrue(1234, "4242"),
@ -831,7 +839,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantErr: controllerlib.ErrSyntheticRequeue.Error(),
wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS}, wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS},
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234},
Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{
Phase: "Error", Phase: "Error",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
@ -872,6 +880,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
wantResultingCache: []*upstreamldap.ProviderConfig{ wantResultingCache: []*upstreamldap.ProviderConfig{
{ {
Name: testName, Name: testName,
ResourceUID: testResourceUID,
Host: testHost, Host: testHost,
ConnectionProtocol: upstreamldap.TLS, ConnectionProtocol: upstreamldap.TLS,
CABundle: testCABundle, CABundle: testCABundle,
@ -892,7 +901,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
}, },
}, },
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234},
Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{
Phase: "Error", Phase: "Error",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
@ -928,7 +937,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
}, },
wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantErr: controllerlib.ErrSyntheticRequeue.Error(),
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234},
Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{
Phase: "Error", Phase: "Error",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
@ -966,7 +975,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
}, },
wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS}, wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS},
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234},
Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{
Phase: "Ready", Phase: "Ready",
Conditions: allConditionsTrue(1234, "4242"), Conditions: allConditionsTrue(1234, "4242"),
@ -995,6 +1004,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
wantResultingCache: []*upstreamldap.ProviderConfig{ wantResultingCache: []*upstreamldap.ProviderConfig{
{ {
Name: testName, Name: testName,
ResourceUID: testResourceUID,
Host: testHost, Host: testHost,
ConnectionProtocol: upstreamldap.TLS, ConnectionProtocol: upstreamldap.TLS,
CABundle: testCABundle, CABundle: testCABundle,
@ -1015,7 +1025,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
}, },
}, },
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234},
Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{
Phase: "Ready", Phase: "Ready",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
@ -1045,6 +1055,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
wantResultingCache: []*upstreamldap.ProviderConfig{ wantResultingCache: []*upstreamldap.ProviderConfig{
{ {
Name: testName, Name: testName,
ResourceUID: testResourceUID,
Host: testHost, Host: testHost,
ConnectionProtocol: upstreamldap.TLS, ConnectionProtocol: upstreamldap.TLS,
CABundle: testCABundle, CABundle: testCABundle,
@ -1065,7 +1076,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
}, },
}, },
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234},
Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{
Phase: "Ready", Phase: "Ready",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
@ -1100,7 +1111,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
}, },
wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithStartTLS}, wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithStartTLS},
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234},
Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{
Phase: "Ready", Phase: "Ready",
Conditions: allConditionsTrue(1234, "4242"), Conditions: allConditionsTrue(1234, "4242"),
@ -1137,7 +1148,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
}, },
wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS}, wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS},
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234},
Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{
Phase: "Ready", Phase: "Ready",
Conditions: allConditionsTrue(1234, "4242"), Conditions: allConditionsTrue(1234, "4242"),
@ -1175,7 +1186,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
}, },
wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS}, wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS},
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234},
Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{
Phase: "Ready", Phase: "Ready",
Conditions: allConditionsTrue(1234, "4242"), Conditions: allConditionsTrue(1234, "4242"),
@ -1212,7 +1223,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
}, },
wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS}, wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS},
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234},
Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{
Phase: "Ready", Phase: "Ready",
Conditions: allConditionsTrue(1234, "4242"), Conditions: allConditionsTrue(1234, "4242"),
@ -1243,6 +1254,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
wantResultingCache: []*upstreamldap.ProviderConfig{ wantResultingCache: []*upstreamldap.ProviderConfig{
{ {
Name: testName, Name: testName,
ResourceUID: testResourceUID,
Host: testHost, Host: testHost,
ConnectionProtocol: upstreamldap.TLS, ConnectionProtocol: upstreamldap.TLS,
CABundle: testCABundle, CABundle: testCABundle,
@ -1264,7 +1276,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
}, },
}, },
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234},
Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{
Phase: "Ready", Phase: "Ready",
Conditions: allConditionsTrue(1234, "4242"), Conditions: allConditionsTrue(1234, "4242"),
@ -1295,6 +1307,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
wantResultingCache: []*upstreamldap.ProviderConfig{ wantResultingCache: []*upstreamldap.ProviderConfig{
{ {
Name: testName, Name: testName,
ResourceUID: testResourceUID,
Host: testHost, Host: testHost,
ConnectionProtocol: upstreamldap.TLS, ConnectionProtocol: upstreamldap.TLS,
CABundle: testCABundle, CABundle: testCABundle,
@ -1315,7 +1328,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
}, },
}, },
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234},
Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{
Phase: "Ready", Phase: "Ready",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
@ -1350,6 +1363,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
wantResultingCache: []*upstreamldap.ProviderConfig{ wantResultingCache: []*upstreamldap.ProviderConfig{
{ {
Name: testName, Name: testName,
ResourceUID: testResourceUID,
Host: testHost, Host: testHost,
ConnectionProtocol: upstreamldap.TLS, ConnectionProtocol: upstreamldap.TLS,
CABundle: testCABundle, CABundle: testCABundle,
@ -1370,7 +1384,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
}, },
}, },
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234},
Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{
Phase: "Ready", Phase: "Ready",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
@ -1399,6 +1413,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
wantResultingCache: []*upstreamldap.ProviderConfig{ wantResultingCache: []*upstreamldap.ProviderConfig{
{ {
Name: testName, Name: testName,
ResourceUID: testResourceUID,
Host: testHost, Host: testHost,
ConnectionProtocol: upstreamldap.TLS, ConnectionProtocol: upstreamldap.TLS,
CABundle: testCABundle, CABundle: testCABundle,
@ -1419,7 +1434,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
}, },
}, },
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234},
Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{
Phase: "Ready", Phase: "Ready",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
@ -1447,7 +1462,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
}, },
wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantErr: controllerlib.ErrSyntheticRequeue.Error(),
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234},
Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{
Phase: "Error", Phase: "Error",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
@ -1483,7 +1498,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
}, },
wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantErr: controllerlib.ErrSyntheticRequeue.Error(),
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234},
Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{
Phase: "Error", Phase: "Error",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
@ -1525,7 +1540,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
}, },
wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantErr: controllerlib.ErrSyntheticRequeue.Error(),
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234},
Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{
Phase: "Error", Phase: "Error",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
@ -1554,7 +1569,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
}, },
wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantErr: controllerlib.ErrSyntheticRequeue.Error(),
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234},
Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{
Phase: "Error", Phase: "Error",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
@ -1594,6 +1609,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
wantResultingCache: []*upstreamldap.ProviderConfig{ wantResultingCache: []*upstreamldap.ProviderConfig{
{ {
Name: testName, Name: testName,
ResourceUID: testResourceUID,
Host: testHost, Host: testHost,
ConnectionProtocol: upstreamldap.TLS, ConnectionProtocol: upstreamldap.TLS,
CABundle: testCABundle, CABundle: testCABundle,
@ -1614,7 +1630,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
}, },
}, },
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234},
Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{
Phase: "Ready", Phase: "Ready",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{

View File

@ -226,8 +226,9 @@ func (c *ldapWatcherController) validateUpstream(ctx context.Context, upstream *
spec := upstream.Spec spec := upstream.Spec
config := &upstreamldap.ProviderConfig{ config := &upstreamldap.ProviderConfig{
Name: upstream.Name, Name: upstream.Name,
Host: spec.Host, ResourceUID: upstream.UID,
Host: spec.Host,
UserSearch: upstreamldap.UserSearchConfig{ UserSearch: upstreamldap.UserSearchConfig{
Base: spec.UserSearch.Base, Base: spec.UserSearch.Base,
Filter: spec.UserSearch.Filter, Filter: spec.UserSearch.Filter,

View File

@ -150,6 +150,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
const ( const (
testNamespace = "test-namespace" testNamespace = "test-namespace"
testName = "test-name" testName = "test-name"
testResourceUID = "test-resource-uid"
testSecretName = "test-bind-secret" testSecretName = "test-bind-secret"
testBindUsername = "test-bind-username" testBindUsername = "test-bind-username"
testBindPassword = "test-bind-password" testBindPassword = "test-bind-password"
@ -171,7 +172,12 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
testCABundleBase64Encoded := base64.StdEncoding.EncodeToString(testCABundle) testCABundleBase64Encoded := base64.StdEncoding.EncodeToString(testCABundle)
validUpstream := &v1alpha1.LDAPIdentityProvider{ validUpstream := &v1alpha1.LDAPIdentityProvider{
ObjectMeta: metav1.ObjectMeta{Name: testName, Namespace: testNamespace, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{
Name: testName,
Namespace: testNamespace,
Generation: 1234,
UID: testResourceUID,
},
Spec: v1alpha1.LDAPIdentityProviderSpec{ Spec: v1alpha1.LDAPIdentityProviderSpec{
Host: testHost, Host: testHost,
TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testCABundleBase64Encoded}, TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testCABundleBase64Encoded},
@ -201,6 +207,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
providerConfigForValidUpstreamWithTLS := &upstreamldap.ProviderConfig{ providerConfigForValidUpstreamWithTLS := &upstreamldap.ProviderConfig{
Name: testName, Name: testName,
ResourceUID: testResourceUID,
Host: testHost, Host: testHost,
ConnectionProtocol: upstreamldap.TLS, ConnectionProtocol: upstreamldap.TLS,
CABundle: testCABundle, CABundle: testCABundle,
@ -299,7 +306,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
}, },
wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS}, wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS},
wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testResourceUID},
Status: v1alpha1.LDAPIdentityProviderStatus{ Status: v1alpha1.LDAPIdentityProviderStatus{
Phase: "Ready", Phase: "Ready",
Conditions: allConditionsTrue(1234, "4242"), Conditions: allConditionsTrue(1234, "4242"),
@ -320,7 +327,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantErr: controllerlib.ErrSyntheticRequeue.Error(),
wantResultingCache: []*upstreamldap.ProviderConfig{}, wantResultingCache: []*upstreamldap.ProviderConfig{},
wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testResourceUID},
Status: v1alpha1.LDAPIdentityProviderStatus{ Status: v1alpha1.LDAPIdentityProviderStatus{
Phase: "Error", Phase: "Error",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
@ -348,7 +355,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantErr: controllerlib.ErrSyntheticRequeue.Error(),
wantResultingCache: []*upstreamldap.ProviderConfig{}, wantResultingCache: []*upstreamldap.ProviderConfig{},
wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testResourceUID},
Status: v1alpha1.LDAPIdentityProviderStatus{ Status: v1alpha1.LDAPIdentityProviderStatus{
Phase: "Error", Phase: "Error",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
@ -375,7 +382,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantErr: controllerlib.ErrSyntheticRequeue.Error(),
wantResultingCache: []*upstreamldap.ProviderConfig{}, wantResultingCache: []*upstreamldap.ProviderConfig{},
wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testResourceUID},
Status: v1alpha1.LDAPIdentityProviderStatus{ Status: v1alpha1.LDAPIdentityProviderStatus{
Phase: "Error", Phase: "Error",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
@ -401,7 +408,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantErr: controllerlib.ErrSyntheticRequeue.Error(),
wantResultingCache: []*upstreamldap.ProviderConfig{}, wantResultingCache: []*upstreamldap.ProviderConfig{},
wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testResourceUID},
Status: v1alpha1.LDAPIdentityProviderStatus{ Status: v1alpha1.LDAPIdentityProviderStatus{
Phase: "Error", Phase: "Error",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
@ -427,7 +434,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantErr: controllerlib.ErrSyntheticRequeue.Error(),
wantResultingCache: []*upstreamldap.ProviderConfig{}, wantResultingCache: []*upstreamldap.ProviderConfig{},
wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testResourceUID},
Status: v1alpha1.LDAPIdentityProviderStatus{ Status: v1alpha1.LDAPIdentityProviderStatus{
Phase: "Error", Phase: "Error",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
@ -458,6 +465,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
wantResultingCache: []*upstreamldap.ProviderConfig{ wantResultingCache: []*upstreamldap.ProviderConfig{
{ {
Name: testName, Name: testName,
ResourceUID: testResourceUID,
Host: testHost, Host: testHost,
ConnectionProtocol: upstreamldap.TLS, ConnectionProtocol: upstreamldap.TLS,
CABundle: nil, CABundle: nil,
@ -477,7 +485,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
}, },
}, },
wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testResourceUID},
Status: v1alpha1.LDAPIdentityProviderStatus{ Status: v1alpha1.LDAPIdentityProviderStatus{
Phase: "Ready", Phase: "Ready",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
@ -519,6 +527,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
wantResultingCache: []*upstreamldap.ProviderConfig{ wantResultingCache: []*upstreamldap.ProviderConfig{
{ {
Name: testName, Name: testName,
ResourceUID: testResourceUID,
Host: "ldap.example.com", Host: "ldap.example.com",
ConnectionProtocol: upstreamldap.StartTLS, // successfully fell back to using StartTLS ConnectionProtocol: upstreamldap.StartTLS, // successfully fell back to using StartTLS
CABundle: testCABundle, CABundle: testCABundle,
@ -538,7 +547,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
}, },
}, },
wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testResourceUID},
Status: v1alpha1.LDAPIdentityProviderStatus{ Status: v1alpha1.LDAPIdentityProviderStatus{
Phase: "Ready", Phase: "Ready",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
@ -580,6 +589,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
// even though the connection test failed, still loads into the cache because it is treated like a warning // even though the connection test failed, still loads into the cache because it is treated like a warning
{ {
Name: testName, Name: testName,
ResourceUID: testResourceUID,
Host: "ldap.example.com:5678", Host: "ldap.example.com:5678",
ConnectionProtocol: upstreamldap.TLS, // need to pick TLS or StartTLS to load into the cache when both fail, so choose TLS ConnectionProtocol: upstreamldap.TLS, // need to pick TLS or StartTLS to load into the cache when both fail, so choose TLS
CABundle: testCABundle, CABundle: testCABundle,
@ -600,7 +610,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
}, },
wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantErr: controllerlib.ErrSyntheticRequeue.Error(),
wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testResourceUID},
Status: v1alpha1.LDAPIdentityProviderStatus{ Status: v1alpha1.LDAPIdentityProviderStatus{
Phase: "Error", Phase: "Error",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
@ -635,6 +645,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
wantResultingCache: []*upstreamldap.ProviderConfig{ wantResultingCache: []*upstreamldap.ProviderConfig{
{ {
Name: testName, Name: testName,
ResourceUID: testResourceUID,
Host: testHost, Host: testHost,
ConnectionProtocol: upstreamldap.TLS, ConnectionProtocol: upstreamldap.TLS,
CABundle: nil, CABundle: nil,
@ -654,7 +665,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
}, },
}, },
wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testResourceUID},
Status: v1alpha1.LDAPIdentityProviderStatus{ Status: v1alpha1.LDAPIdentityProviderStatus{
Phase: "Ready", Phase: "Ready",
Conditions: allConditionsTrue(1234, "4242"), Conditions: allConditionsTrue(1234, "4242"),
@ -674,6 +685,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
upstream.Name = "other-upstream" upstream.Name = "other-upstream"
upstream.Generation = 42 upstream.Generation = 42
upstream.Spec.Bind.SecretName = "non-existent-secret" upstream.Spec.Bind.SecretName = "non-existent-secret"
upstream.UID = "other-uid"
})}, })},
inputSecrets: []runtime.Object{validBindUserSecret("4242")}, inputSecrets: []runtime.Object{validBindUserSecret("4242")},
setupMocks: func(conn *mockldapconn.MockConn) { setupMocks: func(conn *mockldapconn.MockConn) {
@ -685,7 +697,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS}, wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS},
wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{ wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{
{ {
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: "other-upstream", Generation: 42}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: "other-upstream", Generation: 42, UID: "other-uid"},
Status: v1alpha1.LDAPIdentityProviderStatus{ Status: v1alpha1.LDAPIdentityProviderStatus{
Phase: "Error", Phase: "Error",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
@ -702,7 +714,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
}, },
}, },
{ {
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testResourceUID},
Status: v1alpha1.LDAPIdentityProviderStatus{ Status: v1alpha1.LDAPIdentityProviderStatus{
Phase: "Ready", Phase: "Ready",
Conditions: allConditionsTrue(1234, "4242"), Conditions: allConditionsTrue(1234, "4242"),
@ -729,7 +741,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantErr: controllerlib.ErrSyntheticRequeue.Error(),
wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS}, wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS},
wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testResourceUID},
Status: v1alpha1.LDAPIdentityProviderStatus{ Status: v1alpha1.LDAPIdentityProviderStatus{
Phase: "Error", Phase: "Error",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
@ -771,7 +783,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
}, },
wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS}, wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS},
wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testResourceUID},
Status: v1alpha1.LDAPIdentityProviderStatus{ Status: v1alpha1.LDAPIdentityProviderStatus{
Phase: "Ready", Phase: "Ready",
Conditions: allConditionsTrue(1234, "4242"), Conditions: allConditionsTrue(1234, "4242"),
@ -805,7 +817,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
}, },
wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithStartTLS}, wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithStartTLS},
wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testResourceUID},
Status: v1alpha1.LDAPIdentityProviderStatus{ Status: v1alpha1.LDAPIdentityProviderStatus{
Phase: "Ready", Phase: "Ready",
Conditions: allConditionsTrue(1234, "4242"), Conditions: allConditionsTrue(1234, "4242"),
@ -841,7 +853,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
}, },
wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS}, wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS},
wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testResourceUID},
Status: v1alpha1.LDAPIdentityProviderStatus{ Status: v1alpha1.LDAPIdentityProviderStatus{
Phase: "Ready", Phase: "Ready",
Conditions: allConditionsTrue(1234, "4242"), Conditions: allConditionsTrue(1234, "4242"),
@ -877,7 +889,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
}, },
wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS}, wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS},
wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testResourceUID},
Status: v1alpha1.LDAPIdentityProviderStatus{ Status: v1alpha1.LDAPIdentityProviderStatus{
Phase: "Ready", Phase: "Ready",
Conditions: allConditionsTrue(1234, "4242"), Conditions: allConditionsTrue(1234, "4242"),
@ -917,7 +929,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
}, },
wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS}, wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS},
wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testResourceUID},
Status: v1alpha1.LDAPIdentityProviderStatus{ Status: v1alpha1.LDAPIdentityProviderStatus{
Phase: "Ready", Phase: "Ready",
Conditions: allConditionsTrue(1234, "4242"), Conditions: allConditionsTrue(1234, "4242"),
@ -954,7 +966,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
}, },
wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS}, wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS},
wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testResourceUID},
Status: v1alpha1.LDAPIdentityProviderStatus{ Status: v1alpha1.LDAPIdentityProviderStatus{
Phase: "Ready", Phase: "Ready",
Conditions: allConditionsTrue(1234, "4242"), Conditions: allConditionsTrue(1234, "4242"),

View File

@ -12,10 +12,11 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"net/url" "net/url"
"sort"
"strings" "strings"
"time" "time"
"k8s.io/apimachinery/pkg/util/sets"
"github.com/coreos/go-oidc/v3/oidc" "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"
@ -52,16 +53,41 @@ const (
oidcValidatorCacheTTL = 15 * time.Minute oidcValidatorCacheTTL = 15 * time.Minute
// Constants related to conditions. // Constants related to conditions.
typeClientCredentialsValid = "ClientCredentialsValid" typeClientCredentialsValid = "ClientCredentialsValid"
typeOIDCDiscoverySucceeded = "OIDCDiscoverySucceeded" typeAdditionalAuthorizeParametersValid = "AdditionalAuthorizeParametersValid"
typeOIDCDiscoverySucceeded = "OIDCDiscoverySucceeded"
reasonUnreachable = "Unreachable" reasonUnreachable = "Unreachable"
reasonInvalidResponse = "InvalidResponse" reasonInvalidResponse = "InvalidResponse"
reasonDisallowedParameterName = "DisallowedParameterName"
allParamNamesAllowedMsg = "additionalAuthorizeParameters parameter names are allowed"
// Errors that are generated by our reconcile process. // Errors that are generated by our reconcile process.
errOIDCFailureStatus = constable.Error("OIDCIdentityProvider has a failing condition") errOIDCFailureStatus = constable.Error("OIDCIdentityProvider has a failing condition")
) )
var (
disallowedAdditionalAuthorizeParameters = map[string]bool{ //nolint: gochecknoglobals
// Reject these AdditionalAuthorizeParameters to avoid allowing the user's config to overwrite the parameters
// that are always used by Pinniped in authcode authorization requests. The OIDC library used would otherwise
// happily treat the user's config as an override. Users can already set the "client_id" and "scope" params
// using other settings, and the others never make sense to override. This map should be treated as read-only
// since it is a global variable.
"response_type": true,
"scope": true,
"client_id": true,
"state": true,
"nonce": true,
"code_challenge": true,
"code_challenge_method": true,
"redirect_uri": true,
// Reject "hd" for now because it is not safe to use with Google's OIDC provider until Pinniped also
// performs the corresponding validation on the ID token.
"hd": true,
}
)
// UpstreamOIDCIdentityProviderICache is a thread safe cache that holds a list of validated upstream OIDC IDP configurations. // UpstreamOIDCIdentityProviderICache is a thread safe cache that holds a list of validated upstream OIDC IDP configurations.
type UpstreamOIDCIdentityProviderICache interface { type UpstreamOIDCIdentityProviderICache interface {
SetOIDCIdentityProviders([]provider.UpstreamOIDCIdentityProviderI) SetOIDCIdentityProviders([]provider.UpstreamOIDCIdentityProviderI)
@ -167,19 +193,51 @@ func (c *oidcWatcherController) Sync(ctx controllerlib.Context) error {
// validateUpstream validates the provided v1alpha1.OIDCIdentityProvider and returns the validated configuration as a // validateUpstream validates the provided v1alpha1.OIDCIdentityProvider and returns the validated configuration as a
// provider.UpstreamOIDCIdentityProvider. As a side effect, it also updates the status of the v1alpha1.OIDCIdentityProvider. // provider.UpstreamOIDCIdentityProvider. As a side effect, it also updates the status of the v1alpha1.OIDCIdentityProvider.
func (c *oidcWatcherController) validateUpstream(ctx controllerlib.Context, upstream *v1alpha1.OIDCIdentityProvider) *upstreamoidc.ProviderConfig { func (c *oidcWatcherController) validateUpstream(ctx controllerlib.Context, upstream *v1alpha1.OIDCIdentityProvider) *upstreamoidc.ProviderConfig {
authorizationConfig := upstream.Spec.AuthorizationConfig
additionalAuthcodeAuthorizeParameters := map[string]string{}
var rejectedAuthcodeAuthorizeParameters []string
for _, p := range authorizationConfig.AdditionalAuthorizeParameters {
if disallowedAdditionalAuthorizeParameters[p.Name] {
rejectedAuthcodeAuthorizeParameters = append(rejectedAuthcodeAuthorizeParameters, p.Name)
} else {
additionalAuthcodeAuthorizeParameters[p.Name] = p.Value
}
}
result := upstreamoidc.ProviderConfig{ result := upstreamoidc.ProviderConfig{
Name: upstream.Name, Name: upstream.Name,
Config: &oauth2.Config{ Config: &oauth2.Config{
Scopes: computeScopes(upstream.Spec.AuthorizationConfig.AdditionalScopes), Scopes: computeScopes(authorizationConfig.AdditionalScopes),
}, },
UsernameClaim: upstream.Spec.Claims.Username, UsernameClaim: upstream.Spec.Claims.Username,
GroupsClaim: upstream.Spec.Claims.Groups, GroupsClaim: upstream.Spec.Claims.Groups,
AllowPasswordGrant: upstream.Spec.AuthorizationConfig.AllowPasswordGrant, AllowPasswordGrant: authorizationConfig.AllowPasswordGrant,
AdditionalAuthcodeParams: additionalAuthcodeAuthorizeParameters,
ResourceUID: upstream.UID,
} }
conditions := []*v1alpha1.Condition{ conditions := []*v1alpha1.Condition{
c.validateSecret(upstream, &result), c.validateSecret(upstream, &result),
c.validateIssuer(ctx.Context, upstream, &result), c.validateIssuer(ctx.Context, upstream, &result),
} }
if len(rejectedAuthcodeAuthorizeParameters) > 0 {
conditions = append(conditions, &v1alpha1.Condition{
Type: typeAdditionalAuthorizeParametersValid,
Status: v1alpha1.ConditionFalse,
Reason: reasonDisallowedParameterName,
Message: fmt.Sprintf("the following additionalAuthorizeParameters are not allowed: %s",
strings.Join(rejectedAuthcodeAuthorizeParameters, ",")),
})
} else {
conditions = append(conditions, &v1alpha1.Condition{
Type: typeAdditionalAuthorizeParametersValid,
Status: v1alpha1.ConditionTrue,
Reason: upstreamwatchers.ReasonSuccess,
Message: allParamNamesAllowedMsg,
})
}
c.updateStatus(ctx.Context, upstream, conditions) c.updateStatus(ctx.Context, upstream, conditions)
valid := true valid := true
@ -371,20 +429,20 @@ func getTLSConfig(upstream *v1alpha1.OIDCIdentityProvider) (*tls.Config, error)
} }
func computeScopes(additionalScopes []string) []string { func computeScopes(additionalScopes []string) []string {
// First compute the unique set of scopes, including "openid" (de-duplicate). // If none are set then provide a reasonable default which only tries to use scopes defined in the OIDC spec.
set := make(map[string]bool, len(additionalScopes)+1) if len(additionalScopes) == 0 {
set["openid"] = true return []string{"openid", "offline_access", "email", "profile"}
for _, s := range additionalScopes {
set[s] = true
} }
// Then grab all the keys and sort them. // Otherwise, first compute the unique set of scopes, including "openid" (de-duplicate).
scopes := make([]string, 0, len(set)) set := sets.NewString()
for s := range set { set.Insert("openid")
scopes = append(scopes, s) for _, s := range additionalScopes {
set.Insert(s)
} }
sort.Strings(scopes)
return scopes // Return the set as a sorted list.
return set.List()
} }
func truncateMostLongErr(err error) string { func truncateMostLongErr(err error) string {

View File

@ -18,6 +18,7 @@ import (
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/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/informers" "k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes/fake" "k8s.io/client-go/kubernetes/fake"
@ -118,17 +119,31 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
wrongCABase64 := base64.StdEncoding.EncodeToString(wrongCA.Bundle()) wrongCABase64 := base64.StdEncoding.EncodeToString(wrongCA.Bundle())
happyAdditionalAuthorizeParametersValidCondition := v1alpha1.Condition{
Type: "AdditionalAuthorizeParametersValid",
Status: "True",
Reason: "Success",
Message: "additionalAuthorizeParameters parameter names are allowed",
LastTransitionTime: now,
}
happyAdditionalAuthorizeParametersValidConditionEarlier := happyAdditionalAuthorizeParametersValidCondition
happyAdditionalAuthorizeParametersValidConditionEarlier.LastTransitionTime = earlier
var ( var (
testNamespace = "test-namespace" testNamespace = "test-namespace"
testName = "test-name" testName = "test-name"
testSecretName = "test-client-secret" testSecretName = "test-client-secret"
testAdditionalScopes = []string{"scope1", "scope2", "scope3"} testAdditionalScopes = []string{"scope1", "scope2", "scope3"}
testExpectedScopes = []string{"openid", "scope1", "scope2", "scope3"} testExpectedScopes = []string{"openid", "scope1", "scope2", "scope3"}
testClientID = "test-oidc-client-id" testDefaultExpectedScopes = []string{"openid", "offline_access", "email", "profile"}
testClientSecret = "test-oidc-client-secret" testAdditionalParams = []v1alpha1.Parameter{{Name: "prompt", Value: "consent"}, {Name: "foo", Value: "bar"}}
testValidSecretData = map[string][]byte{"clientID": []byte(testClientID), "clientSecret": []byte(testClientSecret)} testExpectedAdditionalParams = map[string]string{"prompt": "consent", "foo": "bar"}
testGroupsClaim = "test-groups-claim" testClientID = "test-oidc-client-id"
testUsernameClaim = "test-username-claim" testClientSecret = "test-oidc-client-secret"
testValidSecretData = map[string][]byte{"clientID": []byte(testClientID), "clientSecret": []byte(testClientSecret)}
testGroupsClaim = "test-groups-claim"
testUsernameClaim = "test-username-claim"
testUID = types.UID("test-uid")
) )
tests := []struct { tests := []struct {
name string name string
@ -147,10 +162,9 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) {
inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{ inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName},
Spec: v1alpha1.OIDCIdentityProviderSpec{ Spec: v1alpha1.OIDCIdentityProviderSpec{
Issuer: testIssuerURL, Issuer: testIssuerURL,
TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64}, TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64},
Client: v1alpha1.OIDCClient{SecretName: testSecretName}, Client: v1alpha1.OIDCClient{SecretName: testSecretName},
AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: testAdditionalScopes},
}, },
}}, }},
inputSecrets: []runtime.Object{}, inputSecrets: []runtime.Object{},
@ -158,6 +172,7 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) {
wantLogs: []string{ wantLogs: []string{
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="secret \"test-client-secret\" not found" "reason"="SecretNotFound" "status"="False" "type"="ClientCredentialsValid"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="secret \"test-client-secret\" not found" "reason"="SecretNotFound" "status"="False" "type"="ClientCredentialsValid"`,
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="discovered issuer configuration" "reason"="Success" "status"="True" "type"="OIDCDiscoverySucceeded"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="discovered issuer configuration" "reason"="Success" "status"="True" "type"="OIDCDiscoverySucceeded"`,
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="additionalAuthorizeParameters parameter names are allowed" "reason"="Success" "status"="True" "type"="AdditionalAuthorizeParametersValid"`,
`oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="secret \"test-client-secret\" not found" "name"="test-name" "namespace"="test-namespace" "reason"="SecretNotFound" "type"="ClientCredentialsValid"`, `oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="secret \"test-client-secret\" not found" "name"="test-name" "namespace"="test-namespace" "reason"="SecretNotFound" "type"="ClientCredentialsValid"`,
}, },
wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{}, wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{},
@ -166,6 +181,7 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) {
Status: v1alpha1.OIDCIdentityProviderStatus{ Status: v1alpha1.OIDCIdentityProviderStatus{
Phase: "Error", Phase: "Error",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
happyAdditionalAuthorizeParametersValidCondition,
{ {
Type: "ClientCredentialsValid", Type: "ClientCredentialsValid",
Status: "False", Status: "False",
@ -189,10 +205,9 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) {
inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{ inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName},
Spec: v1alpha1.OIDCIdentityProviderSpec{ Spec: v1alpha1.OIDCIdentityProviderSpec{
Issuer: testIssuerURL, Issuer: testIssuerURL,
TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64}, TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64},
Client: v1alpha1.OIDCClient{SecretName: testSecretName}, Client: v1alpha1.OIDCClient{SecretName: testSecretName},
AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: testAdditionalScopes},
}, },
}}, }},
inputSecrets: []runtime.Object{&corev1.Secret{ inputSecrets: []runtime.Object{&corev1.Secret{
@ -204,6 +219,7 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) {
wantLogs: []string{ wantLogs: []string{
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="referenced Secret \"test-client-secret\" has wrong type \"some-other-type\" (should be \"secrets.pinniped.dev/oidc-client\")" "reason"="SecretWrongType" "status"="False" "type"="ClientCredentialsValid"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="referenced Secret \"test-client-secret\" has wrong type \"some-other-type\" (should be \"secrets.pinniped.dev/oidc-client\")" "reason"="SecretWrongType" "status"="False" "type"="ClientCredentialsValid"`,
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="discovered issuer configuration" "reason"="Success" "status"="True" "type"="OIDCDiscoverySucceeded"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="discovered issuer configuration" "reason"="Success" "status"="True" "type"="OIDCDiscoverySucceeded"`,
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="additionalAuthorizeParameters parameter names are allowed" "reason"="Success" "status"="True" "type"="AdditionalAuthorizeParametersValid"`,
`oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="referenced Secret \"test-client-secret\" has wrong type \"some-other-type\" (should be \"secrets.pinniped.dev/oidc-client\")" "name"="test-name" "namespace"="test-namespace" "reason"="SecretWrongType" "type"="ClientCredentialsValid"`, `oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="referenced Secret \"test-client-secret\" has wrong type \"some-other-type\" (should be \"secrets.pinniped.dev/oidc-client\")" "name"="test-name" "namespace"="test-namespace" "reason"="SecretWrongType" "type"="ClientCredentialsValid"`,
}, },
wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{}, wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{},
@ -212,6 +228,7 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) {
Status: v1alpha1.OIDCIdentityProviderStatus{ Status: v1alpha1.OIDCIdentityProviderStatus{
Phase: "Error", Phase: "Error",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
happyAdditionalAuthorizeParametersValidCondition,
{ {
Type: "ClientCredentialsValid", Type: "ClientCredentialsValid",
Status: "False", Status: "False",
@ -235,10 +252,9 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) {
inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{ inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName},
Spec: v1alpha1.OIDCIdentityProviderSpec{ Spec: v1alpha1.OIDCIdentityProviderSpec{
Issuer: testIssuerURL, Issuer: testIssuerURL,
TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64}, TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64},
Client: v1alpha1.OIDCClient{SecretName: testSecretName}, Client: v1alpha1.OIDCClient{SecretName: testSecretName},
AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: testAdditionalScopes},
}, },
}}, }},
inputSecrets: []runtime.Object{&corev1.Secret{ inputSecrets: []runtime.Object{&corev1.Secret{
@ -249,6 +265,7 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) {
wantLogs: []string{ wantLogs: []string{
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="referenced Secret \"test-client-secret\" is missing required keys [\"clientID\" \"clientSecret\"]" "reason"="SecretMissingKeys" "status"="False" "type"="ClientCredentialsValid"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="referenced Secret \"test-client-secret\" is missing required keys [\"clientID\" \"clientSecret\"]" "reason"="SecretMissingKeys" "status"="False" "type"="ClientCredentialsValid"`,
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="discovered issuer configuration" "reason"="Success" "status"="True" "type"="OIDCDiscoverySucceeded"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="discovered issuer configuration" "reason"="Success" "status"="True" "type"="OIDCDiscoverySucceeded"`,
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="additionalAuthorizeParameters parameter names are allowed" "reason"="Success" "status"="True" "type"="AdditionalAuthorizeParametersValid"`,
`oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="referenced Secret \"test-client-secret\" is missing required keys [\"clientID\" \"clientSecret\"]" "name"="test-name" "namespace"="test-namespace" "reason"="SecretMissingKeys" "type"="ClientCredentialsValid"`, `oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="referenced Secret \"test-client-secret\" is missing required keys [\"clientID\" \"clientSecret\"]" "name"="test-name" "namespace"="test-namespace" "reason"="SecretMissingKeys" "type"="ClientCredentialsValid"`,
}, },
wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{}, wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{},
@ -257,6 +274,7 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) {
Status: v1alpha1.OIDCIdentityProviderStatus{ Status: v1alpha1.OIDCIdentityProviderStatus{
Phase: "Error", Phase: "Error",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
happyAdditionalAuthorizeParametersValidCondition,
{ {
Type: "ClientCredentialsValid", Type: "ClientCredentialsValid",
Status: "False", Status: "False",
@ -284,8 +302,7 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) {
TLS: &v1alpha1.TLSSpec{ TLS: &v1alpha1.TLSSpec{
CertificateAuthorityData: "invalid-base64", CertificateAuthorityData: "invalid-base64",
}, },
Client: v1alpha1.OIDCClient{SecretName: testSecretName}, Client: v1alpha1.OIDCClient{SecretName: testSecretName},
AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: append(testAdditionalScopes, "xyz", "openid")},
}, },
}}, }},
inputSecrets: []runtime.Object{&corev1.Secret{ inputSecrets: []runtime.Object{&corev1.Secret{
@ -297,6 +314,7 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) {
wantLogs: []string{ wantLogs: []string{
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`,
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="spec.certificateAuthorityData is invalid: illegal base64 data at input byte 7" "reason"="InvalidTLSConfig" "status"="False" "type"="OIDCDiscoverySucceeded"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="spec.certificateAuthorityData is invalid: illegal base64 data at input byte 7" "reason"="InvalidTLSConfig" "status"="False" "type"="OIDCDiscoverySucceeded"`,
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="additionalAuthorizeParameters parameter names are allowed" "reason"="Success" "status"="True" "type"="AdditionalAuthorizeParametersValid"`,
`oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="spec.certificateAuthorityData is invalid: illegal base64 data at input byte 7" "name"="test-name" "namespace"="test-namespace" "reason"="InvalidTLSConfig" "type"="OIDCDiscoverySucceeded"`, `oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="spec.certificateAuthorityData is invalid: illegal base64 data at input byte 7" "name"="test-name" "namespace"="test-namespace" "reason"="InvalidTLSConfig" "type"="OIDCDiscoverySucceeded"`,
}, },
wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{}, wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{},
@ -305,6 +323,7 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) {
Status: v1alpha1.OIDCIdentityProviderStatus{ Status: v1alpha1.OIDCIdentityProviderStatus{
Phase: "Error", Phase: "Error",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
happyAdditionalAuthorizeParametersValidCondition,
{ {
Type: "ClientCredentialsValid", Type: "ClientCredentialsValid",
Status: "True", Status: "True",
@ -332,8 +351,7 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) {
TLS: &v1alpha1.TLSSpec{ TLS: &v1alpha1.TLSSpec{
CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte("not-a-pem-ca-bundle")), CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte("not-a-pem-ca-bundle")),
}, },
Client: v1alpha1.OIDCClient{SecretName: testSecretName}, Client: v1alpha1.OIDCClient{SecretName: testSecretName},
AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: append(testAdditionalScopes, "xyz", "openid")},
}, },
}}, }},
inputSecrets: []runtime.Object{&corev1.Secret{ inputSecrets: []runtime.Object{&corev1.Secret{
@ -345,6 +363,7 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) {
wantLogs: []string{ wantLogs: []string{
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`,
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="spec.certificateAuthorityData is invalid: no certificates found" "reason"="InvalidTLSConfig" "status"="False" "type"="OIDCDiscoverySucceeded"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="spec.certificateAuthorityData is invalid: no certificates found" "reason"="InvalidTLSConfig" "status"="False" "type"="OIDCDiscoverySucceeded"`,
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="additionalAuthorizeParameters parameter names are allowed" "reason"="Success" "status"="True" "type"="AdditionalAuthorizeParametersValid"`,
`oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="spec.certificateAuthorityData is invalid: no certificates found" "name"="test-name" "namespace"="test-namespace" "reason"="InvalidTLSConfig" "type"="OIDCDiscoverySucceeded"`, `oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="spec.certificateAuthorityData is invalid: no certificates found" "name"="test-name" "namespace"="test-namespace" "reason"="InvalidTLSConfig" "type"="OIDCDiscoverySucceeded"`,
}, },
wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{}, wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{},
@ -353,6 +372,7 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) {
Status: v1alpha1.OIDCIdentityProviderStatus{ Status: v1alpha1.OIDCIdentityProviderStatus{
Phase: "Error", Phase: "Error",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
happyAdditionalAuthorizeParametersValidCondition,
{ {
Type: "ClientCredentialsValid", Type: "ClientCredentialsValid",
Status: "True", Status: "True",
@ -376,9 +396,8 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) {
inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{ inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName},
Spec: v1alpha1.OIDCIdentityProviderSpec{ Spec: v1alpha1.OIDCIdentityProviderSpec{
Issuer: "invalid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", Issuer: "invalid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
Client: v1alpha1.OIDCClient{SecretName: testSecretName}, Client: v1alpha1.OIDCClient{SecretName: testSecretName},
AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: testAdditionalScopes},
}, },
}}, }},
inputSecrets: []runtime.Object{&corev1.Secret{ inputSecrets: []runtime.Object{&corev1.Secret{
@ -391,6 +410,7 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) {
`oidc-upstream-observer "msg"="failed to perform OIDC discovery" "error"="Get \"invalid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee/.well-known/openid-configuration\": unsupported protocol scheme \"\"" "issuer"="invalid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" "name"="test-name" "namespace"="test-namespace"`, `oidc-upstream-observer "msg"="failed to perform OIDC discovery" "error"="Get \"invalid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee/.well-known/openid-configuration\": unsupported protocol scheme \"\"" "issuer"="invalid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" "name"="test-name" "namespace"="test-namespace"`,
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`,
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="failed to perform OIDC discovery against \"invalid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee\":\nGet \"invalid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee/.well-known/openid-configuration\": unsupported protocol [truncated 9 chars]" "reason"="Unreachable" "status"="False" "type"="OIDCDiscoverySucceeded"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="failed to perform OIDC discovery against \"invalid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee\":\nGet \"invalid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee/.well-known/openid-configuration\": unsupported protocol [truncated 9 chars]" "reason"="Unreachable" "status"="False" "type"="OIDCDiscoverySucceeded"`,
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="additionalAuthorizeParameters parameter names are allowed" "reason"="Success" "status"="True" "type"="AdditionalAuthorizeParametersValid"`,
`oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="failed to perform OIDC discovery against \"invalid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee\":\nGet \"invalid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee/.well-known/openid-configuration\": unsupported protocol [truncated 9 chars]" "name"="test-name" "namespace"="test-namespace" "reason"="Unreachable" "type"="OIDCDiscoverySucceeded"`, `oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="failed to perform OIDC discovery against \"invalid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee\":\nGet \"invalid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee/.well-known/openid-configuration\": unsupported protocol [truncated 9 chars]" "name"="test-name" "namespace"="test-namespace" "reason"="Unreachable" "type"="OIDCDiscoverySucceeded"`,
}, },
wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{}, wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{},
@ -399,6 +419,7 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) {
Status: v1alpha1.OIDCIdentityProviderStatus{ Status: v1alpha1.OIDCIdentityProviderStatus{
Phase: "Error", Phase: "Error",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
happyAdditionalAuthorizeParametersValidCondition,
{ {
Type: "ClientCredentialsValid", Type: "ClientCredentialsValid",
Status: "True", Status: "True",
@ -423,10 +444,9 @@ Get "invalid-url-that-is-really-really-long-nanananananananannanananan-batman-na
inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{ inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName},
Spec: v1alpha1.OIDCIdentityProviderSpec{ Spec: v1alpha1.OIDCIdentityProviderSpec{
Issuer: testIssuerURL + "/valid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", Issuer: testIssuerURL + "/valid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: wrongCABase64}, TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: wrongCABase64},
Client: v1alpha1.OIDCClient{SecretName: testSecretName}, Client: v1alpha1.OIDCClient{SecretName: testSecretName},
AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: testAdditionalScopes},
}, },
}}, }},
inputSecrets: []runtime.Object{&corev1.Secret{ inputSecrets: []runtime.Object{&corev1.Secret{
@ -439,6 +459,7 @@ Get "invalid-url-that-is-really-really-long-nanananananananannanananan-batman-na
`oidc-upstream-observer "msg"="failed to perform OIDC discovery" "error"="Get \"` + testIssuerURL + `/valid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee/.well-known/openid-configuration\": x509: certificate signed by unknown authority" "issuer"="` + testIssuerURL + `/valid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" "name"="test-name" "namespace"="test-namespace"`, `oidc-upstream-observer "msg"="failed to perform OIDC discovery" "error"="Get \"` + testIssuerURL + `/valid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee/.well-known/openid-configuration\": x509: certificate signed by unknown authority" "issuer"="` + testIssuerURL + `/valid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" "name"="test-name" "namespace"="test-namespace"`,
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`,
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="failed to perform OIDC discovery against \"` + testIssuerURL + `/valid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee\":\nGet \"` + testIssuerURL + `/valid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee/.well-known/openid-configuration\": x509: certificate signed by unknown authority" "reason"="Unreachable" "status"="False" "type"="OIDCDiscoverySucceeded"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="failed to perform OIDC discovery against \"` + testIssuerURL + `/valid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee\":\nGet \"` + testIssuerURL + `/valid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee/.well-known/openid-configuration\": x509: certificate signed by unknown authority" "reason"="Unreachable" "status"="False" "type"="OIDCDiscoverySucceeded"`,
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="additionalAuthorizeParameters parameter names are allowed" "reason"="Success" "status"="True" "type"="AdditionalAuthorizeParametersValid"`,
`oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="failed to perform OIDC discovery against \"` + testIssuerURL + `/valid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee\":\nGet \"` + testIssuerURL + `/valid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee/.well-known/openid-configuration\": x509: certificate signed by unknown authority" "name"="test-name" "namespace"="test-namespace" "reason"="Unreachable" "type"="OIDCDiscoverySucceeded"`, `oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="failed to perform OIDC discovery against \"` + testIssuerURL + `/valid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee\":\nGet \"` + testIssuerURL + `/valid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee/.well-known/openid-configuration\": x509: certificate signed by unknown authority" "name"="test-name" "namespace"="test-namespace" "reason"="Unreachable" "type"="OIDCDiscoverySucceeded"`,
}, },
wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{}, wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{},
@ -447,6 +468,7 @@ Get "invalid-url-that-is-really-really-long-nanananananananannanananan-batman-na
Status: v1alpha1.OIDCIdentityProviderStatus{ Status: v1alpha1.OIDCIdentityProviderStatus{
Phase: "Error", Phase: "Error",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
happyAdditionalAuthorizeParametersValidCondition,
{ {
Type: "ClientCredentialsValid", Type: "ClientCredentialsValid",
Status: "True", Status: "True",
@ -471,10 +493,9 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana
inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{ inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName},
Spec: v1alpha1.OIDCIdentityProviderSpec{ Spec: v1alpha1.OIDCIdentityProviderSpec{
Issuer: testIssuerURL + "/invalid", Issuer: testIssuerURL + "/invalid",
TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64}, TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64},
Client: v1alpha1.OIDCClient{SecretName: testSecretName}, Client: v1alpha1.OIDCClient{SecretName: testSecretName},
AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: testAdditionalScopes},
}, },
}}, }},
inputSecrets: []runtime.Object{&corev1.Secret{ inputSecrets: []runtime.Object{&corev1.Secret{
@ -486,6 +507,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana
wantLogs: []string{ wantLogs: []string{
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`,
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="failed to parse authorization endpoint URL: parse \"%\": invalid URL escape \"%\"" "reason"="InvalidResponse" "status"="False" "type"="OIDCDiscoverySucceeded"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="failed to parse authorization endpoint URL: parse \"%\": invalid URL escape \"%\"" "reason"="InvalidResponse" "status"="False" "type"="OIDCDiscoverySucceeded"`,
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="additionalAuthorizeParameters parameter names are allowed" "reason"="Success" "status"="True" "type"="AdditionalAuthorizeParametersValid"`,
`oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="failed to parse authorization endpoint URL: parse \"%\": invalid URL escape \"%\"" "name"="test-name" "namespace"="test-namespace" "reason"="InvalidResponse" "type"="OIDCDiscoverySucceeded"`, `oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="failed to parse authorization endpoint URL: parse \"%\": invalid URL escape \"%\"" "name"="test-name" "namespace"="test-namespace" "reason"="InvalidResponse" "type"="OIDCDiscoverySucceeded"`,
}, },
wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{}, wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{},
@ -494,6 +516,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana
Status: v1alpha1.OIDCIdentityProviderStatus{ Status: v1alpha1.OIDCIdentityProviderStatus{
Phase: "Error", Phase: "Error",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
happyAdditionalAuthorizeParametersValidCondition,
{ {
Type: "ClientCredentialsValid", Type: "ClientCredentialsValid",
Status: "True", Status: "True",
@ -517,10 +540,9 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana
inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{ inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName},
Spec: v1alpha1.OIDCIdentityProviderSpec{ Spec: v1alpha1.OIDCIdentityProviderSpec{
Issuer: testIssuerURL + "/insecure", Issuer: testIssuerURL + "/insecure",
TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64}, TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64},
Client: v1alpha1.OIDCClient{SecretName: testSecretName}, Client: v1alpha1.OIDCClient{SecretName: testSecretName},
AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: testAdditionalScopes},
}, },
}}, }},
inputSecrets: []runtime.Object{&corev1.Secret{ inputSecrets: []runtime.Object{&corev1.Secret{
@ -532,6 +554,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana
wantLogs: []string{ wantLogs: []string{
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`,
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="authorization endpoint URL scheme must be \"https\", not \"http\"" "reason"="InvalidResponse" "status"="False" "type"="OIDCDiscoverySucceeded"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="authorization endpoint URL scheme must be \"https\", not \"http\"" "reason"="InvalidResponse" "status"="False" "type"="OIDCDiscoverySucceeded"`,
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="additionalAuthorizeParameters parameter names are allowed" "reason"="Success" "status"="True" "type"="AdditionalAuthorizeParametersValid"`,
`oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="authorization endpoint URL scheme must be \"https\", not \"http\"" "name"="test-name" "namespace"="test-namespace" "reason"="InvalidResponse" "type"="OIDCDiscoverySucceeded"`, `oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="authorization endpoint URL scheme must be \"https\", not \"http\"" "name"="test-name" "namespace"="test-namespace" "reason"="InvalidResponse" "type"="OIDCDiscoverySucceeded"`,
}, },
wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{}, wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{},
@ -540,6 +563,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana
Status: v1alpha1.OIDCIdentityProviderStatus{ Status: v1alpha1.OIDCIdentityProviderStatus{
Phase: "Error", Phase: "Error",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
happyAdditionalAuthorizeParametersValidCondition,
{ {
Type: "ClientCredentialsValid", Type: "ClientCredentialsValid",
Status: "True", Status: "True",
@ -561,13 +585,13 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana
{ {
name: "upstream with error becomes valid", name: "upstream with error becomes valid",
inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{ inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: "test-name"}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: "test-name", UID: testUID},
Spec: v1alpha1.OIDCIdentityProviderSpec{ Spec: v1alpha1.OIDCIdentityProviderSpec{
Issuer: testIssuerURL, Issuer: testIssuerURL,
TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64}, TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64},
Client: v1alpha1.OIDCClient{SecretName: testSecretName}, Client: v1alpha1.OIDCClient{SecretName: testSecretName},
AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{ AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{
AdditionalScopes: append(testAdditionalScopes, "xyz", "openid"), AdditionalScopes: append(testAdditionalScopes, "xyz", "openid"), // adds openid unnecessarily
AllowPasswordGrant: true, AllowPasswordGrant: true,
}, },
Claims: v1alpha1.OIDCClaims{Groups: testGroupsClaim, Username: testUsernameClaim}, Claims: v1alpha1.OIDCClaims{Groups: testGroupsClaim, Username: testUsernameClaim},
@ -588,23 +612,27 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana
wantLogs: []string{ wantLogs: []string{
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`,
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="discovered issuer configuration" "reason"="Success" "status"="True" "type"="OIDCDiscoverySucceeded"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="discovered issuer configuration" "reason"="Success" "status"="True" "type"="OIDCDiscoverySucceeded"`,
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="additionalAuthorizeParameters parameter names are allowed" "reason"="Success" "status"="True" "type"="AdditionalAuthorizeParametersValid"`,
}, },
wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{ wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{
&oidctestutil.TestUpstreamOIDCIdentityProvider{ &oidctestutil.TestUpstreamOIDCIdentityProvider{
Name: testName, Name: testName,
ClientID: testClientID, ClientID: testClientID,
AuthorizationURL: *testIssuerAuthorizeURL, AuthorizationURL: *testIssuerAuthorizeURL,
Scopes: append(testExpectedScopes, "xyz"), Scopes: append(testExpectedScopes, "xyz"), // includes openid only once
UsernameClaim: testUsernameClaim, UsernameClaim: testUsernameClaim,
GroupsClaim: testGroupsClaim, GroupsClaim: testGroupsClaim,
AllowPasswordGrant: true, AllowPasswordGrant: true,
AdditionalAuthcodeParams: map[string]string{},
ResourceUID: testUID,
}, },
}, },
wantResultingUpstreams: []v1alpha1.OIDCIdentityProvider{{ wantResultingUpstreams: []v1alpha1.OIDCIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testUID},
Status: v1alpha1.OIDCIdentityProviderStatus{ Status: v1alpha1.OIDCIdentityProviderStatus{
Phase: "Ready", Phase: "Ready",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
happyAdditionalAuthorizeParametersValidCondition,
{Type: "ClientCredentialsValid", Status: "True", LastTransitionTime: now, Reason: "Success", Message: "loaded client credentials"}, {Type: "ClientCredentialsValid", Status: "True", LastTransitionTime: now, Reason: "Success", Message: "loaded client credentials"},
{Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: now, Reason: "Success", Message: "discovered issuer configuration"}, {Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: now, Reason: "Success", Message: "discovered issuer configuration"},
}, },
@ -612,22 +640,135 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana
}}, }},
}, },
{ {
name: "existing valid upstream", name: "existing valid upstream with default authorizationConfig",
inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{ inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID},
Spec: v1alpha1.OIDCIdentityProviderSpec{ Spec: v1alpha1.OIDCIdentityProviderSpec{
Issuer: testIssuerURL, Issuer: testIssuerURL,
TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64}, TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64},
Client: v1alpha1.OIDCClient{SecretName: testSecretName}, Client: v1alpha1.OIDCClient{SecretName: testSecretName},
Claims: v1alpha1.OIDCClaims{Groups: testGroupsClaim, Username: testUsernameClaim},
},
Status: v1alpha1.OIDCIdentityProviderStatus{
Phase: "Ready",
Conditions: []v1alpha1.Condition{
happyAdditionalAuthorizeParametersValidConditionEarlier,
{Type: "ClientCredentialsValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "loaded client credentials"},
{Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "discovered issuer configuration"},
},
},
}},
inputSecrets: []runtime.Object{&corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testSecretName},
Type: "secrets.pinniped.dev/oidc-client",
Data: testValidSecretData,
}},
wantLogs: []string{
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`,
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="discovered issuer configuration" "reason"="Success" "status"="True" "type"="OIDCDiscoverySucceeded"`,
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="additionalAuthorizeParameters parameter names are allowed" "reason"="Success" "status"="True" "type"="AdditionalAuthorizeParametersValid"`,
},
wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{
&oidctestutil.TestUpstreamOIDCIdentityProvider{
Name: testName,
ClientID: testClientID,
AuthorizationURL: *testIssuerAuthorizeURL,
Scopes: testDefaultExpectedScopes,
UsernameClaim: testUsernameClaim,
GroupsClaim: testGroupsClaim,
AllowPasswordGrant: false,
AdditionalAuthcodeParams: map[string]string{},
ResourceUID: testUID,
},
},
wantResultingUpstreams: []v1alpha1.OIDCIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID},
Status: v1alpha1.OIDCIdentityProviderStatus{
Phase: "Ready",
Conditions: []v1alpha1.Condition{
{Type: "AdditionalAuthorizeParametersValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "additionalAuthorizeParameters parameter names are allowed", ObservedGeneration: 1234},
{Type: "ClientCredentialsValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "loaded client credentials", ObservedGeneration: 1234},
{Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "discovered issuer configuration", ObservedGeneration: 1234},
},
},
}},
},
{
name: "existing valid upstream with additionalScopes set to override the default",
inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID},
Spec: v1alpha1.OIDCIdentityProviderSpec{
Issuer: testIssuerURL,
TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64},
Client: v1alpha1.OIDCClient{SecretName: testSecretName},
Claims: v1alpha1.OIDCClaims{Groups: testGroupsClaim, Username: testUsernameClaim},
AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{ AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{
AdditionalScopes: testAdditionalScopes, AdditionalScopes: testAdditionalScopes,
AllowPasswordGrant: false, },
},
Status: v1alpha1.OIDCIdentityProviderStatus{
Phase: "Ready",
Conditions: []v1alpha1.Condition{
happyAdditionalAuthorizeParametersValidConditionEarlier,
{Type: "ClientCredentialsValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "loaded client credentials"},
{Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "discovered issuer configuration"},
},
},
}},
inputSecrets: []runtime.Object{&corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testSecretName},
Type: "secrets.pinniped.dev/oidc-client",
Data: testValidSecretData,
}},
wantLogs: []string{
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`,
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="discovered issuer configuration" "reason"="Success" "status"="True" "type"="OIDCDiscoverySucceeded"`,
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="additionalAuthorizeParameters parameter names are allowed" "reason"="Success" "status"="True" "type"="AdditionalAuthorizeParametersValid"`,
},
wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{
&oidctestutil.TestUpstreamOIDCIdentityProvider{
Name: testName,
ClientID: testClientID,
AuthorizationURL: *testIssuerAuthorizeURL,
Scopes: testExpectedScopes,
UsernameClaim: testUsernameClaim,
GroupsClaim: testGroupsClaim,
AllowPasswordGrant: false,
AdditionalAuthcodeParams: map[string]string{},
ResourceUID: testUID,
},
},
wantResultingUpstreams: []v1alpha1.OIDCIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID},
Status: v1alpha1.OIDCIdentityProviderStatus{
Phase: "Ready",
Conditions: []v1alpha1.Condition{
{Type: "AdditionalAuthorizeParametersValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "additionalAuthorizeParameters parameter names are allowed", ObservedGeneration: 1234},
{Type: "ClientCredentialsValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "loaded client credentials", ObservedGeneration: 1234},
{Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "discovered issuer configuration", ObservedGeneration: 1234},
},
},
}},
},
{
name: "existing valid upstream with trailing slash and more optional settings",
inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID},
Spec: v1alpha1.OIDCIdentityProviderSpec{
Issuer: testIssuerURL + "/ends-with-slash/",
TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64},
Client: v1alpha1.OIDCClient{SecretName: testSecretName},
AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{
AdditionalScopes: testAdditionalScopes,
AdditionalAuthorizeParameters: testAdditionalParams,
AllowPasswordGrant: true,
}, },
Claims: v1alpha1.OIDCClaims{Groups: testGroupsClaim, Username: testUsernameClaim}, Claims: v1alpha1.OIDCClaims{Groups: testGroupsClaim, Username: testUsernameClaim},
}, },
Status: v1alpha1.OIDCIdentityProviderStatus{ Status: v1alpha1.OIDCIdentityProviderStatus{
Phase: "Ready", Phase: "Ready",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
happyAdditionalAuthorizeParametersValidConditionEarlier,
{Type: "ClientCredentialsValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "loaded client credentials"}, {Type: "ClientCredentialsValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "loaded client credentials"},
{Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "discovered issuer configuration"}, {Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "discovered issuer configuration"},
}, },
@ -641,23 +782,27 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana
wantLogs: []string{ wantLogs: []string{
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`,
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="discovered issuer configuration" "reason"="Success" "status"="True" "type"="OIDCDiscoverySucceeded"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="discovered issuer configuration" "reason"="Success" "status"="True" "type"="OIDCDiscoverySucceeded"`,
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="additionalAuthorizeParameters parameter names are allowed" "reason"="Success" "status"="True" "type"="AdditionalAuthorizeParametersValid"`,
}, },
wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{ wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{
&oidctestutil.TestUpstreamOIDCIdentityProvider{ &oidctestutil.TestUpstreamOIDCIdentityProvider{
Name: testName, Name: testName,
ClientID: testClientID, ClientID: testClientID,
AuthorizationURL: *testIssuerAuthorizeURL, AuthorizationURL: *testIssuerAuthorizeURL,
Scopes: testExpectedScopes, Scopes: testExpectedScopes, // does not include the default scopes
UsernameClaim: testUsernameClaim, UsernameClaim: testUsernameClaim,
GroupsClaim: testGroupsClaim, GroupsClaim: testGroupsClaim,
AllowPasswordGrant: false, AllowPasswordGrant: true,
AdditionalAuthcodeParams: testExpectedAdditionalParams,
ResourceUID: testUID,
}, },
}, },
wantResultingUpstreams: []v1alpha1.OIDCIdentityProvider{{ wantResultingUpstreams: []v1alpha1.OIDCIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID},
Status: v1alpha1.OIDCIdentityProviderStatus{ Status: v1alpha1.OIDCIdentityProviderStatus{
Phase: "Ready", Phase: "Ready",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
{Type: "AdditionalAuthorizeParametersValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "additionalAuthorizeParameters parameter names are allowed", ObservedGeneration: 1234},
{Type: "ClientCredentialsValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "loaded client credentials", ObservedGeneration: 1234}, {Type: "ClientCredentialsValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "loaded client credentials", ObservedGeneration: 1234},
{Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "discovered issuer configuration", ObservedGeneration: 1234}, {Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "discovered issuer configuration", ObservedGeneration: 1234},
}, },
@ -665,21 +810,26 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana
}}, }},
}, },
{ {
name: "existing valid upstream with trailing slash", name: "has disallowed additionalAuthorizeParams keys",
inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{ inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID},
Spec: v1alpha1.OIDCIdentityProviderSpec{ Spec: v1alpha1.OIDCIdentityProviderSpec{
Issuer: testIssuerURL + "/ends-with-slash/", Issuer: testIssuerURL,
TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64}, TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64},
Client: v1alpha1.OIDCClient{SecretName: testSecretName}, Client: v1alpha1.OIDCClient{SecretName: testSecretName},
AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: testAdditionalScopes}, AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{
Claims: v1alpha1.OIDCClaims{Groups: testGroupsClaim, Username: testUsernameClaim}, AdditionalAuthorizeParameters: []v1alpha1.Parameter{
}, {Name: "response_type", Value: "foo"},
Status: v1alpha1.OIDCIdentityProviderStatus{ {Name: "scope", Value: "foo"},
Phase: "Ready", {Name: "client_id", Value: "foo"},
Conditions: []v1alpha1.Condition{ {Name: "state", Value: "foo"},
{Type: "ClientCredentialsValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "loaded client credentials"}, {Name: "nonce", Value: "foo"},
{Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "discovered issuer configuration"}, {Name: "code_challenge", Value: "foo"},
{Name: "code_challenge_method", Value: "foo"},
{Name: "redirect_uri", Value: "foo"},
{Name: "hd", Value: "foo"},
{Name: "this_one_is_allowed", Value: "foo"},
},
}, },
}, },
}}, }},
@ -688,41 +838,36 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana
Type: "secrets.pinniped.dev/oidc-client", Type: "secrets.pinniped.dev/oidc-client",
Data: testValidSecretData, Data: testValidSecretData,
}}, }},
wantErr: controllerlib.ErrSyntheticRequeue.Error(),
wantLogs: []string{ wantLogs: []string{
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`,
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="discovered issuer configuration" "reason"="Success" "status"="True" "type"="OIDCDiscoverySucceeded"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="discovered issuer configuration" "reason"="Success" "status"="True" "type"="OIDCDiscoverySucceeded"`,
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="the following additionalAuthorizeParameters are not allowed: response_type,scope,client_id,state,nonce,code_challenge,code_challenge_method,redirect_uri,hd" "reason"="DisallowedParameterName" "status"="False" "type"="AdditionalAuthorizeParametersValid"`,
`oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="the following additionalAuthorizeParameters are not allowed: response_type,scope,client_id,state,nonce,code_challenge,code_challenge_method,redirect_uri,hd" "name"="test-name" "namespace"="test-namespace" "reason"="DisallowedParameterName" "type"="AdditionalAuthorizeParametersValid"`,
}, },
wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{ wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{},
&oidctestutil.TestUpstreamOIDCIdentityProvider{
Name: testName,
ClientID: testClientID,
AuthorizationURL: *testIssuerAuthorizeURL,
Scopes: testExpectedScopes,
UsernameClaim: testUsernameClaim,
GroupsClaim: testGroupsClaim,
AllowPasswordGrant: false,
},
},
wantResultingUpstreams: []v1alpha1.OIDCIdentityProvider{{ wantResultingUpstreams: []v1alpha1.OIDCIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID},
Status: v1alpha1.OIDCIdentityProviderStatus{ Status: v1alpha1.OIDCIdentityProviderStatus{
Phase: "Ready", Phase: "Error",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
{Type: "ClientCredentialsValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "loaded client credentials", ObservedGeneration: 1234}, {Type: "AdditionalAuthorizeParametersValid", Status: "False", LastTransitionTime: now, Reason: "DisallowedParameterName",
{Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "discovered issuer configuration", ObservedGeneration: 1234}, Message: "the following additionalAuthorizeParameters are not allowed: " +
"response_type,scope,client_id,state,nonce,code_challenge,code_challenge_method,redirect_uri,hd", ObservedGeneration: 1234},
{Type: "ClientCredentialsValid", Status: "True", LastTransitionTime: now, Reason: "Success", Message: "loaded client credentials", ObservedGeneration: 1234},
{Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: now, Reason: "Success", Message: "discovered issuer configuration", ObservedGeneration: 1234},
}, },
}, },
}}, }},
}, },
{ {
name: "issuer is invalid URL, missing trailing slash", name: "issuer is invalid URL, missing trailing slash when the OIDC discovery endpoint returns the URL with a trailing slash",
inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{ inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName},
Spec: v1alpha1.OIDCIdentityProviderSpec{ Spec: v1alpha1.OIDCIdentityProviderSpec{
Issuer: testIssuerURL + "/ends-with-slash", Issuer: testIssuerURL + "/ends-with-slash", // this does not end with slash when it should, thus this is an error case
TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64}, TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64},
Client: v1alpha1.OIDCClient{SecretName: testSecretName}, Client: v1alpha1.OIDCClient{SecretName: testSecretName},
AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: testAdditionalScopes},
}, },
}}, }},
inputSecrets: []runtime.Object{&corev1.Secret{ inputSecrets: []runtime.Object{&corev1.Secret{
@ -735,6 +880,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana
`oidc-upstream-observer "msg"="failed to perform OIDC discovery" "error"="oidc: issuer did not match the issuer returned by provider, expected \"` + testIssuerURL + `/ends-with-slash\" got \"` + testIssuerURL + `/ends-with-slash/\"" "issuer"="` + testIssuerURL + `/ends-with-slash" "name"="test-name" "namespace"="test-namespace"`, `oidc-upstream-observer "msg"="failed to perform OIDC discovery" "error"="oidc: issuer did not match the issuer returned by provider, expected \"` + testIssuerURL + `/ends-with-slash\" got \"` + testIssuerURL + `/ends-with-slash/\"" "issuer"="` + testIssuerURL + `/ends-with-slash" "name"="test-name" "namespace"="test-namespace"`,
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`,
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="failed to perform OIDC discovery against \"` + testIssuerURL + `/ends-with-slash\":\noidc: issuer did not match the issuer returned by provider, expected \"` + testIssuerURL + `/ends-with-slash\" got \"` + testIssuerURL + `/ends-with-slash/\"" "reason"="Unreachable" "status"="False" "type"="OIDCDiscoverySucceeded"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="failed to perform OIDC discovery against \"` + testIssuerURL + `/ends-with-slash\":\noidc: issuer did not match the issuer returned by provider, expected \"` + testIssuerURL + `/ends-with-slash\" got \"` + testIssuerURL + `/ends-with-slash/\"" "reason"="Unreachable" "status"="False" "type"="OIDCDiscoverySucceeded"`,
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="additionalAuthorizeParameters parameter names are allowed" "reason"="Success" "status"="True" "type"="AdditionalAuthorizeParametersValid"`,
`oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="failed to perform OIDC discovery against \"` + testIssuerURL + `/ends-with-slash\":\noidc: issuer did not match the issuer returned by provider, expected \"` + testIssuerURL + `/ends-with-slash\" got \"` + testIssuerURL + `/ends-with-slash/\"" "name"="test-name" "namespace"="test-namespace" "reason"="Unreachable" "type"="OIDCDiscoverySucceeded"`, `oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="failed to perform OIDC discovery against \"` + testIssuerURL + `/ends-with-slash\":\noidc: issuer did not match the issuer returned by provider, expected \"` + testIssuerURL + `/ends-with-slash\" got \"` + testIssuerURL + `/ends-with-slash/\"" "name"="test-name" "namespace"="test-namespace" "reason"="Unreachable" "type"="OIDCDiscoverySucceeded"`,
}, },
wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{}, wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{},
@ -743,6 +889,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana
Status: v1alpha1.OIDCIdentityProviderStatus{ Status: v1alpha1.OIDCIdentityProviderStatus{
Phase: "Error", Phase: "Error",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
happyAdditionalAuthorizeParametersValidCondition,
{ {
Type: "ClientCredentialsValid", Type: "ClientCredentialsValid",
Status: "True", Status: "True",
@ -767,10 +914,9 @@ oidc: issuer did not match the issuer returned by provider, expected "` + testIs
inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{ inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName},
Spec: v1alpha1.OIDCIdentityProviderSpec{ Spec: v1alpha1.OIDCIdentityProviderSpec{
Issuer: testIssuerURL + "/", Issuer: testIssuerURL + "/",
TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64}, TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64},
Client: v1alpha1.OIDCClient{SecretName: testSecretName}, Client: v1alpha1.OIDCClient{SecretName: testSecretName},
AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: testAdditionalScopes},
}, },
}}, }},
inputSecrets: []runtime.Object{&corev1.Secret{ inputSecrets: []runtime.Object{&corev1.Secret{
@ -783,6 +929,7 @@ oidc: issuer did not match the issuer returned by provider, expected "` + testIs
`oidc-upstream-observer "msg"="failed to perform OIDC discovery" "error"="oidc: issuer did not match the issuer returned by provider, expected \"` + testIssuerURL + `/\" got \"` + testIssuerURL + `\"" "issuer"="` + testIssuerURL + `/" "name"="test-name" "namespace"="test-namespace"`, `oidc-upstream-observer "msg"="failed to perform OIDC discovery" "error"="oidc: issuer did not match the issuer returned by provider, expected \"` + testIssuerURL + `/\" got \"` + testIssuerURL + `\"" "issuer"="` + testIssuerURL + `/" "name"="test-name" "namespace"="test-namespace"`,
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`,
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="failed to perform OIDC discovery against \"` + testIssuerURL + `/\":\noidc: issuer did not match the issuer returned by provider, expected \"` + testIssuerURL + `/\" got \"` + testIssuerURL + `\"" "reason"="Unreachable" "status"="False" "type"="OIDCDiscoverySucceeded"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="failed to perform OIDC discovery against \"` + testIssuerURL + `/\":\noidc: issuer did not match the issuer returned by provider, expected \"` + testIssuerURL + `/\" got \"` + testIssuerURL + `\"" "reason"="Unreachable" "status"="False" "type"="OIDCDiscoverySucceeded"`,
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="additionalAuthorizeParameters parameter names are allowed" "reason"="Success" "status"="True" "type"="AdditionalAuthorizeParametersValid"`,
`oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="failed to perform OIDC discovery against \"` + testIssuerURL + `/\":\noidc: issuer did not match the issuer returned by provider, expected \"` + testIssuerURL + `/\" got \"` + testIssuerURL + `\"" "name"="test-name" "namespace"="test-namespace" "reason"="Unreachable" "type"="OIDCDiscoverySucceeded"`, `oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="failed to perform OIDC discovery against \"` + testIssuerURL + `/\":\noidc: issuer did not match the issuer returned by provider, expected \"` + testIssuerURL + `/\" got \"` + testIssuerURL + `\"" "name"="test-name" "namespace"="test-namespace" "reason"="Unreachable" "type"="OIDCDiscoverySucceeded"`,
}, },
wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{}, wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{},
@ -791,6 +938,7 @@ oidc: issuer did not match the issuer returned by provider, expected "` + testIs
Status: v1alpha1.OIDCIdentityProviderStatus{ Status: v1alpha1.OIDCIdentityProviderStatus{
Phase: "Error", Phase: "Error",
Conditions: []v1alpha1.Condition{ Conditions: []v1alpha1.Condition{
happyAdditionalAuthorizeParametersValidCondition,
{ {
Type: "ClientCredentialsValid", Type: "ClientCredentialsValid",
Status: "True", Status: "True",
@ -860,6 +1008,8 @@ oidc: issuer did not match the issuer returned by provider, expected "` + testIs
require.Equal(t, tt.wantResultingCache[i].GetUsernameClaim(), actualIDP.GetUsernameClaim()) require.Equal(t, tt.wantResultingCache[i].GetUsernameClaim(), actualIDP.GetUsernameClaim())
require.Equal(t, tt.wantResultingCache[i].GetGroupsClaim(), actualIDP.GetGroupsClaim()) require.Equal(t, tt.wantResultingCache[i].GetGroupsClaim(), actualIDP.GetGroupsClaim())
require.Equal(t, tt.wantResultingCache[i].AllowsPasswordGrant(), actualIDP.AllowsPasswordGrant()) require.Equal(t, tt.wantResultingCache[i].AllowsPasswordGrant(), actualIDP.AllowsPasswordGrant())
require.Equal(t, tt.wantResultingCache[i].GetAdditionalAuthcodeParams(), actualIDP.GetAdditionalAuthcodeParams())
require.Equal(t, tt.wantResultingCache[i].GetResourceUID(), actualIDP.GetResourceUID())
require.ElementsMatch(t, tt.wantResultingCache[i].GetScopes(), actualIDP.GetScopes()) require.ElementsMatch(t, tt.wantResultingCache[i].GetScopes(), actualIDP.GetScopes())
// We always want to use the proxy from env on these clients, so although the following assertions // We always want to use the proxy from env on these clients, so although the following assertions

View File

@ -1,4 +1,4 @@
// Copyright 2020 the Pinniped contributors. All Rights Reserved. // Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
package crud package crud
@ -109,6 +109,10 @@ func (s *secretsStorage) validateSecret(secret *corev1.Secret) error {
} }
func (s *secretsStorage) Update(ctx context.Context, signature, resourceVersion string, data JSON) (string, error) { func (s *secretsStorage) Update(ctx context.Context, signature, resourceVersion string, data JSON) (string, error) {
// Note: There may be a small bug here in that toSecret will move the SecretLifetimeAnnotationKey date forward
// instead of keeping the storage resource's original SecretLifetimeAnnotationKey value. However, we only use
// this Update method in one place, and it doesn't matter in that place. Be aware that it might need improvement
// if we start using this Update method in more places.
secret, err := s.toSecret(signature, resourceVersion, data, nil) secret, err := s.toSecret(signature, resourceVersion, data, nil)
if err != nil { if err != nil {
return "", err return "", err

View File

@ -10,7 +10,6 @@ import (
"github.com/ory/fosite" "github.com/ory/fosite"
"github.com/ory/fosite/handler/oauth2" "github.com/ory/fosite/handler/oauth2"
"github.com/ory/fosite/handler/openid"
"k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/errors"
corev1client "k8s.io/client-go/kubernetes/typed/core/v1" corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
@ -18,6 +17,7 @@ import (
"go.pinniped.dev/internal/crud" "go.pinniped.dev/internal/crud"
"go.pinniped.dev/internal/fositestorage" "go.pinniped.dev/internal/fositestorage"
"go.pinniped.dev/internal/oidc/clientregistry" "go.pinniped.dev/internal/oidc/clientregistry"
"go.pinniped.dev/internal/psession"
) )
const ( const (
@ -26,7 +26,9 @@ const (
ErrInvalidAccessTokenRequestVersion = constable.Error("access token request data has wrong version") ErrInvalidAccessTokenRequestVersion = constable.Error("access token request data has wrong version")
ErrInvalidAccessTokenRequestData = constable.Error("access token request data must be present") ErrInvalidAccessTokenRequestData = constable.Error("access token request data must be present")
accessTokenStorageVersion = "1" // Version 1 was the initial release of storage.
// Version 2 is when we switched to storing psession.PinnipedSession inside the fosite request.
accessTokenStorageVersion = "2"
) )
type RevocationStorage interface { type RevocationStorage interface {
@ -110,7 +112,7 @@ func newValidEmptyAccessTokenSession() *session {
return &session{ return &session{
Request: &fosite.Request{ Request: &fosite.Request{
Client: &clientregistry.Client{}, Client: &clientregistry.Client{},
Session: &openid.DefaultSession{}, Session: &psession.PinnipedSession{},
}, },
} }
} }

View File

@ -10,7 +10,6 @@ import (
"time" "time"
"github.com/ory/fosite" "github.com/ory/fosite"
"github.com/ory/fosite/handler/openid"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
@ -22,6 +21,8 @@ import (
coretesting "k8s.io/client-go/testing" coretesting "k8s.io/client-go/testing"
"go.pinniped.dev/internal/oidc/clientregistry" "go.pinniped.dev/internal/oidc/clientregistry"
"go.pinniped.dev/internal/psession"
"go.pinniped.dev/internal/testutil"
) )
const namespace = "test-ns" const namespace = "test-ns"
@ -51,7 +52,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":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"requestedAudience":null,"grantedAudience":null},"version":"1"}`), "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","oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token"}}},"requestedAudience":null,"grantedAudience":null},"version":"2"}`),
"pinniped-storage-version": []byte("1"), "pinniped-storage-version": []byte("1"),
}, },
Type: "storage.pinniped.dev/access-token", Type: "storage.pinniped.dev/access-token",
@ -84,16 +85,10 @@ func TestAccessTokenStorage(t *testing.T) {
RequestObjectSigningAlgorithm: "", RequestObjectSigningAlgorithm: "",
TokenEndpointAuthSigningAlgorithm: "", TokenEndpointAuthSigningAlgorithm: "",
}}, }},
RequestedScope: nil, RequestedScope: nil,
GrantedScope: nil, GrantedScope: nil,
Form: url.Values{"key": []string{"val"}}, Form: url.Values{"key": []string{"val"}},
Session: &openid.DefaultSession{ Session: testutil.NewFakePinnipedSession(),
Claims: nil,
Headers: nil,
ExpiresAt: nil,
Username: "snorlax",
Subject: "panda",
},
RequestedAudience: nil, RequestedAudience: nil,
GrantedAudience: nil, GrantedAudience: nil,
} }
@ -107,6 +102,7 @@ func TestAccessTokenStorage(t *testing.T) {
err = storage.DeleteAccessTokenSession(ctx, "fancy-signature") err = storage.DeleteAccessTokenSession(ctx, "fancy-signature")
require.NoError(t, err) require.NoError(t, err)
testutil.LogActualJSONFromCreateAction(t, client, 0) // makes it easier to update expected values when needed
require.Equal(t, wantActions, client.Actions()) require.Equal(t, wantActions, client.Actions())
} }
@ -125,7 +121,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":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"requestedAudience":null,"grantedAudience":null},"version":"1"}`), "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","oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token"}}},"requestedAudience":null,"grantedAudience":null},"version":"2"}`),
"pinniped-storage-version": []byte("1"), "pinniped-storage-version": []byte("1"),
}, },
Type: "storage.pinniped.dev/access-token", Type: "storage.pinniped.dev/access-token",
@ -151,11 +147,8 @@ func TestAccessTokenStorageRevocation(t *testing.T) {
TokenEndpointAuthMethod: "something", TokenEndpointAuthMethod: "something",
}, },
}, },
Form: url.Values{"key": []string{"val"}}, Form: url.Values{"key": []string{"val"}},
Session: &openid.DefaultSession{ Session: testutil.NewFakePinnipedSession(),
Username: "snorlax",
Subject: "panda",
},
} }
err := storage.CreateAccessTokenSession(ctx, "fancy-signature", request) err := storage.CreateAccessTokenSession(ctx, "fancy-signature", request)
require.NoError(t, err) require.NoError(t, err)
@ -164,6 +157,7 @@ func TestAccessTokenStorageRevocation(t *testing.T) {
err = storage.RevokeAccessToken(ctx, "abcd-1") err = storage.RevokeAccessToken(ctx, "abcd-1")
require.NoError(t, err) require.NoError(t, err)
testutil.LogActualJSONFromCreateAction(t, client, 0) // makes it easier to update expected values when needed
require.Equal(t, wantActions, client.Actions()) require.Equal(t, wantActions, client.Actions())
} }
@ -190,7 +184,7 @@ func TestWrongVersion(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":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"requestedAudience":null,"grantedAudience":null},"version":"not-the-right-version"}`), "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1"},"version":"not-the-right-version"}`),
"pinniped-storage-version": []byte("1"), "pinniped-storage-version": []byte("1"),
}, },
Type: "storage.pinniped.dev/access-token", Type: "storage.pinniped.dev/access-token",
@ -200,7 +194,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 1") 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")
} }
func TestNilSessionRequest(t *testing.T) { func TestNilSessionRequest(t *testing.T) {
@ -218,7 +212,7 @@ func TestNilSessionRequest(t *testing.T) {
}, },
}, },
Data: map[string][]byte{ Data: map[string][]byte{
"pinniped-storage-data": []byte(`{"nonsense-key": "nonsense-value","version":"1"}`), "pinniped-storage-data": []byte(`{"nonsense-key": "nonsense-value","version":"2"}`),
"pinniped-storage-version": []byte("1"), "pinniped-storage-version": []byte("1"),
}, },
Type: "storage.pinniped.dev/access-token", Type: "storage.pinniped.dev/access-token",
@ -246,10 +240,10 @@ func TestCreateWithWrongRequesterDataTypes(t *testing.T) {
Client: &clientregistry.Client{}, Client: &clientregistry.Client{},
} }
err := storage.CreateAccessTokenSession(ctx, "signature-doesnt-matter", request) err := storage.CreateAccessTokenSession(ctx, "signature-doesnt-matter", request)
require.EqualError(t, err, "requester's session must be of type openid.DefaultSession") require.EqualError(t, err, "requester's session must be of type PinnipedSession")
request = &fosite.Request{ request = &fosite.Request{
Session: &openid.DefaultSession{}, Session: &psession.PinnipedSession{},
Client: nil, Client: nil,
} }
err = storage.CreateAccessTokenSession(ctx, "signature-doesnt-matter", request) err = storage.CreateAccessTokenSession(ctx, "signature-doesnt-matter", request)
@ -261,7 +255,7 @@ func TestCreateWithoutRequesterID(t *testing.T) {
request := &fosite.Request{ request := &fosite.Request{
ID: "", // empty ID ID: "", // empty ID
Session: &openid.DefaultSession{}, Session: &psession.PinnipedSession{},
Client: &clientregistry.Client{}, Client: &clientregistry.Client{},
} }
err := storage.CreateAccessTokenSession(ctx, "signature-doesnt-matter", request) err := storage.CreateAccessTokenSession(ctx, "signature-doesnt-matter", request)

View File

@ -11,7 +11,6 @@ import (
"github.com/ory/fosite" "github.com/ory/fosite"
"github.com/ory/fosite/handler/oauth2" "github.com/ory/fosite/handler/oauth2"
"github.com/ory/fosite/handler/openid"
"k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/errors"
corev1client "k8s.io/client-go/kubernetes/typed/core/v1" corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
@ -19,6 +18,7 @@ import (
"go.pinniped.dev/internal/crud" "go.pinniped.dev/internal/crud"
"go.pinniped.dev/internal/fositestorage" "go.pinniped.dev/internal/fositestorage"
"go.pinniped.dev/internal/oidc/clientregistry" "go.pinniped.dev/internal/oidc/clientregistry"
"go.pinniped.dev/internal/psession"
) )
const ( const (
@ -27,7 +27,9 @@ const (
ErrInvalidAuthorizeRequestData = constable.Error("authorization request data must be present") ErrInvalidAuthorizeRequestData = constable.Error("authorization request data must be present")
ErrInvalidAuthorizeRequestVersion = constable.Error("authorization request data has wrong version") ErrInvalidAuthorizeRequestVersion = constable.Error("authorization request data has wrong version")
authorizeCodeStorageVersion = "1" // Version 1 was the initial release of storage.
// Version 2 is when we switched to storing psession.PinnipedSession inside the fosite request.
authorizeCodeStorageVersion = "2"
) )
var _ oauth2.AuthorizeCodeStorage = &authorizeCodeStorage{} var _ oauth2.AuthorizeCodeStorage = &authorizeCodeStorage{}
@ -139,7 +141,7 @@ func NewValidEmptyAuthorizeCodeSession() *AuthorizeCodeSession {
return &AuthorizeCodeSession{ return &AuthorizeCodeSession{
Request: &fosite.Request{ Request: &fosite.Request{
Client: &clientregistry.Client{}, Client: &clientregistry.Client{},
Session: &openid.DefaultSession{}, Session: &psession.PinnipedSession{},
}, },
} }
} }
@ -169,161 +171,172 @@ func (e *errSerializationFailureWithCause) Error() string {
// ExpectedAuthorizeCodeSessionJSONFromFuzzing is used for round tripping tests. // ExpectedAuthorizeCodeSessionJSONFromFuzzing is used for round tripping tests.
// It is exported to allow integration tests to use it. // It is exported to allow integration tests to use it.
const ExpectedAuthorizeCodeSessionJSONFromFuzzing = `{ const ExpectedAuthorizeCodeSessionJSONFromFuzzing = `{
"active": true, "active": true,
"request": { "request": {
"id": "曑x螠Gæ鄋楨", "id": "曑x螠Gæ鄋楨",
"requestedAt": "2082-11-10T18:36:11.627253638Z", "requestedAt": "2082-11-10T18:36:11.627253638Z",
"client": { "client": {
"id": ":NJ¸Ɣ8(黋馛ÄRɴJa¶z", "id": ":NJ¸Ɣ8(黋馛ÄRɴJa¶z",
"client_secret": "UQ==", "client_secret": "UQ==",
"redirect_uris": [ "redirect_uris": [
"ǖ枭kʍ切厦ȳ箦;¥ʊXĝ奨誷傥祩d", "ǖ枭kʍ切厦ȳ箦;¥ʊXĝ奨誷傥祩d",
"zŇZ", "zŇZ",
"優蒼ĊɌț訫DŽǽeʀO2ƚ&N" "優蒼ĊɌț訫DŽǽeʀO2ƚ\u0026N"
], ],
"grant_types": [ "grant_types": [
"唐W6ɻ橩斚薛ɑƐ" "唐W6ɻ橩斚薛ɑƐ"
], ],
"response_types": [ "response_types": [
"w", "w",
"ǔŭe[u@阽羂ŷ-Ĵ½輢OÅ濲喾H" "ǔŭe[u@阽羂ŷ-Ĵ½輢OÅ濲喾H"
], ],
"scopes": [ "scopes": [
"G螩歐湡ƙı唡ɸğƎ&胢輢Ƈĵƚ" "G螩歐湡ƙı唡ɸğƎ\u0026胢輢Ƈĵƚ"
], ],
"audience": [ "audience": [
"ě" "ě"
], ],
"public": false, "public": false,
"jwks_uri": "o*泞羅ʘ Ⱦķ瀊垰7ã\")", "jwks_uri": "o*泞羅ʘ Ⱦķ瀊垰7ã\")",
"jwks": { "jwks": {
"keys": [ "keys": [
{ {
"kty": "OKP", "kty": "OKP",
"crv": "Ed25519", "crv": "Ed25519",
"x": "nK9xgX_iN7u3u_i8YOO7ZRT_WK028Vd_nhtsUu7Eo6E", "x": "nK9xgX_iN7u3u_i8YOO7ZRT_WK028Vd_nhtsUu7Eo6E",
"x5u": { "x5u": {
"Scheme": "", "Scheme": "",
"Opaque": "", "Opaque": "",
"User": null, "User": null,
"Host": "", "Host": "",
"Path": "", "Path": "",
"RawPath": "", "RawPath": "",
"ForceQuery": false, "ForceQuery": false,
"RawQuery": "", "RawQuery": "",
"Fragment": "", "Fragment": "",
"RawFragment": "" "RawFragment": ""
} }
}, },
{ {
"kty": "OKP", "kty": "OKP",
"crv": "Ed25519", "crv": "Ed25519",
"x": "UbbswQgzWhfGCRlwQmMp6fw_HoIoqkIaKT-2XN2fuYU", "x": "UbbswQgzWhfGCRlwQmMp6fw_HoIoqkIaKT-2XN2fuYU",
"x5u": { "x5u": {
"Scheme": "", "Scheme": "",
"Opaque": "", "Opaque": "",
"User": null, "User": null,
"Host": "", "Host": "",
"Path": "", "Path": "",
"RawPath": "", "RawPath": "",
"ForceQuery": false, "ForceQuery": false,
"RawQuery": "", "RawQuery": "",
"Fragment": "", "Fragment": "",
"RawFragment": "" "RawFragment": ""
} }
} }
] ]
}, },
"token_endpoint_auth_method": "ƿʥǟȒ伉<x¹T鼓c吏", "token_endpoint_auth_method": "ƿʥǟȒ伉\u003cx¹T鼓c吏",
"request_uris": [ "request_uris": [
"Ć捘j]=谅ʑɑɮ$Ól4Ȟ", "Ć捘j]=谅ʑɑɮ$Ól4Ȟ",
",Q7钎漡臧n" ",Q7钎漡臧n"
], ],
"request_object_signing_alg": "3@¡廜+v,淬Ʋ4Dʧ呩锏緍场", "request_object_signing_alg": "3@¡廜+v,淬Ʋ4Dʧ呩锏緍场",
"token_endpoint_auth_signing_alg": "(ưƓǴ罷ǹ~]ea胠" "token_endpoint_auth_signing_alg": "(ưƓǴ罷ǹ~]ea胠"
}, },
"scopes": [ "scopes": [
"ĩv絹b垇IŕĩǀŻQ'k頂箨J-a稆", "ĩv絹b垇IŕĩǀŻQ'k頂箨J-a稆",
"啶#昏Q遐*\\髎bŸ" "啶#昏Q遐*\\髎bŸ"
], ],
"grantedScopes": [ "grantedScopes": [
"慂UFƼĮǡ鑻Z" "慂UFƼĮǡ鑻Z"
], ],
"form": { "form": {
"褾攚ŝlĆ厦駳骪l拁乖¡J¿Ƈ妔": [ "褾攚ŝlĆ厦駳骪l拁乖¡J¿Ƈ妔": [
"懧¥ɂĵ~Čyʊ恀c\"NJřðȿ/", "懧¥ɂĵ~Čyʊ恀c\"NJřðȿ/",
"裢?霃谥vƘ:ƿ/濔Aʉ<", "裢?霃谥vƘ:ƿ/濔Aʉ\u003c",
"ȭ$奍囀Dž悷鵱民撲ʓeŘ嬀j¤" "ȭ$奍囀Dž悷鵱民撲ʓeŘ嬀j¤"
], ],
"诞": [ "诞": [
"狲N<Cq罉ZPſĝEK郊©l", "狲N\u003cCq罉ZPſĝEK郊©l",
"餚LJ/ɷȑ潠[ĝU噤'pX ", "餚LJ/ɷȑ潠[ĝU噤'pX ",
"Y妶ǵ!ȁu狍ɶȳsčɦƦ诱" "Y妶ǵ!ȁu狍ɶȳsčɦƦ诱"
] ]
}, },
"session": { "session": {
"Claims": { "fosite": {
"JTI": "攬林Ñz焁糳¿o>Q鱙翑ȲŻ", "Claims": {
"Issuer": "锰劝旣樎Ȱ鍌#ȳńƩŴȭ", "JTI": "u妔隤ʑƍš駎竪0ɔ闏À1",
"Subject": "绝TFNJĆw宵ɚeY48珎²", "Issuer": "麤ã桒嘞\\摗Ǘū稖咾鎅ǸÖ绝TF",
"Audience": [ "Subject": "巽ēđų蓼tùZ蛆鬣a\"ÙǞ0觢Û±",
"éã越|j¦鲶H股ƲLŋZ-{5£踉4" "Audience": [
], "H股ƲL",
"Nonce": "5^驜Ŗ~ů崧軒q腟u尿", "肟v\u0026đehpƧ",
"ExpiresAt": "2065-11-30T13:47:03.613000626Z", "5^驜Ŗ~ů崧軒q腟u尿"
"IssuedAt": "1976-02-22T09:57:20.479850437Z", ],
"RequestedAt": "2016-04-13T04:18:53.648949323Z", "Nonce": "ğ",
"AuthTime": "2098-07-12T04:38:54.034043015Z", "ExpiresAt": "2016-11-22T21:33:58.460521133Z",
"AccessTokenHash": "嫯R", "IssuedAt": "1990-07-25T23:42:07.055978334Z",
"AuthenticationContextClassReference": "¤'+ʣ", "RequestedAt": "1971-01-30T00:23:36.377684025Z",
"AuthenticationMethodsReference": "L&ɽ艄ʬʏ", "AuthTime": "2088-11-09T12:09:14.051840239Z",
"CodeHash": "ğǫ\\aȊ4ț髄Al", "AccessTokenHash": "蕖¤'+ʣȍ瓁U4鞀",
"Extra": { "AuthenticationContextClassReference": "ʏÑęN\u003c_z",
"PƢ曰": { "AuthenticationMethodsReference": "ț髄A",
"ĸŴB岺Ð嫹Sx镯荫ő": [ "CodeHash": "4磔_袻vÓG-壧丵礴鋈k蟵pAɂʅ",
843216989 "Extra": {
], "#\u0026PƢ曰l騌蘙螤\\阏Đ镴Ƥm蔻ǭ\\鿞": 1677215584,
"疂ư墫ɓ": { "Y\u0026鶡萷ɵ啜s攦Ɩïdnǔ": {
"\\BRë_g\"ʎ啴SƇMǃļ": { ",t猟i\u0026\u0026Q@ǤǟǗǪ飘ȱF?Ƈ": {
"ʦ4": false "~劰û橸ɽ銐ƭ?}H": null,
}, "癑勦e骲v0H晦XŘO溪V蔓": {
"鶡萷ɵ啜s攦": null "碼Ǫ": false
} }
}, },
"曓蓳n匟鯘磹*金爃鶴滱ůĮǐ_c3#": 2520197933 "钻煐ɨəÅDČ{Ȩʦ4撎": [
} 3684968178
}, ]
"Headers": { }
"Extra": { }
"寱ĊƑ÷Ƒ螞费Ďğ~劰û橸ɽ銐ƭ?}": {
"ȜʁɁ;Bd謺錳4帳ŅǃĊd": {
"翢砜Fȏl鐉诳DT=3骜": {
"ų厷ɁOƪ穋嶿鳈恱va|载ǰɱ汶C": false
},
"鸨EJ毕懴řĬń戹%c": null
}, },
"室癑勦e骲v0H晦XŘO溪V蔓Ȍ+~ē": [ "Headers": {
954647573 "Extra": {
] "ĊdŘ鸨EJ毕懴řĬń戹": {
}, "诳DT=3骜Ǹ,": {
"麈ƵDǀ\\郂üţ垂": 1572524915 "\u003e": {
"ǰ": false
},
"ɁOƪ穋嶿鳈恱va": null
},
"豑觳翢砜Fȏl": [
927958776
]
},
"埅ȜʁɁ;Bd謺錳4帳Ņ": 388005986
}
},
"ExpiresAt": {
"C]ɲ'=ĸ闒NȢȰ.醋": "1970-07-19T18:03:29.902062193Z",
"fɤȆʪ融ƆuŤn": "2064-01-24T20:34:16.593152073Z",
"爣縗ɦüHêQ仏1ő": "2102-03-17T06:24:40.256846902Z"
},
"Username": "韁臯氃妪婝rȤ\"h丬鎒ơ娻}ɼƟ",
"Subject": "闺髉龳ǽÙ龦O亾EW莛8嘶×"
},
"custom": {
"providerUID": "鵮碡ʯiŬŽ非Ĝ眧Ĭ葜SŦ餧Ĭ倏4",
"providerName": "nŐǛ3",
"providerType": "闣ʬ橳(ý綃ʃʚƟ覣k眐4Ĉt",
"oidc": {
"upstreamRefreshToken": "嵽痊w©Ź榨Q|ôɵt毇妬"
}
} }
},
"ExpiresAt": {
"'=ĸ闒NȢȰ.醋fʜ": "2031-10-18T22:07:34.950803105Z",
"ɦüHêQ仏1őƖ2Ė暮唍ǞʜƢú4": "2049-05-13T15:27:20.968432454Z"
},
"Username": "+韁臯氃妪婝rȤ\"h丬鎒ơ娻}ɼƟȥE",
"Subject": "龳ǽÙ龦O亾EW莛8嘶×姮c恭企"
}, },
"requestedAudience": [ "requestedAudience": [
"邖ɐ5檄¬", "6鉢緋uƴŤȱʀļÂ?墖\u003cƬb獭潜Ʃ饾"
"Ĭ葜SŦ餧Ĭ倏4ĵ嶼仒篻ɥ闣ʬ橳(ý綃"
], ],
"grantedAudience": [ "grantedAudience": [
"ʚƟ覣k眐4ĈtC嵽痊w©Ź榨Q|ô", "|鬌R蜚蠣麹概÷驣7Ʀ澉1æɽ誮rʨ鷞"
"猊Ia瓕巈環_ɑƍ蛊ʚ£:設虝2"
] ]
}, },
"version": "1" "version": "2"
}` }`

View File

@ -18,7 +18,6 @@ import (
fuzz "github.com/google/gofuzz" fuzz "github.com/google/gofuzz"
"github.com/ory/fosite" "github.com/ory/fosite"
"github.com/ory/fosite/handler/oauth2" "github.com/ory/fosite/handler/oauth2"
"github.com/ory/fosite/handler/openid"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"gopkg.in/square/go-jose.v2" "gopkg.in/square/go-jose.v2"
@ -27,6 +26,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/clock" "k8s.io/apimachinery/pkg/util/clock"
"k8s.io/client-go/kubernetes/fake" "k8s.io/client-go/kubernetes/fake"
corev1client "k8s.io/client-go/kubernetes/typed/core/v1" corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
@ -34,6 +34,8 @@ import (
"go.pinniped.dev/internal/fositestorage" "go.pinniped.dev/internal/fositestorage"
"go.pinniped.dev/internal/oidc/clientregistry" "go.pinniped.dev/internal/oidc/clientregistry"
"go.pinniped.dev/internal/psession"
"go.pinniped.dev/internal/testutil"
) )
const namespace = "test-ns" const namespace = "test-ns"
@ -62,7 +64,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":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"requestedAudience":null,"grantedAudience":null},"version":"1"}`), "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","oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token"}}},"requestedAudience":null,"grantedAudience":null},"version":"2"}`),
"pinniped-storage-version": []byte("1"), "pinniped-storage-version": []byte("1"),
}, },
Type: "storage.pinniped.dev/authcode", Type: "storage.pinniped.dev/authcode",
@ -81,7 +83,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":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"requestedAudience":null,"grantedAudience":null},"version":"1"}`), "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","oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token"}}},"requestedAudience":null,"grantedAudience":null},"version":"2"}`),
"pinniped-storage-version": []byte("1"), "pinniped-storage-version": []byte("1"),
}, },
Type: "storage.pinniped.dev/authcode", Type: "storage.pinniped.dev/authcode",
@ -113,16 +115,10 @@ func TestAuthorizationCodeStorage(t *testing.T) {
TokenEndpointAuthSigningAlgorithm: "", TokenEndpointAuthSigningAlgorithm: "",
}, },
}, },
RequestedScope: nil, RequestedScope: nil,
GrantedScope: nil, GrantedScope: nil,
Form: url.Values{"key": []string{"val"}}, Form: url.Values{"key": []string{"val"}},
Session: &openid.DefaultSession{ Session: testutil.NewFakePinnipedSession(),
Claims: nil,
Headers: nil,
ExpiresAt: nil,
Username: "snorlax",
Subject: "panda",
},
RequestedAudience: nil, RequestedAudience: nil,
GrantedAudience: nil, GrantedAudience: nil,
} }
@ -136,6 +132,8 @@ func TestAuthorizationCodeStorage(t *testing.T) {
err = storage.InvalidateAuthorizeCodeSession(ctx, "fancy-signature") err = storage.InvalidateAuthorizeCodeSession(ctx, "fancy-signature")
require.NoError(t, err) require.NoError(t, err)
testutil.LogActualJSONFromCreateAction(t, client, 0) // makes it easier to update expected values when needed
testutil.LogActualJSONFromUpdateAction(t, client, 3) // makes it easier to update expected values when needed
require.Equal(t, wantActions, client.Actions()) require.Equal(t, wantActions, client.Actions())
// Doing a Get on an invalidated session should still return the session, but also return an error. // Doing a Get on an invalidated session should still return the session, but also return an error.
@ -173,7 +171,7 @@ func TestInvalidateWhenConflictOnUpdateHappens(t *testing.T) {
request := &fosite.Request{ request := &fosite.Request{
ID: "some-request-id", ID: "some-request-id",
Client: &clientregistry.Client{}, Client: &clientregistry.Client{},
Session: &openid.DefaultSession{}, Session: testutil.NewFakePinnipedSession(),
} }
err := storage.CreateAuthorizeCodeSession(ctx, "fancy-signature", request) err := storage.CreateAuthorizeCodeSession(ctx, "fancy-signature", request)
require.NoError(t, err) require.NoError(t, err)
@ -193,7 +191,7 @@ func TestWrongVersion(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":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"requestedAudience":null,"grantedAudience":null},"version":"not-the-right-version", "active": true}`), "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1"},"version":"not-the-right-version","active": true}`),
"pinniped-storage-version": []byte("1"), "pinniped-storage-version": []byte("1"),
}, },
Type: "storage.pinniped.dev/authcode", Type: "storage.pinniped.dev/authcode",
@ -203,7 +201,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 1") 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")
} }
func TestNilSessionRequest(t *testing.T) { func TestNilSessionRequest(t *testing.T) {
@ -218,7 +216,7 @@ func TestNilSessionRequest(t *testing.T) {
}, },
}, },
Data: map[string][]byte{ Data: map[string][]byte{
"pinniped-storage-data": []byte(`{"nonsense-key": "nonsense-value", "version":"1", "active": true}`), "pinniped-storage-data": []byte(`{"nonsense-key": "nonsense-value", "version":"2", "active": true}`),
"pinniped-storage-version": []byte("1"), "pinniped-storage-version": []byte("1"),
}, },
Type: "storage.pinniped.dev/authcode", Type: "storage.pinniped.dev/authcode",
@ -246,10 +244,10 @@ func TestCreateWithWrongRequesterDataTypes(t *testing.T) {
Client: &clientregistry.Client{}, Client: &clientregistry.Client{},
} }
err := storage.CreateAuthorizeCodeSession(ctx, "signature-doesnt-matter", request) err := storage.CreateAuthorizeCodeSession(ctx, "signature-doesnt-matter", request)
require.EqualError(t, err, "requester's session must be of type openid.DefaultSession") require.EqualError(t, err, "requester's session must be of type PinnipedSession")
request = &fosite.Request{ request = &fosite.Request{
Session: &openid.DefaultSession{}, Session: &psession.PinnipedSession{},
Client: nil, Client: nil,
} }
err = storage.CreateAuthorizeCodeSession(ctx, "signature-doesnt-matter", request) err = storage.CreateAuthorizeCodeSession(ctx, "signature-doesnt-matter", request)
@ -274,7 +272,7 @@ func TestFuzzAndJSONNewValidEmptyAuthorizeCodeSession(t *testing.T) {
// checked above // checked above
defaultClient := validSession.Request.Client.(*clientregistry.Client) defaultClient := validSession.Request.Client.(*clientregistry.Client)
defaultSession := validSession.Request.Session.(*openid.DefaultSession) pinnipedSession := validSession.Request.Session.(*psession.PinnipedSession)
// makes it easier to use a raw string // makes it easier to use a raw string
replacer := strings.NewReplacer("`", "a") replacer := strings.NewReplacer("`", "a")
@ -297,12 +295,12 @@ func TestFuzzAndJSONNewValidEmptyAuthorizeCodeSession(t *testing.T) {
*fc = defaultClient *fc = defaultClient
}, },
func(fs *fosite.Session, c fuzz.Continue) { func(fs *fosite.Session, c fuzz.Continue) {
c.Fuzz(defaultSession) c.Fuzz(pinnipedSession)
*fs = defaultSession *fs = pinnipedSession
}, },
// these types contain an interface{} that we need to handle // these types contain an interface{} that we need to handle
// this is safe because we explicitly provide the openid.DefaultSession concrete type // this is safe because we explicitly provide the PinnipedSession concrete type
func(value *map[string]interface{}, c fuzz.Continue) { func(value *map[string]interface{}, c fuzz.Continue) {
// cover all the JSON data types just in case // cover all the JSON data types just in case
*value = map[string]interface{}{ *value = map[string]interface{}{
@ -346,6 +344,9 @@ func TestFuzzAndJSONNewValidEmptyAuthorizeCodeSession(t *testing.T) {
func(s *fosite.TokenType, c fuzz.Continue) { func(s *fosite.TokenType, c fuzz.Continue) {
*s = fosite.TokenType(randString(c)) *s = fosite.TokenType(randString(c))
}, },
func(s *types.UID, c fuzz.Continue) {
*s = types.UID(randString(c))
},
// handle string type alias // handle string type alias
func(s *fosite.Arguments, c fuzz.Continue) { func(s *fosite.Arguments, c fuzz.Continue) {
n := c.Intn(3) + 1 // 1 to 3 items n := c.Intn(3) + 1 // 1 to 3 items
@ -382,7 +383,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 = "1" validSession.Version = "2"
validSessionJSONBytes, err := json.MarshalIndent(validSession, "", "\t") validSessionJSONBytes, err := json.MarshalIndent(validSession, "", "\t")
require.NoError(t, err) require.NoError(t, err)

View File

@ -5,16 +5,16 @@ package fositestorage
import ( import (
"github.com/ory/fosite" "github.com/ory/fosite"
"github.com/ory/fosite/handler/openid"
"go.pinniped.dev/internal/constable" "go.pinniped.dev/internal/constable"
"go.pinniped.dev/internal/oidc/clientregistry" "go.pinniped.dev/internal/oidc/clientregistry"
"go.pinniped.dev/internal/psession"
) )
const ( const (
ErrInvalidRequestType = constable.Error("requester must be of type fosite.Request") ErrInvalidRequestType = constable.Error("requester must be of type fosite.Request")
ErrInvalidClientType = constable.Error("requester's client must be of type clientregistry.Client") ErrInvalidClientType = constable.Error("requester's client must be of type clientregistry.Client")
ErrInvalidSessionType = constable.Error("requester's session must be of type openid.DefaultSession") ErrInvalidSessionType = constable.Error("requester's session must be of type PinnipedSession")
StorageRequestIDLabelName = "storage.pinniped.dev/request-id" //nolint:gosec // this is not a credential StorageRequestIDLabelName = "storage.pinniped.dev/request-id" //nolint:gosec // this is not a credential
) )
@ -27,7 +27,7 @@ func ValidateAndExtractAuthorizeRequest(requester fosite.Requester) (*fosite.Req
if !ok2 { if !ok2 {
return nil, ErrInvalidClientType return nil, ErrInvalidClientType
} }
_, ok3 := request.Session.(*openid.DefaultSession) _, ok3 := request.Session.(*psession.PinnipedSession)
if !ok3 { if !ok3 {
return nil, ErrInvalidSessionType return nil, ErrInvalidSessionType
} }

View File

@ -18,6 +18,7 @@ import (
"go.pinniped.dev/internal/crud" "go.pinniped.dev/internal/crud"
"go.pinniped.dev/internal/fositestorage" "go.pinniped.dev/internal/fositestorage"
"go.pinniped.dev/internal/oidc/clientregistry" "go.pinniped.dev/internal/oidc/clientregistry"
"go.pinniped.dev/internal/psession"
) )
const ( const (
@ -27,7 +28,9 @@ const (
ErrInvalidOIDCRequestData = constable.Error("oidc request data must be present") ErrInvalidOIDCRequestData = constable.Error("oidc request data must be present")
ErrMalformedAuthorizationCode = constable.Error("malformed authorization code") ErrMalformedAuthorizationCode = constable.Error("malformed authorization code")
oidcStorageVersion = "1" // Version 1 was the initial release of storage.
// Version 2 is when we switched to storing psession.PinnipedSession inside the fosite request.
oidcStorageVersion = "2"
) )
var _ openid.OpenIDConnectRequestStorage = &openIDConnectRequestStorage{} var _ openid.OpenIDConnectRequestStorage = &openIDConnectRequestStorage{}
@ -112,7 +115,7 @@ func newValidEmptyOIDCSession() *session {
return &session{ return &session{
Request: &fosite.Request{ Request: &fosite.Request{
Client: &clientregistry.Client{}, Client: &clientregistry.Client{},
Session: &openid.DefaultSession{}, Session: &psession.PinnipedSession{},
}, },
} }
} }

View File

@ -22,6 +22,8 @@ import (
coretesting "k8s.io/client-go/testing" coretesting "k8s.io/client-go/testing"
"go.pinniped.dev/internal/oidc/clientregistry" "go.pinniped.dev/internal/oidc/clientregistry"
"go.pinniped.dev/internal/psession"
"go.pinniped.dev/internal/testutil"
) )
const namespace = "test-ns" const namespace = "test-ns"
@ -50,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":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"requestedAudience":null,"grantedAudience":null},"version":"1"}`), "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","oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token"}}},"requestedAudience":null,"grantedAudience":null},"version":"2"}`),
"pinniped-storage-version": []byte("1"), "pinniped-storage-version": []byte("1"),
}, },
Type: "storage.pinniped.dev/oidc", Type: "storage.pinniped.dev/oidc",
@ -84,16 +86,10 @@ func TestOpenIdConnectStorage(t *testing.T) {
TokenEndpointAuthSigningAlgorithm: "", TokenEndpointAuthSigningAlgorithm: "",
}, },
}, },
RequestedScope: nil, RequestedScope: nil,
GrantedScope: nil, GrantedScope: nil,
Form: url.Values{"key": []string{"val"}}, Form: url.Values{"key": []string{"val"}},
Session: &openid.DefaultSession{ Session: testutil.NewFakePinnipedSession(),
Claims: nil,
Headers: nil,
ExpiresAt: nil,
Username: "snorlax",
Subject: "panda",
},
RequestedAudience: nil, RequestedAudience: nil,
GrantedAudience: nil, GrantedAudience: nil,
} }
@ -107,6 +103,7 @@ func TestOpenIdConnectStorage(t *testing.T) {
err = storage.DeleteOpenIDConnectSession(ctx, "fancy-code.fancy-signature") err = storage.DeleteOpenIDConnectSession(ctx, "fancy-code.fancy-signature")
require.NoError(t, err) require.NoError(t, err)
testutil.LogActualJSONFromCreateAction(t, client, 0) // makes it easier to update expected values when needed
require.Equal(t, wantActions, client.Actions()) require.Equal(t, wantActions, client.Actions())
} }
@ -130,7 +127,7 @@ func TestWrongVersion(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":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"requestedAudience":null,"grantedAudience":null},"version":"not-the-right-version"}`), "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1"},"version":"not-the-right-version"}`),
"pinniped-storage-version": []byte("1"), "pinniped-storage-version": []byte("1"),
}, },
Type: "storage.pinniped.dev/oidc", Type: "storage.pinniped.dev/oidc",
@ -140,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 1") require.EqualError(t, err, "oidc request data has wrong version: oidc session for fancy-signature has version not-the-right-version instead of 2")
} }
func TestNilSessionRequest(t *testing.T) { func TestNilSessionRequest(t *testing.T) {
@ -155,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":"1"}`), "pinniped-storage-data": []byte(`{"nonsense-key": "nonsense-value","version":"2"}`),
"pinniped-storage-version": []byte("1"), "pinniped-storage-version": []byte("1"),
}, },
Type: "storage.pinniped.dev/oidc", Type: "storage.pinniped.dev/oidc",
@ -183,10 +180,10 @@ func TestCreateWithWrongRequesterDataTypes(t *testing.T) {
Client: &clientregistry.Client{}, Client: &clientregistry.Client{},
} }
err := storage.CreateOpenIDConnectSession(ctx, "authcode.signature-doesnt-matter", request) err := storage.CreateOpenIDConnectSession(ctx, "authcode.signature-doesnt-matter", request)
require.EqualError(t, err, "requester's session must be of type openid.DefaultSession") require.EqualError(t, err, "requester's session must be of type PinnipedSession")
request = &fosite.Request{ request = &fosite.Request{
Session: &openid.DefaultSession{}, Session: &psession.PinnipedSession{},
Client: nil, Client: nil,
} }
err = storage.CreateOpenIDConnectSession(ctx, "authcode.signature-doesnt-matter", request) err = storage.CreateOpenIDConnectSession(ctx, "authcode.signature-doesnt-matter", request)

View File

@ -9,7 +9,6 @@ import (
"time" "time"
"github.com/ory/fosite" "github.com/ory/fosite"
"github.com/ory/fosite/handler/openid"
"github.com/ory/fosite/handler/pkce" "github.com/ory/fosite/handler/pkce"
"k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/errors"
corev1client "k8s.io/client-go/kubernetes/typed/core/v1" corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
@ -18,6 +17,7 @@ import (
"go.pinniped.dev/internal/crud" "go.pinniped.dev/internal/crud"
"go.pinniped.dev/internal/fositestorage" "go.pinniped.dev/internal/fositestorage"
"go.pinniped.dev/internal/oidc/clientregistry" "go.pinniped.dev/internal/oidc/clientregistry"
"go.pinniped.dev/internal/psession"
) )
const ( const (
@ -26,7 +26,9 @@ const (
ErrInvalidPKCERequestVersion = constable.Error("pkce request data has wrong version") ErrInvalidPKCERequestVersion = constable.Error("pkce request data has wrong version")
ErrInvalidPKCERequestData = constable.Error("pkce request data must be present") ErrInvalidPKCERequestData = constable.Error("pkce request data must be present")
pkceStorageVersion = "1" // Version 1 was the initial release of storage.
// Version 2 is when we switched to storing psession.PinnipedSession inside the fosite request.
pkceStorageVersion = "2"
) )
var _ pkce.PKCERequestStorage = &pkceStorage{} var _ pkce.PKCERequestStorage = &pkceStorage{}
@ -96,7 +98,7 @@ func newValidEmptyPKCESession() *session {
return &session{ return &session{
Request: &fosite.Request{ Request: &fosite.Request{
Client: &clientregistry.Client{}, Client: &clientregistry.Client{},
Session: &openid.DefaultSession{}, Session: &psession.PinnipedSession{},
}, },
} }
} }

View File

@ -10,7 +10,6 @@ import (
"time" "time"
"github.com/ory/fosite" "github.com/ory/fosite"
"github.com/ory/fosite/handler/openid"
"github.com/ory/fosite/handler/pkce" "github.com/ory/fosite/handler/pkce"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -23,6 +22,8 @@ import (
coretesting "k8s.io/client-go/testing" coretesting "k8s.io/client-go/testing"
"go.pinniped.dev/internal/oidc/clientregistry" "go.pinniped.dev/internal/oidc/clientregistry"
"go.pinniped.dev/internal/psession"
"go.pinniped.dev/internal/testutil"
) )
const namespace = "test-ns" const namespace = "test-ns"
@ -51,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":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"requestedAudience":null,"grantedAudience":null},"version":"1"}`), "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","oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token"}}},"requestedAudience":null,"grantedAudience":null},"version":"2"}`),
"pinniped-storage-version": []byte("1"), "pinniped-storage-version": []byte("1"),
}, },
Type: "storage.pinniped.dev/pkce", Type: "storage.pinniped.dev/pkce",
@ -85,16 +86,10 @@ func TestPKCEStorage(t *testing.T) {
TokenEndpointAuthSigningAlgorithm: "", TokenEndpointAuthSigningAlgorithm: "",
}, },
}, },
RequestedScope: nil, RequestedScope: nil,
GrantedScope: nil, GrantedScope: nil,
Form: url.Values{"key": []string{"val"}}, Form: url.Values{"key": []string{"val"}},
Session: &openid.DefaultSession{ Session: testutil.NewFakePinnipedSession(),
Claims: nil,
Headers: nil,
ExpiresAt: nil,
Username: "snorlax",
Subject: "panda",
},
RequestedAudience: nil, RequestedAudience: nil,
GrantedAudience: nil, GrantedAudience: nil,
} }
@ -108,6 +103,7 @@ func TestPKCEStorage(t *testing.T) {
err = storage.DeletePKCERequestSession(ctx, "fancy-signature") err = storage.DeletePKCERequestSession(ctx, "fancy-signature")
require.NoError(t, err) require.NoError(t, err)
testutil.LogActualJSONFromCreateAction(t, client, 0) // makes it easier to update expected values when needed
require.Equal(t, wantActions, client.Actions()) require.Equal(t, wantActions, client.Actions())
} }
@ -134,7 +130,7 @@ func TestWrongVersion(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":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"requestedAudience":null,"grantedAudience":null},"version":"not-the-right-version"}`), "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1"},"version":"not-the-right-version"}`),
"pinniped-storage-version": []byte("1"), "pinniped-storage-version": []byte("1"),
}, },
Type: "storage.pinniped.dev/pkce", Type: "storage.pinniped.dev/pkce",
@ -144,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 1") require.EqualError(t, err, "pkce request data has wrong version: pkce session for fancy-signature has version not-the-right-version instead of 2")
} }
func TestNilSessionRequest(t *testing.T) { func TestNilSessionRequest(t *testing.T) {
@ -162,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":"1"}`), "pinniped-storage-data": []byte(`{"nonsense-key": "nonsense-value","version":"2"}`),
"pinniped-storage-version": []byte("1"), "pinniped-storage-version": []byte("1"),
}, },
Type: "storage.pinniped.dev/pkce", Type: "storage.pinniped.dev/pkce",
@ -190,10 +186,10 @@ func TestCreateWithWrongRequesterDataTypes(t *testing.T) {
Client: &clientregistry.Client{}, Client: &clientregistry.Client{},
} }
err := storage.CreatePKCERequestSession(ctx, "signature-doesnt-matter", request) err := storage.CreatePKCERequestSession(ctx, "signature-doesnt-matter", request)
require.EqualError(t, err, "requester's session must be of type openid.DefaultSession") require.EqualError(t, err, "requester's session must be of type PinnipedSession")
request = &fosite.Request{ request = &fosite.Request{
Session: &openid.DefaultSession{}, Session: &psession.PinnipedSession{},
Client: nil, Client: nil,
} }
err = storage.CreatePKCERequestSession(ctx, "signature-doesnt-matter", request) err = storage.CreatePKCERequestSession(ctx, "signature-doesnt-matter", request)

View File

@ -10,7 +10,6 @@ import (
"github.com/ory/fosite" "github.com/ory/fosite"
"github.com/ory/fosite/handler/oauth2" "github.com/ory/fosite/handler/oauth2"
"github.com/ory/fosite/handler/openid"
"k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/errors"
corev1client "k8s.io/client-go/kubernetes/typed/core/v1" corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
@ -18,6 +17,7 @@ import (
"go.pinniped.dev/internal/crud" "go.pinniped.dev/internal/crud"
"go.pinniped.dev/internal/fositestorage" "go.pinniped.dev/internal/fositestorage"
"go.pinniped.dev/internal/oidc/clientregistry" "go.pinniped.dev/internal/oidc/clientregistry"
"go.pinniped.dev/internal/psession"
) )
const ( const (
@ -26,7 +26,9 @@ const (
ErrInvalidRefreshTokenRequestVersion = constable.Error("refresh token request data has wrong version") ErrInvalidRefreshTokenRequestVersion = constable.Error("refresh token request data has wrong version")
ErrInvalidRefreshTokenRequestData = constable.Error("refresh token request data must be present") ErrInvalidRefreshTokenRequestData = constable.Error("refresh token request data must be present")
refreshTokenStorageVersion = "1" // Version 1 was the initial release of storage.
// Version 2 is when we switched to storing psession.PinnipedSession inside the fosite request.
refreshTokenStorageVersion = "2"
) )
type RevocationStorage interface { type RevocationStorage interface {
@ -110,7 +112,7 @@ func newValidEmptyRefreshTokenSession() *session {
return &session{ return &session{
Request: &fosite.Request{ Request: &fosite.Request{
Client: &clientregistry.Client{}, Client: &clientregistry.Client{},
Session: &openid.DefaultSession{}, Session: &psession.PinnipedSession{},
}, },
} }
} }

View File

@ -10,7 +10,6 @@ import (
"time" "time"
"github.com/ory/fosite" "github.com/ory/fosite"
"github.com/ory/fosite/handler/openid"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
@ -22,6 +21,8 @@ import (
coretesting "k8s.io/client-go/testing" coretesting "k8s.io/client-go/testing"
"go.pinniped.dev/internal/oidc/clientregistry" "go.pinniped.dev/internal/oidc/clientregistry"
"go.pinniped.dev/internal/psession"
"go.pinniped.dev/internal/testutil"
) )
const namespace = "test-ns" const namespace = "test-ns"
@ -50,7 +51,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":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"requestedAudience":null,"grantedAudience":null},"version":"1"}`), "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","oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token"}}},"requestedAudience":null,"grantedAudience":null},"version":"2"}`),
"pinniped-storage-version": []byte("1"), "pinniped-storage-version": []byte("1"),
}, },
Type: "storage.pinniped.dev/refresh-token", Type: "storage.pinniped.dev/refresh-token",
@ -84,16 +85,10 @@ func TestRefreshTokenStorage(t *testing.T) {
TokenEndpointAuthSigningAlgorithm: "", TokenEndpointAuthSigningAlgorithm: "",
}, },
}, },
RequestedScope: nil, RequestedScope: nil,
GrantedScope: nil, GrantedScope: nil,
Form: url.Values{"key": []string{"val"}}, Form: url.Values{"key": []string{"val"}},
Session: &openid.DefaultSession{ Session: testutil.NewFakePinnipedSession(),
Claims: nil,
Headers: nil,
ExpiresAt: nil,
Username: "snorlax",
Subject: "panda",
},
RequestedAudience: nil, RequestedAudience: nil,
GrantedAudience: nil, GrantedAudience: nil,
} }
@ -107,6 +102,7 @@ func TestRefreshTokenStorage(t *testing.T) {
err = storage.DeleteRefreshTokenSession(ctx, "fancy-signature") err = storage.DeleteRefreshTokenSession(ctx, "fancy-signature")
require.NoError(t, err) require.NoError(t, err)
testutil.LogActualJSONFromCreateAction(t, client, 0) // makes it easier to update expected values when needed
require.Equal(t, wantActions, client.Actions()) require.Equal(t, wantActions, client.Actions())
} }
@ -125,7 +121,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":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"requestedAudience":null,"grantedAudience":null},"version":"1"}`), "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","oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token"}}},"requestedAudience":null,"grantedAudience":null},"version":"2"}`),
"pinniped-storage-version": []byte("1"), "pinniped-storage-version": []byte("1"),
}, },
Type: "storage.pinniped.dev/refresh-token", Type: "storage.pinniped.dev/refresh-token",
@ -151,11 +147,8 @@ func TestRefreshTokenStorageRevocation(t *testing.T) {
TokenEndpointAuthMethod: "something", TokenEndpointAuthMethod: "something",
}, },
}, },
Form: url.Values{"key": []string{"val"}}, Form: url.Values{"key": []string{"val"}},
Session: &openid.DefaultSession{ Session: testutil.NewFakePinnipedSession(),
Username: "snorlax",
Subject: "panda",
},
} }
err := storage.CreateRefreshTokenSession(ctx, "fancy-signature", request) err := storage.CreateRefreshTokenSession(ctx, "fancy-signature", request)
require.NoError(t, err) require.NoError(t, err)
@ -164,6 +157,7 @@ func TestRefreshTokenStorageRevocation(t *testing.T) {
err = storage.RevokeRefreshToken(ctx, "abcd-1") err = storage.RevokeRefreshToken(ctx, "abcd-1")
require.NoError(t, err) require.NoError(t, err)
testutil.LogActualJSONFromCreateAction(t, client, 0) // makes it easier to update expected values when needed
require.Equal(t, wantActions, client.Actions()) require.Equal(t, wantActions, client.Actions())
} }
@ -190,7 +184,7 @@ func TestWrongVersion(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":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"requestedAudience":null,"grantedAudience":null},"version":"not-the-right-version"}`), "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1"},"version":"not-the-right-version"}`),
"pinniped-storage-version": []byte("1"), "pinniped-storage-version": []byte("1"),
}, },
Type: "storage.pinniped.dev/refresh-token", Type: "storage.pinniped.dev/refresh-token",
@ -200,7 +194,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 1") 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")
} }
func TestNilSessionRequest(t *testing.T) { func TestNilSessionRequest(t *testing.T) {
@ -218,7 +212,7 @@ func TestNilSessionRequest(t *testing.T) {
}, },
}, },
Data: map[string][]byte{ Data: map[string][]byte{
"pinniped-storage-data": []byte(`{"nonsense-key": "nonsense-value","version":"1"}`), "pinniped-storage-data": []byte(`{"nonsense-key": "nonsense-value","version":"2"}`),
"pinniped-storage-version": []byte("1"), "pinniped-storage-version": []byte("1"),
}, },
Type: "storage.pinniped.dev/refresh-token", Type: "storage.pinniped.dev/refresh-token",
@ -246,10 +240,10 @@ func TestCreateWithWrongRequesterDataTypes(t *testing.T) {
Client: &clientregistry.Client{}, Client: &clientregistry.Client{},
} }
err := storage.CreateRefreshTokenSession(ctx, "signature-doesnt-matter", request) err := storage.CreateRefreshTokenSession(ctx, "signature-doesnt-matter", request)
require.EqualError(t, err, "requester's session must be of type openid.DefaultSession") require.EqualError(t, err, "requester's session must be of type PinnipedSession")
request = &fosite.Request{ request = &fosite.Request{
Session: &openid.DefaultSession{}, Session: &psession.PinnipedSession{},
Client: nil, Client: nil,
} }
err = storage.CreateRefreshTokenSession(ctx, "signature-doesnt-matter", request) err = storage.CreateRefreshTokenSession(ctx, "signature-doesnt-matter", request)
@ -261,7 +255,7 @@ func TestCreateWithoutRequesterID(t *testing.T) {
request := &fosite.Request{ request := &fosite.Request{
ID: "", // empty ID ID: "", // empty ID
Session: &openid.DefaultSession{}, Session: &psession.PinnipedSession{},
Client: &clientregistry.Client{}, Client: &clientregistry.Client{},
} }
err := storage.CreateRefreshTokenSession(ctx, "signature-doesnt-matter", request) err := storage.CreateRefreshTokenSession(ctx, "signature-doesnt-matter", request)

View File

@ -18,6 +18,7 @@ import (
oidctypes "go.pinniped.dev/pkg/oidcclient/oidctypes" oidctypes "go.pinniped.dev/pkg/oidcclient/oidctypes"
pkce "go.pinniped.dev/pkg/oidcclient/pkce" pkce "go.pinniped.dev/pkg/oidcclient/pkce"
oauth2 "golang.org/x/oauth2" oauth2 "golang.org/x/oauth2"
types "k8s.io/apimachinery/pkg/types"
) )
// MockUpstreamOIDCIdentityProviderI is a mock of UpstreamOIDCIdentityProviderI interface. // MockUpstreamOIDCIdentityProviderI is a mock of UpstreamOIDCIdentityProviderI interface.
@ -72,6 +73,20 @@ func (mr *MockUpstreamOIDCIdentityProviderIMockRecorder) ExchangeAuthcodeAndVali
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExchangeAuthcodeAndValidateTokens", reflect.TypeOf((*MockUpstreamOIDCIdentityProviderI)(nil).ExchangeAuthcodeAndValidateTokens), arg0, arg1, arg2, arg3, arg4) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExchangeAuthcodeAndValidateTokens", reflect.TypeOf((*MockUpstreamOIDCIdentityProviderI)(nil).ExchangeAuthcodeAndValidateTokens), arg0, arg1, arg2, arg3, arg4)
} }
// GetAdditionalAuthcodeParams mocks base method.
func (m *MockUpstreamOIDCIdentityProviderI) GetAdditionalAuthcodeParams() map[string]string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAdditionalAuthcodeParams")
ret0, _ := ret[0].(map[string]string)
return ret0
}
// GetAdditionalAuthcodeParams indicates an expected call of GetAdditionalAuthcodeParams.
func (mr *MockUpstreamOIDCIdentityProviderIMockRecorder) GetAdditionalAuthcodeParams() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAdditionalAuthcodeParams", reflect.TypeOf((*MockUpstreamOIDCIdentityProviderI)(nil).GetAdditionalAuthcodeParams))
}
// GetAuthorizationURL mocks base method. // GetAuthorizationURL mocks base method.
func (m *MockUpstreamOIDCIdentityProviderI) GetAuthorizationURL() *url.URL { func (m *MockUpstreamOIDCIdentityProviderI) GetAuthorizationURL() *url.URL {
m.ctrl.T.Helper() m.ctrl.T.Helper()
@ -128,6 +143,20 @@ func (mr *MockUpstreamOIDCIdentityProviderIMockRecorder) GetName() *gomock.Call
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetName", reflect.TypeOf((*MockUpstreamOIDCIdentityProviderI)(nil).GetName)) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetName", reflect.TypeOf((*MockUpstreamOIDCIdentityProviderI)(nil).GetName))
} }
// GetResourceUID mocks base method.
func (m *MockUpstreamOIDCIdentityProviderI) GetResourceUID() types.UID {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetResourceUID")
ret0, _ := ret[0].(types.UID)
return ret0
}
// GetResourceUID indicates an expected call of GetResourceUID.
func (mr *MockUpstreamOIDCIdentityProviderIMockRecorder) GetResourceUID() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetResourceUID", reflect.TypeOf((*MockUpstreamOIDCIdentityProviderI)(nil).GetResourceUID))
}
// GetScopes mocks base method. // GetScopes mocks base method.
func (m *MockUpstreamOIDCIdentityProviderI) GetScopes() []string { func (m *MockUpstreamOIDCIdentityProviderI) GetScopes() []string {
m.ctrl.T.Helper() m.ctrl.T.Helper()
@ -171,6 +200,21 @@ func (mr *MockUpstreamOIDCIdentityProviderIMockRecorder) PasswordCredentialsGran
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PasswordCredentialsGrantAndValidateTokens", reflect.TypeOf((*MockUpstreamOIDCIdentityProviderI)(nil).PasswordCredentialsGrantAndValidateTokens), arg0, arg1, arg2) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PasswordCredentialsGrantAndValidateTokens", reflect.TypeOf((*MockUpstreamOIDCIdentityProviderI)(nil).PasswordCredentialsGrantAndValidateTokens), arg0, arg1, arg2)
} }
// PerformRefresh mocks base method.
func (m *MockUpstreamOIDCIdentityProviderI) PerformRefresh(arg0 context.Context, arg1 string) (*oauth2.Token, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "PerformRefresh", arg0, arg1)
ret0, _ := ret[0].(*oauth2.Token)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// PerformRefresh indicates an expected call of PerformRefresh.
func (mr *MockUpstreamOIDCIdentityProviderIMockRecorder) PerformRefresh(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PerformRefresh", reflect.TypeOf((*MockUpstreamOIDCIdentityProviderI)(nil).PerformRefresh), arg0, arg1)
}
// ValidateToken mocks base method. // ValidateToken mocks base method.
func (m *MockUpstreamOIDCIdentityProviderI) ValidateToken(arg0 context.Context, arg1 *oauth2.Token, arg2 nonce.Nonce) (*oidctypes.Token, error) { func (m *MockUpstreamOIDCIdentityProviderI) ValidateToken(arg0 context.Context, arg1 *oauth2.Token, arg2 nonce.Nonce) (*oidctypes.Token, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()

View File

@ -25,10 +25,16 @@ import (
"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"
"go.pinniped.dev/internal/psession"
"go.pinniped.dev/pkg/oidcclient/nonce" "go.pinniped.dev/pkg/oidcclient/nonce"
"go.pinniped.dev/pkg/oidcclient/pkce" "go.pinniped.dev/pkg/oidcclient/pkce"
) )
const (
promptParamName = "prompt"
promptParamNone = "none"
)
func NewHandler( func NewHandler(
downstreamIssuer string, downstreamIssuer string,
idpLister oidc.UpstreamIdentityProvidersLister, idpLister oidc.UpstreamIdentityProvidersLister,
@ -48,13 +54,13 @@ 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)
} }
oidcUpstream, ldapUpstream, 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)
return err return err
} }
if oidcUpstream != nil { if idpType == psession.ProviderTypeOIDC {
if len(r.Header.Values(supervisoroidc.AuthorizeUsernameHeaderName)) > 0 { if len(r.Header.Values(supervisoroidc.AuthorizeUsernameHeaderName)) > 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)
@ -71,6 +77,7 @@ func NewHandler(
return handleAuthRequestForLDAPUpstream(r, w, return handleAuthRequestForLDAPUpstream(r, w,
oauthHelperWithStorage, oauthHelperWithStorage,
ldapUpstream, ldapUpstream,
idpType,
) )
})) }))
} }
@ -80,6 +87,7 @@ func handleAuthRequestForLDAPUpstream(
w http.ResponseWriter, w http.ResponseWriter,
oauthHelper fosite.OAuth2Provider, oauthHelper fosite.OAuth2Provider,
ldapUpstream provider.UpstreamLDAPIdentityProviderI, ldapUpstream provider.UpstreamLDAPIdentityProviderI,
idpType psession.ProviderType,
) error { ) error {
authorizeRequester, created := newAuthorizeRequest(r, w, oauthHelper) authorizeRequester, created := newAuthorizeRequest(r, w, oauthHelper)
if !created { if !created {
@ -105,7 +113,14 @@ func handleAuthRequestForLDAPUpstream(
username = authenticateResponse.User.GetName() username = authenticateResponse.User.GetName()
groups := authenticateResponse.User.GetGroups() groups := authenticateResponse.User.GetGroups()
return makeDownstreamSessionAndReturnAuthcodeRedirect(r, w, oauthHelper, authorizeRequester, subject, username, groups) customSessionData := &psession.CustomSessionData{
ProviderUID: ldapUpstream.GetResourceUID(),
ProviderName: ldapUpstream.GetName(),
ProviderType: idpType,
}
return makeDownstreamSessionAndReturnAuthcodeRedirect(r, w,
oauthHelper, authorizeRequester, subject, username, groups, customSessionData)
} }
func handleAuthRequestForOIDCUpstreamPasswordGrant( func handleAuthRequestForOIDCUpstreamPasswordGrant(
@ -144,6 +159,16 @@ func handleAuthRequestForOIDCUpstreamPasswordGrant(
fosite.ErrAccessDenied.WithDebug(err.Error())) // WithDebug hides the error from the client fosite.ErrAccessDenied.WithDebug(err.Error())) // WithDebug hides the error from the client
} }
if token.RefreshToken == nil || token.RefreshToken.Token == "" {
plog.Warning("refresh token not returned by upstream provider during password grant, "+
"please check configuration of OIDCIdentityProvider and the client in the upstream provider's API/UI",
"upstreamName", oidcUpstream.GetName(),
"scopes", oidcUpstream.GetScopes())
return writeAuthorizeError(w, oauthHelper, authorizeRequester,
fosite.ErrAccessDenied.WithHint(
"Refresh token not returned by upstream provider during password grant."))
}
subject, username, groups, err := downstreamsession.GetDownstreamIdentityFromUpstreamIDToken(oidcUpstream, token.IDToken.Claims) subject, username, groups, err := downstreamsession.GetDownstreamIdentityFromUpstreamIDToken(oidcUpstream, token.IDToken.Claims)
if err != nil { if err != nil {
// Return a user-friendly error for this case which is entirely within our control. // Return a user-friendly error for this case which is entirely within our control.
@ -152,7 +177,15 @@ func handleAuthRequestForOIDCUpstreamPasswordGrant(
) )
} }
return makeDownstreamSessionAndReturnAuthcodeRedirect(r, w, oauthHelper, authorizeRequester, subject, username, groups) customSessionData := &psession.CustomSessionData{
ProviderUID: oidcUpstream.GetResourceUID(),
ProviderName: oidcUpstream.GetName(),
ProviderType: psession.ProviderTypeOIDC,
OIDC: &psession.OIDCSessionData{
UpstreamRefreshToken: token.RefreshToken.Token,
},
}
return makeDownstreamSessionAndReturnAuthcodeRedirect(r, w, oauthHelper, authorizeRequester, subject, username, groups, customSessionData)
} }
func handleAuthRequestForOIDCUpstreamAuthcodeGrant( func handleAuthRequestForOIDCUpstreamAuthcodeGrant(
@ -173,12 +206,14 @@ func handleAuthRequestForOIDCUpstreamAuthcodeGrant(
} }
now := time.Now() now := time.Now()
_, err := oauthHelper.NewAuthorizeResponse(r.Context(), authorizeRequester, &openid.DefaultSession{ _, err := oauthHelper.NewAuthorizeResponse(r.Context(), authorizeRequester, &psession.PinnipedSession{
Claims: &jwt.IDTokenClaims{ Fosite: &openid.DefaultSession{
// Temporary claim values to allow `NewAuthorizeResponse` to perform other OIDC validations. Claims: &jwt.IDTokenClaims{
Subject: "none", // Temporary claim values to allow `NewAuthorizeResponse` to perform other OIDC validations.
AuthTime: now, Subject: "none",
RequestedAt: now, AuthTime: now,
RequestedAt: now,
},
}, },
}) })
if err != nil { if err != nil {
@ -217,6 +252,21 @@ func handleAuthRequestForOIDCUpstreamAuthcodeGrant(
return err return err
} }
authCodeOptions := []oauth2.AuthCodeOption{
nonceValue.Param(),
pkceValue.Challenge(),
pkceValue.Method(),
}
promptParam := r.Form.Get(promptParamName)
if promptParam == promptParamNone && oidc.ScopeWasRequested(authorizeRequester, coreosoidc.ScopeOpenID) {
return writeAuthorizeError(w, oauthHelper, authorizeRequester, fosite.ErrLoginRequired)
}
for key, val := range oidcUpstream.GetAdditionalAuthcodeParams() {
authCodeOptions = append(authCodeOptions, oauth2.SetAuthURLParam(key, val))
}
if csrfFromCookie == "" { if csrfFromCookie == "" {
// We did not receive an incoming CSRF cookie, so write a new one. // We did not receive an incoming CSRF cookie, so write a new one.
err := addCSRFSetCookieHeader(w, csrfValue, cookieCodec) err := addCSRFSetCookieHeader(w, csrfValue, cookieCodec)
@ -226,18 +276,6 @@ func handleAuthRequestForOIDCUpstreamAuthcodeGrant(
} }
} }
authCodeOptions := []oauth2.AuthCodeOption{
oauth2.AccessTypeOffline,
nonceValue.Param(),
pkceValue.Challenge(),
pkceValue.Method(),
}
promptParam := r.Form.Get("prompt")
if promptParam != "" && oidc.ScopeWasRequested(authorizeRequester, coreosoidc.ScopeOpenID) {
authCodeOptions = append(authCodeOptions, oauth2.SetAuthURLParam("prompt", promptParam))
}
http.Redirect(w, r, http.Redirect(w, r,
upstreamOAuthConfig.AuthCodeURL( upstreamOAuthConfig.AuthCodeURL(
encodedStateParamValue, encodedStateParamValue,
@ -275,8 +313,9 @@ func makeDownstreamSessionAndReturnAuthcodeRedirect(
subject string, subject string,
username string, username string,
groups []string, groups []string,
customSessionData *psession.CustomSessionData,
) error { ) error {
openIDSession := downstreamsession.MakeDownstreamSession(subject, username, groups) openIDSession := downstreamsession.MakeDownstreamSession(subject, username, groups, customSessionData)
authorizeResponder, err := oauthHelper.NewAuthorizeResponse(r.Context(), authorizeRequester, openIDSession) authorizeResponder, err := oauthHelper.NewAuthorizeResponse(r.Context(), authorizeRequester, openIDSession)
if err != nil { if err != nil {
@ -335,13 +374,13 @@ func readCSRFCookie(r *http.Request, codec oidc.Decoder) csrftoken.CSRFToken {
} }
// Select either an OIDC, an LDAP or an AD IDP, or return an error. // Select either an OIDC, an LDAP or an AD IDP, or return an error.
func chooseUpstreamIDP(idpLister oidc.UpstreamIdentityProvidersLister) (provider.UpstreamOIDCIdentityProviderI, provider.UpstreamLDAPIdentityProviderI, error) { func chooseUpstreamIDP(idpLister oidc.UpstreamIdentityProvidersLister) (provider.UpstreamOIDCIdentityProviderI, provider.UpstreamLDAPIdentityProviderI, psession.ProviderType, error) {
oidcUpstreams := idpLister.GetOIDCIdentityProviders() oidcUpstreams := idpLister.GetOIDCIdentityProviders()
ldapUpstreams := idpLister.GetLDAPIdentityProviders() ldapUpstreams := idpLister.GetLDAPIdentityProviders()
adUpstreams := idpLister.GetActiveDirectoryIdentityProviders() adUpstreams := idpLister.GetActiveDirectoryIdentityProviders()
switch { switch {
case len(oidcUpstreams)+len(ldapUpstreams)+len(adUpstreams) == 0: case len(oidcUpstreams)+len(ldapUpstreams)+len(adUpstreams) == 0:
return nil, nil, httperr.New( return nil, nil, "", httperr.New(
http.StatusUnprocessableEntity, http.StatusUnprocessableEntity,
"No upstream providers are configured", "No upstream providers are configured",
) )
@ -357,16 +396,16 @@ func chooseUpstreamIDP(idpLister oidc.UpstreamIdentityProvidersLister) (provider
upstreamIDPNames = append(upstreamIDPNames, idp.GetName()) upstreamIDPNames = append(upstreamIDPNames, idp.GetName())
} }
plog.Warning("Too many upstream providers are configured (found: %s)", upstreamIDPNames) plog.Warning("Too many upstream providers are configured (found: %s)", upstreamIDPNames)
return nil, nil, httperr.New( return nil, nil, "", httperr.New(
http.StatusUnprocessableEntity, http.StatusUnprocessableEntity,
"Too many upstream providers are configured (support for multiple upstreams is not yet implemented)", "Too many upstream providers are configured (support for multiple upstreams is not yet implemented)",
) )
case len(oidcUpstreams) == 1: case len(oidcUpstreams) == 1:
return oidcUpstreams[0], nil, nil return oidcUpstreams[0], nil, psession.ProviderTypeOIDC, nil
case len(adUpstreams) == 1: case len(adUpstreams) == 1:
return nil, adUpstreams[0], nil return nil, adUpstreams[0], psession.ProviderTypeActiveDirectory, nil
default: default:
return nil, ldapUpstreams[0], nil return nil, ldapUpstreams[0], psession.ProviderTypeLDAP, nil
} }
} }

View File

@ -30,6 +30,7 @@ import (
"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/provider" "go.pinniped.dev/internal/oidc/provider"
"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"
"go.pinniped.dev/pkg/oidcclient/nonce" "go.pinniped.dev/pkg/oidcclient/nonce"
@ -38,16 +39,23 @@ import (
func TestAuthorizationEndpoint(t *testing.T) { func TestAuthorizationEndpoint(t *testing.T) {
const ( const (
oidcUpstreamName = "some-oidc-idp" oidcUpstreamName = "some-oidc-idp"
oidcPasswordGrantUpstreamName = "some-password-granting-oidc-idp" oidcUpstreamResourceUID = "oidc-resource-uid"
oidcPasswordGrantUpstreamName = "some-password-granting-oidc-idp"
oidcPasswordGrantUpstreamResourceUID = "some-password-granting-resource-uid"
ldapUpstreamName = "some-ldap-idp"
ldapUpstreamResourceUID = "ldap-resource-uid"
activeDirectoryUpstreamName = "some-active-directory-idp"
activeDirectoryUpstreamResourceUID = "active-directory-resource-uid"
oidcUpstreamIssuer = "https://my-upstream-issuer.com" oidcUpstreamIssuer = "https://my-upstream-issuer.com"
oidcUpstreamSubject = "abc123-some guid" // has a space character which should get escaped in URL oidcUpstreamSubject = "abc123-some guid" // has a space character which should get escaped in URL
oidcUpstreamSubjectQueryEscaped = "abc123-some+guid" oidcUpstreamSubjectQueryEscaped = "abc123-some+guid"
oidcUpstreamUsername = "test-oidc-pinniped-username" oidcUpstreamUsername = "test-oidc-pinniped-username"
oidcUpstreamPassword = "test-oidc-pinniped-password" //nolint: gosec oidcUpstreamPassword = "test-oidc-pinniped-password" //nolint: gosec
oidcUpstreamUsernameClaim = "the-user-claim" oidcUpstreamUsernameClaim = "the-user-claim"
oidcUpstreamGroupsClaim = "the-groups-claim" oidcUpstreamGroupsClaim = "the-groups-claim"
oidcPasswordGrantUpstreamRefreshToken = "some-opaque-token" //nolint: gosec
downstreamIssuer = "https://my-downstream-issuer.com/some-path" downstreamIssuer = "https://my-downstream-issuer.com/some-path"
downstreamRedirectURI = "http://127.0.0.1/callback" downstreamRedirectURI = "http://127.0.0.1/callback"
@ -146,6 +154,12 @@ func TestAuthorizationEndpoint(t *testing.T) {
"state": happyState, "state": happyState,
} }
fositeAccessDeniedWithMissingRefreshTokenErrorQuery = map[string]string{
"error": "access_denied",
"error_description": "The resource owner or authorization server denied the request. Refresh token not returned by upstream provider during password grant.",
"state": happyState,
}
fositeAccessDeniedWithPasswordGrantDisallowedHintErrorQuery = map[string]string{ fositeAccessDeniedWithPasswordGrantDisallowedHintErrorQuery = map[string]string{
"error": "access_denied", "error": "access_denied",
"error_description": "The resource owner or authorization server denied the request. Resource owner password credentials grant is not allowed for this upstream provider according to its configuration.", "error_description": "The resource owner or authorization server denied the request. Resource owner password credentials grant is not allowed for this upstream provider according to its configuration.",
@ -181,6 +195,12 @@ func TestAuthorizationEndpoint(t *testing.T) {
"error_description": "The resource owner or authorization server denied the request. Reason: required claim in upstream ID token has invalid format.", "error_description": "The resource owner or authorization server denied the request. Reason: required claim in upstream ID token has invalid format.",
"state": happyState, "state": happyState,
} }
fositeLoginRequiredErrorQuery = map[string]string{
"error": "login_required",
"error_description": "The Authorization Server requires End-User authentication.",
"state": happyState,
}
) )
hmacSecretFunc := func() []byte { return []byte("some secret - must have at least 32 bytes") } hmacSecretFunc := func() []byte { return []byte("some secret - must have at least 32 bytes") }
@ -202,20 +222,22 @@ func TestAuthorizationEndpoint(t *testing.T) {
upstreamAuthURL, err := url.Parse("https://some-upstream-idp:8443/auth") upstreamAuthURL, err := url.Parse("https://some-upstream-idp:8443/auth")
require.NoError(t, err) require.NoError(t, err)
upstreamOIDCIdentityProvider := func() *oidctestutil.TestUpstreamOIDCIdentityProvider { upstreamOIDCIdentityProviderBuilder := func() *oidctestutil.TestUpstreamOIDCIdentityProviderBuilder {
return oidctestutil.NewTestUpstreamOIDCIdentityProviderBuilder(). return oidctestutil.NewTestUpstreamOIDCIdentityProviderBuilder().
WithName(oidcUpstreamName). WithName(oidcUpstreamName).
WithResourceUID(oidcUpstreamResourceUID).
WithClientID("some-client-id"). WithClientID("some-client-id").
WithAuthorizationURL(*upstreamAuthURL). WithAuthorizationURL(*upstreamAuthURL).
WithScopes([]string{"scope1", "scope2"}). // the scopes to request when starting the upstream authorization flow WithScopes([]string{"scope1", "scope2"}). // the scopes to request when starting the upstream authorization flow
WithAllowPasswordGrant(false). WithAllowPasswordGrant(false).
WithPasswordGrantError(errors.New("should not have used password grant on this instance")). WithAdditionalAuthcodeParams(map[string]string{}).
Build() WithPasswordGrantError(errors.New("should not have used password grant on this instance"))
} }
passwordGrantUpstreamOIDCIdentityProviderBuilder := func() *oidctestutil.TestUpstreamOIDCIdentityProviderBuilder { passwordGrantUpstreamOIDCIdentityProviderBuilder := func() *oidctestutil.TestUpstreamOIDCIdentityProviderBuilder {
return oidctestutil.NewTestUpstreamOIDCIdentityProviderBuilder(). return oidctestutil.NewTestUpstreamOIDCIdentityProviderBuilder().
WithName(oidcPasswordGrantUpstreamName). WithName(oidcPasswordGrantUpstreamName).
WithResourceUID(oidcPasswordGrantUpstreamResourceUID).
WithClientID("some-client-id"). WithClientID("some-client-id").
WithAuthorizationURL(*upstreamAuthURL). WithAuthorizationURL(*upstreamAuthURL).
WithScopes([]string{"scope1", "scope2"}). // the scopes to request when starting the upstream authorization flow WithScopes([]string{"scope1", "scope2"}). // the scopes to request when starting the upstream authorization flow
@ -228,6 +250,8 @@ func TestAuthorizationEndpoint(t *testing.T) {
WithIDTokenClaim(oidcUpstreamGroupsClaim, oidcUpstreamGroupMembership). WithIDTokenClaim(oidcUpstreamGroupsClaim, oidcUpstreamGroupMembership).
WithIDTokenClaim("other-claim", "should be ignored"). WithIDTokenClaim("other-claim", "should be ignored").
WithAllowPasswordGrant(true). WithAllowPasswordGrant(true).
WithRefreshToken(oidcPasswordGrantUpstreamRefreshToken).
WithAdditionalAuthcodeParams(map[string]string{"should-be-ignored": "doesn't apply to password grant"}).
WithUpstreamAuthcodeExchangeError(errors.New("should not have tried to exchange upstream authcode on this instance")) WithUpstreamAuthcodeExchangeError(errors.New("should not have tried to exchange upstream authcode on this instance"))
} }
@ -248,28 +272,39 @@ func TestAuthorizationEndpoint(t *testing.T) {
parsedUpstreamLDAPURL, err := url.Parse(upstreamLDAPURL) parsedUpstreamLDAPURL, err := url.Parse(upstreamLDAPURL)
require.NoError(t, err) require.NoError(t, err)
ldapAuthenticateFunc := func(ctx context.Context, username, password string) (*authenticator.Response, bool, error) {
if username == "" || password == "" {
return nil, false, fmt.Errorf("should not have passed empty username or password to the authenticator")
}
if username == happyLDAPUsername && password == happyLDAPPassword {
return &authenticator.Response{
User: &user.DefaultInfo{
Name: happyLDAPUsernameFromAuthenticator,
UID: happyLDAPUID,
Groups: happyLDAPGroups,
},
}, true, nil
}
return nil, false, nil
}
upstreamLDAPIdentityProvider := oidctestutil.TestUpstreamLDAPIdentityProvider{ upstreamLDAPIdentityProvider := oidctestutil.TestUpstreamLDAPIdentityProvider{
Name: "some-ldap-idp", Name: ldapUpstreamName,
URL: parsedUpstreamLDAPURL, ResourceUID: ldapUpstreamResourceUID,
AuthenticateFunc: func(ctx context.Context, username, password string) (*authenticator.Response, bool, error) { URL: parsedUpstreamLDAPURL,
if username == "" || password == "" { AuthenticateFunc: ldapAuthenticateFunc,
return nil, false, fmt.Errorf("should not have passed empty username or password to the authenticator") }
}
if username == happyLDAPUsername && password == happyLDAPPassword { upstreamActiveDirectoryIdentityProvider := oidctestutil.TestUpstreamLDAPIdentityProvider{
return &authenticator.Response{ Name: activeDirectoryUpstreamName,
User: &user.DefaultInfo{ ResourceUID: activeDirectoryUpstreamResourceUID,
Name: happyLDAPUsernameFromAuthenticator, URL: parsedUpstreamLDAPURL,
UID: happyLDAPUID, AuthenticateFunc: ldapAuthenticateFunc,
Groups: happyLDAPGroups,
},
}, true, nil
}
return nil, false, nil
},
} }
erroringUpstreamLDAPIdentityProvider := oidctestutil.TestUpstreamLDAPIdentityProvider{ erroringUpstreamLDAPIdentityProvider := oidctestutil.TestUpstreamLDAPIdentityProvider{
Name: "some-ldap-idp", Name: ldapUpstreamName,
ResourceUID: ldapUpstreamResourceUID,
AuthenticateFunc: func(ctx context.Context, username, password string) (*authenticator.Response, bool, error) { AuthenticateFunc: func(ctx context.Context, username, password string) (*authenticator.Response, bool, error) {
return nil, false, fmt.Errorf("some ldap upstream auth error") return nil, false, fmt.Errorf("some ldap upstream auth error")
}, },
@ -381,10 +416,9 @@ func TestAuthorizationEndpoint(t *testing.T) {
return encoded return encoded
} }
expectedRedirectLocationForUpstreamOIDC := func(expectedUpstreamState string, expectedPrompt string) string { expectedRedirectLocationForUpstreamOIDC := func(expectedUpstreamState string, expectedAdditionalParams map[string]string) string {
query := map[string]string{ query := map[string]string{
"response_type": "code", "response_type": "code",
"access_type": "offline",
"scope": "scope1 scope2", "scope": "scope1 scope2",
"client_id": "some-client-id", "client_id": "some-client-id",
"state": expectedUpstreamState, "state": expectedUpstreamState,
@ -393,12 +427,35 @@ func TestAuthorizationEndpoint(t *testing.T) {
"code_challenge_method": downstreamPKCEChallengeMethod, "code_challenge_method": downstreamPKCEChallengeMethod,
"redirect_uri": downstreamIssuer + "/callback", "redirect_uri": downstreamIssuer + "/callback",
} }
if expectedPrompt != "" { for key, val := range expectedAdditionalParams {
query["prompt"] = expectedPrompt query[key] = val
} }
return urlWithQuery(upstreamAuthURL.String(), query) return urlWithQuery(upstreamAuthURL.String(), query)
} }
expectedHappyActiveDirectoryUpstreamCustomSession := &psession.CustomSessionData{
ProviderUID: activeDirectoryUpstreamResourceUID,
ProviderName: activeDirectoryUpstreamName,
ProviderType: psession.ProviderTypeActiveDirectory,
OIDC: nil,
}
expectedHappyLDAPUpstreamCustomSession := &psession.CustomSessionData{
ProviderUID: ldapUpstreamResourceUID,
ProviderName: ldapUpstreamName,
ProviderType: psession.ProviderTypeLDAP,
OIDC: nil,
}
expectedHappyOIDCPasswordGrantCustomSession := &psession.CustomSessionData{
ProviderUID: oidcPasswordGrantUpstreamResourceUID,
ProviderName: oidcPasswordGrantUpstreamName,
ProviderType: psession.ProviderTypeOIDC,
OIDC: &psession.OIDCSessionData{
UpstreamRefreshToken: oidcPasswordGrantUpstreamRefreshToken,
},
}
// 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&state=` + happyState happyAuthcodeDownstreamRedirectLocationRegexp := downstreamRedirectURI + `\?code=([^&]+)&scope=openid&state=` + happyState
@ -446,11 +503,12 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantDownstreamNonce string wantDownstreamNonce string
wantUnnecessaryStoredRecords int wantUnnecessaryStoredRecords int
wantPasswordGrantCall *expectedPasswordGrant wantPasswordGrantCall *expectedPasswordGrant
wantDownstreamCustomSessionData *psession.CustomSessionData
} }
tests := []testCase{ tests := []testCase{
{ {
name: "OIDC upstream browser flow happy path using GET without a CSRF cookie", name: "OIDC upstream browser flow happy path using GET without a CSRF cookie",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
generateCSRF: happyCSRFGenerator, generateCSRF: happyCSRFGenerator,
generatePKCE: happyPKCEGenerator, generatePKCE: happyPKCEGenerator,
generateNonce: happyNonceGenerator, generateNonce: happyNonceGenerator,
@ -461,7 +519,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
wantContentType: htmlContentType, wantContentType: htmlContentType,
wantCSRFValueInCookieHeader: happyCSRF, wantCSRFValueInCookieHeader: happyCSRF,
wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(nil, "", ""), ""), wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(nil, "", ""), nil),
wantUpstreamStateParamInLocationHeader: true, wantUpstreamStateParamInLocationHeader: true,
wantBodyStringWithLocationInHref: true, wantBodyStringWithLocationInHref: true,
}, },
@ -485,6 +543,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: expectedHappyOIDCPasswordGrantCustomSession,
}, },
{ {
name: "LDAP upstream happy path using GET", name: "LDAP upstream happy path using GET",
@ -505,10 +564,11 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: expectedHappyLDAPUpstreamCustomSession,
}, },
{ {
name: "ActiveDirectory upstream happy path using GET", name: "ActiveDirectory upstream happy path using GET",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamLDAPIdentityProvider), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider),
method: http.MethodGet, method: http.MethodGet,
path: happyGetRequestPath, path: happyGetRequestPath,
customUsernameHeader: pointer.StringPtr(happyLDAPUsername), customUsernameHeader: pointer.StringPtr(happyLDAPUsername),
@ -525,10 +585,11 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: expectedHappyActiveDirectoryUpstreamCustomSession,
}, },
{ {
name: "OIDC upstream browser flow happy path using GET with a CSRF cookie", name: "OIDC upstream browser flow happy path using GET with a CSRF cookie",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
generateCSRF: happyCSRFGenerator, generateCSRF: happyCSRFGenerator,
generatePKCE: happyPKCEGenerator, generatePKCE: happyPKCEGenerator,
generateNonce: happyNonceGenerator, generateNonce: happyNonceGenerator,
@ -539,13 +600,13 @@ func TestAuthorizationEndpoint(t *testing.T) {
csrfCookie: "__Host-pinniped-csrf=" + encodedIncomingCookieCSRFValue + " ", csrfCookie: "__Host-pinniped-csrf=" + encodedIncomingCookieCSRFValue + " ",
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
wantContentType: htmlContentType, wantContentType: htmlContentType,
wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(nil, incomingCookieCSRFValue, ""), ""), wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(nil, incomingCookieCSRFValue, ""), nil),
wantUpstreamStateParamInLocationHeader: true, wantUpstreamStateParamInLocationHeader: true,
wantBodyStringWithLocationInHref: true, wantBodyStringWithLocationInHref: true,
}, },
{ {
name: "OIDC upstream browser flow happy path using POST", name: "OIDC upstream browser flow happy path using POST",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
generateCSRF: happyCSRFGenerator, generateCSRF: happyCSRFGenerator,
generatePKCE: happyPKCEGenerator, generatePKCE: happyPKCEGenerator,
generateNonce: happyNonceGenerator, generateNonce: happyNonceGenerator,
@ -559,7 +620,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantContentType: "", wantContentType: "",
wantBodyString: "", wantBodyString: "",
wantCSRFValueInCookieHeader: happyCSRF, wantCSRFValueInCookieHeader: happyCSRF,
wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(nil, "", ""), ""), wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(nil, "", ""), nil),
wantUpstreamStateParamInLocationHeader: true, wantUpstreamStateParamInLocationHeader: true,
}, },
{ {
@ -584,6 +645,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: expectedHappyOIDCPasswordGrantCustomSession,
}, },
{ {
name: "LDAP upstream happy path using POST", name: "LDAP upstream happy path using POST",
@ -606,10 +668,11 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: expectedHappyLDAPUpstreamCustomSession,
}, },
{ {
name: "Active Directory upstream happy path using POST", name: "Active Directory upstream happy path using POST",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamLDAPIdentityProvider), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider),
method: http.MethodPost, method: http.MethodPost,
path: "/some/path", path: "/some/path",
contentType: "application/x-www-form-urlencoded", contentType: "application/x-www-form-urlencoded",
@ -628,10 +691,11 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: expectedHappyActiveDirectoryUpstreamCustomSession,
}, },
{ {
name: "OIDC upstream browser flow happy path with prompt param login passed through to redirect uri", name: "OIDC upstream browser flow happy path with prompt param other than none that gets ignored",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
generateCSRF: happyCSRFGenerator, generateCSRF: happyCSRFGenerator,
generatePKCE: happyPKCEGenerator, generatePKCE: happyPKCEGenerator,
generateNonce: happyNonceGenerator, generateNonce: happyNonceGenerator,
@ -645,12 +709,48 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantContentType: htmlContentType, wantContentType: htmlContentType,
wantBodyStringWithLocationInHref: true, wantBodyStringWithLocationInHref: true,
wantCSRFValueInCookieHeader: happyCSRF, wantCSRFValueInCookieHeader: happyCSRF,
wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(map[string]string{"prompt": "login"}, "", ""), "login"), wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(map[string]string{"prompt": "login"}, "", ""), nil),
wantUpstreamStateParamInLocationHeader: true, wantUpstreamStateParamInLocationHeader: true,
}, },
{
name: "OIDC upstream browser flow happy path with extra params that get passed through",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().WithAdditionalAuthcodeParams(map[string]string{"prompt": "consent", "abc": "123", "def": "456"}).Build()),
generateCSRF: happyCSRFGenerator,
generatePKCE: happyPKCEGenerator,
generateNonce: happyNonceGenerator,
stateEncoder: happyStateEncoder,
cookieEncoder: happyCookieEncoder,
method: http.MethodGet,
path: modifiedHappyGetRequestPath(map[string]string{"prompt": "login"}),
contentType: "application/x-www-form-urlencoded",
body: encodeQuery(happyGetRequestQueryMap),
wantStatus: http.StatusFound,
wantContentType: htmlContentType,
wantBodyStringWithLocationInHref: true,
wantCSRFValueInCookieHeader: happyCSRF,
wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(map[string]string{"prompt": "login"}, "", ""), map[string]string{"prompt": "consent", "abc": "123", "def": "456"}),
wantUpstreamStateParamInLocationHeader: true,
},
{
name: "OIDC upstream browser flow with prompt param none throws an error because we want to independently decide the upstream prompt param",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
generateCSRF: happyCSRFGenerator,
generatePKCE: happyPKCEGenerator,
generateNonce: happyNonceGenerator,
stateEncoder: happyStateEncoder,
cookieEncoder: happyCookieEncoder,
method: http.MethodGet,
path: modifiedHappyGetRequestPath(map[string]string{"prompt": "none"}),
contentType: "application/x-www-form-urlencoded",
body: encodeQuery(happyGetRequestQueryMap),
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeLoginRequiredErrorQuery),
wantBodyString: "",
},
{ {
name: "OIDC upstream browser flow with error while decoding CSRF cookie just generates a new cookie and succeeds as usual", name: "OIDC upstream browser flow with error while decoding CSRF cookie just generates a new cookie and succeeds as usual",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
generateCSRF: happyCSRFGenerator, generateCSRF: happyCSRFGenerator,
generatePKCE: happyPKCEGenerator, generatePKCE: happyPKCEGenerator,
generateNonce: happyNonceGenerator, generateNonce: happyNonceGenerator,
@ -663,13 +763,13 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantContentType: htmlContentType, wantContentType: htmlContentType,
// Generated a new CSRF cookie and set it in the response. // Generated a new CSRF cookie and set it in the response.
wantCSRFValueInCookieHeader: happyCSRF, wantCSRFValueInCookieHeader: happyCSRF,
wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(nil, "", ""), ""), wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(nil, "", ""), nil),
wantUpstreamStateParamInLocationHeader: true, wantUpstreamStateParamInLocationHeader: true,
wantBodyStringWithLocationInHref: true, wantBodyStringWithLocationInHref: true,
}, },
{ {
name: "OIDC upstream browser flow happy path when downstream redirect uri matches what is configured for client except for the port number", name: "OIDC upstream browser flow happy path when downstream redirect uri matches what is configured for client except for the port number",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
generateCSRF: happyCSRFGenerator, generateCSRF: happyCSRFGenerator,
generatePKCE: happyPKCEGenerator, generatePKCE: happyPKCEGenerator,
generateNonce: happyNonceGenerator, generateNonce: happyNonceGenerator,
@ -684,7 +784,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantCSRFValueInCookieHeader: happyCSRF, wantCSRFValueInCookieHeader: happyCSRF,
wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(map[string]string{ wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(map[string]string{
"redirect_uri": downstreamRedirectURIWithDifferentPort, // not the same port number that is registered for the client "redirect_uri": downstreamRedirectURIWithDifferentPort, // not the same port number that is registered for the client
}, "", ""), ""), }, "", ""), nil),
wantUpstreamStateParamInLocationHeader: true, wantUpstreamStateParamInLocationHeader: true,
wantBodyStringWithLocationInHref: true, wantBodyStringWithLocationInHref: true,
}, },
@ -710,6 +810,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: expectedHappyOIDCPasswordGrantCustomSession,
}, },
{ {
name: "LDAP upstream happy path when downstream redirect uri matches what is configured for client except for the port number", name: "LDAP upstream happy path when downstream redirect uri matches what is configured for client except for the port number",
@ -732,10 +833,11 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: expectedHappyLDAPUpstreamCustomSession,
}, },
{ {
name: "OIDC upstream browser flow happy path when downstream requested scopes include offline_access", name: "OIDC upstream browser flow happy path when downstream requested scopes include offline_access",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
generateCSRF: happyCSRFGenerator, generateCSRF: happyCSRFGenerator,
generatePKCE: happyPKCEGenerator, generatePKCE: happyPKCEGenerator,
generateNonce: happyNonceGenerator, generateNonce: happyNonceGenerator,
@ -748,7 +850,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantCSRFValueInCookieHeader: happyCSRF, wantCSRFValueInCookieHeader: happyCSRF,
wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(map[string]string{ wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(map[string]string{
"scope": "openid offline_access", "scope": "openid offline_access",
}, "", ""), ""), }, "", ""), nil),
wantUpstreamStateParamInLocationHeader: true, wantUpstreamStateParamInLocationHeader: true,
wantBodyStringWithLocationInHref: true, wantBodyStringWithLocationInHref: true,
}, },
@ -811,7 +913,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "wrong upstream password for Active Directory authentication", name: "wrong upstream password for Active Directory authentication",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamLDAPIdentityProvider), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider),
method: http.MethodGet, method: http.MethodGet,
path: happyGetRequestPath, path: happyGetRequestPath,
customUsernameHeader: pointer.StringPtr(happyLDAPUsername), customUsernameHeader: pointer.StringPtr(happyLDAPUsername),
@ -835,7 +937,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "wrong upstream username for Active Directory authentication", name: "wrong upstream username for Active Directory authentication",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamLDAPIdentityProvider), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider),
method: http.MethodGet, method: http.MethodGet,
path: happyGetRequestPath, path: happyGetRequestPath,
customUsernameHeader: pointer.StringPtr("wrong-username"), customUsernameHeader: pointer.StringPtr("wrong-username"),
@ -859,7 +961,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "missing upstream username on request for Active Directory authentication", name: "missing upstream username on request for Active Directory authentication",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamLDAPIdentityProvider), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider),
method: http.MethodGet, method: http.MethodGet,
path: happyGetRequestPath, path: happyGetRequestPath,
customUsernameHeader: nil, // do not send header customUsernameHeader: nil, // do not send header
@ -883,7 +985,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "missing upstream password on request for Active Directory authentication", name: "missing upstream password on request for Active Directory authentication",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamLDAPIdentityProvider), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider),
method: http.MethodGet, method: http.MethodGet,
path: happyGetRequestPath, path: happyGetRequestPath,
customUsernameHeader: pointer.StringPtr(happyLDAPUsername), customUsernameHeader: pointer.StringPtr(happyLDAPUsername),
@ -893,6 +995,32 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingUsernamePasswordHintErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingUsernamePasswordHintErrorQuery),
wantBodyString: "", wantBodyString: "",
}, },
{
name: "return an error when upstream IDP did not return a refresh token",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().WithoutRefreshToken().Build()),
method: http.MethodGet,
path: happyGetRequestPath,
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingRefreshTokenErrorQuery),
wantBodyString: "",
},
{
name: "return an error when upstream IDP did not return a refresh token",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().WithEmptyRefreshToken().Build()),
method: http.MethodGet,
path: happyGetRequestPath,
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingRefreshTokenErrorQuery),
wantBodyString: "",
},
{ {
name: "missing upstream password on request for OIDC password grant authentication", name: "missing upstream password on request for OIDC password grant authentication",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()),
@ -907,7 +1035,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "using the custom username header on request for OIDC password grant authentication when OIDCIdentityProvider does not allow password grants", name: "using the custom username header on request for OIDC password grant authentication when OIDCIdentityProvider does not allow password grants",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
method: http.MethodGet, method: http.MethodGet,
path: happyGetRequestPath, path: happyGetRequestPath,
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername), customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
@ -919,7 +1047,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "downstream redirect uri does not match what is configured for client when using OIDC upstream browser flow", name: "downstream redirect uri does not match what is configured for client when using OIDC upstream browser flow",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
generateCSRF: happyCSRFGenerator, generateCSRF: happyCSRFGenerator,
generatePKCE: happyPKCEGenerator, generatePKCE: happyPKCEGenerator,
generateNonce: happyNonceGenerator, generateNonce: happyNonceGenerator,
@ -961,7 +1089,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "downstream redirect uri does not match what is configured for client when using active directory upstream", name: "downstream redirect uri does not match what is configured for client when using active directory upstream",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamLDAPIdentityProvider), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider),
method: http.MethodGet, method: http.MethodGet,
path: modifiedHappyGetRequestPath(map[string]string{ path: modifiedHappyGetRequestPath(map[string]string{
"redirect_uri": "http://127.0.0.1/does-not-match-what-is-configured-for-pinniped-cli-client", "redirect_uri": "http://127.0.0.1/does-not-match-what-is-configured-for-pinniped-cli-client",
@ -974,7 +1102,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "downstream client does not exist when using OIDC upstream browser flow", name: "downstream client does not exist when using OIDC upstream browser flow",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
generateCSRF: happyCSRFGenerator, generateCSRF: happyCSRFGenerator,
generatePKCE: happyPKCEGenerator, generatePKCE: happyPKCEGenerator,
generateNonce: happyNonceGenerator, generateNonce: happyNonceGenerator,
@ -1008,7 +1136,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "downstream client does not exist when using active directory upstream", name: "downstream client does not exist when using active directory upstream",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamLDAPIdentityProvider), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider),
method: http.MethodGet, method: http.MethodGet,
path: modifiedHappyGetRequestPath(map[string]string{"client_id": "invalid-client"}), path: modifiedHappyGetRequestPath(map[string]string{"client_id": "invalid-client"}),
wantStatus: http.StatusUnauthorized, wantStatus: http.StatusUnauthorized,
@ -1017,7 +1145,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "response type is unsupported when using OIDC upstream browser flow", name: "response type is unsupported when using OIDC upstream browser flow",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
generateCSRF: happyCSRFGenerator, generateCSRF: happyCSRFGenerator,
generatePKCE: happyPKCEGenerator, generatePKCE: happyPKCEGenerator,
generateNonce: happyNonceGenerator, generateNonce: happyNonceGenerator,
@ -1054,7 +1182,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "response type is unsupported when using active directory upstream", name: "response type is unsupported when using active directory upstream",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamLDAPIdentityProvider), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider),
method: http.MethodGet, method: http.MethodGet,
path: modifiedHappyGetRequestPath(map[string]string{"response_type": "unsupported"}), path: modifiedHappyGetRequestPath(map[string]string{"response_type": "unsupported"}),
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
@ -1064,7 +1192,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "downstream scopes do not match what is configured for client using OIDC upstream browser flow", name: "downstream scopes do not match what is configured for client using OIDC upstream browser flow",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
generateCSRF: happyCSRFGenerator, generateCSRF: happyCSRFGenerator,
generatePKCE: happyPKCEGenerator, generatePKCE: happyPKCEGenerator,
generateNonce: happyNonceGenerator, generateNonce: happyNonceGenerator,
@ -1103,7 +1231,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "downstream scopes do not match what is configured for client using Active Directory upstream", name: "downstream scopes do not match what is configured for client using Active Directory upstream",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamLDAPIdentityProvider), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider),
method: http.MethodGet, method: http.MethodGet,
path: modifiedHappyGetRequestPath(map[string]string{"scope": "openid tuna"}), path: modifiedHappyGetRequestPath(map[string]string{"scope": "openid tuna"}),
customUsernameHeader: pointer.StringPtr(happyLDAPUsername), customUsernameHeader: pointer.StringPtr(happyLDAPUsername),
@ -1115,7 +1243,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "missing response type in request using OIDC upstream browser flow", name: "missing response type in request using OIDC upstream browser flow",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
generateCSRF: happyCSRFGenerator, generateCSRF: happyCSRFGenerator,
generatePKCE: happyPKCEGenerator, generatePKCE: happyPKCEGenerator,
generateNonce: happyNonceGenerator, generateNonce: happyNonceGenerator,
@ -1152,7 +1280,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "missing response type in request using Active Directory upstream", name: "missing response type in request using Active Directory upstream",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamLDAPIdentityProvider), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider),
method: http.MethodGet, method: http.MethodGet,
path: modifiedHappyGetRequestPath(map[string]string{"response_type": ""}), path: modifiedHappyGetRequestPath(map[string]string{"response_type": ""}),
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
@ -1162,7 +1290,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "missing client id in request using OIDC upstream browser flow", name: "missing client id in request using OIDC upstream browser flow",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
generateCSRF: happyCSRFGenerator, generateCSRF: happyCSRFGenerator,
generatePKCE: happyPKCEGenerator, generatePKCE: happyPKCEGenerator,
generateNonce: happyNonceGenerator, generateNonce: happyNonceGenerator,
@ -1196,7 +1324,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "missing PKCE code_challenge in request using OIDC upstream browser flow", // See https://tools.ietf.org/html/rfc7636#section-4.4.1 name: "missing PKCE code_challenge in request using OIDC upstream browser flow", // See https://tools.ietf.org/html/rfc7636#section-4.4.1
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
generateCSRF: happyCSRFGenerator, generateCSRF: happyCSRFGenerator,
generatePKCE: happyPKCEGenerator, generatePKCE: happyPKCEGenerator,
generateNonce: happyNonceGenerator, generateNonce: happyNonceGenerator,
@ -1238,7 +1366,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "invalid value for PKCE code_challenge_method in request using OIDC upstream browser flow", // https://tools.ietf.org/html/rfc7636#section-4.3 name: "invalid value for PKCE code_challenge_method in request using OIDC upstream browser flow", // https://tools.ietf.org/html/rfc7636#section-4.3
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
generateCSRF: happyCSRFGenerator, generateCSRF: happyCSRFGenerator,
generatePKCE: happyPKCEGenerator, generatePKCE: happyPKCEGenerator,
generateNonce: happyNonceGenerator, generateNonce: happyNonceGenerator,
@ -1280,7 +1408,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "when PKCE code_challenge_method in request is `plain` using OIDC upstream browser flow", // https://tools.ietf.org/html/rfc7636#section-4.3 name: "when PKCE code_challenge_method in request is `plain` using OIDC upstream browser flow", // https://tools.ietf.org/html/rfc7636#section-4.3
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
generateCSRF: happyCSRFGenerator, generateCSRF: happyCSRFGenerator,
generatePKCE: happyPKCEGenerator, generatePKCE: happyPKCEGenerator,
generateNonce: happyNonceGenerator, generateNonce: happyNonceGenerator,
@ -1322,7 +1450,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "missing PKCE code_challenge_method in request using OIDC upstream browser flow", // See https://tools.ietf.org/html/rfc7636#section-4.4.1 name: "missing PKCE code_challenge_method in request using OIDC upstream browser flow", // See https://tools.ietf.org/html/rfc7636#section-4.4.1
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
generateCSRF: happyCSRFGenerator, generateCSRF: happyCSRFGenerator,
generatePKCE: happyPKCEGenerator, generatePKCE: happyPKCEGenerator,
generateNonce: happyNonceGenerator, generateNonce: happyNonceGenerator,
@ -1366,7 +1494,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
// This is just one of the many OIDC validations run by fosite. This test is to ensure that we are running // This is just one of the many OIDC validations run by fosite. This test is to ensure that we are running
// through that part of the fosite library when using an OIDC upstream browser flow. // through that part of the fosite library when using an OIDC upstream browser flow.
name: "prompt param is not allowed to have none and another legal value at the same time using OIDC upstream browser flow", name: "prompt param is not allowed to have none and another legal value at the same time using OIDC upstream browser flow",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
generateCSRF: happyCSRFGenerator, generateCSRF: happyCSRFGenerator,
generatePKCE: happyPKCEGenerator, generatePKCE: happyPKCEGenerator,
generateNonce: happyNonceGenerator, generateNonce: happyNonceGenerator,
@ -1412,7 +1540,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "happy path: downstream OIDC validations are skipped when the openid scope was not requested using OIDC upstream browser flow", name: "happy path: downstream OIDC validations are skipped when the openid scope was not requested using OIDC upstream browser flow",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
generateCSRF: happyCSRFGenerator, generateCSRF: happyCSRFGenerator,
generatePKCE: happyPKCEGenerator, generatePKCE: happyPKCEGenerator,
generateNonce: happyNonceGenerator, generateNonce: happyNonceGenerator,
@ -1426,7 +1554,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantCSRFValueInCookieHeader: happyCSRF, wantCSRFValueInCookieHeader: happyCSRF,
wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam( wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(
map[string]string{"prompt": "none login", "scope": "email"}, "", "", map[string]string{"prompt": "none login", "scope": "email"}, "", "",
), ""), ), nil),
wantUpstreamStateParamInLocationHeader: true, wantUpstreamStateParamInLocationHeader: true,
wantBodyStringWithLocationInHref: true, wantBodyStringWithLocationInHref: true,
}, },
@ -1451,6 +1579,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: expectedHappyOIDCPasswordGrantCustomSession,
}, },
{ {
name: "happy path: downstream OIDC validations are skipped when the openid scope was not requested using LDAP upstream", name: "happy path: downstream OIDC validations are skipped when the openid scope was not requested using LDAP upstream",
@ -1472,6 +1601,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: expectedHappyLDAPUpstreamCustomSession,
}, },
{ {
name: "OIDC upstream password grant: upstream IDP provides no username or group claim configuration, so we use default username claim and skip groups", name: "OIDC upstream password grant: upstream IDP provides no username or group claim configuration, so we use default username claim and skip groups",
@ -1495,6 +1625,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: expectedHappyOIDCPasswordGrantCustomSession,
}, },
{ {
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",
@ -1520,6 +1651,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: expectedHappyOIDCPasswordGrantCustomSession,
}, },
{ {
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",
@ -1546,6 +1678,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: expectedHappyOIDCPasswordGrantCustomSession,
}, },
{ {
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",
@ -1573,6 +1706,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: expectedHappyOIDCPasswordGrantCustomSession,
}, },
{ {
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",
@ -1632,6 +1766,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: expectedHappyOIDCPasswordGrantCustomSession,
}, },
{ {
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",
@ -1656,6 +1791,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: expectedHappyOIDCPasswordGrantCustomSession,
}, },
{ {
name: "OIDC upstream password grant: upstream IDP's configured groups claim in the ID token is a slice of interfaces", name: "OIDC upstream password grant: upstream IDP's configured groups claim in the ID token is a slice of interfaces",
@ -1680,6 +1816,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: expectedHappyOIDCPasswordGrantCustomSession,
}, },
{ {
name: "OIDC upstream password grant: upstream ID token does not contain requested username claim", name: "OIDC upstream password grant: upstream ID token does not contain requested username claim",
@ -1718,6 +1855,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: expectedHappyOIDCPasswordGrantCustomSession,
}, },
{ {
name: "OIDC upstream password grant: upstream ID token contains username claim with weird format", name: "OIDC upstream password grant: upstream ID token contains username claim with weird format",
@ -1886,7 +2024,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "downstream state does not have enough entropy using OIDC upstream browser flow", name: "downstream state does not have enough entropy using OIDC upstream browser flow",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
generateCSRF: happyCSRFGenerator, generateCSRF: happyCSRFGenerator,
generatePKCE: happyPKCEGenerator, generatePKCE: happyPKCEGenerator,
generateNonce: happyNonceGenerator, generateNonce: happyNonceGenerator,
@ -1925,7 +2063,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "error while encoding upstream state param using OIDC upstream browser flow", name: "error while encoding upstream state param using OIDC upstream browser flow",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
generateCSRF: happyCSRFGenerator, generateCSRF: happyCSRFGenerator,
generatePKCE: happyPKCEGenerator, generatePKCE: happyPKCEGenerator,
generateNonce: happyNonceGenerator, generateNonce: happyNonceGenerator,
@ -1939,7 +2077,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "error while encoding CSRF cookie value for new cookie using OIDC upstream browser flow", name: "error while encoding CSRF cookie value for new cookie using OIDC upstream browser flow",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
generateCSRF: happyCSRFGenerator, generateCSRF: happyCSRFGenerator,
generatePKCE: happyPKCEGenerator, generatePKCE: happyPKCEGenerator,
generateNonce: happyNonceGenerator, generateNonce: happyNonceGenerator,
@ -1953,7 +2091,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "error while generating CSRF token using OIDC upstream browser flow", name: "error while generating CSRF token using OIDC upstream browser flow",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
generateCSRF: sadCSRFGenerator, generateCSRF: sadCSRFGenerator,
generatePKCE: happyPKCEGenerator, generatePKCE: happyPKCEGenerator,
generateNonce: happyNonceGenerator, generateNonce: happyNonceGenerator,
@ -1967,7 +2105,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "error while generating nonce using OIDC upstream browser flow", name: "error while generating nonce using OIDC upstream browser flow",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
generateCSRF: happyCSRFGenerator, generateCSRF: happyCSRFGenerator,
generatePKCE: happyPKCEGenerator, generatePKCE: happyPKCEGenerator,
generateNonce: sadNonceGenerator, generateNonce: sadNonceGenerator,
@ -1981,7 +2119,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "error while generating PKCE using OIDC upstream browser flow", name: "error while generating PKCE using OIDC upstream browser flow",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
generateCSRF: happyCSRFGenerator, generateCSRF: happyCSRFGenerator,
generatePKCE: sadPKCEGenerator, generatePKCE: sadPKCEGenerator,
generateNonce: happyNonceGenerator, generateNonce: happyNonceGenerator,
@ -2004,7 +2142,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "too many upstream providers are configured: multiple OIDC", name: "too many upstream providers are configured: multiple OIDC",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider(), upstreamOIDCIdentityProvider()), // more than one not allowed idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build(), upstreamOIDCIdentityProviderBuilder().Build()), // more than one not allowed
method: http.MethodGet, method: http.MethodGet,
path: happyGetRequestPath, path: happyGetRequestPath,
wantStatus: http.StatusUnprocessableEntity, wantStatus: http.StatusUnprocessableEntity,
@ -2031,7 +2169,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "too many upstream providers are configured: both OIDC and LDAP", name: "too many upstream providers are configured: both OIDC and LDAP",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()).WithLDAP(&upstreamLDAPIdentityProvider), // more than one not allowed idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()).WithLDAP(&upstreamLDAPIdentityProvider), // more than one not allowed
method: http.MethodGet, method: http.MethodGet,
path: happyGetRequestPath, path: happyGetRequestPath,
wantStatus: http.StatusUnprocessableEntity, wantStatus: http.StatusUnprocessableEntity,
@ -2040,7 +2178,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "too many upstream providers are configured: OIDC, LDAP and AD", name: "too many upstream providers are configured: OIDC, LDAP and AD",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()).WithLDAP(&upstreamLDAPIdentityProvider).WithActiveDirectory(&upstreamLDAPIdentityProvider), // more than one not allowed idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()).WithLDAP(&upstreamLDAPIdentityProvider).WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider), // more than one not allowed
method: http.MethodGet, method: http.MethodGet,
path: happyGetRequestPath, path: happyGetRequestPath,
wantStatus: http.StatusUnprocessableEntity, wantStatus: http.StatusUnprocessableEntity,
@ -2049,7 +2187,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "PUT is a bad method", name: "PUT is a bad method",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
method: http.MethodPut, method: http.MethodPut,
path: "/some/path", path: "/some/path",
wantStatus: http.StatusMethodNotAllowed, wantStatus: http.StatusMethodNotAllowed,
@ -2058,7 +2196,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "PATCH is a bad method", name: "PATCH is a bad method",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
method: http.MethodPatch, method: http.MethodPatch,
path: "/some/path", path: "/some/path",
wantStatus: http.StatusMethodNotAllowed, wantStatus: http.StatusMethodNotAllowed,
@ -2067,7 +2205,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "DELETE is a bad method", name: "DELETE is a bad method",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
method: http.MethodDelete, method: http.MethodDelete,
path: "/some/path", path: "/some/path",
wantStatus: http.StatusMethodNotAllowed, wantStatus: http.StatusMethodNotAllowed,
@ -2143,6 +2281,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
test.wantDownstreamNonce, test.wantDownstreamNonce,
downstreamClientID, downstreamClientID,
test.wantDownstreamRedirectURI, test.wantDownstreamRedirectURI,
test.wantDownstreamCustomSessionData,
) )
default: default:
require.Empty(t, rsp.Header().Values("Location")) require.Empty(t, rsp.Header().Values("Location"))
@ -2216,6 +2355,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
WithClientID("some-other-new-client-id"). WithClientID("some-other-new-client-id").
WithAuthorizationURL(*upstreamAuthURL). WithAuthorizationURL(*upstreamAuthURL).
WithScopes([]string{"some-other-new-scope1", "some-other-new-scope2"}). WithScopes([]string{"some-other-new-scope1", "some-other-new-scope2"}).
WithAdditionalAuthcodeParams(map[string]string{"prompt": "consent", "abc": "123"}).
Build() Build()
idpLister.SetOIDCIdentityProviders([]provider.UpstreamOIDCIdentityProviderI{provider.UpstreamOIDCIdentityProviderI(newProviderSettings)}) idpLister.SetOIDCIdentityProviders([]provider.UpstreamOIDCIdentityProviderI{provider.UpstreamOIDCIdentityProviderI(newProviderSettings)})
@ -2223,7 +2363,8 @@ func TestAuthorizationEndpoint(t *testing.T) {
test.wantLocationHeader = urlWithQuery(upstreamAuthURL.String(), test.wantLocationHeader = urlWithQuery(upstreamAuthURL.String(),
map[string]string{ map[string]string{
"response_type": "code", "response_type": "code",
"access_type": "offline", "prompt": "consent",
"abc": "123",
"scope": "some-other-new-scope1 some-other-new-scope2", // updated expectation "scope": "some-other-new-scope1 some-other-new-scope2", // updated expectation
"client_id": "some-other-new-client-id", // updated expectation "client_id": "some-other-new-client-id", // updated expectation
"state": expectedUpstreamStateParam( "state": expectedUpstreamStateParam(

View File

@ -19,6 +19,7 @@ import (
"go.pinniped.dev/internal/oidc/provider" "go.pinniped.dev/internal/oidc/provider"
"go.pinniped.dev/internal/oidc/provider/formposthtml" "go.pinniped.dev/internal/oidc/provider/formposthtml"
"go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/plog"
"go.pinniped.dev/internal/psession"
) )
func NewHandler( func NewHandler(
@ -68,12 +69,28 @@ func NewHandler(
return httperr.New(http.StatusBadGateway, "error exchanging and validating upstream tokens") return httperr.New(http.StatusBadGateway, "error exchanging and validating upstream tokens")
} }
if token.RefreshToken == nil || token.RefreshToken.Token == "" {
plog.Warning("refresh token not returned by upstream provider during authcode exchange, "+
"please check configuration of OIDCIdentityProvider and the client in the upstream provider's API/UI",
"upstreamName", upstreamIDPConfig.GetName(),
"scopes", upstreamIDPConfig.GetScopes(),
"additionalParams", upstreamIDPConfig.GetAdditionalAuthcodeParams())
return httperr.New(http.StatusUnprocessableEntity, "refresh token not returned by upstream provider during authcode exchange")
}
subject, username, groups, err := downstreamsession.GetDownstreamIdentityFromUpstreamIDToken(upstreamIDPConfig, token.IDToken.Claims) subject, username, groups, err := downstreamsession.GetDownstreamIdentityFromUpstreamIDToken(upstreamIDPConfig, token.IDToken.Claims)
if err != nil { if err != nil {
return httperr.Wrap(http.StatusUnprocessableEntity, err.Error(), err) return httperr.Wrap(http.StatusUnprocessableEntity, err.Error(), err)
} }
openIDSession := downstreamsession.MakeDownstreamSession(subject, username, groups) openIDSession := downstreamsession.MakeDownstreamSession(subject, username, groups, &psession.CustomSessionData{
ProviderUID: upstreamIDPConfig.GetResourceUID(),
ProviderName: upstreamIDPConfig.GetName(),
ProviderType: psession.ProviderTypeOIDC,
OIDC: &psession.OIDCSessionData{
UpstreamRefreshToken: token.RefreshToken.Token,
},
})
authorizeResponder, err := oauthHelper.NewAuthorizeResponse(r.Context(), authorizeRequester, openIDSession) authorizeResponder, err := oauthHelper.NewAuthorizeResponse(r.Context(), authorizeRequester, openIDSession)
if err != nil { if err != nil {

View File

@ -18,6 +18,7 @@ import (
"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/psession"
"go.pinniped.dev/internal/testutil" "go.pinniped.dev/internal/testutil"
"go.pinniped.dev/internal/testutil/oidctestutil" "go.pinniped.dev/internal/testutil/oidctestutil"
"go.pinniped.dev/pkg/oidcclient/nonce" "go.pinniped.dev/pkg/oidcclient/nonce"
@ -25,9 +26,11 @@ import (
) )
const ( const (
happyUpstreamIDPName = "upstream-idp-name" happyUpstreamIDPName = "upstream-idp-name"
happyUpstreamIDPResourceUID = "upstream-uid"
oidcUpstreamIssuer = "https://my-upstream-issuer.com" oidcUpstreamIssuer = "https://my-upstream-issuer.com"
oidcUpstreamRefreshToken = "test-refresh-token"
oidcUpstreamSubject = "abc123-some guid" // has a space character which should get escaped in URL oidcUpstreamSubject = "abc123-some guid" // has a space character which should get escaped in URL
oidcUpstreamSubjectQueryEscaped = "abc123-some+guid" oidcUpstreamSubjectQueryEscaped = "abc123-some+guid"
oidcUpstreamUsername = "test-pinniped-username" oidcUpstreamUsername = "test-pinniped-username"
@ -69,7 +72,13 @@ var (
"code_challenge_method": []string{downstreamPKCEChallengeMethod}, "code_challenge_method": []string{downstreamPKCEChallengeMethod},
"redirect_uri": []string{downstreamRedirectURI}, "redirect_uri": []string{downstreamRedirectURI},
} }
happyDownstreamRequestParams = happyDownstreamRequestParamsQuery.Encode() happyDownstreamRequestParams = happyDownstreamRequestParamsQuery.Encode()
happyDownstreamCustomSessionData = &psession.CustomSessionData{
ProviderUID: happyUpstreamIDPResourceUID,
ProviderName: happyUpstreamIDPName,
ProviderType: psession.ProviderTypeOIDC,
OIDC: &psession.OIDCSessionData{UpstreamRefreshToken: oidcUpstreamRefreshToken},
}
) )
func TestCallbackEndpoint(t *testing.T) { func TestCallbackEndpoint(t *testing.T) {
@ -130,6 +139,7 @@ func TestCallbackEndpoint(t *testing.T) {
wantDownstreamNonce string wantDownstreamNonce string
wantDownstreamPKCEChallenge string wantDownstreamPKCEChallenge string
wantDownstreamPKCEChallengeMethod string wantDownstreamPKCEChallengeMethod string
wantDownstreamCustomSessionData *psession.CustomSessionData
wantAuthcodeExchangeCall *expectedAuthcodeExchange wantAuthcodeExchangeCall *expectedAuthcodeExchange
}{ }{
@ -157,6 +167,7 @@ func TestCallbackEndpoint(t *testing.T) {
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: happyDownstreamCustomSessionData,
wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ wantAuthcodeExchangeCall: &expectedAuthcodeExchange{
performedByUpstreamName: happyUpstreamIDPName, performedByUpstreamName: happyUpstreamIDPName,
args: happyExchangeAndValidateTokensArgs, args: happyExchangeAndValidateTokensArgs,
@ -179,6 +190,7 @@ func TestCallbackEndpoint(t *testing.T) {
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: happyDownstreamCustomSessionData,
wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ wantAuthcodeExchangeCall: &expectedAuthcodeExchange{
performedByUpstreamName: happyUpstreamIDPName, performedByUpstreamName: happyUpstreamIDPName,
args: happyExchangeAndValidateTokensArgs, args: happyExchangeAndValidateTokensArgs,
@ -203,6 +215,7 @@ func TestCallbackEndpoint(t *testing.T) {
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: happyDownstreamCustomSessionData,
wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ wantAuthcodeExchangeCall: &expectedAuthcodeExchange{
performedByUpstreamName: happyUpstreamIDPName, performedByUpstreamName: happyUpstreamIDPName,
args: happyExchangeAndValidateTokensArgs, args: happyExchangeAndValidateTokensArgs,
@ -227,6 +240,7 @@ func TestCallbackEndpoint(t *testing.T) {
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: happyDownstreamCustomSessionData,
wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ wantAuthcodeExchangeCall: &expectedAuthcodeExchange{
performedByUpstreamName: happyUpstreamIDPName, performedByUpstreamName: happyUpstreamIDPName,
args: happyExchangeAndValidateTokensArgs, args: happyExchangeAndValidateTokensArgs,
@ -253,6 +267,7 @@ func TestCallbackEndpoint(t *testing.T) {
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: happyDownstreamCustomSessionData,
wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ wantAuthcodeExchangeCall: &expectedAuthcodeExchange{
performedByUpstreamName: happyUpstreamIDPName, performedByUpstreamName: happyUpstreamIDPName,
args: happyExchangeAndValidateTokensArgs, args: happyExchangeAndValidateTokensArgs,
@ -280,6 +295,7 @@ func TestCallbackEndpoint(t *testing.T) {
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: happyDownstreamCustomSessionData,
wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ wantAuthcodeExchangeCall: &expectedAuthcodeExchange{
performedByUpstreamName: happyUpstreamIDPName, performedByUpstreamName: happyUpstreamIDPName,
args: happyExchangeAndValidateTokensArgs, args: happyExchangeAndValidateTokensArgs,
@ -302,6 +318,34 @@ func TestCallbackEndpoint(t *testing.T) {
args: happyExchangeAndValidateTokensArgs, args: happyExchangeAndValidateTokensArgs,
}, },
}, },
{
name: "return an error when upstream IDP did not return a refresh token",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().WithoutRefreshToken().Build()),
method: http.MethodGet,
path: newRequestPath().WithState(happyState).String(),
csrfCookie: happyCSRFCookie,
wantStatus: http.StatusUnprocessableEntity,
wantContentType: htmlContentType,
wantBody: "Unprocessable Entity: refresh token not returned by upstream provider during authcode exchange\n",
wantAuthcodeExchangeCall: &expectedAuthcodeExchange{
performedByUpstreamName: happyUpstreamIDPName,
args: happyExchangeAndValidateTokensArgs,
},
},
{
name: "return an error when upstream IDP returned an empty refresh token",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().WithEmptyRefreshToken().Build()),
method: http.MethodGet,
path: newRequestPath().WithState(happyState).String(),
csrfCookie: happyCSRFCookie,
wantStatus: http.StatusUnprocessableEntity,
wantContentType: htmlContentType,
wantBody: "Unprocessable Entity: refresh token not returned by upstream provider during authcode exchange\n",
wantAuthcodeExchangeCall: &expectedAuthcodeExchange{
performedByUpstreamName: happyUpstreamIDPName,
args: happyExchangeAndValidateTokensArgs,
},
},
{ {
name: "upstream IDP configures username claim as special claim `email` and `email_verified` upstream claim is present with false value", name: "upstream IDP configures username claim as special claim `email` and `email_verified` upstream claim is present with false value",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(
@ -339,6 +383,7 @@ func TestCallbackEndpoint(t *testing.T) {
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: happyDownstreamCustomSessionData,
wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ wantAuthcodeExchangeCall: &expectedAuthcodeExchange{
performedByUpstreamName: happyUpstreamIDPName, performedByUpstreamName: happyUpstreamIDPName,
args: happyExchangeAndValidateTokensArgs, args: happyExchangeAndValidateTokensArgs,
@ -363,6 +408,7 @@ func TestCallbackEndpoint(t *testing.T) {
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: happyDownstreamCustomSessionData,
wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ wantAuthcodeExchangeCall: &expectedAuthcodeExchange{
performedByUpstreamName: happyUpstreamIDPName, performedByUpstreamName: happyUpstreamIDPName,
args: happyExchangeAndValidateTokensArgs, args: happyExchangeAndValidateTokensArgs,
@ -387,6 +433,7 @@ func TestCallbackEndpoint(t *testing.T) {
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: happyDownstreamCustomSessionData,
wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ wantAuthcodeExchangeCall: &expectedAuthcodeExchange{
performedByUpstreamName: happyUpstreamIDPName, performedByUpstreamName: happyUpstreamIDPName,
args: happyExchangeAndValidateTokensArgs, args: happyExchangeAndValidateTokensArgs,
@ -537,6 +584,7 @@ func TestCallbackEndpoint(t *testing.T) {
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: happyDownstreamCustomSessionData,
wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ wantAuthcodeExchangeCall: &expectedAuthcodeExchange{
performedByUpstreamName: happyUpstreamIDPName, performedByUpstreamName: happyUpstreamIDPName,
args: happyExchangeAndValidateTokensArgs, args: happyExchangeAndValidateTokensArgs,
@ -563,6 +611,7 @@ func TestCallbackEndpoint(t *testing.T) {
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: happyDownstreamCustomSessionData,
wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ wantAuthcodeExchangeCall: &expectedAuthcodeExchange{
performedByUpstreamName: happyUpstreamIDPName, performedByUpstreamName: happyUpstreamIDPName,
args: happyExchangeAndValidateTokensArgs, args: happyExchangeAndValidateTokensArgs,
@ -660,6 +709,7 @@ func TestCallbackEndpoint(t *testing.T) {
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: happyDownstreamCustomSessionData,
wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ wantAuthcodeExchangeCall: &expectedAuthcodeExchange{
performedByUpstreamName: happyUpstreamIDPName, performedByUpstreamName: happyUpstreamIDPName,
args: happyExchangeAndValidateTokensArgs, args: happyExchangeAndValidateTokensArgs,
@ -907,6 +957,7 @@ func TestCallbackEndpoint(t *testing.T) {
test.wantDownstreamNonce, test.wantDownstreamNonce,
downstreamClientID, downstreamClientID,
downstreamRedirectURI, downstreamRedirectURI,
test.wantDownstreamCustomSessionData,
) )
// Otherwise, expect an empty response body. // Otherwise, expect an empty response body.
@ -933,6 +984,7 @@ func TestCallbackEndpoint(t *testing.T) {
test.wantDownstreamNonce, test.wantDownstreamNonce,
downstreamClientID, downstreamClientID,
downstreamRedirectURI, downstreamRedirectURI,
test.wantDownstreamCustomSessionData,
) )
} }
}) })
@ -1036,6 +1088,7 @@ func (b *upstreamStateParamBuilder) WithStateVersion(version string) *upstreamSt
func happyUpstream() *oidctestutil.TestUpstreamOIDCIdentityProviderBuilder { func happyUpstream() *oidctestutil.TestUpstreamOIDCIdentityProviderBuilder {
return oidctestutil.NewTestUpstreamOIDCIdentityProviderBuilder(). return oidctestutil.NewTestUpstreamOIDCIdentityProviderBuilder().
WithName(happyUpstreamIDPName). WithName(happyUpstreamIDPName).
WithResourceUID(happyUpstreamIDPResourceUID).
WithClientID("some-client-id"). WithClientID("some-client-id").
WithScopes([]string{"scope1", "scope2"}). WithScopes([]string{"scope1", "scope2"}).
WithUsernameClaim(oidcUpstreamUsernameClaim). WithUsernameClaim(oidcUpstreamUsernameClaim).
@ -1046,6 +1099,7 @@ func happyUpstream() *oidctestutil.TestUpstreamOIDCIdentityProviderBuilder {
WithIDTokenClaim(oidcUpstreamGroupsClaim, oidcUpstreamGroupMembership). WithIDTokenClaim(oidcUpstreamGroupsClaim, oidcUpstreamGroupMembership).
WithIDTokenClaim("other-claim", "should be ignored"). WithIDTokenClaim("other-claim", "should be ignored").
WithAllowPasswordGrant(false). WithAllowPasswordGrant(false).
WithRefreshToken(oidcUpstreamRefreshToken).
WithPasswordGrantError(errors.New("the callback endpoint should not use password grants")) WithPasswordGrantError(errors.New("the callback endpoint should not use password grants"))
} }

View File

@ -18,6 +18,7 @@ import (
"go.pinniped.dev/internal/oidc" "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/internal/psession"
) )
const ( const (
@ -35,19 +36,22 @@ const (
) )
// MakeDownstreamSession creates a downstream OIDC session. // MakeDownstreamSession creates a downstream OIDC session.
func MakeDownstreamSession(subject string, username string, groups []string) *openid.DefaultSession { func MakeDownstreamSession(subject string, username string, groups []string, custom *psession.CustomSessionData) *psession.PinnipedSession {
now := time.Now().UTC() now := time.Now().UTC()
openIDSession := &openid.DefaultSession{ openIDSession := &psession.PinnipedSession{
Claims: &jwt.IDTokenClaims{ Fosite: &openid.DefaultSession{
Subject: subject, Claims: &jwt.IDTokenClaims{
RequestedAt: now, Subject: subject,
AuthTime: now, RequestedAt: now,
AuthTime: now,
},
}, },
Custom: custom,
} }
if groups == nil { if groups == nil {
groups = []string{} groups = []string{}
} }
openIDSession.Claims.Extra = map[string]interface{}{ openIDSession.IDTokenClaims().Extra = map[string]interface{}{
oidc.DownstreamUsernameClaim: username, oidc.DownstreamUsernameClaim: username,
oidc.DownstreamGroupsClaim: groups, oidc.DownstreamGroupsClaim: groups,
} }

View File

@ -210,8 +210,6 @@ func FositeOauth2Helper(
// 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
// We'll make a best effort to support these by passing the value of this prompt param to the upstream IDP
// and rely on its implementation of this param.
AllowedPromptValues: nil, AllowedPromptValues: nil,
// Use the fosite default to make it more likely that off the shelf OIDC clients can work with the supervisor. // Use the fosite default to make it more likely that off the shelf OIDC clients can work with the supervisor.
@ -232,7 +230,7 @@ func FositeOauth2Helper(
compose.OpenIDConnectExplicitFactory, compose.OpenIDConnectExplicitFactory,
compose.OpenIDConnectRefreshFactory, compose.OpenIDConnectRefreshFactory,
compose.OAuth2PKCEFactory, compose.OAuth2PKCEFactory,
TokenExchangeFactory, TokenExchangeFactory, // handle the "urn:ietf:params:oauth:grant-type:token-exchange" grant type
) )
provider.(*fosite.Fosite).FormPostHTMLTemplate = formposthtml.Template() provider.(*fosite.Fosite).FormPostHTMLTemplate = formposthtml.Template()
return provider return provider

View File

@ -9,6 +9,7 @@ import (
"sync" "sync"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"k8s.io/apimachinery/pkg/types"
"go.pinniped.dev/internal/authenticators" "go.pinniped.dev/internal/authenticators"
"go.pinniped.dev/pkg/oidcclient/nonce" "go.pinniped.dev/pkg/oidcclient/nonce"
@ -24,6 +25,9 @@ type UpstreamOIDCIdentityProviderI interface {
// GetClientID returns the OAuth client ID registered with the upstream provider to be used in the authorization code flow. // GetClientID returns the OAuth client ID registered with the upstream provider to be used in the authorization code flow.
GetClientID() string GetClientID() string
// GetResourceUID returns the Kubernetes resource ID
GetResourceUID() types.UID
// GetAuthorizationURL returns the Authorization Endpoint fetched from discovery. // GetAuthorizationURL returns the Authorization Endpoint fetched from discovery.
GetAuthorizationURL() *url.URL GetAuthorizationURL() *url.URL
@ -42,6 +46,9 @@ type UpstreamOIDCIdentityProviderI interface {
// flow with this upstream provider. When false, it should not be allowed. // flow with this upstream provider. When false, it should not be allowed.
AllowsPasswordGrant() bool AllowsPasswordGrant() bool
// GetAdditionalAuthcodeParams returns additional params to be sent on authcode requests.
GetAdditionalAuthcodeParams() map[string]string
// PasswordCredentialsGrantAndValidateTokens performs upstream OIDC resource owner password credentials grant and // PasswordCredentialsGrantAndValidateTokens performs upstream OIDC resource owner password credentials grant and
// token validation. Returns the validated raw tokens as well as the parsed claims of the ID token. // token validation. Returns the validated raw tokens as well as the parsed claims of the ID token.
PasswordCredentialsGrantAndValidateTokens(ctx context.Context, username, password string) (*oidctypes.Token, error) PasswordCredentialsGrantAndValidateTokens(ctx context.Context, username, password string) (*oidctypes.Token, error)
@ -56,6 +63,14 @@ type UpstreamOIDCIdentityProviderI interface {
redirectURI string, redirectURI string,
) (*oidctypes.Token, error) ) (*oidctypes.Token, error)
// PerformRefresh will call the provider's token endpoint to perform a refresh grant. The provider may or may not
// return a new ID or refresh token in the response. If it returns an ID token, then use ValidateToken to
// validate the ID token.
PerformRefresh(ctx context.Context, refreshToken string) (*oauth2.Token, error)
// ValidateToken will validate the ID token. It will also merge the claims from the userinfo endpoint response
// into the ID token's claims, if the provider offers the userinfo endpoint. It returns the validated/updated
// tokens, or an error.
ValidateToken(ctx context.Context, tok *oauth2.Token, expectedIDTokenNonce nonce.Nonce) (*oidctypes.Token, error) ValidateToken(ctx context.Context, tok *oauth2.Token, expectedIDTokenNonce nonce.Nonce) (*oidctypes.Token, error)
} }
@ -68,6 +83,9 @@ type UpstreamLDAPIdentityProviderI interface {
// identifier by being combined with the user's UID, since user UIDs are only unique within one provider. // identifier by being combined with the user's UID, since user UIDs are only unique within one provider.
GetURL() *url.URL GetURL() *url.URL
// GetResourceUID returns the Kubernetes resource ID
GetResourceUID() types.UID
// UserAuthenticator adds an interface method for performing user authentication against the upstream LDAP provider. // UserAuthenticator adds an interface method for performing user authentication against the upstream LDAP provider.
authenticators.UserAuthenticator authenticators.UserAuthenticator
} }

View File

@ -130,6 +130,7 @@ func (m *Manager) SetProviders(federationDomains ...*provider.FederationDomainIs
) )
m.providerHandlers[(issuerHostWithPath + oidc.TokenEndpointPath)] = token.NewHandler( m.providerHandlers[(issuerHostWithPath + oidc.TokenEndpointPath)] = token.NewHandler(
m.upstreamIDPs,
oauthHelperWithKubeStorage, oauthHelperWithKubeStorage,
) )

View File

@ -264,6 +264,7 @@ func TestManager(t *testing.T) {
"groups": "test-group1", "groups": "test-group1",
}, },
}, },
RefreshToken: &oidctypes.RefreshToken{Token: "some-opaque-token"},
}, nil }, nil
}, },
}).Build() }).Build()

View File

@ -1,32 +1,65 @@
// Copyright 2020 the Pinniped contributors. All Rights Reserved. // Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
// Package token provides a handler for the OIDC token endpoint. // Package token provides a handler for the OIDC token endpoint.
package token package token
import ( import (
"context"
"net/http" "net/http"
"github.com/ory/fosite" "github.com/ory/fosite"
"github.com/ory/fosite/handler/openid" "github.com/ory/x/errorsx"
"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/provider"
"go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/plog"
"go.pinniped.dev/internal/psession"
)
var (
errMissingUpstreamSessionInternalError = &fosite.RFC6749Error{
ErrorField: "error",
DescriptionField: "There was an internal server error.",
HintField: "Required upstream data not found in session.",
CodeField: http.StatusInternalServerError,
}
errUpstreamRefreshError = &fosite.RFC6749Error{
ErrorField: "error",
DescriptionField: "Error during upstream refresh.",
CodeField: http.StatusUnauthorized,
}
) )
func NewHandler( func NewHandler(
idpLister oidc.UpstreamIdentityProvidersLister,
oauthHelper fosite.OAuth2Provider, oauthHelper fosite.OAuth2Provider,
) http.Handler { ) http.Handler {
return httperr.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { return httperr.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
var session openid.DefaultSession session := psession.NewPinnipedSession()
accessRequest, err := oauthHelper.NewAccessRequest(r.Context(), r, &session) accessRequest, err := oauthHelper.NewAccessRequest(r.Context(), r, session)
if err != nil { if err != nil {
plog.Info("token request error", oidc.FositeErrorForLog(err)...) plog.Info("token request error", oidc.FositeErrorForLog(err)...)
oauthHelper.WriteAccessError(w, accessRequest, err) oauthHelper.WriteAccessError(w, accessRequest, err)
return nil return nil
} }
// Check if we are performing a refresh grant.
if accessRequest.GetGrantTypes().ExactOne("refresh_token") {
// 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
// from the Kube storage layer and added to the accessRequest. Additionally, the audience and scopes may
// have already been granted on the accessRequest.
err = upstreamRefresh(r.Context(), accessRequest, idpLister)
if err != nil {
plog.Info("upstream refresh error", oidc.FositeErrorForLog(err)...)
oauthHelper.WriteAccessError(w, accessRequest, err)
return nil
}
}
accessResponse, err := oauthHelper.NewAccessResponse(r.Context(), accessRequest) accessResponse, err := oauthHelper.NewAccessResponse(r.Context(), accessRequest)
if err != nil { if err != nil {
plog.Info("token response error", oidc.FositeErrorForLog(err)...) plog.Info("token response error", oidc.FositeErrorForLog(err)...)
@ -39,3 +72,96 @@ func NewHandler(
return nil return nil
}) })
} }
func upstreamRefresh(ctx context.Context, accessRequest fosite.AccessRequester, providerCache oidc.UpstreamIdentityProvidersLister) error {
session := accessRequest.GetSession().(*psession.PinnipedSession)
customSessionData := session.Custom
if customSessionData == nil {
return errorsx.WithStack(errMissingUpstreamSessionInternalError)
}
providerName := customSessionData.ProviderName
providerUID := customSessionData.ProviderUID
if providerUID == "" || providerName == "" {
return errorsx.WithStack(errMissingUpstreamSessionInternalError)
}
switch customSessionData.ProviderType {
case psession.ProviderTypeOIDC:
return upstreamOIDCRefresh(ctx, customSessionData, providerCache)
case psession.ProviderTypeLDAP:
// upstream refresh not yet implemented for LDAP, so do nothing
case psession.ProviderTypeActiveDirectory:
// upstream refresh not yet implemented for AD, so do nothing
default:
return errorsx.WithStack(errMissingUpstreamSessionInternalError)
}
return nil
}
func upstreamOIDCRefresh(ctx context.Context, s *psession.CustomSessionData, providerCache oidc.UpstreamIdentityProvidersLister) error {
if s.OIDC == nil || s.OIDC.UpstreamRefreshToken == "" {
return errorsx.WithStack(errMissingUpstreamSessionInternalError)
}
p, err := findOIDCProviderByNameAndValidateUID(s, providerCache)
if err != nil {
return err
}
plog.Debug("attempting upstream refresh request",
"providerName", s.ProviderName, "providerType", s.ProviderType, "providerUID", s.ProviderUID)
refreshedTokens, err := p.PerformRefresh(ctx, s.OIDC.UpstreamRefreshToken)
if err != nil {
return errorsx.WithStack(errUpstreamRefreshError.WithHintf(
"Upstream refresh failed using provider %q of type %q.",
s.ProviderName, s.ProviderType).WithWrap(err))
}
// Upstream refresh may or may not return a new ID token. From the spec:
// "the response body is the Token Response of Section 3.1.3.3 except that it might not contain an id_token."
// https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokenResponse
_, hasIDTok := refreshedTokens.Extra("id_token").(string)
if hasIDTok {
// The spec is not 100% clear about whether an ID token from the refresh flow should include a nonce, and at
// least some providers do not include one, so we skip the nonce validation here (but not other validations).
_, err = p.ValidateToken(ctx, refreshedTokens, "")
if err != nil {
return errorsx.WithStack(errUpstreamRefreshError.WithHintf(
"Upstream refresh returned an invalid ID token using provider %q of type %q.",
s.ProviderName, s.ProviderType).WithWrap(err))
}
} else {
plog.Debug("upstream refresh request did not return a new ID token",
"providerName", s.ProviderName, "providerType", s.ProviderType, "providerUID", s.ProviderUID)
}
// Upstream refresh may or may not return a new refresh token. If we got a new refresh token, then update it in
// the user's session. If we did not get a new refresh token, then keep the old one in the session by avoiding
// overwriting the old one.
if refreshedTokens.RefreshToken != "" {
plog.Debug("upstream refresh request did not return a new refresh token",
"providerName", s.ProviderName, "providerType", s.ProviderType, "providerUID", s.ProviderUID)
s.OIDC.UpstreamRefreshToken = refreshedTokens.RefreshToken
}
return nil
}
func findOIDCProviderByNameAndValidateUID(
s *psession.CustomSessionData,
providerCache oidc.UpstreamIdentityProvidersLister,
) (provider.UpstreamOIDCIdentityProviderI, error) {
for _, p := range providerCache.GetOIDCIdentityProviders() {
if p.GetName() == s.ProviderName {
if p.GetResourceUID() != s.ProviderUID {
return nil, errorsx.WithStack(errUpstreamRefreshError.WithHint(
"Provider from upstream session data has changed its resource UID since authentication."))
}
return p, nil
}
}
return nil, errorsx.WithStack(errUpstreamRefreshError.
WithHintf("Provider %q of type %q from upstream session data was not found.", s.ProviderName, s.ProviderType))
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,108 @@
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package psession
import (
"time"
"github.com/mohae/deepcopy"
"github.com/ory/fosite"
"github.com/ory/fosite/handler/openid"
"github.com/ory/fosite/token/jwt"
"k8s.io/apimachinery/pkg/types"
)
// PinnipedSession is a session container which includes the fosite standard stuff plus custom Pinniped stuff.
type PinnipedSession struct {
// Delegate most things to the standard fosite OpenID JWT session.
Fosite *openid.DefaultSession `json:"fosite,omitempty"`
// Custom Pinniped extensions to the session data.
Custom *CustomSessionData `json:"custom,omitempty"`
}
var _ openid.Session = &PinnipedSession{}
// 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.
type CustomSessionData struct {
// 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
// a different identity provider CRD which just happens to have the same name.
// This implies that when a user deletes an identity provider CRD, then the sessions that were started
// using that identity provider will not be able to perform any more downstream refreshes.
ProviderUID types.UID `json:"providerUID"`
// The Kubernetes resource name of the identity provider CRD for the upstream IDP used to start this session.
// Used during a downstream refresh to decide which upstream to refresh.
// Also used to decide which of the pointer types below should be used.
ProviderName string `json:"providerName"`
// The type of the identity provider for the upstream IDP used to start this session.
// Used during a downstream refresh to decide which upstream to refresh.
ProviderType ProviderType `json:"providerType"`
// Only used when ProviderType == "oidc".
OIDC *OIDCSessionData `json:"oidc,omitempty"`
}
type ProviderType string
const (
ProviderTypeOIDC ProviderType = "oidc"
ProviderTypeLDAP ProviderType = "ldap"
ProviderTypeActiveDirectory ProviderType = "activedirectory"
)
// OIDCSessionData is the additional data needed by Pinniped when the upstream IDP is an OIDC provider.
type OIDCSessionData struct {
UpstreamRefreshToken string `json:"upstreamRefreshToken"`
}
// NewPinnipedSession returns a new empty session.
func NewPinnipedSession() *PinnipedSession {
return &PinnipedSession{
Fosite: &openid.DefaultSession{
Claims: &jwt.IDTokenClaims{},
Headers: &jwt.Headers{},
},
Custom: &CustomSessionData{},
}
}
func (s *PinnipedSession) Clone() fosite.Session {
// Implementation copied from openid.DefaultSession's clone method.
if s == nil {
return nil
}
return deepcopy.Copy(s).(fosite.Session)
}
func (s *PinnipedSession) SetExpiresAt(key fosite.TokenType, exp time.Time) {
s.Fosite.SetExpiresAt(key, exp)
}
func (s *PinnipedSession) GetExpiresAt(key fosite.TokenType) time.Time {
return s.Fosite.GetExpiresAt(key)
}
func (s *PinnipedSession) GetUsername() string {
return s.Fosite.GetUsername()
}
func (s *PinnipedSession) SetSubject(subject string) {
s.Fosite.SetSubject(subject)
}
func (s *PinnipedSession) GetSubject() string {
return s.Fosite.GetSubject()
}
func (s *PinnipedSession) IDTokenHeaders() *jwt.Headers {
return s.Fosite.IDTokenHeaders()
}
func (s *PinnipedSession) IDTokenClaims() *jwt.IDTokenClaims {
return s.Fosite.IDTokenClaims()
}

View File

@ -16,11 +16,11 @@ import (
coreosoidc "github.com/coreos/go-oidc/v3/oidc" coreosoidc "github.com/coreos/go-oidc/v3/oidc"
"github.com/ory/fosite" "github.com/ory/fosite"
"github.com/ory/fosite/handler/openid"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"gopkg.in/square/go-jose.v2" "gopkg.in/square/go-jose.v2"
"k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apiserver/pkg/authentication/authenticator" "k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/client-go/kubernetes/fake" "k8s.io/client-go/kubernetes/fake"
v1 "k8s.io/client-go/kubernetes/typed/core/v1" v1 "k8s.io/client-go/kubernetes/typed/core/v1"
@ -31,6 +31,7 @@ import (
pkce2 "go.pinniped.dev/internal/fositestorage/pkce" pkce2 "go.pinniped.dev/internal/fositestorage/pkce"
"go.pinniped.dev/internal/fositestoragei" "go.pinniped.dev/internal/fositestoragei"
"go.pinniped.dev/internal/oidc/provider" "go.pinniped.dev/internal/oidc/provider"
"go.pinniped.dev/internal/psession"
"go.pinniped.dev/internal/testutil" "go.pinniped.dev/internal/testutil"
"go.pinniped.dev/pkg/oidcclient/nonce" "go.pinniped.dev/pkg/oidcclient/nonce"
"go.pinniped.dev/pkg/oidcclient/oidctypes" "go.pinniped.dev/pkg/oidcclient/oidctypes"
@ -57,14 +58,34 @@ type PasswordCredentialsGrantAndValidateTokensArgs struct {
Password string Password string
} }
// PerformRefreshArgs is used to spy on calls to
// TestUpstreamOIDCIdentityProvider.PerformRefreshFunc().
type PerformRefreshArgs struct {
Ctx context.Context
RefreshToken string
}
// ValidateTokenArgs is used to spy on calls to
// TestUpstreamOIDCIdentityProvider.ValidateTokenFunc().
type ValidateTokenArgs struct {
Ctx context.Context
Tok *oauth2.Token
ExpectedIDTokenNonce nonce.Nonce
}
type TestUpstreamLDAPIdentityProvider struct { type TestUpstreamLDAPIdentityProvider struct {
Name string Name string
ResourceUID types.UID
URL *url.URL URL *url.URL
AuthenticateFunc func(ctx context.Context, username, password string) (*authenticator.Response, bool, error) AuthenticateFunc func(ctx context.Context, username, password string) (*authenticator.Response, bool, error)
} }
var _ provider.UpstreamLDAPIdentityProviderI = &TestUpstreamLDAPIdentityProvider{} var _ provider.UpstreamLDAPIdentityProviderI = &TestUpstreamLDAPIdentityProvider{}
func (u *TestUpstreamLDAPIdentityProvider) GetResourceUID() types.UID {
return u.ResourceUID
}
func (u *TestUpstreamLDAPIdentityProvider) GetName() string { func (u *TestUpstreamLDAPIdentityProvider) GetName() string {
return u.Name return u.Name
} }
@ -78,13 +99,15 @@ func (u *TestUpstreamLDAPIdentityProvider) GetURL() *url.URL {
} }
type TestUpstreamOIDCIdentityProvider struct { type TestUpstreamOIDCIdentityProvider struct {
Name string Name string
ClientID string ClientID string
AuthorizationURL url.URL ResourceUID types.UID
UsernameClaim string AuthorizationURL url.URL
GroupsClaim string UsernameClaim string
Scopes []string GroupsClaim string
AllowPasswordGrant bool Scopes []string
AdditionalAuthcodeParams map[string]string
AllowPasswordGrant bool
ExchangeAuthcodeAndValidateTokensFunc func( ExchangeAuthcodeAndValidateTokensFunc func(
ctx context.Context, ctx context.Context,
@ -99,10 +122,28 @@ type TestUpstreamOIDCIdentityProvider struct {
password string, password string,
) (*oidctypes.Token, error) ) (*oidctypes.Token, error)
PerformRefreshFunc func(ctx context.Context, refreshToken string) (*oauth2.Token, error)
ValidateTokenFunc func(ctx context.Context, tok *oauth2.Token, expectedIDTokenNonce nonce.Nonce) (*oidctypes.Token, error)
exchangeAuthcodeAndValidateTokensCallCount int exchangeAuthcodeAndValidateTokensCallCount int
exchangeAuthcodeAndValidateTokensArgs []*ExchangeAuthcodeAndValidateTokenArgs exchangeAuthcodeAndValidateTokensArgs []*ExchangeAuthcodeAndValidateTokenArgs
passwordCredentialsGrantAndValidateTokensCallCount int passwordCredentialsGrantAndValidateTokensCallCount int
passwordCredentialsGrantAndValidateTokensArgs []*PasswordCredentialsGrantAndValidateTokensArgs passwordCredentialsGrantAndValidateTokensArgs []*PasswordCredentialsGrantAndValidateTokensArgs
performRefreshCallCount int
performRefreshArgs []*PerformRefreshArgs
validateTokenCallCount int
validateTokenArgs []*ValidateTokenArgs
}
var _ provider.UpstreamOIDCIdentityProviderI = &TestUpstreamOIDCIdentityProvider{}
func (u *TestUpstreamOIDCIdentityProvider) GetResourceUID() types.UID {
return u.ResourceUID
}
func (u *TestUpstreamOIDCIdentityProvider) GetAdditionalAuthcodeParams() map[string]string {
return u.AdditionalAuthcodeParams
} }
func (u *TestUpstreamOIDCIdentityProvider) GetName() string { func (u *TestUpstreamOIDCIdentityProvider) GetName() string {
@ -175,8 +216,51 @@ func (u *TestUpstreamOIDCIdentityProvider) ExchangeAuthcodeAndValidateTokensArgs
return u.exchangeAuthcodeAndValidateTokensArgs[call] return u.exchangeAuthcodeAndValidateTokensArgs[call]
} }
func (u *TestUpstreamOIDCIdentityProvider) ValidateToken(_ context.Context, _ *oauth2.Token, _ nonce.Nonce) (*oidctypes.Token, error) { func (u *TestUpstreamOIDCIdentityProvider) PerformRefresh(ctx context.Context, refreshToken string) (*oauth2.Token, error) {
panic("implement me") if u.performRefreshArgs == nil {
u.performRefreshArgs = make([]*PerformRefreshArgs, 0)
}
u.performRefreshCallCount++
u.performRefreshArgs = append(u.performRefreshArgs, &PerformRefreshArgs{
Ctx: ctx,
RefreshToken: refreshToken,
})
return u.PerformRefreshFunc(ctx, refreshToken)
}
func (u *TestUpstreamOIDCIdentityProvider) PerformRefreshCallCount() int {
return u.performRefreshCallCount
}
func (u *TestUpstreamOIDCIdentityProvider) PerformRefreshArgs(call int) *PerformRefreshArgs {
if u.performRefreshArgs == nil {
u.performRefreshArgs = make([]*PerformRefreshArgs, 0)
}
return u.performRefreshArgs[call]
}
func (u *TestUpstreamOIDCIdentityProvider) ValidateToken(ctx context.Context, tok *oauth2.Token, expectedIDTokenNonce nonce.Nonce) (*oidctypes.Token, error) {
if u.validateTokenArgs == nil {
u.validateTokenArgs = make([]*ValidateTokenArgs, 0)
}
u.validateTokenCallCount++
u.validateTokenArgs = append(u.validateTokenArgs, &ValidateTokenArgs{
Ctx: ctx,
Tok: tok,
ExpectedIDTokenNonce: expectedIDTokenNonce,
})
return u.ValidateTokenFunc(ctx, tok, expectedIDTokenNonce)
}
func (u *TestUpstreamOIDCIdentityProvider) ValidateTokenCallCount() int {
return u.validateTokenCallCount
}
func (u *TestUpstreamOIDCIdentityProvider) ValidateTokenArgs(call int) *ValidateTokenArgs {
if u.validateTokenArgs == nil {
u.validateTokenArgs = make([]*ValidateTokenArgs, 0)
}
return u.validateTokenArgs[call]
} }
type UpstreamIDPListerBuilder struct { type UpstreamIDPListerBuilder struct {
@ -298,21 +382,102 @@ func (b *UpstreamIDPListerBuilder) RequireExactlyZeroCallsToExchangeAuthcodeAndV
) )
} }
func (b *UpstreamIDPListerBuilder) RequireExactlyOneCallToPerformRefresh(
t *testing.T,
expectedPerformedByUpstreamName string,
expectedArgs *PerformRefreshArgs,
) {
t.Helper()
var actualArgs *PerformRefreshArgs
var actualNameOfUpstreamWhichMadeCall string
actualCallCountAcrossAllOIDCUpstreams := 0
for _, upstreamOIDC := range b.upstreamOIDCIdentityProviders {
callCountOnThisUpstream := upstreamOIDC.performRefreshCallCount
actualCallCountAcrossAllOIDCUpstreams += callCountOnThisUpstream
if callCountOnThisUpstream == 1 {
actualNameOfUpstreamWhichMadeCall = upstreamOIDC.Name
actualArgs = upstreamOIDC.performRefreshArgs[0]
}
}
require.Equal(t, 1, actualCallCountAcrossAllOIDCUpstreams,
"should have been exactly one call to PerformRefresh() by all OIDC upstreams",
)
require.Equal(t, expectedPerformedByUpstreamName, actualNameOfUpstreamWhichMadeCall,
"PerformRefresh() was called on the wrong OIDC upstream",
)
require.Equal(t, expectedArgs, actualArgs)
}
func (b *UpstreamIDPListerBuilder) RequireExactlyZeroCallsToPerformRefresh(t *testing.T) {
t.Helper()
actualCallCountAcrossAllOIDCUpstreams := 0
for _, upstreamOIDC := range b.upstreamOIDCIdentityProviders {
actualCallCountAcrossAllOIDCUpstreams += upstreamOIDC.performRefreshCallCount
}
require.Equal(t, 0, actualCallCountAcrossAllOIDCUpstreams,
"expected exactly zero calls to PerformRefresh()",
)
}
func (b *UpstreamIDPListerBuilder) RequireExactlyOneCallToValidateToken(
t *testing.T,
expectedPerformedByUpstreamName string,
expectedArgs *ValidateTokenArgs,
) {
t.Helper()
var actualArgs *ValidateTokenArgs
var actualNameOfUpstreamWhichMadeCall string
actualCallCountAcrossAllOIDCUpstreams := 0
for _, upstreamOIDC := range b.upstreamOIDCIdentityProviders {
callCountOnThisUpstream := upstreamOIDC.validateTokenCallCount
actualCallCountAcrossAllOIDCUpstreams += callCountOnThisUpstream
if callCountOnThisUpstream == 1 {
actualNameOfUpstreamWhichMadeCall = upstreamOIDC.Name
actualArgs = upstreamOIDC.validateTokenArgs[0]
}
}
require.Equal(t, 1, actualCallCountAcrossAllOIDCUpstreams,
"should have been exactly one call to ValidateToken() by all OIDC upstreams",
)
require.Equal(t, expectedPerformedByUpstreamName, actualNameOfUpstreamWhichMadeCall,
"ValidateToken() was called on the wrong OIDC upstream",
)
require.Equal(t, expectedArgs, actualArgs)
}
func (b *UpstreamIDPListerBuilder) RequireExactlyZeroCallsToValidateToken(t *testing.T) {
t.Helper()
actualCallCountAcrossAllOIDCUpstreams := 0
for _, upstreamOIDC := range b.upstreamOIDCIdentityProviders {
actualCallCountAcrossAllOIDCUpstreams += upstreamOIDC.validateTokenCallCount
}
require.Equal(t, 0, actualCallCountAcrossAllOIDCUpstreams,
"expected exactly zero calls to ValidateToken()",
)
}
func NewUpstreamIDPListerBuilder() *UpstreamIDPListerBuilder { func NewUpstreamIDPListerBuilder() *UpstreamIDPListerBuilder {
return &UpstreamIDPListerBuilder{} return &UpstreamIDPListerBuilder{}
} }
type TestUpstreamOIDCIdentityProviderBuilder struct { type TestUpstreamOIDCIdentityProviderBuilder struct {
name string name string
clientID string resourceUID types.UID
scopes []string clientID string
idToken map[string]interface{} scopes []string
usernameClaim string idToken map[string]interface{}
groupsClaim string refreshToken *oidctypes.RefreshToken
authorizationURL url.URL usernameClaim string
allowPasswordGrant bool groupsClaim string
authcodeExchangeErr error refreshedTokens *oauth2.Token
passwordGrantErr error validatedTokens *oidctypes.Token
authorizationURL url.URL
additionalAuthcodeParams map[string]string
allowPasswordGrant bool
authcodeExchangeErr error
passwordGrantErr error
performRefreshErr error
validateTokenErr error
} }
func (u *TestUpstreamOIDCIdentityProviderBuilder) WithName(value string) *TestUpstreamOIDCIdentityProviderBuilder { func (u *TestUpstreamOIDCIdentityProviderBuilder) WithName(value string) *TestUpstreamOIDCIdentityProviderBuilder {
@ -320,6 +485,11 @@ func (u *TestUpstreamOIDCIdentityProviderBuilder) WithName(value string) *TestUp
return u return u
} }
func (u *TestUpstreamOIDCIdentityProviderBuilder) WithResourceUID(value types.UID) *TestUpstreamOIDCIdentityProviderBuilder {
u.resourceUID = value
return u
}
func (u *TestUpstreamOIDCIdentityProviderBuilder) WithClientID(value string) *TestUpstreamOIDCIdentityProviderBuilder { func (u *TestUpstreamOIDCIdentityProviderBuilder) WithClientID(value string) *TestUpstreamOIDCIdentityProviderBuilder {
u.clientID = value u.clientID = value
return u return u
@ -373,6 +543,26 @@ func (u *TestUpstreamOIDCIdentityProviderBuilder) WithoutIDTokenClaim(claim stri
return u return u
} }
func (u *TestUpstreamOIDCIdentityProviderBuilder) WithAdditionalAuthcodeParams(params map[string]string) *TestUpstreamOIDCIdentityProviderBuilder {
u.additionalAuthcodeParams = params
return u
}
func (u *TestUpstreamOIDCIdentityProviderBuilder) WithRefreshToken(token string) *TestUpstreamOIDCIdentityProviderBuilder {
u.refreshToken = &oidctypes.RefreshToken{Token: token}
return u
}
func (u *TestUpstreamOIDCIdentityProviderBuilder) WithEmptyRefreshToken() *TestUpstreamOIDCIdentityProviderBuilder {
u.refreshToken = &oidctypes.RefreshToken{Token: ""}
return u
}
func (u *TestUpstreamOIDCIdentityProviderBuilder) WithoutRefreshToken() *TestUpstreamOIDCIdentityProviderBuilder {
u.refreshToken = nil
return u
}
func (u *TestUpstreamOIDCIdentityProviderBuilder) WithUpstreamAuthcodeExchangeError(err error) *TestUpstreamOIDCIdentityProviderBuilder { func (u *TestUpstreamOIDCIdentityProviderBuilder) WithUpstreamAuthcodeExchangeError(err error) *TestUpstreamOIDCIdentityProviderBuilder {
u.authcodeExchangeErr = err u.authcodeExchangeErr = err
return u return u
@ -383,26 +573,60 @@ func (u *TestUpstreamOIDCIdentityProviderBuilder) WithPasswordGrantError(err err
return u return u
} }
func (u *TestUpstreamOIDCIdentityProviderBuilder) WithRefreshedTokens(tokens *oauth2.Token) *TestUpstreamOIDCIdentityProviderBuilder {
u.refreshedTokens = tokens
return u
}
func (u *TestUpstreamOIDCIdentityProviderBuilder) WithPerformRefreshError(err error) *TestUpstreamOIDCIdentityProviderBuilder {
u.performRefreshErr = err
return u
}
func (u *TestUpstreamOIDCIdentityProviderBuilder) WithValidatedTokens(tokens *oidctypes.Token) *TestUpstreamOIDCIdentityProviderBuilder {
u.validatedTokens = tokens
return u
}
func (u *TestUpstreamOIDCIdentityProviderBuilder) WithValidateTokenError(err error) *TestUpstreamOIDCIdentityProviderBuilder {
u.validateTokenErr = err
return u
}
func (u *TestUpstreamOIDCIdentityProviderBuilder) Build() *TestUpstreamOIDCIdentityProvider { func (u *TestUpstreamOIDCIdentityProviderBuilder) Build() *TestUpstreamOIDCIdentityProvider {
return &TestUpstreamOIDCIdentityProvider{ return &TestUpstreamOIDCIdentityProvider{
Name: u.name, Name: u.name,
ClientID: u.clientID, ClientID: u.clientID,
UsernameClaim: u.usernameClaim, ResourceUID: u.resourceUID,
GroupsClaim: u.groupsClaim, UsernameClaim: u.usernameClaim,
Scopes: u.scopes, GroupsClaim: u.groupsClaim,
AllowPasswordGrant: u.allowPasswordGrant, Scopes: u.scopes,
AuthorizationURL: u.authorizationURL, AllowPasswordGrant: u.allowPasswordGrant,
AuthorizationURL: u.authorizationURL,
AdditionalAuthcodeParams: u.additionalAuthcodeParams,
ExchangeAuthcodeAndValidateTokensFunc: func(ctx context.Context, authcode string, pkceCodeVerifier pkce.Code, expectedIDTokenNonce nonce.Nonce) (*oidctypes.Token, error) { ExchangeAuthcodeAndValidateTokensFunc: func(ctx context.Context, authcode string, pkceCodeVerifier pkce.Code, expectedIDTokenNonce nonce.Nonce) (*oidctypes.Token, error) {
if u.authcodeExchangeErr != nil { if u.authcodeExchangeErr != nil {
return nil, u.authcodeExchangeErr return nil, u.authcodeExchangeErr
} }
return &oidctypes.Token{IDToken: &oidctypes.IDToken{Claims: u.idToken}}, nil return &oidctypes.Token{IDToken: &oidctypes.IDToken{Claims: u.idToken}, RefreshToken: u.refreshToken}, nil
}, },
PasswordCredentialsGrantAndValidateTokensFunc: func(ctx context.Context, username, password string) (*oidctypes.Token, error) { PasswordCredentialsGrantAndValidateTokensFunc: func(ctx context.Context, username, password string) (*oidctypes.Token, error) {
if u.passwordGrantErr != nil { if u.passwordGrantErr != nil {
return nil, u.passwordGrantErr return nil, u.passwordGrantErr
} }
return &oidctypes.Token{IDToken: &oidctypes.IDToken{Claims: u.idToken}}, nil return &oidctypes.Token{IDToken: &oidctypes.IDToken{Claims: u.idToken}, RefreshToken: u.refreshToken}, nil
},
PerformRefreshFunc: func(ctx context.Context, refreshToken string) (*oauth2.Token, error) {
if u.performRefreshErr != nil {
return nil, u.performRefreshErr
}
return u.refreshedTokens, nil
},
ValidateTokenFunc: func(ctx context.Context, tok *oauth2.Token, expectedIDTokenNonce nonce.Nonce) (*oidctypes.Token, error) {
if u.validateTokenErr != nil {
return nil, u.validateTokenErr
}
return u.validatedTokens, nil
}, },
} }
} }
@ -479,6 +703,7 @@ func RequireAuthCodeRegexpMatch(
wantDownstreamNonce string, wantDownstreamNonce string,
wantDownstreamClientID string, wantDownstreamClientID string,
wantDownstreamRedirectURI string, wantDownstreamRedirectURI string,
wantCustomSessionData *psession.CustomSessionData,
) { ) {
t.Helper() t.Helper()
@ -513,6 +738,7 @@ func RequireAuthCodeRegexpMatch(
wantDownstreamRequestedScopes, wantDownstreamRequestedScopes,
wantDownstreamClientID, wantDownstreamClientID,
wantDownstreamRedirectURI, wantDownstreamRedirectURI,
wantCustomSessionData,
) )
// One PKCE should have been stored. // One PKCE should have been stored.
@ -563,7 +789,8 @@ func validateAuthcodeStorage(
wantDownstreamRequestedScopes []string, wantDownstreamRequestedScopes []string,
wantDownstreamClientID string, wantDownstreamClientID string,
wantDownstreamRedirectURI string, wantDownstreamRedirectURI string,
) (*fosite.Request, *openid.DefaultSession) { wantCustomSessionData *psession.CustomSessionData,
) (*fosite.Request, *psession.PinnipedSession) {
t.Helper() t.Helper()
const ( const (
@ -591,16 +818,16 @@ func validateAuthcodeStorage(
testutil.RequireTimeInDelta(t, time.Now(), storedRequestFromAuthcode.RequestedAt, timeComparisonFudgeFactor) testutil.RequireTimeInDelta(t, time.Now(), storedRequestFromAuthcode.RequestedAt, timeComparisonFudgeFactor)
// We're not using these fields yet, so confirm that we did not set them (for now). // We're not using these fields yet, so confirm that we did not set them (for now).
require.Empty(t, storedSessionFromAuthcode.Subject) require.Empty(t, storedSessionFromAuthcode.Fosite.Subject)
require.Empty(t, storedSessionFromAuthcode.Username) require.Empty(t, storedSessionFromAuthcode.Fosite.Username)
require.Empty(t, storedSessionFromAuthcode.Headers) require.Empty(t, storedSessionFromAuthcode.Fosite.Headers)
// The authcode that we are issuing should be good for the length of time that we declare in the fosite config. // The authcode that we are issuing should be good for the length of time that we declare in the fosite config.
testutil.RequireTimeInDelta(t, time.Now().Add(authCodeExpirationSeconds*time.Second), storedSessionFromAuthcode.ExpiresAt[fosite.AuthorizeCode], timeComparisonFudgeFactor) testutil.RequireTimeInDelta(t, time.Now().Add(authCodeExpirationSeconds*time.Second), storedSessionFromAuthcode.Fosite.ExpiresAt[fosite.AuthorizeCode], timeComparisonFudgeFactor)
require.Len(t, storedSessionFromAuthcode.ExpiresAt, 1) require.Len(t, storedSessionFromAuthcode.Fosite.ExpiresAt, 1)
// Now confirm the ID token claims. // Now confirm the ID token claims.
actualClaims := storedSessionFromAuthcode.Claims actualClaims := storedSessionFromAuthcode.Fosite.Claims
// 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)
@ -634,6 +861,9 @@ func validateAuthcodeStorage(
require.Empty(t, actualClaims.AuthenticationContextClassReference) require.Empty(t, actualClaims.AuthenticationContextClassReference)
require.Empty(t, actualClaims.AuthenticationMethodsReference) require.Empty(t, actualClaims.AuthenticationMethodsReference)
// Check that the custom Pinniped session data matches.
require.Equal(t, wantCustomSessionData, storedSessionFromAuthcode.Custom)
return storedRequestFromAuthcode, storedSessionFromAuthcode return storedRequestFromAuthcode, storedSessionFromAuthcode
} }
@ -642,7 +872,7 @@ func validatePKCEStorage(
oauthStore fositestoragei.AllFositeStorage, oauthStore fositestoragei.AllFositeStorage,
storeKey string, storeKey string,
storedRequestFromAuthcode *fosite.Request, storedRequestFromAuthcode *fosite.Request,
storedSessionFromAuthcode *openid.DefaultSession, storedSessionFromAuthcode *psession.PinnipedSession,
wantDownstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod string, wantDownstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod string,
) { ) {
t.Helper() t.Helper()
@ -667,7 +897,7 @@ func validateIDSessionStorage(
oauthStore fositestoragei.AllFositeStorage, oauthStore fositestoragei.AllFositeStorage,
storeKey string, storeKey string,
storedRequestFromAuthcode *fosite.Request, storedRequestFromAuthcode *fosite.Request,
storedSessionFromAuthcode *openid.DefaultSession, storedSessionFromAuthcode *psession.PinnipedSession,
wantDownstreamNonce string, wantDownstreamNonce string,
) { ) {
t.Helper() t.Helper()
@ -686,13 +916,13 @@ func validateIDSessionStorage(
require.Equal(t, wantDownstreamNonce, storedRequestFromIDSession.Form.Get("nonce")) require.Equal(t, wantDownstreamNonce, storedRequestFromIDSession.Form.Get("nonce"))
} }
func castStoredAuthorizeRequest(t *testing.T, storedAuthorizeRequest fosite.Requester) (*fosite.Request, *openid.DefaultSession) { func castStoredAuthorizeRequest(t *testing.T, storedAuthorizeRequest fosite.Requester) (*fosite.Request, *psession.PinnipedSession) {
t.Helper() t.Helper()
storedRequest, ok := storedAuthorizeRequest.(*fosite.Request) storedRequest, ok := storedAuthorizeRequest.(*fosite.Request)
require.Truef(t, ok, "could not cast %T to %T", storedAuthorizeRequest, &fosite.Request{}) require.Truef(t, ok, "could not cast %T to %T", storedAuthorizeRequest, &fosite.Request{})
storedSession, ok := storedAuthorizeRequest.GetSession().(*openid.DefaultSession) storedSession, ok := storedAuthorizeRequest.GetSession().(*psession.PinnipedSession)
require.Truef(t, ok, "could not cast %T to %T", storedAuthorizeRequest.GetSession(), &openid.DefaultSession{}) require.Truef(t, ok, "could not cast %T to %T", storedAuthorizeRequest.GetSession(), &psession.PinnipedSession{})
return storedRequest, storedSession return storedRequest, storedSession
} }

View File

@ -0,0 +1,43 @@
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package testutil
import (
"testing"
"github.com/ory/fosite/handler/openid"
v1 "k8s.io/api/core/v1"
"k8s.io/client-go/kubernetes/fake"
testing2 "k8s.io/client-go/testing"
"go.pinniped.dev/internal/psession"
)
func NewFakePinnipedSession() *psession.PinnipedSession {
return &psession.PinnipedSession{
Fosite: &openid.DefaultSession{
Claims: nil,
Headers: nil,
ExpiresAt: nil,
Username: "snorlax",
Subject: "panda",
},
Custom: &psession.CustomSessionData{
ProviderUID: "fake-provider-uid",
ProviderType: "fake-provider-type",
ProviderName: "fake-provider-name",
OIDC: &psession.OIDCSessionData{
UpstreamRefreshToken: "fake-upstream-refresh-token",
},
},
}
}
func LogActualJSONFromCreateAction(t *testing.T, client *fake.Clientset, actionIndex int) {
t.Log("actual value of CreateAction secret data", string(client.Actions()[actionIndex].(testing2.CreateActionImpl).Object.(*v1.Secret).Data["pinniped-storage-data"]))
}
func LogActualJSONFromUpdateAction(t *testing.T, client *fake.Clientset, actionIndex int) {
t.Log("actual value of UpdateAction secret data", string(client.Actions()[actionIndex].(testing2.UpdateActionImpl).Object.(*v1.Secret).Data["pinniped-storage-data"]))
}

View File

@ -20,6 +20,7 @@ import (
"github.com/go-ldap/ldap/v3" "github.com/go-ldap/ldap/v3"
"github.com/google/uuid" "github.com/google/uuid"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apiserver/pkg/authentication/authenticator" "k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/authentication/user" "k8s.io/apiserver/pkg/authentication/user"
"k8s.io/utils/trace" "k8s.io/utils/trace"
@ -82,6 +83,9 @@ type ProviderConfig struct {
// Name is the unique name of this upstream LDAP IDP. // Name is the unique name of this upstream LDAP IDP.
Name string Name string
// ResourceUID is the Kubernetes resource UID of this identity provider.
ResourceUID types.UID
// Host is the hostname or "hostname:port" of the LDAP server. When the port is not specified, // Host is the hostname or "hostname:port" of the LDAP server. When the port is not specified,
// the default LDAP port will be used. // the default LDAP port will be used.
Host string Host string
@ -265,6 +269,10 @@ func (p *Provider) GetName() string {
return p.c.Name return p.c.Name
} }
func (p *Provider) GetResourceUID() types.UID {
return p.c.ResourceUID
}
// Return a URL which uniquely identifies this LDAP provider, e.g. "ldaps://host.example.com:1234?base=user-search-base". // Return a URL which uniquely identifies this LDAP provider, e.g. "ldaps://host.example.com:1234?base=user-search-base".
// This URL is not used for connecting to the provider, but rather is used for creating a globally unique user // This URL is not used for connecting to the provider, but rather is used for creating a globally unique user
// identifier by being combined with the user's UID, since user UIDs are only unique within one provider. // identifier by being combined with the user's UID, since user UIDs are only unique within one provider.

View File

@ -14,6 +14,7 @@ import (
coreosoidc "github.com/coreos/go-oidc/v3/oidc" coreosoidc "github.com/coreos/go-oidc/v3/oidc"
"golang.org/x/oauth2" "golang.org/x/oauth2"
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/util/sets" "k8s.io/apimachinery/pkg/util/sets"
"go.pinniped.dev/internal/httputil/httperr" "go.pinniped.dev/internal/httputil/httperr"
@ -31,19 +32,31 @@ func New(config *oauth2.Config, provider *coreosoidc.Provider, client *http.Clie
// ProviderConfig holds the active configuration of an upstream OIDC provider. // ProviderConfig holds the active configuration of an upstream OIDC provider.
type ProviderConfig struct { type ProviderConfig struct {
Name string Name string
UsernameClaim string ResourceUID types.UID
GroupsClaim string UsernameClaim string
Config *oauth2.Config GroupsClaim string
Client *http.Client Config *oauth2.Config
AllowPasswordGrant bool Client *http.Client
Provider interface { AllowPasswordGrant bool
AdditionalAuthcodeParams map[string]string
Provider interface {
Verifier(*coreosoidc.Config) *coreosoidc.IDTokenVerifier Verifier(*coreosoidc.Config) *coreosoidc.IDTokenVerifier
Claims(v interface{}) error Claims(v interface{}) error
UserInfo(ctx context.Context, tokenSource oauth2.TokenSource) (*coreosoidc.UserInfo, error) UserInfo(ctx context.Context, tokenSource oauth2.TokenSource) (*coreosoidc.UserInfo, error)
} }
} }
var _ provider.UpstreamOIDCIdentityProviderI = (*ProviderConfig)(nil)
func (p *ProviderConfig) GetResourceUID() types.UID {
return p.ResourceUID
}
func (p *ProviderConfig) GetAdditionalAuthcodeParams() map[string]string {
return p.AdditionalAuthcodeParams
}
func (p *ProviderConfig) GetName() string { func (p *ProviderConfig) GetName() string {
return p.Name return p.Name
} }
@ -109,6 +122,16 @@ func (p *ProviderConfig) ExchangeAuthcodeAndValidateTokens(ctx context.Context,
return p.ValidateToken(ctx, tok, expectedIDTokenNonce) return p.ValidateToken(ctx, tok, expectedIDTokenNonce)
} }
func (p *ProviderConfig) PerformRefresh(ctx context.Context, refreshToken string) (*oauth2.Token, error) {
// Use the provided HTTP client to benefit from its CA, proxy, and other settings.
httpClientContext := coreosoidc.ClientContext(ctx, p.Client)
// Create a TokenSource without an access token, so it thinks that a refresh is immediately required.
// Then ask it for the tokens to cause it to perform the refresh and return the results.
return p.Config.TokenSource(httpClientContext, &oauth2.Token{RefreshToken: refreshToken}).Token()
}
// ValidateToken will validate the ID token. It will also merge the claims from the userinfo endpoint response,
// if the provider offers the userinfo endpoint.
func (p *ProviderConfig) ValidateToken(ctx context.Context, tok *oauth2.Token, expectedIDTokenNonce nonce.Nonce) (*oidctypes.Token, error) { func (p *ProviderConfig) ValidateToken(ctx context.Context, tok *oauth2.Token, expectedIDTokenNonce nonce.Nonce) (*oidctypes.Token, error) {
idTok, hasIDTok := tok.Extra("id_token").(string) idTok, hasIDTok := tok.Extra("id_token").(string)
if !hasIDTok { if !hasIDTok {
@ -135,7 +158,7 @@ func (p *ProviderConfig) ValidateToken(ctx context.Context, tok *oauth2.Token, e
} }
maybeLogClaims("claims from ID token", p.Name, validatedClaims) maybeLogClaims("claims from ID token", p.Name, validatedClaims)
if err := p.fetchUserInfo(ctx, tok, validatedClaims); err != nil { if err := p.maybeFetchUserInfoAndMergeClaims(ctx, tok, validatedClaims); err != nil {
return nil, httperr.Wrap(http.StatusInternalServerError, "could not fetch user info claims", err) return nil, httperr.Wrap(http.StatusInternalServerError, "could not fetch user info claims", err)
} }
@ -156,7 +179,7 @@ func (p *ProviderConfig) ValidateToken(ctx context.Context, tok *oauth2.Token, e
}, nil }, nil
} }
func (p *ProviderConfig) fetchUserInfo(ctx context.Context, tok *oauth2.Token, claims map[string]interface{}) error { func (p *ProviderConfig) maybeFetchUserInfoAndMergeClaims(ctx context.Context, tok *oauth2.Token, claims map[string]interface{}) error {
idTokenSubject, _ := claims[oidc.IDTokenSubjectClaim].(string) idTokenSubject, _ := claims[oidc.IDTokenSubjectClaim].(string)
if len(idTokenSubject) == 0 { if len(idTokenSubject) == 0 {
return nil // defer to existing ID token validation return nil // defer to existing ID token validation

View File

@ -23,6 +23,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"go.pinniped.dev/internal/mocks/mockkeyset" "go.pinniped.dev/internal/mocks/mockkeyset"
"go.pinniped.dev/internal/testutil"
"go.pinniped.dev/pkg/oidcclient/nonce" "go.pinniped.dev/pkg/oidcclient/nonce"
"go.pinniped.dev/pkg/oidcclient/oidctypes" "go.pinniped.dev/pkg/oidcclient/oidctypes"
) )
@ -288,6 +289,171 @@ func TestProviderConfig(t *testing.T) {
} }
}) })
t.Run("PerformRefresh", func(t *testing.T) {
tests := []struct {
name string
returnIDTok string
returnAccessTok string
returnRefreshTok string
returnTokType string
returnExpiresIn string
tokenStatusCode int
wantErr string
wantToken *oauth2.Token
wantTokenExtras map[string]interface{}
}{
{
name: "success when the server returns all tokens in the refresh result",
returnIDTok: "test-id-token",
returnAccessTok: "test-access-token",
returnRefreshTok: "test-refresh-token",
returnTokType: "test-token-type",
returnExpiresIn: "42",
tokenStatusCode: http.StatusOK,
wantToken: &oauth2.Token{
AccessToken: "test-access-token",
RefreshToken: "test-refresh-token",
TokenType: "test-token-type",
Expiry: time.Now().Add(42 * time.Second),
},
wantTokenExtras: map[string]interface{}{
// the ID token only appears in the extras map
"id_token": "test-id-token",
// the library also repeats all the other keys/values returned by the server in the raw extras map
"access_token": "test-access-token",
"refresh_token": "test-refresh-token",
"token_type": "test-token-type",
"expires_in": "42",
// the library also adds this zero-value even though the server did not return it
"expiry": "0001-01-01T00:00:00Z",
},
},
{
name: "success when the server does not return a new refresh token in the refresh result",
returnIDTok: "test-id-token",
returnAccessTok: "test-access-token",
returnRefreshTok: "",
returnTokType: "test-token-type",
returnExpiresIn: "42",
tokenStatusCode: http.StatusOK,
wantToken: &oauth2.Token{
AccessToken: "test-access-token",
// the library sets the original refresh token into the result, even though the server did not return that
RefreshToken: "test-initial-refresh-token",
TokenType: "test-token-type",
Expiry: time.Now().Add(42 * time.Second),
},
wantTokenExtras: map[string]interface{}{
// the ID token only appears in the extras map
"id_token": "test-id-token",
// the library also repeats all the other keys/values returned by the server in the raw extras map
"access_token": "test-access-token",
"token_type": "test-token-type",
"expires_in": "42",
// the library also adds this zero-value even though the server did not return it
"expiry": "0001-01-01T00:00:00Z",
},
},
{
name: "success when the server does not return a new ID token in the refresh result",
returnIDTok: "",
returnAccessTok: "test-access-token",
returnRefreshTok: "test-refresh-token",
returnTokType: "test-token-type",
returnExpiresIn: "42",
tokenStatusCode: http.StatusOK,
wantToken: &oauth2.Token{
AccessToken: "test-access-token",
RefreshToken: "test-refresh-token",
TokenType: "test-token-type",
Expiry: time.Now().Add(42 * time.Second),
},
wantTokenExtras: map[string]interface{}{
// the library also repeats all the other keys/values returned by the server in the raw extras map
"access_token": "test-access-token",
"refresh_token": "test-refresh-token",
"token_type": "test-token-type",
"expires_in": "42",
// the library also adds this zero-value even though the server did not return it
"expiry": "0001-01-01T00:00:00Z",
},
},
{
name: "server returns an error on token refresh",
tokenStatusCode: http.StatusForbidden,
wantErr: "oauth2: cannot fetch token: 403 Forbidden\nResponse: fake error\n",
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
tokenServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, http.MethodPost, r.Method)
require.NoError(t, r.ParseForm())
require.Equal(t, 4, len(r.Form))
require.Equal(t, "test-client-id", r.Form.Get("client_id"))
require.Equal(t, "test-client-secret", r.Form.Get("client_secret"))
require.Equal(t, "refresh_token", r.Form.Get("grant_type"))
require.Equal(t, "test-initial-refresh-token", r.Form.Get("refresh_token"))
if tt.tokenStatusCode != http.StatusOK {
http.Error(w, "fake error", tt.tokenStatusCode)
return
}
var response struct {
oauth2.Token
IDToken string `json:"id_token,omitempty"`
ExpiresIn string `json:"expires_in,omitempty"`
}
response.IDToken = tt.returnIDTok
response.AccessToken = tt.returnAccessTok
response.RefreshToken = tt.returnRefreshTok
response.TokenType = tt.returnTokType
response.ExpiresIn = tt.returnExpiresIn
w.Header().Set("content-type", "application/json")
require.NoError(t, json.NewEncoder(w).Encode(&response))
}))
t.Cleanup(tokenServer.Close)
p := ProviderConfig{
Name: "test-name",
UsernameClaim: "test-username-claim",
GroupsClaim: "test-groups-claim",
Config: &oauth2.Config{
ClientID: "test-client-id",
ClientSecret: "test-client-secret",
Endpoint: oauth2.Endpoint{
AuthURL: "https://example.com",
TokenURL: tokenServer.URL,
AuthStyle: oauth2.AuthStyleInParams,
},
Scopes: []string{"scope1", "scope2"},
},
}
tok, err := p.PerformRefresh(
context.Background(),
"test-initial-refresh-token",
)
if tt.wantErr != "" {
require.EqualError(t, err, tt.wantErr)
require.Nil(t, tok)
return
}
require.NoError(t, err)
require.Equal(t, tt.wantToken.TokenType, tok.TokenType)
require.Equal(t, tt.wantToken.RefreshToken, tok.RefreshToken)
require.Equal(t, tt.wantToken.AccessToken, tok.AccessToken)
testutil.RequireTimeInDelta(t, tt.wantToken.Expiry, tok.Expiry, 5*time.Second)
for k, v := range tt.wantTokenExtras {
require.Equal(t, v, tok.Extra(k))
}
})
}
})
t.Run("ExchangeAuthcodeAndValidateTokens", func(t *testing.T) { t.Run("ExchangeAuthcodeAndValidateTokens", func(t *testing.T) {
tests := []struct { tests := []struct {
name string name string

View File

@ -808,17 +808,18 @@ func (h *handlerState) tokenExchangeRFC8693(baseToken *oidctypes.Token) (*oidcty
func (h *handlerState) handleRefresh(ctx context.Context, refreshToken *oidctypes.RefreshToken) (*oidctypes.Token, error) { func (h *handlerState) handleRefresh(ctx context.Context, refreshToken *oidctypes.RefreshToken) (*oidctypes.Token, error) {
h.logger.V(debugLogLevel).Info("Pinniped: Refreshing cached token.") h.logger.V(debugLogLevel).Info("Pinniped: Refreshing cached token.")
refreshSource := h.oauth2Config.TokenSource(ctx, &oauth2.Token{RefreshToken: refreshToken.Token}) upstreamOIDCIdentityProvider := h.getProvider(h.oauth2Config, h.provider, h.httpClient)
refreshed, err := refreshSource.Token() refreshed, err := upstreamOIDCIdentityProvider.PerformRefresh(ctx, refreshToken.Token)
if err != nil { if err != nil {
// Ignore errors during refresh, but return nil which will trigger the full login flow. // Ignore errors during refresh, but return nil which will trigger the full login flow.
h.logger.V(debugLogLevel).Info("Pinniped: Refresh failed.", "error", err.Error())
return nil, nil return nil, nil
} }
// The spec is not 100% clear about whether an ID token from the refresh flow should include a nonce, and at least // The spec is not 100% clear about whether an ID token from the refresh flow should include a nonce, and at least
// some providers do not include one, so we skip the nonce validation here (but not other validations). // some providers do not include one, so we skip the nonce validation here (but not other validations).
return h.getProvider(h.oauth2Config, h.provider, h.httpClient).ValidateToken(ctx, refreshed, "") return upstreamOIDCIdentityProvider.ValidateToken(ctx, refreshed, "")
} }
func (h *handlerState) handleAuthCodeCallback(w http.ResponseWriter, r *http.Request) (err error) { func (h *handlerState) handleAuthCodeCallback(w http.ResponseWriter, r *http.Request) (err error) {

View File

@ -35,6 +35,7 @@ import (
"go.pinniped.dev/internal/oidc/provider" "go.pinniped.dev/internal/oidc/provider"
"go.pinniped.dev/internal/testutil" "go.pinniped.dev/internal/testutil"
"go.pinniped.dev/internal/testutil/testlogger" "go.pinniped.dev/internal/testutil/testlogger"
"go.pinniped.dev/internal/upstreamoidc"
"go.pinniped.dev/pkg/oidcclient/nonce" "go.pinniped.dev/pkg/oidcclient/nonce"
"go.pinniped.dev/pkg/oidcclient/oidctypes" "go.pinniped.dev/pkg/oidcclient/oidctypes"
"go.pinniped.dev/pkg/oidcclient/pkce" "go.pinniped.dev/pkg/oidcclient/pkce"
@ -404,11 +405,17 @@ func TestLogin(t *testing.T) { // nolint:gocyclo
clientID: "test-client-id", clientID: "test-client-id",
opt: func(t *testing.T) Option { opt: func(t *testing.T) Option {
return func(h *handlerState) error { return func(h *handlerState) error {
h.getProvider = func(_ *oauth2.Config, _ *oidc.Provider, _ *http.Client) provider.UpstreamOIDCIdentityProviderI { h.getProvider = func(config *oauth2.Config, provider *oidc.Provider, client *http.Client) provider.UpstreamOIDCIdentityProviderI {
mock := mockUpstream(t) mock := mockUpstream(t)
mock.EXPECT(). mock.EXPECT().
ValidateToken(gomock.Any(), HasAccessToken(testToken.AccessToken.Token), nonce.Nonce("")). ValidateToken(gomock.Any(), HasAccessToken(testToken.AccessToken.Token), nonce.Nonce("")).
Return(&testToken, nil) Return(&testToken, nil)
mock.EXPECT().
PerformRefresh(gomock.Any(), testToken.RefreshToken.Token).
DoAndReturn(func(ctx context.Context, refreshToken string) (*oauth2.Token, error) {
// Call the real production code to perform a refresh.
return upstreamoidc.New(config, provider, client).PerformRefresh(ctx, refreshToken)
})
return mock return mock
} }
@ -445,11 +452,17 @@ func TestLogin(t *testing.T) { // nolint:gocyclo
clientID: "test-client-id", clientID: "test-client-id",
opt: func(t *testing.T) Option { opt: func(t *testing.T) Option {
return func(h *handlerState) error { return func(h *handlerState) error {
h.getProvider = func(_ *oauth2.Config, _ *oidc.Provider, _ *http.Client) provider.UpstreamOIDCIdentityProviderI { h.getProvider = func(config *oauth2.Config, provider *oidc.Provider, client *http.Client) provider.UpstreamOIDCIdentityProviderI {
mock := mockUpstream(t) mock := mockUpstream(t)
mock.EXPECT(). mock.EXPECT().
ValidateToken(gomock.Any(), HasAccessToken(testToken.AccessToken.Token), nonce.Nonce("")). ValidateToken(gomock.Any(), HasAccessToken(testToken.AccessToken.Token), nonce.Nonce("")).
Return(nil, fmt.Errorf("some validation error")) Return(nil, fmt.Errorf("some validation error"))
mock.EXPECT().
PerformRefresh(gomock.Any(), "test-refresh-token-returning-invalid-id-token").
DoAndReturn(func(ctx context.Context, refreshToken string) (*oauth2.Token, error) {
// Call the real production code to perform a refresh.
return upstreamoidc.New(config, provider, client).PerformRefresh(ctx, refreshToken)
})
return mock return mock
} }
@ -500,6 +513,7 @@ func TestLogin(t *testing.T) { // nolint:gocyclo
wantLogs: []string{ wantLogs: []string{
`"level"=4 "msg"="Pinniped: Performing OIDC discovery" "issuer"="` + successServer.URL + `"`, `"level"=4 "msg"="Pinniped: Performing OIDC discovery" "issuer"="` + successServer.URL + `"`,
`"level"=4 "msg"="Pinniped: Refreshing cached token."`, `"level"=4 "msg"="Pinniped: Refreshing cached token."`,
`"level"=4 "msg"="Pinniped: Refresh failed." "error"="oauth2: cannot fetch token: 400 Bad Request\nResponse: expected client_id 'test-client-id'\n"`,
`"msg"="could not open callback listener" "error"="some listen error"`, `"msg"="could not open callback listener" "error"="some listen error"`,
}, },
// Expect this to fall through to the authorization code flow, so it fails here. // Expect this to fall through to the authorization code flow, so it fails here.
@ -1522,11 +1536,17 @@ func TestLogin(t *testing.T) { // nolint:gocyclo
}) })
h.cache = cache h.cache = cache
h.getProvider = func(_ *oauth2.Config, _ *oidc.Provider, _ *http.Client) provider.UpstreamOIDCIdentityProviderI { h.getProvider = func(config *oauth2.Config, provider *oidc.Provider, client *http.Client) provider.UpstreamOIDCIdentityProviderI {
mock := mockUpstream(t) mock := mockUpstream(t)
mock.EXPECT(). mock.EXPECT().
ValidateToken(gomock.Any(), HasAccessToken(testToken.AccessToken.Token), nonce.Nonce("")). ValidateToken(gomock.Any(), HasAccessToken(testToken.AccessToken.Token), nonce.Nonce("")).
Return(&testToken, nil) Return(&testToken, nil)
mock.EXPECT().
PerformRefresh(gomock.Any(), testToken.RefreshToken.Token).
DoAndReturn(func(ctx context.Context, refreshToken string) (*oauth2.Token, error) {
// Call the real production code to perform a refresh.
return upstreamoidc.New(config, provider, client).PerformRefresh(ctx, refreshToken)
})
return mock return mock
} }

View File

@ -85,11 +85,19 @@ spec:
# Specify the upstream issuer URL (no trailing slash). # Specify the upstream issuer URL (no trailing slash).
issuer: https://<dex-dns-record> issuer: https://<dex-dns-record>
# Request any scopes other than "openid" for claims besides # Specify how to form authorization requests to Dex.
# the default claims in your token. The "openid" scope is always
# included.
authorizationConfig: authorizationConfig:
additionalScopes: [groups, email]
# Request any scopes other than "openid" for claims besides
# the default claims in your token. The "openid" scope is always
# included.
additionalScopes: [offline_access, groups, email]
# If you would also like to allow your end users to authenticate using
# a password grant, then change this to true.
# Password grants with Dex will only work in Dex versions that include
# this bug fix: https://github.com/dexidp/dex/pull/2234
allowPasswordGrant: false
# Specify how Dex claims are mapped to Kubernetes identities. # Specify how Dex claims are mapped to Kubernetes identities.
claims: claims:

View File

@ -30,8 +30,13 @@ For example, to create a user-owned application:
1. Create a new application: 1. Create a new application:
1. Enter a name for your application, such as "My Kubernetes Clusters". 1. Enter a name for your application, such as "My Kubernetes Clusters".
1. Enter the redirect URI. This is the `spec.issuer` you configured in your `FederationDomain` appended with `/callback`. 1. Enter the redirect URI. This is the `spec.issuer` you configured in your `FederationDomain` appended with `/callback`.
1. Check the box saying that the application is _Confidential_. 1. Check the box saying that the application is _Confidential_. This is required and will cause GitLab to autogenerate
1. Select scope `openid`. This provides access to the `nickname` (GitLab username) and `groups` (GitLab groups) claims. a client ID and client secret for your application.
1. Check the box saying to _Expire Access Tokens_ to cause refresh tokens to be returned to the Supervisor.
1. Select scope `openid`. This is required to get ID tokens. Also, this provides access to the `nickname` (GitLab username)
and `groups` (GitLab groups) claims in the ID tokens.
1. Optionally select other scopes which might provide access to other claims that you might want to use to determine
the usernames of your users, for example `email`.
1. Save the application and make note of the _Application ID_ and _Secret_. 1. Save the application and make note of the _Application ID_ and _Secret_.
## Configure the Supervisor cluster ## Configure the Supervisor cluster
@ -51,6 +56,23 @@ spec:
# Specify the upstream issuer URL. # Specify the upstream issuer URL.
issuer: https://gitlab.com issuer: https://gitlab.com
# Specify how to form authorization requests to GitLab.
authorizationConfig:
# GitLab is unusual among OIDC providers in that it returns an
# error if you request the "offline_access" scope during an
# authorization flow, so ask Pinniped to avoid requesting that
# scope when using GitLab by excluding it from this list.
# By specifying only "openid" here then Pinniped will only
# request "openid".
additionalScopes: [openid]
# If you would also like to allow your end users to authenticate using
# a password grant, then change this to true. See
# https://docs.gitlab.com/ee/api/oauth2.html#resource-owner-password-credentials-flow
# for more information about using the password grant with GitLab.
allowPasswordGrant: false
# Specify how GitLab claims are mapped to Kubernetes identities. # Specify how GitLab claims are mapped to Kubernetes identities.
claims: claims:

View File

@ -32,11 +32,18 @@ For example, to create an app:
1. Create a new app: 1. Create a new app:
1. Click `Create App Integration`. 1. Click `Create App Integration`.
1. For `Sign-on method`, select `OIDC`. 1. For `Sign-on method`, select `OIDC`.
1. For `Application type`, app `Web Application`, then click next. 1. For `Application type`, app `Web Application`, then click next. Only if you would like to offer the
password grant flow to your end users, then choose `Native Application` instead.
1. Enter a name for your app, such as "My Kubernetes Clusters". 1. Enter a name for your app, such as "My Kubernetes Clusters".
1. If you chose to create a `Web Application` then in the General Settings section, choose Grant Types
`Authorization Code` and `Refresh Token`.
1. If you chose `Native Application` then in the General Settings section, choose Grant Types `Authorization Code`,
`Refresh Token`, and `Resource Owner Password`.
1. Enter the sign-in redirect URI. This is the `spec.issuer` you configured in your `FederationDomain` appended with `/callback`. 1. Enter the sign-in redirect URI. This is the `spec.issuer` you configured in your `FederationDomain` appended with `/callback`.
1. Optionally select `Limit access to selected groups` to restrict which Okta users can log in to Kubernetes using this integration. 1. Optionally select `Limit access to selected groups` to restrict which Okta users can log in to Kubernetes using this integration.
1. Save the app and make note of the _Client ID_ and _Client secret_. 1. Save the app and make note of the _Client ID_ and _Client secret_. If you chose to create a `Native Application`
then there is an extra step required to get a client secret: after saving the app, in the
Client Credentials section click `Edit`, choose `Use Client Authentication`, and click `Save`.
1. Navigate to the _Sign On_ tab > _OpenID Connect ID Token_ and click `Edit`. Fill in the Groups claim filter. 1. Navigate to the _Sign On_ tab > _OpenID Connect ID Token_ and click `Edit`. Fill in the Groups claim filter.
For example, for all groups to be present under the claim name `groups`, fill in "groups" in the first box, then select "Matches regex" and ".*". For example, for all groups to be present under the claim name `groups`, fill in "groups" in the first box, then select "Matches regex" and ".*".
@ -54,17 +61,25 @@ metadata:
name: okta name: okta
spec: spec:
# Specify the upstream issuer URL (no trailing slash). # Specify the upstream issuer URL (no trailing slash). Change this to be the
# actual issuer provided by your Okta account.
issuer: https://my-company.okta.com issuer: https://my-company.okta.com
# Request any scopes other than "openid" for claims besides # Specify how to form authorization requests to Okta.
# the default claims in your token. The "openid" scope is always
# included.
#
# To learn more about how to customize the claims returned, see here:
# https://developer.okta.com/docs/guides/customize-tokens-returned-from-okta/overview/
authorizationConfig: authorizationConfig:
additionalScopes: [groups, email]
# Request any scopes other than "openid" for claims besides
# the default claims in your token. The "openid" scope is always
# included.
#
# To learn more about how to customize the claims returned, see here:
# https://developer.okta.com/docs/guides/customize-tokens-returned-from-okta/overview/
additionalScopes: [offline_access, groups, email]
# If you would also like to allow your end users to authenticate using
# a password grant, then change this to true. Password grants only work
# with applications created in Okta as "Native Applications".
allowPasswordGrant: false
# Specify how Okta claims are mapped to Kubernetes identities. # Specify how Okta claims are mapped to Kubernetes identities.
claims: claims:

View File

@ -105,6 +105,7 @@ func TestFormPostHTML_Parallel(t *testing.T) {
// //
// The test server supports special `?fail=close` and `?fail=500` to force error cases. // The test server supports special `?fail=close` and `?fail=500` to force error cases.
func formpostCallbackServer(t *testing.T) (string, func(*testing.T, url.Values)) { func formpostCallbackServer(t *testing.T) (string, func(*testing.T, url.Values)) {
t.Helper()
results := make(chan url.Values) results := make(chan url.Values)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@ -155,6 +156,7 @@ func formpostCallbackServer(t *testing.T) (string, func(*testing.T, url.Values))
// formpostTemplateServer runs a test server that serves formposthtml.Template() rendered with test parameters. // formpostTemplateServer runs a test server that serves formposthtml.Template() rendered with test parameters.
func formpostTemplateServer(t *testing.T, redirectURI string, responseParams url.Values) string { func formpostTemplateServer(t *testing.T, redirectURI string, responseParams url.Values) string {
t.Helper()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fosite.WriteAuthorizeFormPostResponse(redirectURI, responseParams, formposthtml.Template(), w) fosite.WriteAuthorizeFormPostResponse(redirectURI, responseParams, formposthtml.Template(), w)
}) })
@ -168,6 +170,7 @@ func formpostTemplateServer(t *testing.T, redirectURI string, responseParams url
// formpostRandomParams is a helper to generate random OAuth2 response parameters for testing. // formpostRandomParams is a helper to generate random OAuth2 response parameters for testing.
func formpostRandomParams(t *testing.T) url.Values { func formpostRandomParams(t *testing.T) url.Values {
t.Helper()
generator := &hmac.HMACStrategy{GlobalSecret: testlib.RandBytes(t, 32), TokenEntropy: 32} generator := &hmac.HMACStrategy{GlobalSecret: testlib.RandBytes(t, 32), TokenEntropy: 32}
authCode, _, err := generator.Generate() authCode, _, err := generator.Generate()
require.NoError(t, err) require.NoError(t, err)
@ -180,6 +183,7 @@ func formpostRandomParams(t *testing.T) url.Values {
// formpostExpectTitle asserts that the page has the expected title. // formpostExpectTitle asserts that the page has the expected title.
func formpostExpectTitle(t *testing.T, page *agouti.Page, expected string) { func formpostExpectTitle(t *testing.T, page *agouti.Page, expected string) {
t.Helper()
actual, err := page.Title() actual, err := page.Title()
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, expected, actual) require.Equal(t, expected, actual)
@ -187,6 +191,7 @@ func formpostExpectTitle(t *testing.T, page *agouti.Page, expected string) {
// formpostExpectTitle asserts that the page has the expected SVG/emoji favicon. // formpostExpectTitle asserts that the page has the expected SVG/emoji favicon.
func formpostExpectFavicon(t *testing.T, page *agouti.Page, expected string) { func formpostExpectFavicon(t *testing.T, page *agouti.Page, expected string) {
t.Helper()
iconURL, err := page.First("#favicon").Attribute("href") iconURL, err := page.First("#favicon").Attribute("href")
require.NoError(t, err) require.NoError(t, err)
require.True(t, strings.HasPrefix(iconURL, "data:image/svg+xml,<svg")) require.True(t, strings.HasPrefix(iconURL, "data:image/svg+xml,<svg"))
@ -203,6 +208,7 @@ func formpostExpectFavicon(t *testing.T, page *agouti.Page, expected string) {
// formpostInitiate navigates to the template server endpoint and expects the // formpostInitiate navigates to the template server endpoint and expects the
// loading animation to be shown. // loading animation to be shown.
func formpostInitiate(t *testing.T, page *agouti.Page, url string) { func formpostInitiate(t *testing.T, page *agouti.Page, url string) {
t.Helper()
require.NoError(t, page.Reset()) require.NoError(t, page.Reset())
t.Logf("navigating to mock form_post template URL %s...", url) t.Logf("navigating to mock form_post template URL %s...", url)
require.NoError(t, page.Navigate(url)) require.NoError(t, page.Navigate(url))
@ -215,6 +221,7 @@ func formpostInitiate(t *testing.T, page *agouti.Page, url string) {
// formpostExpectSuccessState asserts that the page is in the "success" state. // formpostExpectSuccessState asserts that the page is in the "success" state.
func formpostExpectSuccessState(t *testing.T, page *agouti.Page) { func formpostExpectSuccessState(t *testing.T, page *agouti.Page) {
t.Helper()
t.Logf("expecting to see success message become visible...") t.Logf("expecting to see success message become visible...")
browsertest.WaitForVisibleElements(t, page, "#success") browsertest.WaitForVisibleElements(t, page, "#success")
successDivText, err := page.First("#success").Text() successDivText, err := page.First("#success").Text()
@ -227,6 +234,7 @@ func formpostExpectSuccessState(t *testing.T, page *agouti.Page) {
// formpostExpectManualState asserts that the page is in the "manual" state and returns the auth code. // formpostExpectManualState asserts that the page is in the "manual" state and returns the auth code.
func formpostExpectManualState(t *testing.T, page *agouti.Page) string { func formpostExpectManualState(t *testing.T, page *agouti.Page) string {
t.Helper()
t.Logf("expecting to see manual message become visible...") t.Logf("expecting to see manual message become visible...")
browsertest.WaitForVisibleElements(t, page, "#manual") browsertest.WaitForVisibleElements(t, page, "#manual")
manualDivText, err := page.First("#manual").Text() manualDivText, err := page.First("#manual").Text()

View File

@ -29,6 +29,7 @@ import (
idpv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1" idpv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1"
"go.pinniped.dev/internal/certauthority" "go.pinniped.dev/internal/certauthority"
"go.pinniped.dev/internal/oidc" "go.pinniped.dev/internal/oidc"
"go.pinniped.dev/internal/psession"
"go.pinniped.dev/internal/testutil" "go.pinniped.dev/internal/testutil"
"go.pinniped.dev/pkg/oidcclient/nonce" "go.pinniped.dev/pkg/oidcclient/nonce"
"go.pinniped.dev/pkg/oidcclient/pkce" "go.pinniped.dev/pkg/oidcclient/pkce"
@ -50,6 +51,11 @@ func TestSupervisorLogin(t *testing.T) {
wantDownstreamIDTokenGroups []string wantDownstreamIDTokenGroups []string
wantErrorDescription string wantErrorDescription string
wantErrorType string wantErrorType string
// We don't necessarily have any way to revoke the user's session on the upstream provider,
// so to cause the upstream refresh to fail we can cheat by manipulating the user's session
// data in such a way that it should cause the next upstream refresh attempt to fail.
breakRefreshSessionData func(t *testing.T, customSessionData *psession.CustomSessionData)
}{ }{
{ {
name: "oidc with default username and groups claim settings", name: "oidc with default username and groups claim settings",
@ -69,6 +75,11 @@ func TestSupervisorLogin(t *testing.T) {
}, idpv1alpha1.PhaseReady) }, idpv1alpha1.PhaseReady)
}, },
requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlow, requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlow,
breakRefreshSessionData: func(t *testing.T, customSessionData *psession.CustomSessionData) {
require.Equal(t, psession.ProviderTypeOIDC, customSessionData.ProviderType)
require.NotEmpty(t, customSessionData.OIDC.UpstreamRefreshToken)
customSessionData.OIDC.UpstreamRefreshToken = "invalid-updated-refresh-token"
},
// the ID token Subject should include the upstream user ID after the upstream issuer name // the ID token Subject should include the upstream user ID after the upstream issuer name
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
@ -98,7 +109,12 @@ func TestSupervisorLogin(t *testing.T) {
}, },
}, idpv1alpha1.PhaseReady) }, idpv1alpha1.PhaseReady)
}, },
requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlow, requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlow,
breakRefreshSessionData: func(t *testing.T, customSessionData *psession.CustomSessionData) {
require.Equal(t, psession.ProviderTypeOIDC, customSessionData.ProviderType)
require.NotEmpty(t, customSessionData.OIDC.UpstreamRefreshToken)
customSessionData.OIDC.UpstreamRefreshToken = "invalid-updated-refresh-token"
},
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+", wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+",
wantDownstreamIDTokenUsernameToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Username) + "$", wantDownstreamIDTokenUsernameToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Username) + "$",
wantDownstreamIDTokenGroups: env.SupervisorUpstreamOIDC.ExpectedGroups, wantDownstreamIDTokenGroups: env.SupervisorUpstreamOIDC.ExpectedGroups,
@ -132,6 +148,11 @@ func TestSupervisorLogin(t *testing.T) {
false, false,
) )
}, },
breakRefreshSessionData: func(t *testing.T, customSessionData *psession.CustomSessionData) {
require.Equal(t, psession.ProviderTypeOIDC, customSessionData.ProviderType)
require.NotEmpty(t, customSessionData.OIDC.UpstreamRefreshToken)
customSessionData.OIDC.UpstreamRefreshToken = "invalid-updated-refresh-token"
},
// the ID token Subject should include the upstream user ID after the upstream issuer name // the ID token Subject should include the upstream user ID after the upstream issuer name
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
@ -193,6 +214,7 @@ func TestSupervisorLogin(t *testing.T) {
false, false,
) )
}, },
breakRefreshSessionData: nil, // upstream refresh not yet implemented for this IDP type
// 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(
"ldaps://"+env.SupervisorUpstreamLDAP.Host+ "ldaps://"+env.SupervisorUpstreamLDAP.Host+
@ -259,6 +281,7 @@ func TestSupervisorLogin(t *testing.T) {
false, false,
) )
}, },
breakRefreshSessionData: nil, // upstream refresh not yet implemented for this IDP type
// 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(
"ldaps://"+env.SupervisorUpstreamLDAP.StartTLSOnlyHost+ "ldaps://"+env.SupervisorUpstreamLDAP.StartTLSOnlyHost+
@ -325,8 +348,9 @@ func TestSupervisorLogin(t *testing.T) {
true, true,
) )
}, },
wantErrorDescription: "The resource owner or authorization server denied the request. Username/password not accepted by LDAP provider.", breakRefreshSessionData: nil, // upstream refresh not yet implemented for this IDP type
wantErrorType: "access_denied", wantErrorDescription: "The resource owner or authorization server denied the request. Username/password not accepted by LDAP provider.",
wantErrorType: "access_denied",
}, },
{ {
name: "ldap login still works after updating bind secret", name: "ldap login still works after updating bind secret",
@ -402,6 +426,7 @@ func TestSupervisorLogin(t *testing.T) {
false, false,
) )
}, },
breakRefreshSessionData: nil, // upstream refresh not yet implemented for this IDP type
// 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(
"ldaps://"+env.SupervisorUpstreamLDAP.Host+ "ldaps://"+env.SupervisorUpstreamLDAP.Host+
@ -500,6 +525,7 @@ func TestSupervisorLogin(t *testing.T) {
false, false,
) )
}, },
breakRefreshSessionData: nil, // upstream refresh not yet implemented for this IDP type
// 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(
"ldaps://"+env.SupervisorUpstreamLDAP.Host+ "ldaps://"+env.SupervisorUpstreamLDAP.Host+
@ -554,6 +580,7 @@ func TestSupervisorLogin(t *testing.T) {
false, false,
) )
}, },
breakRefreshSessionData: nil, // upstream refresh not yet implemented for this IDP type
// 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(
"ldaps://"+env.SupervisorUpstreamActiveDirectory.Host+ "ldaps://"+env.SupervisorUpstreamActiveDirectory.Host+
@ -621,6 +648,7 @@ func TestSupervisorLogin(t *testing.T) {
false, false,
) )
}, },
breakRefreshSessionData: nil, // upstream refresh not yet implemented for this IDP type
// 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(
"ldaps://"+env.SupervisorUpstreamActiveDirectory.Host+ "ldaps://"+env.SupervisorUpstreamActiveDirectory.Host+
@ -693,6 +721,7 @@ func TestSupervisorLogin(t *testing.T) {
false, false,
) )
}, },
breakRefreshSessionData: nil, // upstream refresh not yet implemented for this IDP type
// 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(
"ldaps://"+env.SupervisorUpstreamActiveDirectory.Host+ "ldaps://"+env.SupervisorUpstreamActiveDirectory.Host+
@ -780,6 +809,7 @@ func TestSupervisorLogin(t *testing.T) {
false, false,
) )
}, },
breakRefreshSessionData: nil, // upstream refresh not yet implemented for this IDP type
// 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(
"ldaps://"+env.SupervisorUpstreamActiveDirectory.Host+ "ldaps://"+env.SupervisorUpstreamActiveDirectory.Host+
@ -834,8 +864,9 @@ func TestSupervisorLogin(t *testing.T) {
true, true,
) )
}, },
wantErrorDescription: "The resource owner or authorization server denied the request. Username/password not accepted by LDAP provider.", breakRefreshSessionData: nil, // upstream refresh not yet implemented for this IDP type
wantErrorType: "access_denied", wantErrorDescription: "The resource owner or authorization server denied the request. Username/password not accepted by LDAP provider.",
wantErrorType: "access_denied",
}, },
} }
for _, test := range tests { for _, test := range tests {
@ -846,10 +877,12 @@ func TestSupervisorLogin(t *testing.T) {
testSupervisorLogin(t, testSupervisorLogin(t,
tt.createIDP, tt.createIDP,
tt.requestAuthorization, tt.requestAuthorization,
tt.breakRefreshSessionData,
tt.wantDownstreamIDTokenSubjectToMatch, tt.wantDownstreamIDTokenSubjectToMatch,
tt.wantDownstreamIDTokenUsernameToMatch, tt.wantDownstreamIDTokenUsernameToMatch,
tt.wantDownstreamIDTokenGroups, tt.wantDownstreamIDTokenGroups,
tt.wantErrorDescription, tt.wantErrorType, tt.wantErrorDescription,
tt.wantErrorType,
) )
}) })
} }
@ -976,6 +1009,7 @@ func testSupervisorLogin(
t *testing.T, t *testing.T,
createIDP func(t *testing.T), createIDP func(t *testing.T),
requestAuthorization func(t *testing.T, downstreamAuthorizeURL, downstreamCallbackURL string, httpClient *http.Client), requestAuthorization func(t *testing.T, downstreamAuthorizeURL, downstreamCallbackURL string, httpClient *http.Client),
breakRefreshSessionData func(t *testing.T, customSessionData *psession.CustomSessionData),
wantDownstreamIDTokenSubjectToMatch, wantDownstreamIDTokenUsernameToMatch string, wantDownstreamIDTokenGroups []string, wantDownstreamIDTokenSubjectToMatch, wantDownstreamIDTokenUsernameToMatch string, wantDownstreamIDTokenGroups []string,
wantErrorDescription string, wantErrorType string, wantErrorDescription string, wantErrorType string,
) { ) {
@ -1140,6 +1174,42 @@ func testSupervisorLogin(
// token exchange on the refreshed token // token exchange on the refreshed token
doTokenExchange(t, &downstreamOAuth2Config, refreshedTokenResponse, httpClient, discovery) doTokenExchange(t, &downstreamOAuth2Config, refreshedTokenResponse, httpClient, discovery)
// Now that we have successfully performed a refresh, let's test what happens when an
// upstream refresh fails during the next downstream refresh.
if breakRefreshSessionData != nil {
latestRefreshToken := refreshedTokenResponse.RefreshToken
signatureOfLatestRefreshToken := getFositeDataSignature(t, latestRefreshToken)
// First use the latest downstream refresh token to look up the corresponding session in the Supervisor's storage.
kubeClient := testlib.NewKubernetesClientset(t)
supervisorSecretsClient := kubeClient.CoreV1().Secrets(env.SupervisorNamespace)
oauthStore := oidc.NewKubeStorage(supervisorSecretsClient, oidc.DefaultOIDCTimeoutsConfiguration())
storedRefreshSession, err := oauthStore.GetRefreshTokenSession(ctx, signatureOfLatestRefreshToken, nil)
require.NoError(t, err)
// Next mutate the part of the session that is used during upstream refresh.
pinnipedSession, ok := storedRefreshSession.GetSession().(*psession.PinnipedSession)
require.True(t, ok, "should have been able to cast session data to PinnipedSession")
breakRefreshSessionData(t, pinnipedSession.Custom)
// Then save the mutated Secret back to Kubernetes.
// There is no update function, so delete and create again at the same name.
require.NoError(t, oauthStore.DeleteRefreshTokenSession(ctx, signatureOfLatestRefreshToken))
require.NoError(t, oauthStore.CreateRefreshTokenSession(ctx, signatureOfLatestRefreshToken, storedRefreshSession))
// Now try to perform a downstream refresh again, knowing that the corresponding upstream refresh should fail.
_, err = downstreamOAuth2Config.TokenSource(oidcHTTPClientContext, &oauth2.Token{RefreshToken: latestRefreshToken}).Token()
// Should have got an error since the upstream refresh should have failed.
require.Error(t, err)
require.Regexp(t,
regexp.QuoteMeta("oauth2: cannot fetch token: 401 Unauthorized\n")+
regexp.QuoteMeta(`Response: {"error":"error","error_description":"Error during upstream refresh. Upstream refresh failed using provider '`)+
"[^']+"+ // this would be the name of the identity provider CR
regexp.QuoteMeta(fmt.Sprintf(`' of type '%s'."`, pinnipedSession.Custom.ProviderType)),
err.Error(),
)
}
} else { } else {
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")
@ -1148,6 +1218,15 @@ func testSupervisorLogin(
} }
} }
// getFositeDataSignature returns the signature of the provided data. The provided data could be an auth code, access
// token, etc. It is assumed that the code is of the format "data.signature", which is how Fosite generates auth codes
// and access tokens.
func getFositeDataSignature(t *testing.T, data string) string {
split := strings.Split(data, ".")
require.Len(t, split, 2)
return split[1]
}
func verifyTokenResponse( func verifyTokenResponse(
t *testing.T, t *testing.T,
tokenResponse *oauth2.Token, tokenResponse *oauth2.Token,

View File

@ -40,6 +40,12 @@ func TestSupervisorUpstreamOIDCDiscovery(t *testing.T) {
Message: `failed to perform OIDC discovery against "https://127.0.0.1:444444/invalid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee": Message: `failed to perform OIDC discovery against "https://127.0.0.1:444444/invalid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee":
Get "https://127.0.0.1:444444/invalid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee/.well-known/openid-configuration": dial tcp: address 444444: in [truncated 10 chars]`, Get "https://127.0.0.1:444444/invalid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee/.well-known/openid-configuration": dial tcp: address 444444: in [truncated 10 chars]`,
}, },
{
Type: "AdditionalAuthorizeParametersValid",
Status: "True",
Reason: "Success",
Message: "additionalAuthorizeParameters parameter names are allowed",
},
}) })
}) })
@ -72,6 +78,12 @@ Get "https://127.0.0.1:444444/invalid-url-that-is-really-really-long-nananananan
Message: `failed to perform OIDC discovery against "` + env.SupervisorUpstreamOIDC.Issuer + `/": Message: `failed to perform OIDC discovery against "` + env.SupervisorUpstreamOIDC.Issuer + `/":
oidc: issuer did not match the issuer returned by provider, expected "` + env.SupervisorUpstreamOIDC.Issuer + `/" got "` + env.SupervisorUpstreamOIDC.Issuer + `"`, oidc: issuer did not match the issuer returned by provider, expected "` + env.SupervisorUpstreamOIDC.Issuer + `/" got "` + env.SupervisorUpstreamOIDC.Issuer + `"`,
}, },
{
Type: "AdditionalAuthorizeParametersValid",
Status: "True",
Reason: "Success",
Message: "additionalAuthorizeParameters parameter names are allowed",
},
}) })
}) })
@ -103,6 +115,12 @@ oidc: issuer did not match the issuer returned by provider, expected "` + env.Su
Reason: "Success", Reason: "Success",
Message: "discovered issuer configuration", Message: "discovered issuer configuration",
}, },
{
Type: "AdditionalAuthorizeParametersValid",
Status: "True",
Reason: "Success",
Message: "additionalAuthorizeParameters parameter names are allowed",
},
}) })
}) })
} }

View File

@ -247,7 +247,7 @@ func loadEnvVars(t *testing.T, result *TestEnv) {
result.SupervisorUpstreamOIDC = TestOIDCUpstream{ result.SupervisorUpstreamOIDC = TestOIDCUpstream{
Issuer: needEnv(t, "PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_ISSUER"), Issuer: needEnv(t, "PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_ISSUER"),
CABundle: base64Decoded(t, os.Getenv("PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_ISSUER_CA_BUNDLE")), CABundle: base64Decoded(t, os.Getenv("PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_ISSUER_CA_BUNDLE")),
AdditionalScopes: strings.Fields(os.Getenv("PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_ADDITIONAL_SCOPES")), AdditionalScopes: filterEmpty(strings.Split(strings.ReplaceAll(os.Getenv("PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_ADDITIONAL_SCOPES"), " ", ""), ",")),
UsernameClaim: os.Getenv("PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_USERNAME_CLAIM"), UsernameClaim: os.Getenv("PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_USERNAME_CLAIM"),
GroupsClaim: os.Getenv("PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_GROUPS_CLAIM"), GroupsClaim: os.Getenv("PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_GROUPS_CLAIM"),
ClientID: needEnv(t, "PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_CLIENT_ID"), ClientID: needEnv(t, "PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_CLIENT_ID"),