From 8ff6ef32e94a661edf6a00c32401ad1273aee7a8 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Tue, 20 Sep 2022 14:54:10 -0700 Subject: [PATCH] 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 Co-authored-by: Joshua Casey --- .../types_oidcidentityprovider.go.tmpl | 11 + .../oidc/types_supervisor_oidc.go.tmpl | 4 + ...or.pinniped.dev_oidcidentityproviders.yaml | 18 + generated/1.17/README.adoc | 1 + .../v1alpha1/types_oidcidentityprovider.go | 11 + .../idp/v1alpha1/zz_generated.deepcopy.go | 9 +- .../supervisor/oidc/types_supervisor_oidc.go | 4 + ...or.pinniped.dev_oidcidentityproviders.yaml | 18 + generated/1.18/README.adoc | 1 + .../v1alpha1/types_oidcidentityprovider.go | 11 + .../idp/v1alpha1/zz_generated.deepcopy.go | 9 +- .../supervisor/oidc/types_supervisor_oidc.go | 4 + ...or.pinniped.dev_oidcidentityproviders.yaml | 18 + generated/1.19/README.adoc | 1 + .../v1alpha1/types_oidcidentityprovider.go | 11 + .../idp/v1alpha1/zz_generated.deepcopy.go | 9 +- .../supervisor/oidc/types_supervisor_oidc.go | 4 + ...or.pinniped.dev_oidcidentityproviders.yaml | 18 + generated/1.20/README.adoc | 1 + .../v1alpha1/types_oidcidentityprovider.go | 11 + .../idp/v1alpha1/zz_generated.deepcopy.go | 9 +- .../supervisor/oidc/types_supervisor_oidc.go | 4 + ...or.pinniped.dev_oidcidentityproviders.yaml | 18 + generated/1.21/README.adoc | 1 + .../v1alpha1/types_oidcidentityprovider.go | 11 + .../idp/v1alpha1/zz_generated.deepcopy.go | 9 +- .../supervisor/oidc/types_supervisor_oidc.go | 4 + ...or.pinniped.dev_oidcidentityproviders.yaml | 18 + generated/1.22/README.adoc | 1 + .../v1alpha1/types_oidcidentityprovider.go | 11 + .../idp/v1alpha1/zz_generated.deepcopy.go | 9 +- .../supervisor/oidc/types_supervisor_oidc.go | 4 + ...or.pinniped.dev_oidcidentityproviders.yaml | 18 + generated/1.23/README.adoc | 1 + .../v1alpha1/types_oidcidentityprovider.go | 11 + .../idp/v1alpha1/zz_generated.deepcopy.go | 9 +- .../supervisor/oidc/types_supervisor_oidc.go | 4 + ...or.pinniped.dev_oidcidentityproviders.yaml | 18 + generated/1.24/README.adoc | 1 + .../v1alpha1/types_oidcidentityprovider.go | 11 + .../idp/v1alpha1/zz_generated.deepcopy.go | 9 +- .../supervisor/oidc/types_supervisor_oidc.go | 4 + ...or.pinniped.dev_oidcidentityproviders.yaml | 18 + generated/1.25/README.adoc | 1 + .../v1alpha1/types_oidcidentityprovider.go | 11 + .../idp/v1alpha1/zz_generated.deepcopy.go | 9 +- .../supervisor/oidc/types_supervisor_oidc.go | 4 + ...or.pinniped.dev_oidcidentityproviders.yaml | 18 + .../v1alpha1/types_oidcidentityprovider.go | 11 + .../idp/v1alpha1/zz_generated.deepcopy.go | 9 +- .../supervisor/oidc/types_supervisor_oidc.go | 4 + .../oidc_upstream_watcher.go | 1 + .../oidc_upstream_watcher_test.go | 18 +- .../mockupstreamoidcidentityprovider.go | 14 + internal/oidc/auth/auth_handler.go | 6 +- internal/oidc/auth/auth_handler_test.go | 72 ++- internal/oidc/callback/callback_handler.go | 4 +- .../oidc/callback/callback_handler_test.go | 46 ++ internal/oidc/discovery/discovery_handler.go | 2 +- .../oidc/discovery/discovery_handler_test.go | 2 +- .../downstreamsession/downstream_session.go | 25 ++ .../downstream_session_test.go | 72 +++ internal/oidc/login/post_login_handler.go | 2 +- .../oidc/login/post_login_handler_test.go | 2 + .../provider/dynamic_upstream_idp_provider.go | 3 + internal/oidc/token/token_handler_test.go | 419 ++++++++++++++---- .../testutil/oidctestutil/oidctestutil.go | 29 ++ internal/upstreamoidc/upstreamoidc.go | 5 + internal/upstreamoidc/upstreamoidc_test.go | 10 + test/integration/supervisor_discovery_test.go | 2 +- 70 files changed, 1084 insertions(+), 94 deletions(-) create mode 100644 internal/oidc/downstreamsession/downstream_session_test.go diff --git a/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go.tmpl b/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go.tmpl index 798275a9..f9c0b28e 100644 --- a/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go.tmpl +++ b/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go.tmpl @@ -138,6 +138,17 @@ type OIDCClaims struct { // the ID token. // +optional 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 diff --git a/apis/supervisor/oidc/types_supervisor_oidc.go.tmpl b/apis/supervisor/oidc/types_supervisor_oidc.go.tmpl index cb6fe627..e0ab770f 100644 --- a/apis/supervisor/oidc/types_supervisor_oidc.go.tmpl +++ b/apis/supervisor/oidc/types_supervisor_oidc.go.tmpl @@ -40,6 +40,10 @@ const ( // group names which were mapped from the upstream identity provider. 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 = "authorization_code" diff --git a/deploy/supervisor/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml b/deploy/supervisor/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml index 2b91026a..9bb24fd9 100644 --- a/deploy/supervisor/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml +++ b/deploy/supervisor/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml @@ -185,6 +185,24 @@ spec: description: Claims provides the names of token claims that will be used when inspecting an identity from this OIDC identity provider. 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: description: Groups provides the name of the ID token claim or userinfo endpoint response claim that will be used to ascertain diff --git a/generated/1.17/README.adoc b/generated/1.17/README.adoc index 57238ca5..a7f8a622 100644 --- a/generated/1.17/README.adoc +++ b/generated/1.17/README.adoc @@ -1391,6 +1391,7 @@ OIDCClaims provides a mapping from upstream claims into identities. | 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. | *`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. |=== diff --git a/generated/1.17/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go b/generated/1.17/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go index 798275a9..f9c0b28e 100644 --- a/generated/1.17/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go +++ b/generated/1.17/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go @@ -138,6 +138,17 @@ type OIDCClaims struct { // the ID token. // +optional 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 diff --git a/generated/1.17/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go b/generated/1.17/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go index 5f5be6f3..a187b7ca 100644 --- a/generated/1.17/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.17/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go @@ -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. func (in *OIDCClaims) DeepCopyInto(out *OIDCClaims) { *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 } @@ -537,7 +544,7 @@ func (in *OIDCIdentityProviderSpec) DeepCopyInto(out *OIDCIdentityProviderSpec) **out = **in } in.AuthorizationConfig.DeepCopyInto(&out.AuthorizationConfig) - out.Claims = in.Claims + in.Claims.DeepCopyInto(&out.Claims) out.Client = in.Client return } diff --git a/generated/1.17/apis/supervisor/oidc/types_supervisor_oidc.go b/generated/1.17/apis/supervisor/oidc/types_supervisor_oidc.go index cb6fe627..e0ab770f 100644 --- a/generated/1.17/apis/supervisor/oidc/types_supervisor_oidc.go +++ b/generated/1.17/apis/supervisor/oidc/types_supervisor_oidc.go @@ -40,6 +40,10 @@ const ( // group names which were mapped from the upstream identity provider. 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 = "authorization_code" diff --git a/generated/1.17/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml b/generated/1.17/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml index 2b91026a..9bb24fd9 100644 --- a/generated/1.17/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml +++ b/generated/1.17/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml @@ -185,6 +185,24 @@ spec: description: Claims provides the names of token claims that will be used when inspecting an identity from this OIDC identity provider. 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: description: Groups provides the name of the ID token claim or userinfo endpoint response claim that will be used to ascertain diff --git a/generated/1.18/README.adoc b/generated/1.18/README.adoc index 698221e8..992d27c1 100644 --- a/generated/1.18/README.adoc +++ b/generated/1.18/README.adoc @@ -1391,6 +1391,7 @@ OIDCClaims provides a mapping from upstream claims into identities. | 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. | *`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. |=== diff --git a/generated/1.18/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go b/generated/1.18/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go index 798275a9..f9c0b28e 100644 --- a/generated/1.18/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go +++ b/generated/1.18/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go @@ -138,6 +138,17 @@ type OIDCClaims struct { // the ID token. // +optional 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 diff --git a/generated/1.18/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go b/generated/1.18/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go index 5f5be6f3..a187b7ca 100644 --- a/generated/1.18/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.18/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go @@ -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. func (in *OIDCClaims) DeepCopyInto(out *OIDCClaims) { *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 } @@ -537,7 +544,7 @@ func (in *OIDCIdentityProviderSpec) DeepCopyInto(out *OIDCIdentityProviderSpec) **out = **in } in.AuthorizationConfig.DeepCopyInto(&out.AuthorizationConfig) - out.Claims = in.Claims + in.Claims.DeepCopyInto(&out.Claims) out.Client = in.Client return } diff --git a/generated/1.18/apis/supervisor/oidc/types_supervisor_oidc.go b/generated/1.18/apis/supervisor/oidc/types_supervisor_oidc.go index cb6fe627..e0ab770f 100644 --- a/generated/1.18/apis/supervisor/oidc/types_supervisor_oidc.go +++ b/generated/1.18/apis/supervisor/oidc/types_supervisor_oidc.go @@ -40,6 +40,10 @@ const ( // group names which were mapped from the upstream identity provider. 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 = "authorization_code" diff --git a/generated/1.18/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml b/generated/1.18/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml index 2b91026a..9bb24fd9 100644 --- a/generated/1.18/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml +++ b/generated/1.18/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml @@ -185,6 +185,24 @@ spec: description: Claims provides the names of token claims that will be used when inspecting an identity from this OIDC identity provider. 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: description: Groups provides the name of the ID token claim or userinfo endpoint response claim that will be used to ascertain diff --git a/generated/1.19/README.adoc b/generated/1.19/README.adoc index 1c3ee7e9..c15fa11d 100644 --- a/generated/1.19/README.adoc +++ b/generated/1.19/README.adoc @@ -1391,6 +1391,7 @@ OIDCClaims provides a mapping from upstream claims into identities. | 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. | *`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. |=== diff --git a/generated/1.19/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go b/generated/1.19/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go index 798275a9..f9c0b28e 100644 --- a/generated/1.19/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go +++ b/generated/1.19/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go @@ -138,6 +138,17 @@ type OIDCClaims struct { // the ID token. // +optional 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 diff --git a/generated/1.19/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go b/generated/1.19/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go index 5f5be6f3..a187b7ca 100644 --- a/generated/1.19/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.19/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go @@ -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. func (in *OIDCClaims) DeepCopyInto(out *OIDCClaims) { *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 } @@ -537,7 +544,7 @@ func (in *OIDCIdentityProviderSpec) DeepCopyInto(out *OIDCIdentityProviderSpec) **out = **in } in.AuthorizationConfig.DeepCopyInto(&out.AuthorizationConfig) - out.Claims = in.Claims + in.Claims.DeepCopyInto(&out.Claims) out.Client = in.Client return } diff --git a/generated/1.19/apis/supervisor/oidc/types_supervisor_oidc.go b/generated/1.19/apis/supervisor/oidc/types_supervisor_oidc.go index cb6fe627..e0ab770f 100644 --- a/generated/1.19/apis/supervisor/oidc/types_supervisor_oidc.go +++ b/generated/1.19/apis/supervisor/oidc/types_supervisor_oidc.go @@ -40,6 +40,10 @@ const ( // group names which were mapped from the upstream identity provider. 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 = "authorization_code" diff --git a/generated/1.19/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml b/generated/1.19/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml index 2b91026a..9bb24fd9 100644 --- a/generated/1.19/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml +++ b/generated/1.19/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml @@ -185,6 +185,24 @@ spec: description: Claims provides the names of token claims that will be used when inspecting an identity from this OIDC identity provider. 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: description: Groups provides the name of the ID token claim or userinfo endpoint response claim that will be used to ascertain diff --git a/generated/1.20/README.adoc b/generated/1.20/README.adoc index 03683bf9..f027b91c 100644 --- a/generated/1.20/README.adoc +++ b/generated/1.20/README.adoc @@ -1391,6 +1391,7 @@ OIDCClaims provides a mapping from upstream claims into identities. | 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. | *`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. |=== diff --git a/generated/1.20/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go b/generated/1.20/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go index 798275a9..f9c0b28e 100644 --- a/generated/1.20/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go +++ b/generated/1.20/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go @@ -138,6 +138,17 @@ type OIDCClaims struct { // the ID token. // +optional 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 diff --git a/generated/1.20/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go b/generated/1.20/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go index 5f5be6f3..a187b7ca 100644 --- a/generated/1.20/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.20/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go @@ -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. func (in *OIDCClaims) DeepCopyInto(out *OIDCClaims) { *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 } @@ -537,7 +544,7 @@ func (in *OIDCIdentityProviderSpec) DeepCopyInto(out *OIDCIdentityProviderSpec) **out = **in } in.AuthorizationConfig.DeepCopyInto(&out.AuthorizationConfig) - out.Claims = in.Claims + in.Claims.DeepCopyInto(&out.Claims) out.Client = in.Client return } diff --git a/generated/1.20/apis/supervisor/oidc/types_supervisor_oidc.go b/generated/1.20/apis/supervisor/oidc/types_supervisor_oidc.go index cb6fe627..e0ab770f 100644 --- a/generated/1.20/apis/supervisor/oidc/types_supervisor_oidc.go +++ b/generated/1.20/apis/supervisor/oidc/types_supervisor_oidc.go @@ -40,6 +40,10 @@ const ( // group names which were mapped from the upstream identity provider. 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 = "authorization_code" diff --git a/generated/1.20/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml b/generated/1.20/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml index 2b91026a..9bb24fd9 100644 --- a/generated/1.20/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml +++ b/generated/1.20/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml @@ -185,6 +185,24 @@ spec: description: Claims provides the names of token claims that will be used when inspecting an identity from this OIDC identity provider. 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: description: Groups provides the name of the ID token claim or userinfo endpoint response claim that will be used to ascertain diff --git a/generated/1.21/README.adoc b/generated/1.21/README.adoc index 4332b524..af34613b 100644 --- a/generated/1.21/README.adoc +++ b/generated/1.21/README.adoc @@ -1391,6 +1391,7 @@ OIDCClaims provides a mapping from upstream claims into identities. | 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. | *`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. |=== diff --git a/generated/1.21/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go b/generated/1.21/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go index 798275a9..f9c0b28e 100644 --- a/generated/1.21/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go +++ b/generated/1.21/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go @@ -138,6 +138,17 @@ type OIDCClaims struct { // the ID token. // +optional 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 diff --git a/generated/1.21/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go b/generated/1.21/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go index 5f5be6f3..a187b7ca 100644 --- a/generated/1.21/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.21/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go @@ -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. func (in *OIDCClaims) DeepCopyInto(out *OIDCClaims) { *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 } @@ -537,7 +544,7 @@ func (in *OIDCIdentityProviderSpec) DeepCopyInto(out *OIDCIdentityProviderSpec) **out = **in } in.AuthorizationConfig.DeepCopyInto(&out.AuthorizationConfig) - out.Claims = in.Claims + in.Claims.DeepCopyInto(&out.Claims) out.Client = in.Client return } diff --git a/generated/1.21/apis/supervisor/oidc/types_supervisor_oidc.go b/generated/1.21/apis/supervisor/oidc/types_supervisor_oidc.go index cb6fe627..e0ab770f 100644 --- a/generated/1.21/apis/supervisor/oidc/types_supervisor_oidc.go +++ b/generated/1.21/apis/supervisor/oidc/types_supervisor_oidc.go @@ -40,6 +40,10 @@ const ( // group names which were mapped from the upstream identity provider. 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 = "authorization_code" diff --git a/generated/1.21/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml b/generated/1.21/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml index 2b91026a..9bb24fd9 100644 --- a/generated/1.21/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml +++ b/generated/1.21/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml @@ -185,6 +185,24 @@ spec: description: Claims provides the names of token claims that will be used when inspecting an identity from this OIDC identity provider. 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: description: Groups provides the name of the ID token claim or userinfo endpoint response claim that will be used to ascertain diff --git a/generated/1.22/README.adoc b/generated/1.22/README.adoc index 2a4bf990..3967d637 100644 --- a/generated/1.22/README.adoc +++ b/generated/1.22/README.adoc @@ -1391,6 +1391,7 @@ OIDCClaims provides a mapping from upstream claims into identities. | 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. | *`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. |=== diff --git a/generated/1.22/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go b/generated/1.22/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go index 798275a9..f9c0b28e 100644 --- a/generated/1.22/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go +++ b/generated/1.22/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go @@ -138,6 +138,17 @@ type OIDCClaims struct { // the ID token. // +optional 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 diff --git a/generated/1.22/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go b/generated/1.22/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go index 5f5be6f3..a187b7ca 100644 --- a/generated/1.22/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.22/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go @@ -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. func (in *OIDCClaims) DeepCopyInto(out *OIDCClaims) { *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 } @@ -537,7 +544,7 @@ func (in *OIDCIdentityProviderSpec) DeepCopyInto(out *OIDCIdentityProviderSpec) **out = **in } in.AuthorizationConfig.DeepCopyInto(&out.AuthorizationConfig) - out.Claims = in.Claims + in.Claims.DeepCopyInto(&out.Claims) out.Client = in.Client return } diff --git a/generated/1.22/apis/supervisor/oidc/types_supervisor_oidc.go b/generated/1.22/apis/supervisor/oidc/types_supervisor_oidc.go index cb6fe627..e0ab770f 100644 --- a/generated/1.22/apis/supervisor/oidc/types_supervisor_oidc.go +++ b/generated/1.22/apis/supervisor/oidc/types_supervisor_oidc.go @@ -40,6 +40,10 @@ const ( // group names which were mapped from the upstream identity provider. 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 = "authorization_code" diff --git a/generated/1.22/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml b/generated/1.22/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml index 2b91026a..9bb24fd9 100644 --- a/generated/1.22/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml +++ b/generated/1.22/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml @@ -185,6 +185,24 @@ spec: description: Claims provides the names of token claims that will be used when inspecting an identity from this OIDC identity provider. 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: description: Groups provides the name of the ID token claim or userinfo endpoint response claim that will be used to ascertain diff --git a/generated/1.23/README.adoc b/generated/1.23/README.adoc index 05a0fbe9..2e47487a 100644 --- a/generated/1.23/README.adoc +++ b/generated/1.23/README.adoc @@ -1391,6 +1391,7 @@ OIDCClaims provides a mapping from upstream claims into identities. | 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. | *`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. |=== diff --git a/generated/1.23/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go b/generated/1.23/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go index 798275a9..f9c0b28e 100644 --- a/generated/1.23/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go +++ b/generated/1.23/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go @@ -138,6 +138,17 @@ type OIDCClaims struct { // the ID token. // +optional 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 diff --git a/generated/1.23/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go b/generated/1.23/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go index 5f5be6f3..a187b7ca 100644 --- a/generated/1.23/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.23/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go @@ -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. func (in *OIDCClaims) DeepCopyInto(out *OIDCClaims) { *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 } @@ -537,7 +544,7 @@ func (in *OIDCIdentityProviderSpec) DeepCopyInto(out *OIDCIdentityProviderSpec) **out = **in } in.AuthorizationConfig.DeepCopyInto(&out.AuthorizationConfig) - out.Claims = in.Claims + in.Claims.DeepCopyInto(&out.Claims) out.Client = in.Client return } diff --git a/generated/1.23/apis/supervisor/oidc/types_supervisor_oidc.go b/generated/1.23/apis/supervisor/oidc/types_supervisor_oidc.go index cb6fe627..e0ab770f 100644 --- a/generated/1.23/apis/supervisor/oidc/types_supervisor_oidc.go +++ b/generated/1.23/apis/supervisor/oidc/types_supervisor_oidc.go @@ -40,6 +40,10 @@ const ( // group names which were mapped from the upstream identity provider. 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 = "authorization_code" diff --git a/generated/1.23/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml b/generated/1.23/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml index 2b91026a..9bb24fd9 100644 --- a/generated/1.23/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml +++ b/generated/1.23/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml @@ -185,6 +185,24 @@ spec: description: Claims provides the names of token claims that will be used when inspecting an identity from this OIDC identity provider. 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: description: Groups provides the name of the ID token claim or userinfo endpoint response claim that will be used to ascertain diff --git a/generated/1.24/README.adoc b/generated/1.24/README.adoc index fc9c24c0..d69b3c1d 100644 --- a/generated/1.24/README.adoc +++ b/generated/1.24/README.adoc @@ -1391,6 +1391,7 @@ OIDCClaims provides a mapping from upstream claims into identities. | 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. | *`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. |=== diff --git a/generated/1.24/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go b/generated/1.24/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go index 798275a9..f9c0b28e 100644 --- a/generated/1.24/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go +++ b/generated/1.24/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go @@ -138,6 +138,17 @@ type OIDCClaims struct { // the ID token. // +optional 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 diff --git a/generated/1.24/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go b/generated/1.24/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go index 5f5be6f3..a187b7ca 100644 --- a/generated/1.24/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.24/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go @@ -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. func (in *OIDCClaims) DeepCopyInto(out *OIDCClaims) { *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 } @@ -537,7 +544,7 @@ func (in *OIDCIdentityProviderSpec) DeepCopyInto(out *OIDCIdentityProviderSpec) **out = **in } in.AuthorizationConfig.DeepCopyInto(&out.AuthorizationConfig) - out.Claims = in.Claims + in.Claims.DeepCopyInto(&out.Claims) out.Client = in.Client return } diff --git a/generated/1.24/apis/supervisor/oidc/types_supervisor_oidc.go b/generated/1.24/apis/supervisor/oidc/types_supervisor_oidc.go index cb6fe627..e0ab770f 100644 --- a/generated/1.24/apis/supervisor/oidc/types_supervisor_oidc.go +++ b/generated/1.24/apis/supervisor/oidc/types_supervisor_oidc.go @@ -40,6 +40,10 @@ const ( // group names which were mapped from the upstream identity provider. 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 = "authorization_code" diff --git a/generated/1.24/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml b/generated/1.24/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml index 2b91026a..9bb24fd9 100644 --- a/generated/1.24/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml +++ b/generated/1.24/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml @@ -185,6 +185,24 @@ spec: description: Claims provides the names of token claims that will be used when inspecting an identity from this OIDC identity provider. 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: description: Groups provides the name of the ID token claim or userinfo endpoint response claim that will be used to ascertain diff --git a/generated/1.25/README.adoc b/generated/1.25/README.adoc index 683593d3..204f1ea0 100644 --- a/generated/1.25/README.adoc +++ b/generated/1.25/README.adoc @@ -1387,6 +1387,7 @@ OIDCClaims provides a mapping from upstream claims into identities. | 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. | *`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. |=== diff --git a/generated/1.25/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go b/generated/1.25/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go index 798275a9..f9c0b28e 100644 --- a/generated/1.25/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go +++ b/generated/1.25/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go @@ -138,6 +138,17 @@ type OIDCClaims struct { // the ID token. // +optional 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 diff --git a/generated/1.25/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go b/generated/1.25/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go index 5f5be6f3..a187b7ca 100644 --- a/generated/1.25/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.25/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go @@ -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. func (in *OIDCClaims) DeepCopyInto(out *OIDCClaims) { *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 } @@ -537,7 +544,7 @@ func (in *OIDCIdentityProviderSpec) DeepCopyInto(out *OIDCIdentityProviderSpec) **out = **in } in.AuthorizationConfig.DeepCopyInto(&out.AuthorizationConfig) - out.Claims = in.Claims + in.Claims.DeepCopyInto(&out.Claims) out.Client = in.Client return } diff --git a/generated/1.25/apis/supervisor/oidc/types_supervisor_oidc.go b/generated/1.25/apis/supervisor/oidc/types_supervisor_oidc.go index cb6fe627..e0ab770f 100644 --- a/generated/1.25/apis/supervisor/oidc/types_supervisor_oidc.go +++ b/generated/1.25/apis/supervisor/oidc/types_supervisor_oidc.go @@ -40,6 +40,10 @@ const ( // group names which were mapped from the upstream identity provider. 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 = "authorization_code" diff --git a/generated/1.25/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml b/generated/1.25/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml index 2b91026a..9bb24fd9 100644 --- a/generated/1.25/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml +++ b/generated/1.25/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml @@ -185,6 +185,24 @@ spec: description: Claims provides the names of token claims that will be used when inspecting an identity from this OIDC identity provider. 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: description: Groups provides the name of the ID token claim or userinfo endpoint response claim that will be used to ascertain diff --git a/generated/latest/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go b/generated/latest/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go index 798275a9..f9c0b28e 100644 --- a/generated/latest/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go +++ b/generated/latest/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go @@ -138,6 +138,17 @@ type OIDCClaims struct { // the ID token. // +optional 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 diff --git a/generated/latest/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go b/generated/latest/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go index 5f5be6f3..a187b7ca 100644 --- a/generated/latest/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go +++ b/generated/latest/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go @@ -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. func (in *OIDCClaims) DeepCopyInto(out *OIDCClaims) { *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 } @@ -537,7 +544,7 @@ func (in *OIDCIdentityProviderSpec) DeepCopyInto(out *OIDCIdentityProviderSpec) **out = **in } in.AuthorizationConfig.DeepCopyInto(&out.AuthorizationConfig) - out.Claims = in.Claims + in.Claims.DeepCopyInto(&out.Claims) out.Client = in.Client return } diff --git a/generated/latest/apis/supervisor/oidc/types_supervisor_oidc.go b/generated/latest/apis/supervisor/oidc/types_supervisor_oidc.go index cb6fe627..e0ab770f 100644 --- a/generated/latest/apis/supervisor/oidc/types_supervisor_oidc.go +++ b/generated/latest/apis/supervisor/oidc/types_supervisor_oidc.go @@ -40,6 +40,10 @@ const ( // group names which were mapped from the upstream identity provider. 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 = "authorization_code" diff --git a/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher.go b/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher.go index e8792c4b..f75aca7b 100644 --- a/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher.go +++ b/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher.go @@ -215,6 +215,7 @@ func (c *oidcWatcherController) validateUpstream(ctx controllerlib.Context, upst GroupsClaim: upstream.Spec.Claims.Groups, AllowPasswordGrant: authorizationConfig.AllowPasswordGrant, AdditionalAuthcodeParams: additionalAuthcodeAuthorizeParameters, + AdditionalClaimMappings: upstream.Spec.Claims.AdditionalClaimMappings, ResourceUID: upstream.UID, } diff --git a/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher_test.go b/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher_test.go index 6a17908c..e30fc824 100644 --- a/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher_test.go +++ b/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher_test.go @@ -999,6 +999,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana GroupsClaim: testGroupsClaim, AllowPasswordGrant: true, AdditionalAuthcodeParams: map[string]string{}, + AdditionalClaimMappings: nil, // Does not default to empty map ResourceUID: testUID, }, }, @@ -1054,6 +1055,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana GroupsClaim: testGroupsClaim, AllowPasswordGrant: false, AdditionalAuthcodeParams: map[string]string{}, + AdditionalClaimMappings: nil, // Does not default to empty map ResourceUID: testUID, }, }, @@ -1109,6 +1111,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana GroupsClaim: testGroupsClaim, AllowPasswordGrant: false, AdditionalAuthcodeParams: map[string]string{}, + AdditionalClaimMappings: nil, // Does not default to empty map ResourceUID: testUID, }, }, @@ -1167,6 +1170,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana GroupsClaim: testGroupsClaim, AllowPasswordGrant: false, AdditionalAuthcodeParams: map[string]string{}, + AdditionalClaimMappings: nil, // Does not default to empty map ResourceUID: testUID, }, }, @@ -1195,7 +1199,13 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana AdditionalAuthorizeParameters: testAdditionalParams, 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{ Phase: "Ready", @@ -1227,7 +1237,10 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana GroupsClaim: testGroupsClaim, AllowPasswordGrant: true, AdditionalAuthcodeParams: testExpectedAdditionalParams, - ResourceUID: testUID, + AdditionalClaimMappings: map[string]string{ + "downstream": "upstream", + }, + ResourceUID: testUID, }, }, 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].AllowsPasswordGrant(), actualIDP.AllowsPasswordGrant()) 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].GetRevocationURL(), actualIDP.GetRevocationURL()) require.ElementsMatch(t, tt.wantResultingCache[i].GetScopes(), actualIDP.GetScopes()) diff --git a/internal/mocks/mockupstreamoidcidentityprovider/mockupstreamoidcidentityprovider.go b/internal/mocks/mockupstreamoidcidentityprovider/mockupstreamoidcidentityprovider.go index 87d5b75b..6b6c6ee2 100644 --- a/internal/mocks/mockupstreamoidcidentityprovider/mockupstreamoidcidentityprovider.go +++ b/internal/mocks/mockupstreamoidcidentityprovider/mockupstreamoidcidentityprovider.go @@ -88,6 +88,20 @@ func (mr *MockUpstreamOIDCIdentityProviderIMockRecorder) GetAdditionalAuthcodePa 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. func (m *MockUpstreamOIDCIdentityProviderI) GetAuthorizationURL() *url.URL { m.ctrl.T.Helper() diff --git a/internal/oidc/auth/auth_handler.go b/internal/oidc/auth/auth_handler.go index 614e8b68..c1fcc35f 100644 --- a/internal/oidc/auth/auth_handler.go +++ b/internal/oidc/auth/auth_handler.go @@ -150,7 +150,7 @@ func handleAuthRequestForLDAPUpstreamCLIFlow( groups := authenticateResponse.User.GetGroups() customSessionData := downstreamsession.MakeDownstreamLDAPOrADCustomSessionData(ldapUpstream, idpType, authenticateResponse, username) 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) return nil @@ -243,6 +243,8 @@ func handleAuthRequestForOIDCUpstreamPasswordGrant( return nil } + additionalClaims := downstreamsession.MapAdditionalClaimsFromUpstreamIDToken(oidcUpstream, token.IDToken.Claims) + customSessionData, err := downstreamsession.MakeDownstreamOIDCCustomSessionData(oidcUpstream, token, username) if err != nil { oidc.WriteAuthorizeError(r, w, oauthHelper, authorizeRequester, @@ -252,7 +254,7 @@ func handleAuthRequestForOIDCUpstreamPasswordGrant( } 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) diff --git a/internal/oidc/auth/auth_handler_test.go b/internal/oidc/auth/auth_handler_test.go index 0e1af24f..fe36c36f 100644 --- a/internal/oidc/auth/auth_handler_test.go +++ b/internal/oidc/auth/auth_handler_test.go @@ -582,6 +582,7 @@ func TestAuthorizationEndpoint(t *testing.T) { wantUnnecessaryStoredRecords int wantPasswordGrantCall *expectedPasswordGrant wantDownstreamCustomSessionData *psession.CustomSessionData + wantAdditionalClaims map[string]interface{} } tests := []testCase{ { @@ -711,6 +712,68 @@ func TestAuthorizationEndpoint(t *testing.T) { wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, 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", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), @@ -3126,6 +3189,7 @@ func TestAuthorizationEndpoint(t *testing.T) { test.wantDownstreamClientID, test.wantDownstreamRedirectURI, test.wantDownstreamCustomSessionData, + test.wantAdditionalClaims, ) default: require.Empty(t, rsp.Header().Values("Location")) @@ -3176,9 +3240,15 @@ func TestAuthorizationEndpoint(t *testing.T) { oidcClientsClient := supervisorClient.ConfigV1alpha1().OIDCClients("some-namespace") oauthHelperWithRealStorage, kubeOauthStore := createOauthHelperWithRealStorage(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( downstreamIssuer, - test.idps.Build(), + idps, oauthHelperWithNullStorage, oauthHelperWithRealStorage, test.generateCSRF, test.generatePKCE, test.generateNonce, test.stateEncoder, test.cookieEncoder, diff --git a/internal/oidc/callback/callback_handler.go b/internal/oidc/callback/callback_handler.go index d1896bc2..ccededb4 100644 --- a/internal/oidc/callback/callback_handler.go +++ b/internal/oidc/callback/callback_handler.go @@ -74,13 +74,15 @@ func NewHandler( return httperr.Wrap(http.StatusUnprocessableEntity, err.Error(), err) } + additionalClaims := downstreamsession.MapAdditionalClaimsFromUpstreamIDToken(upstreamIDPConfig, token.IDToken.Claims) + customSessionData, err := downstreamsession.MakeDownstreamOIDCCustomSessionData(upstreamIDPConfig, token, username) if err != nil { return httperr.Wrap(http.StatusUnprocessableEntity, err.Error(), err) } 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) if err != nil { diff --git a/internal/oidc/callback/callback_handler_test.go b/internal/oidc/callback/callback_handler_test.go index 44794ea5..a94fed33 100644 --- a/internal/oidc/callback/callback_handler_test.go +++ b/internal/oidc/callback/callback_handler_test.go @@ -189,6 +189,7 @@ func TestCallbackEndpoint(t *testing.T) { wantDownstreamPKCEChallenge string wantDownstreamPKCEChallengeMethod string wantDownstreamCustomSessionData *psession.CustomSessionData + wantAdditionalClaims map[string]interface{} wantAuthcodeExchangeCall *expectedAuthcodeExchange }{ @@ -223,6 +224,49 @@ func TestCallbackEndpoint(t *testing.T) { 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: `(.+)`, + 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", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), @@ -1463,6 +1507,7 @@ func TestCallbackEndpoint(t *testing.T) { test.wantDownstreamClientID, downstreamRedirectURI, test.wantDownstreamCustomSessionData, + test.wantAdditionalClaims, ) // Otherwise, expect an empty response body. @@ -1490,6 +1535,7 @@ func TestCallbackEndpoint(t *testing.T) { test.wantDownstreamClientID, downstreamRedirectURI, test.wantDownstreamCustomSessionData, + test.wantAdditionalClaims, ) } }) diff --git a/internal/oidc/discovery/discovery_handler.go b/internal/oidc/discovery/discovery_handler.go index 7c4b9e53..fd045faa 100644 --- a/internal/oidc/discovery/discovery_handler.go +++ b/internal/oidc/discovery/discovery_handler.go @@ -70,7 +70,7 @@ func NewHandler(issuerURL string) http.Handler { TokenEndpointAuthMethodsSupported: []string{"client_secret_basic"}, CodeChallengeMethodsSupported: []string{"S256"}, 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 diff --git a/internal/oidc/discovery/discovery_handler_test.go b/internal/oidc/discovery/discovery_handler_test.go index 94592e7c..85dd54ca 100644 --- a/internal/oidc/discovery/discovery_handler_test.go +++ b/internal/oidc/discovery/discovery_handler_test.go @@ -47,7 +47,7 @@ func TestDiscovery(t *testing.T) { "token_endpoint_auth_methods_supported": ["client_secret_basic"], "scopes_supported": ["openid", "offline_access", "pinniped:request-audience", "username", "groups"], "code_challenge_methods_supported": ["S256"], - "claims_supported": ["username", "groups"], + "claims_supported": ["username", "groups", "additionalClaims"], "discovery.supervisor.pinniped.dev/v1alpha1": { "pinniped_identity_providers_endpoint": "https://some-issuer.com/some/path/v1alpha1/pinniped_identity_providers" } diff --git a/internal/oidc/downstreamsession/downstream_session.go b/internal/oidc/downstreamsession/downstream_session.go index 809a48f4..d83a317f 100644 --- a/internal/oidc/downstreamsession/downstream_session.go +++ b/internal/oidc/downstreamsession/downstream_session.go @@ -48,6 +48,7 @@ func MakeDownstreamSession( grantedScopes []string, clientID string, custom *psession.CustomSessionData, + additionalClaims map[string]interface{}, ) *psession.PinnipedSession { now := time.Now().UTC() openIDSession := &psession.PinnipedSession{ @@ -72,6 +73,9 @@ func MakeDownstreamSession( if slices.Contains(grantedScopes, oidcapi.ScopeGroups) { extras[oidcapi.IDTokenClaimGroups] = groups } + if len(additionalClaims) > 0 { + extras[oidcapi.IDTokenClaimAdditionalClaims] = additionalClaims + } openIDSession.IDTokenClaims().Extra = extras return openIDSession @@ -212,6 +216,27 @@ func GetDownstreamIdentityFromUpstreamIDToken( 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( upstreamIDPConfig provider.UpstreamOIDCIdentityProviderI, idTokenClaims map[string]interface{}, diff --git a/internal/oidc/downstreamsession/downstream_session_test.go b/internal/oidc/downstreamsession/downstream_session_test.go new file mode 100644 index 00000000..3e6caab4 --- /dev/null +++ b/internal/oidc/downstreamsession/downstream_session_test.go @@ -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) + }) + } +} diff --git a/internal/oidc/login/post_login_handler.go b/internal/oidc/login/post_login_handler.go index a5a2d04e..fa8ebb39 100644 --- a/internal/oidc/login/post_login_handler.go +++ b/internal/oidc/login/post_login_handler.go @@ -84,7 +84,7 @@ func NewPostHandler(issuerURL string, upstreamIDPs oidc.UpstreamIdentityProvider groups := authenticateResponse.User.GetGroups() customSessionData := downstreamsession.MakeDownstreamLDAPOrADCustomSessionData(ldapUpstream, idpType, authenticateResponse, username) 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) return nil diff --git a/internal/oidc/login/post_login_handler_test.go b/internal/oidc/login/post_login_handler_test.go index 72bce69a..cd401dc0 100644 --- a/internal/oidc/login/post_login_handler_test.go +++ b/internal/oidc/login/post_login_handler_test.go @@ -1027,6 +1027,7 @@ func TestPostLoginEndpoint(t *testing.T) { tt.wantDownstreamClient, tt.wantDownstreamRedirectURI, tt.wantDownstreamCustomSessionData, + map[string]interface{}{}, ) case tt.wantRedirectToLoginPageError != "": // Expecting an error redirect to the login UI page. @@ -1062,6 +1063,7 @@ func TestPostLoginEndpoint(t *testing.T) { tt.wantDownstreamClient, tt.wantDownstreamRedirectURI, tt.wantDownstreamCustomSessionData, + map[string]interface{}{}, ) default: require.Failf(t, "test should have expected a redirect or form body", diff --git a/internal/oidc/provider/dynamic_upstream_idp_provider.go b/internal/oidc/provider/dynamic_upstream_idp_provider.go index a5eabea5..6b0c40cb 100644 --- a/internal/oidc/provider/dynamic_upstream_idp_provider.go +++ b/internal/oidc/provider/dynamic_upstream_idp_provider.go @@ -61,6 +61,9 @@ type UpstreamOIDCIdentityProviderI interface { // GetAdditionalAuthcodeParams returns additional params to be sent on authcode requests. 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 // 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) diff --git a/internal/oidc/token/token_handler_test.go b/internal/oidc/token/token_handler_test.go index 0ea559b5..aeffb0b2 100644 --- a/internal/oidc/token/token_handler_test.go +++ b/internal/oidc/token/token_handler_test.go @@ -40,6 +40,7 @@ import ( v1 "k8s.io/client-go/kubernetes/typed/core/v1" 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" "go.pinniped.dev/internal/crud" "go.pinniped.dev/internal/fositestorage/accesstoken" @@ -285,19 +286,16 @@ type tokenEndpointResponseExpectedValues struct { wantUpstreamOIDCValidateTokenCall *expectedUpstreamValidateTokens wantCustomSessionDataStored *psession.CustomSessionData wantWarnings []RecordedWarning + wantAdditionalClaims map[string]interface{} } type authcodeExchangeInputs struct { - modifyAuthRequest func(authRequest *http.Request) - modifyTokenRequest func(tokenRequest *http.Request, authCode string) - modifyStorage func( - t *testing.T, - s fositestoragei.AllFositeStorage, - authCode string, - ) - makeOathHelper OauthHelperFactoryFunc - customSessionData *psession.CustomSessionData - want tokenEndpointResponseExpectedValues + modifyAuthRequest func(authRequest *http.Request) + modifyTokenRequest func(tokenRequest *http.Request, authCode string) + makeJwksSigningKeyAndProvider MakeJwksSigningKeyAndProviderFunc + customSessionData *psession.CustomSessionData + modifySession func(*psession.PinnipedSession) + want tokenEndpointResponseExpectedValues } 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", 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", 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", authcodeExchange: authcodeExchangeInputs{ - makeOathHelper: makeOauthHelperWithNilPrivateJWTSigningKey, + makeJwksSigningKeyAndProvider: func(t *testing.T, issuer string) (*ecdsa.PrivateKey, jwks.DynamicJWKSProvider) { + return nil, jwks.NewDynamicJWKSProvider() + }, want: tokenEndpointResponseExpectedValues{ wantStatus: http.StatusServiceUnavailable, wantErrorResponseBody: fositeTemporarilyUnavailableErrorBody, @@ -870,7 +937,7 @@ func TestTokenEndpointWhenAuthcodeIsUsedTwice(t *testing.T) { test.authcodeExchange.want.wantClientID, test.authcodeExchange.want.wantRequestedScopes, test.authcodeExchange.want.wantGrantedScopes, 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. 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", 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", authcodeExchange: authcodeExchangeInputs{ @@ -973,6 +1075,50 @@ func TestTokenEndpointTokenExchange(t *testing.T) { // tests for grant_type "urn requestedAudience: "some-workload-cluster", 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", 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") }, // Fail to fetch a JWK signing key after the authcode exchange has happened. - makeOathHelper: makeOauthHelperWithJWTKeyThatWorksOnlyOnce, - want: successfulAuthCodeExchange, + makeJwksSigningKeyAndProvider: func(t *testing.T, issuer string) (*ecdsa.PrivateKey, jwks.DynamicJWKSProvider) { + jwtSigningKey, jwkProvider := generateJWTSigningKeyAndJWKSProvider(t, goodIssuer) + return jwtSigningKey, &singleUseJWKProvider{DynamicJWKSProvider: jwkProvider} + }, + want: successfulAuthCodeExchange, }, requestedAudience: "some-workload-cluster", wantStatus: http.StatusServiceUnavailable, @@ -1481,6 +1630,9 @@ func TestTokenEndpointTokenExchange(t *testing.T) { // tests for grant_type "urn if test.authcodeExchange.want.wantGroups != nil { idTokenFields = append(idTokenFields, "groups") } + if len(test.authcodeExchange.want.wantAdditionalClaims) > 0 { + idTokenFields = append(idTokenFields, oidcapi.IDTokenClaimAdditionalClaims) + } require.ElementsMatch(t, idTokenFields, getMapKeys(tokenClaims)) // 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"]) } + 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. requireClaimsAreEqual(t, "iss", claimsOfFirstIDToken, tokenClaims) // issuer requireClaimsAreEqual(t, "sub", claimsOfFirstIDToken, tokenClaims) // subject requireClaimsAreEqual(t, "rat", claimsOfFirstIDToken, tokenClaims) // requested at 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. requireClaimsAreNotEqual(t, "jti", claimsOfFirstIDToken, tokenClaims) // JWT ID @@ -1691,6 +1854,12 @@ func TestRefreshGrant(t *testing.T) { 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 { want := happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(wantCustomSessionDataStored) 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", 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", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( @@ -3852,16 +4135,13 @@ func exchangeAuthcodeForTokens( var oauthHelper fosite.OAuth2Provider // 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) - if test.makeOathHelper != nil { - oauthHelper, authCode, jwtSigningKey = test.makeOathHelper(t, authRequest, oauthStore, test.customSessionData) - } else { - // Note that makeHappyOauthHelper() calls simulateAuthEndpointHavingAlreadyRun() to preload the session storage. - oauthHelper, authCode, jwtSigningKey = makeHappyOauthHelper(t, authRequest, oauthStore, test.customSessionData) + + if test.makeJwksSigningKeyAndProvider == nil { + test.makeJwksSigningKeyAndProvider = generateJWTSigningKeyAndJWKSProvider } - if test.modifyStorage != nil { - test.modifyStorage(t, oauthStore, authCode) - } + // Note that makeHappyOauthHelper() calls simulateAuthEndpointHavingAlreadyRun() to preload the session storage. + oauthHelper, authCode, jwtSigningKey = makeHappyOauthHelper(t, authRequest, oauthStore, test.makeJwksSigningKeyAndProvider, test.customSessionData, test.modifySession) subject = NewHandler(idps, oauthHelper) @@ -3936,10 +4216,10 @@ func requireTokenEndpointBehavior( wantRefreshToken := contains(test.wantSuccessBodyFields, "refresh_token") 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) // 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 if wantRefreshToken { @@ -3948,10 +4228,10 @@ func requireTokenEndpointBehavior( expectedNumberOfIDSessionsStored := 0 if wantIDToken { 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 { - 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) @@ -4053,24 +4333,21 @@ func getFositeDataSignature(t *testing.T, data string) string { return split[1] } -type OauthHelperFactoryFunc func( - t *testing.T, - authRequest *http.Request, - store fositestoragei.AllFositeStorage, - initialCustomSessionData *psession.CustomSessionData, -) (fosite.OAuth2Provider, string, *ecdsa.PrivateKey) +type MakeJwksSigningKeyAndProviderFunc func(t *testing.T, issuer string) (*ecdsa.PrivateKey, jwks.DynamicJWKSProvider) func makeHappyOauthHelper( t *testing.T, authRequest *http.Request, store fositestoragei.AllFositeStorage, + makeJwksSigningKeyAndProvider MakeJwksSigningKeyAndProviderFunc, initialCustomSessionData *psession.CustomSessionData, + modifySession func(session *psession.PinnipedSession), ) (fosite.OAuth2Provider, string, *ecdsa.PrivateKey) { t.Helper() - jwtSigningKey, jwkProvider := generateJWTSigningKeyAndJWKSProvider(t, goodIssuer) + jwtSigningKey, jwkProvider := makeJwksSigningKeyAndProvider(t, goodIssuer) 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 } @@ -4087,40 +4364,13 @@ func (s *singleUseJWKProvider) GetJWKS(issuerName string) (jwks *jose.JSONWebKey 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. func simulateAuthEndpointHavingAlreadyRun( t *testing.T, authRequest *http.Request, oauthHelper fosite.OAuth2Provider, initialCustomSessionData *psession.CustomSessionData, + modifySession func(session *psession.PinnipedSession), ) fosite.AuthorizeResponder { // We only set the fields in the session that Fosite wants us to set. ctx := context.Background() @@ -4137,6 +4387,10 @@ func simulateAuthEndpointHavingAlreadyRun( }, Custom: initialCustomSessionData, } + if modifySession != nil { + modifySession(session) + } + authRequester, err := oauthHelper.NewAuthorizeRequest(ctx, authRequest) require.NoError(t, err) if strings.Contains(authRequest.Form.Get("scope"), "openid") { @@ -4212,6 +4466,7 @@ func requireValidRefreshTokenStorage( wantUsername string, wantGroups []string, wantCustomSessionData *psession.CustomSessionData, + wantAdditionalClaims map[string]interface{}, secrets v1.SecretInterface, requestTime time.Time, ) { @@ -4241,6 +4496,7 @@ func requireValidRefreshTokenStorage( wantUsername, wantGroups, wantCustomSessionData, + wantAdditionalClaims, requestTime, ) @@ -4257,6 +4513,7 @@ func requireValidAccessTokenStorage( wantUsername string, wantGroups []string, wantCustomSessionData *psession.CustomSessionData, + wantAdditionalClaims map[string]interface{}, secrets v1.SecretInterface, requestTime time.Time, ) { @@ -4305,6 +4562,7 @@ func requireValidAccessTokenStorage( wantUsername, wantGroups, wantCustomSessionData, + wantAdditionalClaims, requestTime, ) @@ -4351,6 +4609,7 @@ func requireValidOIDCStorage( wantUsername string, wantGroups []string, wantCustomSessionData *psession.CustomSessionData, + wantAdditionalClaims map[string]interface{}, requestTime time.Time, ) { t.Helper() @@ -4378,6 +4637,7 @@ func requireValidOIDCStorage( wantUsername, wantGroups, wantCustomSessionData, + wantAdditionalClaims, requestTime, ) } else { @@ -4397,6 +4657,7 @@ func requireValidStoredRequest( wantUsername string, wantGroups []string, wantCustomSessionData *psession.CustomSessionData, + wantAdditionalClaims map[string]interface{}, requestTime time.Time, ) { t.Helper() @@ -4429,6 +4690,9 @@ func requireValidStoredRequest( expectedExtra["groups"] = toSliceOfInterface(wantGroups) } expectedExtra["azp"] = wantClientID + if len(wantAdditionalClaims) > 0 { + expectedExtra[oidcapi.IDTokenClaimAdditionalClaims] = wantAdditionalClaims + } require.Equal(t, expectedExtra, claims.Extra) // 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, wantUsernameInIDToken string, wantGroupsInIDToken []string, + wantAdditionalClaims map[string]interface{}, actualAccessToken string, requestTime time.Time, ) { @@ -4532,18 +4797,19 @@ func requireValidIDToken( token := oidctestutil.VerifyECDSAIDToken(t, goodIssuer, wantClientID, jwtSigningKey, idTokenString) var claims struct { - Subject string `json:"sub"` - Audience []string `json:"aud"` - Issuer string `json:"iss"` - JTI string `json:"jti"` - Nonce string `json:"nonce"` - AccessTokenHash string `json:"at_hash"` - ExpiresAt int64 `json:"exp"` - IssuedAt int64 `json:"iat"` - RequestedAt int64 `json:"rat"` - AuthTime int64 `json:"auth_time"` - Groups []string `json:"groups"` - Username string `json:"username"` + Subject string `json:"sub"` + Audience []string `json:"aud"` + Issuer string `json:"iss"` + JTI string `json:"jti"` + Nonce string `json:"nonce"` + AccessTokenHash string `json:"at_hash"` + ExpiresAt int64 `json:"exp"` + IssuedAt int64 `json:"iat"` + RequestedAt int64 `json:"rat"` + AuthTime int64 `json:"auth_time"` + Groups []string `json:"groups"` + Username string `json:"username"` + AdditionalClaims map[string]interface{} `json:"additionalClaims"` } idTokenFields := []string{"sub", "aud", "iss", "jti", "auth_time", "exp", "iat", "rat", "azp", "at_hash"} @@ -4556,6 +4822,9 @@ func requireValidIDToken( if wantGroupsInIDToken != nil { idTokenFields = append(idTokenFields, "groups") } + if len(wantAdditionalClaims) > 0 { + idTokenFields = append(idTokenFields, oidcapi.IDTokenClaimAdditionalClaims) + } // make sure that these are the only fields in the token var m map[string]interface{} @@ -4573,6 +4842,8 @@ func requireValidIDToken( require.Equal(t, wantClientID, m["azp"]) require.Equal(t, goodIssuer, claims.Issuer) 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 { require.Equal(t, goodNonce, claims.Nonce) diff --git a/internal/testutil/oidctestutil/oidctestutil.go b/internal/testutil/oidctestutil/oidctestutil.go index f5de96e7..92f470b8 100644 --- a/internal/testutil/oidctestutil/oidctestutil.go +++ b/internal/testutil/oidctestutil/oidctestutil.go @@ -28,6 +28,7 @@ import ( kubetesting "k8s.io/client-go/testing" "k8s.io/utils/strings/slices" + oidcapi "go.pinniped.dev/generated/latest/apis/supervisor/oidc" "go.pinniped.dev/internal/authenticators" "go.pinniped.dev/internal/crud" "go.pinniped.dev/internal/fositestorage/authorizationcode" @@ -164,6 +165,7 @@ type TestUpstreamOIDCIdentityProvider struct { GroupsClaim string Scopes []string AdditionalAuthcodeParams map[string]string + AdditionalClaimMappings map[string]string AllowPasswordGrant bool ExchangeAuthcodeAndValidateTokensFunc func( @@ -207,6 +209,10 @@ func (u *TestUpstreamOIDCIdentityProvider) GetAdditionalAuthcodeParams() map[str return u.AdditionalAuthcodeParams } +func (u *TestUpstreamOIDCIdentityProvider) GetAdditionalClaimMappings() map[string]string { + return u.AdditionalClaimMappings +} + func (u *TestUpstreamOIDCIdentityProvider) GetName() string { return u.Name } @@ -630,6 +636,7 @@ type TestUpstreamOIDCIdentityProviderBuilder struct { authorizationURL url.URL hasUserInfoURL bool additionalAuthcodeParams map[string]string + additionalClaimMappings map[string]string allowPasswordGrant bool authcodeExchangeErr error passwordGrantErr error @@ -716,6 +723,11 @@ func (u *TestUpstreamOIDCIdentityProviderBuilder) WithAdditionalAuthcodeParams(p return u } +func (u *TestUpstreamOIDCIdentityProviderBuilder) WithAdditionalClaimMappings(m map[string]string) *TestUpstreamOIDCIdentityProviderBuilder { + u.additionalClaimMappings = m + return u +} + func (u *TestUpstreamOIDCIdentityProviderBuilder) WithRefreshToken(token string) *TestUpstreamOIDCIdentityProviderBuilder { u.refreshToken = &oidctypes.RefreshToken{Token: token} return u @@ -792,6 +804,7 @@ func (u *TestUpstreamOIDCIdentityProviderBuilder) Build() *TestUpstreamOIDCIdent AuthorizationURL: u.authorizationURL, UserInfoURL: u.hasUserInfoURL, AdditionalAuthcodeParams: u.additionalAuthcodeParams, + AdditionalClaimMappings: u.additionalClaimMappings, ExchangeAuthcodeAndValidateTokensFunc: func(ctx context.Context, authcode string, pkceCodeVerifier pkce.Code, expectedIDTokenNonce nonce.Nonce) (*oidctypes.Token, error) { if u.authcodeExchangeErr != nil { return nil, u.authcodeExchangeErr @@ -934,6 +947,7 @@ func RequireAuthCodeRegexpMatch( wantDownstreamClientID string, wantDownstreamRedirectURI string, wantCustomSessionData *psession.CustomSessionData, + wantAdditionalClaims map[string]interface{}, ) { t.Helper() @@ -972,6 +986,7 @@ func RequireAuthCodeRegexpMatch( wantDownstreamClientID, wantDownstreamRedirectURI, wantCustomSessionData, + wantAdditionalClaims, ) // One PKCE should have been stored. @@ -1011,6 +1026,7 @@ func includesOpenIDScope(scopes []string) bool { return false } +//nolint:funlen func validateAuthcodeStorage( t *testing.T, oauthStore fositestoragei.AllFositeStorage, @@ -1023,6 +1039,7 @@ func validateAuthcodeStorage( wantDownstreamClientID string, wantDownstreamRedirectURI string, wantCustomSessionData *psession.CustomSessionData, + wantAdditionalClaims map[string]interface{}, ) (*fosite.Request, *psession.PinnipedSession) { t.Helper() @@ -1066,6 +1083,10 @@ func validateAuthcodeStorage( require.Equal(t, wantDownstreamClientID, actualClaims.Extra["azp"]) 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. require.Equal(t, wantDownstreamIDTokenSubject, actualClaims.Subject) if wantDownstreamIDTokenUsername == "" { @@ -1085,6 +1106,14 @@ func validateAuthcodeStorage( actualDownstreamIDTokenGroups := actualClaims.Extra["groups"] 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. require.Len(t, actualClaims.Extra, wantDownstreamIDTokenExtraClaimsCount) diff --git a/internal/upstreamoidc/upstreamoidc.go b/internal/upstreamoidc/upstreamoidc.go index dfe31137..af5c682c 100644 --- a/internal/upstreamoidc/upstreamoidc.go +++ b/internal/upstreamoidc/upstreamoidc.go @@ -43,6 +43,7 @@ type ProviderConfig struct { Client *http.Client AllowPasswordGrant bool AdditionalAuthcodeParams map[string]string + AdditionalClaimMappings map[string]string RevocationURL *url.URL // will commonly be nil: many providers do not offer this Provider interface { Verifier(*coreosoidc.Config) *coreosoidc.IDTokenVerifier @@ -78,6 +79,10 @@ func (p *ProviderConfig) GetAdditionalAuthcodeParams() map[string]string { return p.AdditionalAuthcodeParams } +func (p *ProviderConfig) GetAdditionalClaimMappings() map[string]string { + return p.AdditionalClaimMappings +} + func (p *ProviderConfig) GetName() string { return p.Name } diff --git a/internal/upstreamoidc/upstreamoidc_test.go b/internal/upstreamoidc/upstreamoidc_test.go index 79ae3c55..974048c2 100644 --- a/internal/upstreamoidc/upstreamoidc_test.go +++ b/internal/upstreamoidc/upstreamoidc_test.go @@ -68,6 +68,16 @@ func TestProviderConfig(t *testing.T) { rawClaims: []byte(`{`), } 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 ( diff --git a/test/integration/supervisor_discovery_test.go b/test/integration/supervisor_discovery_test.go index 0c0d7e6e..7463c2ff 100644 --- a/test/integration/supervisor_discovery_test.go +++ b/test/integration/supervisor_discovery_test.go @@ -505,7 +505,7 @@ func requireWellKnownEndpointIsWorking(t *testing.T, supervisorScheme, superviso "response_types_supported": ["code"], "response_modes_supported": ["query", "form_post"], "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"}, "subject_types_supported": ["public"], "id_token_signing_alg_values_supported": ["ES256"]