From def2b35e6e8ece979e34fb2c75d5f4fc8a6bdb53 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Fri, 2 Feb 2024 10:57:57 -0800 Subject: [PATCH 1/8] Make ID token lifetimes configurable on OIDCClient resources --- .../config/v1alpha1/types_oidcclient.go.tmpl | 24 +++- ...g.supervisor.pinniped.dev_oidcclients.yaml | 21 +++ generated/1.21/README.adoc | 18 +++ .../config/v1alpha1/types_oidcclient.go | 24 +++- .../config/v1alpha1/zz_generated.deepcopy.go | 22 +++ ...g.supervisor.pinniped.dev_oidcclients.yaml | 21 +++ generated/1.22/README.adoc | 18 +++ .../config/v1alpha1/types_oidcclient.go | 24 +++- .../config/v1alpha1/zz_generated.deepcopy.go | 22 +++ ...g.supervisor.pinniped.dev_oidcclients.yaml | 21 +++ generated/1.23/README.adoc | 18 +++ .../config/v1alpha1/types_oidcclient.go | 24 +++- .../config/v1alpha1/zz_generated.deepcopy.go | 22 +++ ...g.supervisor.pinniped.dev_oidcclients.yaml | 21 +++ generated/1.24/README.adoc | 18 +++ .../config/v1alpha1/types_oidcclient.go | 24 +++- .../config/v1alpha1/zz_generated.deepcopy.go | 22 +++ ...g.supervisor.pinniped.dev_oidcclients.yaml | 21 +++ generated/1.25/README.adoc | 18 +++ .../config/v1alpha1/types_oidcclient.go | 24 +++- .../config/v1alpha1/zz_generated.deepcopy.go | 22 +++ ...g.supervisor.pinniped.dev_oidcclients.yaml | 21 +++ generated/1.26/README.adoc | 18 +++ .../config/v1alpha1/types_oidcclient.go | 24 +++- .../config/v1alpha1/zz_generated.deepcopy.go | 22 +++ ...g.supervisor.pinniped.dev_oidcclients.yaml | 21 +++ generated/1.27/README.adoc | 18 +++ .../config/v1alpha1/types_oidcclient.go | 24 +++- .../config/v1alpha1/zz_generated.deepcopy.go | 22 +++ ...g.supervisor.pinniped.dev_oidcclients.yaml | 21 +++ generated/1.28/README.adoc | 18 +++ .../config/v1alpha1/types_oidcclient.go | 24 +++- .../config/v1alpha1/zz_generated.deepcopy.go | 22 +++ ...g.supervisor.pinniped.dev_oidcclients.yaml | 21 +++ generated/1.29/README.adoc | 18 +++ .../config/v1alpha1/types_oidcclient.go | 24 +++- .../config/v1alpha1/zz_generated.deepcopy.go | 22 +++ ...g.supervisor.pinniped.dev_oidcclients.yaml | 21 +++ generated/latest/README.adoc | 18 +++ .../config/v1alpha1/types_oidcclient.go | 24 +++- .../config/v1alpha1/zz_generated.deepcopy.go | 22 +++ internal/crud/crud.go | 20 ++- .../clientregistry/clientregistry.go | 18 ++- .../endpoints/token/token_handler.go | 30 +++- .../endpointsmanager/manager.go | 4 +- .../idtokenlifespan/idtoken_lifespan.go | 55 +++++++ internal/federationdomain/oidc/oidc.go | 136 +++++++++++++++--- .../timeouts/timeouts_configuration.go | 40 +++++- .../fositestorage/accesstoken/accesstoken.go | 14 +- .../authorizationcode/authorizationcode.go | 18 ++- .../openidconnect/openidconnect.go | 18 ++- internal/fositestorage/pkce/pkce.go | 18 ++- .../refreshtoken/refreshtoken.go | 14 +- .../oidcclientsecretstorage.go | 6 +- .../supervisor_oidc_client_test.go | 51 ++++++- 55 files changed, 1238 insertions(+), 78 deletions(-) create mode 100644 internal/federationdomain/idtokenlifespan/idtoken_lifespan.go diff --git a/apis/supervisor/config/v1alpha1/types_oidcclient.go.tmpl b/apis/supervisor/config/v1alpha1/types_oidcclient.go.tmpl index 61106fdba..b02307f94 100644 --- a/apis/supervisor/config/v1alpha1/types_oidcclient.go.tmpl +++ b/apis/supervisor/config/v1alpha1/types_oidcclient.go.tmpl @@ -1,4 +1,4 @@ -// Copyright 2022-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2022-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package v1alpha1 @@ -71,6 +71,28 @@ type OIDCClientSpec struct { // +listType=set // +kubebuilder:validation:MinItems=1 AllowedScopes []Scope `json:"allowedScopes"` + + // tokenLifetimes are the optional overrides of token lifetimes for an OIDCClient. + // +optional + TokenLifetimes OIDCClientTokenLifetimes `json:"tokenLifetimes,omitempty"` +} + +// OIDCClientTokenLifetimes describes the optional overrides of token lifetimes for an OIDCClient. +type OIDCClientTokenLifetimes struct { + // idTokenSeconds is the lifetime of ID tokens issued to this client, in seconds. This will choose the lifetime of + // ID tokens returned by the authorization flow and the refresh grant. It will not influence the lifetime of the ID + // tokens returned by RFC8693 token exchange. When null, a short-lived default value will be used. + // This value must be between 120 and 1,800 seconds (30 minutes), inclusive. It is recommended to make these tokens + // short-lived to force the client to perform the refresh grant often, because the refresh grant will check with the + // external identity provider to decide if it is acceptable for the end user to continue their session, and will + // update the end user's group memberships from the external identity provider. Giving these tokens a long life is + // will allow the end user to continue to use a token while avoiding these updates from the external identity + // provider. However, some web applications may have reasons specific to the design of that application to prefer + // longer lifetimes. + // +kubebuilder:validation:Minimum=120 + // +kubebuilder:validation:Maximum=1800 + // +optional + IDTokenSeconds *int32 `json:"idTokenSeconds,omitempty"` } // OIDCClientStatus is a struct that describes the actual state of an OIDCClient. diff --git a/deploy/supervisor/config.supervisor.pinniped.dev_oidcclients.yaml b/deploy/supervisor/config.supervisor.pinniped.dev_oidcclients.yaml index 0960ca0de..d33b31bf1 100644 --- a/deploy/supervisor/config.supervisor.pinniped.dev_oidcclients.yaml +++ b/deploy/supervisor/config.supervisor.pinniped.dev_oidcclients.yaml @@ -119,6 +119,27 @@ spec: minItems: 1 type: array x-kubernetes-list-type: set + tokenLifetimes: + description: tokenLifetimes are the optional overrides of token lifetimes + for an OIDCClient. + properties: + idTokenSeconds: + description: |- + idTokenSeconds is the lifetime of ID tokens issued to this client, in seconds. This will choose the lifetime of + ID tokens returned by the authorization flow and the refresh grant. It will not influence the lifetime of the ID + tokens returned by RFC8693 token exchange. When null, a short-lived default value will be used. + This value must be between 120 and 1,800 seconds (30 minutes), inclusive. It is recommended to make these tokens + short-lived to force the client to perform the refresh grant often, because the refresh grant will check with the + external identity provider to decide if it is acceptable for the end user to continue their session, and will + update the end user's group memberships from the external identity provider. Giving these tokens a long life is + will allow the end user to continue to use a token while avoiding these updates from the external identity + provider. However, some web applications may have reasons specific to the design of that application to prefer + longer lifetimes. + format: int32 + maximum: 1800 + minimum: 120 + type: integer + type: object required: - allowedGrantTypes - allowedRedirectURIs diff --git a/generated/1.21/README.adoc b/generated/1.21/README.adoc index 677bd9552..3e0a81969 100644 --- a/generated/1.21/README.adoc +++ b/generated/1.21/README.adoc @@ -937,6 +937,7 @@ Must only contain the following values: - authorization_code: allows the client | *`allowedScopes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-scope[$$Scope$$] array__ | allowedScopes is a list of the allowed scopes param values that should be accepted during OIDC flows with this client. + Must only contain the following values: - openid: The client is allowed to request ID tokens. ID tokens only include the required claims by default (iss, sub, aud, exp, iat). This scope must always be listed. - offline_access: The client is allowed to request an initial refresh token during the authorization code grant flow. This scope must be listed if allowedGrantTypes lists refresh_token. - pinniped:request-audience: The client is allowed to request a new audience value during a RFC8693 token exchange, which is a step in the process to be able to get a cluster credential for the user. openid, username and groups scopes must be listed when this scope is present. This scope must be listed if allowedGrantTypes lists urn:ietf:params:oauth:grant-type:token-exchange. - username: The client is allowed to request that ID tokens contain the user's username. Without the username scope being requested and allowed, the ID token will not contain the user's username. - groups: The client is allowed to request that ID tokens contain the user's group membership, if their group membership is discoverable by the Supervisor. Without the groups scope being requested and allowed, the ID token will not contain groups. +| *`tokenLifetimes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-oidcclienttokenlifetimes[$$OIDCClientTokenLifetimes$$]__ | tokenLifetimes are the optional overrides of token lifetimes for an OIDCClient. |=== @@ -959,6 +960,23 @@ OIDCClientStatus is a struct that describes the actual state of an OIDCClient. |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-oidcclienttokenlifetimes"] +==== OIDCClientTokenLifetimes + +OIDCClientTokenLifetimes describes the optional overrides of token lifetimes for an OIDCClient. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-oidcclientspec[$$OIDCClientSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`idTokenSeconds`* __integer__ | idTokenSeconds is the lifetime of ID tokens issued to this client, in seconds. This will choose the lifetime of ID tokens returned by the authorization flow and the refresh grant. It will not influence the lifetime of the ID tokens returned by RFC8693 token exchange. When null, a short-lived default value will be used. This value must be between 120 and 1,800 seconds (30 minutes), inclusive. It is recommended to make these tokens short-lived to force the client to perform the refresh grant often, because the refresh grant will check with the external identity provider to decide if it is acceptable for the end user to continue their session, and will update the end user's group memberships from the external identity provider. Giving these tokens a long life is will allow the end user to continue to use a token while avoiding these updates from the external identity provider. However, some web applications may have reasons specific to the design of that application to prefer longer lifetimes. +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-redirecturi"] ==== RedirectURI (string) diff --git a/generated/1.21/apis/supervisor/config/v1alpha1/types_oidcclient.go b/generated/1.21/apis/supervisor/config/v1alpha1/types_oidcclient.go index 61106fdba..b02307f94 100644 --- a/generated/1.21/apis/supervisor/config/v1alpha1/types_oidcclient.go +++ b/generated/1.21/apis/supervisor/config/v1alpha1/types_oidcclient.go @@ -1,4 +1,4 @@ -// Copyright 2022-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2022-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package v1alpha1 @@ -71,6 +71,28 @@ type OIDCClientSpec struct { // +listType=set // +kubebuilder:validation:MinItems=1 AllowedScopes []Scope `json:"allowedScopes"` + + // tokenLifetimes are the optional overrides of token lifetimes for an OIDCClient. + // +optional + TokenLifetimes OIDCClientTokenLifetimes `json:"tokenLifetimes,omitempty"` +} + +// OIDCClientTokenLifetimes describes the optional overrides of token lifetimes for an OIDCClient. +type OIDCClientTokenLifetimes struct { + // idTokenSeconds is the lifetime of ID tokens issued to this client, in seconds. This will choose the lifetime of + // ID tokens returned by the authorization flow and the refresh grant. It will not influence the lifetime of the ID + // tokens returned by RFC8693 token exchange. When null, a short-lived default value will be used. + // This value must be between 120 and 1,800 seconds (30 minutes), inclusive. It is recommended to make these tokens + // short-lived to force the client to perform the refresh grant often, because the refresh grant will check with the + // external identity provider to decide if it is acceptable for the end user to continue their session, and will + // update the end user's group memberships from the external identity provider. Giving these tokens a long life is + // will allow the end user to continue to use a token while avoiding these updates from the external identity + // provider. However, some web applications may have reasons specific to the design of that application to prefer + // longer lifetimes. + // +kubebuilder:validation:Minimum=120 + // +kubebuilder:validation:Maximum=1800 + // +optional + IDTokenSeconds *int32 `json:"idTokenSeconds,omitempty"` } // OIDCClientStatus is a struct that describes the actual state of an OIDCClient. diff --git a/generated/1.21/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go b/generated/1.21/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go index ad308c3de..d3c7ec69e 100644 --- a/generated/1.21/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.21/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go @@ -374,6 +374,7 @@ func (in *OIDCClientSpec) DeepCopyInto(out *OIDCClientSpec) { *out = make([]Scope, len(*in)) copy(*out, *in) } + in.TokenLifetimes.DeepCopyInto(&out.TokenLifetimes) return } @@ -409,3 +410,24 @@ func (in *OIDCClientStatus) DeepCopy() *OIDCClientStatus { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OIDCClientTokenLifetimes) DeepCopyInto(out *OIDCClientTokenLifetimes) { + *out = *in + if in.IDTokenSeconds != nil { + in, out := &in.IDTokenSeconds, &out.IDTokenSeconds + *out = new(int32) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OIDCClientTokenLifetimes. +func (in *OIDCClientTokenLifetimes) DeepCopy() *OIDCClientTokenLifetimes { + if in == nil { + return nil + } + out := new(OIDCClientTokenLifetimes) + in.DeepCopyInto(out) + return out +} diff --git a/generated/1.21/crds/config.supervisor.pinniped.dev_oidcclients.yaml b/generated/1.21/crds/config.supervisor.pinniped.dev_oidcclients.yaml index 30c154f7e..40cb1e21e 100644 --- a/generated/1.21/crds/config.supervisor.pinniped.dev_oidcclients.yaml +++ b/generated/1.21/crds/config.supervisor.pinniped.dev_oidcclients.yaml @@ -119,6 +119,27 @@ spec: minItems: 1 type: array x-kubernetes-list-type: set + tokenLifetimes: + description: tokenLifetimes are the optional overrides of token lifetimes + for an OIDCClient. + properties: + idTokenSeconds: + description: |- + idTokenSeconds is the lifetime of ID tokens issued to this client, in seconds. This will choose the lifetime of + ID tokens returned by the authorization flow and the refresh grant. It will not influence the lifetime of the ID + tokens returned by RFC8693 token exchange. When null, a short-lived default value will be used. + This value must be between 120 and 1,800 seconds (30 minutes), inclusive. It is recommended to make these tokens + short-lived to force the client to perform the refresh grant often, because the refresh grant will check with the + external identity provider to decide if it is acceptable for the end user to continue their session, and will + update the end user's group memberships from the external identity provider. Giving these tokens a long life is + will allow the end user to continue to use a token while avoiding these updates from the external identity + provider. However, some web applications may have reasons specific to the design of that application to prefer + longer lifetimes. + format: int32 + maximum: 1800 + minimum: 120 + type: integer + type: object required: - allowedGrantTypes - allowedRedirectURIs diff --git a/generated/1.22/README.adoc b/generated/1.22/README.adoc index de663d43f..412049ec8 100644 --- a/generated/1.22/README.adoc +++ b/generated/1.22/README.adoc @@ -937,6 +937,7 @@ Must only contain the following values: - authorization_code: allows the client | *`allowedScopes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-scope[$$Scope$$] array__ | allowedScopes is a list of the allowed scopes param values that should be accepted during OIDC flows with this client. + Must only contain the following values: - openid: The client is allowed to request ID tokens. ID tokens only include the required claims by default (iss, sub, aud, exp, iat). This scope must always be listed. - offline_access: The client is allowed to request an initial refresh token during the authorization code grant flow. This scope must be listed if allowedGrantTypes lists refresh_token. - pinniped:request-audience: The client is allowed to request a new audience value during a RFC8693 token exchange, which is a step in the process to be able to get a cluster credential for the user. openid, username and groups scopes must be listed when this scope is present. This scope must be listed if allowedGrantTypes lists urn:ietf:params:oauth:grant-type:token-exchange. - username: The client is allowed to request that ID tokens contain the user's username. Without the username scope being requested and allowed, the ID token will not contain the user's username. - groups: The client is allowed to request that ID tokens contain the user's group membership, if their group membership is discoverable by the Supervisor. Without the groups scope being requested and allowed, the ID token will not contain groups. +| *`tokenLifetimes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-oidcclienttokenlifetimes[$$OIDCClientTokenLifetimes$$]__ | tokenLifetimes are the optional overrides of token lifetimes for an OIDCClient. |=== @@ -959,6 +960,23 @@ OIDCClientStatus is a struct that describes the actual state of an OIDCClient. |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-oidcclienttokenlifetimes"] +==== OIDCClientTokenLifetimes + +OIDCClientTokenLifetimes describes the optional overrides of token lifetimes for an OIDCClient. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-oidcclientspec[$$OIDCClientSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`idTokenSeconds`* __integer__ | idTokenSeconds is the lifetime of ID tokens issued to this client, in seconds. This will choose the lifetime of ID tokens returned by the authorization flow and the refresh grant. It will not influence the lifetime of the ID tokens returned by RFC8693 token exchange. When null, a short-lived default value will be used. This value must be between 120 and 1,800 seconds (30 minutes), inclusive. It is recommended to make these tokens short-lived to force the client to perform the refresh grant often, because the refresh grant will check with the external identity provider to decide if it is acceptable for the end user to continue their session, and will update the end user's group memberships from the external identity provider. Giving these tokens a long life is will allow the end user to continue to use a token while avoiding these updates from the external identity provider. However, some web applications may have reasons specific to the design of that application to prefer longer lifetimes. +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-redirecturi"] ==== RedirectURI (string) diff --git a/generated/1.22/apis/supervisor/config/v1alpha1/types_oidcclient.go b/generated/1.22/apis/supervisor/config/v1alpha1/types_oidcclient.go index 61106fdba..b02307f94 100644 --- a/generated/1.22/apis/supervisor/config/v1alpha1/types_oidcclient.go +++ b/generated/1.22/apis/supervisor/config/v1alpha1/types_oidcclient.go @@ -1,4 +1,4 @@ -// Copyright 2022-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2022-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package v1alpha1 @@ -71,6 +71,28 @@ type OIDCClientSpec struct { // +listType=set // +kubebuilder:validation:MinItems=1 AllowedScopes []Scope `json:"allowedScopes"` + + // tokenLifetimes are the optional overrides of token lifetimes for an OIDCClient. + // +optional + TokenLifetimes OIDCClientTokenLifetimes `json:"tokenLifetimes,omitempty"` +} + +// OIDCClientTokenLifetimes describes the optional overrides of token lifetimes for an OIDCClient. +type OIDCClientTokenLifetimes struct { + // idTokenSeconds is the lifetime of ID tokens issued to this client, in seconds. This will choose the lifetime of + // ID tokens returned by the authorization flow and the refresh grant. It will not influence the lifetime of the ID + // tokens returned by RFC8693 token exchange. When null, a short-lived default value will be used. + // This value must be between 120 and 1,800 seconds (30 minutes), inclusive. It is recommended to make these tokens + // short-lived to force the client to perform the refresh grant often, because the refresh grant will check with the + // external identity provider to decide if it is acceptable for the end user to continue their session, and will + // update the end user's group memberships from the external identity provider. Giving these tokens a long life is + // will allow the end user to continue to use a token while avoiding these updates from the external identity + // provider. However, some web applications may have reasons specific to the design of that application to prefer + // longer lifetimes. + // +kubebuilder:validation:Minimum=120 + // +kubebuilder:validation:Maximum=1800 + // +optional + IDTokenSeconds *int32 `json:"idTokenSeconds,omitempty"` } // OIDCClientStatus is a struct that describes the actual state of an OIDCClient. diff --git a/generated/1.22/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go b/generated/1.22/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go index ad308c3de..d3c7ec69e 100644 --- a/generated/1.22/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.22/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go @@ -374,6 +374,7 @@ func (in *OIDCClientSpec) DeepCopyInto(out *OIDCClientSpec) { *out = make([]Scope, len(*in)) copy(*out, *in) } + in.TokenLifetimes.DeepCopyInto(&out.TokenLifetimes) return } @@ -409,3 +410,24 @@ func (in *OIDCClientStatus) DeepCopy() *OIDCClientStatus { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OIDCClientTokenLifetimes) DeepCopyInto(out *OIDCClientTokenLifetimes) { + *out = *in + if in.IDTokenSeconds != nil { + in, out := &in.IDTokenSeconds, &out.IDTokenSeconds + *out = new(int32) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OIDCClientTokenLifetimes. +func (in *OIDCClientTokenLifetimes) DeepCopy() *OIDCClientTokenLifetimes { + if in == nil { + return nil + } + out := new(OIDCClientTokenLifetimes) + in.DeepCopyInto(out) + return out +} diff --git a/generated/1.22/crds/config.supervisor.pinniped.dev_oidcclients.yaml b/generated/1.22/crds/config.supervisor.pinniped.dev_oidcclients.yaml index 30c154f7e..40cb1e21e 100644 --- a/generated/1.22/crds/config.supervisor.pinniped.dev_oidcclients.yaml +++ b/generated/1.22/crds/config.supervisor.pinniped.dev_oidcclients.yaml @@ -119,6 +119,27 @@ spec: minItems: 1 type: array x-kubernetes-list-type: set + tokenLifetimes: + description: tokenLifetimes are the optional overrides of token lifetimes + for an OIDCClient. + properties: + idTokenSeconds: + description: |- + idTokenSeconds is the lifetime of ID tokens issued to this client, in seconds. This will choose the lifetime of + ID tokens returned by the authorization flow and the refresh grant. It will not influence the lifetime of the ID + tokens returned by RFC8693 token exchange. When null, a short-lived default value will be used. + This value must be between 120 and 1,800 seconds (30 minutes), inclusive. It is recommended to make these tokens + short-lived to force the client to perform the refresh grant often, because the refresh grant will check with the + external identity provider to decide if it is acceptable for the end user to continue their session, and will + update the end user's group memberships from the external identity provider. Giving these tokens a long life is + will allow the end user to continue to use a token while avoiding these updates from the external identity + provider. However, some web applications may have reasons specific to the design of that application to prefer + longer lifetimes. + format: int32 + maximum: 1800 + minimum: 120 + type: integer + type: object required: - allowedGrantTypes - allowedRedirectURIs diff --git a/generated/1.23/README.adoc b/generated/1.23/README.adoc index d542fc70c..f4b6d03f2 100644 --- a/generated/1.23/README.adoc +++ b/generated/1.23/README.adoc @@ -937,6 +937,7 @@ Must only contain the following values: - authorization_code: allows the client | *`allowedScopes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-scope[$$Scope$$] array__ | allowedScopes is a list of the allowed scopes param values that should be accepted during OIDC flows with this client. + Must only contain the following values: - openid: The client is allowed to request ID tokens. ID tokens only include the required claims by default (iss, sub, aud, exp, iat). This scope must always be listed. - offline_access: The client is allowed to request an initial refresh token during the authorization code grant flow. This scope must be listed if allowedGrantTypes lists refresh_token. - pinniped:request-audience: The client is allowed to request a new audience value during a RFC8693 token exchange, which is a step in the process to be able to get a cluster credential for the user. openid, username and groups scopes must be listed when this scope is present. This scope must be listed if allowedGrantTypes lists urn:ietf:params:oauth:grant-type:token-exchange. - username: The client is allowed to request that ID tokens contain the user's username. Without the username scope being requested and allowed, the ID token will not contain the user's username. - groups: The client is allowed to request that ID tokens contain the user's group membership, if their group membership is discoverable by the Supervisor. Without the groups scope being requested and allowed, the ID token will not contain groups. +| *`tokenLifetimes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-oidcclienttokenlifetimes[$$OIDCClientTokenLifetimes$$]__ | tokenLifetimes are the optional overrides of token lifetimes for an OIDCClient. |=== @@ -959,6 +960,23 @@ OIDCClientStatus is a struct that describes the actual state of an OIDCClient. |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-oidcclienttokenlifetimes"] +==== OIDCClientTokenLifetimes + +OIDCClientTokenLifetimes describes the optional overrides of token lifetimes for an OIDCClient. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-oidcclientspec[$$OIDCClientSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`idTokenSeconds`* __integer__ | idTokenSeconds is the lifetime of ID tokens issued to this client, in seconds. This will choose the lifetime of ID tokens returned by the authorization flow and the refresh grant. It will not influence the lifetime of the ID tokens returned by RFC8693 token exchange. When null, a short-lived default value will be used. This value must be between 120 and 1,800 seconds (30 minutes), inclusive. It is recommended to make these tokens short-lived to force the client to perform the refresh grant often, because the refresh grant will check with the external identity provider to decide if it is acceptable for the end user to continue their session, and will update the end user's group memberships from the external identity provider. Giving these tokens a long life is will allow the end user to continue to use a token while avoiding these updates from the external identity provider. However, some web applications may have reasons specific to the design of that application to prefer longer lifetimes. +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-redirecturi"] ==== RedirectURI (string) diff --git a/generated/1.23/apis/supervisor/config/v1alpha1/types_oidcclient.go b/generated/1.23/apis/supervisor/config/v1alpha1/types_oidcclient.go index 61106fdba..b02307f94 100644 --- a/generated/1.23/apis/supervisor/config/v1alpha1/types_oidcclient.go +++ b/generated/1.23/apis/supervisor/config/v1alpha1/types_oidcclient.go @@ -1,4 +1,4 @@ -// Copyright 2022-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2022-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package v1alpha1 @@ -71,6 +71,28 @@ type OIDCClientSpec struct { // +listType=set // +kubebuilder:validation:MinItems=1 AllowedScopes []Scope `json:"allowedScopes"` + + // tokenLifetimes are the optional overrides of token lifetimes for an OIDCClient. + // +optional + TokenLifetimes OIDCClientTokenLifetimes `json:"tokenLifetimes,omitempty"` +} + +// OIDCClientTokenLifetimes describes the optional overrides of token lifetimes for an OIDCClient. +type OIDCClientTokenLifetimes struct { + // idTokenSeconds is the lifetime of ID tokens issued to this client, in seconds. This will choose the lifetime of + // ID tokens returned by the authorization flow and the refresh grant. It will not influence the lifetime of the ID + // tokens returned by RFC8693 token exchange. When null, a short-lived default value will be used. + // This value must be between 120 and 1,800 seconds (30 minutes), inclusive. It is recommended to make these tokens + // short-lived to force the client to perform the refresh grant often, because the refresh grant will check with the + // external identity provider to decide if it is acceptable for the end user to continue their session, and will + // update the end user's group memberships from the external identity provider. Giving these tokens a long life is + // will allow the end user to continue to use a token while avoiding these updates from the external identity + // provider. However, some web applications may have reasons specific to the design of that application to prefer + // longer lifetimes. + // +kubebuilder:validation:Minimum=120 + // +kubebuilder:validation:Maximum=1800 + // +optional + IDTokenSeconds *int32 `json:"idTokenSeconds,omitempty"` } // OIDCClientStatus is a struct that describes the actual state of an OIDCClient. diff --git a/generated/1.23/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go b/generated/1.23/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go index ad308c3de..d3c7ec69e 100644 --- a/generated/1.23/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.23/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go @@ -374,6 +374,7 @@ func (in *OIDCClientSpec) DeepCopyInto(out *OIDCClientSpec) { *out = make([]Scope, len(*in)) copy(*out, *in) } + in.TokenLifetimes.DeepCopyInto(&out.TokenLifetimes) return } @@ -409,3 +410,24 @@ func (in *OIDCClientStatus) DeepCopy() *OIDCClientStatus { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OIDCClientTokenLifetimes) DeepCopyInto(out *OIDCClientTokenLifetimes) { + *out = *in + if in.IDTokenSeconds != nil { + in, out := &in.IDTokenSeconds, &out.IDTokenSeconds + *out = new(int32) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OIDCClientTokenLifetimes. +func (in *OIDCClientTokenLifetimes) DeepCopy() *OIDCClientTokenLifetimes { + if in == nil { + return nil + } + out := new(OIDCClientTokenLifetimes) + in.DeepCopyInto(out) + return out +} diff --git a/generated/1.23/crds/config.supervisor.pinniped.dev_oidcclients.yaml b/generated/1.23/crds/config.supervisor.pinniped.dev_oidcclients.yaml index 0960ca0de..d33b31bf1 100644 --- a/generated/1.23/crds/config.supervisor.pinniped.dev_oidcclients.yaml +++ b/generated/1.23/crds/config.supervisor.pinniped.dev_oidcclients.yaml @@ -119,6 +119,27 @@ spec: minItems: 1 type: array x-kubernetes-list-type: set + tokenLifetimes: + description: tokenLifetimes are the optional overrides of token lifetimes + for an OIDCClient. + properties: + idTokenSeconds: + description: |- + idTokenSeconds is the lifetime of ID tokens issued to this client, in seconds. This will choose the lifetime of + ID tokens returned by the authorization flow and the refresh grant. It will not influence the lifetime of the ID + tokens returned by RFC8693 token exchange. When null, a short-lived default value will be used. + This value must be between 120 and 1,800 seconds (30 minutes), inclusive. It is recommended to make these tokens + short-lived to force the client to perform the refresh grant often, because the refresh grant will check with the + external identity provider to decide if it is acceptable for the end user to continue their session, and will + update the end user's group memberships from the external identity provider. Giving these tokens a long life is + will allow the end user to continue to use a token while avoiding these updates from the external identity + provider. However, some web applications may have reasons specific to the design of that application to prefer + longer lifetimes. + format: int32 + maximum: 1800 + minimum: 120 + type: integer + type: object required: - allowedGrantTypes - allowedRedirectURIs diff --git a/generated/1.24/README.adoc b/generated/1.24/README.adoc index 782f1cc2b..39889c3f0 100644 --- a/generated/1.24/README.adoc +++ b/generated/1.24/README.adoc @@ -937,6 +937,7 @@ Must only contain the following values: - authorization_code: allows the client | *`allowedScopes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-scope[$$Scope$$] array__ | allowedScopes is a list of the allowed scopes param values that should be accepted during OIDC flows with this client. + Must only contain the following values: - openid: The client is allowed to request ID tokens. ID tokens only include the required claims by default (iss, sub, aud, exp, iat). This scope must always be listed. - offline_access: The client is allowed to request an initial refresh token during the authorization code grant flow. This scope must be listed if allowedGrantTypes lists refresh_token. - pinniped:request-audience: The client is allowed to request a new audience value during a RFC8693 token exchange, which is a step in the process to be able to get a cluster credential for the user. openid, username and groups scopes must be listed when this scope is present. This scope must be listed if allowedGrantTypes lists urn:ietf:params:oauth:grant-type:token-exchange. - username: The client is allowed to request that ID tokens contain the user's username. Without the username scope being requested and allowed, the ID token will not contain the user's username. - groups: The client is allowed to request that ID tokens contain the user's group membership, if their group membership is discoverable by the Supervisor. Without the groups scope being requested and allowed, the ID token will not contain groups. +| *`tokenLifetimes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-oidcclienttokenlifetimes[$$OIDCClientTokenLifetimes$$]__ | tokenLifetimes are the optional overrides of token lifetimes for an OIDCClient. |=== @@ -959,6 +960,23 @@ OIDCClientStatus is a struct that describes the actual state of an OIDCClient. |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-oidcclienttokenlifetimes"] +==== OIDCClientTokenLifetimes + +OIDCClientTokenLifetimes describes the optional overrides of token lifetimes for an OIDCClient. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-oidcclientspec[$$OIDCClientSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`idTokenSeconds`* __integer__ | idTokenSeconds is the lifetime of ID tokens issued to this client, in seconds. This will choose the lifetime of ID tokens returned by the authorization flow and the refresh grant. It will not influence the lifetime of the ID tokens returned by RFC8693 token exchange. When null, a short-lived default value will be used. This value must be between 120 and 1,800 seconds (30 minutes), inclusive. It is recommended to make these tokens short-lived to force the client to perform the refresh grant often, because the refresh grant will check with the external identity provider to decide if it is acceptable for the end user to continue their session, and will update the end user's group memberships from the external identity provider. Giving these tokens a long life is will allow the end user to continue to use a token while avoiding these updates from the external identity provider. However, some web applications may have reasons specific to the design of that application to prefer longer lifetimes. +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-redirecturi"] ==== RedirectURI (string) diff --git a/generated/1.24/apis/supervisor/config/v1alpha1/types_oidcclient.go b/generated/1.24/apis/supervisor/config/v1alpha1/types_oidcclient.go index 61106fdba..b02307f94 100644 --- a/generated/1.24/apis/supervisor/config/v1alpha1/types_oidcclient.go +++ b/generated/1.24/apis/supervisor/config/v1alpha1/types_oidcclient.go @@ -1,4 +1,4 @@ -// Copyright 2022-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2022-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package v1alpha1 @@ -71,6 +71,28 @@ type OIDCClientSpec struct { // +listType=set // +kubebuilder:validation:MinItems=1 AllowedScopes []Scope `json:"allowedScopes"` + + // tokenLifetimes are the optional overrides of token lifetimes for an OIDCClient. + // +optional + TokenLifetimes OIDCClientTokenLifetimes `json:"tokenLifetimes,omitempty"` +} + +// OIDCClientTokenLifetimes describes the optional overrides of token lifetimes for an OIDCClient. +type OIDCClientTokenLifetimes struct { + // idTokenSeconds is the lifetime of ID tokens issued to this client, in seconds. This will choose the lifetime of + // ID tokens returned by the authorization flow and the refresh grant. It will not influence the lifetime of the ID + // tokens returned by RFC8693 token exchange. When null, a short-lived default value will be used. + // This value must be between 120 and 1,800 seconds (30 minutes), inclusive. It is recommended to make these tokens + // short-lived to force the client to perform the refresh grant often, because the refresh grant will check with the + // external identity provider to decide if it is acceptable for the end user to continue their session, and will + // update the end user's group memberships from the external identity provider. Giving these tokens a long life is + // will allow the end user to continue to use a token while avoiding these updates from the external identity + // provider. However, some web applications may have reasons specific to the design of that application to prefer + // longer lifetimes. + // +kubebuilder:validation:Minimum=120 + // +kubebuilder:validation:Maximum=1800 + // +optional + IDTokenSeconds *int32 `json:"idTokenSeconds,omitempty"` } // OIDCClientStatus is a struct that describes the actual state of an OIDCClient. diff --git a/generated/1.24/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go b/generated/1.24/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go index ad308c3de..d3c7ec69e 100644 --- a/generated/1.24/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.24/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go @@ -374,6 +374,7 @@ func (in *OIDCClientSpec) DeepCopyInto(out *OIDCClientSpec) { *out = make([]Scope, len(*in)) copy(*out, *in) } + in.TokenLifetimes.DeepCopyInto(&out.TokenLifetimes) return } @@ -409,3 +410,24 @@ func (in *OIDCClientStatus) DeepCopy() *OIDCClientStatus { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OIDCClientTokenLifetimes) DeepCopyInto(out *OIDCClientTokenLifetimes) { + *out = *in + if in.IDTokenSeconds != nil { + in, out := &in.IDTokenSeconds, &out.IDTokenSeconds + *out = new(int32) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OIDCClientTokenLifetimes. +func (in *OIDCClientTokenLifetimes) DeepCopy() *OIDCClientTokenLifetimes { + if in == nil { + return nil + } + out := new(OIDCClientTokenLifetimes) + in.DeepCopyInto(out) + return out +} diff --git a/generated/1.24/crds/config.supervisor.pinniped.dev_oidcclients.yaml b/generated/1.24/crds/config.supervisor.pinniped.dev_oidcclients.yaml index 0960ca0de..d33b31bf1 100644 --- a/generated/1.24/crds/config.supervisor.pinniped.dev_oidcclients.yaml +++ b/generated/1.24/crds/config.supervisor.pinniped.dev_oidcclients.yaml @@ -119,6 +119,27 @@ spec: minItems: 1 type: array x-kubernetes-list-type: set + tokenLifetimes: + description: tokenLifetimes are the optional overrides of token lifetimes + for an OIDCClient. + properties: + idTokenSeconds: + description: |- + idTokenSeconds is the lifetime of ID tokens issued to this client, in seconds. This will choose the lifetime of + ID tokens returned by the authorization flow and the refresh grant. It will not influence the lifetime of the ID + tokens returned by RFC8693 token exchange. When null, a short-lived default value will be used. + This value must be between 120 and 1,800 seconds (30 minutes), inclusive. It is recommended to make these tokens + short-lived to force the client to perform the refresh grant often, because the refresh grant will check with the + external identity provider to decide if it is acceptable for the end user to continue their session, and will + update the end user's group memberships from the external identity provider. Giving these tokens a long life is + will allow the end user to continue to use a token while avoiding these updates from the external identity + provider. However, some web applications may have reasons specific to the design of that application to prefer + longer lifetimes. + format: int32 + maximum: 1800 + minimum: 120 + type: integer + type: object required: - allowedGrantTypes - allowedRedirectURIs diff --git a/generated/1.25/README.adoc b/generated/1.25/README.adoc index cb137d202..08e3839ea 100644 --- a/generated/1.25/README.adoc +++ b/generated/1.25/README.adoc @@ -937,6 +937,7 @@ Must only contain the following values: - authorization_code: allows the client | *`allowedScopes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-scope[$$Scope$$] array__ | allowedScopes is a list of the allowed scopes param values that should be accepted during OIDC flows with this client. + Must only contain the following values: - openid: The client is allowed to request ID tokens. ID tokens only include the required claims by default (iss, sub, aud, exp, iat). This scope must always be listed. - offline_access: The client is allowed to request an initial refresh token during the authorization code grant flow. This scope must be listed if allowedGrantTypes lists refresh_token. - pinniped:request-audience: The client is allowed to request a new audience value during a RFC8693 token exchange, which is a step in the process to be able to get a cluster credential for the user. openid, username and groups scopes must be listed when this scope is present. This scope must be listed if allowedGrantTypes lists urn:ietf:params:oauth:grant-type:token-exchange. - username: The client is allowed to request that ID tokens contain the user's username. Without the username scope being requested and allowed, the ID token will not contain the user's username. - groups: The client is allowed to request that ID tokens contain the user's group membership, if their group membership is discoverable by the Supervisor. Without the groups scope being requested and allowed, the ID token will not contain groups. +| *`tokenLifetimes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-oidcclienttokenlifetimes[$$OIDCClientTokenLifetimes$$]__ | tokenLifetimes are the optional overrides of token lifetimes for an OIDCClient. |=== @@ -959,6 +960,23 @@ OIDCClientStatus is a struct that describes the actual state of an OIDCClient. |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-oidcclienttokenlifetimes"] +==== OIDCClientTokenLifetimes + +OIDCClientTokenLifetimes describes the optional overrides of token lifetimes for an OIDCClient. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-oidcclientspec[$$OIDCClientSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`idTokenSeconds`* __integer__ | idTokenSeconds is the lifetime of ID tokens issued to this client, in seconds. This will choose the lifetime of ID tokens returned by the authorization flow and the refresh grant. It will not influence the lifetime of the ID tokens returned by RFC8693 token exchange. When null, a short-lived default value will be used. This value must be between 120 and 1,800 seconds (30 minutes), inclusive. It is recommended to make these tokens short-lived to force the client to perform the refresh grant often, because the refresh grant will check with the external identity provider to decide if it is acceptable for the end user to continue their session, and will update the end user's group memberships from the external identity provider. Giving these tokens a long life is will allow the end user to continue to use a token while avoiding these updates from the external identity provider. However, some web applications may have reasons specific to the design of that application to prefer longer lifetimes. +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-config-v1alpha1-redirecturi"] ==== RedirectURI (string) diff --git a/generated/1.25/apis/supervisor/config/v1alpha1/types_oidcclient.go b/generated/1.25/apis/supervisor/config/v1alpha1/types_oidcclient.go index 61106fdba..b02307f94 100644 --- a/generated/1.25/apis/supervisor/config/v1alpha1/types_oidcclient.go +++ b/generated/1.25/apis/supervisor/config/v1alpha1/types_oidcclient.go @@ -1,4 +1,4 @@ -// Copyright 2022-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2022-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package v1alpha1 @@ -71,6 +71,28 @@ type OIDCClientSpec struct { // +listType=set // +kubebuilder:validation:MinItems=1 AllowedScopes []Scope `json:"allowedScopes"` + + // tokenLifetimes are the optional overrides of token lifetimes for an OIDCClient. + // +optional + TokenLifetimes OIDCClientTokenLifetimes `json:"tokenLifetimes,omitempty"` +} + +// OIDCClientTokenLifetimes describes the optional overrides of token lifetimes for an OIDCClient. +type OIDCClientTokenLifetimes struct { + // idTokenSeconds is the lifetime of ID tokens issued to this client, in seconds. This will choose the lifetime of + // ID tokens returned by the authorization flow and the refresh grant. It will not influence the lifetime of the ID + // tokens returned by RFC8693 token exchange. When null, a short-lived default value will be used. + // This value must be between 120 and 1,800 seconds (30 minutes), inclusive. It is recommended to make these tokens + // short-lived to force the client to perform the refresh grant often, because the refresh grant will check with the + // external identity provider to decide if it is acceptable for the end user to continue their session, and will + // update the end user's group memberships from the external identity provider. Giving these tokens a long life is + // will allow the end user to continue to use a token while avoiding these updates from the external identity + // provider. However, some web applications may have reasons specific to the design of that application to prefer + // longer lifetimes. + // +kubebuilder:validation:Minimum=120 + // +kubebuilder:validation:Maximum=1800 + // +optional + IDTokenSeconds *int32 `json:"idTokenSeconds,omitempty"` } // OIDCClientStatus is a struct that describes the actual state of an OIDCClient. diff --git a/generated/1.25/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go b/generated/1.25/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go index ad308c3de..d3c7ec69e 100644 --- a/generated/1.25/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.25/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go @@ -374,6 +374,7 @@ func (in *OIDCClientSpec) DeepCopyInto(out *OIDCClientSpec) { *out = make([]Scope, len(*in)) copy(*out, *in) } + in.TokenLifetimes.DeepCopyInto(&out.TokenLifetimes) return } @@ -409,3 +410,24 @@ func (in *OIDCClientStatus) DeepCopy() *OIDCClientStatus { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OIDCClientTokenLifetimes) DeepCopyInto(out *OIDCClientTokenLifetimes) { + *out = *in + if in.IDTokenSeconds != nil { + in, out := &in.IDTokenSeconds, &out.IDTokenSeconds + *out = new(int32) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OIDCClientTokenLifetimes. +func (in *OIDCClientTokenLifetimes) DeepCopy() *OIDCClientTokenLifetimes { + if in == nil { + return nil + } + out := new(OIDCClientTokenLifetimes) + in.DeepCopyInto(out) + return out +} diff --git a/generated/1.25/crds/config.supervisor.pinniped.dev_oidcclients.yaml b/generated/1.25/crds/config.supervisor.pinniped.dev_oidcclients.yaml index 0960ca0de..d33b31bf1 100644 --- a/generated/1.25/crds/config.supervisor.pinniped.dev_oidcclients.yaml +++ b/generated/1.25/crds/config.supervisor.pinniped.dev_oidcclients.yaml @@ -119,6 +119,27 @@ spec: minItems: 1 type: array x-kubernetes-list-type: set + tokenLifetimes: + description: tokenLifetimes are the optional overrides of token lifetimes + for an OIDCClient. + properties: + idTokenSeconds: + description: |- + idTokenSeconds is the lifetime of ID tokens issued to this client, in seconds. This will choose the lifetime of + ID tokens returned by the authorization flow and the refresh grant. It will not influence the lifetime of the ID + tokens returned by RFC8693 token exchange. When null, a short-lived default value will be used. + This value must be between 120 and 1,800 seconds (30 minutes), inclusive. It is recommended to make these tokens + short-lived to force the client to perform the refresh grant often, because the refresh grant will check with the + external identity provider to decide if it is acceptable for the end user to continue their session, and will + update the end user's group memberships from the external identity provider. Giving these tokens a long life is + will allow the end user to continue to use a token while avoiding these updates from the external identity + provider. However, some web applications may have reasons specific to the design of that application to prefer + longer lifetimes. + format: int32 + maximum: 1800 + minimum: 120 + type: integer + type: object required: - allowedGrantTypes - allowedRedirectURIs diff --git a/generated/1.26/README.adoc b/generated/1.26/README.adoc index 17170f767..d776379c2 100644 --- a/generated/1.26/README.adoc +++ b/generated/1.26/README.adoc @@ -937,6 +937,7 @@ Must only contain the following values: - authorization_code: allows the client | *`allowedScopes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-scope[$$Scope$$] array__ | allowedScopes is a list of the allowed scopes param values that should be accepted during OIDC flows with this client. + Must only contain the following values: - openid: The client is allowed to request ID tokens. ID tokens only include the required claims by default (iss, sub, aud, exp, iat). This scope must always be listed. - offline_access: The client is allowed to request an initial refresh token during the authorization code grant flow. This scope must be listed if allowedGrantTypes lists refresh_token. - pinniped:request-audience: The client is allowed to request a new audience value during a RFC8693 token exchange, which is a step in the process to be able to get a cluster credential for the user. openid, username and groups scopes must be listed when this scope is present. This scope must be listed if allowedGrantTypes lists urn:ietf:params:oauth:grant-type:token-exchange. - username: The client is allowed to request that ID tokens contain the user's username. Without the username scope being requested and allowed, the ID token will not contain the user's username. - groups: The client is allowed to request that ID tokens contain the user's group membership, if their group membership is discoverable by the Supervisor. Without the groups scope being requested and allowed, the ID token will not contain groups. +| *`tokenLifetimes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-oidcclienttokenlifetimes[$$OIDCClientTokenLifetimes$$]__ | tokenLifetimes are the optional overrides of token lifetimes for an OIDCClient. |=== @@ -959,6 +960,23 @@ OIDCClientStatus is a struct that describes the actual state of an OIDCClient. |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-oidcclienttokenlifetimes"] +==== OIDCClientTokenLifetimes + +OIDCClientTokenLifetimes describes the optional overrides of token lifetimes for an OIDCClient. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-oidcclientspec[$$OIDCClientSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`idTokenSeconds`* __integer__ | idTokenSeconds is the lifetime of ID tokens issued to this client, in seconds. This will choose the lifetime of ID tokens returned by the authorization flow and the refresh grant. It will not influence the lifetime of the ID tokens returned by RFC8693 token exchange. When null, a short-lived default value will be used. This value must be between 120 and 1,800 seconds (30 minutes), inclusive. It is recommended to make these tokens short-lived to force the client to perform the refresh grant often, because the refresh grant will check with the external identity provider to decide if it is acceptable for the end user to continue their session, and will update the end user's group memberships from the external identity provider. Giving these tokens a long life is will allow the end user to continue to use a token while avoiding these updates from the external identity provider. However, some web applications may have reasons specific to the design of that application to prefer longer lifetimes. +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-config-v1alpha1-redirecturi"] ==== RedirectURI (string) diff --git a/generated/1.26/apis/supervisor/config/v1alpha1/types_oidcclient.go b/generated/1.26/apis/supervisor/config/v1alpha1/types_oidcclient.go index 61106fdba..b02307f94 100644 --- a/generated/1.26/apis/supervisor/config/v1alpha1/types_oidcclient.go +++ b/generated/1.26/apis/supervisor/config/v1alpha1/types_oidcclient.go @@ -1,4 +1,4 @@ -// Copyright 2022-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2022-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package v1alpha1 @@ -71,6 +71,28 @@ type OIDCClientSpec struct { // +listType=set // +kubebuilder:validation:MinItems=1 AllowedScopes []Scope `json:"allowedScopes"` + + // tokenLifetimes are the optional overrides of token lifetimes for an OIDCClient. + // +optional + TokenLifetimes OIDCClientTokenLifetimes `json:"tokenLifetimes,omitempty"` +} + +// OIDCClientTokenLifetimes describes the optional overrides of token lifetimes for an OIDCClient. +type OIDCClientTokenLifetimes struct { + // idTokenSeconds is the lifetime of ID tokens issued to this client, in seconds. This will choose the lifetime of + // ID tokens returned by the authorization flow and the refresh grant. It will not influence the lifetime of the ID + // tokens returned by RFC8693 token exchange. When null, a short-lived default value will be used. + // This value must be between 120 and 1,800 seconds (30 minutes), inclusive. It is recommended to make these tokens + // short-lived to force the client to perform the refresh grant often, because the refresh grant will check with the + // external identity provider to decide if it is acceptable for the end user to continue their session, and will + // update the end user's group memberships from the external identity provider. Giving these tokens a long life is + // will allow the end user to continue to use a token while avoiding these updates from the external identity + // provider. However, some web applications may have reasons specific to the design of that application to prefer + // longer lifetimes. + // +kubebuilder:validation:Minimum=120 + // +kubebuilder:validation:Maximum=1800 + // +optional + IDTokenSeconds *int32 `json:"idTokenSeconds,omitempty"` } // OIDCClientStatus is a struct that describes the actual state of an OIDCClient. diff --git a/generated/1.26/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go b/generated/1.26/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go index ad308c3de..d3c7ec69e 100644 --- a/generated/1.26/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.26/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go @@ -374,6 +374,7 @@ func (in *OIDCClientSpec) DeepCopyInto(out *OIDCClientSpec) { *out = make([]Scope, len(*in)) copy(*out, *in) } + in.TokenLifetimes.DeepCopyInto(&out.TokenLifetimes) return } @@ -409,3 +410,24 @@ func (in *OIDCClientStatus) DeepCopy() *OIDCClientStatus { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OIDCClientTokenLifetimes) DeepCopyInto(out *OIDCClientTokenLifetimes) { + *out = *in + if in.IDTokenSeconds != nil { + in, out := &in.IDTokenSeconds, &out.IDTokenSeconds + *out = new(int32) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OIDCClientTokenLifetimes. +func (in *OIDCClientTokenLifetimes) DeepCopy() *OIDCClientTokenLifetimes { + if in == nil { + return nil + } + out := new(OIDCClientTokenLifetimes) + in.DeepCopyInto(out) + return out +} diff --git a/generated/1.26/crds/config.supervisor.pinniped.dev_oidcclients.yaml b/generated/1.26/crds/config.supervisor.pinniped.dev_oidcclients.yaml index 0960ca0de..d33b31bf1 100644 --- a/generated/1.26/crds/config.supervisor.pinniped.dev_oidcclients.yaml +++ b/generated/1.26/crds/config.supervisor.pinniped.dev_oidcclients.yaml @@ -119,6 +119,27 @@ spec: minItems: 1 type: array x-kubernetes-list-type: set + tokenLifetimes: + description: tokenLifetimes are the optional overrides of token lifetimes + for an OIDCClient. + properties: + idTokenSeconds: + description: |- + idTokenSeconds is the lifetime of ID tokens issued to this client, in seconds. This will choose the lifetime of + ID tokens returned by the authorization flow and the refresh grant. It will not influence the lifetime of the ID + tokens returned by RFC8693 token exchange. When null, a short-lived default value will be used. + This value must be between 120 and 1,800 seconds (30 minutes), inclusive. It is recommended to make these tokens + short-lived to force the client to perform the refresh grant often, because the refresh grant will check with the + external identity provider to decide if it is acceptable for the end user to continue their session, and will + update the end user's group memberships from the external identity provider. Giving these tokens a long life is + will allow the end user to continue to use a token while avoiding these updates from the external identity + provider. However, some web applications may have reasons specific to the design of that application to prefer + longer lifetimes. + format: int32 + maximum: 1800 + minimum: 120 + type: integer + type: object required: - allowedGrantTypes - allowedRedirectURIs diff --git a/generated/1.27/README.adoc b/generated/1.27/README.adoc index 026512700..c0276b3b1 100644 --- a/generated/1.27/README.adoc +++ b/generated/1.27/README.adoc @@ -937,6 +937,7 @@ Must only contain the following values: - authorization_code: allows the client | *`allowedScopes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-scope[$$Scope$$] array__ | allowedScopes is a list of the allowed scopes param values that should be accepted during OIDC flows with this client. + Must only contain the following values: - openid: The client is allowed to request ID tokens. ID tokens only include the required claims by default (iss, sub, aud, exp, iat). This scope must always be listed. - offline_access: The client is allowed to request an initial refresh token during the authorization code grant flow. This scope must be listed if allowedGrantTypes lists refresh_token. - pinniped:request-audience: The client is allowed to request a new audience value during a RFC8693 token exchange, which is a step in the process to be able to get a cluster credential for the user. openid, username and groups scopes must be listed when this scope is present. This scope must be listed if allowedGrantTypes lists urn:ietf:params:oauth:grant-type:token-exchange. - username: The client is allowed to request that ID tokens contain the user's username. Without the username scope being requested and allowed, the ID token will not contain the user's username. - groups: The client is allowed to request that ID tokens contain the user's group membership, if their group membership is discoverable by the Supervisor. Without the groups scope being requested and allowed, the ID token will not contain groups. +| *`tokenLifetimes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-oidcclienttokenlifetimes[$$OIDCClientTokenLifetimes$$]__ | tokenLifetimes are the optional overrides of token lifetimes for an OIDCClient. |=== @@ -959,6 +960,23 @@ OIDCClientStatus is a struct that describes the actual state of an OIDCClient. |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-oidcclienttokenlifetimes"] +==== OIDCClientTokenLifetimes + +OIDCClientTokenLifetimes describes the optional overrides of token lifetimes for an OIDCClient. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-oidcclientspec[$$OIDCClientSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`idTokenSeconds`* __integer__ | idTokenSeconds is the lifetime of ID tokens issued to this client, in seconds. This will choose the lifetime of ID tokens returned by the authorization flow and the refresh grant. It will not influence the lifetime of the ID tokens returned by RFC8693 token exchange. When null, a short-lived default value will be used. This value must be between 120 and 1,800 seconds (30 minutes), inclusive. It is recommended to make these tokens short-lived to force the client to perform the refresh grant often, because the refresh grant will check with the external identity provider to decide if it is acceptable for the end user to continue their session, and will update the end user's group memberships from the external identity provider. Giving these tokens a long life is will allow the end user to continue to use a token while avoiding these updates from the external identity provider. However, some web applications may have reasons specific to the design of that application to prefer longer lifetimes. +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-config-v1alpha1-redirecturi"] ==== RedirectURI (string) diff --git a/generated/1.27/apis/supervisor/config/v1alpha1/types_oidcclient.go b/generated/1.27/apis/supervisor/config/v1alpha1/types_oidcclient.go index 61106fdba..b02307f94 100644 --- a/generated/1.27/apis/supervisor/config/v1alpha1/types_oidcclient.go +++ b/generated/1.27/apis/supervisor/config/v1alpha1/types_oidcclient.go @@ -1,4 +1,4 @@ -// Copyright 2022-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2022-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package v1alpha1 @@ -71,6 +71,28 @@ type OIDCClientSpec struct { // +listType=set // +kubebuilder:validation:MinItems=1 AllowedScopes []Scope `json:"allowedScopes"` + + // tokenLifetimes are the optional overrides of token lifetimes for an OIDCClient. + // +optional + TokenLifetimes OIDCClientTokenLifetimes `json:"tokenLifetimes,omitempty"` +} + +// OIDCClientTokenLifetimes describes the optional overrides of token lifetimes for an OIDCClient. +type OIDCClientTokenLifetimes struct { + // idTokenSeconds is the lifetime of ID tokens issued to this client, in seconds. This will choose the lifetime of + // ID tokens returned by the authorization flow and the refresh grant. It will not influence the lifetime of the ID + // tokens returned by RFC8693 token exchange. When null, a short-lived default value will be used. + // This value must be between 120 and 1,800 seconds (30 minutes), inclusive. It is recommended to make these tokens + // short-lived to force the client to perform the refresh grant often, because the refresh grant will check with the + // external identity provider to decide if it is acceptable for the end user to continue their session, and will + // update the end user's group memberships from the external identity provider. Giving these tokens a long life is + // will allow the end user to continue to use a token while avoiding these updates from the external identity + // provider. However, some web applications may have reasons specific to the design of that application to prefer + // longer lifetimes. + // +kubebuilder:validation:Minimum=120 + // +kubebuilder:validation:Maximum=1800 + // +optional + IDTokenSeconds *int32 `json:"idTokenSeconds,omitempty"` } // OIDCClientStatus is a struct that describes the actual state of an OIDCClient. diff --git a/generated/1.27/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go b/generated/1.27/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go index ad308c3de..d3c7ec69e 100644 --- a/generated/1.27/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.27/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go @@ -374,6 +374,7 @@ func (in *OIDCClientSpec) DeepCopyInto(out *OIDCClientSpec) { *out = make([]Scope, len(*in)) copy(*out, *in) } + in.TokenLifetimes.DeepCopyInto(&out.TokenLifetimes) return } @@ -409,3 +410,24 @@ func (in *OIDCClientStatus) DeepCopy() *OIDCClientStatus { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OIDCClientTokenLifetimes) DeepCopyInto(out *OIDCClientTokenLifetimes) { + *out = *in + if in.IDTokenSeconds != nil { + in, out := &in.IDTokenSeconds, &out.IDTokenSeconds + *out = new(int32) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OIDCClientTokenLifetimes. +func (in *OIDCClientTokenLifetimes) DeepCopy() *OIDCClientTokenLifetimes { + if in == nil { + return nil + } + out := new(OIDCClientTokenLifetimes) + in.DeepCopyInto(out) + return out +} diff --git a/generated/1.27/crds/config.supervisor.pinniped.dev_oidcclients.yaml b/generated/1.27/crds/config.supervisor.pinniped.dev_oidcclients.yaml index 0960ca0de..d33b31bf1 100644 --- a/generated/1.27/crds/config.supervisor.pinniped.dev_oidcclients.yaml +++ b/generated/1.27/crds/config.supervisor.pinniped.dev_oidcclients.yaml @@ -119,6 +119,27 @@ spec: minItems: 1 type: array x-kubernetes-list-type: set + tokenLifetimes: + description: tokenLifetimes are the optional overrides of token lifetimes + for an OIDCClient. + properties: + idTokenSeconds: + description: |- + idTokenSeconds is the lifetime of ID tokens issued to this client, in seconds. This will choose the lifetime of + ID tokens returned by the authorization flow and the refresh grant. It will not influence the lifetime of the ID + tokens returned by RFC8693 token exchange. When null, a short-lived default value will be used. + This value must be between 120 and 1,800 seconds (30 minutes), inclusive. It is recommended to make these tokens + short-lived to force the client to perform the refresh grant often, because the refresh grant will check with the + external identity provider to decide if it is acceptable for the end user to continue their session, and will + update the end user's group memberships from the external identity provider. Giving these tokens a long life is + will allow the end user to continue to use a token while avoiding these updates from the external identity + provider. However, some web applications may have reasons specific to the design of that application to prefer + longer lifetimes. + format: int32 + maximum: 1800 + minimum: 120 + type: integer + type: object required: - allowedGrantTypes - allowedRedirectURIs diff --git a/generated/1.28/README.adoc b/generated/1.28/README.adoc index 9f6f45886..36162c948 100644 --- a/generated/1.28/README.adoc +++ b/generated/1.28/README.adoc @@ -937,6 +937,7 @@ Must only contain the following values: - authorization_code: allows the client | *`allowedScopes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-config-v1alpha1-scope[$$Scope$$] array__ | allowedScopes is a list of the allowed scopes param values that should be accepted during OIDC flows with this client. + Must only contain the following values: - openid: The client is allowed to request ID tokens. ID tokens only include the required claims by default (iss, sub, aud, exp, iat). This scope must always be listed. - offline_access: The client is allowed to request an initial refresh token during the authorization code grant flow. This scope must be listed if allowedGrantTypes lists refresh_token. - pinniped:request-audience: The client is allowed to request a new audience value during a RFC8693 token exchange, which is a step in the process to be able to get a cluster credential for the user. openid, username and groups scopes must be listed when this scope is present. This scope must be listed if allowedGrantTypes lists urn:ietf:params:oauth:grant-type:token-exchange. - username: The client is allowed to request that ID tokens contain the user's username. Without the username scope being requested and allowed, the ID token will not contain the user's username. - groups: The client is allowed to request that ID tokens contain the user's group membership, if their group membership is discoverable by the Supervisor. Without the groups scope being requested and allowed, the ID token will not contain groups. +| *`tokenLifetimes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-config-v1alpha1-oidcclienttokenlifetimes[$$OIDCClientTokenLifetimes$$]__ | tokenLifetimes are the optional overrides of token lifetimes for an OIDCClient. |=== @@ -959,6 +960,23 @@ OIDCClientStatus is a struct that describes the actual state of an OIDCClient. |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-config-v1alpha1-oidcclienttokenlifetimes"] +==== OIDCClientTokenLifetimes + +OIDCClientTokenLifetimes describes the optional overrides of token lifetimes for an OIDCClient. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-config-v1alpha1-oidcclientspec[$$OIDCClientSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`idTokenSeconds`* __integer__ | idTokenSeconds is the lifetime of ID tokens issued to this client, in seconds. This will choose the lifetime of ID tokens returned by the authorization flow and the refresh grant. It will not influence the lifetime of the ID tokens returned by RFC8693 token exchange. When null, a short-lived default value will be used. This value must be between 120 and 1,800 seconds (30 minutes), inclusive. It is recommended to make these tokens short-lived to force the client to perform the refresh grant often, because the refresh grant will check with the external identity provider to decide if it is acceptable for the end user to continue their session, and will update the end user's group memberships from the external identity provider. Giving these tokens a long life is will allow the end user to continue to use a token while avoiding these updates from the external identity provider. However, some web applications may have reasons specific to the design of that application to prefer longer lifetimes. +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-config-v1alpha1-redirecturi"] ==== RedirectURI (string) diff --git a/generated/1.28/apis/supervisor/config/v1alpha1/types_oidcclient.go b/generated/1.28/apis/supervisor/config/v1alpha1/types_oidcclient.go index 61106fdba..b02307f94 100644 --- a/generated/1.28/apis/supervisor/config/v1alpha1/types_oidcclient.go +++ b/generated/1.28/apis/supervisor/config/v1alpha1/types_oidcclient.go @@ -1,4 +1,4 @@ -// Copyright 2022-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2022-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package v1alpha1 @@ -71,6 +71,28 @@ type OIDCClientSpec struct { // +listType=set // +kubebuilder:validation:MinItems=1 AllowedScopes []Scope `json:"allowedScopes"` + + // tokenLifetimes are the optional overrides of token lifetimes for an OIDCClient. + // +optional + TokenLifetimes OIDCClientTokenLifetimes `json:"tokenLifetimes,omitempty"` +} + +// OIDCClientTokenLifetimes describes the optional overrides of token lifetimes for an OIDCClient. +type OIDCClientTokenLifetimes struct { + // idTokenSeconds is the lifetime of ID tokens issued to this client, in seconds. This will choose the lifetime of + // ID tokens returned by the authorization flow and the refresh grant. It will not influence the lifetime of the ID + // tokens returned by RFC8693 token exchange. When null, a short-lived default value will be used. + // This value must be between 120 and 1,800 seconds (30 minutes), inclusive. It is recommended to make these tokens + // short-lived to force the client to perform the refresh grant often, because the refresh grant will check with the + // external identity provider to decide if it is acceptable for the end user to continue their session, and will + // update the end user's group memberships from the external identity provider. Giving these tokens a long life is + // will allow the end user to continue to use a token while avoiding these updates from the external identity + // provider. However, some web applications may have reasons specific to the design of that application to prefer + // longer lifetimes. + // +kubebuilder:validation:Minimum=120 + // +kubebuilder:validation:Maximum=1800 + // +optional + IDTokenSeconds *int32 `json:"idTokenSeconds,omitempty"` } // OIDCClientStatus is a struct that describes the actual state of an OIDCClient. diff --git a/generated/1.28/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go b/generated/1.28/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go index ad308c3de..d3c7ec69e 100644 --- a/generated/1.28/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.28/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go @@ -374,6 +374,7 @@ func (in *OIDCClientSpec) DeepCopyInto(out *OIDCClientSpec) { *out = make([]Scope, len(*in)) copy(*out, *in) } + in.TokenLifetimes.DeepCopyInto(&out.TokenLifetimes) return } @@ -409,3 +410,24 @@ func (in *OIDCClientStatus) DeepCopy() *OIDCClientStatus { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OIDCClientTokenLifetimes) DeepCopyInto(out *OIDCClientTokenLifetimes) { + *out = *in + if in.IDTokenSeconds != nil { + in, out := &in.IDTokenSeconds, &out.IDTokenSeconds + *out = new(int32) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OIDCClientTokenLifetimes. +func (in *OIDCClientTokenLifetimes) DeepCopy() *OIDCClientTokenLifetimes { + if in == nil { + return nil + } + out := new(OIDCClientTokenLifetimes) + in.DeepCopyInto(out) + return out +} diff --git a/generated/1.28/crds/config.supervisor.pinniped.dev_oidcclients.yaml b/generated/1.28/crds/config.supervisor.pinniped.dev_oidcclients.yaml index 0960ca0de..d33b31bf1 100644 --- a/generated/1.28/crds/config.supervisor.pinniped.dev_oidcclients.yaml +++ b/generated/1.28/crds/config.supervisor.pinniped.dev_oidcclients.yaml @@ -119,6 +119,27 @@ spec: minItems: 1 type: array x-kubernetes-list-type: set + tokenLifetimes: + description: tokenLifetimes are the optional overrides of token lifetimes + for an OIDCClient. + properties: + idTokenSeconds: + description: |- + idTokenSeconds is the lifetime of ID tokens issued to this client, in seconds. This will choose the lifetime of + ID tokens returned by the authorization flow and the refresh grant. It will not influence the lifetime of the ID + tokens returned by RFC8693 token exchange. When null, a short-lived default value will be used. + This value must be between 120 and 1,800 seconds (30 minutes), inclusive. It is recommended to make these tokens + short-lived to force the client to perform the refresh grant often, because the refresh grant will check with the + external identity provider to decide if it is acceptable for the end user to continue their session, and will + update the end user's group memberships from the external identity provider. Giving these tokens a long life is + will allow the end user to continue to use a token while avoiding these updates from the external identity + provider. However, some web applications may have reasons specific to the design of that application to prefer + longer lifetimes. + format: int32 + maximum: 1800 + minimum: 120 + type: integer + type: object required: - allowedGrantTypes - allowedRedirectURIs diff --git a/generated/1.29/README.adoc b/generated/1.29/README.adoc index dbecc10cf..ea1a39312 100644 --- a/generated/1.29/README.adoc +++ b/generated/1.29/README.adoc @@ -937,6 +937,7 @@ Must only contain the following values: - authorization_code: allows the client | *`allowedScopes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-supervisor-config-v1alpha1-scope[$$Scope$$] array__ | allowedScopes is a list of the allowed scopes param values that should be accepted during OIDC flows with this client. + Must only contain the following values: - openid: The client is allowed to request ID tokens. ID tokens only include the required claims by default (iss, sub, aud, exp, iat). This scope must always be listed. - offline_access: The client is allowed to request an initial refresh token during the authorization code grant flow. This scope must be listed if allowedGrantTypes lists refresh_token. - pinniped:request-audience: The client is allowed to request a new audience value during a RFC8693 token exchange, which is a step in the process to be able to get a cluster credential for the user. openid, username and groups scopes must be listed when this scope is present. This scope must be listed if allowedGrantTypes lists urn:ietf:params:oauth:grant-type:token-exchange. - username: The client is allowed to request that ID tokens contain the user's username. Without the username scope being requested and allowed, the ID token will not contain the user's username. - groups: The client is allowed to request that ID tokens contain the user's group membership, if their group membership is discoverable by the Supervisor. Without the groups scope being requested and allowed, the ID token will not contain groups. +| *`tokenLifetimes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-supervisor-config-v1alpha1-oidcclienttokenlifetimes[$$OIDCClientTokenLifetimes$$]__ | tokenLifetimes are the optional overrides of token lifetimes for an OIDCClient. |=== @@ -959,6 +960,23 @@ OIDCClientStatus is a struct that describes the actual state of an OIDCClient. |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-supervisor-config-v1alpha1-oidcclienttokenlifetimes"] +==== OIDCClientTokenLifetimes + +OIDCClientTokenLifetimes describes the optional overrides of token lifetimes for an OIDCClient. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-supervisor-config-v1alpha1-oidcclientspec[$$OIDCClientSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`idTokenSeconds`* __integer__ | idTokenSeconds is the lifetime of ID tokens issued to this client, in seconds. This will choose the lifetime of ID tokens returned by the authorization flow and the refresh grant. It will not influence the lifetime of the ID tokens returned by RFC8693 token exchange. When null, a short-lived default value will be used. This value must be between 120 and 1,800 seconds (30 minutes), inclusive. It is recommended to make these tokens short-lived to force the client to perform the refresh grant often, because the refresh grant will check with the external identity provider to decide if it is acceptable for the end user to continue their session, and will update the end user's group memberships from the external identity provider. Giving these tokens a long life is will allow the end user to continue to use a token while avoiding these updates from the external identity provider. However, some web applications may have reasons specific to the design of that application to prefer longer lifetimes. +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-supervisor-config-v1alpha1-redirecturi"] ==== RedirectURI (string) diff --git a/generated/1.29/apis/supervisor/config/v1alpha1/types_oidcclient.go b/generated/1.29/apis/supervisor/config/v1alpha1/types_oidcclient.go index 61106fdba..b02307f94 100644 --- a/generated/1.29/apis/supervisor/config/v1alpha1/types_oidcclient.go +++ b/generated/1.29/apis/supervisor/config/v1alpha1/types_oidcclient.go @@ -1,4 +1,4 @@ -// Copyright 2022-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2022-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package v1alpha1 @@ -71,6 +71,28 @@ type OIDCClientSpec struct { // +listType=set // +kubebuilder:validation:MinItems=1 AllowedScopes []Scope `json:"allowedScopes"` + + // tokenLifetimes are the optional overrides of token lifetimes for an OIDCClient. + // +optional + TokenLifetimes OIDCClientTokenLifetimes `json:"tokenLifetimes,omitempty"` +} + +// OIDCClientTokenLifetimes describes the optional overrides of token lifetimes for an OIDCClient. +type OIDCClientTokenLifetimes struct { + // idTokenSeconds is the lifetime of ID tokens issued to this client, in seconds. This will choose the lifetime of + // ID tokens returned by the authorization flow and the refresh grant. It will not influence the lifetime of the ID + // tokens returned by RFC8693 token exchange. When null, a short-lived default value will be used. + // This value must be between 120 and 1,800 seconds (30 minutes), inclusive. It is recommended to make these tokens + // short-lived to force the client to perform the refresh grant often, because the refresh grant will check with the + // external identity provider to decide if it is acceptable for the end user to continue their session, and will + // update the end user's group memberships from the external identity provider. Giving these tokens a long life is + // will allow the end user to continue to use a token while avoiding these updates from the external identity + // provider. However, some web applications may have reasons specific to the design of that application to prefer + // longer lifetimes. + // +kubebuilder:validation:Minimum=120 + // +kubebuilder:validation:Maximum=1800 + // +optional + IDTokenSeconds *int32 `json:"idTokenSeconds,omitempty"` } // OIDCClientStatus is a struct that describes the actual state of an OIDCClient. diff --git a/generated/1.29/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go b/generated/1.29/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go index ad308c3de..d3c7ec69e 100644 --- a/generated/1.29/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.29/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go @@ -374,6 +374,7 @@ func (in *OIDCClientSpec) DeepCopyInto(out *OIDCClientSpec) { *out = make([]Scope, len(*in)) copy(*out, *in) } + in.TokenLifetimes.DeepCopyInto(&out.TokenLifetimes) return } @@ -409,3 +410,24 @@ func (in *OIDCClientStatus) DeepCopy() *OIDCClientStatus { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OIDCClientTokenLifetimes) DeepCopyInto(out *OIDCClientTokenLifetimes) { + *out = *in + if in.IDTokenSeconds != nil { + in, out := &in.IDTokenSeconds, &out.IDTokenSeconds + *out = new(int32) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OIDCClientTokenLifetimes. +func (in *OIDCClientTokenLifetimes) DeepCopy() *OIDCClientTokenLifetimes { + if in == nil { + return nil + } + out := new(OIDCClientTokenLifetimes) + in.DeepCopyInto(out) + return out +} diff --git a/generated/1.29/crds/config.supervisor.pinniped.dev_oidcclients.yaml b/generated/1.29/crds/config.supervisor.pinniped.dev_oidcclients.yaml index 0960ca0de..d33b31bf1 100644 --- a/generated/1.29/crds/config.supervisor.pinniped.dev_oidcclients.yaml +++ b/generated/1.29/crds/config.supervisor.pinniped.dev_oidcclients.yaml @@ -119,6 +119,27 @@ spec: minItems: 1 type: array x-kubernetes-list-type: set + tokenLifetimes: + description: tokenLifetimes are the optional overrides of token lifetimes + for an OIDCClient. + properties: + idTokenSeconds: + description: |- + idTokenSeconds is the lifetime of ID tokens issued to this client, in seconds. This will choose the lifetime of + ID tokens returned by the authorization flow and the refresh grant. It will not influence the lifetime of the ID + tokens returned by RFC8693 token exchange. When null, a short-lived default value will be used. + This value must be between 120 and 1,800 seconds (30 minutes), inclusive. It is recommended to make these tokens + short-lived to force the client to perform the refresh grant often, because the refresh grant will check with the + external identity provider to decide if it is acceptable for the end user to continue their session, and will + update the end user's group memberships from the external identity provider. Giving these tokens a long life is + will allow the end user to continue to use a token while avoiding these updates from the external identity + provider. However, some web applications may have reasons specific to the design of that application to prefer + longer lifetimes. + format: int32 + maximum: 1800 + minimum: 120 + type: integer + type: object required: - allowedGrantTypes - allowedRedirectURIs diff --git a/generated/latest/README.adoc b/generated/latest/README.adoc index dbecc10cf..ea1a39312 100644 --- a/generated/latest/README.adoc +++ b/generated/latest/README.adoc @@ -937,6 +937,7 @@ Must only contain the following values: - authorization_code: allows the client | *`allowedScopes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-supervisor-config-v1alpha1-scope[$$Scope$$] array__ | allowedScopes is a list of the allowed scopes param values that should be accepted during OIDC flows with this client. + Must only contain the following values: - openid: The client is allowed to request ID tokens. ID tokens only include the required claims by default (iss, sub, aud, exp, iat). This scope must always be listed. - offline_access: The client is allowed to request an initial refresh token during the authorization code grant flow. This scope must be listed if allowedGrantTypes lists refresh_token. - pinniped:request-audience: The client is allowed to request a new audience value during a RFC8693 token exchange, which is a step in the process to be able to get a cluster credential for the user. openid, username and groups scopes must be listed when this scope is present. This scope must be listed if allowedGrantTypes lists urn:ietf:params:oauth:grant-type:token-exchange. - username: The client is allowed to request that ID tokens contain the user's username. Without the username scope being requested and allowed, the ID token will not contain the user's username. - groups: The client is allowed to request that ID tokens contain the user's group membership, if their group membership is discoverable by the Supervisor. Without the groups scope being requested and allowed, the ID token will not contain groups. +| *`tokenLifetimes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-supervisor-config-v1alpha1-oidcclienttokenlifetimes[$$OIDCClientTokenLifetimes$$]__ | tokenLifetimes are the optional overrides of token lifetimes for an OIDCClient. |=== @@ -959,6 +960,23 @@ OIDCClientStatus is a struct that describes the actual state of an OIDCClient. |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-supervisor-config-v1alpha1-oidcclienttokenlifetimes"] +==== OIDCClientTokenLifetimes + +OIDCClientTokenLifetimes describes the optional overrides of token lifetimes for an OIDCClient. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-supervisor-config-v1alpha1-oidcclientspec[$$OIDCClientSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`idTokenSeconds`* __integer__ | idTokenSeconds is the lifetime of ID tokens issued to this client, in seconds. This will choose the lifetime of ID tokens returned by the authorization flow and the refresh grant. It will not influence the lifetime of the ID tokens returned by RFC8693 token exchange. When null, a short-lived default value will be used. This value must be between 120 and 1,800 seconds (30 minutes), inclusive. It is recommended to make these tokens short-lived to force the client to perform the refresh grant often, because the refresh grant will check with the external identity provider to decide if it is acceptable for the end user to continue their session, and will update the end user's group memberships from the external identity provider. Giving these tokens a long life is will allow the end user to continue to use a token while avoiding these updates from the external identity provider. However, some web applications may have reasons specific to the design of that application to prefer longer lifetimes. +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-supervisor-config-v1alpha1-redirecturi"] ==== RedirectURI (string) diff --git a/generated/latest/apis/supervisor/config/v1alpha1/types_oidcclient.go b/generated/latest/apis/supervisor/config/v1alpha1/types_oidcclient.go index 61106fdba..b02307f94 100644 --- a/generated/latest/apis/supervisor/config/v1alpha1/types_oidcclient.go +++ b/generated/latest/apis/supervisor/config/v1alpha1/types_oidcclient.go @@ -1,4 +1,4 @@ -// Copyright 2022-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2022-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package v1alpha1 @@ -71,6 +71,28 @@ type OIDCClientSpec struct { // +listType=set // +kubebuilder:validation:MinItems=1 AllowedScopes []Scope `json:"allowedScopes"` + + // tokenLifetimes are the optional overrides of token lifetimes for an OIDCClient. + // +optional + TokenLifetimes OIDCClientTokenLifetimes `json:"tokenLifetimes,omitempty"` +} + +// OIDCClientTokenLifetimes describes the optional overrides of token lifetimes for an OIDCClient. +type OIDCClientTokenLifetimes struct { + // idTokenSeconds is the lifetime of ID tokens issued to this client, in seconds. This will choose the lifetime of + // ID tokens returned by the authorization flow and the refresh grant. It will not influence the lifetime of the ID + // tokens returned by RFC8693 token exchange. When null, a short-lived default value will be used. + // This value must be between 120 and 1,800 seconds (30 minutes), inclusive. It is recommended to make these tokens + // short-lived to force the client to perform the refresh grant often, because the refresh grant will check with the + // external identity provider to decide if it is acceptable for the end user to continue their session, and will + // update the end user's group memberships from the external identity provider. Giving these tokens a long life is + // will allow the end user to continue to use a token while avoiding these updates from the external identity + // provider. However, some web applications may have reasons specific to the design of that application to prefer + // longer lifetimes. + // +kubebuilder:validation:Minimum=120 + // +kubebuilder:validation:Maximum=1800 + // +optional + IDTokenSeconds *int32 `json:"idTokenSeconds,omitempty"` } // OIDCClientStatus is a struct that describes the actual state of an OIDCClient. diff --git a/generated/latest/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go b/generated/latest/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go index ad308c3de..d3c7ec69e 100644 --- a/generated/latest/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go +++ b/generated/latest/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go @@ -374,6 +374,7 @@ func (in *OIDCClientSpec) DeepCopyInto(out *OIDCClientSpec) { *out = make([]Scope, len(*in)) copy(*out, *in) } + in.TokenLifetimes.DeepCopyInto(&out.TokenLifetimes) return } @@ -409,3 +410,24 @@ func (in *OIDCClientStatus) DeepCopy() *OIDCClientStatus { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OIDCClientTokenLifetimes) DeepCopyInto(out *OIDCClientTokenLifetimes) { + *out = *in + if in.IDTokenSeconds != nil { + in, out := &in.IDTokenSeconds, &out.IDTokenSeconds + *out = new(int32) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OIDCClientTokenLifetimes. +func (in *OIDCClientTokenLifetimes) DeepCopy() *OIDCClientTokenLifetimes { + if in == nil { + return nil + } + out := new(OIDCClientTokenLifetimes) + in.DeepCopyInto(out) + return out +} diff --git a/internal/crud/crud.go b/internal/crud/crud.go index e3212fd5c..03aff68e8 100644 --- a/internal/crud/crud.go +++ b/internal/crud/crud.go @@ -1,4 +1,4 @@ -// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package crud @@ -40,7 +40,7 @@ const ( ) type Storage interface { - Create(ctx context.Context, signature string, data JSON, additionalLabels map[string]string, ownerReferences []metav1.OwnerReference) (resourceVersion string, err error) + Create(ctx context.Context, signature string, data JSON, additionalLabels map[string]string, ownerReferences []metav1.OwnerReference, lifetime time.Duration) (resourceVersion string, err error) Get(ctx context.Context, signature string, data JSON) (resourceVersion string, err error) Update(ctx context.Context, signature, resourceVersion string, data JSON) (newResourceVersion string, err error) Delete(ctx context.Context, signature string) error @@ -50,13 +50,12 @@ type Storage interface { type JSON interface{} // document that we need valid JSON types -func New(resource string, secrets corev1client.SecretInterface, clock func() time.Time, lifetime time.Duration) Storage { +func New(resource string, secrets corev1client.SecretInterface, clock func() time.Time) Storage { return &secretsStorage{ resource: resource, secretType: secretType(resource), secrets: secrets, clock: clock, - lifetime: lifetime, } } @@ -65,11 +64,10 @@ type secretsStorage struct { secretType corev1.SecretType secrets corev1client.SecretInterface clock func() time.Time - lifetime time.Duration } -func (s *secretsStorage) Create(ctx context.Context, signature string, data JSON, additionalLabels map[string]string, ownerReferences []metav1.OwnerReference) (string, error) { - secret, err := s.toSecret(signature, "", data, additionalLabels, ownerReferences) +func (s *secretsStorage) Create(ctx context.Context, signature string, data JSON, additionalLabels map[string]string, ownerReferences []metav1.OwnerReference, lifetime time.Duration) (string, error) { + secret, err := s.toSecret(signature, "", data, additionalLabels, ownerReferences, lifetime) if err != nil { return "", err } @@ -96,7 +94,7 @@ func (s *secretsStorage) Get(ctx context.Context, signature string, data JSON) ( // Update takes a resourceVersion because it assumes Get has been recently called to obtain the latest resource version. // This is to ensure that concurrent edits are treated as conflict errors (only one will win). func (s *secretsStorage) Update(ctx context.Context, signature, resourceVersion string, data JSON) (string, error) { - secret, err := s.toSecret(signature, resourceVersion, data, nil, nil) + secret, err := s.toSecret(signature, resourceVersion, data, nil, nil, 0) if err != nil { return "", err } @@ -189,7 +187,7 @@ func (s *secretsStorage) GetName(signature string) string { return fmt.Sprintf(secretNameFormat, s.resource, signatureAsValidName) } -func (s *secretsStorage) toSecret(signature, resourceVersion string, data JSON, additionalLabels map[string]string, ownerReferences []metav1.OwnerReference) (*corev1.Secret, error) { +func (s *secretsStorage) toSecret(signature, resourceVersion string, data JSON, additionalLabels map[string]string, ownerReferences []metav1.OwnerReference, lifetime time.Duration) (*corev1.Secret, error) { buf, err := json.Marshal(data) if err != nil { return nil, fmt.Errorf("failed to encode secret data for %s: %w", s.GetName(signature), err) @@ -202,9 +200,9 @@ func (s *secretsStorage) toSecret(signature, resourceVersion string, data JSON, labelsToAdd[SecretLabelKey] = s.resource // make it easier to find this stuff via kubectl var annotations map[string]string - if s.lifetime > 0 { + if lifetime > 0 { annotations = map[string]string{ - SecretLifetimeAnnotationKey: s.clock().Add(s.lifetime).UTC().Format(SecretLifetimeAnnotationDateFormat), + SecretLifetimeAnnotationKey: s.clock().Add(lifetime).UTC().Format(SecretLifetimeAnnotationDateFormat), } } diff --git a/internal/federationdomain/clientregistry/clientregistry.go b/internal/federationdomain/clientregistry/clientregistry.go index 24dfaedfb..573de6fe7 100644 --- a/internal/federationdomain/clientregistry/clientregistry.go +++ b/internal/federationdomain/clientregistry/clientregistry.go @@ -1,4 +1,4 @@ -// Copyright 2021-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2021-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 // Package clientregistry defines Pinniped's OAuth2/OIDC clients. @@ -27,6 +27,12 @@ import ( // or a dynamic client defined by an OIDCClient CR. type Client struct { fosite.DefaultOpenIDConnectClient + + // Optionally provide a lifetime for ID tokens that result from authcode exchanges (initial logins) + // and refresh grants for this specific client. This will not impact the lifetime of ID tokens created + // via RFC8693 token exchange. When zero, the ID token lifetime will be determined by the defaults + // for the FederationDomain. + IDTokenLifetimeConfiguration time.Duration } // Client implements the base, OIDC, and response_mode client interfaces of Fosite. @@ -165,10 +171,19 @@ func PinnipedCLI() *Client { TokenEndpointAuthSigningAlgorithm: coreosoidc.RS256, TokenEndpointAuthMethod: "none", }, + IDTokenLifetimeConfiguration: 0, // never override the default timeouts for this client } } func oidcClientCRToFositeClient(oidcClient *configv1alpha1.OIDCClient, clientSecrets []string) *Client { + // Allow the user to optionally override the default timeouts for these clients. + idTokenLifetimeOverrideInSeconds := oidcClient.Spec.TokenLifetimes.IDTokenSeconds + var idTokenLifetime time.Duration + if idTokenLifetimeOverrideInSeconds != nil { + // It should be safe to cast this int32 to time.Duration, because time.Duration is an int64. + idTokenLifetime = time.Duration(*(oidcClient.Spec.TokenLifetimes.IDTokenSeconds)) * time.Second + } + return &Client{ DefaultOpenIDConnectClient: fosite.DefaultOpenIDConnectClient{ DefaultClient: &fosite.DefaultClient{ @@ -192,6 +207,7 @@ func oidcClientCRToFositeClient(oidcClient *configv1alpha1.OIDCClient, clientSec TokenEndpointAuthSigningAlgorithm: coreosoidc.RS256, TokenEndpointAuthMethod: "client_secret_basic", }, + IDTokenLifetimeConfiguration: idTokenLifetime, } } diff --git a/internal/federationdomain/endpoints/token/token_handler.go b/internal/federationdomain/endpoints/token/token_handler.go index bcfd31fc2..e441f6ce6 100644 --- a/internal/federationdomain/endpoints/token/token_handler.go +++ b/internal/federationdomain/endpoints/token/token_handler.go @@ -9,6 +9,7 @@ import ( "errors" "fmt" "net/http" + "time" "github.com/ory/fosite" errorsx "github.com/pkg/errors" @@ -19,8 +20,10 @@ import ( oidcapi "go.pinniped.dev/generated/latest/apis/supervisor/oidc" "go.pinniped.dev/internal/federationdomain/federationdomainproviders" + "go.pinniped.dev/internal/federationdomain/idtokenlifespan" "go.pinniped.dev/internal/federationdomain/oidc" "go.pinniped.dev/internal/federationdomain/resolvedprovider" + "go.pinniped.dev/internal/federationdomain/timeouts" "go.pinniped.dev/internal/httputil/httperr" "go.pinniped.dev/internal/idtransform" "go.pinniped.dev/internal/plog" @@ -30,6 +33,8 @@ import ( func NewHandler( idpLister federationdomainproviders.FederationDomainIdentityProvidersListerI, oauthHelper fosite.OAuth2Provider, + overrideAccessTokenLifespan timeouts.OverrideLifespan, + overrideIDTokenLifespan timeouts.OverrideLifespan, ) http.Handler { return httperr.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { session := psession.NewPinnipedSession() @@ -66,7 +71,17 @@ func NewHandler( } } - accessResponse, err := oauthHelper.NewAccessResponse(r.Context(), accessRequest) + // Lifetimes of the access and refresh tokens are determined by the above call to NewAccessRequest. + // Depending on the request, sometimes override the default access token lifespan. + maybeOverrideDefaultAccessTokenLifetime(overrideAccessTokenLifespan, accessRequest) + + // Create the token response. + // The lifetime of the ID token will be determined inside the call NewAccessResponse. + // Depending on the request, sometimes override the default ID token lifespan by putting + // the override value onto the context. + accessResponse, err := oauthHelper.NewAccessResponse( + maybeOverrideDefaultIDTokenLifetime(r.Context(), overrideIDTokenLifespan, accessRequest), + accessRequest) if err != nil { plog.Info("token response error", oidc.FositeErrorForLog(err)...) oauthHelper.WriteAccessError(r.Context(), w, accessRequest, err) @@ -79,6 +94,19 @@ func NewHandler( }) } +func maybeOverrideDefaultAccessTokenLifetime(overrideAccessTokenLifespan timeouts.OverrideLifespan, accessRequest fosite.AccessRequester) { + if doOverride, newLifespan := overrideAccessTokenLifespan(accessRequest); doOverride { + accessRequest.GetSession().SetExpiresAt(fosite.AccessToken, time.Now().UTC().Add(newLifespan).Round(time.Second)) + } +} + +func maybeOverrideDefaultIDTokenLifetime(baseCtx context.Context, overrideIDTokenLifespan timeouts.OverrideLifespan, accessRequest fosite.AccessRequester) context.Context { + if doOverride, newLifespan := overrideIDTokenLifespan(accessRequest); doOverride { + return idtokenlifespan.OverrideIDTokenLifespanInContext(baseCtx, newLifespan) + } + return baseCtx +} + func errMissingUpstreamSessionInternalError() *fosite.RFC6749Error { return &fosite.RFC6749Error{ ErrorField: "error", diff --git a/internal/federationdomain/endpointsmanager/manager.go b/internal/federationdomain/endpointsmanager/manager.go index 55b6ceca6..ea5f6e016 100644 --- a/internal/federationdomain/endpointsmanager/manager.go +++ b/internal/federationdomain/endpointsmanager/manager.go @@ -1,4 +1,4 @@ -// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package endpointsmanager @@ -161,6 +161,8 @@ func (m *Manager) SetFederationDomains(federationDomains ...*federationdomainpro m.providerHandlers[(issuerHostWithPath + oidc.TokenEndpointPath)] = token.NewHandler( idpLister, oauthHelperWithKubeStorage, + timeoutsConfiguration.OverrideDefaultAccessTokenLifespan, + timeoutsConfiguration.OverrideDefaultIDTokenLifespan, ) m.providerHandlers[(issuerHostWithPath + oidc.PinnipedLoginPath)] = login.NewHandler( diff --git a/internal/federationdomain/idtokenlifespan/idtoken_lifespan.go b/internal/federationdomain/idtokenlifespan/idtoken_lifespan.go new file mode 100644 index 000000000..919d41a99 --- /dev/null +++ b/internal/federationdomain/idtokenlifespan/idtoken_lifespan.go @@ -0,0 +1,55 @@ +// Copyright 2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package idtokenlifespan + +import ( + "context" + "time" + + "github.com/ory/fosite" + "github.com/ory/fosite/compose" + "github.com/ory/fosite/handler/openid" +) + +// contextKey type is unexported to prevent collisions. +type contextKey int + +const idTokenLifetimeOverrideKey contextKey = iota + +// OpenIDConnectExplicitFactory is similar to the function of the same name in the fosite compose package, +// except it allows wrapping the IDTokenLifespanProvider. +func OpenIDConnectExplicitFactory(config fosite.Configurator, storage interface{}, strategy interface{}) interface{} { + openIDConnectExplicitHandler := compose.OpenIDConnectExplicitFactory(config, storage, strategy).(*openid.OpenIDConnectExplicitHandler) + // Overwrite the config with a wrapper around the fosite.IDTokenLifespanProvider. + openIDConnectExplicitHandler.Config = &contextAwareIDTokenLifespanProvider{DelegateConfig: config} + return openIDConnectExplicitHandler +} + +// OpenIDConnectRefreshFactory is similar to the function of the same name in the fosite compose package, +// except it allows wrapping the IDTokenLifespanProvider. +func OpenIDConnectRefreshFactory(config fosite.Configurator, _ interface{}, strategy interface{}) interface{} { + openIDConnectRefreshHandler := compose.OpenIDConnectRefreshFactory(config, nil, strategy).(*openid.OpenIDConnectRefreshHandler) + // Overwrite the config with a wrapper around the fosite.IDTokenLifespanProvider. + openIDConnectRefreshHandler.Config = &contextAwareIDTokenLifespanProvider{DelegateConfig: config} + return openIDConnectRefreshHandler +} + +var _ fosite.IDTokenLifespanProvider = (*contextAwareIDTokenLifespanProvider)(nil) + +type contextAwareIDTokenLifespanProvider struct { + DelegateConfig fosite.IDTokenLifespanProvider +} + +func (c *contextAwareIDTokenLifespanProvider) GetIDTokenLifespan(ctx context.Context) time.Duration { + idTokenLifespanOverride, ok := ctx.Value(idTokenLifetimeOverrideKey).(time.Duration) + if ok { + return idTokenLifespanOverride + } + // When there is no override on the context, just return the default by calling the delegate. + return c.DelegateConfig.GetIDTokenLifespan(ctx) +} + +func OverrideIDTokenLifespanInContext(ctx context.Context, newLifespan time.Duration) context.Context { + return context.WithValue(ctx, idTokenLifetimeOverrideKey, newLifespan) +} diff --git a/internal/federationdomain/oidc/oidc.go b/internal/federationdomain/oidc/oidc.go index dddee951e..7715f9e89 100644 --- a/internal/federationdomain/oidc/oidc.go +++ b/internal/federationdomain/oidc/oidc.go @@ -7,8 +7,10 @@ package oidc import ( "crypto/subtle" + "errors" "fmt" "net/http" + "reflect" "time" "github.com/felixge/httpsnoop" @@ -17,10 +19,12 @@ import ( errorsx "github.com/pkg/errors" oidcapi "go.pinniped.dev/generated/latest/apis/supervisor/oidc" + "go.pinniped.dev/internal/federationdomain/clientregistry" "go.pinniped.dev/internal/federationdomain/csrftoken" "go.pinniped.dev/internal/federationdomain/endpoints/jwks" "go.pinniped.dev/internal/federationdomain/endpoints/tokenexchange" "go.pinniped.dev/internal/federationdomain/formposthtml" + "go.pinniped.dev/internal/federationdomain/idtokenlifespan" "go.pinniped.dev/internal/federationdomain/strategy" "go.pinniped.dev/internal/federationdomain/timeouts" "go.pinniped.dev/internal/httputil/httperr" @@ -102,23 +106,121 @@ type UpstreamStateParamData struct { FormatVersion string `json:"v"` } -// Get the defaults for the Supervisor server. +// DefaultOIDCTimeoutsConfiguration returns the default timeouts for the Supervisor server. func DefaultOIDCTimeoutsConfiguration() timeouts.Configuration { - accessTokenLifespan := 2 * time.Minute + // Note: The maximum time that users can access Kubernetes clusters without + // needing to do a Supervisor refresh is the sum of the access token lifetime, + // the ID token lifetime, and the Concierge's mTLS client cert lifetime. + // This is because a client can exchange the access token just before it expires + // for a new cluster-scoped ID token, and use that just before it expires to get + // a new mTLS client cert, which grants access to the cluster until it expires. + // + // Note that the Concierge's mTLS client cert lifetime is 5 minutes, which can + // be seen in its source at credentialrequest/rest.go. + // + // This maximum total time is important because it represents the longest possible + // time that a user could continue to use a cluster based on their original login + // (or most recent refresh) after an administrator of an external identity provider + // removes the user, revokes their session, changes their group membership, + // or otherwise makes any type of change to the user's account in the external + // identity provider that should be noticed by the Supervisor during an upstream + // refresh. + // + // Given the timeouts specified below, this is: 2 + 2 + 5 = 9 minutes. + // Note that this may be different if an OIDCClient's configuration has changed + // the lifetime of the ID tokens issued to that client, but usually will not be + // different because that configuration does not change the lifetime of the + // cluster-scoped ID tokens. The only case where that configuration would change + // it is if the admin configured a cluster to accept the initial ID token's + // audience instead of the cluster-scoped ID token's audience. + // + // The CLI will use a cached mTLS client cert until it expires. + // Because of the default timeouts, when the first mTLS client cert expires after + // five minutes, the CLI will need to perform a refresh before it can get a second + // client cert, due to the original access token and cluster-scoped ID token having + // already expired by that time (after two minutes). + + // Give a generous amount of time for an authorized client to be able to exchange + // its authcode for tokens. authorizationCodeLifespan := 10 * time.Minute + + // This is intended to give a very short amount of time to allow the client to + // use the access token to exchange for cluster-scoped ID token(s). After this + // time runs out, they will need to perform a refresh to get a new tokens, + // ensuring the Supervisor has a chance to revalidate their session often. + accessTokenLifespan := 2 * time.Minute + + // The ID token will have the same default lifespan as the access token for a + // similar reason. This is the default lifespan for ID tokens issued by the + // authcode flow, the refresh flow, and the cluster-scoped token exchange. + // The cluster-scoped ID token can be exchanged for an mTLS client cert, so + // limit the window of opportunity to make that exchange to be small. + idTokenLifespan := accessTokenLifespan + + // This is just long enough to cover a typical work day, giving the end user an + // experience of logging in once per day to access all their Kubernetes clusters. refreshTokenLifespan := 9 * time.Hour + // Give a little extra time for some storage lifetimes, to avoid the possibility + // that the storage be garbage collected in the middle of trying to look up the token. + storageExtraLifetime := time.Minute + return timeouts.Configuration{ - UpstreamStateParamLifespan: 90 * time.Minute, - AuthorizeCodeLifespan: authorizationCodeLifespan, - AccessTokenLifespan: accessTokenLifespan, - IDTokenLifespan: accessTokenLifespan, - RefreshTokenLifespan: refreshTokenLifespan, - AuthorizationCodeSessionStorageLifetime: authorizationCodeLifespan + refreshTokenLifespan, - PKCESessionStorageLifetime: authorizationCodeLifespan + (1 * time.Minute), - OIDCSessionStorageLifetime: authorizationCodeLifespan + (1 * time.Minute), - AccessTokenSessionStorageLifetime: refreshTokenLifespan + accessTokenLifespan, - RefreshTokenSessionStorageLifetime: refreshTokenLifespan + accessTokenLifespan, + // Give enough time for someone to start an interactive authorization flow, go eat lunch, + // and then finish the authorization afterward. + UpstreamStateParamLifespan: 90 * time.Minute, + + AuthorizeCodeLifespan: authorizationCodeLifespan, + + AccessTokenLifespan: accessTokenLifespan, + OverrideDefaultAccessTokenLifespan: func(accessRequest fosite.AccessRequester) (bool, time.Duration) { + // Not currently overriding the defaults. + return false, 0 + }, + + IDTokenLifespan: idTokenLifespan, + OverrideDefaultIDTokenLifespan: func(accessRequest fosite.AccessRequester) (bool, time.Duration) { + client := accessRequest.GetClient() + // Don't allow OIDCClients to override the default lifetime for ID tokens returned + // by RFC8693 token exchange. This is not user configurable for now. + if !accessRequest.GetGrantTypes().ExactOne(oidcapi.GrantTypeTokenExchange) { + if castClient, ok := client.(*clientregistry.Client); !ok { + // All clients returned by our client registry implement clientregistry.Client, + // so this should be a safe cast in practice. + plog.Error("could not check if client overrides token lifetimes", + errors.New("could not cast client to *clientregistry.Client"), + "clientID", client.GetID(), "clientType", reflect.TypeOf(client)) + } else if castClient.IDTokenLifetimeConfiguration > 0 { + // An OIDCClient resource has provided an override, so use it. + // Note that the pinniped-cli client never overrides this value. + return true, castClient.IDTokenLifetimeConfiguration + } + } + // Otherwise, do not override the defaults. + return false, 0 + }, + + RefreshTokenLifespan: refreshTokenLifespan, + + AuthorizationCodeSessionStorageLifetime: func(requester fosite.Requester) time.Duration { + return authorizationCodeLifespan + refreshTokenLifespan + }, + + PKCESessionStorageLifetime: func(_requester fosite.Requester) time.Duration { + return authorizationCodeLifespan + storageExtraLifetime + }, + + OIDCSessionStorageLifetime: func(_requester fosite.Requester) time.Duration { + return authorizationCodeLifespan + storageExtraLifetime + }, + + AccessTokenSessionStorageLifetime: func(requester fosite.Requester) time.Duration { + return refreshTokenLifespan + accessTokenLifespan + }, + + RefreshTokenSessionStorageLifetime: func(requester fosite.Requester) time.Duration { + return refreshTokenLifespan + accessTokenLifespan + }, } } @@ -170,8 +272,10 @@ func FositeOauth2Helper( }, compose.OAuth2AuthorizeExplicitFactory, compose.OAuth2RefreshTokenGrantFactory, - compose.OpenIDConnectExplicitFactory, - compose.OpenIDConnectRefreshFactory, + // Use a custom factory to allow selective overrides of the ID token lifespan during authcode exchange. + idtokenlifespan.OpenIDConnectExplicitFactory, + // Use a custom factory to allow selective overrides of the ID token lifespan during refresh. + idtokenlifespan.OpenIDConnectRefreshFactory, compose.OAuth2PKCEFactory, tokenexchange.HandlerFactory, // handle the "urn:ietf:params:oauth:grant-type:token-exchange" grant type ) @@ -341,8 +445,8 @@ func rewriteStatusSeeOtherToStatusFoundForBrowserless(w http.ResponseWriter) htt // https://tools.ietf.org/id/draft-ietf-oauth-security-topics-18.html#section-4.11 // Safari has the bad behavior in the case of http.StatusFound and not just http.StatusTemporaryRedirect. // - // in the browserless flows, the OAuth client is the pinniped CLI and it already has access to the user's - // password. Thus there is no security issue with using http.StatusFound vs. http.StatusSeeOther. + // In the browserless flows, the OAuth client is the pinniped CLI, and it already has access to the user's + // password. Thus, there is no security issue with using http.StatusFound vs. http.StatusSeeOther. return httpsnoop.Wrap(w, httpsnoop.Hooks{ WriteHeader: func(delegate httpsnoop.WriteHeaderFunc) httpsnoop.WriteHeaderFunc { return func(code int) { diff --git a/internal/federationdomain/timeouts/timeouts_configuration.go b/internal/federationdomain/timeouts/timeouts_configuration.go index cd55cfde6..639051b15 100644 --- a/internal/federationdomain/timeouts/timeouts_configuration.go +++ b/internal/federationdomain/timeouts/timeouts_configuration.go @@ -3,7 +3,18 @@ package timeouts -import "time" +import ( + "time" + + "github.com/ory/fosite" +) + +// StorageLifetime is a function that can, given a request, decide how long it should live in session storage. +type StorageLifetime func(requester fosite.Requester) time.Duration + +// OverrideLifespan is a function that, given a request, can suggest to override the default lifespan +// by returning true along with a new lifespan. When false is returned, the returned duration should be ignored. +type OverrideLifespan func(accessRequest fosite.AccessRequester) (bool, time.Duration) type Configuration struct { // The length of time that our state param that we encrypt and pass to the upstream OIDC IDP should be considered @@ -22,11 +33,28 @@ type Configuration struct { // be fairly short-lived. AccessTokenLifespan time.Duration + // Optionally override the default AccessTokenLifespan depending on the specific request. + // Note that access tokens can be issued by authcode exchanges and refreshes (with different grant types on the + // request), so implementations of this method should handle choosing lifespans for both cases as desired. + // Note that fosite offers the fosite.ClientWithCustomTokenLifespans interface, but that interface does not + // pass the full request details to the GetEffectiveLifespan() function, so it does not suit our needs, + // and we use this technique instead. + OverrideDefaultAccessTokenLifespan OverrideLifespan + // The lifetime of an downstream ID token issued by the token endpoint. This should generally be the same // as the AccessTokenLifespan, or longer if it would be useful for the user's proof of identity to be valid // for longer than their proof of authorization. IDTokenLifespan time.Duration + // Optionally override the default IDTokenLifespan depending on the specific request. + // Note that ID tokens can be issued by authcode exchanges, refreshes, and RFC8693 token exchanges + // (with different grant types on the request), so implementations of this method should handle choosing + // lifespans for all three cases as desired. + // Note that fosite offers the fosite.ClientWithCustomTokenLifespans interface, but that interface does not + // pass the full request details to the GetEffectiveLifespan() function, so it does not suit our needs, + // and we use this technique instead. + OverrideDefaultIDTokenLifespan OverrideLifespan + // The lifetime of an downstream refresh token issued by the token endpoint. This should generally be // significantly longer than the access token lifetime, so it can be used to refresh the access token // multiple times. Once the refresh token expires, the user's session is over and they will need @@ -40,7 +68,7 @@ type Configuration struct { // include revoking the access and refresh tokens associated with the session. Therefore, this should be // significantly longer than the AuthorizeCodeLifespan, and there is probably no reason to make it longer than // the sum of the AuthorizeCodeLifespan and the RefreshTokenLifespan. - AuthorizationCodeSessionStorageLifetime time.Duration + AuthorizationCodeSessionStorageLifetime StorageLifetime // PKCESessionStorageLifetime is the length of time after which PKCE data is allowed to be garbage collected from // storage. PKCE sessions are closely related to authorization code sessions. After the authcode is successfully @@ -48,19 +76,19 @@ type Configuration struct { // but it is not explicitly deleted. Therefore, this can be just slightly longer than the AuthorizeCodeLifespan. We'll // avoid making it exactly the same as AuthorizeCodeLifespan to avoid any chance of the garbage collector deleting it // while it is being used. - PKCESessionStorageLifetime time.Duration + PKCESessionStorageLifetime StorageLifetime // OIDCSessionStorageLifetime is the length of time after which the OIDC session data related to an authcode // is allowed to be garbage collected from storage. After the authcode is successfully redeemed, the OIDC session is // explicitly deleted. Similar to the PKCE session, they are not needed anymore after the corresponding authcode has expired. // Therefore, this can be just slightly longer than the AuthorizeCodeLifespan. We'll avoid making it exactly the same // as AuthorizeCodeLifespan to avoid any chance of the garbage collector deleting it while it is being used. - OIDCSessionStorageLifetime time.Duration + OIDCSessionStorageLifetime StorageLifetime // AccessTokenSessionStorageLifetime is the length of time after which an access token's session data is allowed // to be garbage collected from storage. These must exist in storage for as long as the refresh token is valid // or else the refresh flow will not work properly. So this must be longer than RefreshTokenLifespan. - AccessTokenSessionStorageLifetime time.Duration + AccessTokenSessionStorageLifetime StorageLifetime // RefreshTokenSessionStorageLifetime is the length of time after which a refresh token's session data is allowed // to be garbage collected from storage. These must exist in storage for as long as the refresh token is valid. @@ -70,5 +98,5 @@ type Configuration struct { // error message telling them that the token is expired, rather than a more generic error that is returned // when the token does not exist. If this is desirable, then the RefreshTokenSessionStorageLifetime can be made // to be significantly larger than RefreshTokenLifespan, at the cost of slower cleanup. - RefreshTokenSessionStorageLifetime time.Duration + RefreshTokenSessionStorageLifetime StorageLifetime } diff --git a/internal/fositestorage/accesstoken/accesstoken.go b/internal/fositestorage/accesstoken/accesstoken.go index 9a5ca87a7..247273c42 100644 --- a/internal/fositestorage/accesstoken/accesstoken.go +++ b/internal/fositestorage/accesstoken/accesstoken.go @@ -1,4 +1,4 @@ -// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package accesstoken @@ -17,6 +17,7 @@ import ( "go.pinniped.dev/internal/constable" "go.pinniped.dev/internal/crud" "go.pinniped.dev/internal/federationdomain/clientregistry" + "go.pinniped.dev/internal/federationdomain/timeouts" "go.pinniped.dev/internal/fositestorage" "go.pinniped.dev/internal/psession" ) @@ -44,7 +45,8 @@ type RevocationStorage interface { var _ RevocationStorage = &accessTokenStorage{} type accessTokenStorage struct { - storage crud.Storage + storage crud.Storage + lifetime timeouts.StorageLifetime } type Session struct { @@ -52,8 +54,8 @@ type Session struct { Version string `json:"version"` } -func New(secrets corev1client.SecretInterface, clock func() time.Time, sessionStorageLifetime time.Duration) RevocationStorage { - return &accessTokenStorage{storage: crud.New(TypeLabelValue, secrets, clock, sessionStorageLifetime)} +func New(secrets corev1client.SecretInterface, clock func() time.Time, sessionStorageLifetime timeouts.StorageLifetime) RevocationStorage { + return &accessTokenStorage{storage: crud.New(TypeLabelValue, secrets, clock), lifetime: sessionStorageLifetime} } // ReadFromSecret reads the contents of a Secret as a Session. @@ -83,12 +85,12 @@ func (a *accessTokenStorage) CreateAccessTokenSession(ctx context.Context, signa return err } - _, err = a.storage.Create( - ctx, + _, err = a.storage.Create(ctx, signature, &Session{Request: request, Version: accessTokenStorageVersion}, map[string]string{fositestorage.StorageRequestIDLabelName: requester.GetID()}, nil, + a.lifetime(requester), ) return err } diff --git a/internal/fositestorage/authorizationcode/authorizationcode.go b/internal/fositestorage/authorizationcode/authorizationcode.go index 41635a7ad..c71f0188b 100644 --- a/internal/fositestorage/authorizationcode/authorizationcode.go +++ b/internal/fositestorage/authorizationcode/authorizationcode.go @@ -1,4 +1,4 @@ -// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package authorizationcode @@ -18,6 +18,7 @@ import ( "go.pinniped.dev/internal/constable" "go.pinniped.dev/internal/crud" "go.pinniped.dev/internal/federationdomain/clientregistry" + "go.pinniped.dev/internal/federationdomain/timeouts" "go.pinniped.dev/internal/fositestorage" "go.pinniped.dev/internal/psession" ) @@ -40,7 +41,8 @@ const ( var _ oauth2.AuthorizeCodeStorage = &authorizeCodeStorage{} type authorizeCodeStorage struct { - storage crud.Storage + storage crud.Storage + lifetime timeouts.StorageLifetime } type Session struct { @@ -49,8 +51,8 @@ type Session struct { Version string `json:"version"` } -func New(secrets corev1client.SecretInterface, clock func() time.Time, sessionStorageLifetime time.Duration) oauth2.AuthorizeCodeStorage { - return &authorizeCodeStorage{storage: crud.New(TypeLabelValue, secrets, clock, sessionStorageLifetime)} +func New(secrets corev1client.SecretInterface, clock func() time.Time, sessionStorageLifetime timeouts.StorageLifetime) oauth2.AuthorizeCodeStorage { + return &authorizeCodeStorage{storage: crud.New(TypeLabelValue, secrets, clock), lifetime: sessionStorageLifetime} } // ReadFromSecret reads the contents of a Secret as a Session. @@ -92,7 +94,13 @@ func (a *authorizeCodeStorage) CreateAuthorizeCodeSession(ctx context.Context, s // of the consent authorization request. It is used to identify the session. // signature for lookup in the DB - _, err = a.storage.Create(ctx, signature, &Session{Active: true, Request: request, Version: authorizeCodeStorageVersion}, nil, nil) + _, err = a.storage.Create(ctx, + signature, + &Session{Active: true, Request: request, Version: authorizeCodeStorageVersion}, + nil, + nil, + a.lifetime(requester), + ) return err } diff --git a/internal/fositestorage/openidconnect/openidconnect.go b/internal/fositestorage/openidconnect/openidconnect.go index 466f0f2e9..58e560a10 100644 --- a/internal/fositestorage/openidconnect/openidconnect.go +++ b/internal/fositestorage/openidconnect/openidconnect.go @@ -1,4 +1,4 @@ -// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package openidconnect @@ -17,6 +17,7 @@ import ( "go.pinniped.dev/internal/constable" "go.pinniped.dev/internal/crud" "go.pinniped.dev/internal/federationdomain/clientregistry" + "go.pinniped.dev/internal/federationdomain/timeouts" "go.pinniped.dev/internal/fositestorage" "go.pinniped.dev/internal/psession" ) @@ -40,7 +41,8 @@ const ( var _ openid.OpenIDConnectRequestStorage = &openIDConnectRequestStorage{} type openIDConnectRequestStorage struct { - storage crud.Storage + storage crud.Storage + lifetime timeouts.StorageLifetime } type session struct { @@ -48,8 +50,8 @@ type session struct { Version string `json:"version"` } -func New(secrets corev1client.SecretInterface, clock func() time.Time, sessionStorageLifetime time.Duration) openid.OpenIDConnectRequestStorage { - return &openIDConnectRequestStorage{storage: crud.New(TypeLabelValue, secrets, clock, sessionStorageLifetime)} +func New(secrets corev1client.SecretInterface, clock func() time.Time, sessionStorageLifetime timeouts.StorageLifetime) openid.OpenIDConnectRequestStorage { + return &openIDConnectRequestStorage{storage: crud.New(TypeLabelValue, secrets, clock), lifetime: sessionStorageLifetime} } func (a *openIDConnectRequestStorage) CreateOpenIDConnectSession(ctx context.Context, authcode string, requester fosite.Requester) error { @@ -63,7 +65,13 @@ func (a *openIDConnectRequestStorage) CreateOpenIDConnectSession(ctx context.Con return err } - _, err = a.storage.Create(ctx, signature, &session{Request: request, Version: oidcStorageVersion}, nil, nil) + _, err = a.storage.Create(ctx, + signature, + &session{Request: request, Version: oidcStorageVersion}, + nil, + nil, + a.lifetime(requester), + ) return err } diff --git a/internal/fositestorage/pkce/pkce.go b/internal/fositestorage/pkce/pkce.go index 78fe0d380..9b6a14d4c 100644 --- a/internal/fositestorage/pkce/pkce.go +++ b/internal/fositestorage/pkce/pkce.go @@ -1,4 +1,4 @@ -// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package pkce @@ -16,6 +16,7 @@ import ( "go.pinniped.dev/internal/constable" "go.pinniped.dev/internal/crud" "go.pinniped.dev/internal/federationdomain/clientregistry" + "go.pinniped.dev/internal/federationdomain/timeouts" "go.pinniped.dev/internal/fositestorage" "go.pinniped.dev/internal/psession" ) @@ -38,7 +39,8 @@ const ( var _ pkce.PKCERequestStorage = &pkceStorage{} type pkceStorage struct { - storage crud.Storage + storage crud.Storage + lifetime timeouts.StorageLifetime } type session struct { @@ -46,8 +48,8 @@ type session struct { Version string `json:"version"` } -func New(secrets corev1client.SecretInterface, clock func() time.Time, sessionStorageLifetime time.Duration) pkce.PKCERequestStorage { - return &pkceStorage{storage: crud.New(TypeLabelValue, secrets, clock, sessionStorageLifetime)} +func New(secrets corev1client.SecretInterface, clock func() time.Time, sessionStorageLifetime timeouts.StorageLifetime) pkce.PKCERequestStorage { + return &pkceStorage{storage: crud.New(TypeLabelValue, secrets, clock), lifetime: sessionStorageLifetime} } func (a *pkceStorage) CreatePKCERequestSession(ctx context.Context, signature string, requester fosite.Requester) error { @@ -56,7 +58,13 @@ func (a *pkceStorage) CreatePKCERequestSession(ctx context.Context, signature st return err } - _, err = a.storage.Create(ctx, signature, &session{Request: request, Version: pkceStorageVersion}, nil, nil) + _, err = a.storage.Create(ctx, + signature, + &session{Request: request, Version: pkceStorageVersion}, + nil, + nil, + a.lifetime(requester), + ) return err } diff --git a/internal/fositestorage/refreshtoken/refreshtoken.go b/internal/fositestorage/refreshtoken/refreshtoken.go index 8f947fcfb..d87c6bb9b 100644 --- a/internal/fositestorage/refreshtoken/refreshtoken.go +++ b/internal/fositestorage/refreshtoken/refreshtoken.go @@ -1,4 +1,4 @@ -// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package refreshtoken @@ -17,6 +17,7 @@ import ( "go.pinniped.dev/internal/constable" "go.pinniped.dev/internal/crud" "go.pinniped.dev/internal/federationdomain/clientregistry" + "go.pinniped.dev/internal/federationdomain/timeouts" "go.pinniped.dev/internal/fositestorage" "go.pinniped.dev/internal/psession" ) @@ -45,7 +46,8 @@ type RevocationStorage interface { var _ RevocationStorage = &refreshTokenStorage{} type refreshTokenStorage struct { - storage crud.Storage + storage crud.Storage + lifetime timeouts.StorageLifetime } type Session struct { @@ -53,8 +55,8 @@ type Session struct { Version string `json:"version"` } -func New(secrets corev1client.SecretInterface, clock func() time.Time, sessionStorageLifetime time.Duration) RevocationStorage { - return &refreshTokenStorage{storage: crud.New(TypeLabelValue, secrets, clock, sessionStorageLifetime)} +func New(secrets corev1client.SecretInterface, clock func() time.Time, sessionStorageLifetime timeouts.StorageLifetime) RevocationStorage { + return &refreshTokenStorage{storage: crud.New(TypeLabelValue, secrets, clock), lifetime: sessionStorageLifetime} } // ReadFromSecret reads the contents of a Secret as a Session. @@ -89,12 +91,12 @@ func (a *refreshTokenStorage) CreateRefreshTokenSession(ctx context.Context, sig return err } - _, err = a.storage.Create( - ctx, + _, err = a.storage.Create(ctx, signature, &Session{Request: request, Version: refreshTokenStorageVersion}, map[string]string{fositestorage.StorageRequestIDLabelName: requester.GetID()}, nil, + a.lifetime(requester), ) return err } diff --git a/internal/oidcclientsecretstorage/oidcclientsecretstorage.go b/internal/oidcclientsecretstorage/oidcclientsecretstorage.go index ae4788d55..1cdba6549 100644 --- a/internal/oidcclientsecretstorage/oidcclientsecretstorage.go +++ b/internal/oidcclientsecretstorage/oidcclientsecretstorage.go @@ -1,4 +1,4 @@ -// Copyright 2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2022-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package oidcclientsecretstorage @@ -45,7 +45,7 @@ type storedClientSecret struct { func New(secrets corev1client.SecretInterface) *OIDCClientSecretStorage { return &OIDCClientSecretStorage{ - storage: crud.New(TypeLabelValue, secrets, nil, 0), // can use nil clock because we are using infinite lifetime + storage: crud.New(TypeLabelValue, secrets, nil), // can use nil clock because we are using infinite lifetime for creates secrets: secrets, } } @@ -91,7 +91,7 @@ func (s *OIDCClientSecretStorage) Set(ctx context.Context, resourceVersion, oidc Controller: nil, // doesn't seem to matter, and there is no particular controller owning this BlockOwnerDeletion: nil, }} - if _, err := s.storage.Create(ctx, name, secret, nil, ownerReferences); err != nil { + if _, err := s.storage.Create(ctx, name, secret, nil, ownerReferences, 0); err != nil { // 0 is infinite lifetime return fmt.Errorf("failed to create client secret for uid %s: %w", oidcClientUID, err) } return nil diff --git a/test/integration/supervisor_oidc_client_test.go b/test/integration/supervisor_oidc_client_test.go index 77f659b63..f76d380fc 100644 --- a/test/integration/supervisor_oidc_client_test.go +++ b/test/integration/supervisor_oidc_client_test.go @@ -1,4 +1,4 @@ -// Copyright 2022-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2022-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package integration @@ -6,6 +6,7 @@ package integration import ( "context" "fmt" + "k8s.io/utils/ptr" "sort" "strings" "testing" @@ -155,6 +156,54 @@ func TestOIDCClientStaticValidation_Parallel(t *testing.T) { }, wantErr: `OIDCClient.config.supervisor.pinniped.dev "client.oauth.pinniped.dev-hello" is invalid: spec.allowedRedirectURIs[1]: Invalid value: "oob": spec.allowedRedirectURIs[1] in body should match '^https://.+|^http://(127\.0\.0\.1|\[::1\])(:\d+)?/'`, }, + { + name: "ID token lifetime too small", + client: &supervisorconfigv1alpha1.OIDCClient{ + ObjectMeta: metav1.ObjectMeta{ + Name: "client.oauth.pinniped.dev-hello", + }, + Spec: supervisorconfigv1alpha1.OIDCClientSpec{ + AllowedRedirectURIs: []supervisorconfigv1alpha1.RedirectURI{ + "http://127.0.0.1/callback", + }, + AllowedGrantTypes: []supervisorconfigv1alpha1.GrantType{ + "refresh_token", + }, + AllowedScopes: []supervisorconfigv1alpha1.Scope{ + "username", + }, + TokenLifetimes: supervisorconfigv1alpha1.OIDCClientTokenLifetimes{ + IDTokenSeconds: ptr.To[int32](119), + }, + }, + }, + wantErr: `OIDCClient.config.supervisor.pinniped.dev "client.oauth.pinniped.dev-hello" is invalid: ` + + `spec.tokenLifetimes.idTokenSeconds: Invalid value: 119: spec.tokenLifetimes.idTokenSeconds in body should be greater than or equal to 120`, + }, + { + name: "ID token lifetime too large", + client: &supervisorconfigv1alpha1.OIDCClient{ + ObjectMeta: metav1.ObjectMeta{ + Name: "client.oauth.pinniped.dev-hello", + }, + Spec: supervisorconfigv1alpha1.OIDCClientSpec{ + AllowedRedirectURIs: []supervisorconfigv1alpha1.RedirectURI{ + "http://127.0.0.1/callback", + }, + AllowedGrantTypes: []supervisorconfigv1alpha1.GrantType{ + "refresh_token", + }, + AllowedScopes: []supervisorconfigv1alpha1.Scope{ + "username", + }, + TokenLifetimes: supervisorconfigv1alpha1.OIDCClientTokenLifetimes{ + IDTokenSeconds: ptr.To[int32](1801), + }, + }, + }, + wantErr: `OIDCClient.config.supervisor.pinniped.dev "client.oauth.pinniped.dev-hello" is invalid: ` + + `spec.tokenLifetimes.idTokenSeconds: Invalid value: 1801: spec.tokenLifetimes.idTokenSeconds in body should be less than or equal to 1800`, + }, { name: "bad grant type", client: &supervisorconfigv1alpha1.OIDCClient{ From c8bc192e0b1fc88ba847f40e3806c374a6e7f2ab Mon Sep 17 00:00:00 2001 From: Joshua Casey Date: Thu, 28 Mar 2024 12:43:52 -0500 Subject: [PATCH 2/8] Start working on units tests for configurable token lifetimes --- internal/crud/crud.go | 2 +- internal/crud/crud_test.go | 28 ++- .../clientregistry/clientregistry_test.go | 5 +- .../endpoints/token/token_handler_test.go | 7 +- .../accesstoken/accesstoken_test.go | 8 +- .../authorizationcode/authorizationcode.go | 160 +++++++++--------- .../authorizationcode_test.go | 10 +- .../openidconnect/openidconnect_test.go | 4 +- internal/fositestorage/pkce/pkce_test.go | 6 +- .../refreshtoken/refreshtoken_test.go | 10 +- 10 files changed, 122 insertions(+), 118 deletions(-) diff --git a/internal/crud/crud.go b/internal/crud/crud.go index 03aff68e8..0160bc182 100644 --- a/internal/crud/crud.go +++ b/internal/crud/crud.go @@ -200,7 +200,7 @@ func (s *secretsStorage) toSecret(signature, resourceVersion string, data JSON, labelsToAdd[SecretLabelKey] = s.resource // make it easier to find this stuff via kubectl var annotations map[string]string - if lifetime > 0 { + if lifetime > 0 && s.clock != nil { annotations = map[string]string{ SecretLifetimeAnnotationKey: s.clock().Add(lifetime).UTC().Format(SecretLifetimeAnnotationDateFormat), } diff --git a/internal/crud/crud_test.go b/internal/crud/crud_test.go index 69c8966cb..44fa81e4c 100644 --- a/internal/crud/crud_test.go +++ b/internal/crud/crud_test.go @@ -1,4 +1,4 @@ -// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package crud @@ -64,7 +64,6 @@ func TestStorage(t *testing.T) { name string resource string mocks func(*testing.T, mocker) - lifetime func() time.Duration run func(*testing.T, Storage, *clocktesting.FakeClock) error useNilClock bool wantActions []coretesting.Action @@ -123,7 +122,7 @@ func TestStorage(t *testing.T) { require.NotEmpty(t, validateSecretName(signature, false)) // signature is not valid secret name as-is data := &testJSON{Data: "create-and-get"} - rv1, err := storage.Create(ctx, signature, data, nil, nil) + rv1, err := storage.Create(ctx, signature, data, nil, nil, lifetime) require.Empty(t, rv1) // fake client does not set this require.NoError(t, err) @@ -183,14 +182,14 @@ func TestStorage(t *testing.T) { mocks: nil, run: func(t *testing.T, storage Storage, fakeClock *clocktesting.FakeClock) error { data := &testJSON{Data: "create1"} - rv1, err := storage.Create(ctx, "sig1", data, nil, nil) + rv1, err := storage.Create(ctx, "sig1", data, nil, nil, lifetime) require.Empty(t, rv1) // fake client does not set this require.NoError(t, err) fakeClock.Step(42 * time.Minute) // simulate that a known amount of time has passed data = &testJSON{Data: "create2"} - rv1, err = storage.Create(ctx, "sig2", data, nil, nil) + rv1, err = storage.Create(ctx, "sig2", data, nil, nil, lifetime) require.Empty(t, rv1) // fake client does not set this require.NoError(t, err) @@ -299,7 +298,7 @@ func TestStorage(t *testing.T) { Kind: "some-kind", Name: "some-owner", UID: "123", - }}) + }}, lifetime) require.Equal(t, "1", rv1) require.NoError(t, err) @@ -1169,15 +1168,14 @@ func TestStorage(t *testing.T) { name: "create and get with infinite lifetime when lifetime is specified as zero", resource: "access-tokens", mocks: nil, - lifetime: func() time.Duration { return 0 }, // 0 == infinity run: func(t *testing.T, storage Storage, fakeClock *clocktesting.FakeClock) error { signature := hmac.AuthorizeCodeSignature(context.Background(), authorizationCode1) require.NotEmpty(t, signature) require.NotEmpty(t, validateSecretName(signature, false)) // signature is not valid secret name as-is data := &testJSON{Data: "create-and-get"} - rv1, err := storage.Create(ctx, signature, data, nil, nil) - require.Empty(t, rv1) // fake client does not set this + rv1, err := storage.Create(ctx, signature, data, nil, nil, 0) // 0 == infinity + require.Empty(t, rv1) // fake client does not set this require.NoError(t, err) out := &testJSON{} @@ -1231,15 +1229,15 @@ func TestStorage(t *testing.T) { resource: "access-tokens", useNilClock: true, mocks: nil, - lifetime: func() time.Duration { return 0 }, // 0 == infinity run: func(t *testing.T, storage Storage, fakeClock *clocktesting.FakeClock) error { signature := hmac.AuthorizeCodeSignature(context.Background(), authorizationCode1) require.NotEmpty(t, signature) require.NotEmpty(t, validateSecretName(signature, false)) // signature is not valid secret name as-is data := &testJSON{Data: "create-and-get"} - rv1, err := storage.Create(ctx, signature, data, nil, nil) - require.Empty(t, rv1) // fake client does not set this + // TODO: Note that this test will pass with just about any value for lifetime + rv1, err := storage.Create(ctx, signature, data, nil, nil, 0) // 0 == infinity + require.Empty(t, rv1) // fake client does not set this require.NoError(t, err) out := &testJSON{} @@ -1299,10 +1297,6 @@ func TestStorage(t *testing.T) { if tt.mocks != nil { tt.mocks(t, client) } - useLifetime := lifetime - if tt.lifetime != nil { - useLifetime = tt.lifetime() - } secrets := client.CoreV1().Secrets(namespace) fakeClock := clocktesting.NewFakeClock(fakeNow) @@ -1312,7 +1306,7 @@ func TestStorage(t *testing.T) { clock = nil } - storage := New(tt.resource, secrets, clock, useLifetime) + storage := New(tt.resource, secrets, clock) err := tt.run(t, storage, fakeClock) diff --git a/internal/federationdomain/clientregistry/clientregistry_test.go b/internal/federationdomain/clientregistry/clientregistry_test.go index 4c33dfccc..b3b798137 100644 --- a/internal/federationdomain/clientregistry/clientregistry_test.go +++ b/internal/federationdomain/clientregistry/clientregistry_test.go @@ -1,4 +1,4 @@ -// Copyright 2021-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2021-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package clientregistry @@ -312,6 +312,7 @@ func requireEqualsPinnipedCLI(t *testing.T, c *Client) { "token_endpoint_auth_method": "none", "request_uris": null, "request_object_signing_alg": "", - "token_endpoint_auth_signing_alg": "RS256" + "token_endpoint_auth_signing_alg": "RS256", + "IDTokenLifetimeConfiguration": 0 }`, string(marshaled)) } diff --git a/internal/federationdomain/endpoints/token/token_handler_test.go b/internal/federationdomain/endpoints/token/token_handler_test.go index 00f845534..bfbfa37f1 100644 --- a/internal/federationdomain/endpoints/token/token_handler_test.go +++ b/internal/federationdomain/endpoints/token/token_handler_test.go @@ -4592,7 +4592,12 @@ func exchangeAuthcodeForTokens( // 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) + subject = NewHandler( + idps, + oauthHelper, + func(accessRequest fosite.AccessRequester) (bool, time.Duration) { return false, 0 }, + func(accessRequest fosite.AccessRequester) (bool, time.Duration) { return false, 0 }, + ) authorizeEndpointGrantedOpenIDScope := strings.Contains(authRequest.Form.Get("scope"), "openid") expectedNumberOfIDSessionsStored := 0 diff --git a/internal/fositestorage/accesstoken/accesstoken_test.go b/internal/fositestorage/accesstoken/accesstoken_test.go index 704c22f9b..adfb67466 100644 --- a/internal/fositestorage/accesstoken/accesstoken_test.go +++ b/internal/fositestorage/accesstoken/accesstoken_test.go @@ -1,4 +1,4 @@ -// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package accesstoken @@ -54,7 +54,7 @@ func TestAccessTokenStorage(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"id_token_claims":null,"headers":null,"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","upstreamUsername":"fake-upstream-username","upstreamGroups":["fake-upstream-group1","fake-upstream-group2"],"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"6"}`), + "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":"","IDTokenLifetimeConfiguration":0},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"id_token_claims":null,"headers":null,"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","upstreamUsername":"fake-upstream-username","upstreamGroups":["fake-upstream-group1","fake-upstream-group2"],"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"6"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/access-token", @@ -123,7 +123,7 @@ func TestAccessTokenStorageRevocation(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"id_token_claims":null,"headers":null,"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","upstreamUsername":"fake-upstream-username","upstreamGroups":["fake-upstream-group1","fake-upstream-group2"],"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"6"}`), + "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":"","IDTokenLifetimeConfiguration":0},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"id_token_claims":null,"headers":null,"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","upstreamUsername":"fake-upstream-username","upstreamGroups":["fake-upstream-group1","fake-upstream-group2"],"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"6"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/access-token", @@ -277,7 +277,7 @@ func TestCreateWithoutRequesterID(t *testing.T) { func makeTestSubject() (context.Context, *fake.Clientset, corev1client.SecretInterface, RevocationStorage) { client := fake.NewSimpleClientset() secrets := client.CoreV1().Secrets(namespace) - return context.Background(), client, secrets, New(secrets, clocktesting.NewFakeClock(fakeNow).Now, lifetime) + return context.Background(), client, secrets, New(secrets, clocktesting.NewFakeClock(fakeNow).Now, func(requester fosite.Requester) time.Duration { return lifetime }) } func TestReadFromSecret(t *testing.T) { diff --git a/internal/fositestorage/authorizationcode/authorizationcode.go b/internal/fositestorage/authorizationcode/authorizationcode.go index c71f0188b..11ad6d999 100644 --- a/internal/fositestorage/authorizationcode/authorizationcode.go +++ b/internal/fositestorage/authorizationcode/authorizationcode.go @@ -263,130 +263,134 @@ const ExpectedAuthorizeCodeSessionJSONFromFuzzing = `{ "Q7钎漡臧n栀,i" ], "request_object_signing_alg": "廜+v,淬Ʋ4Dʧ呩锏緍场脋", - "token_endpoint_auth_signing_alg": "ưƓǴ罷ǹ~]ea胠Ĺĩv絹b垇I" + "token_endpoint_auth_signing_alg": "ưƓǴ罷ǹ~]ea胠Ĺĩv絹b垇I", + "IDTokenLifetimeConfiguration":2.593156354696909e+18 }, "scopes": [ - "ĩǀŻQ'k頂箨J-a", - "ɓ啶#昏Q遐*\\髎bŸ1慂U" + "ǀŻQ'k頂箨J-", + "銈ɓ" ], "grantedScopes": [ - "ƼĮǡ鑻Z¥篚h°ʣ£ǖ%\"砬ʍ" + "#昏Q遐*\\髎bŸ1慂UFƼ", + "Oǹ冟[ǟ褾攚ŝlĆ", + "駳骪l拁乖¡J¿Ƈ妔M" ], "form": { - "¡": [ - "Ła卦牟懧¥ɂĵ", - "ɎǛƍdÚ慂+槰蚪i齥篗裢?霃谥vƘ:", - "/濔Aʉ\u003cS獾蔀OƭUǦ" + "¥": [ + "碓ɎǛƍdÚ慂+槰蚪i齥篗裢?霃谥v" ], - "民撲ʓeŘ嬀j¤囡莒汗狲N\u003cCq": [ - "5ȏ樛ȧ.mĔ櫓Ǩ療騃Ǐ}ɟ", - "潠[ĝU噤'", - "ŁȗɉY妶ǵ!ȁ" + "囡莒汗狲N": [ + "霋Ɔ輡5ȏ樛ȧ.mĔ櫓Ǩ療", + "LJ/" ], - "褰ʎɰ癟VĎĢ婄磫绒u妔隤ʑƍš駎竪": [ - "鱙翑ȲŻ麤ã桒嘞\\摗Ǘū稖咾鎅ǸÖ" + "礐jµ": [ + "A", + "Jǽȭ$奍囀Dž悷鵱民撲ʓeŘ嬀", + "行" ] }, "session": { "fosite": { "id_token_claims": { - "jti": "褗6巽ēđų蓼tùZ蛆鬣a\"ÙǞ0觢", - "iss": "j¦鲶H股ƲLŋZ-{", - "sub": "ehpƧ蓟", + "jti": "8", + "iss": "[ĝU噤'pX ʨ裄@", + "sub": "!ȁu狍ɶȳsčɦƦ诱ļ攬林Ñ", "aud": [ - "驜Ŗ~ů崧軒q腟u尿宲!" + "ƍ", + "¿o\u003e" ], - "nonce": "ǎ^嫯R忑隯ƗƋ*L\u0026", - "exp": "1989-06-02T14:40:29.613836765Z", - "iat": "2052-03-26T02:39:27.882495556Z", - "rat": "2038-04-06T10:46:24.698586972Z", - "auth_time": "2003-01-05T11:30:18.206004879Z", - "at_hash": "ğǫ\\aȊ4ț髄Al", - "acr": "曓蓳n匟鯘磹*金爃鶴滱ůĮǐ_c3#", + "nonce": "ɔ闏À1#锰劝旣樎Ȱ", + "exp": "2008-03-21T05:57:43.261171532Z", + "iat": "2080-07-31T09:39:36.259602759Z", + "rat": "2093-01-01T11:32:44.398071123Z", + "auth_time": "2088-07-12T21:20:22.8199645Z", + "at_hash": "鎅ǸÖ绝TFNJĆw宵ɚe", + "acr": "ùZ蛆鬣a\"ÙǞ0觢Û±¤ǟaȭ_Ǣ", "amr": [ - "装ƹýĸŴB岺Ð嫹Sx镯荫őł疂ư墫" + "-{5£踉4" ], - "c_hash": "\u0026鶡", + "c_hash": "5^驜Ŗ~ů崧軒q腟u尿", "ext": { - "rǓ\\BRë_g\"ʎ啴SƇMǃļū": { - "4撎胬龯,t猟i\u0026\u0026Q@ǤǟǗ": [ - 1239190737 + "ğ": 1479850437, + "ǎ^嫯R忑隯ƗƋ*L\u0026": { + "4鞀腉篓ğǫ\\aȊ4ț髄AlȒ曓蓳n匟": [ + 1260036883 ], - "飘ȱF?Ƈ畋": { - "劰û橸ɽ銐ƭ?}HƟ玈鳚": null, - "骲v0H晦XŘO溪V蔓Ȍ+~ē埅Ȝ": { - "4Ǟ": false - } - } - }, - "鑳绪": 2738428764 - } - }, - "headers": { - "extra": { - "d謺錳4帳ŅǃĊ": 663773398, - "Ř鸨EJ": { - "Ǽǟ迍阊v\"豑觳翢砜": [ - 995342744 - ], - "ȏl鐉诳DT=3骜Ǹ": { - "厷ɁOƪ穋嶿鳈恱va|载ǰɱ汶C]ɲ": null, - "荤Ý呐ʣ®DžȪǣǎǔ爣縗ɦü": { - "H :靥湤庤毩fɤȆʪ融ƆuŤn": true + "磹*金爃鶴滱ůĮǐ": { + "c3#\u0026PƢ曰l騌蘙螤": null, + "Ð嫹Sx镯荫őł": { + "鿞ČY\u0026鶡萷ɵ啜s攦Ɩ": true } } } } }, - "expires_at": { - "韁臯氃妪婝rȤ\"h丬鎒ơ娻}ɼƟ": "1970-04-27T04:31:30.902468229Z" + "headers": { + "extra": { + "Rë_g\"": 573016912, + "啴SƇMǃļū@$": { + "i\u0026\u0026Q@Ǥ": { + "ĊƑ÷Ƒ螞费": null, + "Ƈ畋rɞ?Ɵ]旎Ȳ濡胉室癑勦e": { + "9ǍȬ劘$iA砳_": true + } + }, + "胬龯,t": [ + 1355041984 + ] + } + } }, - "username": "髉龳ǽÙ", - "subject": "\u0026¥潝邎Ȗ莅ŝǔ盕戙鵮碡ʯiŬŽ" + "expires_at": { + "埅ȜʁɁ;Bd謺錳4帳Ņ": "1982-04-18T19:26:28.008651843Z", + "碼Ǫ": "2028-05-31T03:22:30.23394531Z" + }, + "username": "鋖颤ōɓɡ Ǽǟ迍阊v\"豑觳翢砜", + "subject": "ɆƊ#XɗD愌铵ĸYų厷ɁOƪ" }, "custom": { - "username": "Ĝ眧Ĭ", - "upstreamUsername": "ʼn2ƋŢ觛ǂ焺nŐǛ", + "username": "嶿鳈恱va|载ǰɱ汶C]ɲ'=ĸ", + "upstreamUsername": "ʣ®DžȪǣǎǔ爣縗ɦüHêQ仏1őƖ2", "upstreamGroups": [ - "闣ʬ橳(ý綃ʃʚƟ覣k眐4Ĉt", - "ʃƸ澺淗a紽ǒ|鰽ŋ猊Ia瓕巈環_ɑ" + "Ȇ", + "ǞʜƢú4¶鎰" ], - "providerUID": "ƴŤȱʀļÂ?墖", - "providerName": "7就伒犘c钡", - "providerType": "k|鬌R蜚蠣麹概÷驣7Ʀ澉1æɽ誮", + "providerUID": "韁臯氃妪婝rȤ\"h丬鎒ơ娻}ɼƟ", + "providerName": "闺髉龳ǽÙ龦O亾EW莛8嘶×", + "providerType": "戙鵮碡ʯiŬŽ非Ĝ眧Ĭ葜SŦ", "warnings": [ - "鷞aŚB碠k9帴ʘ赱", - "ď逳鞪?3)藵睋邔\u0026Ű惫蜀Ģ¡圔" + "觛ǂ焺nŐǛ3}Ü#", + "(ý綃ʃʚƟ覣k眐4ĈtC嵽痊w©" ], "oidc": { - "upstreamRefreshToken": "墀jMʥ", - "upstreamAccessToken": "+î艔垎0", - "upstreamSubject": "ĝ", - "upstreamIssuer": "ǢIȽ" + "upstreamRefreshToken": "榨Q|ôɵt毇", + "upstreamAccessToken": "瓕巈", + "upstreamSubject": "鉢緋uƴŤȱʀļÂ?", + "upstreamIssuer": "27就伒犘c钡ɏȫ" }, "ldap": { - "userDN": "士b", + "userDN": "š%OpKȱ藚ɏ¬Ê蒭堜", "extraRefreshAttributes": { - "O灞浛a齙\\蹼偦歛ơ 皦pSǬŝ": "Džķ?吭匞饫Ƽĝ\"zvư", - "f跞@)¿,ɭS隑ip偶宾儮猷": "面@yȝƋ鬯犦獢9c5¤" + "1飞": "笿0D餹", + "誮rʨ鷞aŚB碠k9帴ʘ赱ŕ瑹xȢ~": ")藵睋邔\u0026Ű惫蜀Ģ¡圔鎥墀" } }, "activedirectory": { - "userDN": "置b", + "userDN": "êĝ", "extraRefreshAttributes": { - "MN\u0026錝D肁Ŷɽ蔒PR}Ųʓl{鼐": "$+溪ŸȢŒų崓ļ憽", - "ĩŦʀ宍D挟": "q萮左/篣AÚƄŕ~čfVLPC諡}", - "姧骦:駝重EȫʆɵʮGɃ": "囤1+,Ȳ齠@ɍB鳛Nč乿ƔǴę鏶" + "IȽ齤士bEǎ": "跞@)¿,ɭS隑ip偶宾儮猷V麹", + "ȝƋ鬯犦獢9c5¤.岵": "浛a齙\\蹼偦歛" } } } }, "requestedAudience": [ - "ň" + " 皦pSǬŝ社Vƅȭǝ*擦28Dž", + "vư" ], "grantedAudience": [ - "â融貵捠ʼn", - "d鞕ȸ腿tʏƲ%}ſ¯Ɣ 籌Tǘ乚Ȥ2" + "置b", + "筫MN\u0026錝D肁Ŷɽ蔒PR}Ųʓl{" ] }, "version": "6" diff --git a/internal/fositestorage/authorizationcode/authorizationcode_test.go b/internal/fositestorage/authorizationcode/authorizationcode_test.go index 516c69dc1..c43f32bfc 100644 --- a/internal/fositestorage/authorizationcode/authorizationcode_test.go +++ b/internal/fositestorage/authorizationcode/authorizationcode_test.go @@ -1,4 +1,4 @@ -// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package authorizationcode @@ -66,7 +66,7 @@ func TestAuthorizationCodeStorage(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"active":true,"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"id_token_claims":null,"headers":null,"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","upstreamUsername":"fake-upstream-username","upstreamGroups":["fake-upstream-group1","fake-upstream-group2"],"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"6"}`), + "pinniped-storage-data": []byte(`{"active":true,"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":"","IDTokenLifetimeConfiguration":0},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"id_token_claims":null,"headers":null,"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","upstreamUsername":"fake-upstream-username","upstreamGroups":["fake-upstream-group1","fake-upstream-group2"],"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"6"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/authcode", @@ -86,7 +86,7 @@ func TestAuthorizationCodeStorage(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"active":false,"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"id_token_claims":null,"headers":null,"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","upstreamUsername":"fake-upstream-username","upstreamGroups":["fake-upstream-group1","fake-upstream-group2"],"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"6"}`), + "pinniped-storage-data": []byte(`{"active":false,"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":"","IDTokenLifetimeConfiguration":0},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"id_token_claims":null,"headers":null,"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","upstreamUsername":"fake-upstream-username","upstreamGroups":["fake-upstream-group1","fake-upstream-group2"],"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"6"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/authcode", @@ -260,7 +260,7 @@ func TestCreateWithWrongRequesterDataTypes(t *testing.T) { func makeTestSubject() (context.Context, *fake.Clientset, corev1client.SecretInterface, oauth2.AuthorizeCodeStorage) { client := fake.NewSimpleClientset() secrets := client.CoreV1().Secrets(namespace) - return context.Background(), client, secrets, New(secrets, clocktesting.NewFakeClock(fakeNow).Now, lifetime) + return context.Background(), client, secrets, New(secrets, clocktesting.NewFakeClock(fakeNow).Now, func(requester fosite.Requester) time.Duration { return lifetime }) } // TestFuzzAndJSONNewValidEmptyAuthorizeCodeSession asserts that we can correctly round trip our authorize code session. @@ -366,7 +366,7 @@ func TestFuzzAndJSONNewValidEmptyAuthorizeCodeSession(t *testing.T) { const name = "fuzz" // value is irrelevant ctx := context.Background() secrets := fake.NewSimpleClientset().CoreV1().Secrets(name) - storage := New(secrets, func() time.Time { return fakeNow }, lifetime) + storage := New(secrets, func() time.Time { return fakeNow }, func(requester fosite.Requester) time.Duration { return lifetime }) // issue a create using the fuzzed request to confirm that marshalling works err = storage.CreateAuthorizeCodeSession(ctx, name, validSession.Request) diff --git a/internal/fositestorage/openidconnect/openidconnect_test.go b/internal/fositestorage/openidconnect/openidconnect_test.go index 663feef9d..882eff9ba 100644 --- a/internal/fositestorage/openidconnect/openidconnect_test.go +++ b/internal/fositestorage/openidconnect/openidconnect_test.go @@ -52,7 +52,7 @@ func TestOpenIdConnectStorage(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"id_token_claims":null,"headers":null,"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","upstreamUsername":"fake-upstream-username","upstreamGroups":["fake-upstream-group1","fake-upstream-group2"],"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"6"}`), + "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":"","IDTokenLifetimeConfiguration":0},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"id_token_claims":null,"headers":null,"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","upstreamUsername":"fake-upstream-username","upstreamGroups":["fake-upstream-group1","fake-upstream-group2"],"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"6"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/oidc", @@ -200,5 +200,5 @@ func TestAuthcodeHasNoDot(t *testing.T) { func makeTestSubject() (context.Context, *fake.Clientset, corev1client.SecretInterface, openid.OpenIDConnectRequestStorage) { client := fake.NewSimpleClientset() secrets := client.CoreV1().Secrets(namespace) - return context.Background(), client, secrets, New(secrets, clocktesting.NewFakeClock(fakeNow).Now, lifetime) + return context.Background(), client, secrets, New(secrets, clocktesting.NewFakeClock(fakeNow).Now, func(requester fosite.Requester) time.Duration { return lifetime }) } diff --git a/internal/fositestorage/pkce/pkce_test.go b/internal/fositestorage/pkce/pkce_test.go index f12693bc9..2f1003951 100644 --- a/internal/fositestorage/pkce/pkce_test.go +++ b/internal/fositestorage/pkce/pkce_test.go @@ -1,4 +1,4 @@ -// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package pkce @@ -52,7 +52,7 @@ func TestPKCEStorage(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"id_token_claims":null,"headers":null,"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","upstreamUsername":"fake-upstream-username","upstreamGroups":["fake-upstream-group1","fake-upstream-group2"],"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"6"}`), + "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":"","IDTokenLifetimeConfiguration":0},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"id_token_claims":null,"headers":null,"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","upstreamUsername":"fake-upstream-username","upstreamGroups":["fake-upstream-group1","fake-upstream-group2"],"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"6"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/pkce", @@ -199,5 +199,5 @@ func TestCreateWithWrongRequesterDataTypes(t *testing.T) { func makeTestSubject() (context.Context, *fake.Clientset, corev1client.SecretInterface, pkce.PKCERequestStorage) { client := fake.NewSimpleClientset() secrets := client.CoreV1().Secrets(namespace) - return context.Background(), client, secrets, New(secrets, clocktesting.NewFakeClock(fakeNow).Now, lifetime) + return context.Background(), client, secrets, New(secrets, clocktesting.NewFakeClock(fakeNow).Now, func(requester fosite.Requester) time.Duration { return lifetime }) } diff --git a/internal/fositestorage/refreshtoken/refreshtoken_test.go b/internal/fositestorage/refreshtoken/refreshtoken_test.go index 254f42458..f2e3ac0e9 100644 --- a/internal/fositestorage/refreshtoken/refreshtoken_test.go +++ b/internal/fositestorage/refreshtoken/refreshtoken_test.go @@ -1,4 +1,4 @@ -// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package refreshtoken @@ -53,7 +53,7 @@ func TestRefreshTokenStorage(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"id_token_claims":null,"headers":null,"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","upstreamUsername":"fake-upstream-username","upstreamGroups":["fake-upstream-group1","fake-upstream-group2"],"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"6"}`), + "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":"","IDTokenLifetimeConfiguration":0},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"id_token_claims":null,"headers":null,"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","upstreamUsername":"fake-upstream-username","upstreamGroups":["fake-upstream-group1","fake-upstream-group2"],"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"6"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/refresh-token", @@ -123,7 +123,7 @@ func TestRefreshTokenStorageRevocation(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"id_token_claims":null,"headers":null,"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","upstreamUsername":"fake-upstream-username","upstreamGroups":["fake-upstream-group1","fake-upstream-group2"],"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"6"}`), + "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":"","IDTokenLifetimeConfiguration":0},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"id_token_claims":null,"headers":null,"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","upstreamUsername":"fake-upstream-username","upstreamGroups":["fake-upstream-group1","fake-upstream-group2"],"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"6"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/refresh-token", @@ -178,7 +178,7 @@ func TestRefreshTokenStorageRevokeRefreshTokenMaybeGracePeriod(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"id_token_claims":null,"headers":null,"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","upstreamUsername":"fake-upstream-username","upstreamGroups":["fake-upstream-group1","fake-upstream-group2"],"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"6"}`), + "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":"","IDTokenLifetimeConfiguration":0},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"id_token_claims":null,"headers":null,"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","upstreamUsername":"fake-upstream-username","upstreamGroups":["fake-upstream-group1","fake-upstream-group2"],"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"6"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/refresh-token", @@ -333,7 +333,7 @@ func TestCreateWithoutRequesterID(t *testing.T) { func makeTestSubject() (context.Context, *fake.Clientset, corev1client.SecretInterface, RevocationStorage) { client := fake.NewSimpleClientset() secrets := client.CoreV1().Secrets(namespace) - return context.Background(), client, secrets, New(secrets, clocktesting.NewFakeClock(fakeNow).Now, lifetime) + return context.Background(), client, secrets, New(secrets, clocktesting.NewFakeClock(fakeNow).Now, func(requester fosite.Requester) time.Duration { return lifetime }) } func TestReadFromSecret(t *testing.T) { From b31a893caf4ecda9e82b47dc655474630d475ee5 Mon Sep 17 00:00:00 2001 From: Joshua Casey Date: Thu, 28 Mar 2024 10:33:53 -0500 Subject: [PATCH 3/8] Add integration test and fix totalExpectedAPIFields --- test/integration/kube_api_discovery_test.go | 2 +- test/integration/supervisor_login_test.go | 84 +++++++++++++++++++-- test/integration/supervisor_storage_test.go | 8 +- 3 files changed, 85 insertions(+), 9 deletions(-) diff --git a/test/integration/kube_api_discovery_test.go b/test/integration/kube_api_discovery_test.go index 327c31a45..dbf9e2155 100644 --- a/test/integration/kube_api_discovery_test.go +++ b/test/integration/kube_api_discovery_test.go @@ -438,7 +438,7 @@ func TestGetAPIResourceList(t *testing.T) { //nolint:gocyclo // each t.Run is pr } // manually update this value whenever you add additional fields to an API resource and then run the generator - totalExpectedAPIFields := 261 + totalExpectedAPIFields := 263 // Because we are parsing text from `kubectl explain` and because the format of that text can change // over time, make a rudimentary assertion that this test exercised the whole tree of all fields of all diff --git a/test/integration/supervisor_login_test.go b/test/integration/supervisor_login_test.go index dd373ad45..5443acac3 100644 --- a/test/integration/supervisor_login_test.go +++ b/test/integration/supervisor_login_test.go @@ -281,6 +281,10 @@ func TestSupervisorLogin_Browser(t *testing.T) { // The expected ID token additional claims, which will be nested under claim "additionalClaims", // for the original ID token and the refreshed ID token. wantDownstreamIDTokenAdditionalClaims map[string]interface{} + // The expected ID token lifetime, as calculated by token claim 'exp' subtracting token claim 'iat'. + // ID tokens issued through a token refresh should have the configured lifetime (or default if not configured). + // ID tokens issued through a token exchange should have the default lifetime. + wantDownstreamIDTokenLifetime *time.Duration // Want the authorization endpoint to redirect to the callback with this error type. // The rest of the flow will be skipped since the initial authorization failed. @@ -1423,12 +1427,16 @@ func TestSupervisorLogin_Browser(t *testing.T) { AllowedRedirectURIs: []configv1alpha1.RedirectURI{configv1alpha1.RedirectURI(callbackURL)}, AllowedGrantTypes: []configv1alpha1.GrantType{"authorization_code", "urn:ietf:params:oauth:grant-type:token-exchange", "refresh_token"}, AllowedScopes: []configv1alpha1.Scope{"openid", "offline_access", "pinniped:request-audience", "username", "groups"}, + TokenLifetimes: configv1alpha1.OIDCClientTokenLifetimes{ + IDTokenSeconds: ptr.To[int32](1234), + }, }, configv1alpha1.OIDCClientPhaseReady) }, requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowOIDC, wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamOIDC, wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Username) + "$" }, wantDownstreamIDTokenGroups: env.SupervisorUpstreamOIDC.ExpectedGroups, + wantDownstreamIDTokenLifetime: ptr.To(1234 * time.Second), }, { name: "oidc upstream with downstream dynamic client happy path, requesting all scopes, using the IDP chooser page", @@ -2162,6 +2170,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { tt.wantDownstreamIDTokenUsernameToMatch, tt.wantDownstreamIDTokenGroups, tt.wantDownstreamIDTokenAdditionalClaims, + tt.wantDownstreamIDTokenLifetime, tt.wantAuthorizationErrorType, tt.wantAuthorizationErrorDescription, tt.wantAuthcodeExchangeError, @@ -2317,6 +2326,7 @@ func testSupervisorLogin( wantDownstreamIDTokenUsernameToMatch func(username string) string, wantDownstreamIDTokenGroups []string, wantDownstreamIDTokenAdditionalClaims map[string]interface{}, + wantDownstreamIDTokenLifetime *time.Duration, wantAuthorizationErrorType string, wantAuthorizationErrorDescription string, wantAuthcodeExchangeError string, @@ -2402,7 +2412,7 @@ func testSupervisorLogin( configv1alpha1.FederationDomainPhaseReady, ) - // Ensure the the JWKS data is created and ready for the new FederationDomain by waiting for + // Ensure that the JWKS data is created and ready for the new FederationDomain by waiting for // the `/jwks.json` endpoint to succeed, because there is no point in proceeding and eventually // calling the token endpoint from this test until the JWKS data has been loaded into // the server's in-memory JWKS cache for the token endpoint to use. @@ -2545,6 +2555,11 @@ func testSupervisorLogin( if len(wantDownstreamIDTokenAdditionalClaims) > 0 { expectedIDTokenClaims = append(expectedIDTokenClaims, "additionalClaims") } + defaultIDTokenLifetime := oidc.DefaultOIDCTimeoutsConfiguration().IDTokenLifespan + if wantDownstreamIDTokenLifetime == nil { + wantDownstreamIDTokenLifetime = ptr.To(defaultIDTokenLifetime) + } + initialIDTokenClaims := verifyTokenResponse( t, tokenResponse, @@ -2556,13 +2571,24 @@ func testSupervisorLogin( wantDownstreamIDTokenUsernameToMatch(username), wantDownstreamIDTokenGroups, wantDownstreamIDTokenAdditionalClaims, + *wantDownstreamIDTokenLifetime, ) // token exchange on the original token if requestTokenExchangeAud == "" { requestTokenExchangeAud = "some-cluster-123" // use a default test value } - doTokenExchange(t, requestTokenExchangeAud, &downstreamOAuth2Config, tokenResponse, httpClient, discovery, wantTokenExchangeResponse, initialIDTokenClaims) + doTokenExchange( + t, + requestTokenExchangeAud, + &downstreamOAuth2Config, + tokenResponse, + httpClient, + discovery, + wantTokenExchangeResponse, + initialIDTokenClaims, + defaultIDTokenLifetime, + ) wantRefreshedGroups := wantDownstreamIDTokenGroups if editRefreshSessionDataWithoutBreaking != nil { @@ -2616,6 +2642,7 @@ func testSupervisorLogin( wantDownstreamIDTokenUsernameToMatch(username), wantRefreshedGroups, wantDownstreamIDTokenAdditionalClaims, + *wantDownstreamIDTokenLifetime, ) require.NotEqual(t, tokenResponse.AccessToken, refreshedTokenResponse.AccessToken) @@ -2623,7 +2650,17 @@ func testSupervisorLogin( require.NotEqual(t, tokenResponse.Extra("id_token"), refreshedTokenResponse.Extra("id_token")) // token exchange on the refreshed token - doTokenExchange(t, requestTokenExchangeAud, &downstreamOAuth2Config, refreshedTokenResponse, httpClient, discovery, wantTokenExchangeResponse, refreshedIDTokenClaims) + doTokenExchange( + t, + requestTokenExchangeAud, + &downstreamOAuth2Config, + refreshedTokenResponse, + httpClient, + discovery, + wantTokenExchangeResponse, + refreshedIDTokenClaims, + defaultIDTokenLifetime, + ) // Now that we have successfully performed a refresh, let's test what happens when an // upstream refresh fails during the next downstream refresh. @@ -2675,6 +2712,7 @@ func verifyTokenResponse( wantDownstreamIDTokenSubjectToMatch, wantDownstreamIDTokenUsernameToMatch string, wantDownstreamIDTokenGroups []string, wantDownstreamIDTokenAdditionalClaims map[string]interface{}, + wantDownstreamIDTokenLifetime time.Duration, ) map[string]interface{} { ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() @@ -2693,8 +2731,7 @@ func verifyTokenResponse( require.NoError(t, nonceParam.Validate(idToken)) // Check the exp claim of the ID token. - expectedIDTokenLifetime := oidc.DefaultOIDCTimeoutsConfiguration().IDTokenLifespan - testutil.RequireTimeInDelta(t, time.Now().UTC().Add(expectedIDTokenLifetime), idToken.Expiry, time.Second*30) + testutil.RequireTimeInDelta(t, time.Now().UTC().Add(wantDownstreamIDTokenLifetime), idToken.Expiry, time.Second*30) // Check the full list of claim names of the ID token. idTokenClaims := map[string]interface{}{} @@ -2726,6 +2763,19 @@ func verifyTokenResponse( require.NotContains(t, idTokenClaims, "additionalClaims", "additionalClaims claim should not be present when no sub claims are expected") } + // Check the token lifetime by calculating the delta between "exp" and "iat" + iat := getFloat64Claim(t, idTokenClaims, "iat") + exp := getFloat64Claim(t, idTokenClaims, "exp") + require.InDeltaf( + t, + wantDownstreamIDTokenLifetime.Seconds(), + exp-iat, + 10.0, + "actual token lifetime (%f) must be within 10 seconds of expectation (%f)", + exp-iat, + wantDownstreamIDTokenLifetime.Seconds(), + ) + // Some light verification of the other tokens that were returned. require.NotEmpty(t, tokenResponse.AccessToken) require.Equal(t, "bearer", tokenResponse.TokenType) @@ -2747,6 +2797,16 @@ func verifyTokenResponse( return idTokenClaims } +func getFloat64Claim(t *testing.T, claims map[string]interface{}, claim string) float64 { + t.Helper() + + v, ok := claims[claim] + require.True(t, ok, "claim %s must be present", claim) + f, ok := v.(float64) + require.True(t, ok, "claim %s must be a float64", claim) + return f +} + func hashAccessToken(accessToken string) string { // See https://openid.net/specs/openid-connect-core-1_0.html#CodeIDToken. // "Access Token hash value. Its value is the base64url encoding of the left-most half of @@ -3023,6 +3083,7 @@ func doTokenExchange( provider *coreosoidc.Provider, wantTokenExchangeResponse func(t *testing.T, status int, body string), previousIDTokenClaims map[string]interface{}, + wantIDTokenLifetime time.Duration, ) { ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() @@ -3089,6 +3150,19 @@ func doTokenExchange( require.Contains(t, claims, "exp") // expires at require.Contains(t, claims, "jti") // JWT ID + // Check the token lifetime by calculating the delta between "exp" and "iat" + iat := getFloat64Claim(t, claims, "iat") + exp := getFloat64Claim(t, claims, "exp") + require.InDeltaf( + t, + wantIDTokenLifetime.Seconds(), + exp-iat, + 10.0, + "actual token lifetime (%f) must be within 10 seconds of expectation (%f)", + exp-iat, + wantIDTokenLifetime.Seconds(), + ) + // The original client ID should be preserved in the azp claim, therefore preserving this information // about the original source of the authorization for tracing/auditing purposes, since the "aud" claim // has been updated to have a new value. diff --git a/test/integration/supervisor_storage_test.go b/test/integration/supervisor_storage_test.go index 2bfd91928..b0826e035 100644 --- a/test/integration/supervisor_storage_test.go +++ b/test/integration/supervisor_storage_test.go @@ -1,4 +1,4 @@ -// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package integration @@ -52,7 +52,9 @@ func TestAuthorizeCodeStorage(t *testing.T) { defer cancel() sessionStorageLifetime := 5 * time.Minute - storage := authorizationcode.New(secrets, time.Now, sessionStorageLifetime) + storage := authorizationcode.New(secrets, time.Now, func(requester fosite.Requester) time.Duration { + return sessionStorageLifetime + }) // the session for this signature should not exist yet notFoundRequest, err := storage.GetAuthorizeCodeSession(ctx, signature, nil) @@ -129,7 +131,7 @@ func TestAuthorizeCodeStorage(t *testing.T) { require.Error(t, err) require.True(t, stderrors.Is(err, fosite.ErrInvalidatedAuthorizeCode)) - // the data stored in Kube should be exactly the same but it should be marked as used + // the data stored in Kube should be exactly the same, but it should be marked as used invalidatedSecret, err := secrets.Get(ctx, name, metav1.GetOptions{}) require.NoError(t, err) // InvalidateAuthorizeCodeSession() sets Active to false, so update the expected value accordingly. From af9612e98e9b6ddb65d5924f7195aca18e1e2e66 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Tue, 16 Apr 2024 14:55:13 -0700 Subject: [PATCH 4/8] Update more unit tests for configurable token lifetimes --- internal/crud/crud.go | 2 +- internal/crud/crud_test.go | 1 - .../clientregistry/clientregistry_test.go | 102 ++++++++++++--- .../idtokenlifespan/idtoken_lifespan_test.go | 77 ++++++++++++ internal/federationdomain/oidc/oidc_test.go | 117 ++++++++++++++++++ .../accesstoken/accesstoken_test.go | 39 ++++-- .../authorizationcode_test.go | 41 ++++-- .../openidconnect/openidconnect_test.go | 37 ++++-- internal/fositestorage/pkce/pkce_test.go | 34 +++-- .../refreshtoken/refreshtoken_test.go | 41 ++++-- 10 files changed, 416 insertions(+), 75 deletions(-) create mode 100644 internal/federationdomain/idtokenlifespan/idtoken_lifespan_test.go create mode 100644 internal/federationdomain/oidc/oidc_test.go diff --git a/internal/crud/crud.go b/internal/crud/crud.go index 0160bc182..03aff68e8 100644 --- a/internal/crud/crud.go +++ b/internal/crud/crud.go @@ -200,7 +200,7 @@ func (s *secretsStorage) toSecret(signature, resourceVersion string, data JSON, labelsToAdd[SecretLabelKey] = s.resource // make it easier to find this stuff via kubectl var annotations map[string]string - if lifetime > 0 && s.clock != nil { + if lifetime > 0 { annotations = map[string]string{ SecretLifetimeAnnotationKey: s.clock().Add(lifetime).UTC().Format(SecretLifetimeAnnotationDateFormat), } diff --git a/internal/crud/crud_test.go b/internal/crud/crud_test.go index 44fa81e4c..ea9d8c369 100644 --- a/internal/crud/crud_test.go +++ b/internal/crud/crud_test.go @@ -1235,7 +1235,6 @@ func TestStorage(t *testing.T) { require.NotEmpty(t, validateSecretName(signature, false)) // signature is not valid secret name as-is data := &testJSON{Data: "create-and-get"} - // TODO: Note that this test will pass with just about any value for lifetime rv1, err := storage.Create(ctx, signature, data, nil, nil, 0) // 0 == infinity require.Empty(t, rv1) // fake client does not set this require.NoError(t, err) diff --git a/internal/federationdomain/clientregistry/clientregistry_test.go b/internal/federationdomain/clientregistry/clientregistry_test.go index b3b798137..5f800ff9f 100644 --- a/internal/federationdomain/clientregistry/clientregistry_test.go +++ b/internal/federationdomain/clientregistry/clientregistry_test.go @@ -18,6 +18,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/kubernetes/fake" coretesting "k8s.io/client-go/testing" + "k8s.io/utils/ptr" configv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1" supervisorfake "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned/fake" @@ -180,7 +181,7 @@ func TestClientManager(t *testing.T) { }, }, { - name: "find a valid dynamic client", + name: "find a valid dynamic client without an ID token lifetime configuration", oidcClients: []*configv1alpha1.OIDCClient{ { ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID}, @@ -188,6 +189,7 @@ func TestClientManager(t *testing.T) { AllowedGrantTypes: []configv1alpha1.GrantType{"authorization_code", "urn:ietf:params:oauth:grant-type:token-exchange", "refresh_token"}, AllowedScopes: []configv1alpha1.Scope{"openid", "offline_access", "pinniped:request-audience", "username", "groups"}, AllowedRedirectURIs: []configv1alpha1.RedirectURI{"http://localhost:80", "https://foobar.com/callback"}, + TokenLifetimes: configv1alpha1.OIDCClientTokenLifetimes{IDTokenSeconds: nil}, }, }, { @@ -203,24 +205,49 @@ func TestClientManager(t *testing.T) { require.IsType(t, &Client{}, got) c := got.(*Client) - require.Equal(t, testName, c.GetID()) - require.Nil(t, c.GetHashedSecret()) - require.Len(t, c.GetRotatedHashes(), 2) - require.Equal(t, testutil.HashedPassword1AtSupervisorMinCost, string(c.GetRotatedHashes()[0])) - require.Equal(t, testutil.HashedPassword2AtSupervisorMinCost, string(c.GetRotatedHashes()[1])) - require.Equal(t, []string{"http://localhost:80", "https://foobar.com/callback"}, c.GetRedirectURIs()) - require.Equal(t, fosite.Arguments{"authorization_code", "urn:ietf:params:oauth:grant-type:token-exchange", "refresh_token"}, c.GetGrantTypes()) - require.Equal(t, fosite.Arguments{"code"}, c.GetResponseTypes()) - require.Equal(t, fosite.Arguments{"openid", "offline_access", "pinniped:request-audience", "username", "groups"}, c.GetScopes()) - require.False(t, c.IsPublic()) - require.Nil(t, c.GetAudience()) - require.Nil(t, c.GetRequestURIs()) - require.Nil(t, c.GetJSONWebKeys()) - require.Equal(t, "", c.GetJSONWebKeysURI()) - require.Equal(t, "", c.GetRequestObjectSigningAlgorithm()) - require.Equal(t, "client_secret_basic", c.GetTokenEndpointAuthMethod()) - require.Equal(t, "RS256", c.GetTokenEndpointAuthSigningAlgorithm()) - require.Equal(t, []fosite.ResponseModeType{"", "query"}, c.GetResponseModes()) + requireDynamicOIDCClient(t, c, + testName, + []string{testutil.HashedPassword1AtSupervisorMinCost, testutil.HashedPassword2AtSupervisorMinCost}, + fosite.Arguments{"authorization_code", "urn:ietf:params:oauth:grant-type:token-exchange", "refresh_token"}, + fosite.Arguments{"openid", "offline_access", "pinniped:request-audience", "username", "groups"}, + []string{"http://localhost:80", "https://foobar.com/callback"}, + 0*time.Second, + ) + }, + }, + { + name: "find a valid dynamic client with an ID token lifetime configuration", + oidcClients: []*configv1alpha1.OIDCClient{ + { + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID}, + Spec: configv1alpha1.OIDCClientSpec{ + AllowedGrantTypes: []configv1alpha1.GrantType{"authorization_code", "refresh_token"}, + AllowedScopes: []configv1alpha1.Scope{"openid", "offline_access", "username", "groups"}, + AllowedRedirectURIs: []configv1alpha1.RedirectURI{"http://localhost:8080"}, + TokenLifetimes: configv1alpha1.OIDCClientTokenLifetimes{IDTokenSeconds: ptr.To[int32]((4242))}, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: "other-client", Generation: 1234, UID: testUID}, + }, + }, + secrets: []*corev1.Secret{ + testutil.OIDCClientSecretStorageSecretForUID(t, testNamespace, testUID, []string{testutil.HashedPassword2AtSupervisorMinCost, testutil.HashedPassword1AtSupervisorMinCost}), + }, + run: func(t *testing.T, subject *ClientManager) { + got, err := subject.GetClient(ctx, testName) + require.NoError(t, err) + require.IsType(t, &Client{}, got) + c := got.(*Client) + + requireDynamicOIDCClient(t, c, + testName, + []string{testutil.HashedPassword2AtSupervisorMinCost, testutil.HashedPassword1AtSupervisorMinCost}, + fosite.Arguments{"authorization_code", "refresh_token"}, + fosite.Arguments{"openid", "offline_access", "username", "groups"}, + []string{"http://localhost:8080"}, + 4242*time.Second, + ) }, }, } @@ -279,6 +306,7 @@ func requireEqualsPinnipedCLI(t *testing.T, c *Client) { require.Equal(t, "none", c.GetTokenEndpointAuthMethod()) require.Equal(t, "RS256", c.GetTokenEndpointAuthSigningAlgorithm()) require.Equal(t, []fosite.ResponseModeType{"", "query", "form_post"}, c.GetResponseModes()) + require.Equal(t, 0*time.Second, c.IDTokenLifetimeConfiguration) marshaled, err := json.Marshal(c) require.NoError(t, err) @@ -316,3 +344,39 @@ func requireEqualsPinnipedCLI(t *testing.T, c *Client) { "IDTokenLifetimeConfiguration": 0 }`, string(marshaled)) } + +func requireDynamicOIDCClient( + t *testing.T, + c *Client, + wantClientID string, + wantHashes []string, + wantGrantTypes fosite.Arguments, + wantScopes fosite.Arguments, + wantRedirectURIs []string, + wantIDTokenLifetimeConfiguration time.Duration, +) { + require.Equal(t, wantClientID, c.GetID()) + + require.Len(t, c.GetRotatedHashes(), len(wantHashes)) + for i := range wantHashes { + require.Equal(t, wantHashes[i], string(c.GetRotatedHashes()[i])) + } + + require.Equal(t, wantRedirectURIs, c.GetRedirectURIs()) + require.Equal(t, wantGrantTypes, c.GetGrantTypes()) + require.Equal(t, wantScopes, c.GetScopes()) + require.Equal(t, wantIDTokenLifetimeConfiguration, c.IDTokenLifetimeConfiguration) + + // The following are always the same for all OIDCClients. + require.Nil(t, c.GetHashedSecret()) + require.Equal(t, fosite.Arguments{"code"}, c.GetResponseTypes()) + require.False(t, c.IsPublic()) + require.Nil(t, c.GetAudience()) + require.Nil(t, c.GetRequestURIs()) + require.Nil(t, c.GetJSONWebKeys()) + require.Equal(t, "", c.GetJSONWebKeysURI()) + require.Equal(t, "", c.GetRequestObjectSigningAlgorithm()) + require.Equal(t, "client_secret_basic", c.GetTokenEndpointAuthMethod()) + require.Equal(t, "RS256", c.GetTokenEndpointAuthSigningAlgorithm()) + require.Equal(t, []fosite.ResponseModeType{"", "query"}, c.GetResponseModes()) +} diff --git a/internal/federationdomain/idtokenlifespan/idtoken_lifespan_test.go b/internal/federationdomain/idtokenlifespan/idtoken_lifespan_test.go new file mode 100644 index 000000000..0501525a0 --- /dev/null +++ b/internal/federationdomain/idtokenlifespan/idtoken_lifespan_test.go @@ -0,0 +1,77 @@ +// Copyright 2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package idtokenlifespan + +import ( + "context" + "testing" + "time" + + "github.com/ory/fosite" + "github.com/stretchr/testify/require" +) + +func TestOverrideIDTokenLifespanInContext(t *testing.T) { + tests := []struct { + name string + defaultLifespan time.Duration + overrideLifespan func(ctx context.Context) context.Context + wantLifespan time.Duration + }{ + { + name: "does not override the context's default timeout", + defaultLifespan: 10 * time.Second, + overrideLifespan: func(baseCtx context.Context) context.Context { + return baseCtx // no-op on the context + }, + wantLifespan: 10 * time.Second, + }, + { + name: "overrides the context's default to be 42 seconds", + defaultLifespan: 10 * time.Second, + overrideLifespan: func(baseCtx context.Context) context.Context { + return OverrideIDTokenLifespanInContext(baseCtx, 42*time.Second) + }, + wantLifespan: 42 * time.Second, + }, + { + name: "overrides the context's default to be 42 minutes", + defaultLifespan: 10 * time.Second, + overrideLifespan: func(baseCtx context.Context) context.Context { + return OverrideIDTokenLifespanInContext(baseCtx, 42*time.Minute) + }, + wantLifespan: 42 * time.Minute, + }, + { + name: "somehow accidentally overrides the context's default timeout to be the wrong type", + defaultLifespan: 10 * time.Second, + overrideLifespan: func(baseCtx context.Context) context.Context { + return context.WithValue(baseCtx, idTokenLifetimeOverrideKey, "this should be a duration but is a string") + }, + wantLifespan: 10 * time.Second, // should ignore the illegal value and just return the default + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + baseConfig := fosite.Config{ + IDTokenLifespan: tt.defaultLifespan, + } + + contextAwareProvider := &contextAwareIDTokenLifespanProvider{ + DelegateConfig: &baseConfig, + } + + // Possibly override the default lifespan on the context. + updatedCtx := tt.overrideLifespan(context.Background()) + + // Read the lifespan from the context. + gotLifespan := contextAwareProvider.GetIDTokenLifespan(updatedCtx) + require.Equal(t, tt.wantLifespan, gotLifespan) + }) + } +} diff --git a/internal/federationdomain/oidc/oidc_test.go b/internal/federationdomain/oidc/oidc_test.go new file mode 100644 index 000000000..bf9c30890 --- /dev/null +++ b/internal/federationdomain/oidc/oidc_test.go @@ -0,0 +1,117 @@ +// Copyright 2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package oidc + +import ( + "testing" + "time" + + "github.com/ory/fosite" + "github.com/stretchr/testify/require" + + "go.pinniped.dev/internal/federationdomain/clientregistry" +) + +func TestDefaultLifespans(t *testing.T) { + c := DefaultOIDCTimeoutsConfiguration() + + require.Equal(t, 90*time.Minute, c.UpstreamStateParamLifespan) + require.Equal(t, 10*time.Minute, c.AuthorizeCodeLifespan) + require.Equal(t, 2*time.Minute, c.AccessTokenLifespan) + require.Equal(t, 2*time.Minute, c.IDTokenLifespan) + require.Equal(t, 9*time.Hour, c.RefreshTokenLifespan) +} + +func TestStorageLifetimes(t *testing.T) { + c := DefaultOIDCTimeoutsConfiguration() + + // These are currently hard-coded. + require.Equal(t, 9*time.Hour+10*time.Minute, c.AuthorizationCodeSessionStorageLifetime(nil)) + require.Equal(t, 11*time.Minute, c.PKCESessionStorageLifetime(nil)) + require.Equal(t, 11*time.Minute, c.OIDCSessionStorageLifetime(nil)) + require.Equal(t, 9*time.Hour+2*time.Minute, c.AccessTokenSessionStorageLifetime(nil)) + require.Equal(t, 9*time.Hour+2*time.Minute, c.RefreshTokenSessionStorageLifetime(nil)) +} + +func TestOverrideDefaultAccessTokenLifespan(t *testing.T) { + c := DefaultOIDCTimeoutsConfiguration() + + // We are not yet overriding access token lifetimes. + doOverride, newLifespan := c.OverrideDefaultAccessTokenLifespan(nil) + require.Equal(t, false, doOverride) + require.Equal(t, time.Duration(0), newLifespan) +} + +func TestOverrideIDTokenLifespan(t *testing.T) { + tests := []struct { + name string + accessRequest fosite.AccessRequester + wantOverride bool + wantLifespan time.Duration + }{ + { + name: "the client does not override the default ID token lifespan", + accessRequest: &fosite.AccessRequest{ + GrantTypes: fosite.Arguments{"foo"}, + Request: fosite.Request{ + Client: &clientregistry.Client{ + IDTokenLifetimeConfiguration: 0, // 0 means use the default, so this is not an override + }, + }, + }, + wantOverride: false, + wantLifespan: 0, + }, + { + name: "the client overrides the default ID token lifespan", + accessRequest: &fosite.AccessRequest{ + GrantTypes: fosite.Arguments{"foo"}, + Request: fosite.Request{ + Client: &clientregistry.Client{ + IDTokenLifetimeConfiguration: 42 * time.Second, + }, + }, + }, + wantOverride: true, + wantLifespan: 42 * time.Second, + }, + { + name: "the client overrides the default ID token lifespan, but the request is for the token exchange, so the override is ignored", + accessRequest: &fosite.AccessRequest{ + GrantTypes: fosite.Arguments{"urn:ietf:params:oauth:grant-type:token-exchange"}, + Request: fosite.Request{ + Client: &clientregistry.Client{ + IDTokenLifetimeConfiguration: 42 * time.Second, + }, + }, + }, + wantOverride: false, + wantLifespan: 0, + }, + { + name: "the client is not the expected data type (which shouldn't really happen), so it is assumed to not override the ID token lifespan", + accessRequest: &fosite.AccessRequest{ + GrantTypes: fosite.Arguments{"foo"}, + Request: fosite.Request{ + Client: &fosite.DefaultClient{}, + }, + }, + wantOverride: false, + wantLifespan: 0, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + c := DefaultOIDCTimeoutsConfiguration() + + doOverride, newLifespan := c.OverrideDefaultIDTokenLifespan(tt.accessRequest) + require.Equal(t, tt.wantOverride, doOverride) + require.Equal(t, tt.wantLifespan, newLifespan) + }) + } +} diff --git a/internal/fositestorage/accesstoken/accesstoken_test.go b/internal/fositestorage/accesstoken/accesstoken_test.go index adfb67466..a36e09f80 100644 --- a/internal/fositestorage/accesstoken/accesstoken_test.go +++ b/internal/fositestorage/accesstoken/accesstoken_test.go @@ -23,6 +23,7 @@ import ( clocktesting "k8s.io/utils/clock/testing" "go.pinniped.dev/internal/federationdomain/clientregistry" + "go.pinniped.dev/internal/federationdomain/timeouts" "go.pinniped.dev/internal/psession" "go.pinniped.dev/internal/testutil" ) @@ -32,6 +33,7 @@ const namespace = "test-ns" var fakeNow = time.Date(2030, time.January, 1, 0, 0, 0, 0, time.UTC) var lifetime = time.Minute * 10 var fakeNowPlusLifetimeAsString = metav1.Time{Time: fakeNow.Add(lifetime)}.Format(time.RFC3339) +var lifetimeFunc = func(requester fosite.Requester) time.Duration { return lifetime } var secretsGVR = schema.GroupVersionResource{ Group: "", @@ -54,7 +56,7 @@ func TestAccessTokenStorage(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":"","IDTokenLifetimeConfiguration":0},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"id_token_claims":null,"headers":null,"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","upstreamUsername":"fake-upstream-username","upstreamGroups":["fake-upstream-group1","fake-upstream-group2"],"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"6"}`), + "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":"","IDTokenLifetimeConfiguration":42000000000},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"id_token_claims":null,"headers":null,"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","upstreamUsername":"fake-upstream-username","upstreamGroups":["fake-upstream-group1","fake-upstream-group2"],"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"6"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/access-token", @@ -63,12 +65,19 @@ func TestAccessTokenStorage(t *testing.T) { coretesting.NewDeleteAction(secretsGVR, namespace, "pinniped-storage-access-token-pwu5zs7lekbhnln2w4"), } - ctx, client, _, storage := makeTestSubject() + storageLifetimeFuncCallCount := 0 + var storageLifetimeFuncCallRequesterArg fosite.Requester + ctx, client, _, storage := makeTestSubject(func(requester fosite.Requester) time.Duration { + storageLifetimeFuncCallCount++ + storageLifetimeFuncCallRequesterArg = requester + return lifetime + }) request := &fosite.Request{ ID: "abcd-1", RequestedAt: time.Time{}, Client: &clientregistry.Client{ + IDTokenLifetimeConfiguration: 42 * time.Second, DefaultOpenIDConnectClient: fosite.DefaultOpenIDConnectClient{ DefaultClient: &fosite.DefaultClient{ ID: "pinny", @@ -96,6 +105,8 @@ func TestAccessTokenStorage(t *testing.T) { } err := storage.CreateAccessTokenSession(ctx, "fancy-signature", request) require.NoError(t, err) + require.Equal(t, 1, storageLifetimeFuncCallCount) + require.Equal(t, request, storageLifetimeFuncCallRequesterArg) newRequest, err := storage.GetAccessTokenSession(ctx, "fancy-signature", nil) require.NoError(t, err) @@ -106,6 +117,9 @@ func TestAccessTokenStorage(t *testing.T) { testutil.LogActualJSONFromCreateAction(t, client, 0) // makes it easier to update expected values when needed require.Equal(t, wantActions, client.Actions()) + + // Check that there were no more calls to the lifetime func since the original create. + require.Equal(t, 1, storageLifetimeFuncCallCount) } func TestAccessTokenStorageRevocation(t *testing.T) { @@ -134,7 +148,7 @@ func TestAccessTokenStorageRevocation(t *testing.T) { coretesting.NewDeleteAction(secretsGVR, namespace, "pinniped-storage-access-token-pwu5zs7lekbhnln2w4"), } - ctx, client, _, storage := makeTestSubject() + ctx, client, _, storage := makeTestSubject(lifetimeFunc) request := &fosite.Request{ ID: "abcd-1", @@ -164,7 +178,7 @@ func TestAccessTokenStorageRevocation(t *testing.T) { } func TestGetNotFound(t *testing.T) { - ctx, _, _, storage := makeTestSubject() + ctx, _, _, storage := makeTestSubject(lifetimeFunc) _, notFoundErr := storage.GetAccessTokenSession(ctx, "non-existent-signature", nil) require.EqualError(t, notFoundErr, "not_found") @@ -172,7 +186,7 @@ func TestGetNotFound(t *testing.T) { } func TestWrongVersion(t *testing.T) { - ctx, _, secrets, storage := makeTestSubject() + ctx, _, secrets, storage := makeTestSubject(lifetimeFunc) secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ @@ -200,7 +214,7 @@ func TestWrongVersion(t *testing.T) { } func TestNilSessionRequest(t *testing.T) { - ctx, _, secrets, storage := makeTestSubject() + ctx, _, secrets, storage := makeTestSubject(lifetimeFunc) secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ @@ -228,14 +242,14 @@ func TestNilSessionRequest(t *testing.T) { } func TestCreateWithNilRequester(t *testing.T) { - ctx, _, _, storage := makeTestSubject() + ctx, _, _, storage := makeTestSubject(lifetimeFunc) err := storage.CreateAccessTokenSession(ctx, "signature-doesnt-matter", nil) require.EqualError(t, err, "requester must be of type fosite.Request") } func TestCreateWithWrongRequesterDataTypes(t *testing.T) { - ctx, _, _, storage := makeTestSubject() + ctx, _, _, storage := makeTestSubject(lifetimeFunc) request := &fosite.Request{ Session: nil, @@ -253,7 +267,7 @@ func TestCreateWithWrongRequesterDataTypes(t *testing.T) { } func TestCreateWithoutRequesterID(t *testing.T) { - ctx, client, _, storage := makeTestSubject() + ctx, client, _, storage := makeTestSubject(lifetimeFunc) request := &fosite.Request{ ID: "", // empty ID @@ -274,10 +288,13 @@ func TestCreateWithoutRequesterID(t *testing.T) { require.Equal(t, request.ID, actualSecret.Labels["storage.pinniped.dev/request-id"]) } -func makeTestSubject() (context.Context, *fake.Clientset, corev1client.SecretInterface, RevocationStorage) { +func makeTestSubject(lifetimeFunc timeouts.StorageLifetime) (context.Context, *fake.Clientset, corev1client.SecretInterface, RevocationStorage) { client := fake.NewSimpleClientset() secrets := client.CoreV1().Secrets(namespace) - return context.Background(), client, secrets, New(secrets, clocktesting.NewFakeClock(fakeNow).Now, func(requester fosite.Requester) time.Duration { return lifetime }) + return context.Background(), + client, + secrets, + New(secrets, clocktesting.NewFakeClock(fakeNow).Now, lifetimeFunc) } func TestReadFromSecret(t *testing.T) { diff --git a/internal/fositestorage/authorizationcode/authorizationcode_test.go b/internal/fositestorage/authorizationcode/authorizationcode_test.go index c43f32bfc..a057062f2 100644 --- a/internal/fositestorage/authorizationcode/authorizationcode_test.go +++ b/internal/fositestorage/authorizationcode/authorizationcode_test.go @@ -35,6 +35,7 @@ import ( clocktesting "k8s.io/utils/clock/testing" "go.pinniped.dev/internal/federationdomain/clientregistry" + "go.pinniped.dev/internal/federationdomain/timeouts" "go.pinniped.dev/internal/fositestorage" "go.pinniped.dev/internal/psession" "go.pinniped.dev/internal/testutil" @@ -45,6 +46,7 @@ const namespace = "test-ns" var fakeNow = time.Date(2030, time.January, 1, 0, 0, 0, 0, time.UTC) var lifetime = time.Minute * 10 var fakeNowPlusLifetimeAsString = metav1.Time{Time: fakeNow.Add(lifetime)}.Format(time.RFC3339) +var lifetimeFunc = func(requester fosite.Requester) time.Duration { return lifetime } func TestAuthorizationCodeStorage(t *testing.T) { secretsGVR := schema.GroupVersionResource{ @@ -66,7 +68,7 @@ func TestAuthorizationCodeStorage(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"active":true,"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":"","IDTokenLifetimeConfiguration":0},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"id_token_claims":null,"headers":null,"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","upstreamUsername":"fake-upstream-username","upstreamGroups":["fake-upstream-group1","fake-upstream-group2"],"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"6"}`), + "pinniped-storage-data": []byte(`{"active":true,"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":"","IDTokenLifetimeConfiguration":42000000000},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"id_token_claims":null,"headers":null,"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","upstreamUsername":"fake-upstream-username","upstreamGroups":["fake-upstream-group1","fake-upstream-group2"],"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"6"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/authcode", @@ -86,19 +88,26 @@ func TestAuthorizationCodeStorage(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"active":false,"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":"","IDTokenLifetimeConfiguration":0},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"id_token_claims":null,"headers":null,"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","upstreamUsername":"fake-upstream-username","upstreamGroups":["fake-upstream-group1","fake-upstream-group2"],"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"6"}`), + "pinniped-storage-data": []byte(`{"active":false,"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":"","IDTokenLifetimeConfiguration":42000000000},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"id_token_claims":null,"headers":null,"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","upstreamUsername":"fake-upstream-username","upstreamGroups":["fake-upstream-group1","fake-upstream-group2"],"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"6"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/authcode", }), } - ctx, client, _, storage := makeTestSubject() + storageLifetimeFuncCallCount := 0 + var storageLifetimeFuncCallRequesterArg fosite.Requester + ctx, client, _, storage := makeTestSubject(func(requester fosite.Requester) time.Duration { + storageLifetimeFuncCallCount++ + storageLifetimeFuncCallRequesterArg = requester + return lifetime + }) request := &fosite.Request{ ID: "abcd-1", RequestedAt: time.Time{}, Client: &clientregistry.Client{ + IDTokenLifetimeConfiguration: 42 * time.Second, DefaultOpenIDConnectClient: fosite.DefaultOpenIDConnectClient{ DefaultClient: &fosite.DefaultClient{ ID: "pinny", @@ -127,6 +136,8 @@ func TestAuthorizationCodeStorage(t *testing.T) { } err := storage.CreateAuthorizeCodeSession(ctx, "fancy-signature", request) require.NoError(t, err) + require.Equal(t, 1, storageLifetimeFuncCallCount) + require.Equal(t, request, storageLifetimeFuncCallRequesterArg) newRequest, err := storage.GetAuthorizeCodeSession(ctx, "fancy-signature", nil) require.NoError(t, err) @@ -143,10 +154,13 @@ func TestAuthorizationCodeStorage(t *testing.T) { invalidatedRequest, err := storage.GetAuthorizeCodeSession(ctx, "fancy-signature", nil) require.EqualError(t, err, "authorization code session for fancy-signature has already been used: Authorization code has ben invalidated") require.Equal(t, "abcd-1", invalidatedRequest.GetID()) + + // Check that there were no more calls to the lifetime func since the original create. + require.Equal(t, 1, storageLifetimeFuncCallCount) } func TestGetNotFound(t *testing.T) { - ctx, _, _, storage := makeTestSubject() + ctx, _, _, storage := makeTestSubject(lifetimeFunc) _, notFoundErr := storage.GetAuthorizeCodeSession(ctx, "non-existent-signature", nil) require.EqualError(t, notFoundErr, "not_found") @@ -154,7 +168,7 @@ func TestGetNotFound(t *testing.T) { } func TestInvalidateWhenNotFound(t *testing.T) { - ctx, _, _, storage := makeTestSubject() + ctx, _, _, storage := makeTestSubject(lifetimeFunc) notFoundErr := storage.InvalidateAuthorizeCodeSession(ctx, "non-existent-signature") require.EqualError(t, notFoundErr, "not_found") @@ -162,7 +176,7 @@ func TestInvalidateWhenNotFound(t *testing.T) { } func TestInvalidateWhenConflictOnUpdateHappens(t *testing.T) { - ctx, client, _, storage := makeTestSubject() + ctx, client, _, storage := makeTestSubject(lifetimeFunc) client.PrependReactor("update", "secrets", func(_ kubetesting.Action) (bool, runtime.Object, error) { return true, nil, apierrors.NewConflict(schema.GroupResource{ @@ -183,7 +197,7 @@ func TestInvalidateWhenConflictOnUpdateHappens(t *testing.T) { } func TestWrongVersion(t *testing.T) { - ctx, _, secrets, storage := makeTestSubject() + ctx, _, secrets, storage := makeTestSubject(lifetimeFunc) secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ @@ -208,7 +222,7 @@ func TestWrongVersion(t *testing.T) { } func TestNilSessionRequest(t *testing.T) { - ctx, _, secrets, storage := makeTestSubject() + ctx, _, secrets, storage := makeTestSubject(lifetimeFunc) secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ @@ -233,14 +247,14 @@ func TestNilSessionRequest(t *testing.T) { } func TestCreateWithNilRequester(t *testing.T) { - ctx, _, _, storage := makeTestSubject() + ctx, _, _, storage := makeTestSubject(lifetimeFunc) err := storage.CreateAuthorizeCodeSession(ctx, "signature-doesnt-matter", nil) require.EqualError(t, err, "requester must be of type fosite.Request") } func TestCreateWithWrongRequesterDataTypes(t *testing.T) { - ctx, _, _, storage := makeTestSubject() + ctx, _, _, storage := makeTestSubject(lifetimeFunc) request := &fosite.Request{ Session: nil, @@ -257,10 +271,13 @@ func TestCreateWithWrongRequesterDataTypes(t *testing.T) { require.EqualError(t, err, "requester's client must be of type clientregistry.Client") } -func makeTestSubject() (context.Context, *fake.Clientset, corev1client.SecretInterface, oauth2.AuthorizeCodeStorage) { +func makeTestSubject(lifetimeFunc timeouts.StorageLifetime) (context.Context, *fake.Clientset, corev1client.SecretInterface, oauth2.AuthorizeCodeStorage) { client := fake.NewSimpleClientset() secrets := client.CoreV1().Secrets(namespace) - return context.Background(), client, secrets, New(secrets, clocktesting.NewFakeClock(fakeNow).Now, func(requester fosite.Requester) time.Duration { return lifetime }) + return context.Background(), + client, + secrets, + New(secrets, clocktesting.NewFakeClock(fakeNow).Now, lifetimeFunc) } // TestFuzzAndJSONNewValidEmptyAuthorizeCodeSession asserts that we can correctly round trip our authorize code session. diff --git a/internal/fositestorage/openidconnect/openidconnect_test.go b/internal/fositestorage/openidconnect/openidconnect_test.go index 882eff9ba..781973d36 100644 --- a/internal/fositestorage/openidconnect/openidconnect_test.go +++ b/internal/fositestorage/openidconnect/openidconnect_test.go @@ -22,6 +22,7 @@ import ( clocktesting "k8s.io/utils/clock/testing" "go.pinniped.dev/internal/federationdomain/clientregistry" + "go.pinniped.dev/internal/federationdomain/timeouts" "go.pinniped.dev/internal/psession" "go.pinniped.dev/internal/testutil" ) @@ -31,6 +32,7 @@ const namespace = "test-ns" var fakeNow = time.Date(2030, time.January, 1, 0, 0, 0, 0, time.UTC) var lifetime = time.Minute * 10 var fakeNowPlusLifetimeAsString = metav1.Time{Time: fakeNow.Add(lifetime)}.Format(time.RFC3339) +var lifetimeFunc = func(requester fosite.Requester) time.Duration { return lifetime } func TestOpenIdConnectStorage(t *testing.T) { secretsGVR := schema.GroupVersionResource{ @@ -52,7 +54,7 @@ func TestOpenIdConnectStorage(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":"","IDTokenLifetimeConfiguration":0},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"id_token_claims":null,"headers":null,"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","upstreamUsername":"fake-upstream-username","upstreamGroups":["fake-upstream-group1","fake-upstream-group2"],"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"6"}`), + "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":"","IDTokenLifetimeConfiguration":42000000000},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"id_token_claims":null,"headers":null,"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","upstreamUsername":"fake-upstream-username","upstreamGroups":["fake-upstream-group1","fake-upstream-group2"],"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"6"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/oidc", @@ -61,12 +63,19 @@ func TestOpenIdConnectStorage(t *testing.T) { coretesting.NewDeleteAction(secretsGVR, namespace, "pinniped-storage-oidc-pwu5zs7lekbhnln2w4"), } - ctx, client, _, storage := makeTestSubject() + storageLifetimeFuncCallCount := 0 + var storageLifetimeFuncCallRequesterArg fosite.Requester + ctx, client, _, storage := makeTestSubject(func(requester fosite.Requester) time.Duration { + storageLifetimeFuncCallCount++ + storageLifetimeFuncCallRequesterArg = requester + return lifetime + }) request := &fosite.Request{ ID: "abcd-1", RequestedAt: time.Time{}, Client: &clientregistry.Client{ + IDTokenLifetimeConfiguration: 42 * time.Second, DefaultOpenIDConnectClient: fosite.DefaultOpenIDConnectClient{ DefaultClient: &fosite.DefaultClient{ ID: "pinny", @@ -95,6 +104,8 @@ func TestOpenIdConnectStorage(t *testing.T) { } err := storage.CreateOpenIDConnectSession(ctx, "fancy-code.fancy-signature", request) require.NoError(t, err) + require.Equal(t, 1, storageLifetimeFuncCallCount) + require.Equal(t, request, storageLifetimeFuncCallRequesterArg) newRequest, err := storage.GetOpenIDConnectSession(ctx, "fancy-code.fancy-signature", nil) require.NoError(t, err) @@ -105,10 +116,13 @@ func TestOpenIdConnectStorage(t *testing.T) { testutil.LogActualJSONFromCreateAction(t, client, 0) // makes it easier to update expected values when needed require.Equal(t, wantActions, client.Actions()) + + // Check that there were no more calls to the lifetime func since the original create. + require.Equal(t, 1, storageLifetimeFuncCallCount) } func TestGetNotFound(t *testing.T) { - ctx, _, _, storage := makeTestSubject() + ctx, _, _, storage := makeTestSubject(lifetimeFunc) _, notFoundErr := storage.GetOpenIDConnectSession(ctx, "authcode.non-existent-signature", nil) require.EqualError(t, notFoundErr, "not_found") @@ -116,7 +130,7 @@ func TestGetNotFound(t *testing.T) { } func TestWrongVersion(t *testing.T) { - ctx, _, secrets, storage := makeTestSubject() + ctx, _, secrets, storage := makeTestSubject(lifetimeFunc) secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ @@ -141,7 +155,7 @@ func TestWrongVersion(t *testing.T) { } func TestNilSessionRequest(t *testing.T) { - ctx, _, secrets, storage := makeTestSubject() + ctx, _, secrets, storage := makeTestSubject(lifetimeFunc) secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ @@ -166,14 +180,14 @@ func TestNilSessionRequest(t *testing.T) { } func TestCreateWithNilRequester(t *testing.T) { - ctx, _, _, storage := makeTestSubject() + ctx, _, _, storage := makeTestSubject(lifetimeFunc) err := storage.CreateOpenIDConnectSession(ctx, "authcode.signature-doesnt-matter", nil) require.EqualError(t, err, "requester must be of type fosite.Request") } func TestCreateWithWrongRequesterDataTypes(t *testing.T) { - ctx, _, _, storage := makeTestSubject() + ctx, _, _, storage := makeTestSubject(lifetimeFunc) request := &fosite.Request{ Session: nil, @@ -191,14 +205,17 @@ func TestCreateWithWrongRequesterDataTypes(t *testing.T) { } func TestAuthcodeHasNoDot(t *testing.T) { - ctx, _, _, storage := makeTestSubject() + ctx, _, _, storage := makeTestSubject(lifetimeFunc) err := storage.CreateOpenIDConnectSession(ctx, "all-one-part", nil) require.EqualError(t, err, "malformed authorization code") } -func makeTestSubject() (context.Context, *fake.Clientset, corev1client.SecretInterface, openid.OpenIDConnectRequestStorage) { +func makeTestSubject(lifetimeFunc timeouts.StorageLifetime) (context.Context, *fake.Clientset, corev1client.SecretInterface, openid.OpenIDConnectRequestStorage) { client := fake.NewSimpleClientset() secrets := client.CoreV1().Secrets(namespace) - return context.Background(), client, secrets, New(secrets, clocktesting.NewFakeClock(fakeNow).Now, func(requester fosite.Requester) time.Duration { return lifetime }) + return context.Background(), + client, + secrets, + New(secrets, clocktesting.NewFakeClock(fakeNow).Now, lifetimeFunc) } diff --git a/internal/fositestorage/pkce/pkce_test.go b/internal/fositestorage/pkce/pkce_test.go index 2f1003951..100cbd211 100644 --- a/internal/fositestorage/pkce/pkce_test.go +++ b/internal/fositestorage/pkce/pkce_test.go @@ -22,6 +22,7 @@ import ( clocktesting "k8s.io/utils/clock/testing" "go.pinniped.dev/internal/federationdomain/clientregistry" + "go.pinniped.dev/internal/federationdomain/timeouts" "go.pinniped.dev/internal/psession" "go.pinniped.dev/internal/testutil" ) @@ -31,6 +32,7 @@ const namespace = "test-ns" var fakeNow = time.Date(2030, time.January, 1, 0, 0, 0, 0, time.UTC) var lifetime = time.Minute * 10 var fakeNowPlusLifetimeAsString = metav1.Time{Time: fakeNow.Add(lifetime)}.Format(time.RFC3339) +var lifetimeFunc = func(requester fosite.Requester) time.Duration { return lifetime } func TestPKCEStorage(t *testing.T) { secretsGVR := schema.GroupVersionResource{ @@ -52,7 +54,7 @@ func TestPKCEStorage(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":"","IDTokenLifetimeConfiguration":0},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"id_token_claims":null,"headers":null,"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","upstreamUsername":"fake-upstream-username","upstreamGroups":["fake-upstream-group1","fake-upstream-group2"],"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"6"}`), + "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":"","IDTokenLifetimeConfiguration":42000000000},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"id_token_claims":null,"headers":null,"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","upstreamUsername":"fake-upstream-username","upstreamGroups":["fake-upstream-group1","fake-upstream-group2"],"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"6"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/pkce", @@ -61,12 +63,19 @@ func TestPKCEStorage(t *testing.T) { coretesting.NewDeleteAction(secretsGVR, namespace, "pinniped-storage-pkce-pwu5zs7lekbhnln2w4"), } - ctx, client, _, storage := makeTestSubject() + storageLifetimeFuncCallCount := 0 + var storageLifetimeFuncCallRequesterArg fosite.Requester + ctx, client, _, storage := makeTestSubject(func(requester fosite.Requester) time.Duration { + storageLifetimeFuncCallCount++ + storageLifetimeFuncCallRequesterArg = requester + return lifetime + }) request := &fosite.Request{ ID: "abcd-1", RequestedAt: time.Time{}, Client: &clientregistry.Client{ + IDTokenLifetimeConfiguration: 42 * time.Second, DefaultOpenIDConnectClient: fosite.DefaultOpenIDConnectClient{ DefaultClient: &fosite.DefaultClient{ ID: "pinny", @@ -95,6 +104,8 @@ func TestPKCEStorage(t *testing.T) { } err := storage.CreatePKCERequestSession(ctx, "fancy-signature", request) require.NoError(t, err) + require.Equal(t, 1, storageLifetimeFuncCallCount) + require.Equal(t, request, storageLifetimeFuncCallRequesterArg) newRequest, err := storage.GetPKCERequestSession(ctx, "fancy-signature", nil) require.NoError(t, err) @@ -105,10 +116,12 @@ func TestPKCEStorage(t *testing.T) { testutil.LogActualJSONFromCreateAction(t, client, 0) // makes it easier to update expected values when needed require.Equal(t, wantActions, client.Actions()) + // Check that there were no more calls to the lifetime func since the original create. + require.Equal(t, 1, storageLifetimeFuncCallCount) } func TestGetNotFound(t *testing.T) { - ctx, _, _, storage := makeTestSubject() + ctx, _, _, storage := makeTestSubject(lifetimeFunc) _, notFoundErr := storage.GetPKCERequestSession(ctx, "non-existent-signature", nil) require.EqualError(t, notFoundErr, "not_found") @@ -116,7 +129,7 @@ func TestGetNotFound(t *testing.T) { } func TestWrongVersion(t *testing.T) { - ctx, _, secrets, storage := makeTestSubject() + ctx, _, secrets, storage := makeTestSubject(lifetimeFunc) secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ @@ -144,7 +157,7 @@ func TestWrongVersion(t *testing.T) { } func TestNilSessionRequest(t *testing.T) { - ctx, _, secrets, storage := makeTestSubject() + ctx, _, secrets, storage := makeTestSubject(lifetimeFunc) secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ @@ -172,14 +185,14 @@ func TestNilSessionRequest(t *testing.T) { } func TestCreateWithNilRequester(t *testing.T) { - ctx, _, _, storage := makeTestSubject() + ctx, _, _, storage := makeTestSubject(lifetimeFunc) err := storage.CreatePKCERequestSession(ctx, "signature-doesnt-matter", nil) require.EqualError(t, err, "requester must be of type fosite.Request") } func TestCreateWithWrongRequesterDataTypes(t *testing.T) { - ctx, _, _, storage := makeTestSubject() + ctx, _, _, storage := makeTestSubject(lifetimeFunc) request := &fosite.Request{ Session: nil, @@ -196,8 +209,11 @@ func TestCreateWithWrongRequesterDataTypes(t *testing.T) { require.EqualError(t, err, "requester's client must be of type clientregistry.Client") } -func makeTestSubject() (context.Context, *fake.Clientset, corev1client.SecretInterface, pkce.PKCERequestStorage) { +func makeTestSubject(lifetimeFunc timeouts.StorageLifetime) (context.Context, *fake.Clientset, corev1client.SecretInterface, pkce.PKCERequestStorage) { client := fake.NewSimpleClientset() secrets := client.CoreV1().Secrets(namespace) - return context.Background(), client, secrets, New(secrets, clocktesting.NewFakeClock(fakeNow).Now, func(requester fosite.Requester) time.Duration { return lifetime }) + return context.Background(), + client, + secrets, + New(secrets, clocktesting.NewFakeClock(fakeNow).Now, lifetimeFunc) } diff --git a/internal/fositestorage/refreshtoken/refreshtoken_test.go b/internal/fositestorage/refreshtoken/refreshtoken_test.go index f2e3ac0e9..d6d987c38 100644 --- a/internal/fositestorage/refreshtoken/refreshtoken_test.go +++ b/internal/fositestorage/refreshtoken/refreshtoken_test.go @@ -23,6 +23,7 @@ import ( clocktesting "k8s.io/utils/clock/testing" "go.pinniped.dev/internal/federationdomain/clientregistry" + "go.pinniped.dev/internal/federationdomain/timeouts" "go.pinniped.dev/internal/psession" "go.pinniped.dev/internal/testutil" ) @@ -37,6 +38,7 @@ var secretsGVR = schema.GroupVersionResource{ var fakeNow = time.Date(2030, time.January, 1, 0, 0, 0, 0, time.UTC) var lifetime = time.Minute * 10 var fakeNowPlusLifetimeAsString = metav1.Time{Time: fakeNow.Add(lifetime)}.Format(time.RFC3339) +var lifetimeFunc = func(requester fosite.Requester) time.Duration { return lifetime } func TestRefreshTokenStorage(t *testing.T) { wantActions := []coretesting.Action{ @@ -53,7 +55,7 @@ func TestRefreshTokenStorage(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":"","IDTokenLifetimeConfiguration":0},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"id_token_claims":null,"headers":null,"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","upstreamUsername":"fake-upstream-username","upstreamGroups":["fake-upstream-group1","fake-upstream-group2"],"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"6"}`), + "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":"","IDTokenLifetimeConfiguration":42000000000},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"id_token_claims":null,"headers":null,"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","upstreamUsername":"fake-upstream-username","upstreamGroups":["fake-upstream-group1","fake-upstream-group2"],"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"6"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/refresh-token", @@ -62,12 +64,19 @@ func TestRefreshTokenStorage(t *testing.T) { coretesting.NewDeleteAction(secretsGVR, namespace, "pinniped-storage-refresh-token-pwu5zs7lekbhnln2w4"), } - ctx, client, _, storage := makeTestSubject() + storageLifetimeFuncCallCount := 0 + var storageLifetimeFuncCallRequesterArg fosite.Requester + ctx, client, _, storage := makeTestSubject(func(requester fosite.Requester) time.Duration { + storageLifetimeFuncCallCount++ + storageLifetimeFuncCallRequesterArg = requester + return lifetime + }) request := &fosite.Request{ ID: "abcd-1", RequestedAt: time.Time{}, Client: &clientregistry.Client{ + IDTokenLifetimeConfiguration: 42 * time.Second, DefaultOpenIDConnectClient: fosite.DefaultOpenIDConnectClient{ DefaultClient: &fosite.DefaultClient{ ID: "pinny", @@ -96,6 +105,8 @@ func TestRefreshTokenStorage(t *testing.T) { } err := storage.CreateRefreshTokenSession(ctx, "fancy-signature", request) require.NoError(t, err) + require.Equal(t, 1, storageLifetimeFuncCallCount) + require.Equal(t, request, storageLifetimeFuncCallRequesterArg) newRequest, err := storage.GetRefreshTokenSession(ctx, "fancy-signature", nil) require.NoError(t, err) @@ -106,6 +117,9 @@ func TestRefreshTokenStorage(t *testing.T) { testutil.LogActualJSONFromCreateAction(t, client, 0) // makes it easier to update expected values when needed require.Equal(t, wantActions, client.Actions()) + + // Check that there were no more calls to the lifetime func since the original create. + require.Equal(t, 1, storageLifetimeFuncCallCount) } func TestRefreshTokenStorageRevocation(t *testing.T) { @@ -134,7 +148,7 @@ func TestRefreshTokenStorageRevocation(t *testing.T) { coretesting.NewDeleteAction(secretsGVR, namespace, "pinniped-storage-refresh-token-pwu5zs7lekbhnln2w4"), } - ctx, client, _, storage := makeTestSubject() + ctx, client, _, storage := makeTestSubject(lifetimeFunc) request := &fosite.Request{ ID: "abcd-1", @@ -189,7 +203,7 @@ func TestRefreshTokenStorageRevokeRefreshTokenMaybeGracePeriod(t *testing.T) { coretesting.NewDeleteAction(secretsGVR, namespace, "pinniped-storage-refresh-token-pwu5zs7lekbhnln2w4"), } - ctx, client, _, storage := makeTestSubject() + ctx, client, _, storage := makeTestSubject(lifetimeFunc) request := &fosite.Request{ ID: "abcd-1", @@ -220,7 +234,7 @@ func TestRefreshTokenStorageRevokeRefreshTokenMaybeGracePeriod(t *testing.T) { } func TestGetNotFound(t *testing.T) { - ctx, _, _, storage := makeTestSubject() + ctx, _, _, storage := makeTestSubject(lifetimeFunc) _, notFoundErr := storage.GetRefreshTokenSession(ctx, "non-existent-signature", nil) require.EqualError(t, notFoundErr, "not_found") @@ -228,7 +242,7 @@ func TestGetNotFound(t *testing.T) { } func TestWrongVersion(t *testing.T) { - ctx, _, secrets, storage := makeTestSubject() + ctx, _, secrets, storage := makeTestSubject(lifetimeFunc) secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ @@ -256,7 +270,7 @@ func TestWrongVersion(t *testing.T) { } func TestNilSessionRequest(t *testing.T) { - ctx, _, secrets, storage := makeTestSubject() + ctx, _, secrets, storage := makeTestSubject(lifetimeFunc) secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ @@ -284,14 +298,14 @@ func TestNilSessionRequest(t *testing.T) { } func TestCreateWithNilRequester(t *testing.T) { - ctx, _, _, storage := makeTestSubject() + ctx, _, _, storage := makeTestSubject(lifetimeFunc) err := storage.CreateRefreshTokenSession(ctx, "signature-doesnt-matter", nil) require.EqualError(t, err, "requester must be of type fosite.Request") } func TestCreateWithWrongRequesterDataTypes(t *testing.T) { - ctx, _, _, storage := makeTestSubject() + ctx, _, _, storage := makeTestSubject(lifetimeFunc) request := &fosite.Request{ Session: nil, @@ -309,7 +323,7 @@ func TestCreateWithWrongRequesterDataTypes(t *testing.T) { } func TestCreateWithoutRequesterID(t *testing.T) { - ctx, client, _, storage := makeTestSubject() + ctx, client, _, storage := makeTestSubject(lifetimeFunc) request := &fosite.Request{ ID: "", // empty ID @@ -330,10 +344,13 @@ func TestCreateWithoutRequesterID(t *testing.T) { require.Equal(t, request.ID, actualSecret.Labels["storage.pinniped.dev/request-id"]) } -func makeTestSubject() (context.Context, *fake.Clientset, corev1client.SecretInterface, RevocationStorage) { +func makeTestSubject(lifetimeFunc timeouts.StorageLifetime) (context.Context, *fake.Clientset, corev1client.SecretInterface, RevocationStorage) { client := fake.NewSimpleClientset() secrets := client.CoreV1().Secrets(namespace) - return context.Background(), client, secrets, New(secrets, clocktesting.NewFakeClock(fakeNow).Now, func(requester fosite.Requester) time.Duration { return lifetime }) + return context.Background(), + client, + secrets, + New(secrets, clocktesting.NewFakeClock(fakeNow).Now, lifetimeFunc) } func TestReadFromSecret(t *testing.T) { From 5dbf05c31d5d344f7d4c8b665bdae6038a5a41e5 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Wed, 17 Apr 2024 09:42:15 -0700 Subject: [PATCH 5/8] Update the session storage versions due to new ID token lifetime field --- .../garbage_collector_test.go | 33 ++++++------- .../fositestorage/accesstoken/accesstoken.go | 3 +- .../accesstoken/accesstoken_test.go | 43 +++++++++-------- .../authorizationcode/authorizationcode.go | 5 +- .../authorizationcode_test.go | 47 ++++++++++++------- .../openidconnect/openidconnect.go | 3 +- .../openidconnect/openidconnect_test.go | 21 +++++---- internal/fositestorage/pkce/pkce.go | 3 +- internal/fositestorage/pkce/pkce_test.go | 21 +++++---- .../refreshtoken/refreshtoken.go | 3 +- .../refreshtoken/refreshtoken_test.go | 46 ++++++++++-------- test/integration/supervisor_storage_test.go | 2 +- 12 files changed, 134 insertions(+), 96 deletions(-) diff --git a/internal/controller/supervisorstorage/garbage_collector_test.go b/internal/controller/supervisorstorage/garbage_collector_test.go index a52312567..c020ba92e 100644 --- a/internal/controller/supervisorstorage/garbage_collector_test.go +++ b/internal/controller/supervisorstorage/garbage_collector_test.go @@ -121,7 +121,8 @@ func TestGarbageCollectorControllerSync(t *testing.T) { spec.Run(t, "Sync", func(t *testing.T, when spec.G, it spec.S) { const ( - installedInNamespace = "some-namespace" + installedInNamespace = "some-namespace" + currentSessionStorageVersion = "7" // update this when you update the storage version in the production code ) var ( @@ -265,7 +266,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) { when("there are valid, expired authcode secrets which contain upstream refresh tokens", func() { it.Before(func() { activeOIDCAuthcodeSession := &authorizationcode.Session{ - Version: "6", + Version: currentSessionStorageVersion, Active: true, Request: &fosite.Request{ ID: "request-id-1", @@ -310,7 +311,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) { r.NoError(kubeClient.Tracker().Add(activeOIDCAuthcodeSessionSecret)) inactiveOIDCAuthcodeSession := &authorizationcode.Session{ - Version: "6", + Version: currentSessionStorageVersion, Active: false, Request: &fosite.Request{ ID: "request-id-2", @@ -389,7 +390,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) { when("there are valid, expired authcode secrets which contain upstream access tokens", func() { it.Before(func() { activeOIDCAuthcodeSession := &authorizationcode.Session{ - Version: "6", + Version: currentSessionStorageVersion, Active: true, Request: &fosite.Request{ ID: "request-id-1", @@ -434,7 +435,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) { r.NoError(kubeClient.Tracker().Add(activeOIDCAuthcodeSessionSecret)) inactiveOIDCAuthcodeSession := &authorizationcode.Session{ - Version: "6", + Version: currentSessionStorageVersion, Active: false, Request: &fosite.Request{ ID: "request-id-2", @@ -513,7 +514,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) { when("there is an invalid, expired authcode secret", func() { it.Before(func() { invalidOIDCAuthcodeSession := &authorizationcode.Session{ - Version: "6", + Version: currentSessionStorageVersion, Active: true, Request: &fosite.Request{ ID: "", // it is invalid for there to be a missing request ID @@ -582,7 +583,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) { when("there is a valid, expired authcode secret but its upstream name does not match any existing upstream", func() { it.Before(func() { wrongProviderNameOIDCAuthcodeSession := &authorizationcode.Session{ - Version: "6", + Version: currentSessionStorageVersion, Active: true, Request: &fosite.Request{ ID: "request-id-1", @@ -653,7 +654,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) { when("there is a valid, expired authcode secret but its upstream UID does not match any existing upstream", func() { it.Before(func() { wrongProviderNameOIDCAuthcodeSession := &authorizationcode.Session{ - Version: "6", + Version: currentSessionStorageVersion, Active: true, Request: &fosite.Request{ ID: "request-id-1", @@ -724,7 +725,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) { when("there is a valid, recently expired authcode secret but the upstream revocation fails", func() { it.Before(func() { activeOIDCAuthcodeSession := &authorizationcode.Session{ - Version: "6", + Version: currentSessionStorageVersion, Active: true, Request: &fosite.Request{ ID: "request-id-1", @@ -829,7 +830,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) { when("there is a valid, long-since expired authcode secret but the upstream revocation fails", func() { it.Before(func() { activeOIDCAuthcodeSession := &authorizationcode.Session{ - Version: "6", + Version: currentSessionStorageVersion, Active: true, Request: &fosite.Request{ ID: "request-id-1", @@ -908,7 +909,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) { when("there are valid, expired access token secrets which contain upstream refresh tokens", func() { it.Before(func() { offlineAccessGrantedOIDCAccessTokenSession := &accesstoken.Session{ - Version: "6", + Version: currentSessionStorageVersion, Request: &fosite.Request{ GrantedScope: fosite.Arguments{"scope1", "scope2", "offline_access"}, ID: "request-id-1", @@ -953,7 +954,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) { r.NoError(kubeClient.Tracker().Add(offlineAccessGrantedOIDCAccessTokenSessionSecret)) offlineAccessNotGrantedOIDCAccessTokenSession := &accesstoken.Session{ - Version: "6", + Version: currentSessionStorageVersion, Request: &fosite.Request{ GrantedScope: fosite.Arguments{"scope1", "scope2"}, ID: "request-id-2", @@ -1032,7 +1033,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) { when("there are valid, expired access token secrets which contain upstream access tokens", func() { it.Before(func() { offlineAccessGrantedOIDCAccessTokenSession := &accesstoken.Session{ - Version: "6", + Version: currentSessionStorageVersion, Request: &fosite.Request{ GrantedScope: fosite.Arguments{"scope1", "scope2", "offline_access"}, ID: "request-id-1", @@ -1077,7 +1078,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) { r.NoError(kubeClient.Tracker().Add(offlineAccessGrantedOIDCAccessTokenSessionSecret)) offlineAccessNotGrantedOIDCAccessTokenSession := &accesstoken.Session{ - Version: "6", + Version: currentSessionStorageVersion, Request: &fosite.Request{ GrantedScope: fosite.Arguments{"scope1", "scope2"}, ID: "request-id-2", @@ -1156,7 +1157,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) { when("there are valid, expired refresh secrets which contain upstream refresh tokens", func() { it.Before(func() { oidcRefreshSession := &refreshtoken.Session{ - Version: "6", + Version: currentSessionStorageVersion, Request: &fosite.Request{ ID: "request-id-1", Client: &clientregistry.Client{}, @@ -1233,7 +1234,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) { when("there are valid, expired refresh secrets which contain upstream access tokens", func() { it.Before(func() { oidcRefreshSession := &refreshtoken.Session{ - Version: "6", + Version: currentSessionStorageVersion, Request: &fosite.Request{ ID: "request-id-1", Client: &clientregistry.Client{}, diff --git a/internal/fositestorage/accesstoken/accesstoken.go b/internal/fositestorage/accesstoken/accesstoken.go index 247273c42..c67b9cab7 100644 --- a/internal/fositestorage/accesstoken/accesstoken.go +++ b/internal/fositestorage/accesstoken/accesstoken.go @@ -34,7 +34,8 @@ const ( // Version 4 is when fosite added json tags to their openid.DefaultSession struct. // Version 5 is when we added the UpstreamUsername and UpstreamGroups fields to psession.CustomSessionData. // Version 6 is when we upgraded fosite in Dec 2023. - accessTokenStorageVersion = "6" + // Version 7 is when OIDCClients were given configurable ID token lifetimes. + accessTokenStorageVersion = "7" ) type RevocationStorage interface { diff --git a/internal/fositestorage/accesstoken/accesstoken_test.go b/internal/fositestorage/accesstoken/accesstoken_test.go index a36e09f80..522c24c90 100644 --- a/internal/fositestorage/accesstoken/accesstoken_test.go +++ b/internal/fositestorage/accesstoken/accesstoken_test.go @@ -28,18 +28,23 @@ import ( "go.pinniped.dev/internal/testutil" ) -const namespace = "test-ns" +const ( + namespace = "test-ns" + expectedVersion = "7" // update this when you update the storage version in the production code +) -var fakeNow = time.Date(2030, time.January, 1, 0, 0, 0, 0, time.UTC) -var lifetime = time.Minute * 10 -var fakeNowPlusLifetimeAsString = metav1.Time{Time: fakeNow.Add(lifetime)}.Format(time.RFC3339) -var lifetimeFunc = func(requester fosite.Requester) time.Duration { return lifetime } +var ( + fakeNow = time.Date(2030, time.January, 1, 0, 0, 0, 0, time.UTC) + lifetime = time.Minute * 10 + fakeNowPlusLifetimeAsString = metav1.Time{Time: fakeNow.Add(lifetime)}.Format(time.RFC3339) + lifetimeFunc = func(requester fosite.Requester) time.Duration { return lifetime } -var secretsGVR = schema.GroupVersionResource{ - Group: "", - Version: "v1", - Resource: "secrets", -} + secretsGVR = schema.GroupVersionResource{ + Group: "", + Version: "v1", + Resource: "secrets", + } +) func TestAccessTokenStorage(t *testing.T) { wantActions := []coretesting.Action{ @@ -56,7 +61,7 @@ func TestAccessTokenStorage(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":"","IDTokenLifetimeConfiguration":42000000000},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"id_token_claims":null,"headers":null,"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","upstreamUsername":"fake-upstream-username","upstreamGroups":["fake-upstream-group1","fake-upstream-group2"],"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"6"}`), + "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":"","IDTokenLifetimeConfiguration":42000000000},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"id_token_claims":null,"headers":null,"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","upstreamUsername":"fake-upstream-username","upstreamGroups":["fake-upstream-group1","fake-upstream-group2"],"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"` + expectedVersion + `"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/access-token", @@ -137,7 +142,7 @@ func TestAccessTokenStorageRevocation(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":"","IDTokenLifetimeConfiguration":0},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"id_token_claims":null,"headers":null,"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","upstreamUsername":"fake-upstream-username","upstreamGroups":["fake-upstream-group1","fake-upstream-group2"],"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"6"}`), + "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":"","IDTokenLifetimeConfiguration":0},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"id_token_claims":null,"headers":null,"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","upstreamUsername":"fake-upstream-username","upstreamGroups":["fake-upstream-group1","fake-upstream-group2"],"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"` + expectedVersion + `"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/access-token", @@ -210,7 +215,7 @@ func TestWrongVersion(t *testing.T) { _, err = storage.GetAccessTokenSession(ctx, "fancy-signature", nil) - require.EqualError(t, err, "access token request data has wrong version: access token session for fancy-signature has version not-the-right-version instead of 6") + require.EqualError(t, err, "access token request data has wrong version: access token session for fancy-signature has version not-the-right-version instead of "+expectedVersion) } func TestNilSessionRequest(t *testing.T) { @@ -228,7 +233,7 @@ func TestNilSessionRequest(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"nonsense-key": "nonsense-value","version":"6"}`), + "pinniped-storage-data": []byte(`{"nonsense-key": "nonsense-value","version":"` + expectedVersion + `"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/access-token", @@ -315,13 +320,13 @@ func TestReadFromSecret(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","session":{"fosite":{"id_token_claims":{"jti": "xyz"},"headers":{"extra":{"myheader": "foo"}},"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","upstreamUsername":"fake-upstream-username","upstreamGroups":["fake-upstream-group1","fake-upstream-group2"],"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token"}}}},"version":"6","active": true}`), + "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","session":{"fosite":{"id_token_claims":{"jti": "xyz"},"headers":{"extra":{"myheader": "foo"}},"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","upstreamUsername":"fake-upstream-username","upstreamGroups":["fake-upstream-group1","fake-upstream-group2"],"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token"}}}},"version":"` + expectedVersion + `","active": true}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/access-token", }, wantSession: &Session{ - Version: "6", + Version: expectedVersion, Request: &fosite.Request{ ID: "abcd-1", Client: &clientregistry.Client{}, @@ -358,7 +363,7 @@ func TestReadFromSecret(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1"},"version":"6","active": true}`), + "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1"},"version":"` + expectedVersion + `","active": true}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/not-access-token", @@ -381,7 +386,7 @@ func TestReadFromSecret(t *testing.T) { }, Type: "storage.pinniped.dev/access-token", }, - wantErr: "access token request data has wrong version: access token session has version wrong-version-here instead of 6", + wantErr: "access token request data has wrong version: access token session has version wrong-version-here instead of " + expectedVersion, }, { name: "missing request", @@ -394,7 +399,7 @@ func TestReadFromSecret(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"version":"6","active": true}`), + "pinniped-storage-data": []byte(`{"version":"` + expectedVersion + `","active": true}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/access-token", diff --git a/internal/fositestorage/authorizationcode/authorizationcode.go b/internal/fositestorage/authorizationcode/authorizationcode.go index 11ad6d999..f1187ec83 100644 --- a/internal/fositestorage/authorizationcode/authorizationcode.go +++ b/internal/fositestorage/authorizationcode/authorizationcode.go @@ -35,7 +35,8 @@ const ( // Version 4 is when fosite added json tags to their openid.DefaultSession struct. // Version 5 is when we added the UpstreamUsername and UpstreamGroups fields to psession.CustomSessionData. // Version 6 is when we upgraded fosite in Dec 2023. - authorizeCodeStorageVersion = "6" + // Version 7 is when OIDCClients were given configurable ID token lifetimes. + authorizeCodeStorageVersion = "7" ) var _ oauth2.AuthorizeCodeStorage = &authorizeCodeStorage{} @@ -393,5 +394,5 @@ const ExpectedAuthorizeCodeSessionJSONFromFuzzing = `{ "筫MN\u0026錝D肁Ŷɽ蔒PR}Ųʓl{" ] }, - "version": "6" + "version": "7" }` diff --git a/internal/fositestorage/authorizationcode/authorizationcode_test.go b/internal/fositestorage/authorizationcode/authorizationcode_test.go index a057062f2..568393f32 100644 --- a/internal/fositestorage/authorizationcode/authorizationcode_test.go +++ b/internal/fositestorage/authorizationcode/authorizationcode_test.go @@ -41,12 +41,17 @@ import ( "go.pinniped.dev/internal/testutil" ) -const namespace = "test-ns" +const ( + namespace = "test-ns" + expectedVersion = "7" // update this when you update the storage version in the production code +) -var fakeNow = time.Date(2030, time.January, 1, 0, 0, 0, 0, time.UTC) -var lifetime = time.Minute * 10 -var fakeNowPlusLifetimeAsString = metav1.Time{Time: fakeNow.Add(lifetime)}.Format(time.RFC3339) -var lifetimeFunc = func(requester fosite.Requester) time.Duration { return lifetime } +var ( + fakeNow = time.Date(2030, time.January, 1, 0, 0, 0, 0, time.UTC) + lifetime = time.Minute * 10 + fakeNowPlusLifetimeAsString = metav1.Time{Time: fakeNow.Add(lifetime)}.Format(time.RFC3339) + lifetimeFunc = func(requester fosite.Requester) time.Duration { return lifetime } +) func TestAuthorizationCodeStorage(t *testing.T) { secretsGVR := schema.GroupVersionResource{ @@ -68,7 +73,7 @@ func TestAuthorizationCodeStorage(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"active":true,"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":"","IDTokenLifetimeConfiguration":42000000000},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"id_token_claims":null,"headers":null,"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","upstreamUsername":"fake-upstream-username","upstreamGroups":["fake-upstream-group1","fake-upstream-group2"],"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"6"}`), + "pinniped-storage-data": []byte(`{"active":true,"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":"","IDTokenLifetimeConfiguration":42000000000},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"id_token_claims":null,"headers":null,"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","upstreamUsername":"fake-upstream-username","upstreamGroups":["fake-upstream-group1","fake-upstream-group2"],"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"` + expectedVersion + `"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/authcode", @@ -88,7 +93,7 @@ func TestAuthorizationCodeStorage(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"active":false,"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":"","IDTokenLifetimeConfiguration":42000000000},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"id_token_claims":null,"headers":null,"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","upstreamUsername":"fake-upstream-username","upstreamGroups":["fake-upstream-group1","fake-upstream-group2"],"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"6"}`), + "pinniped-storage-data": []byte(`{"active":false,"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":"","IDTokenLifetimeConfiguration":42000000000},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"id_token_claims":null,"headers":null,"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","upstreamUsername":"fake-upstream-username","upstreamGroups":["fake-upstream-group1","fake-upstream-group2"],"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"` + expectedVersion + `"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/authcode", @@ -218,7 +223,7 @@ func TestWrongVersion(t *testing.T) { _, err = storage.GetAuthorizeCodeSession(ctx, "fancy-signature", nil) - require.EqualError(t, err, "authorization request data has wrong version: authorization code session for fancy-signature has version not-the-right-version instead of 6") + require.EqualError(t, err, "authorization request data has wrong version: authorization code session for fancy-signature has version not-the-right-version instead of "+expectedVersion) } func TestNilSessionRequest(t *testing.T) { @@ -233,7 +238,7 @@ func TestNilSessionRequest(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"nonsense-key": "nonsense-value", "version":"6", "active": true}`), + "pinniped-storage-data": []byte(`{"nonsense-key": "nonsense-value", "version":"` + expectedVersion + `", "active": true}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/authcode", @@ -403,7 +408,7 @@ func TestFuzzAndJSONNewValidEmptyAuthorizeCodeSession(t *testing.T) { // set these to match CreateAuthorizeCodeSession so that .JSONEq works validSession.Active = true - validSession.Version = "6" // update this when you update the storage version in the production code + validSession.Version = expectedVersion validSessionJSONBytes, err := json.MarshalIndent(validSession, "", "\t") require.NoError(t, err) @@ -414,9 +419,15 @@ func TestFuzzAndJSONNewValidEmptyAuthorizeCodeSession(t *testing.T) { t.Log("actual value from fuzzing", authorizeCodeSessionJSONFromFuzzing) // can be useful when updating expected value - // while the fuzzer will panic if AuthorizeRequest changes in a way that cannot be fuzzed, - // if it adds a new field that can be fuzzed, this check will fail - // thus if AuthorizeRequest changes, we will detect it here (though we could possibly miss an omitempty field) + // While the fuzzer will panic if AuthorizeRequest changes in a way that cannot be fuzzed, + // if it adds a new field that can be fuzzed, this check will fail. + // Thus, when AuthorizeRequest changes, we will detect it here (though we could possibly miss an omitempty field). + // Whenever this changes, consider increasing the session storage versions. Consider what would happen if an old + // version of a session Secret is read by new code after a Pinniped upgrade? For example, would there be new unset + // fields in the deserialized session data structs? If so, you probably want to increase the storage versions to + // cause those old session Secrets to be discarded upon read after an upgrade. + // Note that when you change the storage version, you will also need to change it in the JSON content of the + // expected value for this assertion. require.JSONEq(t, ExpectedAuthorizeCodeSessionJSONFromFuzzing, authorizeCodeSessionJSONFromFuzzing, "actual:\n%s", authorizeCodeSessionJSONFromFuzzing) } @@ -438,13 +449,13 @@ func TestReadFromSecret(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","session":{"fosite":{"id_token_claims":{"jti": "xyz"},"headers":{"extra":{"myheader": "foo"}},"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","upstreamUsername":"fake-upstream-username","upstreamGroups":["fake-upstream-group1","fake-upstream-group2"],"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token"}}}},"version":"6","active": true}`), + "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","session":{"fosite":{"id_token_claims":{"jti": "xyz"},"headers":{"extra":{"myheader": "foo"}},"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","upstreamUsername":"fake-upstream-username","upstreamGroups":["fake-upstream-group1","fake-upstream-group2"],"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token"}}}},"version":"` + expectedVersion + `","active": true}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/authcode", }, wantSession: &Session{ - Version: "6", + Version: expectedVersion, Active: true, Request: &fosite.Request{ ID: "abcd-1", @@ -482,7 +493,7 @@ func TestReadFromSecret(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1"},"version":"6","active": true}`), + "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1"},"version":"` + expectedVersion + `","active": true}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/not-authcode", @@ -505,7 +516,7 @@ func TestReadFromSecret(t *testing.T) { }, Type: "storage.pinniped.dev/authcode", }, - wantErr: "authorization request data has wrong version: authorization code session has version wrong-version-here instead of 6", + wantErr: "authorization request data has wrong version: authorization code session has version wrong-version-here instead of " + expectedVersion, }, { name: "missing request", @@ -518,7 +529,7 @@ func TestReadFromSecret(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"version":"6","active": true}`), + "pinniped-storage-data": []byte(`{"version":"` + expectedVersion + `","active": true}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/authcode", diff --git a/internal/fositestorage/openidconnect/openidconnect.go b/internal/fositestorage/openidconnect/openidconnect.go index 58e560a10..a04bea4c3 100644 --- a/internal/fositestorage/openidconnect/openidconnect.go +++ b/internal/fositestorage/openidconnect/openidconnect.go @@ -35,7 +35,8 @@ const ( // Version 4 is when fosite added json tags to their openid.DefaultSession struct. // Version 5 is when we added the UpstreamUsername and UpstreamGroups fields to psession.CustomSessionData. // Version 6 is when we upgraded fosite in Dec 2023. - oidcStorageVersion = "6" + // Version 7 is when OIDCClients were given configurable ID token lifetimes. + oidcStorageVersion = "7" ) var _ openid.OpenIDConnectRequestStorage = &openIDConnectRequestStorage{} diff --git a/internal/fositestorage/openidconnect/openidconnect_test.go b/internal/fositestorage/openidconnect/openidconnect_test.go index 781973d36..8f64299ed 100644 --- a/internal/fositestorage/openidconnect/openidconnect_test.go +++ b/internal/fositestorage/openidconnect/openidconnect_test.go @@ -27,12 +27,17 @@ import ( "go.pinniped.dev/internal/testutil" ) -const namespace = "test-ns" +const ( + namespace = "test-ns" + expectedVersion = "7" // update this when you update the storage version in the production code +) -var fakeNow = time.Date(2030, time.January, 1, 0, 0, 0, 0, time.UTC) -var lifetime = time.Minute * 10 -var fakeNowPlusLifetimeAsString = metav1.Time{Time: fakeNow.Add(lifetime)}.Format(time.RFC3339) -var lifetimeFunc = func(requester fosite.Requester) time.Duration { return lifetime } +var ( + fakeNow = time.Date(2030, time.January, 1, 0, 0, 0, 0, time.UTC) + lifetime = time.Minute * 10 + fakeNowPlusLifetimeAsString = metav1.Time{Time: fakeNow.Add(lifetime)}.Format(time.RFC3339) + lifetimeFunc = func(requester fosite.Requester) time.Duration { return lifetime } +) func TestOpenIdConnectStorage(t *testing.T) { secretsGVR := schema.GroupVersionResource{ @@ -54,7 +59,7 @@ func TestOpenIdConnectStorage(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":"","IDTokenLifetimeConfiguration":42000000000},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"id_token_claims":null,"headers":null,"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","upstreamUsername":"fake-upstream-username","upstreamGroups":["fake-upstream-group1","fake-upstream-group2"],"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"6"}`), + "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":"","IDTokenLifetimeConfiguration":42000000000},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"id_token_claims":null,"headers":null,"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","upstreamUsername":"fake-upstream-username","upstreamGroups":["fake-upstream-group1","fake-upstream-group2"],"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"` + expectedVersion + `"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/oidc", @@ -151,7 +156,7 @@ func TestWrongVersion(t *testing.T) { _, err = storage.GetOpenIDConnectSession(ctx, "fancy-code.fancy-signature", nil) - require.EqualError(t, err, "oidc request data has wrong version: oidc session for fancy-signature has version not-the-right-version instead of 6") + require.EqualError(t, err, "oidc request data has wrong version: oidc session for fancy-signature has version not-the-right-version instead of "+expectedVersion) } func TestNilSessionRequest(t *testing.T) { @@ -166,7 +171,7 @@ func TestNilSessionRequest(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"nonsense-key": "nonsense-value","version":"6"}`), + "pinniped-storage-data": []byte(`{"nonsense-key": "nonsense-value","version":"` + expectedVersion + `"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/oidc", diff --git a/internal/fositestorage/pkce/pkce.go b/internal/fositestorage/pkce/pkce.go index 9b6a14d4c..b0d371b6a 100644 --- a/internal/fositestorage/pkce/pkce.go +++ b/internal/fositestorage/pkce/pkce.go @@ -33,7 +33,8 @@ const ( // Version 4 is when fosite added json tags to their openid.DefaultSession struct. // Version 5 is when we added the UpstreamUsername and UpstreamGroups fields to psession.CustomSessionData. // Version 6 is when we upgraded fosite in Dec 2023. - pkceStorageVersion = "6" + // Version 7 is when OIDCClients were given configurable ID token lifetimes. + pkceStorageVersion = "7" ) var _ pkce.PKCERequestStorage = &pkceStorage{} diff --git a/internal/fositestorage/pkce/pkce_test.go b/internal/fositestorage/pkce/pkce_test.go index 100cbd211..ed38d51f6 100644 --- a/internal/fositestorage/pkce/pkce_test.go +++ b/internal/fositestorage/pkce/pkce_test.go @@ -27,12 +27,17 @@ import ( "go.pinniped.dev/internal/testutil" ) -const namespace = "test-ns" +const ( + namespace = "test-ns" + expectedVersion = "7" // update this when you update the storage version in the production code +) -var fakeNow = time.Date(2030, time.January, 1, 0, 0, 0, 0, time.UTC) -var lifetime = time.Minute * 10 -var fakeNowPlusLifetimeAsString = metav1.Time{Time: fakeNow.Add(lifetime)}.Format(time.RFC3339) -var lifetimeFunc = func(requester fosite.Requester) time.Duration { return lifetime } +var ( + fakeNow = time.Date(2030, time.January, 1, 0, 0, 0, 0, time.UTC) + lifetime = time.Minute * 10 + fakeNowPlusLifetimeAsString = metav1.Time{Time: fakeNow.Add(lifetime)}.Format(time.RFC3339) + lifetimeFunc = func(requester fosite.Requester) time.Duration { return lifetime } +) func TestPKCEStorage(t *testing.T) { secretsGVR := schema.GroupVersionResource{ @@ -54,7 +59,7 @@ func TestPKCEStorage(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":"","IDTokenLifetimeConfiguration":42000000000},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"id_token_claims":null,"headers":null,"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","upstreamUsername":"fake-upstream-username","upstreamGroups":["fake-upstream-group1","fake-upstream-group2"],"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"6"}`), + "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":"","IDTokenLifetimeConfiguration":42000000000},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"id_token_claims":null,"headers":null,"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","upstreamUsername":"fake-upstream-username","upstreamGroups":["fake-upstream-group1","fake-upstream-group2"],"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"` + expectedVersion + `"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/pkce", @@ -153,7 +158,7 @@ func TestWrongVersion(t *testing.T) { _, err = storage.GetPKCERequestSession(ctx, "fancy-signature", nil) - require.EqualError(t, err, "pkce request data has wrong version: pkce session for fancy-signature has version not-the-right-version instead of 6") + require.EqualError(t, err, "pkce request data has wrong version: pkce session for fancy-signature has version not-the-right-version instead of "+expectedVersion) } func TestNilSessionRequest(t *testing.T) { @@ -171,7 +176,7 @@ func TestNilSessionRequest(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"nonsense-key": "nonsense-value","version":"6"}`), + "pinniped-storage-data": []byte(`{"nonsense-key": "nonsense-value","version":"` + expectedVersion + `"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/pkce", diff --git a/internal/fositestorage/refreshtoken/refreshtoken.go b/internal/fositestorage/refreshtoken/refreshtoken.go index d87c6bb9b..efc96071b 100644 --- a/internal/fositestorage/refreshtoken/refreshtoken.go +++ b/internal/fositestorage/refreshtoken/refreshtoken.go @@ -34,7 +34,8 @@ const ( // Version 4 is when fosite added json tags to their openid.DefaultSession struct. // Version 5 is when we added the UpstreamUsername and UpstreamGroups fields to psession.CustomSessionData. // Version 6 is when we upgraded fosite in Dec 2023. - refreshTokenStorageVersion = "6" + // Version 7 is when OIDCClients were given configurable ID token lifetimes. + refreshTokenStorageVersion = "7" ) type RevocationStorage interface { diff --git a/internal/fositestorage/refreshtoken/refreshtoken_test.go b/internal/fositestorage/refreshtoken/refreshtoken_test.go index d6d987c38..6075d0723 100644 --- a/internal/fositestorage/refreshtoken/refreshtoken_test.go +++ b/internal/fositestorage/refreshtoken/refreshtoken_test.go @@ -28,17 +28,23 @@ import ( "go.pinniped.dev/internal/testutil" ) -const namespace = "test-ns" +const ( + namespace = "test-ns" + expectedVersion = "7" // update this when you update the storage version in the production code +) -var secretsGVR = schema.GroupVersionResource{ - Group: "", - Version: "v1", - Resource: "secrets", -} -var fakeNow = time.Date(2030, time.January, 1, 0, 0, 0, 0, time.UTC) -var lifetime = time.Minute * 10 -var fakeNowPlusLifetimeAsString = metav1.Time{Time: fakeNow.Add(lifetime)}.Format(time.RFC3339) -var lifetimeFunc = func(requester fosite.Requester) time.Duration { return lifetime } +var ( + fakeNow = time.Date(2030, time.January, 1, 0, 0, 0, 0, time.UTC) + lifetime = time.Minute * 10 + fakeNowPlusLifetimeAsString = metav1.Time{Time: fakeNow.Add(lifetime)}.Format(time.RFC3339) + lifetimeFunc = func(requester fosite.Requester) time.Duration { return lifetime } + + secretsGVR = schema.GroupVersionResource{ + Group: "", + Version: "v1", + Resource: "secrets", + } +) func TestRefreshTokenStorage(t *testing.T) { wantActions := []coretesting.Action{ @@ -55,7 +61,7 @@ func TestRefreshTokenStorage(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":"","IDTokenLifetimeConfiguration":42000000000},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"id_token_claims":null,"headers":null,"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","upstreamUsername":"fake-upstream-username","upstreamGroups":["fake-upstream-group1","fake-upstream-group2"],"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"6"}`), + "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":"","IDTokenLifetimeConfiguration":42000000000},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"id_token_claims":null,"headers":null,"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","upstreamUsername":"fake-upstream-username","upstreamGroups":["fake-upstream-group1","fake-upstream-group2"],"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"` + expectedVersion + `"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/refresh-token", @@ -137,7 +143,7 @@ func TestRefreshTokenStorageRevocation(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":"","IDTokenLifetimeConfiguration":0},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"id_token_claims":null,"headers":null,"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","upstreamUsername":"fake-upstream-username","upstreamGroups":["fake-upstream-group1","fake-upstream-group2"],"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"6"}`), + "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":"","IDTokenLifetimeConfiguration":0},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"id_token_claims":null,"headers":null,"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","upstreamUsername":"fake-upstream-username","upstreamGroups":["fake-upstream-group1","fake-upstream-group2"],"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"` + expectedVersion + `"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/refresh-token", @@ -192,7 +198,7 @@ func TestRefreshTokenStorageRevokeRefreshTokenMaybeGracePeriod(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":"","IDTokenLifetimeConfiguration":0},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"id_token_claims":null,"headers":null,"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","upstreamUsername":"fake-upstream-username","upstreamGroups":["fake-upstream-group1","fake-upstream-group2"],"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"6"}`), + "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":"","IDTokenLifetimeConfiguration":0},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"id_token_claims":null,"headers":null,"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","upstreamUsername":"fake-upstream-username","upstreamGroups":["fake-upstream-group1","fake-upstream-group2"],"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"` + expectedVersion + `"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/refresh-token", @@ -266,7 +272,7 @@ func TestWrongVersion(t *testing.T) { _, err = storage.GetRefreshTokenSession(ctx, "fancy-signature", nil) - require.EqualError(t, err, "refresh token request data has wrong version: refresh token session for fancy-signature has version not-the-right-version instead of 6") + require.EqualError(t, err, "refresh token request data has wrong version: refresh token session for fancy-signature has version not-the-right-version instead of "+expectedVersion) } func TestNilSessionRequest(t *testing.T) { @@ -284,7 +290,7 @@ func TestNilSessionRequest(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"nonsense-key": "nonsense-value","version":"6"}`), + "pinniped-storage-data": []byte(`{"nonsense-key": "nonsense-value","version":"` + expectedVersion + `"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/refresh-token", @@ -371,13 +377,13 @@ func TestReadFromSecret(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","session":{"fosite":{"id_token_claims":{"jti": "xyz"},"headers":{"extra":{"myheader": "foo"}},"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","upstreamUsername":"fake-upstream-username","upstreamGroups":["fake-upstream-group1","fake-upstream-group2"],"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token"}}}},"version":"6","active": true}`), + "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","session":{"fosite":{"id_token_claims":{"jti": "xyz"},"headers":{"extra":{"myheader": "foo"}},"expires_at":null,"username":"snorlax","subject":"panda"},"custom":{"username":"fake-username","upstreamUsername":"fake-upstream-username","upstreamGroups":["fake-upstream-group1","fake-upstream-group2"],"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token"}}}},"version":"` + expectedVersion + `","active": true}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/refresh-token", }, wantSession: &Session{ - Version: "6", + Version: expectedVersion, Request: &fosite.Request{ ID: "abcd-1", Client: &clientregistry.Client{}, @@ -414,7 +420,7 @@ func TestReadFromSecret(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1"},"version":"6","active": true}`), + "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1"},"version":"` + expectedVersion + `","active": true}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/not-refresh-token", @@ -437,7 +443,7 @@ func TestReadFromSecret(t *testing.T) { }, Type: "storage.pinniped.dev/refresh-token", }, - wantErr: "refresh token request data has wrong version: refresh token session has version wrong-version-here instead of 6", + wantErr: "refresh token request data has wrong version: refresh token session has version wrong-version-here instead of " + expectedVersion, }, { name: "missing request", @@ -450,7 +456,7 @@ func TestReadFromSecret(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"version":"6","active": true}`), + "pinniped-storage-data": []byte(`{"version":"` + expectedVersion + `","active": true}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/refresh-token", diff --git a/test/integration/supervisor_storage_test.go b/test/integration/supervisor_storage_test.go index b0826e035..9da514620 100644 --- a/test/integration/supervisor_storage_test.go +++ b/test/integration/supervisor_storage_test.go @@ -93,7 +93,7 @@ func TestAuthorizeCodeStorage(t *testing.T) { // Note that CreateAuthorizeCodeSession() sets Active to true and also sets the Version before storing the session, // so expect those here. session.Active = true - session.Version = "6" // this is the value of the authorizationcode.authorizeCodeStorageVersion constant + session.Version = "7" // this is the value of the authorizationcode.authorizeCodeStorageVersion constant expectedSessionStorageJSON, err := json.Marshal(session) require.NoError(t, err) require.JSONEq(t, string(expectedSessionStorageJSON), string(initialSecret.Data["pinniped-storage-data"])) From a1efcefdce841aab2e54977bdcf1e3e35ac5fc81 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Thu, 18 Apr 2024 09:48:21 -0700 Subject: [PATCH 6/8] Unit tests for token endpoint for custom ID token lifetimes --- .../endpoints/auth/auth_handler_test.go | 2 +- .../callback/callback_handler_test.go | 10 +- .../login/post_login_handler_test.go | 12 +- .../endpoints/token/token_handler_test.go | 184 +++++++++++++++--- internal/testutil/oidcclient.go | 10 +- 5 files changed, 177 insertions(+), 41 deletions(-) diff --git a/internal/federationdomain/endpoints/auth/auth_handler_test.go b/internal/federationdomain/endpoints/auth/auth_handler_test.go index 21171a639..52cd06b29 100644 --- a/internal/federationdomain/endpoints/auth/auth_handler_test.go +++ b/internal/federationdomain/endpoints/auth/auth_handler_test.go @@ -613,7 +613,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo addFullyCapableDynamicClientAndSecretToKubeResources := func(t *testing.T, supervisorClient *supervisorfake.Clientset, kubeClient *fake.Clientset) { oidcClient, secret := testutil.FullyCapableOIDCClientAndStorageSecret(t, - "some-namespace", dynamicClientID, dynamicClientUID, downstreamRedirectURI, + "some-namespace", dynamicClientID, dynamicClientUID, downstreamRedirectURI, nil, []string{testutil.HashedPassword1AtGoMinCost}, oidcclientvalidator.Validate) require.NoError(t, supervisorClient.Tracker().Add(oidcClient)) require.NoError(t, kubeClient.Tracker().Add(secret)) diff --git a/internal/federationdomain/endpoints/callback/callback_handler_test.go b/internal/federationdomain/endpoints/callback/callback_handler_test.go index d02eba516..433365b9c 100644 --- a/internal/federationdomain/endpoints/callback/callback_handler_test.go +++ b/internal/federationdomain/endpoints/callback/callback_handler_test.go @@ -171,7 +171,7 @@ func TestCallbackEndpoint(t *testing.T) { addFullyCapableDynamicClientAndSecretToKubeResources := func(t *testing.T, supervisorClient *supervisorfake.Clientset, kubeClient *fake.Clientset) { oidcClient, secret := testutil.FullyCapableOIDCClientAndStorageSecret(t, - "some-namespace", downstreamDynamicClientID, downstreamDynamicClientUID, downstreamRedirectURI, + "some-namespace", downstreamDynamicClientID, downstreamDynamicClientUID, downstreamRedirectURI, nil, []string{testutil.HashedPassword1AtGoMinCost}, oidcclientvalidator.Validate) require.NoError(t, supervisorClient.Tracker().Add(oidcClient)) require.NoError(t, kubeClient.Tracker().Add(secret)) @@ -815,7 +815,7 @@ func TestCallbackEndpoint(t *testing.T) { "some-namespace", downstreamDynamicClientID, downstreamDynamicClientUID, []configv1alpha1.GrantType{"authorization_code", "refresh_token"}, // token exchange not allowed (required to exclude username scope) []configv1alpha1.Scope{"openid", "offline_access", "groups"}, // username not allowed - downstreamRedirectURI, []string{testutil.HashedPassword1AtGoMinCost}, oidcclientvalidator.Validate) + downstreamRedirectURI, nil, []string{testutil.HashedPassword1AtGoMinCost}, oidcclientvalidator.Validate) require.NoError(t, supervisorClient.Tracker().Add(oidcClient)) require.NoError(t, kubeClient.Tracker().Add(secret)) }, @@ -858,7 +858,7 @@ func TestCallbackEndpoint(t *testing.T) { "some-namespace", downstreamDynamicClientID, downstreamDynamicClientUID, []configv1alpha1.GrantType{"authorization_code", "refresh_token"}, // token exchange not allowed (required to exclude groups scope) []configv1alpha1.Scope{"openid", "offline_access", "username"}, // groups not allowed - downstreamRedirectURI, []string{testutil.HashedPassword1AtGoMinCost}, oidcclientvalidator.Validate) + downstreamRedirectURI, nil, []string{testutil.HashedPassword1AtGoMinCost}, oidcclientvalidator.Validate) require.NoError(t, supervisorClient.Tracker().Add(oidcClient)) require.NoError(t, kubeClient.Tracker().Add(secret)) }, @@ -1094,7 +1094,7 @@ func TestCallbackEndpoint(t *testing.T) { "some-namespace", downstreamDynamicClientID, downstreamDynamicClientUID, []configv1alpha1.GrantType{"authorization_code", "refresh_token"}, // token exchange not allowed (required to exclude username scope) []configv1alpha1.Scope{"openid", "offline_access", "groups"}, // username not allowed - downstreamRedirectURI, []string{testutil.HashedPassword1AtGoMinCost}, oidcclientvalidator.Validate) + downstreamRedirectURI, nil, []string{testutil.HashedPassword1AtGoMinCost}, oidcclientvalidator.Validate) require.NoError(t, supervisorClient.Tracker().Add(oidcClient)) require.NoError(t, kubeClient.Tracker().Add(secret)) }, @@ -1123,7 +1123,7 @@ func TestCallbackEndpoint(t *testing.T) { "some-namespace", downstreamDynamicClientID, downstreamDynamicClientUID, []configv1alpha1.GrantType{"authorization_code", "refresh_token"}, // token exchange not allowed (required to exclude groups scope) []configv1alpha1.Scope{"openid", "offline_access", "username"}, // groups not allowed - downstreamRedirectURI, []string{testutil.HashedPassword1AtGoMinCost}, oidcclientvalidator.Validate) + downstreamRedirectURI, nil, []string{testutil.HashedPassword1AtGoMinCost}, oidcclientvalidator.Validate) require.NoError(t, supervisorClient.Tracker().Add(oidcClient)) require.NoError(t, kubeClient.Tracker().Add(secret)) }, diff --git a/internal/federationdomain/endpoints/login/post_login_handler_test.go b/internal/federationdomain/endpoints/login/post_login_handler_test.go index 789772f3b..8d1c3112e 100644 --- a/internal/federationdomain/endpoints/login/post_login_handler_test.go +++ b/internal/federationdomain/endpoints/login/post_login_handler_test.go @@ -280,7 +280,7 @@ func TestPostLoginEndpoint(t *testing.T) { addFullyCapableDynamicClientAndSecretToKubeResources := func(t *testing.T, supervisorClient *supervisorfake.Clientset, kubeClient *fake.Clientset) { oidcClient, secret := testutil.FullyCapableOIDCClientAndStorageSecret(t, - "some-namespace", downstreamDynamicClientID, downstreamDynamicClientUID, downstreamRedirectURI, + "some-namespace", downstreamDynamicClientID, downstreamDynamicClientUID, downstreamRedirectURI, nil, []string{testutil.HashedPassword1AtGoMinCost}, oidcclientvalidator.Validate) require.NoError(t, supervisorClient.Tracker().Add(oidcClient)) require.NoError(t, kubeClient.Tracker().Add(secret)) @@ -615,7 +615,7 @@ func TestPostLoginEndpoint(t *testing.T) { "some-namespace", downstreamDynamicClientID, downstreamDynamicClientUID, []configv1alpha1.GrantType{"authorization_code", "refresh_token"}, // token exchange not allowed (required to exclude username scope) []configv1alpha1.Scope{"openid", "offline_access", "groups"}, // username not allowed - downstreamRedirectURI, []string{testutil.HashedPassword1AtGoMinCost}, oidcclientvalidator.Validate) + downstreamRedirectURI, nil, []string{testutil.HashedPassword1AtGoMinCost}, oidcclientvalidator.Validate) require.NoError(t, supervisorClient.Tracker().Add(oidcClient)) require.NoError(t, kubeClient.Tracker().Add(secret)) }, @@ -649,7 +649,7 @@ func TestPostLoginEndpoint(t *testing.T) { "some-namespace", downstreamDynamicClientID, downstreamDynamicClientUID, []configv1alpha1.GrantType{"authorization_code", "refresh_token"}, // token exchange not allowed (required to exclude groups scope) []configv1alpha1.Scope{"openid", "offline_access", "username"}, // groups not allowed - downstreamRedirectURI, []string{testutil.HashedPassword1AtGoMinCost}, oidcclientvalidator.Validate) + downstreamRedirectURI, nil, []string{testutil.HashedPassword1AtGoMinCost}, oidcclientvalidator.Validate) require.NoError(t, supervisorClient.Tracker().Add(oidcClient)) require.NoError(t, kubeClient.Tracker().Add(secret)) }, @@ -693,7 +693,7 @@ func TestPostLoginEndpoint(t *testing.T) { "some-namespace", downstreamDynamicClientID, downstreamDynamicClientUID, []configv1alpha1.GrantType{"authorization_code", "refresh_token"}, // token exchange not allowed (required to exclude groups scope) []configv1alpha1.Scope{"openid", "offline_access", "username"}, // groups not allowed - downstreamRedirectURI, []string{testutil.HashedPassword1AtGoMinCost}, oidcclientvalidator.Validate) + downstreamRedirectURI, nil, []string{testutil.HashedPassword1AtGoMinCost}, oidcclientvalidator.Validate) require.NoError(t, supervisorClient.Tracker().Add(oidcClient)) require.NoError(t, kubeClient.Tracker().Add(secret)) }, @@ -1053,7 +1053,7 @@ func TestPostLoginEndpoint(t *testing.T) { "some-namespace", downstreamDynamicClientID, downstreamDynamicClientUID, []configv1alpha1.GrantType{"authorization_code", "refresh_token"}, // token exchange not allowed (required to exclude username scope) []configv1alpha1.Scope{"openid", "offline_access", "groups"}, // username not allowed - downstreamRedirectURI, []string{testutil.HashedPassword1AtGoMinCost}, oidcclientvalidator.Validate) + downstreamRedirectURI, nil, []string{testutil.HashedPassword1AtGoMinCost}, oidcclientvalidator.Validate) require.NoError(t, supervisorClient.Tracker().Add(oidcClient)) require.NoError(t, kubeClient.Tracker().Add(secret)) }, @@ -1073,7 +1073,7 @@ func TestPostLoginEndpoint(t *testing.T) { "some-namespace", downstreamDynamicClientID, downstreamDynamicClientUID, []configv1alpha1.GrantType{"authorization_code", "refresh_token"}, // token exchange not allowed (required to exclude groups scope) []configv1alpha1.Scope{"openid", "offline_access", "username"}, // groups not allowed - downstreamRedirectURI, []string{testutil.HashedPassword1AtGoMinCost}, oidcclientvalidator.Validate) + downstreamRedirectURI, nil, []string{testutil.HashedPassword1AtGoMinCost}, oidcclientvalidator.Validate) require.NoError(t, supervisorClient.Tracker().Add(oidcClient)) require.NoError(t, kubeClient.Tracker().Add(secret)) }, diff --git a/internal/federationdomain/endpoints/token/token_handler_test.go b/internal/federationdomain/endpoints/token/token_handler_test.go index bfbfa37f1..e33fee2d1 100644 --- a/internal/federationdomain/endpoints/token/token_handler_test.go +++ b/internal/federationdomain/endpoints/token/token_handler_test.go @@ -38,6 +38,7 @@ import ( "k8s.io/apiserver/pkg/warning" "k8s.io/client-go/kubernetes/fake" v1 "k8s.io/client-go/kubernetes/typed/core/v1" + "k8s.io/utils/ptr" "k8s.io/utils/strings/slices" configv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1" @@ -291,6 +292,14 @@ type tokenEndpointResponseExpectedValues struct { wantCustomSessionDataStored *psession.CustomSessionData wantWarnings []RecordedWarning wantAdditionalClaims map[string]interface{} + // The expected lifetime of the ID tokens issued by authcode exchange and refresh, but not token exchange. + // When zero, will assume that the test wants the default value for ID token lifetime. + wantIDTokenLifetimeSeconds int +} + +func withWantCustomIDTokenLifetime(wantIDTokenLifetimeSeconds int, w tokenEndpointResponseExpectedValues) tokenEndpointResponseExpectedValues { + w.wantIDTokenLifetimeSeconds = wantIDTokenLifetimeSeconds + return w } type authcodeExchangeInputs struct { @@ -308,6 +317,7 @@ func addFullyCapableDynamicClientAndSecretToKubeResources(t *testing.T, supervis dynamicClientID, dynamicClientUID, goodRedirectURI, + nil, // no custom ID token lifetime []string{testutil.HashedPassword1AtGoMinCost, testutil.HashedPassword2AtGoMinCost}, oidcclientvalidator.Validate, ) @@ -315,6 +325,22 @@ func addFullyCapableDynamicClientAndSecretToKubeResources(t *testing.T, supervis require.NoError(t, kubeClient.Tracker().Add(secret)) } +func addFullyCapableDynamicClientWithCustomIDTokenLifetimeAndSecretToKubeResources(idTokenLifetime int32) func(t *testing.T, supervisorClient *supervisorfake.Clientset, kubeClient *fake.Clientset) { + return func(t *testing.T, supervisorClient *supervisorfake.Clientset, kubeClient *fake.Clientset) { + oidcClient, secret := testutil.FullyCapableOIDCClientAndStorageSecret(t, + "some-namespace", + dynamicClientID, + dynamicClientUID, + goodRedirectURI, + ptr.To(idTokenLifetime), // with custom ID token lifetime + []string{testutil.HashedPassword1AtGoMinCost, testutil.HashedPassword2AtGoMinCost}, + oidcclientvalidator.Validate, + ) + require.NoError(t, supervisorClient.Tracker().Add(oidcClient)) + require.NoError(t, kubeClient.Tracker().Add(secret)) + } +} + func modifyAuthcodeTokenRequestWithDynamicClientAuth(r *http.Request, authCode string) { r.Body = happyAuthcodeRequestBody(authCode).WithClientID("").ReadCloser() // No client_id in body. r.SetBasicAuth(dynamicClientID, testutil.PlaintextPassword1) // Use basic auth header instead. @@ -403,6 +429,27 @@ func TestTokenEndpointAuthcodeExchange(t *testing.T) { }, }, }, + { + name: "request is valid and tokens are issued for dynamic client which has a custom ID token lifetime", + kubeResources: addFullyCapableDynamicClientWithCustomIDTokenLifetimeAndSecretToKubeResources(4242), + authcodeExchange: authcodeExchangeInputs{ + modifyAuthRequest: func(r *http.Request) { + addDynamicClientIDToFormPostBody(r) + r.Form.Set("scope", "openid pinniped:request-audience username groups") + }, + modifyTokenRequest: modifyAuthcodeTokenRequestWithDynamicClientAuth, + 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, + wantIDTokenLifetimeSeconds: 4242, + }, + }, + }, { name: "request is valid and tokens are issued for dynamic client with additional claims", kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, @@ -972,14 +1019,16 @@ func TestTokenEndpointTokenExchange(t *testing.T) { // tests for grant_type "urn wantGroups: goodGroups, } - successfulAuthCodeExchangeUsingDynamicClient := 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, + successfulAuthCodeExchangeUsingDynamicClient := func() tokenEndpointResponseExpectedValues { + return 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, + } } doValidAuthCodeExchange := authcodeExchangeInputs{ @@ -989,13 +1038,15 @@ func TestTokenEndpointTokenExchange(t *testing.T) { // tests for grant_type "urn want: successfulAuthCodeExchange, } - doValidAuthCodeExchangeUsingDynamicClient := authcodeExchangeInputs{ - modifyAuthRequest: func(authRequest *http.Request) { - addDynamicClientIDToFormPostBody(authRequest) - authRequest.Form.Set("scope", "openid pinniped:request-audience username groups") - }, - modifyTokenRequest: modifyAuthcodeTokenRequestWithDynamicClientAuth, - want: successfulAuthCodeExchangeUsingDynamicClient, + doValidAuthCodeExchangeUsingDynamicClient := func() authcodeExchangeInputs { + return authcodeExchangeInputs{ + modifyAuthRequest: func(authRequest *http.Request) { + addDynamicClientIDToFormPostBody(authRequest) + authRequest.Form.Set("scope", "openid pinniped:request-audience username groups") + }, + modifyTokenRequest: modifyAuthcodeTokenRequestWithDynamicClientAuth, + want: successfulAuthCodeExchangeUsingDynamicClient(), + } } tests := []struct { @@ -1081,7 +1132,27 @@ func TestTokenEndpointTokenExchange(t *testing.T) { // tests for grant_type "urn { name: "happy path with dynamic client", kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, - authcodeExchange: doValidAuthCodeExchangeUsingDynamicClient, + authcodeExchange: doValidAuthCodeExchangeUsingDynamicClient(), + 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 which has a custom ID token lifetime configuration (which does not apply to ID tokens from token exchanges)", + kubeResources: addFullyCapableDynamicClientWithCustomIDTokenLifetimeAndSecretToKubeResources(4242), + authcodeExchange: authcodeExchangeInputs{ + modifyAuthRequest: doValidAuthCodeExchangeUsingDynamicClient().modifyAuthRequest, + modifyTokenRequest: doValidAuthCodeExchangeUsingDynamicClient().modifyTokenRequest, + want: withWantCustomIDTokenLifetime( + 4242, // want custom lifetime for authcode exchange (but not for token exchange) + doValidAuthCodeExchangeUsingDynamicClient().want, + ), + }, modifyRequestParams: func(t *testing.T, params url.Values) { params.Del("client_id") // client auth for dynamic clients must be in basic auth header }, @@ -1403,7 +1474,7 @@ func TestTokenEndpointTokenExchange(t *testing.T) { // tests for grant_type "urn { name: "dynamic client uses wrong client secret", kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, - authcodeExchange: doValidAuthCodeExchangeUsingDynamicClient, + authcodeExchange: doValidAuthCodeExchangeUsingDynamicClient(), modifyRequestParams: func(t *testing.T, params url.Values) { params.Del("client_id") // client auth for dynamic clients must be in basic auth header }, @@ -1418,7 +1489,7 @@ func TestTokenEndpointTokenExchange(t *testing.T) { // tests for grant_type "urn { name: "dynamic client uses wrong auth method (must use basic auth)", kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, - authcodeExchange: doValidAuthCodeExchangeUsingDynamicClient, + authcodeExchange: doValidAuthCodeExchangeUsingDynamicClient(), modifyRequestParams: func(t *testing.T, params url.Values) { // Dynamic clients do not support this method of auth. params.Set("client_id", dynamicClientID) @@ -1604,6 +1675,7 @@ func TestTokenEndpointTokenExchange(t *testing.T) { // tests for grant_type "urn // at and expires at dates which are newer than the old tokens. time.Sleep(1 * time.Second) + // Perform the token exchange. approxRequestTime := time.Now() subject.ServeHTTP(rsp, req) t.Logf("response: %#v", rsp) @@ -1699,12 +1771,21 @@ func TestTokenEndpointTokenExchange(t *testing.T) { // tests for grant_type "urn // Also assert which are the different from the original downstream ID token. requireClaimsAreNotEqual(t, "jti", claimsOfFirstIDToken, tokenClaims) // JWT ID requireClaimsAreNotEqual(t, "aud", claimsOfFirstIDToken, tokenClaims) // audience - requireClaimsAreNotEqual(t, "exp", claimsOfFirstIDToken, tokenClaims) // expires at - require.Greater(t, tokenClaims["exp"], claimsOfFirstIDToken["exp"]) requireClaimsAreNotEqual(t, "iat", claimsOfFirstIDToken, tokenClaims) // issued at require.Greater(t, tokenClaims["iat"], claimsOfFirstIDToken["iat"]) + requireClaimsAreNotEqual(t, "exp", claimsOfFirstIDToken, tokenClaims) // expires at + if test.authcodeExchange.want.wantIDTokenLifetimeSeconds == 0 { + // If the ID token lifetime of the original ID token was not customized by configuration, + // then both the original and new ID tokens should have default 2-minute lifetimes, with the + // clock starting for each at token issuing time. Therefore, the new one should expire + // after the original one (i.e. a moving 2-minute window). + require.Greater(t, tokenClaims["exp"], claimsOfFirstIDToken["exp"]) + } // Assert that the timestamps in the token are approximately as expected. + // When dynamic clients are configured to have a custom ID token lifetime, that does not apply to + // token exchanges. Therefore, we can always assert that the lifetime of the new ID token is always + // the default lifetime. expiresAtAsFloat, ok := tokenClaims["exp"].(float64) require.True(t, ok, "expected exp claim to be a float64") expiresAt := time.Unix(int64(expiresAtAsFloat), 0) @@ -1713,6 +1794,9 @@ func TestTokenEndpointTokenExchange(t *testing.T) { // tests for grant_type "urn require.True(t, ok, "expected iat claim to be a float64") issuedAt := time.Unix(int64(issuedAtAsFloat), 0) testutil.RequireTimeInDelta(t, approxRequestTime.UTC(), issuedAt, timeComparisonFudge) + // The difference between iat (issued at) and exp (expires at) claims should be exactly the lifetime seconds. + require.Equal(t, int64(idTokenExpirationSeconds), int64(expiresAtAsFloat)-int64(issuedAtAsFloat), + "ID token lifetime was not the expected value") // Assert that nothing in storage has been modified. newSecrets, err := secrets.List(context.Background(), metav1.ListOptions{}) @@ -2280,6 +2364,42 @@ func TestRefreshGrant(t *testing.T) { )), }, }, + { + name: "happy path refresh grant with openid scope granted (id token returned) using dynamic client which has custom ID token lifetime configured", + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( + upstreamOIDCIdentityProviderBuilder().WithValidatedAndMergedWithUserInfoTokens(&oidctypes.Token{ + IDToken: &oidctypes.IDToken{ + Claims: map[string]interface{}{ + "sub": goodUpstreamSubject, + }, + }, + }).WithRefreshedTokens(refreshedUpstreamTokensWithIDAndRefreshTokens()).Build()), + kubeResources: addFullyCapableDynamicClientWithCustomIDTokenLifetimeAndSecretToKubeResources(4242), + authcodeExchange: authcodeExchangeInputs{ + customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionData(), + modifyAuthRequest: func(r *http.Request) { + addDynamicClientIDToFormPostBody(r) + r.Form.Set("scope", "openid offline_access username groups") + }, + modifyTokenRequest: modifyAuthcodeTokenRequestWithDynamicClientAuth, + want: withWantCustomIDTokenLifetime(4242, + withWantDynamicClientID( + happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(initialUpstreamOIDCRefreshTokenCustomSessionData()), + ), + ), + }, + refreshRequest: refreshRequestInputs{ + modifyTokenRequest: modifyRefreshTokenRequestWithDynamicClientAuth, + want: withWantCustomIDTokenLifetime(4242, + withWantDynamicClientID( + happyRefreshTokenResponseForOpenIDAndOfflineAccess( + upstreamOIDCCustomSessionDataWithNewRefreshToken(oidcUpstreamRefreshedRefreshToken), + refreshedUpstreamTokensWithIDAndRefreshTokens(), + ), + ), + ), + }, + }, { name: "happy path refresh grant with openid scope granted (id token returned) using dynamic client with additional claims", idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( @@ -4581,22 +4701,25 @@ func exchangeAuthcodeForTokens( kubeResources(t, supervisorClient, kubeClient) } - var oauthHelper fosite.OAuth2Provider + // Use the same timeouts configuration as the production code will use. + timeoutsConfiguration := oidc.DefaultOIDCTimeoutsConfiguration() + // Use lower minimum required bcrypt cost than we would use in production to keep unit the tests fast. - oauthStore = storage.NewKubeStorage(secrets, oidcClientsClient, oidc.DefaultOIDCTimeoutsConfiguration(), bcrypt.MinCost) + oauthStore = storage.NewKubeStorage(secrets, oidcClientsClient, timeoutsConfiguration, bcrypt.MinCost) if test.makeJwksSigningKeyAndProvider == nil { test.makeJwksSigningKeyAndProvider = generateJWTSigningKeyAndJWKSProvider } + var oauthHelper fosite.OAuth2Provider // 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, - func(accessRequest fosite.AccessRequester) (bool, time.Duration) { return false, 0 }, - func(accessRequest fosite.AccessRequester) (bool, time.Duration) { return false, 0 }, + timeoutsConfiguration.OverrideDefaultAccessTokenLifespan, + timeoutsConfiguration.OverrideDefaultIDTokenLifespan, ) authorizeEndpointGrantedOpenIDScope := strings.Contains(authRequest.Form.Get("scope"), "openid") @@ -4674,7 +4797,7 @@ func requireTokenEndpointBehavior( expectedNumberOfRefreshTokenSessionsStored = 1 } if wantIDToken { - requireValidIDToken(t, parsedResponseBody, jwtSigningKey, test.wantClientID, wantNonceValueInIDToken, test.wantUsername, test.wantGroups, test.wantAdditionalClaims, parsedResponseBody["access_token"].(string), requestTime) + requireValidIDToken(t, parsedResponseBody, jwtSigningKey, test.wantClientID, wantNonceValueInIDToken, test.wantUsername, test.wantGroups, test.wantAdditionalClaims, test.wantIDTokenLifetimeSeconds, parsedResponseBody["access_token"].(string), requestTime) } if wantRefreshToken { requireValidRefreshTokenStorage(t, parsedResponseBody, oauthStore, test.wantClientID, test.wantRequestedScopes, test.wantGrantedScopes, test.wantUsername, test.wantGroups, test.wantCustomSessionDataStored, test.wantAdditionalClaims, secrets, requestTime) @@ -5198,6 +5321,7 @@ func requireValidIDToken( wantUsernameInIDToken string, wantGroupsInIDToken []string, wantAdditionalClaims map[string]interface{}, + wantIDTokenLifetimeSeconds int, actualAccessToken string, requestTime time.Time, ) { @@ -5266,11 +5390,19 @@ func requireValidIDToken( require.Empty(t, claims.Nonce) } + if wantIDTokenLifetimeSeconds == 0 { + // When not specified, assert that the ID token has the default lifetime for an ID token. + wantIDTokenLifetimeSeconds = idTokenExpirationSeconds + } + + // The difference between iat (issued at) and exp (expires at) claims should be exactly the lifetime seconds. + require.Equal(t, int64(wantIDTokenLifetimeSeconds), claims.ExpiresAt-claims.IssuedAt, "ID token lifetime was not the expected value") + expiresAt := time.Unix(claims.ExpiresAt, 0) issuedAt := time.Unix(claims.IssuedAt, 0) requestedAt := time.Unix(claims.RequestedAt, 0) authTime := time.Unix(claims.AuthTime, 0) - testutil.RequireTimeInDelta(t, requestTime.UTC().Add(idTokenExpirationSeconds*time.Second), expiresAt, timeComparisonFudge) + testutil.RequireTimeInDelta(t, requestTime.UTC().Add(time.Duration(wantIDTokenLifetimeSeconds)*time.Second), expiresAt, timeComparisonFudge) testutil.RequireTimeInDelta(t, requestTime.UTC(), issuedAt, timeComparisonFudge) testutil.RequireTimeInDelta(t, goodRequestedAtTime, requestedAt, timeComparisonFudge) testutil.RequireTimeInDelta(t, goodAuthTime, authTime, timeComparisonFudge) diff --git a/internal/testutil/oidcclient.go b/internal/testutil/oidcclient.go index 4dc274962..936ec57ac 100644 --- a/internal/testutil/oidcclient.go +++ b/internal/testutil/oidcclient.go @@ -1,4 +1,4 @@ -// Copyright 2022-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2022-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package testutil @@ -50,6 +50,7 @@ func newOIDCClient( redirectURI string, allowedGrantTypes []configv1alpha1.GrantType, allowedScopes []configv1alpha1.Scope, + tokenLifetimesIDTokenSeconds *int32, ) *configv1alpha1.OIDCClient { return &configv1alpha1.OIDCClient{ ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: clientID, Generation: 1, UID: types.UID(clientUID)}, @@ -57,6 +58,7 @@ func newOIDCClient( AllowedGrantTypes: allowedGrantTypes, AllowedScopes: allowedScopes, AllowedRedirectURIs: []configv1alpha1.RedirectURI{configv1alpha1.RedirectURI(redirectURI)}, + TokenLifetimes: configv1alpha1.OIDCClientTokenLifetimes{IDTokenSeconds: tokenLifetimesIDTokenSeconds}, }, } } @@ -73,6 +75,7 @@ func FullyCapableOIDCClientAndStorageSecret( clientID string, clientUID string, redirectURI string, + tokenLifetimesIDTokenSeconds *int32, hashes []string, validateFunc OIDCClientValidatorFunc, ) (*configv1alpha1.OIDCClient, *corev1.Secret) { @@ -82,7 +85,7 @@ func FullyCapableOIDCClientAndStorageSecret( "authorization_code", "urn:ietf:params:oauth:grant-type:token-exchange", "refresh_token", } - return OIDCClientAndStorageSecret(t, namespace, clientID, clientUID, allGrantTypes, allScopes, redirectURI, hashes, validateFunc) + return OIDCClientAndStorageSecret(t, namespace, clientID, clientUID, allGrantTypes, allScopes, redirectURI, tokenLifetimesIDTokenSeconds, hashes, validateFunc) } // OIDCClientAndStorageSecret returns an OIDC client which is allowed to use the specified grant types and scopes, @@ -96,10 +99,11 @@ func OIDCClientAndStorageSecret( allowedGrantTypes []configv1alpha1.GrantType, allowedScopes []configv1alpha1.Scope, redirectURI string, + tokenLifetimesIDTokenSeconds *int32, hashes []string, validateFunc OIDCClientValidatorFunc, ) (*configv1alpha1.OIDCClient, *corev1.Secret) { - oidcClient := newOIDCClient(namespace, clientID, clientUID, redirectURI, allowedGrantTypes, allowedScopes) + oidcClient := newOIDCClient(namespace, clientID, clientUID, redirectURI, allowedGrantTypes, allowedScopes, tokenLifetimesIDTokenSeconds) secret := OIDCClientSecretStorageSecretForUID(t, namespace, clientUID, hashes) // If a test made an invalid OIDCClient then inform the author of the test, so they can fix the test case. From 136bc7ac09b5ec91aaaa9b51c68f828413e00541 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Thu, 18 Apr 2024 10:45:46 -0700 Subject: [PATCH 7/8] Mild refactor of integration test for custom ID token lifetimes --- test/integration/supervisor_login_test.go | 74 ++++++++++++++--------- 1 file changed, 45 insertions(+), 29 deletions(-) diff --git a/test/integration/supervisor_login_test.go b/test/integration/supervisor_login_test.go index 5443acac3..ee5802a25 100644 --- a/test/integration/supervisor_login_test.go +++ b/test/integration/supervisor_login_test.go @@ -282,7 +282,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { // for the original ID token and the refreshed ID token. wantDownstreamIDTokenAdditionalClaims map[string]interface{} // The expected ID token lifetime, as calculated by token claim 'exp' subtracting token claim 'iat'. - // ID tokens issued through a token refresh should have the configured lifetime (or default if not configured). + // ID tokens issued through authcode exchange or token refresh should have the configured lifetime (or default if not configured). // ID tokens issued through a token exchange should have the default lifetime. wantDownstreamIDTokenLifetime *time.Duration @@ -1422,6 +1422,32 @@ func TestSupervisorLogin_Browser(t *testing.T) { } return testlib.CreateTestOIDCIdentityProvider(t, spec, idpv1alpha1.PhaseReady).Name }, + createOIDCClient: func(t *testing.T, callbackURL string) (string, string) { + return testlib.CreateOIDCClient(t, configv1alpha1.OIDCClientSpec{ + AllowedRedirectURIs: []configv1alpha1.RedirectURI{configv1alpha1.RedirectURI(callbackURL)}, + AllowedGrantTypes: []configv1alpha1.GrantType{"authorization_code", "urn:ietf:params:oauth:grant-type:token-exchange", "refresh_token"}, + AllowedScopes: []configv1alpha1.Scope{"openid", "offline_access", "pinniped:request-audience", "username", "groups"}, + }, configv1alpha1.OIDCClientPhaseReady) + }, + requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowOIDC, + wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamOIDC, + wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Username) + "$" }, + wantDownstreamIDTokenGroups: env.SupervisorUpstreamOIDC.ExpectedGroups, + }, + { + name: "oidc upstream with downstream dynamic client happy path, requesting all scopes, with a custom ID token lifetime configuration", + maybeSkip: skipNever, + createIDP: func(t *testing.T) string { + spec := basicOIDCIdentityProviderSpec() + spec.Claims = idpv1alpha1.OIDCClaims{ + Username: env.SupervisorUpstreamOIDC.UsernameClaim, + Groups: env.SupervisorUpstreamOIDC.GroupsClaim, + } + spec.AuthorizationConfig = idpv1alpha1.OIDCAuthorizationConfig{ + AdditionalScopes: env.SupervisorUpstreamOIDC.AdditionalScopes, + } + return testlib.CreateTestOIDCIdentityProvider(t, spec, idpv1alpha1.PhaseReady).Name + }, createOIDCClient: func(t *testing.T, callbackURL string) (string, string) { return testlib.CreateOIDCClient(t, configv1alpha1.OIDCClientSpec{ AllowedRedirectURIs: []configv1alpha1.RedirectURI{configv1alpha1.RedirectURI(callbackURL)}, @@ -2534,7 +2560,7 @@ func testSupervisorLogin( // Authcodes should start with the custom prefix "pin_ac_" to make them identifiable as authcodes when seen by a user out of context. require.True(t, strings.HasPrefix(authcode, "pin_ac_"), "token %q did not have expected prefix 'pin_ac_'", authcode) - // Call the token endpoint to get tokens. + // Call the token endpoint for the first time to get an initial set of tokens. tokenResponse, err := downstreamOAuth2Config.Exchange(oidcHTTPClientContext, authcode, pkceParam.Verifier()) if wantAuthcodeExchangeError != "" { require.EqualError(t, err, wantAuthcodeExchangeError) @@ -2555,13 +2581,14 @@ func testSupervisorLogin( if len(wantDownstreamIDTokenAdditionalClaims) > 0 { expectedIDTokenClaims = append(expectedIDTokenClaims, "additionalClaims") } - defaultIDTokenLifetime := oidc.DefaultOIDCTimeoutsConfiguration().IDTokenLifespan + + defaultIDTokenLifetime := 2 * time.Minute if wantDownstreamIDTokenLifetime == nil { + // When not specified by the test, assume that it wants to assert the default lifetime. wantDownstreamIDTokenLifetime = ptr.To(defaultIDTokenLifetime) } - initialIDTokenClaims := verifyTokenResponse( - t, + initialIDTokenClaims := verifyTokenResponse(t, tokenResponse, discovery, downstreamOAuth2Config, @@ -2574,12 +2601,11 @@ func testSupervisorLogin( *wantDownstreamIDTokenLifetime, ) - // token exchange on the original token + // Perform token exchange on the original token by calling the token endpoint again. if requestTokenExchangeAud == "" { requestTokenExchangeAud = "some-cluster-123" // use a default test value } - doTokenExchange( - t, + doTokenExchange(t, requestTokenExchangeAud, &downstreamOAuth2Config, tokenResponse, @@ -2613,13 +2639,15 @@ func testSupervisorLogin( require.NoError(t, oauthStore.DeleteRefreshTokenSession(ctx, signatureOfLatestRefreshToken)) require.NoError(t, oauthStore.CreateRefreshTokenSession(ctx, signatureOfLatestRefreshToken, storedRefreshSession)) } - // Use the refresh token to get new tokens + + // Use the refresh token to get new tokens by calling the token endpoint again. refreshSource := downstreamOAuth2Config.TokenSource(oidcHTTPClientContext, &oauth2.Token{RefreshToken: tokenResponse.RefreshToken}) refreshedTokenResponse, err := refreshSource.Token() require.NoError(t, err) // When refreshing, do not expect a "nonce" claim. expectRefreshedIDTokenClaims := []string{"iss", "exp", "sub", "aud", "auth_time", "iat", "jti", "rat", "azp", "at_hash"} + if slices.Contains(wantDownstreamScopes, "username") { // If the test wants the username scope to have been granted, then also expect the claim in the refreshed ID token. expectRefreshedIDTokenClaims = append(expectRefreshedIDTokenClaims, "username") @@ -2631,8 +2659,8 @@ func testSupervisorLogin( if len(wantDownstreamIDTokenAdditionalClaims) > 0 { expectRefreshedIDTokenClaims = append(expectRefreshedIDTokenClaims, "additionalClaims") } - refreshedIDTokenClaims := verifyTokenResponse( - t, + + refreshedIDTokenClaims := verifyTokenResponse(t, refreshedTokenResponse, discovery, downstreamOAuth2Config, @@ -2649,7 +2677,7 @@ func testSupervisorLogin( require.NotEqual(t, tokenResponse.RefreshToken, refreshedTokenResponse.RefreshToken) require.NotEqual(t, tokenResponse.Extra("id_token"), refreshedTokenResponse.Extra("id_token")) - // token exchange on the refreshed token + // Perform token exchange on the refreshed token by calling the token endpoint again. doTokenExchange( t, requestTokenExchangeAud, @@ -2662,7 +2690,7 @@ func testSupervisorLogin( defaultIDTokenLifetime, ) - // Now that we have successfully performed a refresh, let's test what happens when an + // Now that we have successfully performed a refresh once, let's test what happens when an // upstream refresh fails during the next downstream refresh. if breakRefreshSessionData != nil { latestRefreshToken := refreshedTokenResponse.RefreshToken @@ -2766,14 +2794,8 @@ func verifyTokenResponse( // Check the token lifetime by calculating the delta between "exp" and "iat" iat := getFloat64Claim(t, idTokenClaims, "iat") exp := getFloat64Claim(t, idTokenClaims, "exp") - require.InDeltaf( - t, - wantDownstreamIDTokenLifetime.Seconds(), - exp-iat, - 10.0, - "actual token lifetime (%f) must be within 10 seconds of expectation (%f)", - exp-iat, - wantDownstreamIDTokenLifetime.Seconds(), + require.InDelta(t, wantDownstreamIDTokenLifetime.Seconds(), exp-iat, 1.0, + "actual token lifetime must be within 1 second of expectation", ) // Some light verification of the other tokens that were returned. @@ -3153,14 +3175,8 @@ func doTokenExchange( // Check the token lifetime by calculating the delta between "exp" and "iat" iat := getFloat64Claim(t, claims, "iat") exp := getFloat64Claim(t, claims, "exp") - require.InDeltaf( - t, - wantIDTokenLifetime.Seconds(), - exp-iat, - 10.0, - "actual token lifetime (%f) must be within 10 seconds of expectation (%f)", - exp-iat, - wantIDTokenLifetime.Seconds(), + require.InDelta(t, wantIDTokenLifetime.Seconds(), exp-iat, 1.0, + "actual token lifetime must be within 1 second of expectation", ) // The original client ID should be preserved in the azp claim, therefore preserving this information From 57a07a498f2493ac9f197a34691e5263897b53a9 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Wed, 24 Apr 2024 15:05:00 -0700 Subject: [PATCH 8/8] Refactors for custom ID token lifetime based on PR feedback --- .../clientregistry/clientregistry.go | 6 +++++- .../clientregistry/clientregistry_test.go | 8 ++++---- .../endpoints/token/token_handler.go | 4 ++-- internal/federationdomain/oidc/oidc.go | 14 +++++++------- internal/federationdomain/oidc/oidc_test.go | 4 ++-- .../timeouts/timeouts_configuration.go | 2 +- test/integration/supervisor_oidc_client_test.go | 2 +- 7 files changed, 22 insertions(+), 18 deletions(-) diff --git a/internal/federationdomain/clientregistry/clientregistry.go b/internal/federationdomain/clientregistry/clientregistry.go index 573de6fe7..d46809bb3 100644 --- a/internal/federationdomain/clientregistry/clientregistry.go +++ b/internal/federationdomain/clientregistry/clientregistry.go @@ -35,6 +35,10 @@ type Client struct { IDTokenLifetimeConfiguration time.Duration } +func (c *Client) GetIDTokenLifetimeConfiguration() time.Duration { + return c.IDTokenLifetimeConfiguration +} + // Client implements the base, OIDC, and response_mode client interfaces of Fosite. var ( _ fosite.Client = (*Client)(nil) @@ -181,7 +185,7 @@ func oidcClientCRToFositeClient(oidcClient *configv1alpha1.OIDCClient, clientSec var idTokenLifetime time.Duration if idTokenLifetimeOverrideInSeconds != nil { // It should be safe to cast this int32 to time.Duration, because time.Duration is an int64. - idTokenLifetime = time.Duration(*(oidcClient.Spec.TokenLifetimes.IDTokenSeconds)) * time.Second + idTokenLifetime = time.Duration(*(idTokenLifetimeOverrideInSeconds)) * time.Second } return &Client{ diff --git a/internal/federationdomain/clientregistry/clientregistry_test.go b/internal/federationdomain/clientregistry/clientregistry_test.go index 5f800ff9f..cc1b0dc6e 100644 --- a/internal/federationdomain/clientregistry/clientregistry_test.go +++ b/internal/federationdomain/clientregistry/clientregistry_test.go @@ -224,7 +224,7 @@ func TestClientManager(t *testing.T) { AllowedGrantTypes: []configv1alpha1.GrantType{"authorization_code", "refresh_token"}, AllowedScopes: []configv1alpha1.Scope{"openid", "offline_access", "username", "groups"}, AllowedRedirectURIs: []configv1alpha1.RedirectURI{"http://localhost:8080"}, - TokenLifetimes: configv1alpha1.OIDCClientTokenLifetimes{IDTokenSeconds: ptr.To[int32]((4242))}, + TokenLifetimes: configv1alpha1.OIDCClientTokenLifetimes{IDTokenSeconds: ptr.To[int32](4242)}, }, }, { @@ -306,7 +306,7 @@ func requireEqualsPinnipedCLI(t *testing.T, c *Client) { require.Equal(t, "none", c.GetTokenEndpointAuthMethod()) require.Equal(t, "RS256", c.GetTokenEndpointAuthSigningAlgorithm()) require.Equal(t, []fosite.ResponseModeType{"", "query", "form_post"}, c.GetResponseModes()) - require.Equal(t, 0*time.Second, c.IDTokenLifetimeConfiguration) + require.Equal(t, 0*time.Second, c.GetIDTokenLifetimeConfiguration()) marshaled, err := json.Marshal(c) require.NoError(t, err) @@ -341,7 +341,7 @@ func requireEqualsPinnipedCLI(t *testing.T, c *Client) { "request_uris": null, "request_object_signing_alg": "", "token_endpoint_auth_signing_alg": "RS256", - "IDTokenLifetimeConfiguration": 0 + "IDTokenLifetimeConfiguration": 0 }`, string(marshaled)) } @@ -365,7 +365,7 @@ func requireDynamicOIDCClient( require.Equal(t, wantRedirectURIs, c.GetRedirectURIs()) require.Equal(t, wantGrantTypes, c.GetGrantTypes()) require.Equal(t, wantScopes, c.GetScopes()) - require.Equal(t, wantIDTokenLifetimeConfiguration, c.IDTokenLifetimeConfiguration) + require.Equal(t, wantIDTokenLifetimeConfiguration, c.GetIDTokenLifetimeConfiguration()) // The following are always the same for all OIDCClients. require.Nil(t, c.GetHashedSecret()) diff --git a/internal/federationdomain/endpoints/token/token_handler.go b/internal/federationdomain/endpoints/token/token_handler.go index e441f6ce6..f9d31a7f2 100644 --- a/internal/federationdomain/endpoints/token/token_handler.go +++ b/internal/federationdomain/endpoints/token/token_handler.go @@ -95,13 +95,13 @@ func NewHandler( } func maybeOverrideDefaultAccessTokenLifetime(overrideAccessTokenLifespan timeouts.OverrideLifespan, accessRequest fosite.AccessRequester) { - if doOverride, newLifespan := overrideAccessTokenLifespan(accessRequest); doOverride { + if newLifespan, doOverride := overrideAccessTokenLifespan(accessRequest); doOverride { accessRequest.GetSession().SetExpiresAt(fosite.AccessToken, time.Now().UTC().Add(newLifespan).Round(time.Second)) } } func maybeOverrideDefaultIDTokenLifetime(baseCtx context.Context, overrideIDTokenLifespan timeouts.OverrideLifespan, accessRequest fosite.AccessRequester) context.Context { - if doOverride, newLifespan := overrideIDTokenLifespan(accessRequest); doOverride { + if newLifespan, doOverride := overrideIDTokenLifespan(accessRequest); doOverride { return idtokenlifespan.OverrideIDTokenLifespanInContext(baseCtx, newLifespan) } return baseCtx diff --git a/internal/federationdomain/oidc/oidc.go b/internal/federationdomain/oidc/oidc.go index 7715f9e89..ba839ca7c 100644 --- a/internal/federationdomain/oidc/oidc.go +++ b/internal/federationdomain/oidc/oidc.go @@ -173,31 +173,31 @@ func DefaultOIDCTimeoutsConfiguration() timeouts.Configuration { AuthorizeCodeLifespan: authorizationCodeLifespan, AccessTokenLifespan: accessTokenLifespan, - OverrideDefaultAccessTokenLifespan: func(accessRequest fosite.AccessRequester) (bool, time.Duration) { + OverrideDefaultAccessTokenLifespan: func(accessRequest fosite.AccessRequester) (time.Duration, bool) { // Not currently overriding the defaults. - return false, 0 + return 0, false }, IDTokenLifespan: idTokenLifespan, - OverrideDefaultIDTokenLifespan: func(accessRequest fosite.AccessRequester) (bool, time.Duration) { + OverrideDefaultIDTokenLifespan: func(accessRequest fosite.AccessRequester) (time.Duration, bool) { client := accessRequest.GetClient() // Don't allow OIDCClients to override the default lifetime for ID tokens returned // by RFC8693 token exchange. This is not user configurable for now. - if !accessRequest.GetGrantTypes().ExactOne(oidcapi.GrantTypeTokenExchange) { + if !accessRequest.GetGrantTypes().Has(oidcapi.GrantTypeTokenExchange) { if castClient, ok := client.(*clientregistry.Client); !ok { // All clients returned by our client registry implement clientregistry.Client, // so this should be a safe cast in practice. plog.Error("could not check if client overrides token lifetimes", errors.New("could not cast client to *clientregistry.Client"), "clientID", client.GetID(), "clientType", reflect.TypeOf(client)) - } else if castClient.IDTokenLifetimeConfiguration > 0 { + } else if castClient.GetIDTokenLifetimeConfiguration() > 0 { // An OIDCClient resource has provided an override, so use it. // Note that the pinniped-cli client never overrides this value. - return true, castClient.IDTokenLifetimeConfiguration + return castClient.GetIDTokenLifetimeConfiguration(), true } } // Otherwise, do not override the defaults. - return false, 0 + return 0, false }, RefreshTokenLifespan: refreshTokenLifespan, diff --git a/internal/federationdomain/oidc/oidc_test.go b/internal/federationdomain/oidc/oidc_test.go index bf9c30890..c629dfcc0 100644 --- a/internal/federationdomain/oidc/oidc_test.go +++ b/internal/federationdomain/oidc/oidc_test.go @@ -38,7 +38,7 @@ func TestOverrideDefaultAccessTokenLifespan(t *testing.T) { c := DefaultOIDCTimeoutsConfiguration() // We are not yet overriding access token lifetimes. - doOverride, newLifespan := c.OverrideDefaultAccessTokenLifespan(nil) + newLifespan, doOverride := c.OverrideDefaultAccessTokenLifespan(nil) require.Equal(t, false, doOverride) require.Equal(t, time.Duration(0), newLifespan) } @@ -109,7 +109,7 @@ func TestOverrideIDTokenLifespan(t *testing.T) { c := DefaultOIDCTimeoutsConfiguration() - doOverride, newLifespan := c.OverrideDefaultIDTokenLifespan(tt.accessRequest) + newLifespan, doOverride := c.OverrideDefaultIDTokenLifespan(tt.accessRequest) require.Equal(t, tt.wantOverride, doOverride) require.Equal(t, tt.wantLifespan, newLifespan) }) diff --git a/internal/federationdomain/timeouts/timeouts_configuration.go b/internal/federationdomain/timeouts/timeouts_configuration.go index 639051b15..782a3c2d1 100644 --- a/internal/federationdomain/timeouts/timeouts_configuration.go +++ b/internal/federationdomain/timeouts/timeouts_configuration.go @@ -14,7 +14,7 @@ type StorageLifetime func(requester fosite.Requester) time.Duration // OverrideLifespan is a function that, given a request, can suggest to override the default lifespan // by returning true along with a new lifespan. When false is returned, the returned duration should be ignored. -type OverrideLifespan func(accessRequest fosite.AccessRequester) (bool, time.Duration) +type OverrideLifespan func(accessRequest fosite.AccessRequester) (time.Duration, bool) type Configuration struct { // The length of time that our state param that we encrypt and pass to the upstream OIDC IDP should be considered diff --git a/test/integration/supervisor_oidc_client_test.go b/test/integration/supervisor_oidc_client_test.go index f76d380fc..1658eceb7 100644 --- a/test/integration/supervisor_oidc_client_test.go +++ b/test/integration/supervisor_oidc_client_test.go @@ -6,7 +6,6 @@ package integration import ( "context" "fmt" - "k8s.io/utils/ptr" "sort" "strings" "testing" @@ -16,6 +15,7 @@ import ( corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" supervisorconfigv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1" "go.pinniped.dev/internal/oidcclientsecretstorage"