Merge pull request #283 from vmware-tanzu/username-and-subject-claims
Adjust subject and username claims
This commit is contained in:
commit
fbe1a202c2
@ -27,11 +27,30 @@ type JWTAuthenticatorSpec struct {
|
|||||||
// +kubebuilder:validation:MinLength=1
|
// +kubebuilder:validation:MinLength=1
|
||||||
Audience string `json:"audience"`
|
Audience string `json:"audience"`
|
||||||
|
|
||||||
|
// Claims allows customization of the claims that will be mapped to user identity
|
||||||
|
// for Kubernetes access.
|
||||||
|
// +optional
|
||||||
|
Claims JWTTokenClaims `json:"claims"`
|
||||||
|
|
||||||
// TLS configuration for communicating with the OIDC provider.
|
// TLS configuration for communicating with the OIDC provider.
|
||||||
// +optional
|
// +optional
|
||||||
TLS *TLSSpec `json:"tls,omitempty"`
|
TLS *TLSSpec `json:"tls,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// JWTTokenClaims allows customization of the claims that will be mapped to user identity
|
||||||
|
// for Kubernetes access.
|
||||||
|
type JWTTokenClaims struct {
|
||||||
|
// Groups is the name of the claim which should be read to extract the user's
|
||||||
|
// group membership from the JWT token. When not specified, it will default to "groups".
|
||||||
|
// +optional
|
||||||
|
Groups string `json:"groups"`
|
||||||
|
|
||||||
|
// Username is the name of the claim which should be read to extract the
|
||||||
|
// username from the JWT token. When not specified, it will default to "username".
|
||||||
|
// +optional
|
||||||
|
Username string `json:"username"`
|
||||||
|
}
|
||||||
|
|
||||||
// JWTAuthenticator describes the configuration of a JWT authenticator.
|
// JWTAuthenticator describes the configuration of a JWT authenticator.
|
||||||
//
|
//
|
||||||
// Upon receiving a signed JWT, a JWTAuthenticator will performs some validation on it (e.g., valid
|
// Upon receiving a signed JWT, a JWTAuthenticator will performs some validation on it (e.g., valid
|
||||||
|
@ -51,6 +51,21 @@ spec:
|
|||||||
description: Audience is the required value of the "aud" JWT claim.
|
description: Audience is the required value of the "aud" JWT claim.
|
||||||
minLength: 1
|
minLength: 1
|
||||||
type: string
|
type: string
|
||||||
|
claims:
|
||||||
|
description: Claims allows customization of the claims that will be
|
||||||
|
mapped to user identity for Kubernetes access.
|
||||||
|
properties:
|
||||||
|
groups:
|
||||||
|
description: Groups is the name of the claim which should be read
|
||||||
|
to extract the user's group membership from the JWT token. When
|
||||||
|
not specified, it will default to "groups".
|
||||||
|
type: string
|
||||||
|
username:
|
||||||
|
description: Username is the name of the claim which should be
|
||||||
|
read to extract the username from the JWT token. When not specified,
|
||||||
|
it will default to "username".
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
issuer:
|
issuer:
|
||||||
description: Issuer is the OIDC issuer URL that will be used to discover
|
description: Issuer is the OIDC issuer URL that will be used to discover
|
||||||
public signing keys. Issuer is also used to validate the "iss" JWT
|
public signing keys. Issuer is also used to validate the "iss" JWT
|
||||||
|
19
generated/1.17/README.adoc
generated
19
generated/1.17/README.adoc
generated
@ -92,6 +92,7 @@ Spec for configuring a JWT authenticator.
|
|||||||
| Field | Description
|
| Field | Description
|
||||||
| *`issuer`* __string__ | Issuer is the OIDC issuer URL that will be used to discover public signing keys. Issuer is also used to validate the "iss" JWT claim.
|
| *`issuer`* __string__ | Issuer is the OIDC issuer URL that will be used to discover public signing keys. Issuer is also used to validate the "iss" JWT claim.
|
||||||
| *`audience`* __string__ | Audience is the required value of the "aud" JWT claim.
|
| *`audience`* __string__ | Audience is the required value of the "aud" JWT claim.
|
||||||
|
| *`claims`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-authentication-v1alpha1-jwttokenclaims[$$JWTTokenClaims$$]__ | Claims allows customization of the claims that will be mapped to user identity for Kubernetes access.
|
||||||
| *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-authentication-v1alpha1-tlsspec[$$TLSSpec$$]__ | TLS configuration for communicating with the OIDC provider.
|
| *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-authentication-v1alpha1-tlsspec[$$TLSSpec$$]__ | TLS configuration for communicating with the OIDC provider.
|
||||||
|===
|
|===
|
||||||
|
|
||||||
@ -113,6 +114,24 @@ Status of a JWT authenticator.
|
|||||||
|===
|
|===
|
||||||
|
|
||||||
|
|
||||||
|
[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-authentication-v1alpha1-jwttokenclaims"]
|
||||||
|
==== JWTTokenClaims
|
||||||
|
|
||||||
|
JWTTokenClaims allows customization of the claims that will be mapped to user identity for Kubernetes access.
|
||||||
|
|
||||||
|
.Appears In:
|
||||||
|
****
|
||||||
|
- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-authentication-v1alpha1-jwtauthenticatorspec[$$JWTAuthenticatorSpec$$]
|
||||||
|
****
|
||||||
|
|
||||||
|
[cols="25a,75a", options="header"]
|
||||||
|
|===
|
||||||
|
| Field | Description
|
||||||
|
| *`groups`* __string__ | Groups is the name of the claim which should be read to extract the user's group membership from the JWT token. When not specified, it will default to "groups".
|
||||||
|
| *`username`* __string__ | Username is the name of the claim which should be read to extract the username from the JWT token. When not specified, it will default to "username".
|
||||||
|
|===
|
||||||
|
|
||||||
|
|
||||||
[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-authentication-v1alpha1-tlsspec"]
|
[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-authentication-v1alpha1-tlsspec"]
|
||||||
==== TLSSpec
|
==== TLSSpec
|
||||||
|
|
||||||
|
@ -27,11 +27,30 @@ type JWTAuthenticatorSpec struct {
|
|||||||
// +kubebuilder:validation:MinLength=1
|
// +kubebuilder:validation:MinLength=1
|
||||||
Audience string `json:"audience"`
|
Audience string `json:"audience"`
|
||||||
|
|
||||||
|
// Claims allows customization of the claims that will be mapped to user identity
|
||||||
|
// for Kubernetes access.
|
||||||
|
// +optional
|
||||||
|
Claims JWTTokenClaims `json:"claims"`
|
||||||
|
|
||||||
// TLS configuration for communicating with the OIDC provider.
|
// TLS configuration for communicating with the OIDC provider.
|
||||||
// +optional
|
// +optional
|
||||||
TLS *TLSSpec `json:"tls,omitempty"`
|
TLS *TLSSpec `json:"tls,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// JWTTokenClaims allows customization of the claims that will be mapped to user identity
|
||||||
|
// for Kubernetes access.
|
||||||
|
type JWTTokenClaims struct {
|
||||||
|
// Groups is the name of the claim which should be read to extract the user's
|
||||||
|
// group membership from the JWT token. When not specified, it will default to "groups".
|
||||||
|
// +optional
|
||||||
|
Groups string `json:"groups"`
|
||||||
|
|
||||||
|
// Username is the name of the claim which should be read to extract the
|
||||||
|
// username from the JWT token. When not specified, it will default to "username".
|
||||||
|
// +optional
|
||||||
|
Username string `json:"username"`
|
||||||
|
}
|
||||||
|
|
||||||
// JWTAuthenticator describes the configuration of a JWT authenticator.
|
// JWTAuthenticator describes the configuration of a JWT authenticator.
|
||||||
//
|
//
|
||||||
// Upon receiving a signed JWT, a JWTAuthenticator will performs some validation on it (e.g., valid
|
// Upon receiving a signed JWT, a JWTAuthenticator will performs some validation on it (e.g., valid
|
||||||
|
@ -92,6 +92,7 @@ func (in *JWTAuthenticatorList) DeepCopyObject() runtime.Object {
|
|||||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
func (in *JWTAuthenticatorSpec) DeepCopyInto(out *JWTAuthenticatorSpec) {
|
func (in *JWTAuthenticatorSpec) DeepCopyInto(out *JWTAuthenticatorSpec) {
|
||||||
*out = *in
|
*out = *in
|
||||||
|
out.Claims = in.Claims
|
||||||
if in.TLS != nil {
|
if in.TLS != nil {
|
||||||
in, out := &in.TLS, &out.TLS
|
in, out := &in.TLS, &out.TLS
|
||||||
*out = new(TLSSpec)
|
*out = new(TLSSpec)
|
||||||
@ -133,6 +134,22 @@ func (in *JWTAuthenticatorStatus) DeepCopy() *JWTAuthenticatorStatus {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *JWTTokenClaims) DeepCopyInto(out *JWTTokenClaims) {
|
||||||
|
*out = *in
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JWTTokenClaims.
|
||||||
|
func (in *JWTTokenClaims) DeepCopy() *JWTTokenClaims {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(JWTTokenClaims)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
func (in *TLSSpec) DeepCopyInto(out *TLSSpec) {
|
func (in *TLSSpec) DeepCopyInto(out *TLSSpec) {
|
||||||
*out = *in
|
*out = *in
|
||||||
|
@ -51,6 +51,21 @@ spec:
|
|||||||
description: Audience is the required value of the "aud" JWT claim.
|
description: Audience is the required value of the "aud" JWT claim.
|
||||||
minLength: 1
|
minLength: 1
|
||||||
type: string
|
type: string
|
||||||
|
claims:
|
||||||
|
description: Claims allows customization of the claims that will be
|
||||||
|
mapped to user identity for Kubernetes access.
|
||||||
|
properties:
|
||||||
|
groups:
|
||||||
|
description: Groups is the name of the claim which should be read
|
||||||
|
to extract the user's group membership from the JWT token. When
|
||||||
|
not specified, it will default to "groups".
|
||||||
|
type: string
|
||||||
|
username:
|
||||||
|
description: Username is the name of the claim which should be
|
||||||
|
read to extract the username from the JWT token. When not specified,
|
||||||
|
it will default to "username".
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
issuer:
|
issuer:
|
||||||
description: Issuer is the OIDC issuer URL that will be used to discover
|
description: Issuer is the OIDC issuer URL that will be used to discover
|
||||||
public signing keys. Issuer is also used to validate the "iss" JWT
|
public signing keys. Issuer is also used to validate the "iss" JWT
|
||||||
|
19
generated/1.18/README.adoc
generated
19
generated/1.18/README.adoc
generated
@ -92,6 +92,7 @@ Spec for configuring a JWT authenticator.
|
|||||||
| Field | Description
|
| Field | Description
|
||||||
| *`issuer`* __string__ | Issuer is the OIDC issuer URL that will be used to discover public signing keys. Issuer is also used to validate the "iss" JWT claim.
|
| *`issuer`* __string__ | Issuer is the OIDC issuer URL that will be used to discover public signing keys. Issuer is also used to validate the "iss" JWT claim.
|
||||||
| *`audience`* __string__ | Audience is the required value of the "aud" JWT claim.
|
| *`audience`* __string__ | Audience is the required value of the "aud" JWT claim.
|
||||||
|
| *`claims`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-concierge-authentication-v1alpha1-jwttokenclaims[$$JWTTokenClaims$$]__ | Claims allows customization of the claims that will be mapped to user identity for Kubernetes access.
|
||||||
| *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-concierge-authentication-v1alpha1-tlsspec[$$TLSSpec$$]__ | TLS configuration for communicating with the OIDC provider.
|
| *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-concierge-authentication-v1alpha1-tlsspec[$$TLSSpec$$]__ | TLS configuration for communicating with the OIDC provider.
|
||||||
|===
|
|===
|
||||||
|
|
||||||
@ -113,6 +114,24 @@ Status of a JWT authenticator.
|
|||||||
|===
|
|===
|
||||||
|
|
||||||
|
|
||||||
|
[id="{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-concierge-authentication-v1alpha1-jwttokenclaims"]
|
||||||
|
==== JWTTokenClaims
|
||||||
|
|
||||||
|
JWTTokenClaims allows customization of the claims that will be mapped to user identity for Kubernetes access.
|
||||||
|
|
||||||
|
.Appears In:
|
||||||
|
****
|
||||||
|
- xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-concierge-authentication-v1alpha1-jwtauthenticatorspec[$$JWTAuthenticatorSpec$$]
|
||||||
|
****
|
||||||
|
|
||||||
|
[cols="25a,75a", options="header"]
|
||||||
|
|===
|
||||||
|
| Field | Description
|
||||||
|
| *`groups`* __string__ | Groups is the name of the claim which should be read to extract the user's group membership from the JWT token. When not specified, it will default to "groups".
|
||||||
|
| *`username`* __string__ | Username is the name of the claim which should be read to extract the username from the JWT token. When not specified, it will default to "username".
|
||||||
|
|===
|
||||||
|
|
||||||
|
|
||||||
[id="{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-concierge-authentication-v1alpha1-tlsspec"]
|
[id="{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-concierge-authentication-v1alpha1-tlsspec"]
|
||||||
==== TLSSpec
|
==== TLSSpec
|
||||||
|
|
||||||
|
@ -27,11 +27,30 @@ type JWTAuthenticatorSpec struct {
|
|||||||
// +kubebuilder:validation:MinLength=1
|
// +kubebuilder:validation:MinLength=1
|
||||||
Audience string `json:"audience"`
|
Audience string `json:"audience"`
|
||||||
|
|
||||||
|
// Claims allows customization of the claims that will be mapped to user identity
|
||||||
|
// for Kubernetes access.
|
||||||
|
// +optional
|
||||||
|
Claims JWTTokenClaims `json:"claims"`
|
||||||
|
|
||||||
// TLS configuration for communicating with the OIDC provider.
|
// TLS configuration for communicating with the OIDC provider.
|
||||||
// +optional
|
// +optional
|
||||||
TLS *TLSSpec `json:"tls,omitempty"`
|
TLS *TLSSpec `json:"tls,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// JWTTokenClaims allows customization of the claims that will be mapped to user identity
|
||||||
|
// for Kubernetes access.
|
||||||
|
type JWTTokenClaims struct {
|
||||||
|
// Groups is the name of the claim which should be read to extract the user's
|
||||||
|
// group membership from the JWT token. When not specified, it will default to "groups".
|
||||||
|
// +optional
|
||||||
|
Groups string `json:"groups"`
|
||||||
|
|
||||||
|
// Username is the name of the claim which should be read to extract the
|
||||||
|
// username from the JWT token. When not specified, it will default to "username".
|
||||||
|
// +optional
|
||||||
|
Username string `json:"username"`
|
||||||
|
}
|
||||||
|
|
||||||
// JWTAuthenticator describes the configuration of a JWT authenticator.
|
// JWTAuthenticator describes the configuration of a JWT authenticator.
|
||||||
//
|
//
|
||||||
// Upon receiving a signed JWT, a JWTAuthenticator will performs some validation on it (e.g., valid
|
// Upon receiving a signed JWT, a JWTAuthenticator will performs some validation on it (e.g., valid
|
||||||
|
@ -92,6 +92,7 @@ func (in *JWTAuthenticatorList) DeepCopyObject() runtime.Object {
|
|||||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
func (in *JWTAuthenticatorSpec) DeepCopyInto(out *JWTAuthenticatorSpec) {
|
func (in *JWTAuthenticatorSpec) DeepCopyInto(out *JWTAuthenticatorSpec) {
|
||||||
*out = *in
|
*out = *in
|
||||||
|
out.Claims = in.Claims
|
||||||
if in.TLS != nil {
|
if in.TLS != nil {
|
||||||
in, out := &in.TLS, &out.TLS
|
in, out := &in.TLS, &out.TLS
|
||||||
*out = new(TLSSpec)
|
*out = new(TLSSpec)
|
||||||
@ -133,6 +134,22 @@ func (in *JWTAuthenticatorStatus) DeepCopy() *JWTAuthenticatorStatus {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *JWTTokenClaims) DeepCopyInto(out *JWTTokenClaims) {
|
||||||
|
*out = *in
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JWTTokenClaims.
|
||||||
|
func (in *JWTTokenClaims) DeepCopy() *JWTTokenClaims {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(JWTTokenClaims)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
func (in *TLSSpec) DeepCopyInto(out *TLSSpec) {
|
func (in *TLSSpec) DeepCopyInto(out *TLSSpec) {
|
||||||
*out = *in
|
*out = *in
|
||||||
|
@ -51,6 +51,21 @@ spec:
|
|||||||
description: Audience is the required value of the "aud" JWT claim.
|
description: Audience is the required value of the "aud" JWT claim.
|
||||||
minLength: 1
|
minLength: 1
|
||||||
type: string
|
type: string
|
||||||
|
claims:
|
||||||
|
description: Claims allows customization of the claims that will be
|
||||||
|
mapped to user identity for Kubernetes access.
|
||||||
|
properties:
|
||||||
|
groups:
|
||||||
|
description: Groups is the name of the claim which should be read
|
||||||
|
to extract the user's group membership from the JWT token. When
|
||||||
|
not specified, it will default to "groups".
|
||||||
|
type: string
|
||||||
|
username:
|
||||||
|
description: Username is the name of the claim which should be
|
||||||
|
read to extract the username from the JWT token. When not specified,
|
||||||
|
it will default to "username".
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
issuer:
|
issuer:
|
||||||
description: Issuer is the OIDC issuer URL that will be used to discover
|
description: Issuer is the OIDC issuer URL that will be used to discover
|
||||||
public signing keys. Issuer is also used to validate the "iss" JWT
|
public signing keys. Issuer is also used to validate the "iss" JWT
|
||||||
|
19
generated/1.19/README.adoc
generated
19
generated/1.19/README.adoc
generated
@ -92,6 +92,7 @@ Spec for configuring a JWT authenticator.
|
|||||||
| Field | Description
|
| Field | Description
|
||||||
| *`issuer`* __string__ | Issuer is the OIDC issuer URL that will be used to discover public signing keys. Issuer is also used to validate the "iss" JWT claim.
|
| *`issuer`* __string__ | Issuer is the OIDC issuer URL that will be used to discover public signing keys. Issuer is also used to validate the "iss" JWT claim.
|
||||||
| *`audience`* __string__ | Audience is the required value of the "aud" JWT claim.
|
| *`audience`* __string__ | Audience is the required value of the "aud" JWT claim.
|
||||||
|
| *`claims`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-concierge-authentication-v1alpha1-jwttokenclaims[$$JWTTokenClaims$$]__ | Claims allows customization of the claims that will be mapped to user identity for Kubernetes access.
|
||||||
| *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-concierge-authentication-v1alpha1-tlsspec[$$TLSSpec$$]__ | TLS configuration for communicating with the OIDC provider.
|
| *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-concierge-authentication-v1alpha1-tlsspec[$$TLSSpec$$]__ | TLS configuration for communicating with the OIDC provider.
|
||||||
|===
|
|===
|
||||||
|
|
||||||
@ -113,6 +114,24 @@ Status of a JWT authenticator.
|
|||||||
|===
|
|===
|
||||||
|
|
||||||
|
|
||||||
|
[id="{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-concierge-authentication-v1alpha1-jwttokenclaims"]
|
||||||
|
==== JWTTokenClaims
|
||||||
|
|
||||||
|
JWTTokenClaims allows customization of the claims that will be mapped to user identity for Kubernetes access.
|
||||||
|
|
||||||
|
.Appears In:
|
||||||
|
****
|
||||||
|
- xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-concierge-authentication-v1alpha1-jwtauthenticatorspec[$$JWTAuthenticatorSpec$$]
|
||||||
|
****
|
||||||
|
|
||||||
|
[cols="25a,75a", options="header"]
|
||||||
|
|===
|
||||||
|
| Field | Description
|
||||||
|
| *`groups`* __string__ | Groups is the name of the claim which should be read to extract the user's group membership from the JWT token. When not specified, it will default to "groups".
|
||||||
|
| *`username`* __string__ | Username is the name of the claim which should be read to extract the username from the JWT token. When not specified, it will default to "username".
|
||||||
|
|===
|
||||||
|
|
||||||
|
|
||||||
[id="{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-concierge-authentication-v1alpha1-tlsspec"]
|
[id="{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-concierge-authentication-v1alpha1-tlsspec"]
|
||||||
==== TLSSpec
|
==== TLSSpec
|
||||||
|
|
||||||
|
@ -27,11 +27,30 @@ type JWTAuthenticatorSpec struct {
|
|||||||
// +kubebuilder:validation:MinLength=1
|
// +kubebuilder:validation:MinLength=1
|
||||||
Audience string `json:"audience"`
|
Audience string `json:"audience"`
|
||||||
|
|
||||||
|
// Claims allows customization of the claims that will be mapped to user identity
|
||||||
|
// for Kubernetes access.
|
||||||
|
// +optional
|
||||||
|
Claims JWTTokenClaims `json:"claims"`
|
||||||
|
|
||||||
// TLS configuration for communicating with the OIDC provider.
|
// TLS configuration for communicating with the OIDC provider.
|
||||||
// +optional
|
// +optional
|
||||||
TLS *TLSSpec `json:"tls,omitempty"`
|
TLS *TLSSpec `json:"tls,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// JWTTokenClaims allows customization of the claims that will be mapped to user identity
|
||||||
|
// for Kubernetes access.
|
||||||
|
type JWTTokenClaims struct {
|
||||||
|
// Groups is the name of the claim which should be read to extract the user's
|
||||||
|
// group membership from the JWT token. When not specified, it will default to "groups".
|
||||||
|
// +optional
|
||||||
|
Groups string `json:"groups"`
|
||||||
|
|
||||||
|
// Username is the name of the claim which should be read to extract the
|
||||||
|
// username from the JWT token. When not specified, it will default to "username".
|
||||||
|
// +optional
|
||||||
|
Username string `json:"username"`
|
||||||
|
}
|
||||||
|
|
||||||
// JWTAuthenticator describes the configuration of a JWT authenticator.
|
// JWTAuthenticator describes the configuration of a JWT authenticator.
|
||||||
//
|
//
|
||||||
// Upon receiving a signed JWT, a JWTAuthenticator will performs some validation on it (e.g., valid
|
// Upon receiving a signed JWT, a JWTAuthenticator will performs some validation on it (e.g., valid
|
||||||
|
@ -92,6 +92,7 @@ func (in *JWTAuthenticatorList) DeepCopyObject() runtime.Object {
|
|||||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
func (in *JWTAuthenticatorSpec) DeepCopyInto(out *JWTAuthenticatorSpec) {
|
func (in *JWTAuthenticatorSpec) DeepCopyInto(out *JWTAuthenticatorSpec) {
|
||||||
*out = *in
|
*out = *in
|
||||||
|
out.Claims = in.Claims
|
||||||
if in.TLS != nil {
|
if in.TLS != nil {
|
||||||
in, out := &in.TLS, &out.TLS
|
in, out := &in.TLS, &out.TLS
|
||||||
*out = new(TLSSpec)
|
*out = new(TLSSpec)
|
||||||
@ -133,6 +134,22 @@ func (in *JWTAuthenticatorStatus) DeepCopy() *JWTAuthenticatorStatus {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *JWTTokenClaims) DeepCopyInto(out *JWTTokenClaims) {
|
||||||
|
*out = *in
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JWTTokenClaims.
|
||||||
|
func (in *JWTTokenClaims) DeepCopy() *JWTTokenClaims {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(JWTTokenClaims)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
func (in *TLSSpec) DeepCopyInto(out *TLSSpec) {
|
func (in *TLSSpec) DeepCopyInto(out *TLSSpec) {
|
||||||
*out = *in
|
*out = *in
|
||||||
|
@ -51,6 +51,21 @@ spec:
|
|||||||
description: Audience is the required value of the "aud" JWT claim.
|
description: Audience is the required value of the "aud" JWT claim.
|
||||||
minLength: 1
|
minLength: 1
|
||||||
type: string
|
type: string
|
||||||
|
claims:
|
||||||
|
description: Claims allows customization of the claims that will be
|
||||||
|
mapped to user identity for Kubernetes access.
|
||||||
|
properties:
|
||||||
|
groups:
|
||||||
|
description: Groups is the name of the claim which should be read
|
||||||
|
to extract the user's group membership from the JWT token. When
|
||||||
|
not specified, it will default to "groups".
|
||||||
|
type: string
|
||||||
|
username:
|
||||||
|
description: Username is the name of the claim which should be
|
||||||
|
read to extract the username from the JWT token. When not specified,
|
||||||
|
it will default to "username".
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
issuer:
|
issuer:
|
||||||
description: Issuer is the OIDC issuer URL that will be used to discover
|
description: Issuer is the OIDC issuer URL that will be used to discover
|
||||||
public signing keys. Issuer is also used to validate the "iss" JWT
|
public signing keys. Issuer is also used to validate the "iss" JWT
|
||||||
|
@ -29,7 +29,7 @@ import (
|
|||||||
// These default values come from the way that the Supervisor issues and signs tokens. We make these
|
// These default values come from the way that the Supervisor issues and signs tokens. We make these
|
||||||
// the defaults for a JWTAuthenticator so that they can easily integrate with the Supervisor.
|
// the defaults for a JWTAuthenticator so that they can easily integrate with the Supervisor.
|
||||||
const (
|
const (
|
||||||
defaultUsernameClaim = "sub"
|
defaultUsernameClaim = "username"
|
||||||
defaultGroupsClaim = "groups"
|
defaultGroupsClaim = "groups"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -169,12 +169,20 @@ func newJWTAuthenticator(spec *auth1alpha1.JWTAuthenticatorSpec) (*jwtAuthentica
|
|||||||
|
|
||||||
caFile = temp.Name()
|
caFile = temp.Name()
|
||||||
}
|
}
|
||||||
|
usernameClaim := spec.Claims.Username
|
||||||
|
if usernameClaim == "" {
|
||||||
|
usernameClaim = defaultUsernameClaim
|
||||||
|
}
|
||||||
|
groupsClaim := spec.Claims.Groups
|
||||||
|
if groupsClaim == "" {
|
||||||
|
groupsClaim = defaultGroupsClaim
|
||||||
|
}
|
||||||
|
|
||||||
authenticator, err := oidc.New(oidc.Options{
|
authenticator, err := oidc.New(oidc.Options{
|
||||||
IssuerURL: spec.Issuer,
|
IssuerURL: spec.Issuer,
|
||||||
ClientID: spec.Audience,
|
ClientID: spec.Audience,
|
||||||
UsernameClaim: defaultUsernameClaim,
|
UsernameClaim: usernameClaim,
|
||||||
GroupsClaim: defaultGroupsClaim,
|
GroupsClaim: groupsClaim,
|
||||||
SupportedSigningAlgs: defaultSupportedSigningAlgos(),
|
SupportedSigningAlgs: defaultSupportedSigningAlgos(),
|
||||||
CAFile: caFile,
|
CAFile: caFile,
|
||||||
})
|
})
|
||||||
|
@ -19,11 +19,10 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gopkg.in/square/go-jose.v2/jwt"
|
|
||||||
|
|
||||||
"github.com/golang/mock/gomock"
|
"github.com/golang/mock/gomock"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"gopkg.in/square/go-jose.v2"
|
"gopkg.in/square/go-jose.v2"
|
||||||
|
"gopkg.in/square/go-jose.v2/jwt"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apiserver/pkg/authentication/authenticator"
|
"k8s.io/apiserver/pkg/authentication/authenticator"
|
||||||
@ -41,225 +40,10 @@ import (
|
|||||||
func TestController(t *testing.T) {
|
func TestController(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
someJWTAuthenticatorSpec := &auth1alpha1.JWTAuthenticatorSpec{
|
|
||||||
Issuer: "https://some-issuer.com",
|
|
||||||
Audience: "some-audience",
|
|
||||||
TLS: &auth1alpha1.TLSSpec{CertificateAuthorityData: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURVVENDQWptZ0F3SUJBZ0lWQUpzNStTbVRtaTJXeUI0bGJJRXBXaUs5a1RkUE1BMEdDU3FHU0liM0RRRUIKQ3dVQU1COHhDekFKQmdOVkJBWVRBbFZUTVJBd0RnWURWUVFLREFkUWFYWnZkR0ZzTUI0WERUSXdNRFV3TkRFMgpNamMxT0ZvWERUSTBNRFV3TlRFMk1qYzFPRm93SHpFTE1Ba0dBMVVFQmhNQ1ZWTXhFREFPQmdOVkJBb01CMUJwCmRtOTBZV3d3Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRRERZWmZvWGR4Z2NXTEMKZEJtbHB5a0tBaG9JMlBuUWtsVFNXMno1cGcwaXJjOGFRL1E3MXZzMTRZYStmdWtFTGlvOTRZYWw4R01DdVFrbApMZ3AvUEE5N1VYelhQNDBpK25iNXcwRGpwWWd2dU9KQXJXMno2MFRnWE5NSFh3VHk4ME1SZEhpUFVWZ0VZd0JpCmtkNThzdEFVS1Y1MnBQTU1reTJjNy9BcFhJNmRXR2xjalUvaFBsNmtpRzZ5dEw2REtGYjJQRWV3MmdJM3pHZ2IKOFVVbnA1V05DZDd2WjNVY0ZHNXlsZEd3aGc3cnZ4U1ZLWi9WOEhCMGJmbjlxamlrSVcxWFM4dzdpUUNlQmdQMApYZWhKZmVITlZJaTJtZlczNlVQbWpMdnVKaGpqNDIrdFBQWndvdDkzdWtlcEgvbWpHcFJEVm9wamJyWGlpTUYrCkYxdnlPNGMxQWdNQkFBR2pnWU13Z1lBd0hRWURWUjBPQkJZRUZNTWJpSXFhdVkwajRVWWphWDl0bDJzby9LQ1IKTUI4R0ExVWRJd1FZTUJhQUZNTWJpSXFhdVkwajRVWWphWDl0bDJzby9LQ1JNQjBHQTFVZEpRUVdNQlFHQ0NzRwpBUVVGQndNQ0JnZ3JCZ0VGQlFjREFUQVBCZ05WSFJNQkFmOEVCVEFEQVFIL01BNEdBMVVkRHdFQi93UUVBd0lCCkJqQU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFYbEh4M2tIMDZwY2NDTDlEVE5qTnBCYnlVSytGd2R6T2IwWFYKcmpNaGtxdHVmdEpUUnR5T3hKZ0ZKNXhUR3pCdEtKamcrVU1pczBOV0t0VDBNWThVMU45U2c5SDl0RFpHRHBjVQpxMlVRU0Y4dXRQMVR3dnJIUzIrdzB2MUoxdHgrTEFiU0lmWmJCV0xXQ21EODUzRlVoWlFZekkvYXpFM28vd0p1CmlPUklMdUpNUk5vNlBXY3VLZmRFVkhaS1RTWnk3a25FcHNidGtsN3EwRE91eUFWdG9HVnlkb3VUR0FOdFhXK2YKczNUSTJjKzErZXg3L2RZOEJGQTFzNWFUOG5vZnU3T1RTTzdiS1kzSkRBUHZOeFQzKzVZUXJwNGR1Nmh0YUFMbAppOHNaRkhidmxpd2EzdlhxL3p1Y2JEaHEzQzBhZnAzV2ZwRGxwSlpvLy9QUUFKaTZLQT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K"},
|
|
||||||
}
|
|
||||||
otherJWTAuthenticatorSpec := &auth1alpha1.JWTAuthenticatorSpec{
|
|
||||||
Issuer: "https://some-other-issuer.com",
|
|
||||||
Audience: "some-audience",
|
|
||||||
TLS: &auth1alpha1.TLSSpec{CertificateAuthorityData: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURVVENDQWptZ0F3SUJBZ0lWQUpzNStTbVRtaTJXeUI0bGJJRXBXaUs5a1RkUE1BMEdDU3FHU0liM0RRRUIKQ3dVQU1COHhDekFKQmdOVkJBWVRBbFZUTVJBd0RnWURWUVFLREFkUWFYWnZkR0ZzTUI0WERUSXdNRFV3TkRFMgpNamMxT0ZvWERUSTBNRFV3TlRFMk1qYzFPRm93SHpFTE1Ba0dBMVVFQmhNQ1ZWTXhFREFPQmdOVkJBb01CMUJwCmRtOTBZV3d3Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRRERZWmZvWGR4Z2NXTEMKZEJtbHB5a0tBaG9JMlBuUWtsVFNXMno1cGcwaXJjOGFRL1E3MXZzMTRZYStmdWtFTGlvOTRZYWw4R01DdVFrbApMZ3AvUEE5N1VYelhQNDBpK25iNXcwRGpwWWd2dU9KQXJXMno2MFRnWE5NSFh3VHk4ME1SZEhpUFVWZ0VZd0JpCmtkNThzdEFVS1Y1MnBQTU1reTJjNy9BcFhJNmRXR2xjalUvaFBsNmtpRzZ5dEw2REtGYjJQRWV3MmdJM3pHZ2IKOFVVbnA1V05DZDd2WjNVY0ZHNXlsZEd3aGc3cnZ4U1ZLWi9WOEhCMGJmbjlxamlrSVcxWFM4dzdpUUNlQmdQMApYZWhKZmVITlZJaTJtZlczNlVQbWpMdnVKaGpqNDIrdFBQWndvdDkzdWtlcEgvbWpHcFJEVm9wamJyWGlpTUYrCkYxdnlPNGMxQWdNQkFBR2pnWU13Z1lBd0hRWURWUjBPQkJZRUZNTWJpSXFhdVkwajRVWWphWDl0bDJzby9LQ1IKTUI4R0ExVWRJd1FZTUJhQUZNTWJpSXFhdVkwajRVWWphWDl0bDJzby9LQ1JNQjBHQTFVZEpRUVdNQlFHQ0NzRwpBUVVGQndNQ0JnZ3JCZ0VGQlFjREFUQVBCZ05WSFJNQkFmOEVCVEFEQVFIL01BNEdBMVVkRHdFQi93UUVBd0lCCkJqQU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFYbEh4M2tIMDZwY2NDTDlEVE5qTnBCYnlVSytGd2R6T2IwWFYKcmpNaGtxdHVmdEpUUnR5T3hKZ0ZKNXhUR3pCdEtKamcrVU1pczBOV0t0VDBNWThVMU45U2c5SDl0RFpHRHBjVQpxMlVRU0Y4dXRQMVR3dnJIUzIrdzB2MUoxdHgrTEFiU0lmWmJCV0xXQ21EODUzRlVoWlFZekkvYXpFM28vd0p1CmlPUklMdUpNUk5vNlBXY3VLZmRFVkhaS1RTWnk3a25FcHNidGtsN3EwRE91eUFWdG9HVnlkb3VUR0FOdFhXK2YKczNUSTJjKzErZXg3L2RZOEJGQTFzNWFUOG5vZnU3T1RTTzdiS1kzSkRBUHZOeFQzKzVZUXJwNGR1Nmh0YUFMbAppOHNaRkhidmxpd2EzdlhxL3p1Y2JEaHEzQzBhZnAzV2ZwRGxwSlpvLy9QUUFKaTZLQT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K"},
|
|
||||||
}
|
|
||||||
missingTLSJWTAuthenticatorSpec := &auth1alpha1.JWTAuthenticatorSpec{
|
|
||||||
Issuer: "https://some-issuer.com",
|
|
||||||
Audience: "some-audience",
|
|
||||||
}
|
|
||||||
invalidTLSJWTAuthenticatorSpec := &auth1alpha1.JWTAuthenticatorSpec{
|
|
||||||
Issuer: "https://some-other-issuer.com",
|
|
||||||
Audience: "some-audience",
|
|
||||||
TLS: &auth1alpha1.TLSSpec{CertificateAuthorityData: "invalid base64-encoded data"},
|
|
||||||
}
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
cache func(*testing.T, *authncache.Cache, bool)
|
|
||||||
wantClose bool
|
|
||||||
syncKey controllerlib.Key
|
|
||||||
jwtAuthenticators []runtime.Object
|
|
||||||
wantErr string
|
|
||||||
wantLogs []string
|
|
||||||
wantCacheEntries int
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "not found",
|
|
||||||
syncKey: controllerlib.Key{Namespace: "test-namespace", Name: "test-name"},
|
|
||||||
wantLogs: []string{
|
|
||||||
`jwtcachefiller-controller "level"=0 "msg"="Sync() found that the JWTAuthenticator does not exist yet or was deleted"`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "valid jwt authenticator with CA",
|
|
||||||
syncKey: controllerlib.Key{Namespace: "test-namespace", Name: "test-name"},
|
|
||||||
jwtAuthenticators: []runtime.Object{
|
|
||||||
&auth1alpha1.JWTAuthenticator{
|
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
|
||||||
Namespace: "test-namespace",
|
|
||||||
Name: "test-name",
|
|
||||||
},
|
|
||||||
Spec: *someJWTAuthenticatorSpec,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wantLogs: []string{
|
|
||||||
`jwtcachefiller-controller "level"=0 "msg"="added new jwt authenticator" "issuer"="https://some-issuer.com" "jwtAuthenticator"={"name":"test-name","namespace":"test-namespace"}`,
|
|
||||||
},
|
|
||||||
wantCacheEntries: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "updating jwt authenticator with new fields closes previous instance",
|
|
||||||
cache: func(t *testing.T, cache *authncache.Cache, wantClose bool) {
|
|
||||||
cache.Store(
|
|
||||||
authncache.Key{
|
|
||||||
Name: "test-name",
|
|
||||||
Namespace: "test-namespace",
|
|
||||||
Kind: "JWTAuthenticator",
|
|
||||||
APIGroup: auth1alpha1.SchemeGroupVersion.Group,
|
|
||||||
},
|
|
||||||
newCacheValue(t, *otherJWTAuthenticatorSpec, wantClose),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
wantClose: true,
|
|
||||||
syncKey: controllerlib.Key{Namespace: "test-namespace", Name: "test-name"},
|
|
||||||
jwtAuthenticators: []runtime.Object{
|
|
||||||
&auth1alpha1.JWTAuthenticator{
|
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
|
||||||
Namespace: "test-namespace",
|
|
||||||
Name: "test-name",
|
|
||||||
},
|
|
||||||
Spec: *someJWTAuthenticatorSpec,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wantLogs: []string{
|
|
||||||
`jwtcachefiller-controller "level"=0 "msg"="added new jwt authenticator" "issuer"="https://some-issuer.com" "jwtAuthenticator"={"name":"test-name","namespace":"test-namespace"}`,
|
|
||||||
},
|
|
||||||
wantCacheEntries: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "updating jwt authenticator with the same value does nothing",
|
|
||||||
cache: func(t *testing.T, cache *authncache.Cache, wantClose bool) {
|
|
||||||
cache.Store(
|
|
||||||
authncache.Key{
|
|
||||||
Name: "test-name",
|
|
||||||
Namespace: "test-namespace",
|
|
||||||
Kind: "JWTAuthenticator",
|
|
||||||
APIGroup: auth1alpha1.SchemeGroupVersion.Group,
|
|
||||||
},
|
|
||||||
newCacheValue(t, *someJWTAuthenticatorSpec, wantClose),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
wantClose: false,
|
|
||||||
syncKey: controllerlib.Key{Namespace: "test-namespace", Name: "test-name"},
|
|
||||||
jwtAuthenticators: []runtime.Object{
|
|
||||||
&auth1alpha1.JWTAuthenticator{
|
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
|
||||||
Namespace: "test-namespace",
|
|
||||||
Name: "test-name",
|
|
||||||
},
|
|
||||||
Spec: *someJWTAuthenticatorSpec,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wantLogs: []string{
|
|
||||||
`jwtcachefiller-controller "level"=0 "msg"="actual jwt authenticator and desired jwt authenticator are the same" "issuer"="https://some-issuer.com" "jwtAuthenticator"={"name":"test-name","namespace":"test-namespace"}`,
|
|
||||||
},
|
|
||||||
wantCacheEntries: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "updating jwt authenticator when cache value is wrong type",
|
|
||||||
cache: func(t *testing.T, cache *authncache.Cache, wantClose bool) {
|
|
||||||
cache.Store(
|
|
||||||
authncache.Key{
|
|
||||||
Name: "test-name",
|
|
||||||
Namespace: "test-namespace",
|
|
||||||
Kind: "JWTAuthenticator",
|
|
||||||
APIGroup: auth1alpha1.SchemeGroupVersion.Group,
|
|
||||||
},
|
|
||||||
struct{ authenticator.Token }{},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
syncKey: controllerlib.Key{Namespace: "test-namespace", Name: "test-name"},
|
|
||||||
jwtAuthenticators: []runtime.Object{
|
|
||||||
&auth1alpha1.JWTAuthenticator{
|
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
|
||||||
Namespace: "test-namespace",
|
|
||||||
Name: "test-name",
|
|
||||||
},
|
|
||||||
Spec: *someJWTAuthenticatorSpec,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wantLogs: []string{
|
|
||||||
`jwtcachefiller-controller "level"=0 "msg"="wrong JWT authenticator type in cache" "actualType"="struct { authenticator.Token }"`,
|
|
||||||
`jwtcachefiller-controller "level"=0 "msg"="added new jwt authenticator" "issuer"="https://some-issuer.com" "jwtAuthenticator"={"name":"test-name","namespace":"test-namespace"}`,
|
|
||||||
},
|
|
||||||
wantCacheEntries: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "valid jwt authenticator without CA",
|
|
||||||
syncKey: controllerlib.Key{Namespace: "test-namespace", Name: "test-name"},
|
|
||||||
jwtAuthenticators: []runtime.Object{
|
|
||||||
&auth1alpha1.JWTAuthenticator{
|
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
|
||||||
Namespace: "test-namespace",
|
|
||||||
Name: "test-name",
|
|
||||||
},
|
|
||||||
Spec: *missingTLSJWTAuthenticatorSpec,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wantLogs: []string{
|
|
||||||
`jwtcachefiller-controller "level"=0 "msg"="added new jwt authenticator" "issuer"="https://some-issuer.com" "jwtAuthenticator"={"name":"test-name","namespace":"test-namespace"}`,
|
|
||||||
},
|
|
||||||
wantCacheEntries: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid jwt authenticator CA",
|
|
||||||
syncKey: controllerlib.Key{Namespace: "test-namespace", Name: "test-name"},
|
|
||||||
jwtAuthenticators: []runtime.Object{
|
|
||||||
&auth1alpha1.JWTAuthenticator{
|
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
|
||||||
Namespace: "test-namespace",
|
|
||||||
Name: "test-name",
|
|
||||||
},
|
|
||||||
Spec: *invalidTLSJWTAuthenticatorSpec,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wantErr: "failed to build jwt authenticator: invalid TLS configuration: illegal base64 data at input byte 7",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
tt := tt
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
fakeClient := pinnipedfake.NewSimpleClientset(tt.jwtAuthenticators...)
|
|
||||||
informers := pinnipedinformers.NewSharedInformerFactory(fakeClient, 0)
|
|
||||||
cache := authncache.New()
|
|
||||||
testLog := testlogger.New(t)
|
|
||||||
|
|
||||||
if tt.cache != nil {
|
|
||||||
tt.cache(t, cache, tt.wantClose)
|
|
||||||
}
|
|
||||||
|
|
||||||
controller := New(cache, informers.Authentication().V1alpha1().JWTAuthenticators(), testLog)
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
informers.Start(ctx.Done())
|
|
||||||
controllerlib.TestRunSynchronously(t, controller)
|
|
||||||
|
|
||||||
syncCtx := controllerlib.Context{Context: ctx, Key: tt.syncKey}
|
|
||||||
|
|
||||||
if err := controllerlib.TestSync(t, controller, syncCtx); tt.wantErr != "" {
|
|
||||||
require.EqualError(t, err, tt.wantErr)
|
|
||||||
} else {
|
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
|
||||||
require.Equal(t, tt.wantLogs, testLog.Lines())
|
|
||||||
require.Equal(t, tt.wantCacheEntries, len(cache.Keys()))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewJWTAuthenticator(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
goodSubject = "some-subject"
|
|
||||||
goodAudience = "some-audience"
|
|
||||||
group0 = "some-group-0"
|
|
||||||
group1 = "some-group-1"
|
|
||||||
|
|
||||||
goodECSigningKeyID = "some-ec-key-id"
|
goodECSigningKeyID = "some-ec-key-id"
|
||||||
goodRSASigningKeyID = "some-rsa-key-id"
|
goodRSASigningKeyID = "some-rsa-key-id"
|
||||||
|
goodAudience = "some-audience"
|
||||||
)
|
)
|
||||||
|
|
||||||
goodECSigningKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
goodECSigningKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
@ -299,29 +83,399 @@ func TestNewJWTAuthenticator(t *testing.T) {
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
goodIssuer := server.URL
|
goodIssuer := server.URL
|
||||||
a, err := newJWTAuthenticator(&auth1alpha1.JWTAuthenticatorSpec{
|
|
||||||
|
someJWTAuthenticatorSpec := &auth1alpha1.JWTAuthenticatorSpec{
|
||||||
Issuer: goodIssuer,
|
Issuer: goodIssuer,
|
||||||
Audience: goodAudience,
|
Audience: goodAudience,
|
||||||
TLS: tlsSpecFromTLSConfig(server.TLS),
|
TLS: tlsSpecFromTLSConfig(server.TLS),
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
t.Cleanup(a.Close)
|
|
||||||
|
|
||||||
// The implementation of AuthenticateToken() that we use waits 10 seconds after creation to
|
|
||||||
// perform OIDC discovery. Therefore, the JWTAuthenticator is not functional for the first 10
|
|
||||||
// seconds. We sleep for 13 seconds in this unit test to give a little bit of cushion to that 10
|
|
||||||
// second delay.
|
|
||||||
//
|
|
||||||
// We should get rid of this 10 second delay. See
|
|
||||||
// https://github.com/vmware-tanzu/pinniped/issues/260.
|
|
||||||
if testing.Short() {
|
|
||||||
t.Skip("skipping this test when '-short' flag is passed to avoid necessary 13 second sleep")
|
|
||||||
}
|
}
|
||||||
time.Sleep(time.Second * 13)
|
someJWTAuthenticatorSpecWithUsernameClaim := &auth1alpha1.JWTAuthenticatorSpec{
|
||||||
|
Issuer: goodIssuer,
|
||||||
|
Audience: goodAudience,
|
||||||
|
TLS: tlsSpecFromTLSConfig(server.TLS),
|
||||||
|
Claims: auth1alpha1.JWTTokenClaims{
|
||||||
|
Username: "my-custom-username-claim",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
someJWTAuthenticatorSpecWithGroupsClaim := &auth1alpha1.JWTAuthenticatorSpec{
|
||||||
|
Issuer: goodIssuer,
|
||||||
|
Audience: goodAudience,
|
||||||
|
TLS: tlsSpecFromTLSConfig(server.TLS),
|
||||||
|
Claims: auth1alpha1.JWTTokenClaims{
|
||||||
|
Groups: "my-custom-groups-claim",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
otherJWTAuthenticatorSpec := &auth1alpha1.JWTAuthenticatorSpec{
|
||||||
|
Issuer: "https://some-other-issuer.com",
|
||||||
|
Audience: goodAudience,
|
||||||
|
TLS: &auth1alpha1.TLSSpec{CertificateAuthorityData: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURVVENDQWptZ0F3SUJBZ0lWQUpzNStTbVRtaTJXeUI0bGJJRXBXaUs5a1RkUE1BMEdDU3FHU0liM0RRRUIKQ3dVQU1COHhDekFKQmdOVkJBWVRBbFZUTVJBd0RnWURWUVFLREFkUWFYWnZkR0ZzTUI0WERUSXdNRFV3TkRFMgpNamMxT0ZvWERUSTBNRFV3TlRFMk1qYzFPRm93SHpFTE1Ba0dBMVVFQmhNQ1ZWTXhFREFPQmdOVkJBb01CMUJwCmRtOTBZV3d3Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRRERZWmZvWGR4Z2NXTEMKZEJtbHB5a0tBaG9JMlBuUWtsVFNXMno1cGcwaXJjOGFRL1E3MXZzMTRZYStmdWtFTGlvOTRZYWw4R01DdVFrbApMZ3AvUEE5N1VYelhQNDBpK25iNXcwRGpwWWd2dU9KQXJXMno2MFRnWE5NSFh3VHk4ME1SZEhpUFVWZ0VZd0JpCmtkNThzdEFVS1Y1MnBQTU1reTJjNy9BcFhJNmRXR2xjalUvaFBsNmtpRzZ5dEw2REtGYjJQRWV3MmdJM3pHZ2IKOFVVbnA1V05DZDd2WjNVY0ZHNXlsZEd3aGc3cnZ4U1ZLWi9WOEhCMGJmbjlxamlrSVcxWFM4dzdpUUNlQmdQMApYZWhKZmVITlZJaTJtZlczNlVQbWpMdnVKaGpqNDIrdFBQWndvdDkzdWtlcEgvbWpHcFJEVm9wamJyWGlpTUYrCkYxdnlPNGMxQWdNQkFBR2pnWU13Z1lBd0hRWURWUjBPQkJZRUZNTWJpSXFhdVkwajRVWWphWDl0bDJzby9LQ1IKTUI4R0ExVWRJd1FZTUJhQUZNTWJpSXFhdVkwajRVWWphWDl0bDJzby9LQ1JNQjBHQTFVZEpRUVdNQlFHQ0NzRwpBUVVGQndNQ0JnZ3JCZ0VGQlFjREFUQVBCZ05WSFJNQkFmOEVCVEFEQVFIL01BNEdBMVVkRHdFQi93UUVBd0lCCkJqQU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFYbEh4M2tIMDZwY2NDTDlEVE5qTnBCYnlVSytGd2R6T2IwWFYKcmpNaGtxdHVmdEpUUnR5T3hKZ0ZKNXhUR3pCdEtKamcrVU1pczBOV0t0VDBNWThVMU45U2c5SDl0RFpHRHBjVQpxMlVRU0Y4dXRQMVR3dnJIUzIrdzB2MUoxdHgrTEFiU0lmWmJCV0xXQ21EODUzRlVoWlFZekkvYXpFM28vd0p1CmlPUklMdUpNUk5vNlBXY3VLZmRFVkhaS1RTWnk3a25FcHNidGtsN3EwRE91eUFWdG9HVnlkb3VUR0FOdFhXK2YKczNUSTJjKzErZXg3L2RZOEJGQTFzNWFUOG5vZnU3T1RTTzdiS1kzSkRBUHZOeFQzKzVZUXJwNGR1Nmh0YUFMbAppOHNaRkhidmxpd2EzdlhxL3p1Y2JEaHEzQzBhZnAzV2ZwRGxwSlpvLy9QUUFKaTZLQT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K"},
|
||||||
|
}
|
||||||
|
missingTLSJWTAuthenticatorSpec := &auth1alpha1.JWTAuthenticatorSpec{
|
||||||
|
Issuer: goodIssuer,
|
||||||
|
Audience: goodAudience,
|
||||||
|
}
|
||||||
|
invalidTLSJWTAuthenticatorSpec := &auth1alpha1.JWTAuthenticatorSpec{
|
||||||
|
Issuer: "https://some-other-issuer.com",
|
||||||
|
Audience: goodAudience,
|
||||||
|
TLS: &auth1alpha1.TLSSpec{CertificateAuthorityData: "invalid base64-encoded data"},
|
||||||
|
}
|
||||||
|
|
||||||
var tests = []struct {
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
cache func(*testing.T, *authncache.Cache, bool)
|
||||||
|
syncKey controllerlib.Key
|
||||||
|
jwtAuthenticators []runtime.Object
|
||||||
|
wantClose bool
|
||||||
|
wantErr string
|
||||||
|
wantLogs []string
|
||||||
|
wantCacheEntries int
|
||||||
|
wantUsernameClaim string
|
||||||
|
wantGroupsClaim string
|
||||||
|
runTestsOnResultingAuthenticator bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "not found",
|
||||||
|
syncKey: controllerlib.Key{Namespace: "test-namespace", Name: "test-name"},
|
||||||
|
wantLogs: []string{
|
||||||
|
`jwtcachefiller-controller "level"=0 "msg"="Sync() found that the JWTAuthenticator does not exist yet or was deleted"`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid jwt authenticator with CA",
|
||||||
|
syncKey: controllerlib.Key{Namespace: "test-namespace", Name: "test-name"},
|
||||||
|
jwtAuthenticators: []runtime.Object{
|
||||||
|
&auth1alpha1.JWTAuthenticator{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Namespace: "test-namespace",
|
||||||
|
Name: "test-name",
|
||||||
|
},
|
||||||
|
Spec: *someJWTAuthenticatorSpec,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantLogs: []string{
|
||||||
|
`jwtcachefiller-controller "level"=0 "msg"="added new jwt authenticator" "issuer"="` + goodIssuer + `" "jwtAuthenticator"={"name":"test-name","namespace":"test-namespace"}`,
|
||||||
|
},
|
||||||
|
wantCacheEntries: 1,
|
||||||
|
runTestsOnResultingAuthenticator: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid jwt authenticator with custom username claim",
|
||||||
|
syncKey: controllerlib.Key{Namespace: "test-namespace", Name: "test-name"},
|
||||||
|
jwtAuthenticators: []runtime.Object{
|
||||||
|
&auth1alpha1.JWTAuthenticator{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Namespace: "test-namespace",
|
||||||
|
Name: "test-name",
|
||||||
|
},
|
||||||
|
Spec: *someJWTAuthenticatorSpecWithUsernameClaim,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantLogs: []string{
|
||||||
|
`jwtcachefiller-controller "level"=0 "msg"="added new jwt authenticator" "issuer"="` + goodIssuer + `" "jwtAuthenticator"={"name":"test-name","namespace":"test-namespace"}`,
|
||||||
|
},
|
||||||
|
wantCacheEntries: 1,
|
||||||
|
wantUsernameClaim: someJWTAuthenticatorSpecWithUsernameClaim.Claims.Username,
|
||||||
|
runTestsOnResultingAuthenticator: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid jwt authenticator with custom groups claim",
|
||||||
|
syncKey: controllerlib.Key{Namespace: "test-namespace", Name: "test-name"},
|
||||||
|
jwtAuthenticators: []runtime.Object{
|
||||||
|
&auth1alpha1.JWTAuthenticator{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Namespace: "test-namespace",
|
||||||
|
Name: "test-name",
|
||||||
|
},
|
||||||
|
Spec: *someJWTAuthenticatorSpecWithGroupsClaim,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantLogs: []string{
|
||||||
|
`jwtcachefiller-controller "level"=0 "msg"="added new jwt authenticator" "issuer"="` + goodIssuer + `" "jwtAuthenticator"={"name":"test-name","namespace":"test-namespace"}`,
|
||||||
|
},
|
||||||
|
wantCacheEntries: 1,
|
||||||
|
wantGroupsClaim: someJWTAuthenticatorSpecWithGroupsClaim.Claims.Groups,
|
||||||
|
runTestsOnResultingAuthenticator: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "updating jwt authenticator with new fields closes previous instance",
|
||||||
|
cache: func(t *testing.T, cache *authncache.Cache, wantClose bool) {
|
||||||
|
cache.Store(
|
||||||
|
authncache.Key{
|
||||||
|
Name: "test-name",
|
||||||
|
Namespace: "test-namespace",
|
||||||
|
Kind: "JWTAuthenticator",
|
||||||
|
APIGroup: auth1alpha1.SchemeGroupVersion.Group,
|
||||||
|
},
|
||||||
|
newCacheValue(t, *otherJWTAuthenticatorSpec, wantClose),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
wantClose: true,
|
||||||
|
syncKey: controllerlib.Key{Namespace: "test-namespace", Name: "test-name"},
|
||||||
|
jwtAuthenticators: []runtime.Object{
|
||||||
|
&auth1alpha1.JWTAuthenticator{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Namespace: "test-namespace",
|
||||||
|
Name: "test-name",
|
||||||
|
},
|
||||||
|
Spec: *someJWTAuthenticatorSpec,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantLogs: []string{
|
||||||
|
`jwtcachefiller-controller "level"=0 "msg"="added new jwt authenticator" "issuer"="` + goodIssuer + `" "jwtAuthenticator"={"name":"test-name","namespace":"test-namespace"}`,
|
||||||
|
},
|
||||||
|
wantCacheEntries: 1,
|
||||||
|
runTestsOnResultingAuthenticator: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "updating jwt authenticator with the same value does nothing",
|
||||||
|
cache: func(t *testing.T, cache *authncache.Cache, wantClose bool) {
|
||||||
|
cache.Store(
|
||||||
|
authncache.Key{
|
||||||
|
Name: "test-name",
|
||||||
|
Namespace: "test-namespace",
|
||||||
|
Kind: "JWTAuthenticator",
|
||||||
|
APIGroup: auth1alpha1.SchemeGroupVersion.Group,
|
||||||
|
},
|
||||||
|
newCacheValue(t, *someJWTAuthenticatorSpec, wantClose),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
wantClose: false,
|
||||||
|
syncKey: controllerlib.Key{Namespace: "test-namespace", Name: "test-name"},
|
||||||
|
jwtAuthenticators: []runtime.Object{
|
||||||
|
&auth1alpha1.JWTAuthenticator{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Namespace: "test-namespace",
|
||||||
|
Name: "test-name",
|
||||||
|
},
|
||||||
|
Spec: *someJWTAuthenticatorSpec,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantLogs: []string{
|
||||||
|
`jwtcachefiller-controller "level"=0 "msg"="actual jwt authenticator and desired jwt authenticator are the same" "issuer"="` + goodIssuer + `" "jwtAuthenticator"={"name":"test-name","namespace":"test-namespace"}`,
|
||||||
|
},
|
||||||
|
wantCacheEntries: 1,
|
||||||
|
runTestsOnResultingAuthenticator: false, // skip the tests because the authenticator left in the cache is the mock version that was added above
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "updating jwt authenticator when cache value is wrong type",
|
||||||
|
cache: func(t *testing.T, cache *authncache.Cache, wantClose bool) {
|
||||||
|
cache.Store(
|
||||||
|
authncache.Key{
|
||||||
|
Name: "test-name",
|
||||||
|
Namespace: "test-namespace",
|
||||||
|
Kind: "JWTAuthenticator",
|
||||||
|
APIGroup: auth1alpha1.SchemeGroupVersion.Group,
|
||||||
|
},
|
||||||
|
struct{ authenticator.Token }{},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
syncKey: controllerlib.Key{Namespace: "test-namespace", Name: "test-name"},
|
||||||
|
jwtAuthenticators: []runtime.Object{
|
||||||
|
&auth1alpha1.JWTAuthenticator{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Namespace: "test-namespace",
|
||||||
|
Name: "test-name",
|
||||||
|
},
|
||||||
|
Spec: *someJWTAuthenticatorSpec,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantLogs: []string{
|
||||||
|
`jwtcachefiller-controller "level"=0 "msg"="wrong JWT authenticator type in cache" "actualType"="struct { authenticator.Token }"`,
|
||||||
|
`jwtcachefiller-controller "level"=0 "msg"="added new jwt authenticator" "issuer"="` + goodIssuer + `" "jwtAuthenticator"={"name":"test-name","namespace":"test-namespace"}`,
|
||||||
|
},
|
||||||
|
wantCacheEntries: 1,
|
||||||
|
runTestsOnResultingAuthenticator: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid jwt authenticator without CA",
|
||||||
|
syncKey: controllerlib.Key{Namespace: "test-namespace", Name: "test-name"},
|
||||||
|
jwtAuthenticators: []runtime.Object{
|
||||||
|
&auth1alpha1.JWTAuthenticator{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Namespace: "test-namespace",
|
||||||
|
Name: "test-name",
|
||||||
|
},
|
||||||
|
Spec: *missingTLSJWTAuthenticatorSpec,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantLogs: []string{
|
||||||
|
`jwtcachefiller-controller "level"=0 "msg"="added new jwt authenticator" "issuer"="` + goodIssuer + `" "jwtAuthenticator"={"name":"test-name","namespace":"test-namespace"}`,
|
||||||
|
},
|
||||||
|
wantCacheEntries: 1,
|
||||||
|
runTestsOnResultingAuthenticator: false, // skip the tests because the authenticator left in the cache doesn't have the CA for our test discovery server
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid jwt authenticator CA",
|
||||||
|
syncKey: controllerlib.Key{Namespace: "test-namespace", Name: "test-name"},
|
||||||
|
jwtAuthenticators: []runtime.Object{
|
||||||
|
&auth1alpha1.JWTAuthenticator{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Namespace: "test-namespace",
|
||||||
|
Name: "test-name",
|
||||||
|
},
|
||||||
|
Spec: *invalidTLSJWTAuthenticatorSpec,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: "failed to build jwt authenticator: invalid TLS configuration: illegal base64 data at input byte 7",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt := tt
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
fakeClient := pinnipedfake.NewSimpleClientset(tt.jwtAuthenticators...)
|
||||||
|
informers := pinnipedinformers.NewSharedInformerFactory(fakeClient, 0)
|
||||||
|
cache := authncache.New()
|
||||||
|
testLog := testlogger.New(t)
|
||||||
|
|
||||||
|
if tt.cache != nil {
|
||||||
|
tt.cache(t, cache, tt.wantClose)
|
||||||
|
}
|
||||||
|
|
||||||
|
controller := New(cache, informers.Authentication().V1alpha1().JWTAuthenticators(), testLog)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
informers.Start(ctx.Done())
|
||||||
|
controllerlib.TestRunSynchronously(t, controller)
|
||||||
|
|
||||||
|
syncCtx := controllerlib.Context{Context: ctx, Key: tt.syncKey}
|
||||||
|
|
||||||
|
if err := controllerlib.TestSync(t, controller, syncCtx); tt.wantErr != "" {
|
||||||
|
require.EqualError(t, err, tt.wantErr)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
require.Equal(t, tt.wantLogs, testLog.Lines())
|
||||||
|
require.Equal(t, tt.wantCacheEntries, len(cache.Keys()))
|
||||||
|
|
||||||
|
if !tt.runTestsOnResultingAuthenticator {
|
||||||
|
return // end of test unless we wanted to run tests on the resulting authenticator from the cache
|
||||||
|
}
|
||||||
|
|
||||||
|
// The implementation of AuthenticateToken() that we use waits 10 seconds after creation to
|
||||||
|
// perform OIDC discovery. Therefore, the JWTAuthenticator is not functional for the first 10
|
||||||
|
// seconds. We sleep for 13 seconds in this unit test to give a little bit of cushion to that 10
|
||||||
|
// second delay.
|
||||||
|
//
|
||||||
|
// We should get rid of this 10 second delay. See
|
||||||
|
// https://github.com/vmware-tanzu/pinniped/issues/260.
|
||||||
|
time.Sleep(time.Second * 13)
|
||||||
|
|
||||||
|
// We expected the cache to have an entry, so pull that entry from the cache and test it.
|
||||||
|
expectedCacheKey := authncache.Key{
|
||||||
|
APIGroup: auth1alpha1.GroupName,
|
||||||
|
Kind: "JWTAuthenticator",
|
||||||
|
Namespace: syncCtx.Key.Namespace,
|
||||||
|
Name: syncCtx.Key.Name,
|
||||||
|
}
|
||||||
|
cachedAuthenticator := cache.Get(expectedCacheKey)
|
||||||
|
require.NotNil(t, cachedAuthenticator)
|
||||||
|
|
||||||
|
// Schedule it to be closed at the end of the test.
|
||||||
|
t.Cleanup(cachedAuthenticator.(*jwtAuthenticator).Close)
|
||||||
|
|
||||||
|
const (
|
||||||
|
goodSubject = "some-subject"
|
||||||
|
group0 = "some-group-0"
|
||||||
|
group1 = "some-group-1"
|
||||||
|
goodUsername = "pinny123"
|
||||||
|
)
|
||||||
|
|
||||||
|
if tt.wantUsernameClaim == "" {
|
||||||
|
tt.wantUsernameClaim = "username"
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.wantGroupsClaim == "" {
|
||||||
|
tt.wantGroupsClaim = "groups"
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testTableForAuthenticateTokenTests(
|
||||||
|
t,
|
||||||
|
goodRSASigningKey,
|
||||||
|
goodRSASigningAlgo,
|
||||||
|
goodRSASigningKeyID,
|
||||||
|
group0,
|
||||||
|
group1,
|
||||||
|
goodUsername,
|
||||||
|
tt.wantUsernameClaim,
|
||||||
|
tt.wantGroupsClaim,
|
||||||
|
) {
|
||||||
|
test := test
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
wellKnownClaims := jwt.Claims{
|
||||||
|
Issuer: goodIssuer,
|
||||||
|
Subject: goodSubject,
|
||||||
|
Audience: []string{goodAudience},
|
||||||
|
Expiry: jwt.NewNumericDate(time.Now().Add(time.Hour)),
|
||||||
|
NotBefore: jwt.NewNumericDate(time.Now().Add(-time.Hour)),
|
||||||
|
IssuedAt: jwt.NewNumericDate(time.Now().Add(-time.Hour)),
|
||||||
|
}
|
||||||
|
var groups interface{}
|
||||||
|
username := goodUsername
|
||||||
|
if test.jwtClaims != nil {
|
||||||
|
test.jwtClaims(&wellKnownClaims, &groups, &username)
|
||||||
|
}
|
||||||
|
|
||||||
|
var signingKey interface{} = goodECSigningKey
|
||||||
|
signingAlgo := goodECSigningAlgo
|
||||||
|
signingKID := goodECSigningKeyID
|
||||||
|
if test.jwtSignature != nil {
|
||||||
|
test.jwtSignature(&signingKey, &signingAlgo, &signingKID)
|
||||||
|
}
|
||||||
|
|
||||||
|
jwt := createJWT(
|
||||||
|
t,
|
||||||
|
signingKey,
|
||||||
|
signingAlgo,
|
||||||
|
signingKID,
|
||||||
|
&wellKnownClaims,
|
||||||
|
tt.wantGroupsClaim,
|
||||||
|
groups,
|
||||||
|
tt.wantUsernameClaim,
|
||||||
|
username,
|
||||||
|
)
|
||||||
|
rsp, authenticated, err := cachedAuthenticator.AuthenticateToken(context.Background(), jwt)
|
||||||
|
if test.wantErrorRegexp != "" {
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Regexp(t, test.wantErrorRegexp, err.Error())
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, test.wantResponse, rsp)
|
||||||
|
require.Equal(t, test.wantAuthenticated, authenticated)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testTableForAuthenticateTokenTests(
|
||||||
|
t *testing.T,
|
||||||
|
goodRSASigningKey *rsa.PrivateKey,
|
||||||
|
goodRSASigningAlgo jose.SignatureAlgorithm,
|
||||||
|
goodRSASigningKeyID string,
|
||||||
|
group0 string,
|
||||||
|
group1 string,
|
||||||
|
goodUsername string,
|
||||||
|
expectedUsernameClaim string,
|
||||||
|
expectedGroupsClaim string,
|
||||||
|
) []struct {
|
||||||
|
name string
|
||||||
|
jwtClaims func(wellKnownClaims *jwt.Claims, groups *interface{}, username *string)
|
||||||
|
jwtSignature func(key *interface{}, algo *jose.SignatureAlgorithm, kid *string)
|
||||||
|
wantResponse *authenticator.Response
|
||||||
|
wantAuthenticated bool
|
||||||
|
wantErrorRegexp string
|
||||||
|
} {
|
||||||
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
jwtClaims func(wellKnownClaims *jwt.Claims, groups *interface{})
|
jwtClaims func(wellKnownClaims *jwt.Claims, groups *interface{}, username *string)
|
||||||
jwtSignature func(key *interface{}, algo *jose.SignatureAlgorithm, kid *string)
|
jwtSignature func(key *interface{}, algo *jose.SignatureAlgorithm, kid *string)
|
||||||
wantResponse *authenticator.Response
|
wantResponse *authenticator.Response
|
||||||
wantAuthenticated bool
|
wantAuthenticated bool
|
||||||
@ -331,7 +485,7 @@ func TestNewJWTAuthenticator(t *testing.T) {
|
|||||||
name: "good token without groups and with EC signature",
|
name: "good token without groups and with EC signature",
|
||||||
wantResponse: &authenticator.Response{
|
wantResponse: &authenticator.Response{
|
||||||
User: &user.DefaultInfo{
|
User: &user.DefaultInfo{
|
||||||
Name: goodSubject,
|
Name: goodUsername,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
wantAuthenticated: true,
|
wantAuthenticated: true,
|
||||||
@ -345,19 +499,19 @@ func TestNewJWTAuthenticator(t *testing.T) {
|
|||||||
},
|
},
|
||||||
wantResponse: &authenticator.Response{
|
wantResponse: &authenticator.Response{
|
||||||
User: &user.DefaultInfo{
|
User: &user.DefaultInfo{
|
||||||
Name: goodSubject,
|
Name: goodUsername,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
wantAuthenticated: true,
|
wantAuthenticated: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "good token with groups as array",
|
name: "good token with groups as array",
|
||||||
jwtClaims: func(_ *jwt.Claims, groups *interface{}) {
|
jwtClaims: func(_ *jwt.Claims, groups *interface{}, username *string) {
|
||||||
*groups = []string{group0, group1}
|
*groups = []string{group0, group1}
|
||||||
},
|
},
|
||||||
wantResponse: &authenticator.Response{
|
wantResponse: &authenticator.Response{
|
||||||
User: &user.DefaultInfo{
|
User: &user.DefaultInfo{
|
||||||
Name: goodSubject,
|
Name: goodUsername,
|
||||||
Groups: []string{group0, group1},
|
Groups: []string{group0, group1},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -365,12 +519,12 @@ func TestNewJWTAuthenticator(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "good token with groups as string",
|
name: "good token with groups as string",
|
||||||
jwtClaims: func(_ *jwt.Claims, groups *interface{}) {
|
jwtClaims: func(_ *jwt.Claims, groups *interface{}, username *string) {
|
||||||
*groups = group0
|
*groups = group0
|
||||||
},
|
},
|
||||||
wantResponse: &authenticator.Response{
|
wantResponse: &authenticator.Response{
|
||||||
User: &user.DefaultInfo{
|
User: &user.DefaultInfo{
|
||||||
Name: goodSubject,
|
Name: goodUsername,
|
||||||
Groups: []string{group0},
|
Groups: []string{group0},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -378,26 +532,26 @@ func TestNewJWTAuthenticator(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "good token with nbf unset",
|
name: "good token with nbf unset",
|
||||||
jwtClaims: func(claims *jwt.Claims, _ *interface{}) {
|
jwtClaims: func(claims *jwt.Claims, _ *interface{}, username *string) {
|
||||||
claims.NotBefore = nil
|
claims.NotBefore = nil
|
||||||
},
|
},
|
||||||
wantResponse: &authenticator.Response{
|
wantResponse: &authenticator.Response{
|
||||||
User: &user.DefaultInfo{
|
User: &user.DefaultInfo{
|
||||||
Name: goodSubject,
|
Name: goodUsername,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
wantAuthenticated: true,
|
wantAuthenticated: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "bad token with groups as map",
|
name: "bad token with groups as map",
|
||||||
jwtClaims: func(_ *jwt.Claims, groups *interface{}) {
|
jwtClaims: func(_ *jwt.Claims, groups *interface{}, username *string) {
|
||||||
*groups = map[string]string{"not an array": "or a string"}
|
*groups = map[string]string{"not an array": "or a string"}
|
||||||
},
|
},
|
||||||
wantErrorRegexp: "oidc: parse groups claim \"groups\": json: cannot unmarshal object into Go value of type string",
|
wantErrorRegexp: "oidc: parse groups claim \"" + expectedGroupsClaim + "\": json: cannot unmarshal object into Go value of type string",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "bad token with wrong issuer",
|
name: "bad token with wrong issuer",
|
||||||
jwtClaims: func(claims *jwt.Claims, _ *interface{}) {
|
jwtClaims: func(claims *jwt.Claims, _ *interface{}, username *string) {
|
||||||
claims.Issuer = "wrong-issuer"
|
claims.Issuer = "wrong-issuer"
|
||||||
},
|
},
|
||||||
wantResponse: nil,
|
wantResponse: nil,
|
||||||
@ -405,38 +559,45 @@ func TestNewJWTAuthenticator(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "bad token with no audience",
|
name: "bad token with no audience",
|
||||||
jwtClaims: func(claims *jwt.Claims, _ *interface{}) {
|
jwtClaims: func(claims *jwt.Claims, _ *interface{}, username *string) {
|
||||||
claims.Audience = nil
|
claims.Audience = nil
|
||||||
},
|
},
|
||||||
wantErrorRegexp: `oidc: verify token: oidc: expected audience "some-audience" got \[\]`,
|
wantErrorRegexp: `oidc: verify token: oidc: expected audience "some-audience" got \[\]`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "bad token with wrong audience",
|
name: "bad token with wrong audience",
|
||||||
jwtClaims: func(claims *jwt.Claims, _ *interface{}) {
|
jwtClaims: func(claims *jwt.Claims, _ *interface{}, username *string) {
|
||||||
claims.Audience = []string{"wrong-audience"}
|
claims.Audience = []string{"wrong-audience"}
|
||||||
},
|
},
|
||||||
wantErrorRegexp: `oidc: verify token: oidc: expected audience "some-audience" got \["wrong-audience"\]`,
|
wantErrorRegexp: `oidc: verify token: oidc: expected audience "some-audience" got \["wrong-audience"\]`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "bad token with nbf in the future",
|
name: "bad token with nbf in the future",
|
||||||
jwtClaims: func(claims *jwt.Claims, _ *interface{}) {
|
jwtClaims: func(claims *jwt.Claims, _ *interface{}, username *string) {
|
||||||
claims.NotBefore = jwt.NewNumericDate(time.Date(3020, 2, 3, 4, 5, 6, 7, time.UTC))
|
claims.NotBefore = jwt.NewNumericDate(time.Date(3020, 2, 3, 4, 5, 6, 7, time.UTC))
|
||||||
},
|
},
|
||||||
wantErrorRegexp: `oidc: verify token: oidc: current time .* before the nbf \(not before\) time: 3020-.*`,
|
wantErrorRegexp: `oidc: verify token: oidc: current time .* before the nbf \(not before\) time: 3020-.*`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "bad token with exp in past",
|
name: "bad token with exp in past",
|
||||||
jwtClaims: func(claims *jwt.Claims, _ *interface{}) {
|
jwtClaims: func(claims *jwt.Claims, _ *interface{}, username *string) {
|
||||||
claims.Expiry = jwt.NewNumericDate(time.Date(1, 2, 3, 4, 5, 6, 7, time.UTC))
|
claims.Expiry = jwt.NewNumericDate(time.Date(1, 2, 3, 4, 5, 6, 7, time.UTC))
|
||||||
},
|
},
|
||||||
wantErrorRegexp: `oidc: verify token: oidc: token is expired \(Token Expiry: 0001-02-02 23:09:04 -0456 LMT\)`,
|
wantErrorRegexp: `oidc: verify token: oidc: token is expired \(Token Expiry: .+`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "bad token without exp",
|
name: "bad token without exp",
|
||||||
jwtClaims: func(claims *jwt.Claims, _ *interface{}) {
|
jwtClaims: func(claims *jwt.Claims, _ *interface{}, username *string) {
|
||||||
claims.Expiry = nil
|
claims.Expiry = nil
|
||||||
},
|
},
|
||||||
wantErrorRegexp: `oidc: verify token: oidc: token is expired \(Token Expiry: 0001-01-01 00:00:00 \+0000 UTC\)`,
|
wantErrorRegexp: `oidc: verify token: oidc: token is expired \(Token Expiry: .+`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "token does not have username claim",
|
||||||
|
jwtClaims: func(claims *jwt.Claims, _ *interface{}, username *string) {
|
||||||
|
*username = ""
|
||||||
|
},
|
||||||
|
wantErrorRegexp: `oidc: parse username claims "` + expectedUsernameClaim + `": claim not present`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "signing key is wrong",
|
name: "signing key is wrong",
|
||||||
@ -459,43 +620,8 @@ func TestNewJWTAuthenticator(t *testing.T) {
|
|||||||
wantErrorRegexp: `oidc: verify token: oidc: id token signed with unsupported algorithm, expected \["RS256" "ES256"\] got "ES384"`,
|
wantErrorRegexp: `oidc: verify token: oidc: id token signed with unsupported algorithm, expected \["RS256" "ES256"\] got "ES384"`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, test := range tests {
|
|
||||||
test := test
|
|
||||||
t.Run(test.name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
wellKnownClaims := jwt.Claims{
|
return tests
|
||||||
Issuer: goodIssuer,
|
|
||||||
Subject: goodSubject,
|
|
||||||
Audience: []string{goodAudience},
|
|
||||||
Expiry: jwt.NewNumericDate(time.Now().Add(time.Hour)),
|
|
||||||
NotBefore: jwt.NewNumericDate(time.Now().Add(-time.Hour)),
|
|
||||||
IssuedAt: jwt.NewNumericDate(time.Now().Add(-time.Hour)),
|
|
||||||
}
|
|
||||||
var groups interface{}
|
|
||||||
if test.jwtClaims != nil {
|
|
||||||
test.jwtClaims(&wellKnownClaims, &groups)
|
|
||||||
}
|
|
||||||
|
|
||||||
var signingKey interface{} = goodECSigningKey
|
|
||||||
signingAlgo := goodECSigningAlgo
|
|
||||||
signingKID := goodECSigningKeyID
|
|
||||||
if test.jwtSignature != nil {
|
|
||||||
test.jwtSignature(&signingKey, &signingAlgo, &signingKID)
|
|
||||||
}
|
|
||||||
|
|
||||||
jwt := createJWT(t, signingKey, signingAlgo, signingKID, &wellKnownClaims, groups)
|
|
||||||
rsp, authenticated, err := a.AuthenticateToken(context.Background(), jwt)
|
|
||||||
if test.wantErrorRegexp != "" {
|
|
||||||
require.Error(t, err)
|
|
||||||
require.Regexp(t, test.wantErrorRegexp, err.Error())
|
|
||||||
} else {
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, test.wantResponse, rsp)
|
|
||||||
require.Equal(t, test.wantAuthenticated, authenticated)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func tlsSpecFromTLSConfig(tls *tls.Config) *auth1alpha1.TLSSpec {
|
func tlsSpecFromTLSConfig(tls *tls.Config) *auth1alpha1.TLSSpec {
|
||||||
@ -519,7 +645,10 @@ func createJWT(
|
|||||||
signingAlgo jose.SignatureAlgorithm,
|
signingAlgo jose.SignatureAlgorithm,
|
||||||
kid string,
|
kid string,
|
||||||
claims *jwt.Claims,
|
claims *jwt.Claims,
|
||||||
groups interface{},
|
groupsClaim string,
|
||||||
|
groupsValue interface{},
|
||||||
|
usernameClaim string,
|
||||||
|
usernameValue string,
|
||||||
) string {
|
) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
@ -530,8 +659,11 @@ func createJWT(
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
builder := jwt.Signed(sig).Claims(claims)
|
builder := jwt.Signed(sig).Claims(claims)
|
||||||
if groups != nil {
|
if groupsValue != nil {
|
||||||
builder = builder.Claims(map[string]interface{}{"groups": groups})
|
builder = builder.Claims(map[string]interface{}{groupsClaim: groupsValue})
|
||||||
|
}
|
||||||
|
if usernameValue != "" {
|
||||||
|
builder = builder.Claims(map[string]interface{}{usernameClaim: usernameValue})
|
||||||
}
|
}
|
||||||
jwt, err := builder.CompactSerialize()
|
jwt, err := builder.CompactSerialize()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
@ -24,22 +24,6 @@ import (
|
|||||||
"go.pinniped.dev/internal/plog"
|
"go.pinniped.dev/internal/plog"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
// The name of the issuer claim specified in the OIDC spec.
|
|
||||||
idTokenIssuerClaim = "iss"
|
|
||||||
|
|
||||||
// The name of the subject claim specified in the OIDC spec.
|
|
||||||
idTokenSubjectClaim = "sub"
|
|
||||||
|
|
||||||
// defaultUpstreamUsernameClaim is what we will use to extract the username from an upstream OIDC
|
|
||||||
// ID token if the upstream OIDC IDP did not tell us to use another claim.
|
|
||||||
defaultUpstreamUsernameClaim = idTokenSubjectClaim
|
|
||||||
|
|
||||||
// downstreamGroupsClaim is what we will use to encode the groups in the downstream OIDC ID token
|
|
||||||
// information.
|
|
||||||
downstreamGroupsClaim = "groups"
|
|
||||||
)
|
|
||||||
|
|
||||||
func NewHandler(
|
func NewHandler(
|
||||||
idpListGetter oidc.IDPListGetter,
|
idpListGetter oidc.IDPListGetter,
|
||||||
oauthHelper fosite.OAuth2Provider,
|
oauthHelper fosite.OAuth2Provider,
|
||||||
@ -89,7 +73,7 @@ func NewHandler(
|
|||||||
return httperr.New(http.StatusBadGateway, "error exchanging and validating upstream tokens")
|
return httperr.New(http.StatusBadGateway, "error exchanging and validating upstream tokens")
|
||||||
}
|
}
|
||||||
|
|
||||||
username, err := getUsernameFromUpstreamIDToken(upstreamIDPConfig, token.IDToken.Claims)
|
subject, username, err := getSubjectAndUsernameFromUpstreamIDToken(upstreamIDPConfig, token.IDToken.Claims)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -99,7 +83,7 @@ func NewHandler(
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
openIDSession := makeDownstreamSession(username, groups)
|
openIDSession := makeDownstreamSession(subject, username, groups)
|
||||||
authorizeResponder, err := oauthHelper.NewAuthorizeResponse(r.Context(), authorizeRequester, openIDSession)
|
authorizeResponder, err := oauthHelper.NewAuthorizeResponse(r.Context(), authorizeRequester, openIDSession)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
plog.WarningErr("error while generating and saving authcode", err, "upstreamName", upstreamIDPConfig.GetName())
|
plog.WarningErr("error while generating and saving authcode", err, "upstreamName", upstreamIDPConfig.GetName())
|
||||||
@ -193,36 +177,54 @@ func readState(r *http.Request, stateDecoder oidc.Decoder) (*oidc.UpstreamStateP
|
|||||||
return &state, nil
|
return &state, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getUsernameFromUpstreamIDToken(
|
func getSubjectAndUsernameFromUpstreamIDToken(
|
||||||
upstreamIDPConfig provider.UpstreamOIDCIdentityProviderI,
|
upstreamIDPConfig provider.UpstreamOIDCIdentityProviderI,
|
||||||
idTokenClaims map[string]interface{},
|
idTokenClaims map[string]interface{},
|
||||||
) (string, error) {
|
) (string, string, error) {
|
||||||
usernameClaim := upstreamIDPConfig.GetUsernameClaim()
|
// The spec says the "sub" claim is only unique per issuer,
|
||||||
|
// so we will prepend the issuer string to make it globally unique.
|
||||||
|
upstreamIssuer := idTokenClaims[oidc.IDTokenIssuerClaim]
|
||||||
|
if upstreamIssuer == "" {
|
||||||
|
plog.Warning(
|
||||||
|
"issuer claim in upstream ID token missing",
|
||||||
|
"upstreamName", upstreamIDPConfig.GetName(),
|
||||||
|
"issClaim", upstreamIssuer,
|
||||||
|
)
|
||||||
|
return "", "", httperr.New(http.StatusUnprocessableEntity, "issuer claim in upstream ID token missing")
|
||||||
|
}
|
||||||
|
upstreamIssuerAsString, ok := upstreamIssuer.(string)
|
||||||
|
if !ok {
|
||||||
|
plog.Warning(
|
||||||
|
"issuer claim in upstream ID token has invalid format",
|
||||||
|
"upstreamName", upstreamIDPConfig.GetName(),
|
||||||
|
"issClaim", upstreamIssuer,
|
||||||
|
)
|
||||||
|
return "", "", httperr.New(http.StatusUnprocessableEntity, "issuer claim in upstream ID token has invalid format")
|
||||||
|
}
|
||||||
|
|
||||||
user := ""
|
subjectAsInterface, ok := idTokenClaims[oidc.IDTokenSubjectClaim]
|
||||||
|
if !ok {
|
||||||
|
plog.Warning(
|
||||||
|
"no subject claim in upstream ID token",
|
||||||
|
"upstreamName", upstreamIDPConfig.GetName(),
|
||||||
|
)
|
||||||
|
return "", "", httperr.New(http.StatusUnprocessableEntity, "no subject claim in upstream ID token")
|
||||||
|
}
|
||||||
|
|
||||||
|
upstreamSubject, ok := subjectAsInterface.(string)
|
||||||
|
if !ok {
|
||||||
|
plog.Warning(
|
||||||
|
"subject claim in upstream ID token has invalid format",
|
||||||
|
"upstreamName", upstreamIDPConfig.GetName(),
|
||||||
|
)
|
||||||
|
return "", "", httperr.New(http.StatusUnprocessableEntity, "subject claim in upstream ID token has invalid format")
|
||||||
|
}
|
||||||
|
|
||||||
|
subject := fmt.Sprintf("%s?%s=%s", upstreamIssuerAsString, oidc.IDTokenSubjectClaim, upstreamSubject)
|
||||||
|
|
||||||
|
usernameClaim := upstreamIDPConfig.GetUsernameClaim()
|
||||||
if usernameClaim == "" {
|
if usernameClaim == "" {
|
||||||
// The spec says the "sub" claim is only unique per issuer, so by default when there is
|
return subject, subject, nil
|
||||||
// no specific username claim configured we will prepend the issuer string to make it globally unique.
|
|
||||||
upstreamIssuer := idTokenClaims[idTokenIssuerClaim]
|
|
||||||
if upstreamIssuer == "" {
|
|
||||||
plog.Warning(
|
|
||||||
"issuer claim in upstream ID token missing",
|
|
||||||
"upstreamName", upstreamIDPConfig.GetName(),
|
|
||||||
"issClaim", upstreamIssuer,
|
|
||||||
)
|
|
||||||
return "", httperr.New(http.StatusUnprocessableEntity, "issuer claim in upstream ID token missing")
|
|
||||||
}
|
|
||||||
upstreamIssuerAsString, ok := upstreamIssuer.(string)
|
|
||||||
if !ok {
|
|
||||||
plog.Warning(
|
|
||||||
"issuer claim in upstream ID token has invalid format",
|
|
||||||
"upstreamName", upstreamIDPConfig.GetName(),
|
|
||||||
"issClaim", upstreamIssuer,
|
|
||||||
)
|
|
||||||
return "", httperr.New(http.StatusUnprocessableEntity, "issuer claim in upstream ID token has invalid format")
|
|
||||||
}
|
|
||||||
user = fmt.Sprintf("%s?%s=", upstreamIssuerAsString, idTokenSubjectClaim)
|
|
||||||
usernameClaim = defaultUpstreamUsernameClaim
|
|
||||||
}
|
}
|
||||||
|
|
||||||
usernameAsInterface, ok := idTokenClaims[usernameClaim]
|
usernameAsInterface, ok := idTokenClaims[usernameClaim]
|
||||||
@ -233,7 +235,7 @@ func getUsernameFromUpstreamIDToken(
|
|||||||
"configuredUsernameClaim", upstreamIDPConfig.GetUsernameClaim(),
|
"configuredUsernameClaim", upstreamIDPConfig.GetUsernameClaim(),
|
||||||
"usernameClaim", usernameClaim,
|
"usernameClaim", usernameClaim,
|
||||||
)
|
)
|
||||||
return "", httperr.New(http.StatusUnprocessableEntity, "no username claim in upstream ID token")
|
return "", "", httperr.New(http.StatusUnprocessableEntity, "no username claim in upstream ID token")
|
||||||
}
|
}
|
||||||
|
|
||||||
username, ok := usernameAsInterface.(string)
|
username, ok := usernameAsInterface.(string)
|
||||||
@ -244,16 +246,16 @@ func getUsernameFromUpstreamIDToken(
|
|||||||
"configuredUsernameClaim", upstreamIDPConfig.GetUsernameClaim(),
|
"configuredUsernameClaim", upstreamIDPConfig.GetUsernameClaim(),
|
||||||
"usernameClaim", usernameClaim,
|
"usernameClaim", usernameClaim,
|
||||||
)
|
)
|
||||||
return "", httperr.New(http.StatusUnprocessableEntity, "username claim in upstream ID token has invalid format")
|
return "", "", httperr.New(http.StatusUnprocessableEntity, "username claim in upstream ID token has invalid format")
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Sprintf("%s%s", user, username), nil
|
return subject, username, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getGroupsFromUpstreamIDToken(
|
func getGroupsFromUpstreamIDToken(
|
||||||
upstreamIDPConfig provider.UpstreamOIDCIdentityProviderI,
|
upstreamIDPConfig provider.UpstreamOIDCIdentityProviderI,
|
||||||
idTokenClaims map[string]interface{},
|
idTokenClaims map[string]interface{},
|
||||||
) ([]string, error) {
|
) (interface{}, error) {
|
||||||
groupsClaim := upstreamIDPConfig.GetGroupsClaim()
|
groupsClaim := upstreamIDPConfig.GetGroupsClaim()
|
||||||
if groupsClaim == "" {
|
if groupsClaim == "" {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@ -270,8 +272,9 @@ func getGroupsFromUpstreamIDToken(
|
|||||||
return nil, httperr.New(http.StatusUnprocessableEntity, "no groups claim in upstream ID token")
|
return nil, httperr.New(http.StatusUnprocessableEntity, "no groups claim in upstream ID token")
|
||||||
}
|
}
|
||||||
|
|
||||||
groups, ok := groupsAsInterface.([]string)
|
groupsAsArray, okAsArray := groupsAsInterface.([]string)
|
||||||
if !ok {
|
groupsAsString, okAsString := groupsAsInterface.(string)
|
||||||
|
if !okAsArray && !okAsString {
|
||||||
plog.Warning(
|
plog.Warning(
|
||||||
"groups claim in upstream ID token has invalid format",
|
"groups claim in upstream ID token has invalid format",
|
||||||
"upstreamName", upstreamIDPConfig.GetName(),
|
"upstreamName", upstreamIDPConfig.GetName(),
|
||||||
@ -281,22 +284,26 @@ func getGroupsFromUpstreamIDToken(
|
|||||||
return nil, httperr.New(http.StatusUnprocessableEntity, "groups claim in upstream ID token has invalid format")
|
return nil, httperr.New(http.StatusUnprocessableEntity, "groups claim in upstream ID token has invalid format")
|
||||||
}
|
}
|
||||||
|
|
||||||
return groups, nil
|
if okAsArray {
|
||||||
|
return groupsAsArray, nil
|
||||||
|
}
|
||||||
|
return groupsAsString, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeDownstreamSession(username string, groups []string) *openid.DefaultSession {
|
func makeDownstreamSession(subject string, username string, groups interface{}) *openid.DefaultSession {
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
openIDSession := &openid.DefaultSession{
|
openIDSession := &openid.DefaultSession{
|
||||||
Claims: &jwt.IDTokenClaims{
|
Claims: &jwt.IDTokenClaims{
|
||||||
Subject: username,
|
Subject: subject,
|
||||||
RequestedAt: now,
|
RequestedAt: now,
|
||||||
AuthTime: now,
|
AuthTime: now,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
openIDSession.Claims.Extra = map[string]interface{}{
|
||||||
|
oidc.DownstreamUsernameClaim: username,
|
||||||
|
}
|
||||||
if groups != nil {
|
if groups != nil {
|
||||||
openIDSession.Claims.Extra = map[string]interface{}{
|
openIDSession.Claims.Extra[oidc.DownstreamGroupsClaim] = groups
|
||||||
downstreamGroupsClaim: groups,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return openIDSession
|
return openIDSession
|
||||||
}
|
}
|
||||||
|
@ -133,7 +133,8 @@ func TestCallbackEndpoint(t *testing.T) {
|
|||||||
wantRedirectLocationRegexp string
|
wantRedirectLocationRegexp string
|
||||||
wantDownstreamGrantedScopes []string
|
wantDownstreamGrantedScopes []string
|
||||||
wantDownstreamIDTokenSubject string
|
wantDownstreamIDTokenSubject string
|
||||||
wantDownstreamIDTokenGroups []string
|
wantDownstreamIDTokenUsername string
|
||||||
|
wantDownstreamIDTokenGroups interface{}
|
||||||
wantDownstreamRequestedScopes []string
|
wantDownstreamRequestedScopes []string
|
||||||
wantDownstreamNonce string
|
wantDownstreamNonce string
|
||||||
wantDownstreamPKCEChallenge string
|
wantDownstreamPKCEChallenge string
|
||||||
@ -150,7 +151,8 @@ func TestCallbackEndpoint(t *testing.T) {
|
|||||||
wantStatus: http.StatusFound,
|
wantStatus: http.StatusFound,
|
||||||
wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp,
|
wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp,
|
||||||
wantBody: "",
|
wantBody: "",
|
||||||
wantDownstreamIDTokenSubject: upstreamUsername,
|
wantDownstreamIDTokenSubject: upstreamIssuer + "?sub=" + upstreamSubject,
|
||||||
|
wantDownstreamIDTokenUsername: upstreamUsername,
|
||||||
wantDownstreamIDTokenGroups: upstreamGroupMembership,
|
wantDownstreamIDTokenGroups: upstreamGroupMembership,
|
||||||
wantDownstreamRequestedScopes: happyDownstreamScopesRequested,
|
wantDownstreamRequestedScopes: happyDownstreamScopesRequested,
|
||||||
wantDownstreamGrantedScopes: happyDownstreamScopesGranted,
|
wantDownstreamGrantedScopes: happyDownstreamScopesGranted,
|
||||||
@ -169,6 +171,7 @@ func TestCallbackEndpoint(t *testing.T) {
|
|||||||
wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp,
|
wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp,
|
||||||
wantBody: "",
|
wantBody: "",
|
||||||
wantDownstreamIDTokenSubject: upstreamIssuer + "?sub=" + upstreamSubject,
|
wantDownstreamIDTokenSubject: upstreamIssuer + "?sub=" + upstreamSubject,
|
||||||
|
wantDownstreamIDTokenUsername: upstreamIssuer + "?sub=" + upstreamSubject,
|
||||||
wantDownstreamIDTokenGroups: nil,
|
wantDownstreamIDTokenGroups: nil,
|
||||||
wantDownstreamRequestedScopes: happyDownstreamScopesRequested,
|
wantDownstreamRequestedScopes: happyDownstreamScopesRequested,
|
||||||
wantDownstreamGrantedScopes: happyDownstreamScopesGranted,
|
wantDownstreamGrantedScopes: happyDownstreamScopesGranted,
|
||||||
@ -186,7 +189,8 @@ func TestCallbackEndpoint(t *testing.T) {
|
|||||||
wantStatus: http.StatusFound,
|
wantStatus: http.StatusFound,
|
||||||
wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp,
|
wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp,
|
||||||
wantBody: "",
|
wantBody: "",
|
||||||
wantDownstreamIDTokenSubject: upstreamSubject,
|
wantDownstreamIDTokenSubject: upstreamIssuer + "?sub=" + upstreamSubject,
|
||||||
|
wantDownstreamIDTokenUsername: upstreamSubject,
|
||||||
wantDownstreamIDTokenGroups: upstreamGroupMembership,
|
wantDownstreamIDTokenGroups: upstreamGroupMembership,
|
||||||
wantDownstreamRequestedScopes: happyDownstreamScopesRequested,
|
wantDownstreamRequestedScopes: happyDownstreamScopesRequested,
|
||||||
wantDownstreamGrantedScopes: happyDownstreamScopesGranted,
|
wantDownstreamGrantedScopes: happyDownstreamScopesGranted,
|
||||||
@ -195,6 +199,25 @@ func TestCallbackEndpoint(t *testing.T) {
|
|||||||
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
|
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
|
||||||
wantExchangeAndValidateTokensCall: happyExchangeAndValidateTokensArgs,
|
wantExchangeAndValidateTokensCall: happyExchangeAndValidateTokensArgs,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "upstream IDP's configured groups claim in the ID token has a non-array value",
|
||||||
|
idp: happyUpstream().WithIDTokenClaim(upstreamGroupsClaim, "notAnArrayGroup1 notAnArrayGroup2").Build(),
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: newRequestPath().WithState(happyState).String(),
|
||||||
|
csrfCookie: happyCSRFCookie,
|
||||||
|
wantStatus: http.StatusFound,
|
||||||
|
wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp,
|
||||||
|
wantBody: "",
|
||||||
|
wantDownstreamIDTokenSubject: upstreamIssuer + "?sub=" + upstreamSubject,
|
||||||
|
wantDownstreamIDTokenUsername: upstreamUsername,
|
||||||
|
wantDownstreamIDTokenGroups: "notAnArrayGroup1 notAnArrayGroup2",
|
||||||
|
wantDownstreamRequestedScopes: happyDownstreamScopesRequested,
|
||||||
|
wantDownstreamGrantedScopes: happyDownstreamScopesGranted,
|
||||||
|
wantDownstreamNonce: downstreamNonce,
|
||||||
|
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
|
||||||
|
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
|
||||||
|
wantExchangeAndValidateTokensCall: happyExchangeAndValidateTokensArgs,
|
||||||
|
},
|
||||||
|
|
||||||
// Pre-upstream-exchange verification
|
// Pre-upstream-exchange verification
|
||||||
{
|
{
|
||||||
@ -312,7 +335,8 @@ func TestCallbackEndpoint(t *testing.T) {
|
|||||||
csrfCookie: happyCSRFCookie,
|
csrfCookie: happyCSRFCookie,
|
||||||
wantStatus: http.StatusFound,
|
wantStatus: http.StatusFound,
|
||||||
wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=&state=` + happyDownstreamState,
|
wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=&state=` + happyDownstreamState,
|
||||||
wantDownstreamIDTokenSubject: upstreamUsername,
|
wantDownstreamIDTokenUsername: upstreamUsername,
|
||||||
|
wantDownstreamIDTokenSubject: upstreamIssuer + "?sub=" + upstreamSubject,
|
||||||
wantDownstreamRequestedScopes: []string{"profile", "email"},
|
wantDownstreamRequestedScopes: []string{"profile", "email"},
|
||||||
wantDownstreamIDTokenGroups: upstreamGroupMembership,
|
wantDownstreamIDTokenGroups: upstreamGroupMembership,
|
||||||
wantDownstreamNonce: downstreamNonce,
|
wantDownstreamNonce: downstreamNonce,
|
||||||
@ -333,7 +357,8 @@ func TestCallbackEndpoint(t *testing.T) {
|
|||||||
csrfCookie: happyCSRFCookie,
|
csrfCookie: happyCSRFCookie,
|
||||||
wantStatus: http.StatusFound,
|
wantStatus: http.StatusFound,
|
||||||
wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=openid%20offline_access&state=` + happyDownstreamState,
|
wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=openid%20offline_access&state=` + happyDownstreamState,
|
||||||
wantDownstreamIDTokenSubject: upstreamUsername,
|
wantDownstreamIDTokenUsername: upstreamUsername,
|
||||||
|
wantDownstreamIDTokenSubject: upstreamIssuer + "?sub=" + upstreamSubject,
|
||||||
wantDownstreamRequestedScopes: []string{"openid", "offline_access"},
|
wantDownstreamRequestedScopes: []string{"openid", "offline_access"},
|
||||||
wantDownstreamGrantedScopes: []string{"openid", "offline_access"},
|
wantDownstreamGrantedScopes: []string{"openid", "offline_access"},
|
||||||
wantDownstreamIDTokenGroups: upstreamGroupMembership,
|
wantDownstreamIDTokenGroups: upstreamGroupMembership,
|
||||||
@ -524,6 +549,7 @@ func TestCallbackEndpoint(t *testing.T) {
|
|||||||
authcodeDataAndSignature[1], // Authcode store key is authcode signature
|
authcodeDataAndSignature[1], // Authcode store key is authcode signature
|
||||||
test.wantDownstreamGrantedScopes,
|
test.wantDownstreamGrantedScopes,
|
||||||
test.wantDownstreamIDTokenSubject,
|
test.wantDownstreamIDTokenSubject,
|
||||||
|
test.wantDownstreamIDTokenUsername,
|
||||||
test.wantDownstreamIDTokenGroups,
|
test.wantDownstreamIDTokenGroups,
|
||||||
test.wantDownstreamRequestedScopes,
|
test.wantDownstreamRequestedScopes,
|
||||||
)
|
)
|
||||||
@ -677,8 +703,8 @@ func happyUpstream() *upstreamOIDCIdentityProviderBuilder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *upstreamOIDCIdentityProviderBuilder) WithUsernameClaim(claim string) *upstreamOIDCIdentityProviderBuilder {
|
func (u *upstreamOIDCIdentityProviderBuilder) WithUsernameClaim(value string) *upstreamOIDCIdentityProviderBuilder {
|
||||||
u.usernameClaim = claim
|
u.usernameClaim = value
|
||||||
return u
|
return u
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -744,7 +770,8 @@ func validateAuthcodeStorage(
|
|||||||
storeKey string,
|
storeKey string,
|
||||||
wantDownstreamGrantedScopes []string,
|
wantDownstreamGrantedScopes []string,
|
||||||
wantDownstreamIDTokenSubject string,
|
wantDownstreamIDTokenSubject string,
|
||||||
wantDownstreamIDTokenGroups []string,
|
wantDownstreamIDTokenUsername string,
|
||||||
|
wantDownstreamIDTokenGroups interface{},
|
||||||
wantDownstreamRequestedScopes []string,
|
wantDownstreamRequestedScopes []string,
|
||||||
) (*fosite.Request, *openid.DefaultSession) {
|
) (*fosite.Request, *openid.DefaultSession) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
@ -780,13 +807,24 @@ func validateAuthcodeStorage(
|
|||||||
// Now confirm the ID token claims.
|
// Now confirm the ID token claims.
|
||||||
actualClaims := storedSessionFromAuthcode.Claims
|
actualClaims := storedSessionFromAuthcode.Claims
|
||||||
|
|
||||||
// Check the user's identity, which are put into the downstream ID token's subject and groups claims.
|
// Check the user's identity, which are put into the downstream ID token's subject, username and groups claims.
|
||||||
require.Equal(t, wantDownstreamIDTokenSubject, actualClaims.Subject)
|
require.Equal(t, wantDownstreamIDTokenSubject, actualClaims.Subject)
|
||||||
if wantDownstreamIDTokenGroups != nil {
|
require.Equal(t, wantDownstreamIDTokenUsername, actualClaims.Extra["username"])
|
||||||
require.Len(t, actualClaims.Extra, 1)
|
if wantDownstreamIDTokenGroups != nil { //nolint:nestif // there are some nested if's here but its probably fine for a test
|
||||||
require.ElementsMatch(t, wantDownstreamIDTokenGroups, actualClaims.Extra["groups"])
|
require.Len(t, actualClaims.Extra, 2)
|
||||||
|
wantArray, ok := wantDownstreamIDTokenGroups.([]string)
|
||||||
|
if ok {
|
||||||
|
require.ElementsMatch(t, wantArray, actualClaims.Extra["groups"])
|
||||||
|
} else {
|
||||||
|
wantString, ok := wantDownstreamIDTokenGroups.(string)
|
||||||
|
if ok {
|
||||||
|
require.Equal(t, wantString, actualClaims.Extra["groups"])
|
||||||
|
} else {
|
||||||
|
require.Fail(t, "wantDownstreamIDTokenGroups should be of type: either []string or string")
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
require.Empty(t, actualClaims.Extra)
|
require.Len(t, actualClaims.Extra, 1)
|
||||||
require.NotContains(t, actualClaims.Extra, "groups")
|
require.NotContains(t, actualClaims.Extra, "groups")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,6 +45,21 @@ const (
|
|||||||
// cookie contents.
|
// cookie contents.
|
||||||
CSRFCookieEncodingName = "csrf"
|
CSRFCookieEncodingName = "csrf"
|
||||||
|
|
||||||
|
// The name of the issuer claim specified in the OIDC spec.
|
||||||
|
IDTokenIssuerClaim = "iss"
|
||||||
|
|
||||||
|
// The name of the subject claim specified in the OIDC spec.
|
||||||
|
IDTokenSubjectClaim = "sub"
|
||||||
|
|
||||||
|
// DownstreamUsernameClaim is a custom claim in the downstream ID token
|
||||||
|
// whose value is mapped from a claim in the upstream token.
|
||||||
|
// By default the value is the same as the downstream subject claim's.
|
||||||
|
DownstreamUsernameClaim = "username"
|
||||||
|
|
||||||
|
// DownstreamGroupsClaim is what we will use to encode the groups in the downstream OIDC ID token
|
||||||
|
// information.
|
||||||
|
DownstreamGroupsClaim = "groups"
|
||||||
|
|
||||||
// CSRFCookieLifespan is the length of time that the CSRF cookie is valid. After this time, the
|
// CSRFCookieLifespan is the length of time that the CSRF cookie is valid. After this time, the
|
||||||
// Supervisor's authorization endpoint should give the browser a new CSRF cookie. We set it to
|
// Supervisor's authorization endpoint should give the browser a new CSRF cookie. We set it to
|
||||||
// a week so that it is unlikely to expire during a login.
|
// a week so that it is unlikely to expire during a login.
|
||||||
|
@ -53,8 +53,9 @@ const (
|
|||||||
goodRedirectURI = "http://127.0.0.1/callback"
|
goodRedirectURI = "http://127.0.0.1/callback"
|
||||||
goodPKCECodeVerifier = "some-pkce-verifier-that-must-be-at-least-43-characters-to-meet-entropy-requirements"
|
goodPKCECodeVerifier = "some-pkce-verifier-that-must-be-at-least-43-characters-to-meet-entropy-requirements"
|
||||||
goodNonce = "some-nonce-value-with-enough-bytes-to-exceed-min-allowed"
|
goodNonce = "some-nonce-value-with-enough-bytes-to-exceed-min-allowed"
|
||||||
goodSubject = "some-subject"
|
goodSubject = "https://issuer?sub=some-subject"
|
||||||
goodUsername = "some-username"
|
goodUsername = "some-username"
|
||||||
|
goodGroups = "group1,groups2"
|
||||||
|
|
||||||
hmacSecret = "this needs to be at least 32 characters to meet entropy requirements"
|
hmacSecret = "this needs to be at least 32 characters to meet entropy requirements"
|
||||||
|
|
||||||
@ -785,11 +786,13 @@ func TestTokenExchange(t *testing.T) {
|
|||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
test := test
|
test := test
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
subject, rsp, _, _, secrets, storage := exchangeAuthcodeForTokens(t, test.authcodeExchange)
|
t.Parallel()
|
||||||
var parsedResponseBody map[string]interface{}
|
|
||||||
require.NoError(t, json.Unmarshal(rsp.Body.Bytes(), &parsedResponseBody))
|
|
||||||
|
|
||||||
request := happyTokenExchangeRequest(test.requestedAudience, parsedResponseBody["access_token"].(string))
|
subject, rsp, _, _, secrets, storage := exchangeAuthcodeForTokens(t, test.authcodeExchange)
|
||||||
|
var parsedAuthcodeExchangeResponseBody map[string]interface{}
|
||||||
|
require.NoError(t, json.Unmarshal(rsp.Body.Bytes(), &parsedAuthcodeExchangeResponseBody))
|
||||||
|
|
||||||
|
request := happyTokenExchangeRequest(test.requestedAudience, parsedAuthcodeExchangeResponseBody["access_token"].(string))
|
||||||
if test.modifyStorage != nil {
|
if test.modifyStorage != nil {
|
||||||
test.modifyStorage(t, storage, request)
|
test.modifyStorage(t, storage, request)
|
||||||
}
|
}
|
||||||
@ -805,6 +808,10 @@ func TestTokenExchange(t *testing.T) {
|
|||||||
existingSecrets, err := secrets.List(context.Background(), metav1.ListOptions{})
|
existingSecrets, err := secrets.List(context.Background(), metav1.ListOptions{})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Wait one second before performing the token exchange so we can see that the new ID token has new issued
|
||||||
|
// at and expires at dates which are newer than the old tokens.
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
|
||||||
subject.ServeHTTP(rsp, req)
|
subject.ServeHTTP(rsp, req)
|
||||||
t.Logf("response: %#v", rsp)
|
t.Logf("response: %#v", rsp)
|
||||||
t.Logf("response body: %q", rsp.Body.String())
|
t.Logf("response body: %q", rsp.Body.String())
|
||||||
@ -820,6 +827,12 @@ func TestTokenExchange(t *testing.T) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
claimsOfFirstIDToken := map[string]interface{}{}
|
||||||
|
originalIDToken := parsedAuthcodeExchangeResponseBody["id_token"].(string)
|
||||||
|
firstIDTokenDecoded, _ := josejwt.ParseSigned(originalIDToken)
|
||||||
|
err = firstIDTokenDecoded.UnsafeClaimsWithoutVerification(&claimsOfFirstIDToken)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
var responseBody map[string]interface{}
|
var responseBody map[string]interface{}
|
||||||
require.NoError(t, json.Unmarshal(rsp.Body.Bytes(), &responseBody))
|
require.NoError(t, json.Unmarshal(rsp.Body.Bytes(), &responseBody))
|
||||||
|
|
||||||
@ -827,18 +840,43 @@ func TestTokenExchange(t *testing.T) {
|
|||||||
require.Equal(t, "N_A", responseBody["token_type"])
|
require.Equal(t, "N_A", responseBody["token_type"])
|
||||||
require.Equal(t, "urn:ietf:params:oauth:token-type:jwt", responseBody["issued_token_type"])
|
require.Equal(t, "urn:ietf:params:oauth:token-type:jwt", responseBody["issued_token_type"])
|
||||||
|
|
||||||
// Assert that the returned token has expected claims.
|
// Parse the returned token.
|
||||||
parsedJWT, err := jose.ParseSigned(responseBody["access_token"].(string))
|
parsedJWT, err := jose.ParseSigned(responseBody["access_token"].(string))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
var tokenClaims map[string]interface{}
|
var tokenClaims map[string]interface{}
|
||||||
require.NoError(t, json.Unmarshal(parsedJWT.UnsafePayloadWithoutVerification(), &tokenClaims))
|
require.NoError(t, json.Unmarshal(parsedJWT.UnsafePayloadWithoutVerification(), &tokenClaims))
|
||||||
require.Contains(t, tokenClaims, "iat")
|
|
||||||
require.Contains(t, tokenClaims, "rat")
|
// Make sure that these are the only fields in the token.
|
||||||
require.Contains(t, tokenClaims, "jti")
|
idTokenFields := []string{"sub", "aud", "iss", "jti", "nonce", "auth_time", "exp", "iat", "rat", "groups", "username"}
|
||||||
|
require.ElementsMatch(t, idTokenFields, getMapKeys(tokenClaims))
|
||||||
|
|
||||||
|
// Assert that the returned token has expected claims values.
|
||||||
|
require.NotEmpty(t, tokenClaims["jti"])
|
||||||
|
require.NotEmpty(t, tokenClaims["auth_time"])
|
||||||
|
require.NotEmpty(t, tokenClaims["exp"])
|
||||||
|
require.NotEmpty(t, tokenClaims["iat"])
|
||||||
|
require.NotEmpty(t, tokenClaims["rat"])
|
||||||
|
require.Empty(t, tokenClaims["nonce"]) // ID tokens only contain nonce during an authcode exchange
|
||||||
require.Len(t, tokenClaims["aud"], 1)
|
require.Len(t, tokenClaims["aud"], 1)
|
||||||
require.Contains(t, tokenClaims["aud"], test.requestedAudience)
|
require.Contains(t, tokenClaims["aud"], test.requestedAudience)
|
||||||
require.Equal(t, goodSubject, tokenClaims["sub"])
|
require.Equal(t, goodSubject, tokenClaims["sub"])
|
||||||
require.Equal(t, goodIssuer, tokenClaims["iss"])
|
require.Equal(t, goodIssuer, tokenClaims["iss"])
|
||||||
|
require.Equal(t, goodUsername, tokenClaims["username"])
|
||||||
|
require.Equal(t, goodGroups, tokenClaims["groups"])
|
||||||
|
|
||||||
|
// Also assert that some are the same as the original downstream ID token.
|
||||||
|
requireClaimsAreEqual(t, "iss", claimsOfFirstIDToken, tokenClaims) // issuer
|
||||||
|
requireClaimsAreEqual(t, "sub", claimsOfFirstIDToken, tokenClaims) // subject
|
||||||
|
requireClaimsAreEqual(t, "rat", claimsOfFirstIDToken, tokenClaims) // requested at
|
||||||
|
requireClaimsAreEqual(t, "auth_time", claimsOfFirstIDToken, tokenClaims) // auth time
|
||||||
|
|
||||||
|
// 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"])
|
||||||
|
|
||||||
// Assert that nothing in storage has been modified.
|
// Assert that nothing in storage has been modified.
|
||||||
newSecrets, err := secrets.List(context.Background(), metav1.ListOptions{})
|
newSecrets, err := secrets.List(context.Background(), metav1.ListOptions{})
|
||||||
@ -1390,11 +1428,15 @@ func simulateAuthEndpointHavingAlreadyRun(t *testing.T, authRequest *http.Reques
|
|||||||
session := &openid.DefaultSession{
|
session := &openid.DefaultSession{
|
||||||
Claims: &jwt.IDTokenClaims{
|
Claims: &jwt.IDTokenClaims{
|
||||||
Subject: goodSubject,
|
Subject: goodSubject,
|
||||||
AuthTime: goodAuthTime,
|
|
||||||
RequestedAt: goodRequestedAtTime,
|
RequestedAt: goodRequestedAtTime,
|
||||||
|
AuthTime: goodAuthTime,
|
||||||
|
Extra: map[string]interface{}{
|
||||||
|
oidc.DownstreamUsernameClaim: goodUsername,
|
||||||
|
oidc.DownstreamGroupsClaim: goodGroups,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Subject: goodSubject,
|
Subject: "", // not used, note that callback_handler.go does not set this
|
||||||
Username: goodUsername,
|
Username: "", // not used, note that callback_handler.go does not set this
|
||||||
}
|
}
|
||||||
authRequester, err := oauthHelper.NewAuthorizeRequest(ctx, authRequest)
|
authRequester, err := oauthHelper.NewAuthorizeRequest(ctx, authRequest)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -1613,6 +1655,12 @@ func requireValidStoredRequest(
|
|||||||
require.Empty(t, claims.JTI) // When claims.JTI is empty, Fosite will generate a UUID for this field.
|
require.Empty(t, claims.JTI) // When claims.JTI is empty, Fosite will generate a UUID for this field.
|
||||||
require.Equal(t, goodSubject, claims.Subject)
|
require.Equal(t, goodSubject, claims.Subject)
|
||||||
|
|
||||||
|
// Our custom claims from the authorize endpoint should still be set.
|
||||||
|
require.Equal(t, map[string]interface{}{
|
||||||
|
"username": goodUsername,
|
||||||
|
"groups": goodGroups,
|
||||||
|
}, claims.Extra)
|
||||||
|
|
||||||
// We are in charge of setting these fields. For the purpose of testing, we ensure that the
|
// We are in charge of setting these fields. For the purpose of testing, we ensure that the
|
||||||
// sentinel test value is set correctly.
|
// sentinel test value is set correctly.
|
||||||
require.Equal(t, goodRequestedAtTime, claims.RequestedAt)
|
require.Equal(t, goodRequestedAtTime, claims.RequestedAt)
|
||||||
@ -1637,7 +1685,6 @@ func requireValidStoredRequest(
|
|||||||
require.Empty(t, claims.AuthenticationContextClassReference)
|
require.Empty(t, claims.AuthenticationContextClassReference)
|
||||||
require.Empty(t, claims.AuthenticationMethodsReference)
|
require.Empty(t, claims.AuthenticationMethodsReference)
|
||||||
require.Empty(t, claims.CodeHash)
|
require.Empty(t, claims.CodeHash)
|
||||||
require.Empty(t, claims.Extra)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assert that the session headers are what we think they should be.
|
// Assert that the session headers are what we think they should be.
|
||||||
@ -1668,9 +1715,9 @@ func requireValidStoredRequest(
|
|||||||
require.False(t, ok, "expected session to not hold expiration time for access token, but it did")
|
require.False(t, ok, "expected session to not hold expiration time for access token, but it did")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assert that the session's username and subject are correct.
|
// We don't use these, so they should be empty.
|
||||||
require.Equal(t, goodUsername, session.Username)
|
require.Empty(t, session.Username)
|
||||||
require.Equal(t, goodSubject, session.Subject)
|
require.Empty(t, session.Subject)
|
||||||
}
|
}
|
||||||
|
|
||||||
func requireValidIDToken(
|
func requireValidIDToken(
|
||||||
@ -1702,12 +1749,14 @@ func requireValidIDToken(
|
|||||||
IssuedAt int64 `json:"iat"`
|
IssuedAt int64 `json:"iat"`
|
||||||
RequestedAt int64 `json:"rat"`
|
RequestedAt int64 `json:"rat"`
|
||||||
AuthTime int64 `json:"auth_time"`
|
AuthTime int64 `json:"auth_time"`
|
||||||
|
Groups string `json:"groups"`
|
||||||
|
Username string `json:"username"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note that there is a bug in fosite which prevents the `at_hash` claim from appearing in this ID token
|
// Note that there is a bug in fosite which prevents the `at_hash` claim from appearing in this ID token
|
||||||
// during the initial authcode exchange, but does not prevent `at_hash` from appearing in the refreshed ID token.
|
// during the initial authcode exchange, but does not prevent `at_hash` from appearing in the refreshed ID token.
|
||||||
// We can add a workaround for this later.
|
// We can add a workaround for this later.
|
||||||
idTokenFields := []string{"sub", "aud", "iss", "jti", "nonce", "auth_time", "exp", "iat", "rat"}
|
idTokenFields := []string{"sub", "aud", "iss", "jti", "nonce", "auth_time", "exp", "iat", "rat", "groups", "username"}
|
||||||
if wantAtHashClaimInIDToken {
|
if wantAtHashClaimInIDToken {
|
||||||
idTokenFields = append(idTokenFields, "at_hash")
|
idTokenFields = append(idTokenFields, "at_hash")
|
||||||
}
|
}
|
||||||
@ -1721,6 +1770,8 @@ func requireValidIDToken(
|
|||||||
err := token.Claims(&claims)
|
err := token.Claims(&claims)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, goodSubject, claims.Subject)
|
require.Equal(t, goodSubject, claims.Subject)
|
||||||
|
require.Equal(t, goodUsername, claims.Username)
|
||||||
|
require.Equal(t, goodGroups, claims.Groups)
|
||||||
require.Len(t, claims.Audience, 1)
|
require.Len(t, claims.Audience, 1)
|
||||||
require.Equal(t, goodClient, claims.Audience[0])
|
require.Equal(t, goodClient, claims.Audience[0])
|
||||||
require.Equal(t, goodIssuer, claims.Issuer)
|
require.Equal(t, goodIssuer, claims.Issuer)
|
||||||
|
@ -65,11 +65,13 @@ func TestSuccessfulCredentialRequest(t *testing.T) {
|
|||||||
credOutput, _ := runPinnipedLoginOIDC(ctx, t, pinnipedExe)
|
credOutput, _ := runPinnipedLoginOIDC(ctx, t, pinnipedExe)
|
||||||
token := credOutput.Status.Token
|
token := credOutput.Status.Token
|
||||||
|
|
||||||
// By default, the JWTAuthenticator expects the username to be in the "sub" claim and the
|
// By default, the JWTAuthenticator expects the username to be in the "username" claim and the
|
||||||
// groups to be in the "groups" claim.
|
// groups to be in the "groups" claim.
|
||||||
|
// However, we are configuring Pinniped in the `CreateTestJWTAuthenticatorForCLIUpstream` method above
|
||||||
|
// to read the username from the "sub" claim of the token instead.
|
||||||
username, groups := getJWTSubAndGroupsClaims(t, token)
|
username, groups := getJWTSubAndGroupsClaims(t, token)
|
||||||
|
|
||||||
return credOutput.Status.Token, username, groups
|
return token, username, groups
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -183,7 +183,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
tokenResponse, err := downstreamOAuth2Config.Exchange(oidcHTTPClientContext, authcode, pkceParam.Verifier())
|
tokenResponse, err := downstreamOAuth2Config.Exchange(oidcHTTPClientContext, authcode, pkceParam.Verifier())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
expectedIDTokenClaims := []string{"iss", "exp", "sub", "aud", "auth_time", "iat", "jti", "nonce", "rat"}
|
expectedIDTokenClaims := []string{"iss", "exp", "sub", "aud", "auth_time", "iat", "jti", "nonce", "rat", "username"}
|
||||||
verifyTokenResponse(t, tokenResponse, discovery, downstreamOAuth2Config, env.SupervisorTestUpstream.Issuer, nonceParam, expectedIDTokenClaims)
|
verifyTokenResponse(t, tokenResponse, discovery, downstreamOAuth2Config, env.SupervisorTestUpstream.Issuer, nonceParam, expectedIDTokenClaims)
|
||||||
|
|
||||||
// token exchange on the original token
|
// token exchange on the original token
|
||||||
@ -240,6 +240,10 @@ func verifyTokenResponse(
|
|||||||
idTokenClaimNames = append(idTokenClaimNames, k)
|
idTokenClaimNames = append(idTokenClaimNames, k)
|
||||||
}
|
}
|
||||||
require.ElementsMatch(t, expectedIDTokenClaims, idTokenClaimNames)
|
require.ElementsMatch(t, expectedIDTokenClaims, idTokenClaimNames)
|
||||||
|
expectedUsernamePrefix := upstreamIssuerName + "?sub="
|
||||||
|
require.True(t, strings.HasPrefix(idTokenClaims["username"].(string), expectedUsernamePrefix))
|
||||||
|
require.Greater(t, len(idTokenClaims["username"].(string)), len(expectedUsernamePrefix),
|
||||||
|
"the ID token Username should include the upstream user ID after the upstream issuer name")
|
||||||
|
|
||||||
// Some light verification of the other tokens that were returned.
|
// Some light verification of the other tokens that were returned.
|
||||||
require.NotEmpty(t, tokenResponse.AccessToken)
|
require.NotEmpty(t, tokenResponse.AccessToken)
|
||||||
|
@ -180,6 +180,9 @@ func CreateTestJWTAuthenticatorForCLIUpstream(ctx context.Context, t *testing.T)
|
|||||||
spec := auth1alpha1.JWTAuthenticatorSpec{
|
spec := auth1alpha1.JWTAuthenticatorSpec{
|
||||||
Issuer: testEnv.CLITestUpstream.Issuer,
|
Issuer: testEnv.CLITestUpstream.Issuer,
|
||||||
Audience: testEnv.CLITestUpstream.ClientID,
|
Audience: testEnv.CLITestUpstream.ClientID,
|
||||||
|
// The default UsernameClaim is "username" but the upstreams that we use for
|
||||||
|
// integration tests won't necessarily have that claim, so use "sub" here.
|
||||||
|
Claims: auth1alpha1.JWTTokenClaims{Username: "sub"},
|
||||||
}
|
}
|
||||||
// If the test upstream does not have a CA bundle specified, then don't configure one in the
|
// If the test upstream does not have a CA bundle specified, then don't configure one in the
|
||||||
// JWTAuthenticator. Leaving TLSSpec set to nil will result in OIDC discovery using the OS's root
|
// JWTAuthenticator. Leaving TLSSpec set to nil will result in OIDC discovery using the OS's root
|
||||||
|
Loading…
Reference in New Issue
Block a user