Allow additional claims to map into an ID token issued by the supervisor

- Specify mappings on OIDCIdentityProvider.spec.claims.additionalClaimMappings
- Advertise additionalClaims in the OIDC discovery endpoint under claims_supported

Co-authored-by: Ryan Richard <richardry@vmware.com>
Co-authored-by: Joshua Casey <joshuatcasey@gmail.com>
This commit is contained in:
Ryan Richard 2022-09-20 14:54:10 -07:00
parent f4c9202f49
commit 8ff6ef32e9
70 changed files with 1084 additions and 94 deletions

View File

@ -138,6 +138,17 @@ type OIDCClaims struct {
// the ID token. // the ID token.
// +optional // +optional
Username string `json:"username"` Username string `json:"username"`
// AdditionalClaimMappings allows for additional arbitrary upstream claim values to be mapped into the
// "additionalClaims" claim of the ID tokens generated by the Supervisor. This should be specified as a map of
// new claim names as the keys, and upstream claim names as the values. These new claim names will be nested
// under the top-level "additionalClaims" claim in ID tokens generated by the Supervisor when this
// OIDCIdentityProvider was used for user authentication. These claims will be made available to all clients.
// This feature is not required to use the Supervisor to provide authentication for Kubernetes clusters, but can be
// used when using the Supervisor for other authentication purposes. When this map is empty or the upstream claims
// are not available, the "additionalClaims" claim will be excluded from the ID tokens generated by the Supervisor.
// +optional
AdditionalClaimMappings map[string]string `json:"additionalClaimMappings,omitempty"`
} }
// OIDCClient contains information about an OIDC client (e.g., client ID and client // OIDCClient contains information about an OIDC client (e.g., client ID and client

View File

@ -40,6 +40,10 @@ const (
// group names which were mapped from the upstream identity provider. // group names which were mapped from the upstream identity provider.
IDTokenClaimGroups = "groups" IDTokenClaimGroups = "groups"
// IDTokenClaimAdditionalClaims is the top level claim used to hold additional claims in the downstream ID
// token, if any claims are present.
IDTokenClaimAdditionalClaims = "additionalClaims"
// GrantTypeAuthorizationCode is the name of the grant type for authorization code flows defined by the OIDC spec. // GrantTypeAuthorizationCode is the name of the grant type for authorization code flows defined by the OIDC spec.
GrantTypeAuthorizationCode = "authorization_code" GrantTypeAuthorizationCode = "authorization_code"

View File

@ -185,6 +185,24 @@ spec:
description: Claims provides the names of token claims that will be description: Claims provides the names of token claims that will be
used when inspecting an identity from this OIDC identity provider. used when inspecting an identity from this OIDC identity provider.
properties: properties:
additionalClaimMappings:
additionalProperties:
type: string
description: AdditionalClaimMappings allows for additional arbitrary
upstream claim values to be mapped into the "additionalClaims"
claim of the ID tokens generated by the Supervisor. This should
be specified as a map of new claim names as the keys, and upstream
claim names as the values. These new claim names will be nested
under the top-level "additionalClaims" claim in ID tokens generated
by the Supervisor when this OIDCIdentityProvider was used for
user authentication. These claims will be made available to
all clients. This feature is not required to use the Supervisor
to provide authentication for Kubernetes clusters, but can be
used when using the Supervisor for other authentication purposes.
When this map is empty or the upstream claims are not available,
the "additionalClaims" claim will be excluded from the ID tokens
generated by the Supervisor.
type: object
groups: groups:
description: Groups provides the name of the ID token claim or description: Groups provides the name of the ID token claim or
userinfo endpoint response claim that will be used to ascertain userinfo endpoint response claim that will be used to ascertain

View File

@ -1391,6 +1391,7 @@ OIDCClaims provides a mapping from upstream claims into identities.
| Field | Description | Field | Description
| *`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. | *`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 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. | *`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.
| *`additionalClaimMappings`* __object (keys:string, values:string)__ | AdditionalClaimMappings allows for additional arbitrary upstream claim values to be mapped into the "additionalClaims" claim of the ID tokens generated by the Supervisor. This should be specified as a map of new claim names as the keys, and upstream claim names as the values. These new claim names will be nested under the top-level "additionalClaims" claim in ID tokens generated by the Supervisor when this OIDCIdentityProvider was used for user authentication. These claims will be made available to all clients. This feature is not required to use the Supervisor to provide authentication for Kubernetes clusters, but can be used when using the Supervisor for other authentication purposes. When this map is empty or the upstream claims are not available, the "additionalClaims" claim will be excluded from the ID tokens generated by the Supervisor.
|=== |===

View File

@ -138,6 +138,17 @@ type OIDCClaims struct {
// the ID token. // the ID token.
// +optional // +optional
Username string `json:"username"` Username string `json:"username"`
// AdditionalClaimMappings allows for additional arbitrary upstream claim values to be mapped into the
// "additionalClaims" claim of the ID tokens generated by the Supervisor. This should be specified as a map of
// new claim names as the keys, and upstream claim names as the values. These new claim names will be nested
// under the top-level "additionalClaims" claim in ID tokens generated by the Supervisor when this
// OIDCIdentityProvider was used for user authentication. These claims will be made available to all clients.
// This feature is not required to use the Supervisor to provide authentication for Kubernetes clusters, but can be
// used when using the Supervisor for other authentication purposes. When this map is empty or the upstream claims
// are not available, the "additionalClaims" claim will be excluded from the ID tokens generated by the Supervisor.
// +optional
AdditionalClaimMappings map[string]string `json:"additionalClaimMappings,omitempty"`
} }
// OIDCClient contains information about an OIDC client (e.g., client ID and client // OIDCClient contains information about an OIDC client (e.g., client ID and client

View File

@ -438,6 +438,13 @@ func (in *OIDCAuthorizationConfig) DeepCopy() *OIDCAuthorizationConfig {
// 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 *OIDCClaims) DeepCopyInto(out *OIDCClaims) { func (in *OIDCClaims) DeepCopyInto(out *OIDCClaims) {
*out = *in *out = *in
if in.AdditionalClaimMappings != nil {
in, out := &in.AdditionalClaimMappings, &out.AdditionalClaimMappings
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
return return
} }
@ -537,7 +544,7 @@ func (in *OIDCIdentityProviderSpec) DeepCopyInto(out *OIDCIdentityProviderSpec)
**out = **in **out = **in
} }
in.AuthorizationConfig.DeepCopyInto(&out.AuthorizationConfig) in.AuthorizationConfig.DeepCopyInto(&out.AuthorizationConfig)
out.Claims = in.Claims in.Claims.DeepCopyInto(&out.Claims)
out.Client = in.Client out.Client = in.Client
return return
} }

View File

@ -40,6 +40,10 @@ const (
// group names which were mapped from the upstream identity provider. // group names which were mapped from the upstream identity provider.
IDTokenClaimGroups = "groups" IDTokenClaimGroups = "groups"
// IDTokenClaimAdditionalClaims is the top level claim used to hold additional claims in the downstream ID
// token, if any claims are present.
IDTokenClaimAdditionalClaims = "additionalClaims"
// GrantTypeAuthorizationCode is the name of the grant type for authorization code flows defined by the OIDC spec. // GrantTypeAuthorizationCode is the name of the grant type for authorization code flows defined by the OIDC spec.
GrantTypeAuthorizationCode = "authorization_code" GrantTypeAuthorizationCode = "authorization_code"

View File

@ -185,6 +185,24 @@ spec:
description: Claims provides the names of token claims that will be description: Claims provides the names of token claims that will be
used when inspecting an identity from this OIDC identity provider. used when inspecting an identity from this OIDC identity provider.
properties: properties:
additionalClaimMappings:
additionalProperties:
type: string
description: AdditionalClaimMappings allows for additional arbitrary
upstream claim values to be mapped into the "additionalClaims"
claim of the ID tokens generated by the Supervisor. This should
be specified as a map of new claim names as the keys, and upstream
claim names as the values. These new claim names will be nested
under the top-level "additionalClaims" claim in ID tokens generated
by the Supervisor when this OIDCIdentityProvider was used for
user authentication. These claims will be made available to
all clients. This feature is not required to use the Supervisor
to provide authentication for Kubernetes clusters, but can be
used when using the Supervisor for other authentication purposes.
When this map is empty or the upstream claims are not available,
the "additionalClaims" claim will be excluded from the ID tokens
generated by the Supervisor.
type: object
groups: groups:
description: Groups provides the name of the ID token claim or description: Groups provides the name of the ID token claim or
userinfo endpoint response claim that will be used to ascertain userinfo endpoint response claim that will be used to ascertain

View File

@ -1391,6 +1391,7 @@ OIDCClaims provides a mapping from upstream claims into identities.
| Field | Description | Field | Description
| *`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. | *`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 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. | *`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.
| *`additionalClaimMappings`* __object (keys:string, values:string)__ | AdditionalClaimMappings allows for additional arbitrary upstream claim values to be mapped into the "additionalClaims" claim of the ID tokens generated by the Supervisor. This should be specified as a map of new claim names as the keys, and upstream claim names as the values. These new claim names will be nested under the top-level "additionalClaims" claim in ID tokens generated by the Supervisor when this OIDCIdentityProvider was used for user authentication. These claims will be made available to all clients. This feature is not required to use the Supervisor to provide authentication for Kubernetes clusters, but can be used when using the Supervisor for other authentication purposes. When this map is empty or the upstream claims are not available, the "additionalClaims" claim will be excluded from the ID tokens generated by the Supervisor.
|=== |===

View File

@ -138,6 +138,17 @@ type OIDCClaims struct {
// the ID token. // the ID token.
// +optional // +optional
Username string `json:"username"` Username string `json:"username"`
// AdditionalClaimMappings allows for additional arbitrary upstream claim values to be mapped into the
// "additionalClaims" claim of the ID tokens generated by the Supervisor. This should be specified as a map of
// new claim names as the keys, and upstream claim names as the values. These new claim names will be nested
// under the top-level "additionalClaims" claim in ID tokens generated by the Supervisor when this
// OIDCIdentityProvider was used for user authentication. These claims will be made available to all clients.
// This feature is not required to use the Supervisor to provide authentication for Kubernetes clusters, but can be
// used when using the Supervisor for other authentication purposes. When this map is empty or the upstream claims
// are not available, the "additionalClaims" claim will be excluded from the ID tokens generated by the Supervisor.
// +optional
AdditionalClaimMappings map[string]string `json:"additionalClaimMappings,omitempty"`
} }
// OIDCClient contains information about an OIDC client (e.g., client ID and client // OIDCClient contains information about an OIDC client (e.g., client ID and client

View File

@ -438,6 +438,13 @@ func (in *OIDCAuthorizationConfig) DeepCopy() *OIDCAuthorizationConfig {
// 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 *OIDCClaims) DeepCopyInto(out *OIDCClaims) { func (in *OIDCClaims) DeepCopyInto(out *OIDCClaims) {
*out = *in *out = *in
if in.AdditionalClaimMappings != nil {
in, out := &in.AdditionalClaimMappings, &out.AdditionalClaimMappings
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
return return
} }
@ -537,7 +544,7 @@ func (in *OIDCIdentityProviderSpec) DeepCopyInto(out *OIDCIdentityProviderSpec)
**out = **in **out = **in
} }
in.AuthorizationConfig.DeepCopyInto(&out.AuthorizationConfig) in.AuthorizationConfig.DeepCopyInto(&out.AuthorizationConfig)
out.Claims = in.Claims in.Claims.DeepCopyInto(&out.Claims)
out.Client = in.Client out.Client = in.Client
return return
} }

View File

@ -40,6 +40,10 @@ const (
// group names which were mapped from the upstream identity provider. // group names which were mapped from the upstream identity provider.
IDTokenClaimGroups = "groups" IDTokenClaimGroups = "groups"
// IDTokenClaimAdditionalClaims is the top level claim used to hold additional claims in the downstream ID
// token, if any claims are present.
IDTokenClaimAdditionalClaims = "additionalClaims"
// GrantTypeAuthorizationCode is the name of the grant type for authorization code flows defined by the OIDC spec. // GrantTypeAuthorizationCode is the name of the grant type for authorization code flows defined by the OIDC spec.
GrantTypeAuthorizationCode = "authorization_code" GrantTypeAuthorizationCode = "authorization_code"

View File

@ -185,6 +185,24 @@ spec:
description: Claims provides the names of token claims that will be description: Claims provides the names of token claims that will be
used when inspecting an identity from this OIDC identity provider. used when inspecting an identity from this OIDC identity provider.
properties: properties:
additionalClaimMappings:
additionalProperties:
type: string
description: AdditionalClaimMappings allows for additional arbitrary
upstream claim values to be mapped into the "additionalClaims"
claim of the ID tokens generated by the Supervisor. This should
be specified as a map of new claim names as the keys, and upstream
claim names as the values. These new claim names will be nested
under the top-level "additionalClaims" claim in ID tokens generated
by the Supervisor when this OIDCIdentityProvider was used for
user authentication. These claims will be made available to
all clients. This feature is not required to use the Supervisor
to provide authentication for Kubernetes clusters, but can be
used when using the Supervisor for other authentication purposes.
When this map is empty or the upstream claims are not available,
the "additionalClaims" claim will be excluded from the ID tokens
generated by the Supervisor.
type: object
groups: groups:
description: Groups provides the name of the ID token claim or description: Groups provides the name of the ID token claim or
userinfo endpoint response claim that will be used to ascertain userinfo endpoint response claim that will be used to ascertain

View File

@ -1391,6 +1391,7 @@ OIDCClaims provides a mapping from upstream claims into identities.
| Field | Description | Field | Description
| *`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. | *`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 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. | *`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.
| *`additionalClaimMappings`* __object (keys:string, values:string)__ | AdditionalClaimMappings allows for additional arbitrary upstream claim values to be mapped into the "additionalClaims" claim of the ID tokens generated by the Supervisor. This should be specified as a map of new claim names as the keys, and upstream claim names as the values. These new claim names will be nested under the top-level "additionalClaims" claim in ID tokens generated by the Supervisor when this OIDCIdentityProvider was used for user authentication. These claims will be made available to all clients. This feature is not required to use the Supervisor to provide authentication for Kubernetes clusters, but can be used when using the Supervisor for other authentication purposes. When this map is empty or the upstream claims are not available, the "additionalClaims" claim will be excluded from the ID tokens generated by the Supervisor.
|=== |===

View File

@ -138,6 +138,17 @@ type OIDCClaims struct {
// the ID token. // the ID token.
// +optional // +optional
Username string `json:"username"` Username string `json:"username"`
// AdditionalClaimMappings allows for additional arbitrary upstream claim values to be mapped into the
// "additionalClaims" claim of the ID tokens generated by the Supervisor. This should be specified as a map of
// new claim names as the keys, and upstream claim names as the values. These new claim names will be nested
// under the top-level "additionalClaims" claim in ID tokens generated by the Supervisor when this
// OIDCIdentityProvider was used for user authentication. These claims will be made available to all clients.
// This feature is not required to use the Supervisor to provide authentication for Kubernetes clusters, but can be
// used when using the Supervisor for other authentication purposes. When this map is empty or the upstream claims
// are not available, the "additionalClaims" claim will be excluded from the ID tokens generated by the Supervisor.
// +optional
AdditionalClaimMappings map[string]string `json:"additionalClaimMappings,omitempty"`
} }
// OIDCClient contains information about an OIDC client (e.g., client ID and client // OIDCClient contains information about an OIDC client (e.g., client ID and client

View File

@ -438,6 +438,13 @@ func (in *OIDCAuthorizationConfig) DeepCopy() *OIDCAuthorizationConfig {
// 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 *OIDCClaims) DeepCopyInto(out *OIDCClaims) { func (in *OIDCClaims) DeepCopyInto(out *OIDCClaims) {
*out = *in *out = *in
if in.AdditionalClaimMappings != nil {
in, out := &in.AdditionalClaimMappings, &out.AdditionalClaimMappings
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
return return
} }
@ -537,7 +544,7 @@ func (in *OIDCIdentityProviderSpec) DeepCopyInto(out *OIDCIdentityProviderSpec)
**out = **in **out = **in
} }
in.AuthorizationConfig.DeepCopyInto(&out.AuthorizationConfig) in.AuthorizationConfig.DeepCopyInto(&out.AuthorizationConfig)
out.Claims = in.Claims in.Claims.DeepCopyInto(&out.Claims)
out.Client = in.Client out.Client = in.Client
return return
} }

View File

@ -40,6 +40,10 @@ const (
// group names which were mapped from the upstream identity provider. // group names which were mapped from the upstream identity provider.
IDTokenClaimGroups = "groups" IDTokenClaimGroups = "groups"
// IDTokenClaimAdditionalClaims is the top level claim used to hold additional claims in the downstream ID
// token, if any claims are present.
IDTokenClaimAdditionalClaims = "additionalClaims"
// GrantTypeAuthorizationCode is the name of the grant type for authorization code flows defined by the OIDC spec. // GrantTypeAuthorizationCode is the name of the grant type for authorization code flows defined by the OIDC spec.
GrantTypeAuthorizationCode = "authorization_code" GrantTypeAuthorizationCode = "authorization_code"

View File

@ -185,6 +185,24 @@ spec:
description: Claims provides the names of token claims that will be description: Claims provides the names of token claims that will be
used when inspecting an identity from this OIDC identity provider. used when inspecting an identity from this OIDC identity provider.
properties: properties:
additionalClaimMappings:
additionalProperties:
type: string
description: AdditionalClaimMappings allows for additional arbitrary
upstream claim values to be mapped into the "additionalClaims"
claim of the ID tokens generated by the Supervisor. This should
be specified as a map of new claim names as the keys, and upstream
claim names as the values. These new claim names will be nested
under the top-level "additionalClaims" claim in ID tokens generated
by the Supervisor when this OIDCIdentityProvider was used for
user authentication. These claims will be made available to
all clients. This feature is not required to use the Supervisor
to provide authentication for Kubernetes clusters, but can be
used when using the Supervisor for other authentication purposes.
When this map is empty or the upstream claims are not available,
the "additionalClaims" claim will be excluded from the ID tokens
generated by the Supervisor.
type: object
groups: groups:
description: Groups provides the name of the ID token claim or description: Groups provides the name of the ID token claim or
userinfo endpoint response claim that will be used to ascertain userinfo endpoint response claim that will be used to ascertain

View File

@ -1391,6 +1391,7 @@ OIDCClaims provides a mapping from upstream claims into identities.
| Field | Description | Field | Description
| *`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. | *`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 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. | *`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.
| *`additionalClaimMappings`* __object (keys:string, values:string)__ | AdditionalClaimMappings allows for additional arbitrary upstream claim values to be mapped into the "additionalClaims" claim of the ID tokens generated by the Supervisor. This should be specified as a map of new claim names as the keys, and upstream claim names as the values. These new claim names will be nested under the top-level "additionalClaims" claim in ID tokens generated by the Supervisor when this OIDCIdentityProvider was used for user authentication. These claims will be made available to all clients. This feature is not required to use the Supervisor to provide authentication for Kubernetes clusters, but can be used when using the Supervisor for other authentication purposes. When this map is empty or the upstream claims are not available, the "additionalClaims" claim will be excluded from the ID tokens generated by the Supervisor.
|=== |===

View File

@ -138,6 +138,17 @@ type OIDCClaims struct {
// the ID token. // the ID token.
// +optional // +optional
Username string `json:"username"` Username string `json:"username"`
// AdditionalClaimMappings allows for additional arbitrary upstream claim values to be mapped into the
// "additionalClaims" claim of the ID tokens generated by the Supervisor. This should be specified as a map of
// new claim names as the keys, and upstream claim names as the values. These new claim names will be nested
// under the top-level "additionalClaims" claim in ID tokens generated by the Supervisor when this
// OIDCIdentityProvider was used for user authentication. These claims will be made available to all clients.
// This feature is not required to use the Supervisor to provide authentication for Kubernetes clusters, but can be
// used when using the Supervisor for other authentication purposes. When this map is empty or the upstream claims
// are not available, the "additionalClaims" claim will be excluded from the ID tokens generated by the Supervisor.
// +optional
AdditionalClaimMappings map[string]string `json:"additionalClaimMappings,omitempty"`
} }
// OIDCClient contains information about an OIDC client (e.g., client ID and client // OIDCClient contains information about an OIDC client (e.g., client ID and client

View File

@ -438,6 +438,13 @@ func (in *OIDCAuthorizationConfig) DeepCopy() *OIDCAuthorizationConfig {
// 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 *OIDCClaims) DeepCopyInto(out *OIDCClaims) { func (in *OIDCClaims) DeepCopyInto(out *OIDCClaims) {
*out = *in *out = *in
if in.AdditionalClaimMappings != nil {
in, out := &in.AdditionalClaimMappings, &out.AdditionalClaimMappings
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
return return
} }
@ -537,7 +544,7 @@ func (in *OIDCIdentityProviderSpec) DeepCopyInto(out *OIDCIdentityProviderSpec)
**out = **in **out = **in
} }
in.AuthorizationConfig.DeepCopyInto(&out.AuthorizationConfig) in.AuthorizationConfig.DeepCopyInto(&out.AuthorizationConfig)
out.Claims = in.Claims in.Claims.DeepCopyInto(&out.Claims)
out.Client = in.Client out.Client = in.Client
return return
} }

View File

@ -40,6 +40,10 @@ const (
// group names which were mapped from the upstream identity provider. // group names which were mapped from the upstream identity provider.
IDTokenClaimGroups = "groups" IDTokenClaimGroups = "groups"
// IDTokenClaimAdditionalClaims is the top level claim used to hold additional claims in the downstream ID
// token, if any claims are present.
IDTokenClaimAdditionalClaims = "additionalClaims"
// GrantTypeAuthorizationCode is the name of the grant type for authorization code flows defined by the OIDC spec. // GrantTypeAuthorizationCode is the name of the grant type for authorization code flows defined by the OIDC spec.
GrantTypeAuthorizationCode = "authorization_code" GrantTypeAuthorizationCode = "authorization_code"

View File

@ -185,6 +185,24 @@ spec:
description: Claims provides the names of token claims that will be description: Claims provides the names of token claims that will be
used when inspecting an identity from this OIDC identity provider. used when inspecting an identity from this OIDC identity provider.
properties: properties:
additionalClaimMappings:
additionalProperties:
type: string
description: AdditionalClaimMappings allows for additional arbitrary
upstream claim values to be mapped into the "additionalClaims"
claim of the ID tokens generated by the Supervisor. This should
be specified as a map of new claim names as the keys, and upstream
claim names as the values. These new claim names will be nested
under the top-level "additionalClaims" claim in ID tokens generated
by the Supervisor when this OIDCIdentityProvider was used for
user authentication. These claims will be made available to
all clients. This feature is not required to use the Supervisor
to provide authentication for Kubernetes clusters, but can be
used when using the Supervisor for other authentication purposes.
When this map is empty or the upstream claims are not available,
the "additionalClaims" claim will be excluded from the ID tokens
generated by the Supervisor.
type: object
groups: groups:
description: Groups provides the name of the ID token claim or description: Groups provides the name of the ID token claim or
userinfo endpoint response claim that will be used to ascertain userinfo endpoint response claim that will be used to ascertain

View File

@ -1391,6 +1391,7 @@ OIDCClaims provides a mapping from upstream claims into identities.
| Field | Description | Field | Description
| *`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. | *`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 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. | *`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.
| *`additionalClaimMappings`* __object (keys:string, values:string)__ | AdditionalClaimMappings allows for additional arbitrary upstream claim values to be mapped into the "additionalClaims" claim of the ID tokens generated by the Supervisor. This should be specified as a map of new claim names as the keys, and upstream claim names as the values. These new claim names will be nested under the top-level "additionalClaims" claim in ID tokens generated by the Supervisor when this OIDCIdentityProvider was used for user authentication. These claims will be made available to all clients. This feature is not required to use the Supervisor to provide authentication for Kubernetes clusters, but can be used when using the Supervisor for other authentication purposes. When this map is empty or the upstream claims are not available, the "additionalClaims" claim will be excluded from the ID tokens generated by the Supervisor.
|=== |===

View File

@ -138,6 +138,17 @@ type OIDCClaims struct {
// the ID token. // the ID token.
// +optional // +optional
Username string `json:"username"` Username string `json:"username"`
// AdditionalClaimMappings allows for additional arbitrary upstream claim values to be mapped into the
// "additionalClaims" claim of the ID tokens generated by the Supervisor. This should be specified as a map of
// new claim names as the keys, and upstream claim names as the values. These new claim names will be nested
// under the top-level "additionalClaims" claim in ID tokens generated by the Supervisor when this
// OIDCIdentityProvider was used for user authentication. These claims will be made available to all clients.
// This feature is not required to use the Supervisor to provide authentication for Kubernetes clusters, but can be
// used when using the Supervisor for other authentication purposes. When this map is empty or the upstream claims
// are not available, the "additionalClaims" claim will be excluded from the ID tokens generated by the Supervisor.
// +optional
AdditionalClaimMappings map[string]string `json:"additionalClaimMappings,omitempty"`
} }
// OIDCClient contains information about an OIDC client (e.g., client ID and client // OIDCClient contains information about an OIDC client (e.g., client ID and client

View File

@ -438,6 +438,13 @@ func (in *OIDCAuthorizationConfig) DeepCopy() *OIDCAuthorizationConfig {
// 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 *OIDCClaims) DeepCopyInto(out *OIDCClaims) { func (in *OIDCClaims) DeepCopyInto(out *OIDCClaims) {
*out = *in *out = *in
if in.AdditionalClaimMappings != nil {
in, out := &in.AdditionalClaimMappings, &out.AdditionalClaimMappings
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
return return
} }
@ -537,7 +544,7 @@ func (in *OIDCIdentityProviderSpec) DeepCopyInto(out *OIDCIdentityProviderSpec)
**out = **in **out = **in
} }
in.AuthorizationConfig.DeepCopyInto(&out.AuthorizationConfig) in.AuthorizationConfig.DeepCopyInto(&out.AuthorizationConfig)
out.Claims = in.Claims in.Claims.DeepCopyInto(&out.Claims)
out.Client = in.Client out.Client = in.Client
return return
} }

View File

@ -40,6 +40,10 @@ const (
// group names which were mapped from the upstream identity provider. // group names which were mapped from the upstream identity provider.
IDTokenClaimGroups = "groups" IDTokenClaimGroups = "groups"
// IDTokenClaimAdditionalClaims is the top level claim used to hold additional claims in the downstream ID
// token, if any claims are present.
IDTokenClaimAdditionalClaims = "additionalClaims"
// GrantTypeAuthorizationCode is the name of the grant type for authorization code flows defined by the OIDC spec. // GrantTypeAuthorizationCode is the name of the grant type for authorization code flows defined by the OIDC spec.
GrantTypeAuthorizationCode = "authorization_code" GrantTypeAuthorizationCode = "authorization_code"

View File

@ -185,6 +185,24 @@ spec:
description: Claims provides the names of token claims that will be description: Claims provides the names of token claims that will be
used when inspecting an identity from this OIDC identity provider. used when inspecting an identity from this OIDC identity provider.
properties: properties:
additionalClaimMappings:
additionalProperties:
type: string
description: AdditionalClaimMappings allows for additional arbitrary
upstream claim values to be mapped into the "additionalClaims"
claim of the ID tokens generated by the Supervisor. This should
be specified as a map of new claim names as the keys, and upstream
claim names as the values. These new claim names will be nested
under the top-level "additionalClaims" claim in ID tokens generated
by the Supervisor when this OIDCIdentityProvider was used for
user authentication. These claims will be made available to
all clients. This feature is not required to use the Supervisor
to provide authentication for Kubernetes clusters, but can be
used when using the Supervisor for other authentication purposes.
When this map is empty or the upstream claims are not available,
the "additionalClaims" claim will be excluded from the ID tokens
generated by the Supervisor.
type: object
groups: groups:
description: Groups provides the name of the ID token claim or description: Groups provides the name of the ID token claim or
userinfo endpoint response claim that will be used to ascertain userinfo endpoint response claim that will be used to ascertain

View File

@ -1391,6 +1391,7 @@ OIDCClaims provides a mapping from upstream claims into identities.
| Field | Description | Field | Description
| *`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. | *`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 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. | *`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.
| *`additionalClaimMappings`* __object (keys:string, values:string)__ | AdditionalClaimMappings allows for additional arbitrary upstream claim values to be mapped into the "additionalClaims" claim of the ID tokens generated by the Supervisor. This should be specified as a map of new claim names as the keys, and upstream claim names as the values. These new claim names will be nested under the top-level "additionalClaims" claim in ID tokens generated by the Supervisor when this OIDCIdentityProvider was used for user authentication. These claims will be made available to all clients. This feature is not required to use the Supervisor to provide authentication for Kubernetes clusters, but can be used when using the Supervisor for other authentication purposes. When this map is empty or the upstream claims are not available, the "additionalClaims" claim will be excluded from the ID tokens generated by the Supervisor.
|=== |===

View File

@ -138,6 +138,17 @@ type OIDCClaims struct {
// the ID token. // the ID token.
// +optional // +optional
Username string `json:"username"` Username string `json:"username"`
// AdditionalClaimMappings allows for additional arbitrary upstream claim values to be mapped into the
// "additionalClaims" claim of the ID tokens generated by the Supervisor. This should be specified as a map of
// new claim names as the keys, and upstream claim names as the values. These new claim names will be nested
// under the top-level "additionalClaims" claim in ID tokens generated by the Supervisor when this
// OIDCIdentityProvider was used for user authentication. These claims will be made available to all clients.
// This feature is not required to use the Supervisor to provide authentication for Kubernetes clusters, but can be
// used when using the Supervisor for other authentication purposes. When this map is empty or the upstream claims
// are not available, the "additionalClaims" claim will be excluded from the ID tokens generated by the Supervisor.
// +optional
AdditionalClaimMappings map[string]string `json:"additionalClaimMappings,omitempty"`
} }
// OIDCClient contains information about an OIDC client (e.g., client ID and client // OIDCClient contains information about an OIDC client (e.g., client ID and client

View File

@ -438,6 +438,13 @@ func (in *OIDCAuthorizationConfig) DeepCopy() *OIDCAuthorizationConfig {
// 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 *OIDCClaims) DeepCopyInto(out *OIDCClaims) { func (in *OIDCClaims) DeepCopyInto(out *OIDCClaims) {
*out = *in *out = *in
if in.AdditionalClaimMappings != nil {
in, out := &in.AdditionalClaimMappings, &out.AdditionalClaimMappings
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
return return
} }
@ -537,7 +544,7 @@ func (in *OIDCIdentityProviderSpec) DeepCopyInto(out *OIDCIdentityProviderSpec)
**out = **in **out = **in
} }
in.AuthorizationConfig.DeepCopyInto(&out.AuthorizationConfig) in.AuthorizationConfig.DeepCopyInto(&out.AuthorizationConfig)
out.Claims = in.Claims in.Claims.DeepCopyInto(&out.Claims)
out.Client = in.Client out.Client = in.Client
return return
} }

View File

@ -40,6 +40,10 @@ const (
// group names which were mapped from the upstream identity provider. // group names which were mapped from the upstream identity provider.
IDTokenClaimGroups = "groups" IDTokenClaimGroups = "groups"
// IDTokenClaimAdditionalClaims is the top level claim used to hold additional claims in the downstream ID
// token, if any claims are present.
IDTokenClaimAdditionalClaims = "additionalClaims"
// GrantTypeAuthorizationCode is the name of the grant type for authorization code flows defined by the OIDC spec. // GrantTypeAuthorizationCode is the name of the grant type for authorization code flows defined by the OIDC spec.
GrantTypeAuthorizationCode = "authorization_code" GrantTypeAuthorizationCode = "authorization_code"

View File

@ -185,6 +185,24 @@ spec:
description: Claims provides the names of token claims that will be description: Claims provides the names of token claims that will be
used when inspecting an identity from this OIDC identity provider. used when inspecting an identity from this OIDC identity provider.
properties: properties:
additionalClaimMappings:
additionalProperties:
type: string
description: AdditionalClaimMappings allows for additional arbitrary
upstream claim values to be mapped into the "additionalClaims"
claim of the ID tokens generated by the Supervisor. This should
be specified as a map of new claim names as the keys, and upstream
claim names as the values. These new claim names will be nested
under the top-level "additionalClaims" claim in ID tokens generated
by the Supervisor when this OIDCIdentityProvider was used for
user authentication. These claims will be made available to
all clients. This feature is not required to use the Supervisor
to provide authentication for Kubernetes clusters, but can be
used when using the Supervisor for other authentication purposes.
When this map is empty or the upstream claims are not available,
the "additionalClaims" claim will be excluded from the ID tokens
generated by the Supervisor.
type: object
groups: groups:
description: Groups provides the name of the ID token claim or description: Groups provides the name of the ID token claim or
userinfo endpoint response claim that will be used to ascertain userinfo endpoint response claim that will be used to ascertain

View File

@ -1391,6 +1391,7 @@ OIDCClaims provides a mapping from upstream claims into identities.
| Field | Description | Field | Description
| *`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. | *`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 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. | *`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.
| *`additionalClaimMappings`* __object (keys:string, values:string)__ | AdditionalClaimMappings allows for additional arbitrary upstream claim values to be mapped into the "additionalClaims" claim of the ID tokens generated by the Supervisor. This should be specified as a map of new claim names as the keys, and upstream claim names as the values. These new claim names will be nested under the top-level "additionalClaims" claim in ID tokens generated by the Supervisor when this OIDCIdentityProvider was used for user authentication. These claims will be made available to all clients. This feature is not required to use the Supervisor to provide authentication for Kubernetes clusters, but can be used when using the Supervisor for other authentication purposes. When this map is empty or the upstream claims are not available, the "additionalClaims" claim will be excluded from the ID tokens generated by the Supervisor.
|=== |===

View File

@ -138,6 +138,17 @@ type OIDCClaims struct {
// the ID token. // the ID token.
// +optional // +optional
Username string `json:"username"` Username string `json:"username"`
// AdditionalClaimMappings allows for additional arbitrary upstream claim values to be mapped into the
// "additionalClaims" claim of the ID tokens generated by the Supervisor. This should be specified as a map of
// new claim names as the keys, and upstream claim names as the values. These new claim names will be nested
// under the top-level "additionalClaims" claim in ID tokens generated by the Supervisor when this
// OIDCIdentityProvider was used for user authentication. These claims will be made available to all clients.
// This feature is not required to use the Supervisor to provide authentication for Kubernetes clusters, but can be
// used when using the Supervisor for other authentication purposes. When this map is empty or the upstream claims
// are not available, the "additionalClaims" claim will be excluded from the ID tokens generated by the Supervisor.
// +optional
AdditionalClaimMappings map[string]string `json:"additionalClaimMappings,omitempty"`
} }
// OIDCClient contains information about an OIDC client (e.g., client ID and client // OIDCClient contains information about an OIDC client (e.g., client ID and client

View File

@ -438,6 +438,13 @@ func (in *OIDCAuthorizationConfig) DeepCopy() *OIDCAuthorizationConfig {
// 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 *OIDCClaims) DeepCopyInto(out *OIDCClaims) { func (in *OIDCClaims) DeepCopyInto(out *OIDCClaims) {
*out = *in *out = *in
if in.AdditionalClaimMappings != nil {
in, out := &in.AdditionalClaimMappings, &out.AdditionalClaimMappings
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
return return
} }
@ -537,7 +544,7 @@ func (in *OIDCIdentityProviderSpec) DeepCopyInto(out *OIDCIdentityProviderSpec)
**out = **in **out = **in
} }
in.AuthorizationConfig.DeepCopyInto(&out.AuthorizationConfig) in.AuthorizationConfig.DeepCopyInto(&out.AuthorizationConfig)
out.Claims = in.Claims in.Claims.DeepCopyInto(&out.Claims)
out.Client = in.Client out.Client = in.Client
return return
} }

View File

@ -40,6 +40,10 @@ const (
// group names which were mapped from the upstream identity provider. // group names which were mapped from the upstream identity provider.
IDTokenClaimGroups = "groups" IDTokenClaimGroups = "groups"
// IDTokenClaimAdditionalClaims is the top level claim used to hold additional claims in the downstream ID
// token, if any claims are present.
IDTokenClaimAdditionalClaims = "additionalClaims"
// GrantTypeAuthorizationCode is the name of the grant type for authorization code flows defined by the OIDC spec. // GrantTypeAuthorizationCode is the name of the grant type for authorization code flows defined by the OIDC spec.
GrantTypeAuthorizationCode = "authorization_code" GrantTypeAuthorizationCode = "authorization_code"

View File

@ -185,6 +185,24 @@ spec:
description: Claims provides the names of token claims that will be description: Claims provides the names of token claims that will be
used when inspecting an identity from this OIDC identity provider. used when inspecting an identity from this OIDC identity provider.
properties: properties:
additionalClaimMappings:
additionalProperties:
type: string
description: AdditionalClaimMappings allows for additional arbitrary
upstream claim values to be mapped into the "additionalClaims"
claim of the ID tokens generated by the Supervisor. This should
be specified as a map of new claim names as the keys, and upstream
claim names as the values. These new claim names will be nested
under the top-level "additionalClaims" claim in ID tokens generated
by the Supervisor when this OIDCIdentityProvider was used for
user authentication. These claims will be made available to
all clients. This feature is not required to use the Supervisor
to provide authentication for Kubernetes clusters, but can be
used when using the Supervisor for other authentication purposes.
When this map is empty or the upstream claims are not available,
the "additionalClaims" claim will be excluded from the ID tokens
generated by the Supervisor.
type: object
groups: groups:
description: Groups provides the name of the ID token claim or description: Groups provides the name of the ID token claim or
userinfo endpoint response claim that will be used to ascertain userinfo endpoint response claim that will be used to ascertain

View File

@ -1391,6 +1391,7 @@ OIDCClaims provides a mapping from upstream claims into identities.
| Field | Description | Field | Description
| *`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. | *`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 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. | *`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.
| *`additionalClaimMappings`* __object (keys:string, values:string)__ | AdditionalClaimMappings allows for additional arbitrary upstream claim values to be mapped into the "additionalClaims" claim of the ID tokens generated by the Supervisor. This should be specified as a map of new claim names as the keys, and upstream claim names as the values. These new claim names will be nested under the top-level "additionalClaims" claim in ID tokens generated by the Supervisor when this OIDCIdentityProvider was used for user authentication. These claims will be made available to all clients. This feature is not required to use the Supervisor to provide authentication for Kubernetes clusters, but can be used when using the Supervisor for other authentication purposes. When this map is empty or the upstream claims are not available, the "additionalClaims" claim will be excluded from the ID tokens generated by the Supervisor.
|=== |===

View File

@ -138,6 +138,17 @@ type OIDCClaims struct {
// the ID token. // the ID token.
// +optional // +optional
Username string `json:"username"` Username string `json:"username"`
// AdditionalClaimMappings allows for additional arbitrary upstream claim values to be mapped into the
// "additionalClaims" claim of the ID tokens generated by the Supervisor. This should be specified as a map of
// new claim names as the keys, and upstream claim names as the values. These new claim names will be nested
// under the top-level "additionalClaims" claim in ID tokens generated by the Supervisor when this
// OIDCIdentityProvider was used for user authentication. These claims will be made available to all clients.
// This feature is not required to use the Supervisor to provide authentication for Kubernetes clusters, but can be
// used when using the Supervisor for other authentication purposes. When this map is empty or the upstream claims
// are not available, the "additionalClaims" claim will be excluded from the ID tokens generated by the Supervisor.
// +optional
AdditionalClaimMappings map[string]string `json:"additionalClaimMappings,omitempty"`
} }
// OIDCClient contains information about an OIDC client (e.g., client ID and client // OIDCClient contains information about an OIDC client (e.g., client ID and client

View File

@ -438,6 +438,13 @@ func (in *OIDCAuthorizationConfig) DeepCopy() *OIDCAuthorizationConfig {
// 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 *OIDCClaims) DeepCopyInto(out *OIDCClaims) { func (in *OIDCClaims) DeepCopyInto(out *OIDCClaims) {
*out = *in *out = *in
if in.AdditionalClaimMappings != nil {
in, out := &in.AdditionalClaimMappings, &out.AdditionalClaimMappings
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
return return
} }
@ -537,7 +544,7 @@ func (in *OIDCIdentityProviderSpec) DeepCopyInto(out *OIDCIdentityProviderSpec)
**out = **in **out = **in
} }
in.AuthorizationConfig.DeepCopyInto(&out.AuthorizationConfig) in.AuthorizationConfig.DeepCopyInto(&out.AuthorizationConfig)
out.Claims = in.Claims in.Claims.DeepCopyInto(&out.Claims)
out.Client = in.Client out.Client = in.Client
return return
} }

View File

@ -40,6 +40,10 @@ const (
// group names which were mapped from the upstream identity provider. // group names which were mapped from the upstream identity provider.
IDTokenClaimGroups = "groups" IDTokenClaimGroups = "groups"
// IDTokenClaimAdditionalClaims is the top level claim used to hold additional claims in the downstream ID
// token, if any claims are present.
IDTokenClaimAdditionalClaims = "additionalClaims"
// GrantTypeAuthorizationCode is the name of the grant type for authorization code flows defined by the OIDC spec. // GrantTypeAuthorizationCode is the name of the grant type for authorization code flows defined by the OIDC spec.
GrantTypeAuthorizationCode = "authorization_code" GrantTypeAuthorizationCode = "authorization_code"

View File

@ -185,6 +185,24 @@ spec:
description: Claims provides the names of token claims that will be description: Claims provides the names of token claims that will be
used when inspecting an identity from this OIDC identity provider. used when inspecting an identity from this OIDC identity provider.
properties: properties:
additionalClaimMappings:
additionalProperties:
type: string
description: AdditionalClaimMappings allows for additional arbitrary
upstream claim values to be mapped into the "additionalClaims"
claim of the ID tokens generated by the Supervisor. This should
be specified as a map of new claim names as the keys, and upstream
claim names as the values. These new claim names will be nested
under the top-level "additionalClaims" claim in ID tokens generated
by the Supervisor when this OIDCIdentityProvider was used for
user authentication. These claims will be made available to
all clients. This feature is not required to use the Supervisor
to provide authentication for Kubernetes clusters, but can be
used when using the Supervisor for other authentication purposes.
When this map is empty or the upstream claims are not available,
the "additionalClaims" claim will be excluded from the ID tokens
generated by the Supervisor.
type: object
groups: groups:
description: Groups provides the name of the ID token claim or description: Groups provides the name of the ID token claim or
userinfo endpoint response claim that will be used to ascertain userinfo endpoint response claim that will be used to ascertain

View File

@ -1387,6 +1387,7 @@ OIDCClaims provides a mapping from upstream claims into identities.
| Field | Description | Field | Description
| *`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. | *`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 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. | *`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.
| *`additionalClaimMappings`* __object (keys:string, values:string)__ | AdditionalClaimMappings allows for additional arbitrary upstream claim values to be mapped into the "additionalClaims" claim of the ID tokens generated by the Supervisor. This should be specified as a map of new claim names as the keys, and upstream claim names as the values. These new claim names will be nested under the top-level "additionalClaims" claim in ID tokens generated by the Supervisor when this OIDCIdentityProvider was used for user authentication. These claims will be made available to all clients. This feature is not required to use the Supervisor to provide authentication for Kubernetes clusters, but can be used when using the Supervisor for other authentication purposes. When this map is empty or the upstream claims are not available, the "additionalClaims" claim will be excluded from the ID tokens generated by the Supervisor.
|=== |===

View File

@ -138,6 +138,17 @@ type OIDCClaims struct {
// the ID token. // the ID token.
// +optional // +optional
Username string `json:"username"` Username string `json:"username"`
// AdditionalClaimMappings allows for additional arbitrary upstream claim values to be mapped into the
// "additionalClaims" claim of the ID tokens generated by the Supervisor. This should be specified as a map of
// new claim names as the keys, and upstream claim names as the values. These new claim names will be nested
// under the top-level "additionalClaims" claim in ID tokens generated by the Supervisor when this
// OIDCIdentityProvider was used for user authentication. These claims will be made available to all clients.
// This feature is not required to use the Supervisor to provide authentication for Kubernetes clusters, but can be
// used when using the Supervisor for other authentication purposes. When this map is empty or the upstream claims
// are not available, the "additionalClaims" claim will be excluded from the ID tokens generated by the Supervisor.
// +optional
AdditionalClaimMappings map[string]string `json:"additionalClaimMappings,omitempty"`
} }
// OIDCClient contains information about an OIDC client (e.g., client ID and client // OIDCClient contains information about an OIDC client (e.g., client ID and client

View File

@ -438,6 +438,13 @@ func (in *OIDCAuthorizationConfig) DeepCopy() *OIDCAuthorizationConfig {
// 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 *OIDCClaims) DeepCopyInto(out *OIDCClaims) { func (in *OIDCClaims) DeepCopyInto(out *OIDCClaims) {
*out = *in *out = *in
if in.AdditionalClaimMappings != nil {
in, out := &in.AdditionalClaimMappings, &out.AdditionalClaimMappings
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
return return
} }
@ -537,7 +544,7 @@ func (in *OIDCIdentityProviderSpec) DeepCopyInto(out *OIDCIdentityProviderSpec)
**out = **in **out = **in
} }
in.AuthorizationConfig.DeepCopyInto(&out.AuthorizationConfig) in.AuthorizationConfig.DeepCopyInto(&out.AuthorizationConfig)
out.Claims = in.Claims in.Claims.DeepCopyInto(&out.Claims)
out.Client = in.Client out.Client = in.Client
return return
} }

View File

@ -40,6 +40,10 @@ const (
// group names which were mapped from the upstream identity provider. // group names which were mapped from the upstream identity provider.
IDTokenClaimGroups = "groups" IDTokenClaimGroups = "groups"
// IDTokenClaimAdditionalClaims is the top level claim used to hold additional claims in the downstream ID
// token, if any claims are present.
IDTokenClaimAdditionalClaims = "additionalClaims"
// GrantTypeAuthorizationCode is the name of the grant type for authorization code flows defined by the OIDC spec. // GrantTypeAuthorizationCode is the name of the grant type for authorization code flows defined by the OIDC spec.
GrantTypeAuthorizationCode = "authorization_code" GrantTypeAuthorizationCode = "authorization_code"

View File

@ -185,6 +185,24 @@ spec:
description: Claims provides the names of token claims that will be description: Claims provides the names of token claims that will be
used when inspecting an identity from this OIDC identity provider. used when inspecting an identity from this OIDC identity provider.
properties: properties:
additionalClaimMappings:
additionalProperties:
type: string
description: AdditionalClaimMappings allows for additional arbitrary
upstream claim values to be mapped into the "additionalClaims"
claim of the ID tokens generated by the Supervisor. This should
be specified as a map of new claim names as the keys, and upstream
claim names as the values. These new claim names will be nested
under the top-level "additionalClaims" claim in ID tokens generated
by the Supervisor when this OIDCIdentityProvider was used for
user authentication. These claims will be made available to
all clients. This feature is not required to use the Supervisor
to provide authentication for Kubernetes clusters, but can be
used when using the Supervisor for other authentication purposes.
When this map is empty or the upstream claims are not available,
the "additionalClaims" claim will be excluded from the ID tokens
generated by the Supervisor.
type: object
groups: groups:
description: Groups provides the name of the ID token claim or description: Groups provides the name of the ID token claim or
userinfo endpoint response claim that will be used to ascertain userinfo endpoint response claim that will be used to ascertain

View File

@ -138,6 +138,17 @@ type OIDCClaims struct {
// the ID token. // the ID token.
// +optional // +optional
Username string `json:"username"` Username string `json:"username"`
// AdditionalClaimMappings allows for additional arbitrary upstream claim values to be mapped into the
// "additionalClaims" claim of the ID tokens generated by the Supervisor. This should be specified as a map of
// new claim names as the keys, and upstream claim names as the values. These new claim names will be nested
// under the top-level "additionalClaims" claim in ID tokens generated by the Supervisor when this
// OIDCIdentityProvider was used for user authentication. These claims will be made available to all clients.
// This feature is not required to use the Supervisor to provide authentication for Kubernetes clusters, but can be
// used when using the Supervisor for other authentication purposes. When this map is empty or the upstream claims
// are not available, the "additionalClaims" claim will be excluded from the ID tokens generated by the Supervisor.
// +optional
AdditionalClaimMappings map[string]string `json:"additionalClaimMappings,omitempty"`
} }
// OIDCClient contains information about an OIDC client (e.g., client ID and client // OIDCClient contains information about an OIDC client (e.g., client ID and client

View File

@ -438,6 +438,13 @@ func (in *OIDCAuthorizationConfig) DeepCopy() *OIDCAuthorizationConfig {
// 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 *OIDCClaims) DeepCopyInto(out *OIDCClaims) { func (in *OIDCClaims) DeepCopyInto(out *OIDCClaims) {
*out = *in *out = *in
if in.AdditionalClaimMappings != nil {
in, out := &in.AdditionalClaimMappings, &out.AdditionalClaimMappings
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
return return
} }
@ -537,7 +544,7 @@ func (in *OIDCIdentityProviderSpec) DeepCopyInto(out *OIDCIdentityProviderSpec)
**out = **in **out = **in
} }
in.AuthorizationConfig.DeepCopyInto(&out.AuthorizationConfig) in.AuthorizationConfig.DeepCopyInto(&out.AuthorizationConfig)
out.Claims = in.Claims in.Claims.DeepCopyInto(&out.Claims)
out.Client = in.Client out.Client = in.Client
return return
} }

View File

@ -40,6 +40,10 @@ const (
// group names which were mapped from the upstream identity provider. // group names which were mapped from the upstream identity provider.
IDTokenClaimGroups = "groups" IDTokenClaimGroups = "groups"
// IDTokenClaimAdditionalClaims is the top level claim used to hold additional claims in the downstream ID
// token, if any claims are present.
IDTokenClaimAdditionalClaims = "additionalClaims"
// GrantTypeAuthorizationCode is the name of the grant type for authorization code flows defined by the OIDC spec. // GrantTypeAuthorizationCode is the name of the grant type for authorization code flows defined by the OIDC spec.
GrantTypeAuthorizationCode = "authorization_code" GrantTypeAuthorizationCode = "authorization_code"

View File

@ -215,6 +215,7 @@ func (c *oidcWatcherController) validateUpstream(ctx controllerlib.Context, upst
GroupsClaim: upstream.Spec.Claims.Groups, GroupsClaim: upstream.Spec.Claims.Groups,
AllowPasswordGrant: authorizationConfig.AllowPasswordGrant, AllowPasswordGrant: authorizationConfig.AllowPasswordGrant,
AdditionalAuthcodeParams: additionalAuthcodeAuthorizeParameters, AdditionalAuthcodeParams: additionalAuthcodeAuthorizeParameters,
AdditionalClaimMappings: upstream.Spec.Claims.AdditionalClaimMappings,
ResourceUID: upstream.UID, ResourceUID: upstream.UID,
} }

View File

@ -999,6 +999,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana
GroupsClaim: testGroupsClaim, GroupsClaim: testGroupsClaim,
AllowPasswordGrant: true, AllowPasswordGrant: true,
AdditionalAuthcodeParams: map[string]string{}, AdditionalAuthcodeParams: map[string]string{},
AdditionalClaimMappings: nil, // Does not default to empty map
ResourceUID: testUID, ResourceUID: testUID,
}, },
}, },
@ -1054,6 +1055,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana
GroupsClaim: testGroupsClaim, GroupsClaim: testGroupsClaim,
AllowPasswordGrant: false, AllowPasswordGrant: false,
AdditionalAuthcodeParams: map[string]string{}, AdditionalAuthcodeParams: map[string]string{},
AdditionalClaimMappings: nil, // Does not default to empty map
ResourceUID: testUID, ResourceUID: testUID,
}, },
}, },
@ -1109,6 +1111,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana
GroupsClaim: testGroupsClaim, GroupsClaim: testGroupsClaim,
AllowPasswordGrant: false, AllowPasswordGrant: false,
AdditionalAuthcodeParams: map[string]string{}, AdditionalAuthcodeParams: map[string]string{},
AdditionalClaimMappings: nil, // Does not default to empty map
ResourceUID: testUID, ResourceUID: testUID,
}, },
}, },
@ -1167,6 +1170,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana
GroupsClaim: testGroupsClaim, GroupsClaim: testGroupsClaim,
AllowPasswordGrant: false, AllowPasswordGrant: false,
AdditionalAuthcodeParams: map[string]string{}, AdditionalAuthcodeParams: map[string]string{},
AdditionalClaimMappings: nil, // Does not default to empty map
ResourceUID: testUID, ResourceUID: testUID,
}, },
}, },
@ -1195,7 +1199,13 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana
AdditionalAuthorizeParameters: testAdditionalParams, AdditionalAuthorizeParameters: testAdditionalParams,
AllowPasswordGrant: true, AllowPasswordGrant: true,
}, },
Claims: v1alpha1.OIDCClaims{Groups: testGroupsClaim, Username: testUsernameClaim}, Claims: v1alpha1.OIDCClaims{
Groups: testGroupsClaim,
Username: testUsernameClaim,
AdditionalClaimMappings: map[string]string{
"downstream": "upstream",
},
},
}, },
Status: v1alpha1.OIDCIdentityProviderStatus{ Status: v1alpha1.OIDCIdentityProviderStatus{
Phase: "Ready", Phase: "Ready",
@ -1227,7 +1237,10 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana
GroupsClaim: testGroupsClaim, GroupsClaim: testGroupsClaim,
AllowPasswordGrant: true, AllowPasswordGrant: true,
AdditionalAuthcodeParams: testExpectedAdditionalParams, AdditionalAuthcodeParams: testExpectedAdditionalParams,
ResourceUID: testUID, AdditionalClaimMappings: map[string]string{
"downstream": "upstream",
},
ResourceUID: testUID,
}, },
}, },
wantResultingUpstreams: []v1alpha1.OIDCIdentityProvider{{ wantResultingUpstreams: []v1alpha1.OIDCIdentityProvider{{
@ -1442,6 +1455,7 @@ oidc: issuer did not match the issuer returned by provider, expected "` + testIs
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].GetAdditionalAuthcodeParams(), actualIDP.GetAdditionalAuthcodeParams())
require.Equal(t, tt.wantResultingCache[i].GetAdditionalClaimMappings(), actualIDP.GetAdditionalClaimMappings())
require.Equal(t, tt.wantResultingCache[i].GetResourceUID(), actualIDP.GetResourceUID()) require.Equal(t, tt.wantResultingCache[i].GetResourceUID(), actualIDP.GetResourceUID())
require.Equal(t, tt.wantResultingCache[i].GetRevocationURL(), actualIDP.GetRevocationURL()) require.Equal(t, tt.wantResultingCache[i].GetRevocationURL(), actualIDP.GetRevocationURL())
require.ElementsMatch(t, tt.wantResultingCache[i].GetScopes(), actualIDP.GetScopes()) require.ElementsMatch(t, tt.wantResultingCache[i].GetScopes(), actualIDP.GetScopes())

View File

@ -88,6 +88,20 @@ func (mr *MockUpstreamOIDCIdentityProviderIMockRecorder) GetAdditionalAuthcodePa
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAdditionalAuthcodeParams", reflect.TypeOf((*MockUpstreamOIDCIdentityProviderI)(nil).GetAdditionalAuthcodeParams)) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAdditionalAuthcodeParams", reflect.TypeOf((*MockUpstreamOIDCIdentityProviderI)(nil).GetAdditionalAuthcodeParams))
} }
// GetAdditionalClaimMappings mocks base method.
func (m *MockUpstreamOIDCIdentityProviderI) GetAdditionalClaimMappings() map[string]string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAdditionalClaimMappings")
ret0, _ := ret[0].(map[string]string)
return ret0
}
// GetAdditionalClaimMappings indicates an expected call of GetAdditionalClaimMappings.
func (mr *MockUpstreamOIDCIdentityProviderIMockRecorder) GetAdditionalClaimMappings() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAdditionalClaimMappings", reflect.TypeOf((*MockUpstreamOIDCIdentityProviderI)(nil).GetAdditionalClaimMappings))
}
// 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()

View File

@ -150,7 +150,7 @@ func handleAuthRequestForLDAPUpstreamCLIFlow(
groups := authenticateResponse.User.GetGroups() groups := authenticateResponse.User.GetGroups()
customSessionData := downstreamsession.MakeDownstreamLDAPOrADCustomSessionData(ldapUpstream, idpType, authenticateResponse, username) customSessionData := downstreamsession.MakeDownstreamLDAPOrADCustomSessionData(ldapUpstream, idpType, authenticateResponse, username)
openIDSession := downstreamsession.MakeDownstreamSession(subject, username, groups, openIDSession := downstreamsession.MakeDownstreamSession(subject, username, groups,
authorizeRequester.GetGrantedScopes(), authorizeRequester.GetClient().GetID(), customSessionData) authorizeRequester.GetGrantedScopes(), authorizeRequester.GetClient().GetID(), customSessionData, map[string]interface{}{})
oidc.PerformAuthcodeRedirect(r, w, oauthHelper, authorizeRequester, openIDSession, true) oidc.PerformAuthcodeRedirect(r, w, oauthHelper, authorizeRequester, openIDSession, true)
return nil return nil
@ -243,6 +243,8 @@ func handleAuthRequestForOIDCUpstreamPasswordGrant(
return nil return nil
} }
additionalClaims := downstreamsession.MapAdditionalClaimsFromUpstreamIDToken(oidcUpstream, token.IDToken.Claims)
customSessionData, err := downstreamsession.MakeDownstreamOIDCCustomSessionData(oidcUpstream, token, username) customSessionData, err := downstreamsession.MakeDownstreamOIDCCustomSessionData(oidcUpstream, token, username)
if err != nil { if err != nil {
oidc.WriteAuthorizeError(r, w, oauthHelper, authorizeRequester, oidc.WriteAuthorizeError(r, w, oauthHelper, authorizeRequester,
@ -252,7 +254,7 @@ func handleAuthRequestForOIDCUpstreamPasswordGrant(
} }
openIDSession := downstreamsession.MakeDownstreamSession(subject, username, groups, openIDSession := downstreamsession.MakeDownstreamSession(subject, username, groups,
authorizeRequester.GetGrantedScopes(), authorizeRequester.GetClient().GetID(), customSessionData) authorizeRequester.GetGrantedScopes(), authorizeRequester.GetClient().GetID(), customSessionData, additionalClaims)
oidc.PerformAuthcodeRedirect(r, w, oauthHelper, authorizeRequester, openIDSession, true) oidc.PerformAuthcodeRedirect(r, w, oauthHelper, authorizeRequester, openIDSession, true)

View File

@ -582,6 +582,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantUnnecessaryStoredRecords int wantUnnecessaryStoredRecords int
wantPasswordGrantCall *expectedPasswordGrant wantPasswordGrantCall *expectedPasswordGrant
wantDownstreamCustomSessionData *psession.CustomSessionData wantDownstreamCustomSessionData *psession.CustomSessionData
wantAdditionalClaims map[string]interface{}
} }
tests := []testCase{ tests := []testCase{
{ {
@ -711,6 +712,68 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: expectedHappyOIDCPasswordGrantCustomSession, wantDownstreamCustomSessionData: expectedHappyOIDCPasswordGrantCustomSession,
}, },
{
name: "OIDC upstream password grant happy path using GET with additional claim mappings",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().
WithAdditionalClaimMappings(map[string]string{
"downstreamCustomClaim": "upstreamCustomClaim",
"downstreamOtherClaim": "upstreamOtherClaim",
"downstreamMissingClaim": "upstreamMissingClaim",
}).
WithIDTokenClaim("upstreamCustomClaim", "i am a claim value").
WithIDTokenClaim("upstreamOtherClaim", "other claim value").
Build()),
method: http.MethodGet,
path: happyGetRequestPath,
customUsernameHeader: pointer.String(oidcUpstreamUsername),
customPasswordHeader: pointer.String(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound,
wantContentType: htmlContentType,
wantRedirectLocationRegexp: happyAuthcodeDownstreamRedirectLocationRegexp,
wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped,
wantDownstreamIDTokenUsername: oidcUpstreamUsername,
wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership,
wantDownstreamRequestedScopes: happyDownstreamScopesRequested,
wantDownstreamRedirectURI: downstreamRedirectURI,
wantDownstreamGrantedScopes: happyDownstreamScopesGranted,
wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: expectedHappyOIDCPasswordGrantCustomSession,
wantAdditionalClaims: map[string]interface{}{
"downstreamCustomClaim": "i am a claim value",
"downstreamOtherClaim": "other claim value",
},
},
{
name: "OIDC upstream password grant happy path using GET with additional claim mappings, when upstream claims are not available",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().
WithAdditionalClaimMappings(map[string]string{
"downstream": "upstream",
}).
WithIDTokenClaim("not-upstream", "value").
Build()),
method: http.MethodGet,
path: happyGetRequestPath,
customUsernameHeader: pointer.String(oidcUpstreamUsername),
customPasswordHeader: pointer.String(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound,
wantContentType: htmlContentType,
wantRedirectLocationRegexp: happyAuthcodeDownstreamRedirectLocationRegexp,
wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped,
wantDownstreamIDTokenUsername: oidcUpstreamUsername,
wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership,
wantDownstreamRequestedScopes: happyDownstreamScopesRequested,
wantDownstreamRedirectURI: downstreamRedirectURI,
wantDownstreamGrantedScopes: happyDownstreamScopesGranted,
wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: expectedHappyOIDCPasswordGrantCustomSession,
wantAdditionalClaims: nil, // downstream claims are empty
},
{ {
name: "LDAP cli upstream happy path using GET", name: "LDAP cli upstream happy path using GET",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
@ -3126,6 +3189,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
test.wantDownstreamClientID, test.wantDownstreamClientID,
test.wantDownstreamRedirectURI, test.wantDownstreamRedirectURI,
test.wantDownstreamCustomSessionData, test.wantDownstreamCustomSessionData,
test.wantAdditionalClaims,
) )
default: default:
require.Empty(t, rsp.Header().Values("Location")) require.Empty(t, rsp.Header().Values("Location"))
@ -3176,9 +3240,15 @@ func TestAuthorizationEndpoint(t *testing.T) {
oidcClientsClient := supervisorClient.ConfigV1alpha1().OIDCClients("some-namespace") oidcClientsClient := supervisorClient.ConfigV1alpha1().OIDCClients("some-namespace")
oauthHelperWithRealStorage, kubeOauthStore := createOauthHelperWithRealStorage(secretsClient, oidcClientsClient) oauthHelperWithRealStorage, kubeOauthStore := createOauthHelperWithRealStorage(secretsClient, oidcClientsClient)
oauthHelperWithNullStorage, _ := createOauthHelperWithNullStorage(secretsClient, oidcClientsClient) oauthHelperWithNullStorage, _ := createOauthHelperWithNullStorage(secretsClient, oidcClientsClient)
idps := test.idps.Build()
if len(test.wantAdditionalClaims) > 0 {
require.True(t, len(idps.GetOIDCIdentityProviders()) > 0, "wantAdditionalClaims requires at least one OIDC IDP")
}
subject := NewHandler( subject := NewHandler(
downstreamIssuer, downstreamIssuer,
test.idps.Build(), idps,
oauthHelperWithNullStorage, oauthHelperWithRealStorage, oauthHelperWithNullStorage, oauthHelperWithRealStorage,
test.generateCSRF, test.generatePKCE, test.generateNonce, test.generateCSRF, test.generatePKCE, test.generateNonce,
test.stateEncoder, test.cookieEncoder, test.stateEncoder, test.cookieEncoder,

View File

@ -74,13 +74,15 @@ func NewHandler(
return httperr.Wrap(http.StatusUnprocessableEntity, err.Error(), err) return httperr.Wrap(http.StatusUnprocessableEntity, err.Error(), err)
} }
additionalClaims := downstreamsession.MapAdditionalClaimsFromUpstreamIDToken(upstreamIDPConfig, token.IDToken.Claims)
customSessionData, err := downstreamsession.MakeDownstreamOIDCCustomSessionData(upstreamIDPConfig, token, username) customSessionData, err := downstreamsession.MakeDownstreamOIDCCustomSessionData(upstreamIDPConfig, token, username)
if err != nil { if err != nil {
return httperr.Wrap(http.StatusUnprocessableEntity, err.Error(), err) return httperr.Wrap(http.StatusUnprocessableEntity, err.Error(), err)
} }
openIDSession := downstreamsession.MakeDownstreamSession(subject, username, groups, openIDSession := downstreamsession.MakeDownstreamSession(subject, username, groups,
authorizeRequester.GetGrantedScopes(), authorizeRequester.GetClient().GetID(), customSessionData) authorizeRequester.GetGrantedScopes(), authorizeRequester.GetClient().GetID(), customSessionData, additionalClaims)
authorizeResponder, err := oauthHelper.NewAuthorizeResponse(r.Context(), authorizeRequester, openIDSession) authorizeResponder, err := oauthHelper.NewAuthorizeResponse(r.Context(), authorizeRequester, openIDSession)
if err != nil { if err != nil {

View File

@ -189,6 +189,7 @@ func TestCallbackEndpoint(t *testing.T) {
wantDownstreamPKCEChallenge string wantDownstreamPKCEChallenge string
wantDownstreamPKCEChallengeMethod string wantDownstreamPKCEChallengeMethod string
wantDownstreamCustomSessionData *psession.CustomSessionData wantDownstreamCustomSessionData *psession.CustomSessionData
wantAdditionalClaims map[string]interface{}
wantAuthcodeExchangeCall *expectedAuthcodeExchange wantAuthcodeExchangeCall *expectedAuthcodeExchange
}{ }{
@ -223,6 +224,49 @@ func TestCallbackEndpoint(t *testing.T) {
args: happyExchangeAndValidateTokensArgs, args: happyExchangeAndValidateTokensArgs,
}, },
}, },
{
name: "GET with good state and cookie with additional params",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().
WithAdditionalClaimMappings(map[string]string{
"downstreamCustomClaim": "upstreamCustomClaim",
"downstreamOtherClaim": "upstreamOtherClaim",
"downstreamMissingClaim": "upstreamMissingClaim",
}).
WithIDTokenClaim("upstreamCustomClaim", "i am a claim value").
WithIDTokenClaim("upstreamOtherClaim", "other claim value").
Build()),
method: http.MethodGet,
path: newRequestPath().WithState(
happyUpstreamStateParam().WithAuthorizeRequestParams(
shallowCopyAndModifyQuery(
happyDownstreamRequestParamsQuery,
map[string]string{"response_mode": "form_post"},
).Encode(),
).Build(t, happyStateCodec),
).String(),
csrfCookie: happyCSRFCookie,
wantStatus: http.StatusOK,
wantContentType: "text/html;charset=UTF-8",
wantBodyFormResponseRegexp: `<code id="manual-auth-code">(.+)</code>`,
wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped,
wantDownstreamIDTokenUsername: oidcUpstreamUsername,
wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership,
wantDownstreamRequestedScopes: happyDownstreamScopesRequested,
wantDownstreamGrantedScopes: happyDownstreamScopesGranted,
wantDownstreamNonce: downstreamNonce,
wantDownstreamClientID: downstreamPinnipedClientID,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: happyDownstreamCustomSessionData,
wantAuthcodeExchangeCall: &expectedAuthcodeExchange{
performedByUpstreamName: happyUpstreamIDPName,
args: happyExchangeAndValidateTokensArgs,
},
wantAdditionalClaims: map[string]interface{}{
"downstreamCustomClaim": "i am a claim value",
"downstreamOtherClaim": "other claim value",
},
},
{ {
name: "GET with good state and cookie and successful upstream token exchange returns 303 to downstream client callback with its state and code", name: "GET with good state and cookie and successful upstream token exchange returns 303 to downstream client callback with its state and code",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()),
@ -1463,6 +1507,7 @@ func TestCallbackEndpoint(t *testing.T) {
test.wantDownstreamClientID, test.wantDownstreamClientID,
downstreamRedirectURI, downstreamRedirectURI,
test.wantDownstreamCustomSessionData, test.wantDownstreamCustomSessionData,
test.wantAdditionalClaims,
) )
// Otherwise, expect an empty response body. // Otherwise, expect an empty response body.
@ -1490,6 +1535,7 @@ func TestCallbackEndpoint(t *testing.T) {
test.wantDownstreamClientID, test.wantDownstreamClientID,
downstreamRedirectURI, downstreamRedirectURI,
test.wantDownstreamCustomSessionData, test.wantDownstreamCustomSessionData,
test.wantAdditionalClaims,
) )
} }
}) })

View File

@ -70,7 +70,7 @@ func NewHandler(issuerURL string) http.Handler {
TokenEndpointAuthMethodsSupported: []string{"client_secret_basic"}, TokenEndpointAuthMethodsSupported: []string{"client_secret_basic"},
CodeChallengeMethodsSupported: []string{"S256"}, CodeChallengeMethodsSupported: []string{"S256"},
ScopesSupported: []string{oidcapi.ScopeOpenID, oidcapi.ScopeOfflineAccess, oidcapi.ScopeRequestAudience, oidcapi.ScopeUsername, oidcapi.ScopeGroups}, ScopesSupported: []string{oidcapi.ScopeOpenID, oidcapi.ScopeOfflineAccess, oidcapi.ScopeRequestAudience, oidcapi.ScopeUsername, oidcapi.ScopeGroups},
ClaimsSupported: []string{oidcapi.IDTokenClaimUsername, oidcapi.IDTokenClaimGroups}, ClaimsSupported: []string{oidcapi.IDTokenClaimUsername, oidcapi.IDTokenClaimGroups, oidcapi.IDTokenClaimAdditionalClaims},
} }
var b bytes.Buffer var b bytes.Buffer

View File

@ -47,7 +47,7 @@ func TestDiscovery(t *testing.T) {
"token_endpoint_auth_methods_supported": ["client_secret_basic"], "token_endpoint_auth_methods_supported": ["client_secret_basic"],
"scopes_supported": ["openid", "offline_access", "pinniped:request-audience", "username", "groups"], "scopes_supported": ["openid", "offline_access", "pinniped:request-audience", "username", "groups"],
"code_challenge_methods_supported": ["S256"], "code_challenge_methods_supported": ["S256"],
"claims_supported": ["username", "groups"], "claims_supported": ["username", "groups", "additionalClaims"],
"discovery.supervisor.pinniped.dev/v1alpha1": { "discovery.supervisor.pinniped.dev/v1alpha1": {
"pinniped_identity_providers_endpoint": "https://some-issuer.com/some/path/v1alpha1/pinniped_identity_providers" "pinniped_identity_providers_endpoint": "https://some-issuer.com/some/path/v1alpha1/pinniped_identity_providers"
} }

View File

@ -48,6 +48,7 @@ func MakeDownstreamSession(
grantedScopes []string, grantedScopes []string,
clientID string, clientID string,
custom *psession.CustomSessionData, custom *psession.CustomSessionData,
additionalClaims map[string]interface{},
) *psession.PinnipedSession { ) *psession.PinnipedSession {
now := time.Now().UTC() now := time.Now().UTC()
openIDSession := &psession.PinnipedSession{ openIDSession := &psession.PinnipedSession{
@ -72,6 +73,9 @@ func MakeDownstreamSession(
if slices.Contains(grantedScopes, oidcapi.ScopeGroups) { if slices.Contains(grantedScopes, oidcapi.ScopeGroups) {
extras[oidcapi.IDTokenClaimGroups] = groups extras[oidcapi.IDTokenClaimGroups] = groups
} }
if len(additionalClaims) > 0 {
extras[oidcapi.IDTokenClaimAdditionalClaims] = additionalClaims
}
openIDSession.IDTokenClaims().Extra = extras openIDSession.IDTokenClaims().Extra = extras
return openIDSession return openIDSession
@ -212,6 +216,27 @@ func GetDownstreamIdentityFromUpstreamIDToken(
return subject, username, groups, err return subject, username, groups, err
} }
// MapAdditionalClaimsFromUpstreamIDToken returns the additionalClaims mapped from the upstream token, if any.
func MapAdditionalClaimsFromUpstreamIDToken(
upstreamIDPConfig provider.UpstreamOIDCIdentityProviderI,
idTokenClaims map[string]interface{},
) map[string]interface{} {
mapped := make(map[string]interface{}, len(upstreamIDPConfig.GetAdditionalClaimMappings()))
for downstreamClaimName, upstreamClaimName := range upstreamIDPConfig.GetAdditionalClaimMappings() {
upstreamClaimValue, ok := idTokenClaims[upstreamClaimName]
if !ok {
plog.Warning(
"additionalClaims mapping claim in upstream ID token missing",
"upstreamName", upstreamIDPConfig.GetName(),
"claimName", upstreamClaimName,
)
} else {
mapped[downstreamClaimName] = upstreamClaimValue
}
}
return mapped
}
func getSubjectAndUsernameFromUpstreamIDToken( func getSubjectAndUsernameFromUpstreamIDToken(
upstreamIDPConfig provider.UpstreamOIDCIdentityProviderI, upstreamIDPConfig provider.UpstreamOIDCIdentityProviderI,
idTokenClaims map[string]interface{}, idTokenClaims map[string]interface{},

View File

@ -0,0 +1,72 @@
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package downstreamsession
import (
"testing"
"github.com/stretchr/testify/require"
"go.pinniped.dev/internal/testutil/oidctestutil"
)
func TestMapAdditionalClaimsFromUpstreamIDToken(t *testing.T) {
tests := []struct {
name string
additionalClaimMappings map[string]string
upstreamClaims map[string]interface{}
wantClaims map[string]interface{}
}{
{
name: "happy path",
additionalClaimMappings: map[string]string{
"email": "notification_email",
},
upstreamClaims: map[string]interface{}{
"notification_email": "test@example.com",
},
wantClaims: map[string]interface{}{
"email": "test@example.com",
},
},
{
name: "missing",
additionalClaimMappings: map[string]string{
"email": "email",
},
upstreamClaims: map[string]interface{}{},
wantClaims: map[string]interface{}{},
},
{
name: "complex",
additionalClaimMappings: map[string]string{
"complex": "complex",
},
upstreamClaims: map[string]interface{}{
"complex": map[string]string{
"subClaim": "subValue",
},
},
wantClaims: map[string]interface{}{
"complex": map[string]string{
"subClaim": "subValue",
},
},
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()
idp := oidctestutil.NewTestUpstreamOIDCIdentityProviderBuilder().
WithAdditionalClaimMappings(test.additionalClaimMappings).
Build()
actual := MapAdditionalClaimsFromUpstreamIDToken(idp, test.upstreamClaims)
require.Equal(t, test.wantClaims, actual)
})
}
}

View File

@ -84,7 +84,7 @@ func NewPostHandler(issuerURL string, upstreamIDPs oidc.UpstreamIdentityProvider
groups := authenticateResponse.User.GetGroups() groups := authenticateResponse.User.GetGroups()
customSessionData := downstreamsession.MakeDownstreamLDAPOrADCustomSessionData(ldapUpstream, idpType, authenticateResponse, username) customSessionData := downstreamsession.MakeDownstreamLDAPOrADCustomSessionData(ldapUpstream, idpType, authenticateResponse, username)
openIDSession := downstreamsession.MakeDownstreamSession(subject, username, groups, openIDSession := downstreamsession.MakeDownstreamSession(subject, username, groups,
authorizeRequester.GetGrantedScopes(), authorizeRequester.GetClient().GetID(), customSessionData) authorizeRequester.GetGrantedScopes(), authorizeRequester.GetClient().GetID(), customSessionData, map[string]interface{}{})
oidc.PerformAuthcodeRedirect(r, w, oauthHelper, authorizeRequester, openIDSession, false) oidc.PerformAuthcodeRedirect(r, w, oauthHelper, authorizeRequester, openIDSession, false)
return nil return nil

View File

@ -1027,6 +1027,7 @@ func TestPostLoginEndpoint(t *testing.T) {
tt.wantDownstreamClient, tt.wantDownstreamClient,
tt.wantDownstreamRedirectURI, tt.wantDownstreamRedirectURI,
tt.wantDownstreamCustomSessionData, tt.wantDownstreamCustomSessionData,
map[string]interface{}{},
) )
case tt.wantRedirectToLoginPageError != "": case tt.wantRedirectToLoginPageError != "":
// Expecting an error redirect to the login UI page. // Expecting an error redirect to the login UI page.
@ -1062,6 +1063,7 @@ func TestPostLoginEndpoint(t *testing.T) {
tt.wantDownstreamClient, tt.wantDownstreamClient,
tt.wantDownstreamRedirectURI, tt.wantDownstreamRedirectURI,
tt.wantDownstreamCustomSessionData, tt.wantDownstreamCustomSessionData,
map[string]interface{}{},
) )
default: default:
require.Failf(t, "test should have expected a redirect or form body", require.Failf(t, "test should have expected a redirect or form body",

View File

@ -61,6 +61,9 @@ type UpstreamOIDCIdentityProviderI interface {
// GetAdditionalAuthcodeParams returns additional params to be sent on authcode requests. // GetAdditionalAuthcodeParams returns additional params to be sent on authcode requests.
GetAdditionalAuthcodeParams() map[string]string GetAdditionalAuthcodeParams() map[string]string
// GetAdditionalClaimMappings returns additional claims to be mapped from the upstream ID token.
GetAdditionalClaimMappings() 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)

View File

@ -40,6 +40,7 @@ import (
v1 "k8s.io/client-go/kubernetes/typed/core/v1" v1 "k8s.io/client-go/kubernetes/typed/core/v1"
configv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1" configv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1"
oidcapi "go.pinniped.dev/generated/latest/apis/supervisor/oidc"
supervisorfake "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned/fake" supervisorfake "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned/fake"
"go.pinniped.dev/internal/crud" "go.pinniped.dev/internal/crud"
"go.pinniped.dev/internal/fositestorage/accesstoken" "go.pinniped.dev/internal/fositestorage/accesstoken"
@ -285,19 +286,16 @@ type tokenEndpointResponseExpectedValues struct {
wantUpstreamOIDCValidateTokenCall *expectedUpstreamValidateTokens wantUpstreamOIDCValidateTokenCall *expectedUpstreamValidateTokens
wantCustomSessionDataStored *psession.CustomSessionData wantCustomSessionDataStored *psession.CustomSessionData
wantWarnings []RecordedWarning wantWarnings []RecordedWarning
wantAdditionalClaims map[string]interface{}
} }
type authcodeExchangeInputs struct { type authcodeExchangeInputs struct {
modifyAuthRequest func(authRequest *http.Request) modifyAuthRequest func(authRequest *http.Request)
modifyTokenRequest func(tokenRequest *http.Request, authCode string) modifyTokenRequest func(tokenRequest *http.Request, authCode string)
modifyStorage func( makeJwksSigningKeyAndProvider MakeJwksSigningKeyAndProviderFunc
t *testing.T, customSessionData *psession.CustomSessionData
s fositestoragei.AllFositeStorage, modifySession func(*psession.PinnipedSession)
authCode string, want tokenEndpointResponseExpectedValues
)
makeOathHelper OauthHelperFactoryFunc
customSessionData *psession.CustomSessionData
want tokenEndpointResponseExpectedValues
} }
func addFullyCapableDynamicClientAndSecretToKubeResources(t *testing.T, supervisorClient *supervisorfake.Clientset, kubeClient *fake.Clientset) { func addFullyCapableDynamicClientAndSecretToKubeResources(t *testing.T, supervisorClient *supervisorfake.Clientset, kubeClient *fake.Clientset) {
@ -344,6 +342,37 @@ func TestTokenEndpointAuthcodeExchange(t *testing.T) {
}, },
}, },
}, },
{
name: "request is valid and tokens are issued with additional claims",
authcodeExchange: authcodeExchangeInputs{
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid profile email username groups") },
modifySession: func(session *psession.PinnipedSession) {
session.IDTokenClaims().Extra[oidcapi.IDTokenClaimAdditionalClaims] = map[string]interface{}{
"upstreamString": "string value",
"upstreamFloat": 42.0,
"upstreamObj": map[string]string{
"name": "value",
},
}
},
want: tokenEndpointResponseExpectedValues{
wantStatus: http.StatusOK,
wantClientID: pinnipedCLIClientID,
wantSuccessBodyFields: []string{"id_token", "access_token", "token_type", "scope", "expires_in"}, // no refresh token
wantRequestedScopes: []string{"openid", "profile", "email", "username", "groups"},
wantGrantedScopes: []string{"openid", "username", "groups"},
wantUsername: goodUsername,
wantGroups: goodGroups,
wantAdditionalClaims: map[string]interface{}{
"upstreamString": "string value",
"upstreamFloat": 42.0,
"upstreamObj": map[string]interface{}{
"name": "value",
},
},
},
},
},
{ {
name: "request is valid and tokens are issued for dynamic client", name: "request is valid and tokens are issued for dynamic client",
kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources,
@ -364,6 +393,42 @@ func TestTokenEndpointAuthcodeExchange(t *testing.T) {
}, },
}, },
}, },
{
name: "request is valid and tokens are issued for dynamic client with additional claims",
kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources,
authcodeExchange: authcodeExchangeInputs{
modifyAuthRequest: func(r *http.Request) {
addDynamicClientIDToFormPostBody(r)
r.Form.Set("scope", "openid pinniped:request-audience username groups")
},
modifyTokenRequest: modifyAuthcodeTokenRequestWithDynamicClientAuth,
modifySession: func(session *psession.PinnipedSession) {
session.IDTokenClaims().Extra[oidcapi.IDTokenClaimAdditionalClaims] = map[string]interface{}{
"upstreamString": "string value",
"upstreamFloat": 42.0,
"upstreamObj": map[string]string{
"name": "value",
},
}
},
want: tokenEndpointResponseExpectedValues{
wantStatus: http.StatusOK,
wantClientID: dynamicClientID,
wantSuccessBodyFields: []string{"id_token", "access_token", "token_type", "scope", "expires_in"}, // no refresh token
wantRequestedScopes: []string{"openid", "pinniped:request-audience", "username", "groups"},
wantGrantedScopes: []string{"openid", "pinniped:request-audience", "username", "groups"},
wantUsername: goodUsername,
wantGroups: goodGroups,
wantAdditionalClaims: map[string]interface{}{
"upstreamString": "string value",
"upstreamFloat": 42.0,
"upstreamObj": map[string]interface{}{
"name": "value",
},
},
},
},
},
{ {
name: "openid scope was not requested from authorize endpoint", name: "openid scope was not requested from authorize endpoint",
authcodeExchange: authcodeExchangeInputs{ authcodeExchange: authcodeExchangeInputs{
@ -788,7 +853,9 @@ func TestTokenEndpointAuthcodeExchange(t *testing.T) {
{ {
name: "private signing key for JWTs has not yet been provided by the controller who is responsible for dynamically providing it", name: "private signing key for JWTs has not yet been provided by the controller who is responsible for dynamically providing it",
authcodeExchange: authcodeExchangeInputs{ authcodeExchange: authcodeExchangeInputs{
makeOathHelper: makeOauthHelperWithNilPrivateJWTSigningKey, makeJwksSigningKeyAndProvider: func(t *testing.T, issuer string) (*ecdsa.PrivateKey, jwks.DynamicJWKSProvider) {
return nil, jwks.NewDynamicJWKSProvider()
},
want: tokenEndpointResponseExpectedValues{ want: tokenEndpointResponseExpectedValues{
wantStatus: http.StatusServiceUnavailable, wantStatus: http.StatusServiceUnavailable,
wantErrorResponseBody: fositeTemporarilyUnavailableErrorBody, wantErrorResponseBody: fositeTemporarilyUnavailableErrorBody,
@ -870,7 +937,7 @@ func TestTokenEndpointWhenAuthcodeIsUsedTwice(t *testing.T) {
test.authcodeExchange.want.wantClientID, test.authcodeExchange.want.wantClientID,
test.authcodeExchange.want.wantRequestedScopes, test.authcodeExchange.want.wantGrantedScopes, test.authcodeExchange.want.wantRequestedScopes, test.authcodeExchange.want.wantGrantedScopes,
test.authcodeExchange.want.wantUsername, test.authcodeExchange.want.wantGroups, test.authcodeExchange.want.wantUsername, test.authcodeExchange.want.wantGroups,
nil, approxRequestTime) nil, test.authcodeExchange.want.wantAdditionalClaims, approxRequestTime)
// Check that the access token and refresh token storage were both deleted, and the number of other storage objects did not change. // Check that the access token and refresh token storage were both deleted, and the number of other storage objects did not change.
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: authorizationcode.TypeLabelValue}, 1) testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: authorizationcode.TypeLabelValue}, 1)
@ -941,6 +1008,41 @@ func TestTokenEndpointTokenExchange(t *testing.T) { // tests for grant_type "urn
requestedAudience: "some-workload-cluster", requestedAudience: "some-workload-cluster",
wantStatus: http.StatusOK, wantStatus: http.StatusOK,
}, },
{
name: "happy path with additional claims",
authcodeExchange: authcodeExchangeInputs{
modifyAuthRequest: func(authRequest *http.Request) {
authRequest.Form.Set("scope", "openid pinniped:request-audience username groups")
},
modifySession: func(session *psession.PinnipedSession) {
session.IDTokenClaims().Extra[oidcapi.IDTokenClaimAdditionalClaims] = map[string]interface{}{
"upstreamString": "string value",
"upstreamFloat": 42.0,
"upstreamObj": map[string]string{
"name": "value",
},
}
},
want: tokenEndpointResponseExpectedValues{
wantStatus: http.StatusOK,
wantClientID: pinnipedCLIClientID,
wantSuccessBodyFields: []string{"id_token", "access_token", "token_type", "expires_in", "scope"},
wantRequestedScopes: []string{"openid", "pinniped:request-audience", "username", "groups"},
wantGrantedScopes: []string{"openid", "pinniped:request-audience", "username", "groups"},
wantUsername: goodUsername,
wantGroups: goodGroups,
wantAdditionalClaims: map[string]interface{}{
"upstreamString": "string value",
"upstreamFloat": 42.0,
"upstreamObj": map[string]interface{}{
"name": "value",
},
},
},
},
requestedAudience: "some-workload-cluster",
wantStatus: http.StatusOK,
},
{ {
name: "happy path without requesting username and groups scopes", name: "happy path without requesting username and groups scopes",
authcodeExchange: authcodeExchangeInputs{ authcodeExchange: authcodeExchangeInputs{
@ -973,6 +1075,50 @@ func TestTokenEndpointTokenExchange(t *testing.T) { // tests for grant_type "urn
requestedAudience: "some-workload-cluster", requestedAudience: "some-workload-cluster",
wantStatus: http.StatusOK, wantStatus: http.StatusOK,
}, },
{
name: "happy path with dynamic client and additional claims",
kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources,
authcodeExchange: authcodeExchangeInputs{
modifyAuthRequest: func(authRequest *http.Request) {
addDynamicClientIDToFormPostBody(authRequest)
authRequest.Form.Set("scope", "openid pinniped:request-audience username groups")
},
modifySession: func(session *psession.PinnipedSession) {
session.IDTokenClaims().Extra[oidcapi.IDTokenClaimAdditionalClaims] = map[string]interface{}{
"upstreamString": "string value",
"upstreamFloat": 42.0,
"upstreamObj": map[string]string{
"name": "value",
},
}
},
modifyTokenRequest: modifyAuthcodeTokenRequestWithDynamicClientAuth,
want: tokenEndpointResponseExpectedValues{
wantStatus: http.StatusOK,
wantClientID: dynamicClientID,
wantSuccessBodyFields: []string{"id_token", "access_token", "token_type", "expires_in", "scope"},
wantRequestedScopes: []string{"openid", "pinniped:request-audience", "username", "groups"},
wantGrantedScopes: []string{"openid", "pinniped:request-audience", "username", "groups"},
wantUsername: goodUsername,
wantGroups: goodGroups,
wantAdditionalClaims: map[string]interface{}{
"upstreamString": "string value",
"upstreamFloat": 42.0,
"upstreamObj": map[string]interface{}{
"name": "value",
},
},
},
},
modifyRequestParams: func(t *testing.T, params url.Values) {
params.Del("client_id") // client auth for dynamic clients must be in basic auth header
},
modifyRequestHeaders: func(r *http.Request) {
r.SetBasicAuth(dynamicClientID, testutil.PlaintextPassword1)
},
requestedAudience: "some-workload-cluster",
wantStatus: http.StatusOK,
},
{ {
name: "happy path with dynamic client without requesting groups, so gets no groups in ID tokens", name: "happy path with dynamic client without requesting groups, so gets no groups in ID tokens",
kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources,
@ -1389,8 +1535,11 @@ func TestTokenEndpointTokenExchange(t *testing.T) { // tests for grant_type "urn
authRequest.Form.Set("scope", "openid pinniped:request-audience username groups") authRequest.Form.Set("scope", "openid pinniped:request-audience username groups")
}, },
// Fail to fetch a JWK signing key after the authcode exchange has happened. // Fail to fetch a JWK signing key after the authcode exchange has happened.
makeOathHelper: makeOauthHelperWithJWTKeyThatWorksOnlyOnce, makeJwksSigningKeyAndProvider: func(t *testing.T, issuer string) (*ecdsa.PrivateKey, jwks.DynamicJWKSProvider) {
want: successfulAuthCodeExchange, jwtSigningKey, jwkProvider := generateJWTSigningKeyAndJWKSProvider(t, goodIssuer)
return jwtSigningKey, &singleUseJWKProvider{DynamicJWKSProvider: jwkProvider}
},
want: successfulAuthCodeExchange,
}, },
requestedAudience: "some-workload-cluster", requestedAudience: "some-workload-cluster",
wantStatus: http.StatusServiceUnavailable, wantStatus: http.StatusServiceUnavailable,
@ -1481,6 +1630,9 @@ func TestTokenEndpointTokenExchange(t *testing.T) { // tests for grant_type "urn
if test.authcodeExchange.want.wantGroups != nil { if test.authcodeExchange.want.wantGroups != nil {
idTokenFields = append(idTokenFields, "groups") idTokenFields = append(idTokenFields, "groups")
} }
if len(test.authcodeExchange.want.wantAdditionalClaims) > 0 {
idTokenFields = append(idTokenFields, oidcapi.IDTokenClaimAdditionalClaims)
}
require.ElementsMatch(t, idTokenFields, getMapKeys(tokenClaims)) require.ElementsMatch(t, idTokenFields, getMapKeys(tokenClaims))
// Assert that the returned token has expected claims values. // Assert that the returned token has expected claims values.
@ -1505,11 +1657,22 @@ func TestTokenEndpointTokenExchange(t *testing.T) { // tests for grant_type "urn
require.Nil(t, tokenClaims["groups"]) require.Nil(t, tokenClaims["groups"])
} }
if len(test.authcodeExchange.want.wantAdditionalClaims) > 0 {
require.Equal(t, test.authcodeExchange.want.wantAdditionalClaims, tokenClaims[oidcapi.IDTokenClaimAdditionalClaims])
}
additionalClaims, ok := tokenClaims[oidcapi.IDTokenClaimAdditionalClaims].(map[string]interface{})
if ok && tokenClaims[oidcapi.IDTokenClaimAdditionalClaims] != nil {
require.True(t, len(additionalClaims) > 0, "additionalClaims may never be present and empty in the id token")
}
// Also assert that some are the same as the original downstream ID token. // Also assert that some are the same as the original downstream ID token.
requireClaimsAreEqual(t, "iss", claimsOfFirstIDToken, tokenClaims) // issuer requireClaimsAreEqual(t, "iss", claimsOfFirstIDToken, tokenClaims) // issuer
requireClaimsAreEqual(t, "sub", claimsOfFirstIDToken, tokenClaims) // subject requireClaimsAreEqual(t, "sub", claimsOfFirstIDToken, tokenClaims) // subject
requireClaimsAreEqual(t, "rat", claimsOfFirstIDToken, tokenClaims) // requested at requireClaimsAreEqual(t, "rat", claimsOfFirstIDToken, tokenClaims) // requested at
requireClaimsAreEqual(t, "auth_time", claimsOfFirstIDToken, tokenClaims) // auth time requireClaimsAreEqual(t, "auth_time", claimsOfFirstIDToken, tokenClaims) // auth time
if len(test.authcodeExchange.want.wantAdditionalClaims) > 0 {
requireClaimsAreEqual(t, oidcapi.IDTokenClaimAdditionalClaims, claimsOfFirstIDToken, tokenClaims)
}
// Also assert which are the different from the original downstream ID token. // Also assert which are the different from the original downstream ID token.
requireClaimsAreNotEqual(t, "jti", claimsOfFirstIDToken, tokenClaims) // JWT ID requireClaimsAreNotEqual(t, "jti", claimsOfFirstIDToken, tokenClaims) // JWT ID
@ -1691,6 +1854,12 @@ func TestRefreshGrant(t *testing.T) {
return want return want
} }
happyRefreshTokenResponseForOpenIDAndOfflineAccessWithAdditionalClaims := func(wantCustomSessionDataStored *psession.CustomSessionData, expectToValidateToken *oauth2.Token, wantAdditionalClaims map[string]interface{}) tokenEndpointResponseExpectedValues {
want := happyRefreshTokenResponseForOpenIDAndOfflineAccess(wantCustomSessionDataStored, expectToValidateToken)
want.wantAdditionalClaims = wantAdditionalClaims
return want
}
happyRefreshTokenResponseForLDAP := func(wantCustomSessionDataStored *psession.CustomSessionData) tokenEndpointResponseExpectedValues { happyRefreshTokenResponseForLDAP := func(wantCustomSessionDataStored *psession.CustomSessionData) tokenEndpointResponseExpectedValues {
want := happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(wantCustomSessionDataStored) want := happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(wantCustomSessionDataStored)
want.wantUpstreamRefreshCall = happyLDAPUpstreamRefreshCall() want.wantUpstreamRefreshCall = happyLDAPUpstreamRefreshCall()
@ -1783,6 +1952,60 @@ func TestRefreshGrant(t *testing.T) {
), ),
}, },
}, },
{
name: "happy path refresh grant with openid scope granted (id token returned) and additionalClaims",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(
upstreamOIDCIdentityProviderBuilder().WithValidatedAndMergedWithUserInfoTokens(&oidctypes.Token{
IDToken: &oidctypes.IDToken{
Claims: map[string]interface{}{
"sub": goodUpstreamSubject,
},
},
}).WithRefreshedTokens(refreshedUpstreamTokensWithIDAndRefreshTokens()).Build()),
authcodeExchange: authcodeExchangeInputs{
customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionData(),
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access username groups") },
modifySession: func(session *psession.PinnipedSession) {
session.IDTokenClaims().Extra[oidcapi.IDTokenClaimAdditionalClaims] = map[string]interface{}{
"upstreamString": "string value",
"upstreamFloat": 42.0,
"upstreamObj": map[string]string{
"name": "value",
},
}
},
want: tokenEndpointResponseExpectedValues{
wantStatus: http.StatusOK,
wantClientID: pinnipedCLIClientID,
wantSuccessBodyFields: []string{"id_token", "refresh_token", "access_token", "token_type", "expires_in", "scope"},
wantRequestedScopes: []string{"openid", "offline_access", "username", "groups"},
wantGrantedScopes: []string{"openid", "offline_access", "username", "groups"},
wantCustomSessionDataStored: initialUpstreamOIDCRefreshTokenCustomSessionData(),
wantUsername: goodUsername,
wantGroups: goodGroups,
wantAdditionalClaims: map[string]interface{}{
"upstreamString": "string value",
"upstreamFloat": 42.0,
"upstreamObj": map[string]interface{}{
"name": "value",
},
},
},
},
refreshRequest: refreshRequestInputs{
want: happyRefreshTokenResponseForOpenIDAndOfflineAccessWithAdditionalClaims(
upstreamOIDCCustomSessionDataWithNewRefreshToken(oidcUpstreamRefreshedRefreshToken),
refreshedUpstreamTokensWithIDAndRefreshTokens(),
map[string]interface{}{
"upstreamString": "string value",
"upstreamFloat": 42.0,
"upstreamObj": map[string]interface{}{
"name": "value",
},
},
),
},
},
{ {
name: "happy path refresh grant with openid scope granted (id token returned) using dynamic client", name: "happy path refresh grant with openid scope granted (id token returned) using dynamic client",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(
@ -1811,6 +2034,66 @@ func TestRefreshGrant(t *testing.T) {
)), )),
}, },
}, },
{
name: "happy path refresh grant with openid scope granted (id token returned) using dynamic client with additional claims",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(
upstreamOIDCIdentityProviderBuilder().WithValidatedAndMergedWithUserInfoTokens(&oidctypes.Token{
IDToken: &oidctypes.IDToken{
Claims: map[string]interface{}{
"sub": goodUpstreamSubject,
},
},
}).WithRefreshedTokens(refreshedUpstreamTokensWithIDAndRefreshTokens()).Build()),
kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources,
authcodeExchange: authcodeExchangeInputs{
customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionData(),
modifyAuthRequest: func(r *http.Request) {
addDynamicClientIDToFormPostBody(r)
r.Form.Set("scope", "openid offline_access username groups")
},
modifySession: func(session *psession.PinnipedSession) {
session.IDTokenClaims().Extra[oidcapi.IDTokenClaimAdditionalClaims] = map[string]interface{}{
"upstreamString": "string value",
"upstreamFloat": 42.0,
"upstreamObj": map[string]string{
"name": "value",
},
}
},
modifyTokenRequest: modifyAuthcodeTokenRequestWithDynamicClientAuth,
want: tokenEndpointResponseExpectedValues{
wantStatus: http.StatusOK,
wantClientID: dynamicClientID,
wantSuccessBodyFields: []string{"id_token", "refresh_token", "access_token", "token_type", "expires_in", "scope"},
wantRequestedScopes: []string{"openid", "offline_access", "username", "groups"},
wantGrantedScopes: []string{"openid", "offline_access", "username", "groups"},
wantCustomSessionDataStored: initialUpstreamOIDCRefreshTokenCustomSessionData(),
wantUsername: goodUsername,
wantGroups: goodGroups,
wantAdditionalClaims: map[string]interface{}{
"upstreamString": "string value",
"upstreamFloat": 42.0,
"upstreamObj": map[string]interface{}{
"name": "value",
},
},
},
},
refreshRequest: refreshRequestInputs{
modifyTokenRequest: modifyRefreshTokenRequestWithDynamicClientAuth,
want: withWantDynamicClientID(happyRefreshTokenResponseForOpenIDAndOfflineAccessWithAdditionalClaims(
upstreamOIDCCustomSessionDataWithNewRefreshToken(oidcUpstreamRefreshedRefreshToken),
refreshedUpstreamTokensWithIDAndRefreshTokens(),
map[string]interface{}{
"upstreamString": "string value",
"upstreamFloat": 42.0,
"upstreamObj": map[string]interface{}{
"name": "value",
},
},
)),
},
},
{ {
name: "happy path refresh grant with upstream username claim but without downstream username scope granted, using dynamic client", name: "happy path refresh grant with upstream username claim but without downstream username scope granted, using dynamic client",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(
@ -3852,16 +4135,13 @@ func exchangeAuthcodeForTokens(
var oauthHelper fosite.OAuth2Provider var oauthHelper fosite.OAuth2Provider
// Use lower minimum required bcrypt cost than we would use in production to keep unit the tests fast. // Use lower minimum required bcrypt cost than we would use in production to keep unit the tests fast.
oauthStore = oidc.NewKubeStorage(secrets, oidcClientsClient, oidc.DefaultOIDCTimeoutsConfiguration(), bcrypt.MinCost) oauthStore = oidc.NewKubeStorage(secrets, oidcClientsClient, oidc.DefaultOIDCTimeoutsConfiguration(), bcrypt.MinCost)
if test.makeOathHelper != nil {
oauthHelper, authCode, jwtSigningKey = test.makeOathHelper(t, authRequest, oauthStore, test.customSessionData) if test.makeJwksSigningKeyAndProvider == nil {
} else { test.makeJwksSigningKeyAndProvider = generateJWTSigningKeyAndJWKSProvider
// Note that makeHappyOauthHelper() calls simulateAuthEndpointHavingAlreadyRun() to preload the session storage.
oauthHelper, authCode, jwtSigningKey = makeHappyOauthHelper(t, authRequest, oauthStore, test.customSessionData)
} }
if test.modifyStorage != nil { // Note that makeHappyOauthHelper() calls simulateAuthEndpointHavingAlreadyRun() to preload the session storage.
test.modifyStorage(t, oauthStore, authCode) oauthHelper, authCode, jwtSigningKey = makeHappyOauthHelper(t, authRequest, oauthStore, test.makeJwksSigningKeyAndProvider, test.customSessionData, test.modifySession)
}
subject = NewHandler(idps, oauthHelper) subject = NewHandler(idps, oauthHelper)
@ -3936,10 +4216,10 @@ func requireTokenEndpointBehavior(
wantRefreshToken := contains(test.wantSuccessBodyFields, "refresh_token") wantRefreshToken := contains(test.wantSuccessBodyFields, "refresh_token")
requireInvalidAuthCodeStorage(t, authCode, oauthStore, secrets, requestTime) requireInvalidAuthCodeStorage(t, authCode, oauthStore, secrets, requestTime)
requireValidAccessTokenStorage(t, parsedResponseBody, oauthStore, test.wantClientID, test.wantRequestedScopes, test.wantGrantedScopes, test.wantUsername, test.wantGroups, test.wantCustomSessionDataStored, secrets, requestTime) requireValidAccessTokenStorage(t, parsedResponseBody, oauthStore, test.wantClientID, test.wantRequestedScopes, test.wantGrantedScopes, test.wantUsername, test.wantGroups, test.wantCustomSessionDataStored, test.wantAdditionalClaims, secrets, requestTime)
requireInvalidPKCEStorage(t, authCode, oauthStore) requireInvalidPKCEStorage(t, authCode, oauthStore)
// Performing a refresh does not update the OIDC storage, so after a refresh it should still have the old custom session data and old username and groups from the initial login. // Performing a refresh does not update the OIDC storage, so after a refresh it should still have the old custom session data and old username and groups from the initial login.
requireValidOIDCStorage(t, parsedResponseBody, authCode, oauthStore, test.wantClientID, test.wantRequestedScopes, test.wantGrantedScopes, oldUsername, oldGroups, oldCustomSessionData, requestTime) requireValidOIDCStorage(t, parsedResponseBody, authCode, oauthStore, test.wantClientID, test.wantRequestedScopes, test.wantGrantedScopes, oldUsername, oldGroups, oldCustomSessionData, test.wantAdditionalClaims, requestTime)
expectedNumberOfRefreshTokenSessionsStored := 0 expectedNumberOfRefreshTokenSessionsStored := 0
if wantRefreshToken { if wantRefreshToken {
@ -3948,10 +4228,10 @@ func requireTokenEndpointBehavior(
expectedNumberOfIDSessionsStored := 0 expectedNumberOfIDSessionsStored := 0
if wantIDToken { if wantIDToken {
expectedNumberOfIDSessionsStored = 1 expectedNumberOfIDSessionsStored = 1
requireValidIDToken(t, parsedResponseBody, jwtSigningKey, test.wantClientID, wantNonceValueInIDToken, test.wantUsername, test.wantGroups, parsedResponseBody["access_token"].(string), requestTime) requireValidIDToken(t, parsedResponseBody, jwtSigningKey, test.wantClientID, wantNonceValueInIDToken, test.wantUsername, test.wantGroups, test.wantAdditionalClaims, parsedResponseBody["access_token"].(string), requestTime)
} }
if wantRefreshToken { if wantRefreshToken {
requireValidRefreshTokenStorage(t, parsedResponseBody, oauthStore, test.wantClientID, test.wantRequestedScopes, test.wantGrantedScopes, test.wantUsername, test.wantGroups, test.wantCustomSessionDataStored, secrets, requestTime) requireValidRefreshTokenStorage(t, parsedResponseBody, oauthStore, test.wantClientID, test.wantRequestedScopes, test.wantGrantedScopes, test.wantUsername, test.wantGroups, test.wantCustomSessionDataStored, test.wantAdditionalClaims, secrets, requestTime)
} }
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: authorizationcode.TypeLabelValue}, 1) testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: authorizationcode.TypeLabelValue}, 1)
@ -4053,24 +4333,21 @@ func getFositeDataSignature(t *testing.T, data string) string {
return split[1] return split[1]
} }
type OauthHelperFactoryFunc func( type MakeJwksSigningKeyAndProviderFunc func(t *testing.T, issuer string) (*ecdsa.PrivateKey, jwks.DynamicJWKSProvider)
t *testing.T,
authRequest *http.Request,
store fositestoragei.AllFositeStorage,
initialCustomSessionData *psession.CustomSessionData,
) (fosite.OAuth2Provider, string, *ecdsa.PrivateKey)
func makeHappyOauthHelper( func makeHappyOauthHelper(
t *testing.T, t *testing.T,
authRequest *http.Request, authRequest *http.Request,
store fositestoragei.AllFositeStorage, store fositestoragei.AllFositeStorage,
makeJwksSigningKeyAndProvider MakeJwksSigningKeyAndProviderFunc,
initialCustomSessionData *psession.CustomSessionData, initialCustomSessionData *psession.CustomSessionData,
modifySession func(session *psession.PinnipedSession),
) (fosite.OAuth2Provider, string, *ecdsa.PrivateKey) { ) (fosite.OAuth2Provider, string, *ecdsa.PrivateKey) {
t.Helper() t.Helper()
jwtSigningKey, jwkProvider := generateJWTSigningKeyAndJWKSProvider(t, goodIssuer) jwtSigningKey, jwkProvider := makeJwksSigningKeyAndProvider(t, goodIssuer)
oauthHelper := oidc.FositeOauth2Helper(store, goodIssuer, hmacSecretFunc, jwkProvider, oidc.DefaultOIDCTimeoutsConfiguration()) oauthHelper := oidc.FositeOauth2Helper(store, goodIssuer, hmacSecretFunc, jwkProvider, oidc.DefaultOIDCTimeoutsConfiguration())
authResponder := simulateAuthEndpointHavingAlreadyRun(t, authRequest, oauthHelper, initialCustomSessionData) authResponder := simulateAuthEndpointHavingAlreadyRun(t, authRequest, oauthHelper, initialCustomSessionData, modifySession)
return oauthHelper, authResponder.GetCode(), jwtSigningKey return oauthHelper, authResponder.GetCode(), jwtSigningKey
} }
@ -4087,40 +4364,13 @@ func (s *singleUseJWKProvider) GetJWKS(issuerName string) (jwks *jose.JSONWebKey
return s.DynamicJWKSProvider.GetJWKS(issuerName) return s.DynamicJWKSProvider.GetJWKS(issuerName)
} }
func makeOauthHelperWithJWTKeyThatWorksOnlyOnce(
t *testing.T,
authRequest *http.Request,
store fositestoragei.AllFositeStorage,
initialCustomSessionData *psession.CustomSessionData,
) (fosite.OAuth2Provider, string, *ecdsa.PrivateKey) {
t.Helper()
jwtSigningKey, jwkProvider := generateJWTSigningKeyAndJWKSProvider(t, goodIssuer)
oauthHelper := oidc.FositeOauth2Helper(store, goodIssuer, hmacSecretFunc, &singleUseJWKProvider{DynamicJWKSProvider: jwkProvider}, oidc.DefaultOIDCTimeoutsConfiguration())
authResponder := simulateAuthEndpointHavingAlreadyRun(t, authRequest, oauthHelper, initialCustomSessionData)
return oauthHelper, authResponder.GetCode(), jwtSigningKey
}
func makeOauthHelperWithNilPrivateJWTSigningKey(
t *testing.T,
authRequest *http.Request,
store fositestoragei.AllFositeStorage,
initialCustomSessionData *psession.CustomSessionData,
) (fosite.OAuth2Provider, string, *ecdsa.PrivateKey) {
t.Helper()
jwkProvider := jwks.NewDynamicJWKSProvider() // empty provider which contains no signing key for this issuer
oauthHelper := oidc.FositeOauth2Helper(store, goodIssuer, hmacSecretFunc, jwkProvider, oidc.DefaultOIDCTimeoutsConfiguration())
authResponder := simulateAuthEndpointHavingAlreadyRun(t, authRequest, oauthHelper, initialCustomSessionData)
return oauthHelper, authResponder.GetCode(), nil
}
// Simulate the auth endpoint running so Fosite code will fill the store with realistic values. // Simulate the auth endpoint running so Fosite code will fill the store with realistic values.
func simulateAuthEndpointHavingAlreadyRun( func simulateAuthEndpointHavingAlreadyRun(
t *testing.T, t *testing.T,
authRequest *http.Request, authRequest *http.Request,
oauthHelper fosite.OAuth2Provider, oauthHelper fosite.OAuth2Provider,
initialCustomSessionData *psession.CustomSessionData, initialCustomSessionData *psession.CustomSessionData,
modifySession func(session *psession.PinnipedSession),
) fosite.AuthorizeResponder { ) fosite.AuthorizeResponder {
// We only set the fields in the session that Fosite wants us to set. // We only set the fields in the session that Fosite wants us to set.
ctx := context.Background() ctx := context.Background()
@ -4137,6 +4387,10 @@ func simulateAuthEndpointHavingAlreadyRun(
}, },
Custom: initialCustomSessionData, Custom: initialCustomSessionData,
} }
if modifySession != nil {
modifySession(session)
}
authRequester, err := oauthHelper.NewAuthorizeRequest(ctx, authRequest) authRequester, err := oauthHelper.NewAuthorizeRequest(ctx, authRequest)
require.NoError(t, err) require.NoError(t, err)
if strings.Contains(authRequest.Form.Get("scope"), "openid") { if strings.Contains(authRequest.Form.Get("scope"), "openid") {
@ -4212,6 +4466,7 @@ func requireValidRefreshTokenStorage(
wantUsername string, wantUsername string,
wantGroups []string, wantGroups []string,
wantCustomSessionData *psession.CustomSessionData, wantCustomSessionData *psession.CustomSessionData,
wantAdditionalClaims map[string]interface{},
secrets v1.SecretInterface, secrets v1.SecretInterface,
requestTime time.Time, requestTime time.Time,
) { ) {
@ -4241,6 +4496,7 @@ func requireValidRefreshTokenStorage(
wantUsername, wantUsername,
wantGroups, wantGroups,
wantCustomSessionData, wantCustomSessionData,
wantAdditionalClaims,
requestTime, requestTime,
) )
@ -4257,6 +4513,7 @@ func requireValidAccessTokenStorage(
wantUsername string, wantUsername string,
wantGroups []string, wantGroups []string,
wantCustomSessionData *psession.CustomSessionData, wantCustomSessionData *psession.CustomSessionData,
wantAdditionalClaims map[string]interface{},
secrets v1.SecretInterface, secrets v1.SecretInterface,
requestTime time.Time, requestTime time.Time,
) { ) {
@ -4305,6 +4562,7 @@ func requireValidAccessTokenStorage(
wantUsername, wantUsername,
wantGroups, wantGroups,
wantCustomSessionData, wantCustomSessionData,
wantAdditionalClaims,
requestTime, requestTime,
) )
@ -4351,6 +4609,7 @@ func requireValidOIDCStorage(
wantUsername string, wantUsername string,
wantGroups []string, wantGroups []string,
wantCustomSessionData *psession.CustomSessionData, wantCustomSessionData *psession.CustomSessionData,
wantAdditionalClaims map[string]interface{},
requestTime time.Time, requestTime time.Time,
) { ) {
t.Helper() t.Helper()
@ -4378,6 +4637,7 @@ func requireValidOIDCStorage(
wantUsername, wantUsername,
wantGroups, wantGroups,
wantCustomSessionData, wantCustomSessionData,
wantAdditionalClaims,
requestTime, requestTime,
) )
} else { } else {
@ -4397,6 +4657,7 @@ func requireValidStoredRequest(
wantUsername string, wantUsername string,
wantGroups []string, wantGroups []string,
wantCustomSessionData *psession.CustomSessionData, wantCustomSessionData *psession.CustomSessionData,
wantAdditionalClaims map[string]interface{},
requestTime time.Time, requestTime time.Time,
) { ) {
t.Helper() t.Helper()
@ -4429,6 +4690,9 @@ func requireValidStoredRequest(
expectedExtra["groups"] = toSliceOfInterface(wantGroups) expectedExtra["groups"] = toSliceOfInterface(wantGroups)
} }
expectedExtra["azp"] = wantClientID expectedExtra["azp"] = wantClientID
if len(wantAdditionalClaims) > 0 {
expectedExtra[oidcapi.IDTokenClaimAdditionalClaims] = wantAdditionalClaims
}
require.Equal(t, expectedExtra, claims.Extra) require.Equal(t, expectedExtra, claims.Extra)
// We are in charge of setting these fields. For the purpose of testing, we ensure that the // We are in charge of setting these fields. For the purpose of testing, we ensure that the
@ -4518,6 +4782,7 @@ func requireValidIDToken(
wantNonceValueInIDToken bool, wantNonceValueInIDToken bool,
wantUsernameInIDToken string, wantUsernameInIDToken string,
wantGroupsInIDToken []string, wantGroupsInIDToken []string,
wantAdditionalClaims map[string]interface{},
actualAccessToken string, actualAccessToken string,
requestTime time.Time, requestTime time.Time,
) { ) {
@ -4532,18 +4797,19 @@ func requireValidIDToken(
token := oidctestutil.VerifyECDSAIDToken(t, goodIssuer, wantClientID, jwtSigningKey, idTokenString) token := oidctestutil.VerifyECDSAIDToken(t, goodIssuer, wantClientID, jwtSigningKey, idTokenString)
var claims struct { var claims struct {
Subject string `json:"sub"` Subject string `json:"sub"`
Audience []string `json:"aud"` Audience []string `json:"aud"`
Issuer string `json:"iss"` Issuer string `json:"iss"`
JTI string `json:"jti"` JTI string `json:"jti"`
Nonce string `json:"nonce"` Nonce string `json:"nonce"`
AccessTokenHash string `json:"at_hash"` AccessTokenHash string `json:"at_hash"`
ExpiresAt int64 `json:"exp"` ExpiresAt int64 `json:"exp"`
IssuedAt int64 `json:"iat"` IssuedAt int64 `json:"iat"`
RequestedAt int64 `json:"rat"` RequestedAt int64 `json:"rat"`
AuthTime int64 `json:"auth_time"` AuthTime int64 `json:"auth_time"`
Groups []string `json:"groups"` Groups []string `json:"groups"`
Username string `json:"username"` Username string `json:"username"`
AdditionalClaims map[string]interface{} `json:"additionalClaims"`
} }
idTokenFields := []string{"sub", "aud", "iss", "jti", "auth_time", "exp", "iat", "rat", "azp", "at_hash"} idTokenFields := []string{"sub", "aud", "iss", "jti", "auth_time", "exp", "iat", "rat", "azp", "at_hash"}
@ -4556,6 +4822,9 @@ func requireValidIDToken(
if wantGroupsInIDToken != nil { if wantGroupsInIDToken != nil {
idTokenFields = append(idTokenFields, "groups") idTokenFields = append(idTokenFields, "groups")
} }
if len(wantAdditionalClaims) > 0 {
idTokenFields = append(idTokenFields, oidcapi.IDTokenClaimAdditionalClaims)
}
// make sure that these are the only fields in the token // make sure that these are the only fields in the token
var m map[string]interface{} var m map[string]interface{}
@ -4573,6 +4842,8 @@ func requireValidIDToken(
require.Equal(t, wantClientID, m["azp"]) require.Equal(t, wantClientID, m["azp"])
require.Equal(t, goodIssuer, claims.Issuer) require.Equal(t, goodIssuer, claims.Issuer)
require.NotEmpty(t, claims.JTI) require.NotEmpty(t, claims.JTI)
require.Equal(t, wantAdditionalClaims, claims.AdditionalClaims)
require.NotEqual(t, map[string]interface{}{}, claims.AdditionalClaims, "additionalClaims may never be present and empty in the id token")
if wantNonceValueInIDToken { if wantNonceValueInIDToken {
require.Equal(t, goodNonce, claims.Nonce) require.Equal(t, goodNonce, claims.Nonce)

View File

@ -28,6 +28,7 @@ import (
kubetesting "k8s.io/client-go/testing" kubetesting "k8s.io/client-go/testing"
"k8s.io/utils/strings/slices" "k8s.io/utils/strings/slices"
oidcapi "go.pinniped.dev/generated/latest/apis/supervisor/oidc"
"go.pinniped.dev/internal/authenticators" "go.pinniped.dev/internal/authenticators"
"go.pinniped.dev/internal/crud" "go.pinniped.dev/internal/crud"
"go.pinniped.dev/internal/fositestorage/authorizationcode" "go.pinniped.dev/internal/fositestorage/authorizationcode"
@ -164,6 +165,7 @@ type TestUpstreamOIDCIdentityProvider struct {
GroupsClaim string GroupsClaim string
Scopes []string Scopes []string
AdditionalAuthcodeParams map[string]string AdditionalAuthcodeParams map[string]string
AdditionalClaimMappings map[string]string
AllowPasswordGrant bool AllowPasswordGrant bool
ExchangeAuthcodeAndValidateTokensFunc func( ExchangeAuthcodeAndValidateTokensFunc func(
@ -207,6 +209,10 @@ func (u *TestUpstreamOIDCIdentityProvider) GetAdditionalAuthcodeParams() map[str
return u.AdditionalAuthcodeParams return u.AdditionalAuthcodeParams
} }
func (u *TestUpstreamOIDCIdentityProvider) GetAdditionalClaimMappings() map[string]string {
return u.AdditionalClaimMappings
}
func (u *TestUpstreamOIDCIdentityProvider) GetName() string { func (u *TestUpstreamOIDCIdentityProvider) GetName() string {
return u.Name return u.Name
} }
@ -630,6 +636,7 @@ type TestUpstreamOIDCIdentityProviderBuilder struct {
authorizationURL url.URL authorizationURL url.URL
hasUserInfoURL bool hasUserInfoURL bool
additionalAuthcodeParams map[string]string additionalAuthcodeParams map[string]string
additionalClaimMappings map[string]string
allowPasswordGrant bool allowPasswordGrant bool
authcodeExchangeErr error authcodeExchangeErr error
passwordGrantErr error passwordGrantErr error
@ -716,6 +723,11 @@ func (u *TestUpstreamOIDCIdentityProviderBuilder) WithAdditionalAuthcodeParams(p
return u return u
} }
func (u *TestUpstreamOIDCIdentityProviderBuilder) WithAdditionalClaimMappings(m map[string]string) *TestUpstreamOIDCIdentityProviderBuilder {
u.additionalClaimMappings = m
return u
}
func (u *TestUpstreamOIDCIdentityProviderBuilder) WithRefreshToken(token string) *TestUpstreamOIDCIdentityProviderBuilder { func (u *TestUpstreamOIDCIdentityProviderBuilder) WithRefreshToken(token string) *TestUpstreamOIDCIdentityProviderBuilder {
u.refreshToken = &oidctypes.RefreshToken{Token: token} u.refreshToken = &oidctypes.RefreshToken{Token: token}
return u return u
@ -792,6 +804,7 @@ func (u *TestUpstreamOIDCIdentityProviderBuilder) Build() *TestUpstreamOIDCIdent
AuthorizationURL: u.authorizationURL, AuthorizationURL: u.authorizationURL,
UserInfoURL: u.hasUserInfoURL, UserInfoURL: u.hasUserInfoURL,
AdditionalAuthcodeParams: u.additionalAuthcodeParams, AdditionalAuthcodeParams: u.additionalAuthcodeParams,
AdditionalClaimMappings: u.additionalClaimMappings,
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
@ -934,6 +947,7 @@ func RequireAuthCodeRegexpMatch(
wantDownstreamClientID string, wantDownstreamClientID string,
wantDownstreamRedirectURI string, wantDownstreamRedirectURI string,
wantCustomSessionData *psession.CustomSessionData, wantCustomSessionData *psession.CustomSessionData,
wantAdditionalClaims map[string]interface{},
) { ) {
t.Helper() t.Helper()
@ -972,6 +986,7 @@ func RequireAuthCodeRegexpMatch(
wantDownstreamClientID, wantDownstreamClientID,
wantDownstreamRedirectURI, wantDownstreamRedirectURI,
wantCustomSessionData, wantCustomSessionData,
wantAdditionalClaims,
) )
// One PKCE should have been stored. // One PKCE should have been stored.
@ -1011,6 +1026,7 @@ func includesOpenIDScope(scopes []string) bool {
return false return false
} }
//nolint:funlen
func validateAuthcodeStorage( func validateAuthcodeStorage(
t *testing.T, t *testing.T,
oauthStore fositestoragei.AllFositeStorage, oauthStore fositestoragei.AllFositeStorage,
@ -1023,6 +1039,7 @@ func validateAuthcodeStorage(
wantDownstreamClientID string, wantDownstreamClientID string,
wantDownstreamRedirectURI string, wantDownstreamRedirectURI string,
wantCustomSessionData *psession.CustomSessionData, wantCustomSessionData *psession.CustomSessionData,
wantAdditionalClaims map[string]interface{},
) (*fosite.Request, *psession.PinnipedSession) { ) (*fosite.Request, *psession.PinnipedSession) {
t.Helper() t.Helper()
@ -1066,6 +1083,10 @@ func validateAuthcodeStorage(
require.Equal(t, wantDownstreamClientID, actualClaims.Extra["azp"]) require.Equal(t, wantDownstreamClientID, actualClaims.Extra["azp"])
wantDownstreamIDTokenExtraClaimsCount := 1 // should always have azp claim wantDownstreamIDTokenExtraClaimsCount := 1 // should always have azp claim
if len(wantAdditionalClaims) > 0 {
wantDownstreamIDTokenExtraClaimsCount++
}
// 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)
if wantDownstreamIDTokenUsername == "" { if wantDownstreamIDTokenUsername == "" {
@ -1085,6 +1106,14 @@ func validateAuthcodeStorage(
actualDownstreamIDTokenGroups := actualClaims.Extra["groups"] actualDownstreamIDTokenGroups := actualClaims.Extra["groups"]
require.Nil(t, actualDownstreamIDTokenGroups) require.Nil(t, actualDownstreamIDTokenGroups)
} }
if len(wantAdditionalClaims) > 0 {
actualAdditionalClaims, ok := actualClaims.Get(oidcapi.IDTokenClaimAdditionalClaims).(map[string]interface{})
require.True(t, ok, "expected %s to be a map[string]interface{}", oidcapi.IDTokenClaimAdditionalClaims)
require.Equal(t, wantAdditionalClaims, actualAdditionalClaims)
} else {
require.NotContains(t, actualClaims.Extra, oidcapi.IDTokenClaimAdditionalClaims, "%s must not be present when there are no wanted additional claims", oidcapi.IDTokenClaimAdditionalClaims)
}
// Make sure that we asserted on every extra claim. // Make sure that we asserted on every extra claim.
require.Len(t, actualClaims.Extra, wantDownstreamIDTokenExtraClaimsCount) require.Len(t, actualClaims.Extra, wantDownstreamIDTokenExtraClaimsCount)

View File

@ -43,6 +43,7 @@ type ProviderConfig struct {
Client *http.Client Client *http.Client
AllowPasswordGrant bool AllowPasswordGrant bool
AdditionalAuthcodeParams map[string]string AdditionalAuthcodeParams map[string]string
AdditionalClaimMappings map[string]string
RevocationURL *url.URL // will commonly be nil: many providers do not offer this RevocationURL *url.URL // will commonly be nil: many providers do not offer this
Provider interface { Provider interface {
Verifier(*coreosoidc.Config) *coreosoidc.IDTokenVerifier Verifier(*coreosoidc.Config) *coreosoidc.IDTokenVerifier
@ -78,6 +79,10 @@ func (p *ProviderConfig) GetAdditionalAuthcodeParams() map[string]string {
return p.AdditionalAuthcodeParams return p.AdditionalAuthcodeParams
} }
func (p *ProviderConfig) GetAdditionalClaimMappings() map[string]string {
return p.AdditionalClaimMappings
}
func (p *ProviderConfig) GetName() string { func (p *ProviderConfig) GetName() string {
return p.Name return p.Name
} }

View File

@ -68,6 +68,16 @@ func TestProviderConfig(t *testing.T) {
rawClaims: []byte(`{`), rawClaims: []byte(`{`),
} }
require.False(t, p.HasUserInfoURL()) require.False(t, p.HasUserInfoURL())
// AdditionalAuthcodeParams defaults to empty
require.Empty(t, p.AdditionalAuthcodeParams)
p.AdditionalAuthcodeParams = map[string]string{"additional": "authcodeParams"}
require.Equal(t, p.GetAdditionalAuthcodeParams(), map[string]string{"additional": "authcodeParams"})
// AdditionalClaimMappings defaults to empty
require.Empty(t, p.AdditionalClaimMappings)
p.AdditionalClaimMappings = map[string]string{"additional": "claimMappings"}
require.Equal(t, p.GetAdditionalClaimMappings(), map[string]string{"additional": "claimMappings"})
}) })
const ( const (

View File

@ -505,7 +505,7 @@ func requireWellKnownEndpointIsWorking(t *testing.T, supervisorScheme, superviso
"response_types_supported": ["code"], "response_types_supported": ["code"],
"response_modes_supported": ["query", "form_post"], "response_modes_supported": ["query", "form_post"],
"code_challenge_methods_supported": ["S256"], "code_challenge_methods_supported": ["S256"],
"claims_supported": ["username", "groups"], "claims_supported": ["username", "groups", "additionalClaims"],
"discovery.supervisor.pinniped.dev/v1alpha1": {"pinniped_identity_providers_endpoint": "%s/v1alpha1/pinniped_identity_providers"}, "discovery.supervisor.pinniped.dev/v1alpha1": {"pinniped_identity_providers_endpoint": "%s/v1alpha1/pinniped_identity_providers"},
"subject_types_supported": ["public"], "subject_types_supported": ["public"],
"id_token_signing_alg_values_supported": ["ES256"] "id_token_signing_alg_values_supported": ["ES256"]