mirror of
https://github.com/vmware-tanzu/pinniped.git
synced 2026-07-23 08:22:31 +00:00
Merge pull request #1914 from vmware-tanzu/configurable_id_token_length
Make ID token lifetime configurable for `OIDCClients`
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
Generated
+18
@@ -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)
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Generated
+18
@@ -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)
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Generated
+18
@@ -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)
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Generated
+18
@@ -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)
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Generated
+18
@@ -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)
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Generated
+18
@@ -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)
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Generated
+18
@@ -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)
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Generated
+18
@@ -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)
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Generated
+18
@@ -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)
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Generated
+18
@@ -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)
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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{},
|
||||
|
||||
+9
-11
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+10
-17
@@ -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,14 @@ 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
|
||||
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 +1296,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 +1305,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)
|
||||
|
||||
|
||||
@@ -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,16 @@ 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
|
||||
}
|
||||
|
||||
func (c *Client) GetIDTokenLifetimeConfiguration() time.Duration {
|
||||
return c.IDTokenLifetimeConfiguration
|
||||
}
|
||||
|
||||
// Client implements the base, OIDC, and response_mode client interfaces of Fosite.
|
||||
@@ -165,10 +175,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(*(idTokenLifetimeOverrideInSeconds)) * time.Second
|
||||
}
|
||||
|
||||
return &Client{
|
||||
DefaultOpenIDConnectClient: fosite.DefaultOpenIDConnectClient{
|
||||
DefaultClient: &fosite.DefaultClient{
|
||||
@@ -192,6 +211,7 @@ func oidcClientCRToFositeClient(oidcClient *configv1alpha1.OIDCClient, clientSec
|
||||
TokenEndpointAuthSigningAlgorithm: coreosoidc.RS256,
|
||||
TokenEndpointAuthMethod: "client_secret_basic",
|
||||
},
|
||||
IDTokenLifetimeConfiguration: idTokenLifetime,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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.GetIDTokenLifetimeConfiguration())
|
||||
|
||||
marshaled, err := json.Marshal(c)
|
||||
require.NoError(t, err)
|
||||
@@ -312,6 +340,43 @@ 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))
|
||||
}
|
||||
|
||||
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.GetIDTokenLifetimeConfiguration())
|
||||
|
||||
// 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())
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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))
|
||||
},
|
||||
|
||||
@@ -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))
|
||||
},
|
||||
|
||||
@@ -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 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 newLifespan, doOverride := overrideIDTokenLifespan(accessRequest); doOverride {
|
||||
return idtokenlifespan.OverrideIDTokenLifespanInContext(baseCtx, newLifespan)
|
||||
}
|
||||
return baseCtx
|
||||
}
|
||||
|
||||
func errMissingUpstreamSessionInternalError() *fosite.RFC6749Error {
|
||||
return &fosite.RFC6749Error{
|
||||
ErrorField: "error",
|
||||
|
||||
@@ -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,18 +4701,26 @@ 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)
|
||||
subject = NewHandler(
|
||||
idps,
|
||||
oauthHelper,
|
||||
timeoutsConfiguration.OverrideDefaultAccessTokenLifespan,
|
||||
timeoutsConfiguration.OverrideDefaultIDTokenLifespan,
|
||||
)
|
||||
|
||||
authorizeEndpointGrantedOpenIDScope := strings.Contains(authRequest.Form.Get("scope"), "openid")
|
||||
expectedNumberOfIDSessionsStored := 0
|
||||
@@ -4669,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)
|
||||
@@ -5193,6 +5321,7 @@ func requireValidIDToken(
|
||||
wantUsernameInIDToken string,
|
||||
wantGroupsInIDToken []string,
|
||||
wantAdditionalClaims map[string]interface{},
|
||||
wantIDTokenLifetimeSeconds int,
|
||||
actualAccessToken string,
|
||||
requestTime time.Time,
|
||||
) {
|
||||
@@ -5261,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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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) (time.Duration, bool) {
|
||||
// Not currently overriding the defaults.
|
||||
return 0, false
|
||||
},
|
||||
|
||||
IDTokenLifespan: idTokenLifespan,
|
||||
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().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.GetIDTokenLifetimeConfiguration() > 0 {
|
||||
// An OIDCClient resource has provided an override, so use it.
|
||||
// Note that the pinniped-cli client never overrides this value.
|
||||
return castClient.GetIDTokenLifetimeConfiguration(), true
|
||||
}
|
||||
}
|
||||
// Otherwise, do not override the defaults.
|
||||
return 0, false
|
||||
},
|
||||
|
||||
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) {
|
||||
|
||||
@@ -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.
|
||||
newLifespan, doOverride := 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()
|
||||
|
||||
newLifespan, doOverride := c.OverrideDefaultIDTokenLifespan(tt.accessRequest)
|
||||
require.Equal(t, tt.wantOverride, doOverride)
|
||||
require.Equal(t, tt.wantLifespan, newLifespan)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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) (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
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
@@ -33,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 {
|
||||
@@ -44,7 +46,8 @@ type RevocationStorage interface {
|
||||
var _ RevocationStorage = &accessTokenStorage{}
|
||||
|
||||
type accessTokenStorage struct {
|
||||
storage crud.Storage
|
||||
storage crud.Storage
|
||||
lifetime timeouts.StorageLifetime
|
||||
}
|
||||
|
||||
type Session struct {
|
||||
@@ -52,8 +55,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 +86,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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -23,21 +23,28 @@ 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"
|
||||
)
|
||||
|
||||
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 (
|
||||
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{
|
||||
@@ -54,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":""},"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",
|
||||
@@ -63,12 +70,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 +110,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 +122,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) {
|
||||
@@ -123,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":""},"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",
|
||||
@@ -134,7 +153,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 +183,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 +191,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{
|
||||
@@ -196,11 +215,11 @@ 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) {
|
||||
ctx, _, secrets, storage := makeTestSubject()
|
||||
ctx, _, secrets, storage := makeTestSubject(lifetimeFunc)
|
||||
|
||||
secret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
@@ -214,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",
|
||||
@@ -228,14 +247,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 +272,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 +293,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, lifetime)
|
||||
return context.Background(),
|
||||
client,
|
||||
secrets,
|
||||
New(secrets, clocktesting.NewFakeClock(fakeNow).Now, lifetimeFunc)
|
||||
}
|
||||
|
||||
func TestReadFromSecret(t *testing.T) {
|
||||
@@ -298,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{},
|
||||
@@ -341,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",
|
||||
@@ -364,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",
|
||||
@@ -377,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",
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
@@ -34,13 +35,15 @@ 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{}
|
||||
|
||||
type authorizeCodeStorage struct {
|
||||
storage crud.Storage
|
||||
storage crud.Storage
|
||||
lifetime timeouts.StorageLifetime
|
||||
}
|
||||
|
||||
type Session struct {
|
||||
@@ -49,8 +52,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 +95,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
|
||||
}
|
||||
|
||||
@@ -255,131 +264,135 @@ 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"
|
||||
"version": "7"
|
||||
}`
|
||||
|
||||
@@ -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
|
||||
@@ -35,16 +35,23 @@ 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"
|
||||
)
|
||||
|
||||
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 (
|
||||
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{
|
||||
@@ -66,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":""},"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",
|
||||
@@ -86,19 +93,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":""},"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",
|
||||
}),
|
||||
}
|
||||
|
||||
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 +141,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 +159,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 +173,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 +181,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 +202,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{
|
||||
@@ -204,11 +223,11 @@ 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) {
|
||||
ctx, _, secrets, storage := makeTestSubject()
|
||||
ctx, _, secrets, storage := makeTestSubject(lifetimeFunc)
|
||||
|
||||
secret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
@@ -219,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",
|
||||
@@ -233,14 +252,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 +276,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, 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.
|
||||
@@ -366,7 +388,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)
|
||||
@@ -386,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)
|
||||
@@ -397,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)
|
||||
}
|
||||
|
||||
@@ -421,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",
|
||||
@@ -465,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",
|
||||
@@ -488,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",
|
||||
@@ -501,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",
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
@@ -34,13 +35,15 @@ 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{}
|
||||
|
||||
type openIDConnectRequestStorage struct {
|
||||
storage crud.Storage
|
||||
storage crud.Storage
|
||||
lifetime timeouts.StorageLifetime
|
||||
}
|
||||
|
||||
type session struct {
|
||||
@@ -48,8 +51,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 +66,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
|
||||
}
|
||||
|
||||
|
||||
@@ -22,15 +22,22 @@ 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"
|
||||
)
|
||||
|
||||
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 (
|
||||
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{
|
||||
@@ -52,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":""},"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",
|
||||
@@ -61,12 +68,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 +109,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 +121,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 +135,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{
|
||||
@@ -137,11 +156,11 @@ 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) {
|
||||
ctx, _, secrets, storage := makeTestSubject()
|
||||
ctx, _, secrets, storage := makeTestSubject(lifetimeFunc)
|
||||
|
||||
secret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
@@ -152,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",
|
||||
@@ -166,14 +185,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 +210,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, lifetime)
|
||||
return context.Background(),
|
||||
client,
|
||||
secrets,
|
||||
New(secrets, clocktesting.NewFakeClock(fakeNow).Now, lifetimeFunc)
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
@@ -32,13 +33,15 @@ 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{}
|
||||
|
||||
type pkceStorage struct {
|
||||
storage crud.Storage
|
||||
storage crud.Storage
|
||||
lifetime timeouts.StorageLifetime
|
||||
}
|
||||
|
||||
type session struct {
|
||||
@@ -46,8 +49,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 +59,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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -22,15 +22,22 @@ 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"
|
||||
)
|
||||
|
||||
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 (
|
||||
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{
|
||||
@@ -52,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":""},"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",
|
||||
@@ -61,12 +68,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 +109,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 +121,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 +134,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{
|
||||
@@ -140,11 +158,11 @@ 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) {
|
||||
ctx, _, secrets, storage := makeTestSubject()
|
||||
ctx, _, secrets, storage := makeTestSubject(lifetimeFunc)
|
||||
|
||||
secret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
@@ -158,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",
|
||||
@@ -172,14 +190,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 +214,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, lifetime)
|
||||
return context.Background(),
|
||||
client,
|
||||
secrets,
|
||||
New(secrets, clocktesting.NewFakeClock(fakeNow).Now, lifetimeFunc)
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
@@ -33,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 {
|
||||
@@ -45,7 +47,8 @@ type RevocationStorage interface {
|
||||
var _ RevocationStorage = &refreshTokenStorage{}
|
||||
|
||||
type refreshTokenStorage struct {
|
||||
storage crud.Storage
|
||||
storage crud.Storage
|
||||
lifetime timeouts.StorageLifetime
|
||||
}
|
||||
|
||||
type Session struct {
|
||||
@@ -53,8 +56,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 +92,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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -23,20 +23,28 @@ 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"
|
||||
)
|
||||
|
||||
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 (
|
||||
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{
|
||||
@@ -53,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":""},"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",
|
||||
@@ -62,12 +70,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 +111,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 +123,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) {
|
||||
@@ -123,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":""},"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",
|
||||
@@ -134,7 +154,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",
|
||||
@@ -178,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":""},"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",
|
||||
@@ -189,7 +209,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 +240,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 +248,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{
|
||||
@@ -252,11 +272,11 @@ 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) {
|
||||
ctx, _, secrets, storage := makeTestSubject()
|
||||
ctx, _, secrets, storage := makeTestSubject(lifetimeFunc)
|
||||
|
||||
secret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
@@ -270,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",
|
||||
@@ -284,14 +304,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 +329,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 +350,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, lifetime)
|
||||
return context.Background(),
|
||||
client,
|
||||
secrets,
|
||||
New(secrets, clocktesting.NewFakeClock(fakeNow).Now, lifetimeFunc)
|
||||
}
|
||||
|
||||
func TestReadFromSecret(t *testing.T) {
|
||||
@@ -354,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{},
|
||||
@@ -397,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",
|
||||
@@ -420,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",
|
||||
@@ -433,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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 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
|
||||
|
||||
// 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.
|
||||
@@ -1430,6 +1434,36 @@ func TestSupervisorLogin_Browser(t *testing.T) {
|
||||
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)},
|
||||
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",
|
||||
maybeSkip: skipNever,
|
||||
@@ -2162,6 +2196,7 @@ func TestSupervisorLogin_Browser(t *testing.T) {
|
||||
tt.wantDownstreamIDTokenUsernameToMatch,
|
||||
tt.wantDownstreamIDTokenGroups,
|
||||
tt.wantDownstreamIDTokenAdditionalClaims,
|
||||
tt.wantDownstreamIDTokenLifetime,
|
||||
tt.wantAuthorizationErrorType,
|
||||
tt.wantAuthorizationErrorDescription,
|
||||
tt.wantAuthcodeExchangeError,
|
||||
@@ -2317,6 +2352,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 +2438,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.
|
||||
@@ -2524,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)
|
||||
@@ -2545,8 +2581,14 @@ func testSupervisorLogin(
|
||||
if len(wantDownstreamIDTokenAdditionalClaims) > 0 {
|
||||
expectedIDTokenClaims = append(expectedIDTokenClaims, "additionalClaims")
|
||||
}
|
||||
initialIDTokenClaims := verifyTokenResponse(
|
||||
t,
|
||||
|
||||
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,
|
||||
tokenResponse,
|
||||
discovery,
|
||||
downstreamOAuth2Config,
|
||||
@@ -2556,13 +2598,23 @@ func testSupervisorLogin(
|
||||
wantDownstreamIDTokenUsernameToMatch(username),
|
||||
wantDownstreamIDTokenGroups,
|
||||
wantDownstreamIDTokenAdditionalClaims,
|
||||
*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, requestTokenExchangeAud, &downstreamOAuth2Config, tokenResponse, httpClient, discovery, wantTokenExchangeResponse, initialIDTokenClaims)
|
||||
doTokenExchange(t,
|
||||
requestTokenExchangeAud,
|
||||
&downstreamOAuth2Config,
|
||||
tokenResponse,
|
||||
httpClient,
|
||||
discovery,
|
||||
wantTokenExchangeResponse,
|
||||
initialIDTokenClaims,
|
||||
defaultIDTokenLifetime,
|
||||
)
|
||||
|
||||
wantRefreshedGroups := wantDownstreamIDTokenGroups
|
||||
if editRefreshSessionDataWithoutBreaking != nil {
|
||||
@@ -2587,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")
|
||||
@@ -2605,8 +2659,8 @@ func testSupervisorLogin(
|
||||
if len(wantDownstreamIDTokenAdditionalClaims) > 0 {
|
||||
expectRefreshedIDTokenClaims = append(expectRefreshedIDTokenClaims, "additionalClaims")
|
||||
}
|
||||
refreshedIDTokenClaims := verifyTokenResponse(
|
||||
t,
|
||||
|
||||
refreshedIDTokenClaims := verifyTokenResponse(t,
|
||||
refreshedTokenResponse,
|
||||
discovery,
|
||||
downstreamOAuth2Config,
|
||||
@@ -2616,16 +2670,27 @@ func testSupervisorLogin(
|
||||
wantDownstreamIDTokenUsernameToMatch(username),
|
||||
wantRefreshedGroups,
|
||||
wantDownstreamIDTokenAdditionalClaims,
|
||||
*wantDownstreamIDTokenLifetime,
|
||||
)
|
||||
|
||||
require.NotEqual(t, tokenResponse.AccessToken, refreshedTokenResponse.AccessToken)
|
||||
require.NotEqual(t, tokenResponse.RefreshToken, refreshedTokenResponse.RefreshToken)
|
||||
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)
|
||||
// Perform token exchange on the refreshed token by calling the token endpoint again.
|
||||
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
|
||||
// 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
|
||||
@@ -2675,6 +2740,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 +2759,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 +2791,13 @@ 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.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.
|
||||
require.NotEmpty(t, tokenResponse.AccessToken)
|
||||
require.Equal(t, "bearer", tokenResponse.TokenType)
|
||||
@@ -2747,6 +2819,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 +3105,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 +3172,13 @@ 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.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
|
||||
// about the original source of the authorization for tracing/auditing purposes, since the "aud" claim
|
||||
// has been updated to have a new value.
|
||||
|
||||
@@ -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
|
||||
@@ -15,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"
|
||||
@@ -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{
|
||||
|
||||
@@ -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)
|
||||
@@ -91,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"]))
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user