mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-14 22:33:34 +00:00
Merge pull request #123527 from aramase/aramase/f/kep_3331_discovery_url
Add `DiscoveryURL` to Authentication Configuration
This commit is contained in:
commit
ee5eca2a49
@ -175,9 +175,43 @@ type JWTAuthenticator struct {
|
|||||||
UserValidationRules []UserValidationRule
|
UserValidationRules []UserValidationRule
|
||||||
}
|
}
|
||||||
|
|
||||||
// Issuer provides the configuration for a external provider specific settings.
|
// Issuer provides the configuration for an external provider's specific settings.
|
||||||
type Issuer struct {
|
type Issuer struct {
|
||||||
|
// url points to the issuer URL in a format https://url or https://url/path.
|
||||||
|
// This must match the "iss" claim in the presented JWT, and the issuer returned from discovery.
|
||||||
|
// Same value as the --oidc-issuer-url flag.
|
||||||
|
// Discovery information is fetched from "{url}/.well-known/openid-configuration" unless overridden by discoveryURL.
|
||||||
|
// Required to be unique across all JWT authenticators.
|
||||||
|
// Note that egress selection configuration is not used for this network connection.
|
||||||
|
// +required
|
||||||
URL string
|
URL string
|
||||||
|
// discoveryURL, if specified, overrides the URL used to fetch discovery
|
||||||
|
// information instead of using "{url}/.well-known/openid-configuration".
|
||||||
|
// The exact value specified is used, so "/.well-known/openid-configuration"
|
||||||
|
// must be included in discoveryURL if needed.
|
||||||
|
//
|
||||||
|
// The "issuer" field in the fetched discovery information must match the "issuer.url" field
|
||||||
|
// in the AuthenticationConfiguration and will be used to validate the "iss" claim in the presented JWT.
|
||||||
|
// This is for scenarios where the well-known and jwks endpoints are hosted at a different
|
||||||
|
// location than the issuer (such as locally in the cluster).
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
// A discovery url that is exposed using kubernetes service 'oidc' in namespace 'oidc-namespace'
|
||||||
|
// and discovery information is available at '/.well-known/openid-configuration'.
|
||||||
|
// discoveryURL: "https://oidc.oidc-namespace/.well-known/openid-configuration"
|
||||||
|
// certificateAuthority is used to verify the TLS connection and the hostname on the leaf certificate
|
||||||
|
// must be set to 'oidc.oidc-namespace'.
|
||||||
|
//
|
||||||
|
// curl https://oidc.oidc-namespace/.well-known/openid-configuration (.discoveryURL field)
|
||||||
|
// {
|
||||||
|
// issuer: "https://oidc.example.com" (.url field)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// discoveryURL must be different from url.
|
||||||
|
// Required to be unique across all JWT authenticators.
|
||||||
|
// Note that egress selection configuration is not used for this network connection.
|
||||||
|
// +optional
|
||||||
|
DiscoveryURL string
|
||||||
CertificateAuthority string
|
CertificateAuthority string
|
||||||
Audiences []string
|
Audiences []string
|
||||||
AudienceMatchPolicy AudienceMatchPolicyType
|
AudienceMatchPolicy AudienceMatchPolicyType
|
||||||
|
@ -209,17 +209,45 @@ type JWTAuthenticator struct {
|
|||||||
UserValidationRules []UserValidationRule `json:"userValidationRules,omitempty"`
|
UserValidationRules []UserValidationRule `json:"userValidationRules,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Issuer provides the configuration for a external provider specific settings.
|
// Issuer provides the configuration for an external provider's specific settings.
|
||||||
type Issuer struct {
|
type Issuer struct {
|
||||||
// url points to the issuer URL in a format https://url or https://url/path.
|
// url points to the issuer URL in a format https://url or https://url/path.
|
||||||
// This must match the "iss" claim in the presented JWT, and the issuer returned from discovery.
|
// This must match the "iss" claim in the presented JWT, and the issuer returned from discovery.
|
||||||
// Same value as the --oidc-issuer-url flag.
|
// Same value as the --oidc-issuer-url flag.
|
||||||
// Used to fetch discovery information unless overridden by discoveryURL.
|
// Discovery information is fetched from "{url}/.well-known/openid-configuration" unless overridden by discoveryURL.
|
||||||
// Required to be unique.
|
// Required to be unique across all JWT authenticators.
|
||||||
// Note that egress selection configuration is not used for this network connection.
|
// Note that egress selection configuration is not used for this network connection.
|
||||||
// +required
|
// +required
|
||||||
URL string `json:"url"`
|
URL string `json:"url"`
|
||||||
|
|
||||||
|
// discoveryURL, if specified, overrides the URL used to fetch discovery
|
||||||
|
// information instead of using "{url}/.well-known/openid-configuration".
|
||||||
|
// The exact value specified is used, so "/.well-known/openid-configuration"
|
||||||
|
// must be included in discoveryURL if needed.
|
||||||
|
//
|
||||||
|
// The "issuer" field in the fetched discovery information must match the "issuer.url" field
|
||||||
|
// in the AuthenticationConfiguration and will be used to validate the "iss" claim in the presented JWT.
|
||||||
|
// This is for scenarios where the well-known and jwks endpoints are hosted at a different
|
||||||
|
// location than the issuer (such as locally in the cluster).
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
// A discovery url that is exposed using kubernetes service 'oidc' in namespace 'oidc-namespace'
|
||||||
|
// and discovery information is available at '/.well-known/openid-configuration'.
|
||||||
|
// discoveryURL: "https://oidc.oidc-namespace/.well-known/openid-configuration"
|
||||||
|
// certificateAuthority is used to verify the TLS connection and the hostname on the leaf certificate
|
||||||
|
// must be set to 'oidc.oidc-namespace'.
|
||||||
|
//
|
||||||
|
// curl https://oidc.oidc-namespace/.well-known/openid-configuration (.discoveryURL field)
|
||||||
|
// {
|
||||||
|
// issuer: "https://oidc.example.com" (.url field)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// discoveryURL must be different from url.
|
||||||
|
// Required to be unique across all JWT authenticators.
|
||||||
|
// Note that egress selection configuration is not used for this network connection.
|
||||||
|
// +optional
|
||||||
|
DiscoveryURL *string `json:"discoveryURL,omitempty"`
|
||||||
|
|
||||||
// certificateAuthority contains PEM-encoded certificate authority certificates
|
// certificateAuthority contains PEM-encoded certificate authority certificates
|
||||||
// used to validate the connection when fetching discovery information.
|
// used to validate the connection when fetching discovery information.
|
||||||
// If unset, the system verifier is used.
|
// If unset, the system verifier is used.
|
||||||
|
@ -24,6 +24,7 @@ package v1alpha1
|
|||||||
import (
|
import (
|
||||||
unsafe "unsafe"
|
unsafe "unsafe"
|
||||||
|
|
||||||
|
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
conversion "k8s.io/apimachinery/pkg/conversion"
|
conversion "k8s.io/apimachinery/pkg/conversion"
|
||||||
runtime "k8s.io/apimachinery/pkg/runtime"
|
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||||
apiserver "k8s.io/apiserver/pkg/apis/apiserver"
|
apiserver "k8s.io/apiserver/pkg/apis/apiserver"
|
||||||
@ -324,7 +325,17 @@ func Convert_apiserver_AdmissionPluginConfiguration_To_v1alpha1_AdmissionPluginC
|
|||||||
}
|
}
|
||||||
|
|
||||||
func autoConvert_v1alpha1_AuthenticationConfiguration_To_apiserver_AuthenticationConfiguration(in *AuthenticationConfiguration, out *apiserver.AuthenticationConfiguration, s conversion.Scope) error {
|
func autoConvert_v1alpha1_AuthenticationConfiguration_To_apiserver_AuthenticationConfiguration(in *AuthenticationConfiguration, out *apiserver.AuthenticationConfiguration, s conversion.Scope) error {
|
||||||
out.JWT = *(*[]apiserver.JWTAuthenticator)(unsafe.Pointer(&in.JWT))
|
if in.JWT != nil {
|
||||||
|
in, out := &in.JWT, &out.JWT
|
||||||
|
*out = make([]apiserver.JWTAuthenticator, len(*in))
|
||||||
|
for i := range *in {
|
||||||
|
if err := Convert_v1alpha1_JWTAuthenticator_To_apiserver_JWTAuthenticator(&(*in)[i], &(*out)[i], s); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
out.JWT = nil
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -334,7 +345,17 @@ func Convert_v1alpha1_AuthenticationConfiguration_To_apiserver_AuthenticationCon
|
|||||||
}
|
}
|
||||||
|
|
||||||
func autoConvert_apiserver_AuthenticationConfiguration_To_v1alpha1_AuthenticationConfiguration(in *apiserver.AuthenticationConfiguration, out *AuthenticationConfiguration, s conversion.Scope) error {
|
func autoConvert_apiserver_AuthenticationConfiguration_To_v1alpha1_AuthenticationConfiguration(in *apiserver.AuthenticationConfiguration, out *AuthenticationConfiguration, s conversion.Scope) error {
|
||||||
out.JWT = *(*[]JWTAuthenticator)(unsafe.Pointer(&in.JWT))
|
if in.JWT != nil {
|
||||||
|
in, out := &in.JWT, &out.JWT
|
||||||
|
*out = make([]JWTAuthenticator, len(*in))
|
||||||
|
for i := range *in {
|
||||||
|
if err := Convert_apiserver_JWTAuthenticator_To_v1alpha1_JWTAuthenticator(&(*in)[i], &(*out)[i], s); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
out.JWT = nil
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -580,6 +601,9 @@ func Convert_apiserver_ExtraMapping_To_v1alpha1_ExtraMapping(in *apiserver.Extra
|
|||||||
|
|
||||||
func autoConvert_v1alpha1_Issuer_To_apiserver_Issuer(in *Issuer, out *apiserver.Issuer, s conversion.Scope) error {
|
func autoConvert_v1alpha1_Issuer_To_apiserver_Issuer(in *Issuer, out *apiserver.Issuer, s conversion.Scope) error {
|
||||||
out.URL = in.URL
|
out.URL = in.URL
|
||||||
|
if err := v1.Convert_Pointer_string_To_string(&in.DiscoveryURL, &out.DiscoveryURL, s); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
out.CertificateAuthority = in.CertificateAuthority
|
out.CertificateAuthority = in.CertificateAuthority
|
||||||
out.Audiences = *(*[]string)(unsafe.Pointer(&in.Audiences))
|
out.Audiences = *(*[]string)(unsafe.Pointer(&in.Audiences))
|
||||||
out.AudienceMatchPolicy = apiserver.AudienceMatchPolicyType(in.AudienceMatchPolicy)
|
out.AudienceMatchPolicy = apiserver.AudienceMatchPolicyType(in.AudienceMatchPolicy)
|
||||||
@ -593,6 +617,9 @@ func Convert_v1alpha1_Issuer_To_apiserver_Issuer(in *Issuer, out *apiserver.Issu
|
|||||||
|
|
||||||
func autoConvert_apiserver_Issuer_To_v1alpha1_Issuer(in *apiserver.Issuer, out *Issuer, s conversion.Scope) error {
|
func autoConvert_apiserver_Issuer_To_v1alpha1_Issuer(in *apiserver.Issuer, out *Issuer, s conversion.Scope) error {
|
||||||
out.URL = in.URL
|
out.URL = in.URL
|
||||||
|
if err := v1.Convert_string_To_Pointer_string(&in.DiscoveryURL, &out.DiscoveryURL, s); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
out.CertificateAuthority = in.CertificateAuthority
|
out.CertificateAuthority = in.CertificateAuthority
|
||||||
out.Audiences = *(*[]string)(unsafe.Pointer(&in.Audiences))
|
out.Audiences = *(*[]string)(unsafe.Pointer(&in.Audiences))
|
||||||
out.AudienceMatchPolicy = AudienceMatchPolicyType(in.AudienceMatchPolicy)
|
out.AudienceMatchPolicy = AudienceMatchPolicyType(in.AudienceMatchPolicy)
|
||||||
|
@ -308,6 +308,11 @@ func (in *ExtraMapping) DeepCopy() *ExtraMapping {
|
|||||||
// 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 *Issuer) DeepCopyInto(out *Issuer) {
|
func (in *Issuer) DeepCopyInto(out *Issuer) {
|
||||||
*out = *in
|
*out = *in
|
||||||
|
if in.DiscoveryURL != nil {
|
||||||
|
in, out := &in.DiscoveryURL, &out.DiscoveryURL
|
||||||
|
*out = new(string)
|
||||||
|
**out = **in
|
||||||
|
}
|
||||||
if in.Audiences != nil {
|
if in.Audiences != nil {
|
||||||
in, out := &in.Audiences, &out.Audiences
|
in, out := &in.Audiences, &out.Audiences
|
||||||
*out = make([]string, len(*in))
|
*out = make([]string, len(*in))
|
||||||
|
@ -100,21 +100,40 @@ func validateJWTAuthenticator(authenticator api.JWTAuthenticator, fldPath *field
|
|||||||
func validateIssuer(issuer api.Issuer, fldPath *field.Path) field.ErrorList {
|
func validateIssuer(issuer api.Issuer, fldPath *field.Path) field.ErrorList {
|
||||||
var allErrs field.ErrorList
|
var allErrs field.ErrorList
|
||||||
|
|
||||||
allErrs = append(allErrs, validateURL(issuer.URL, fldPath.Child("url"))...)
|
allErrs = append(allErrs, validateIssuerURL(issuer.URL, fldPath.Child("url"))...)
|
||||||
|
allErrs = append(allErrs, validateIssuerDiscoveryURL(issuer.URL, issuer.DiscoveryURL, fldPath.Child("discoveryURL"))...)
|
||||||
allErrs = append(allErrs, validateAudiences(issuer.Audiences, issuer.AudienceMatchPolicy, fldPath.Child("audiences"), fldPath.Child("audienceMatchPolicy"))...)
|
allErrs = append(allErrs, validateAudiences(issuer.Audiences, issuer.AudienceMatchPolicy, fldPath.Child("audiences"), fldPath.Child("audienceMatchPolicy"))...)
|
||||||
allErrs = append(allErrs, validateCertificateAuthority(issuer.CertificateAuthority, fldPath.Child("certificateAuthority"))...)
|
allErrs = append(allErrs, validateCertificateAuthority(issuer.CertificateAuthority, fldPath.Child("certificateAuthority"))...)
|
||||||
|
|
||||||
return allErrs
|
return allErrs
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateURL(issuerURL string, fldPath *field.Path) field.ErrorList {
|
func validateIssuerURL(issuerURL string, fldPath *field.Path) field.ErrorList {
|
||||||
|
if len(issuerURL) == 0 {
|
||||||
|
return field.ErrorList{field.Required(fldPath, "URL is required")}
|
||||||
|
}
|
||||||
|
|
||||||
|
return validateURL(issuerURL, fldPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateIssuerDiscoveryURL(issuerURL, issuerDiscoveryURL string, fldPath *field.Path) field.ErrorList {
|
||||||
var allErrs field.ErrorList
|
var allErrs field.ErrorList
|
||||||
|
|
||||||
if len(issuerURL) == 0 {
|
if len(issuerDiscoveryURL) == 0 {
|
||||||
allErrs = append(allErrs, field.Required(fldPath, "URL is required"))
|
return nil
|
||||||
return allErrs
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(issuerURL) > 0 && strings.TrimRight(issuerURL, "/") == strings.TrimRight(issuerDiscoveryURL, "/") {
|
||||||
|
allErrs = append(allErrs, field.Invalid(fldPath, issuerDiscoveryURL, "discoveryURL must be different from URL"))
|
||||||
|
}
|
||||||
|
|
||||||
|
allErrs = append(allErrs, validateURL(issuerDiscoveryURL, fldPath)...)
|
||||||
|
return allErrs
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateURL(issuerURL string, fldPath *field.Path) field.ErrorList {
|
||||||
|
var allErrs field.ErrorList
|
||||||
|
|
||||||
u, err := url.Parse(issuerURL)
|
u, err := url.Parse(issuerURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
allErrs = append(allErrs, field.Invalid(fldPath, issuerURL, err.Error()))
|
allErrs = append(allErrs, field.Invalid(fldPath, issuerURL, err.Error()))
|
||||||
|
@ -212,7 +212,7 @@ func TestValidateAuthenticationConfiguration(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestValidateURL(t *testing.T) {
|
func TestValidateIssuerURL(t *testing.T) {
|
||||||
fldPath := field.NewPath("issuer", "url")
|
fldPath := field.NewPath("issuer", "url")
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
@ -259,7 +259,92 @@ func TestValidateURL(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range testCases {
|
for _, tt := range testCases {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
got := validateURL(tt.in, fldPath).ToAggregate()
|
got := validateIssuerURL(tt.in, fldPath).ToAggregate()
|
||||||
|
if d := cmp.Diff(tt.want, errString(got)); d != "" {
|
||||||
|
t.Fatalf("URL validation mismatch (-want +got):\n%s", d)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateIssuerDiscoveryURL(t *testing.T) {
|
||||||
|
fldPath := field.NewPath("issuer", "discoveryURL")
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
in string
|
||||||
|
issuerURL string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "url is empty",
|
||||||
|
in: "",
|
||||||
|
want: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "url parse error",
|
||||||
|
in: "https://oidc.oidc-namespace.svc:invalid-port",
|
||||||
|
want: `issuer.discoveryURL: Invalid value: "https://oidc.oidc-namespace.svc:invalid-port": parse "https://oidc.oidc-namespace.svc:invalid-port": invalid port ":invalid-port" after host`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "url is not https",
|
||||||
|
in: "http://oidc.oidc-namespace.svc",
|
||||||
|
want: `issuer.discoveryURL: Invalid value: "http://oidc.oidc-namespace.svc": URL scheme must be https`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "url user info is not allowed",
|
||||||
|
in: "https://user:pass@oidc.oidc-namespace.svc",
|
||||||
|
want: `issuer.discoveryURL: Invalid value: "https://user:pass@oidc.oidc-namespace.svc": URL must not contain a username or password`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "url raw query is not allowed",
|
||||||
|
in: "https://oidc.oidc-namespace.svc?query",
|
||||||
|
want: `issuer.discoveryURL: Invalid value: "https://oidc.oidc-namespace.svc?query": URL must not contain a query`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "url fragment is not allowed",
|
||||||
|
in: "https://oidc.oidc-namespace.svc#fragment",
|
||||||
|
want: `issuer.discoveryURL: Invalid value: "https://oidc.oidc-namespace.svc#fragment": URL must not contain a fragment`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid url",
|
||||||
|
in: "https://oidc.oidc-namespace.svc",
|
||||||
|
want: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid url with path",
|
||||||
|
in: "https://oidc.oidc-namespace.svc/path",
|
||||||
|
want: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "discovery url same as issuer url",
|
||||||
|
issuerURL: "https://issuer-url",
|
||||||
|
in: "https://issuer-url",
|
||||||
|
want: `issuer.discoveryURL: Invalid value: "https://issuer-url": discoveryURL must be different from URL`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "discovery url same as issuer url, with trailing slash",
|
||||||
|
issuerURL: "https://issuer-url",
|
||||||
|
in: "https://issuer-url/",
|
||||||
|
want: `issuer.discoveryURL: Invalid value: "https://issuer-url/": discoveryURL must be different from URL`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "discovery url same as issuer url, with multiple trailing slashes",
|
||||||
|
issuerURL: "https://issuer-url",
|
||||||
|
in: "https://issuer-url///",
|
||||||
|
want: `issuer.discoveryURL: Invalid value: "https://issuer-url///": discoveryURL must be different from URL`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "discovery url same as issuer url, issuer url with trailing slash",
|
||||||
|
issuerURL: "https://issuer-url/",
|
||||||
|
in: "https://issuer-url",
|
||||||
|
want: `issuer.discoveryURL: Invalid value: "https://issuer-url": discoveryURL must be different from URL`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range testCases {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := validateIssuerDiscoveryURL(tt.issuerURL, tt.in, fldPath).ToAggregate()
|
||||||
if d := cmp.Diff(tt.want, errString(got)); d != "" {
|
if d := cmp.Diff(tt.want, errString(got)); d != "" {
|
||||||
t.Fatalf("URL validation mismatch (-want +got):\n%s", d)
|
t.Fatalf("URL validation mismatch (-want +got):\n%s", d)
|
||||||
}
|
}
|
||||||
|
@ -35,6 +35,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@ -66,6 +67,10 @@ var (
|
|||||||
synchronizeTokenIDVerifierForTest = false
|
synchronizeTokenIDVerifierForTest = false
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
wellKnownEndpointPath = "/.well-known/openid-configuration"
|
||||||
|
)
|
||||||
|
|
||||||
type Options struct {
|
type Options struct {
|
||||||
// JWTAuthenticator is the authenticator that will be used to verify the JWT.
|
// JWTAuthenticator is the authenticator that will be used to verify the JWT.
|
||||||
JWTAuthenticator apiserver.JWTAuthenticator
|
JWTAuthenticator apiserver.JWTAuthenticator
|
||||||
@ -268,6 +273,28 @@ func New(opts Options) (authenticator.Token, error) {
|
|||||||
client = &http.Client{Transport: tr, Timeout: 30 * time.Second}
|
client = &http.Client{Transport: tr, Timeout: 30 * time.Second}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the discovery URL is set in authentication configuration, we set up a
|
||||||
|
// roundTripper to rewrite the {url}/.well-known/openid-configuration to
|
||||||
|
// the discovery URL. This is useful for self-hosted providers, for example,
|
||||||
|
// providers that run on top of Kubernetes itself.
|
||||||
|
if len(opts.JWTAuthenticator.Issuer.DiscoveryURL) > 0 {
|
||||||
|
discoveryURL, err := url.Parse(opts.JWTAuthenticator.Issuer.DiscoveryURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("oidc: invalid discovery URL: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
clientWithDiscoveryURL := *client
|
||||||
|
baseTransport := clientWithDiscoveryURL.Transport
|
||||||
|
if baseTransport == nil {
|
||||||
|
baseTransport = http.DefaultTransport
|
||||||
|
}
|
||||||
|
// This matches the url construction in oidc.NewProvider as of go-oidc v2.2.1.
|
||||||
|
// xref: https://github.com/coreos/go-oidc/blob/40cd342c4a2076195294612a834d11df23c1b25a/oidc.go#L114
|
||||||
|
urlToRewrite := strings.TrimSuffix(opts.JWTAuthenticator.Issuer.URL, "/") + wellKnownEndpointPath
|
||||||
|
clientWithDiscoveryURL.Transport = &discoveryURLRoundTripper{baseTransport, discoveryURL, urlToRewrite}
|
||||||
|
client = &clientWithDiscoveryURL
|
||||||
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
ctx = oidc.ClientContext(ctx, client)
|
ctx = oidc.ClientContext(ctx, client)
|
||||||
|
|
||||||
@ -339,6 +366,26 @@ func New(opts Options) (authenticator.Token, error) {
|
|||||||
return newInstrumentedAuthenticator(issuerURL, authenticator), nil
|
return newInstrumentedAuthenticator(issuerURL, authenticator), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// discoveryURLRoundTripper is a http.RoundTripper that rewrites the
|
||||||
|
// {url}/.well-known/openid-configuration to the discovery URL.
|
||||||
|
type discoveryURLRoundTripper struct {
|
||||||
|
base http.RoundTripper
|
||||||
|
// discoveryURL is the URL to use to fetch the openid configuration
|
||||||
|
discoveryURL *url.URL
|
||||||
|
// urlToRewrite is the URL to rewrite to the discovery URL
|
||||||
|
urlToRewrite string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *discoveryURLRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||||
|
if req.Method == http.MethodGet && req.URL.String() == t.urlToRewrite {
|
||||||
|
clone := req.Clone(req.Context())
|
||||||
|
clone.Host = ""
|
||||||
|
clone.URL = t.discoveryURL
|
||||||
|
return t.base.RoundTrip(clone)
|
||||||
|
}
|
||||||
|
return t.base.RoundTrip(req)
|
||||||
|
}
|
||||||
|
|
||||||
// untrustedIssuer extracts an untrusted "iss" claim from the given JWT token,
|
// untrustedIssuer extracts an untrusted "iss" claim from the given JWT token,
|
||||||
// or returns an error if the token can not be parsed. Since the JWT is not
|
// or returns an error if the token can not be parsed. Since the JWT is not
|
||||||
// verified, the returned issuer should not be trusted.
|
// verified, the returned issuer should not be trusted.
|
||||||
|
@ -36,6 +36,7 @@ import (
|
|||||||
|
|
||||||
"gopkg.in/square/go-jose.v2"
|
"gopkg.in/square/go-jose.v2"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/util/wait"
|
||||||
"k8s.io/apiserver/pkg/apis/apiserver"
|
"k8s.io/apiserver/pkg/apis/apiserver"
|
||||||
"k8s.io/apiserver/pkg/authentication/user"
|
"k8s.io/apiserver/pkg/authentication/user"
|
||||||
"k8s.io/apiserver/pkg/features"
|
"k8s.io/apiserver/pkg/features"
|
||||||
@ -146,6 +147,7 @@ type claimsTest struct {
|
|||||||
wantInitErr string
|
wantInitErr string
|
||||||
claimToResponseMap map[string]string
|
claimToResponseMap map[string]string
|
||||||
openIDConfig string
|
openIDConfig string
|
||||||
|
fetchKeysFromRemote bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replace formats the contents of v into the provided template.
|
// Replace formats the contents of v into the provided template.
|
||||||
@ -175,7 +177,8 @@ func newClaimServer(t *testing.T, keys jose.JSONWebKeySet, signer jose.Signer, c
|
|||||||
klog.V(5).Infof("%v: returning: %+v", r.URL, string(keyBytes))
|
klog.V(5).Infof("%v: returning: %+v", r.URL, string(keyBytes))
|
||||||
w.Write(keyBytes)
|
w.Write(keyBytes)
|
||||||
|
|
||||||
case "/.well-known/openid-configuration":
|
// /c/d/bar/.well-known/openid-configuration is used to test issuer url and discovery url with a path
|
||||||
|
case "/.well-known/openid-configuration", "/c/d/bar/.well-known/openid-configuration":
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
klog.V(5).Infof("%v: returning: %+v", r.URL, *openIDConfig)
|
klog.V(5).Infof("%v: returning: %+v", r.URL, *openIDConfig)
|
||||||
w.Write([]byte(*openIDConfig))
|
w.Write([]byte(*openIDConfig))
|
||||||
@ -262,14 +265,17 @@ func (c *claimsTest) run(t *testing.T) {
|
|||||||
c.claims = replace(c.claims, &v)
|
c.claims = replace(c.claims, &v)
|
||||||
c.openIDConfig = replace(c.openIDConfig, &v)
|
c.openIDConfig = replace(c.openIDConfig, &v)
|
||||||
c.options.JWTAuthenticator.Issuer.URL = replace(c.options.JWTAuthenticator.Issuer.URL, &v)
|
c.options.JWTAuthenticator.Issuer.URL = replace(c.options.JWTAuthenticator.Issuer.URL, &v)
|
||||||
|
c.options.JWTAuthenticator.Issuer.DiscoveryURL = replace(c.options.JWTAuthenticator.Issuer.DiscoveryURL, &v)
|
||||||
for claim, response := range c.claimToResponseMap {
|
for claim, response := range c.claimToResponseMap {
|
||||||
c.claimToResponseMap[claim] = replace(response, &v)
|
c.claimToResponseMap[claim] = replace(response, &v)
|
||||||
}
|
}
|
||||||
c.wantErr = replace(c.wantErr, &v)
|
c.wantErr = replace(c.wantErr, &v)
|
||||||
c.wantInitErr = replace(c.wantInitErr, &v)
|
c.wantInitErr = replace(c.wantInitErr, &v)
|
||||||
|
|
||||||
|
if !c.fetchKeysFromRemote {
|
||||||
// Set the verifier to use the public key set instead of reading from a remote.
|
// Set the verifier to use the public key set instead of reading from a remote.
|
||||||
c.options.KeySet = &staticKeySet{keys: c.pubKeys}
|
c.options.KeySet = &staticKeySet{keys: c.pubKeys}
|
||||||
|
}
|
||||||
|
|
||||||
if c.optsFunc != nil {
|
if c.optsFunc != nil {
|
||||||
c.optsFunc(&c.options)
|
c.optsFunc(&c.options)
|
||||||
@ -307,7 +313,27 @@ func (c *claimsTest) run(t *testing.T) {
|
|||||||
t.Fatalf("serialize token: %v", err)
|
t.Fatalf("serialize token: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
got, ok, err := a.AuthenticateToken(testContext(t), token)
|
ia, ok := a.(*instrumentedAuthenticator)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected authenticator to be instrumented")
|
||||||
|
}
|
||||||
|
authenticator, ok := ia.delegate.(*Authenticator)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected delegate to be Authenticator")
|
||||||
|
}
|
||||||
|
ctx := testContext(t)
|
||||||
|
// wait for the authenticator to be initialized
|
||||||
|
err = wait.PollUntilContextCancel(ctx, time.Millisecond, true, func(context.Context) (bool, error) {
|
||||||
|
if v, _ := authenticator.idTokenVerifier(); v == nil {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to initialize the authenticator: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got, ok, err := a.AuthenticateToken(ctx, token)
|
||||||
|
|
||||||
expectErr := len(c.wantErr) > 0
|
expectErr := len(c.wantErr) > 0
|
||||||
|
|
||||||
@ -2986,6 +3012,191 @@ func TestToken(t *testing.T) {
|
|||||||
Name: "jane",
|
Name: "jane",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "discovery-url",
|
||||||
|
options: Options{
|
||||||
|
JWTAuthenticator: apiserver.JWTAuthenticator{
|
||||||
|
Issuer: apiserver.Issuer{
|
||||||
|
URL: "https://auth.example.com",
|
||||||
|
DiscoveryURL: "{{.URL}}/.well-known/openid-configuration",
|
||||||
|
Audiences: []string{"my-client"},
|
||||||
|
},
|
||||||
|
ClaimMappings: apiserver.ClaimMappings{
|
||||||
|
Username: apiserver.PrefixedClaimOrExpression{
|
||||||
|
Claim: "username",
|
||||||
|
Prefix: pointer.String(""),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
now: func() time.Time { return now },
|
||||||
|
},
|
||||||
|
signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||||
|
pubKeys: []*jose.JSONWebKey{
|
||||||
|
loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||||
|
},
|
||||||
|
claims: fmt.Sprintf(`{
|
||||||
|
"iss": "https://auth.example.com",
|
||||||
|
"aud": "my-client",
|
||||||
|
"username": "jane",
|
||||||
|
"exp": %d
|
||||||
|
}`, valid.Unix()),
|
||||||
|
openIDConfig: `{
|
||||||
|
"issuer": "https://auth.example.com",
|
||||||
|
"jwks_uri": "{{.URL}}/.testing/keys"
|
||||||
|
}`,
|
||||||
|
fetchKeysFromRemote: true,
|
||||||
|
want: &user.DefaultInfo{
|
||||||
|
Name: "jane",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "discovery url, issuer has a path",
|
||||||
|
options: Options{
|
||||||
|
JWTAuthenticator: apiserver.JWTAuthenticator{
|
||||||
|
Issuer: apiserver.Issuer{
|
||||||
|
URL: "https://auth.example.com/a/b/foo",
|
||||||
|
DiscoveryURL: "{{.URL}}/.well-known/openid-configuration",
|
||||||
|
Audiences: []string{"my-client"},
|
||||||
|
},
|
||||||
|
ClaimMappings: apiserver.ClaimMappings{
|
||||||
|
Username: apiserver.PrefixedClaimOrExpression{
|
||||||
|
Claim: "username",
|
||||||
|
Prefix: pointer.String(""),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
now: func() time.Time { return now },
|
||||||
|
},
|
||||||
|
signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||||
|
pubKeys: []*jose.JSONWebKey{
|
||||||
|
loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||||
|
},
|
||||||
|
claims: fmt.Sprintf(`{
|
||||||
|
"iss": "https://auth.example.com/a/b/foo",
|
||||||
|
"aud": "my-client",
|
||||||
|
"username": "jane",
|
||||||
|
"exp": %d
|
||||||
|
}`, valid.Unix()),
|
||||||
|
openIDConfig: `{
|
||||||
|
"issuer": "https://auth.example.com/a/b/foo",
|
||||||
|
"jwks_uri": "{{.URL}}/.testing/keys"
|
||||||
|
}`,
|
||||||
|
fetchKeysFromRemote: true,
|
||||||
|
want: &user.DefaultInfo{
|
||||||
|
Name: "jane",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "discovery url has a path, issuer url has no path",
|
||||||
|
options: Options{
|
||||||
|
JWTAuthenticator: apiserver.JWTAuthenticator{
|
||||||
|
Issuer: apiserver.Issuer{
|
||||||
|
URL: "https://auth.example.com",
|
||||||
|
DiscoveryURL: "{{.URL}}/c/d/bar/.well-known/openid-configuration",
|
||||||
|
Audiences: []string{"my-client"},
|
||||||
|
},
|
||||||
|
ClaimMappings: apiserver.ClaimMappings{
|
||||||
|
Username: apiserver.PrefixedClaimOrExpression{
|
||||||
|
Claim: "username",
|
||||||
|
Prefix: pointer.String(""),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
now: func() time.Time { return now },
|
||||||
|
},
|
||||||
|
signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||||
|
pubKeys: []*jose.JSONWebKey{
|
||||||
|
loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||||
|
},
|
||||||
|
claims: fmt.Sprintf(`{
|
||||||
|
"iss": "https://auth.example.com",
|
||||||
|
"aud": "my-client",
|
||||||
|
"username": "jane",
|
||||||
|
"exp": %d
|
||||||
|
}`, valid.Unix()),
|
||||||
|
openIDConfig: `{
|
||||||
|
"issuer": "https://auth.example.com",
|
||||||
|
"jwks_uri": "{{.URL}}/.testing/keys"
|
||||||
|
}`,
|
||||||
|
fetchKeysFromRemote: true,
|
||||||
|
want: &user.DefaultInfo{
|
||||||
|
Name: "jane",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "discovery url and issuer url have paths",
|
||||||
|
options: Options{
|
||||||
|
JWTAuthenticator: apiserver.JWTAuthenticator{
|
||||||
|
Issuer: apiserver.Issuer{
|
||||||
|
URL: "https://auth.example.com/a/b/foo",
|
||||||
|
DiscoveryURL: "{{.URL}}/c/d/bar/.well-known/openid-configuration",
|
||||||
|
Audiences: []string{"my-client"},
|
||||||
|
},
|
||||||
|
ClaimMappings: apiserver.ClaimMappings{
|
||||||
|
Username: apiserver.PrefixedClaimOrExpression{
|
||||||
|
Claim: "username",
|
||||||
|
Prefix: pointer.String(""),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
now: func() time.Time { return now },
|
||||||
|
},
|
||||||
|
signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||||
|
pubKeys: []*jose.JSONWebKey{
|
||||||
|
loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||||
|
},
|
||||||
|
claims: fmt.Sprintf(`{
|
||||||
|
"iss": "https://auth.example.com/a/b/foo",
|
||||||
|
"aud": "my-client",
|
||||||
|
"username": "jane",
|
||||||
|
"exp": %d
|
||||||
|
}`, valid.Unix()),
|
||||||
|
openIDConfig: `{
|
||||||
|
"issuer": "https://auth.example.com/a/b/foo",
|
||||||
|
"jwks_uri": "{{.URL}}/.testing/keys"
|
||||||
|
}`,
|
||||||
|
fetchKeysFromRemote: true,
|
||||||
|
want: &user.DefaultInfo{
|
||||||
|
Name: "jane",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "discovery url and issuer url have paths, issuer url has trailing slash",
|
||||||
|
options: Options{
|
||||||
|
JWTAuthenticator: apiserver.JWTAuthenticator{
|
||||||
|
Issuer: apiserver.Issuer{
|
||||||
|
URL: "https://auth.example.com/a/b/foo/",
|
||||||
|
DiscoveryURL: "{{.URL}}/c/d/bar/.well-known/openid-configuration",
|
||||||
|
Audiences: []string{"my-client"},
|
||||||
|
},
|
||||||
|
ClaimMappings: apiserver.ClaimMappings{
|
||||||
|
Username: apiserver.PrefixedClaimOrExpression{
|
||||||
|
Claim: "username",
|
||||||
|
Prefix: pointer.String(""),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
now: func() time.Time { return now },
|
||||||
|
},
|
||||||
|
signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||||
|
pubKeys: []*jose.JSONWebKey{
|
||||||
|
loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||||
|
},
|
||||||
|
claims: fmt.Sprintf(`{
|
||||||
|
"iss": "https://auth.example.com/a/b/foo/",
|
||||||
|
"aud": "my-client",
|
||||||
|
"username": "jane",
|
||||||
|
"exp": %d
|
||||||
|
}`, valid.Unix()),
|
||||||
|
openIDConfig: `{
|
||||||
|
"issuer": "https://auth.example.com/a/b/foo/",
|
||||||
|
"jwks_uri": "{{.URL}}/.testing/keys"
|
||||||
|
}`,
|
||||||
|
fetchKeysFromRemote: true,
|
||||||
|
want: &user.DefaultInfo{
|
||||||
|
Name: "jane",
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var successTestCount, failureTestCount int
|
var successTestCount, failureTestCount int
|
||||||
|
@ -141,7 +141,7 @@ func runTests(t *testing.T, useAuthenticationConfig bool) {
|
|||||||
) {
|
) {
|
||||||
caCertContent, _, caFilePath, caKeyFilePath := generateCert(t)
|
caCertContent, _, caFilePath, caKeyFilePath := generateCert(t)
|
||||||
signingPrivateKey, publicKey := keyFunc(t)
|
signingPrivateKey, publicKey := keyFunc(t)
|
||||||
oidcServer = utilsoidc.BuildAndRunTestServer(t, caFilePath, caKeyFilePath)
|
oidcServer = utilsoidc.BuildAndRunTestServer(t, caFilePath, caKeyFilePath, "")
|
||||||
|
|
||||||
if useAuthenticationConfig {
|
if useAuthenticationConfig {
|
||||||
authenticationConfig := fmt.Sprintf(`
|
authenticationConfig := fmt.Sprintf(`
|
||||||
@ -274,7 +274,7 @@ jwt:
|
|||||||
|
|
||||||
signingPrivateKey, _ = keyFunc(t)
|
signingPrivateKey, _ = keyFunc(t)
|
||||||
|
|
||||||
oidcServer = utilsoidc.BuildAndRunTestServer(t, caFilePath, caKeyFilePath)
|
oidcServer = utilsoidc.BuildAndRunTestServer(t, caFilePath, caKeyFilePath, "")
|
||||||
|
|
||||||
if useAuthenticationConfig {
|
if useAuthenticationConfig {
|
||||||
authenticationConfig := fmt.Sprintf(`
|
authenticationConfig := fmt.Sprintf(`
|
||||||
@ -888,6 +888,104 @@ jwt:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestStructuredAuthenticationDiscoveryURL tests that the discovery URL configured in jwt.issuer.discoveryURL is used to
|
||||||
|
// fetch the discovery document and the issuer in jwt.issuer.url is used to validate the ID token.
|
||||||
|
func TestStructuredAuthenticationDiscoveryURL(t *testing.T) {
|
||||||
|
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StructuredAuthenticationConfiguration, true)()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
issuerURL string
|
||||||
|
discoveryURL func(baseURL string) string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "discovery url and issuer url with no path",
|
||||||
|
issuerURL: "https://example.com",
|
||||||
|
discoveryURL: func(baseURL string) string { return baseURL },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "discovery url has path, issuer url has no path",
|
||||||
|
issuerURL: "https://example.com",
|
||||||
|
discoveryURL: func(baseURL string) string { return fmt.Sprintf("%s/c/d/bar", baseURL) },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "discovery url has no path, issuer url has path",
|
||||||
|
issuerURL: "https://example.com/a/b/foo",
|
||||||
|
discoveryURL: func(baseURL string) string { return baseURL },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "discovery url and issuer url have paths",
|
||||||
|
issuerURL: "https://example.com/a/b/foo",
|
||||||
|
discoveryURL: func(baseURL string) string {
|
||||||
|
return fmt.Sprintf("%s/c/d/bar", baseURL)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
caCertContent, _, caFilePath, caKeyFilePath := generateCert(t)
|
||||||
|
signingPrivateKey, publicKey := rsaGenerateKey(t)
|
||||||
|
// set the issuer in the discovery document to issuer url (different from the discovery URL) to assert
|
||||||
|
// 1. discovery URL is used to fetch the discovery document and
|
||||||
|
// 2. issuer in the discovery document is used to validate the ID token
|
||||||
|
oidcServer := utilsoidc.BuildAndRunTestServer(t, caFilePath, caKeyFilePath, tt.issuerURL)
|
||||||
|
discoveryURL := strings.TrimSuffix(tt.discoveryURL(oidcServer.URL()), "/") + "/.well-known/openid-configuration"
|
||||||
|
|
||||||
|
authenticationConfig := fmt.Sprintf(`
|
||||||
|
apiVersion: apiserver.config.k8s.io/v1alpha1
|
||||||
|
kind: AuthenticationConfiguration
|
||||||
|
jwt:
|
||||||
|
- issuer:
|
||||||
|
url: %s
|
||||||
|
discoveryURL: %s
|
||||||
|
audiences:
|
||||||
|
- foo
|
||||||
|
audienceMatchPolicy: MatchAny
|
||||||
|
certificateAuthority: |
|
||||||
|
%s
|
||||||
|
claimMappings:
|
||||||
|
username:
|
||||||
|
expression: "'k8s-' + claims.sub"
|
||||||
|
claimValidationRules:
|
||||||
|
- expression: 'claims.hd == "example.com"'
|
||||||
|
message: "the hd claim must be set to example.com"
|
||||||
|
`, tt.issuerURL, discoveryURL, indentCertificateAuthority(string(caCertContent)))
|
||||||
|
|
||||||
|
oidcServer.JwksHandler().EXPECT().KeySet().AnyTimes().DoAndReturn(utilsoidc.DefaultJwksHandlerBehavior(t, publicKey))
|
||||||
|
|
||||||
|
apiServer := startTestAPIServerForOIDC(t, apiServerOIDCConfig{authenticationConfigYAML: authenticationConfig}, publicKey)
|
||||||
|
|
||||||
|
idTokenLifetime := time.Second * 1200
|
||||||
|
oidcServer.TokenHandler().EXPECT().Token().Times(1).DoAndReturn(utilsoidc.TokenHandlerBehaviorReturningPredefinedJWT(
|
||||||
|
t,
|
||||||
|
signingPrivateKey,
|
||||||
|
map[string]interface{}{
|
||||||
|
"iss": tt.issuerURL, // issuer in the discovery document is used to validate the ID token
|
||||||
|
"sub": defaultOIDCClaimedUsername,
|
||||||
|
"aud": "foo",
|
||||||
|
"exp": time.Now().Add(idTokenLifetime).Unix(),
|
||||||
|
"hd": "example.com",
|
||||||
|
},
|
||||||
|
defaultStubAccessToken,
|
||||||
|
defaultStubRefreshToken,
|
||||||
|
))
|
||||||
|
|
||||||
|
tokenURL, err := oidcServer.TokenURL()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
client := configureClientFetchingOIDCCredentials(t, apiServer.ClientConfig, caCertContent, caFilePath, oidcServer.URL(), tokenURL)
|
||||||
|
ctx := testContext(t)
|
||||||
|
res, err := client.AuthenticationV1().SelfSubjectReviews().Create(ctx, &authenticationv1.SelfSubjectReview{}, metav1.CreateOptions{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, authenticationv1.UserInfo{
|
||||||
|
Username: "k8s-john_doe",
|
||||||
|
Groups: []string{"system:authenticated"},
|
||||||
|
}, res.Status.UserInfo)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func rsaGenerateKey(t *testing.T) (*rsa.PrivateKey, *rsa.PublicKey) {
|
func rsaGenerateKey(t *testing.T) (*rsa.PrivateKey, *rsa.PublicKey) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
@ -919,7 +1017,7 @@ func configureTestInfrastructure[K utilsoidc.JosePrivateKey, L utilsoidc.JosePub
|
|||||||
|
|
||||||
signingPrivateKey, publicKey := keyFunc(t)
|
signingPrivateKey, publicKey := keyFunc(t)
|
||||||
|
|
||||||
oidcServer = utilsoidc.BuildAndRunTestServer(t, caFilePath, caKeyFilePath)
|
oidcServer = utilsoidc.BuildAndRunTestServer(t, caFilePath, caKeyFilePath, "")
|
||||||
|
|
||||||
authenticationConfig := fn(t, oidcServer.URL(), string(caCertContent))
|
authenticationConfig := fn(t, oidcServer.URL(), string(caCertContent))
|
||||||
if len(authenticationConfig) > 0 {
|
if len(authenticationConfig) > 0 {
|
||||||
|
@ -80,7 +80,7 @@ func (ts *TestServer) TokenURL() (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// BuildAndRunTestServer configures OIDC TLS server and its routing
|
// BuildAndRunTestServer configures OIDC TLS server and its routing
|
||||||
func BuildAndRunTestServer(t *testing.T, caPath, caKeyPath string) *TestServer {
|
func BuildAndRunTestServer(t *testing.T, caPath, caKeyPath, issuerOverride string) *TestServer {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
certContent, err := os.ReadFile(caPath)
|
certContent, err := os.ReadFile(caPath)
|
||||||
@ -111,33 +111,21 @@ func BuildAndRunTestServer(t *testing.T, caPath, caKeyPath string) *TestServer {
|
|||||||
jwksHandler: NewMockJWKsHandler(mockCtrl),
|
jwksHandler: NewMockJWKsHandler(mockCtrl),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
issuer := httpServer.URL
|
||||||
|
// issuerOverride is used to override the issuer URL in the well-known configuration.
|
||||||
|
// This is useful to validate scenarios where discovery url is different from the issuer url.
|
||||||
|
if len(issuerOverride) > 0 {
|
||||||
|
issuer = issuerOverride
|
||||||
|
}
|
||||||
|
|
||||||
mux.HandleFunc(openIDWellKnownWebPath, func(writer http.ResponseWriter, request *http.Request) {
|
mux.HandleFunc(openIDWellKnownWebPath, func(writer http.ResponseWriter, request *http.Request) {
|
||||||
authURL, err := url.JoinPath(httpServer.URL + authWebPath)
|
discoveryDocHandler(t, writer, httpServer.URL, issuer)
|
||||||
require.NoError(t, err)
|
|
||||||
tokenURL, err := url.JoinPath(httpServer.URL + tokenWebPath)
|
|
||||||
require.NoError(t, err)
|
|
||||||
jwksURL, err := url.JoinPath(httpServer.URL + jwksWebPath)
|
|
||||||
require.NoError(t, err)
|
|
||||||
userInfoURL, err := url.JoinPath(httpServer.URL + authWebPath)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
err = json.NewEncoder(writer).Encode(struct {
|
|
||||||
Issuer string `json:"issuer"`
|
|
||||||
AuthURL string `json:"authorization_endpoint"`
|
|
||||||
TokenURL string `json:"token_endpoint"`
|
|
||||||
JWKSURL string `json:"jwks_uri"`
|
|
||||||
UserInfoURL string `json:"userinfo_endpoint"`
|
|
||||||
}{
|
|
||||||
Issuer: httpServer.URL,
|
|
||||||
AuthURL: authURL,
|
|
||||||
TokenURL: tokenURL,
|
|
||||||
JWKSURL: jwksURL,
|
|
||||||
UserInfoURL: userInfoURL,
|
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
writer.Header().Add("Content-Type", "application/json")
|
// /c/d/bar/.well-known/openid-configuration is used to validate scenarios where discovery url is different from the issuer url
|
||||||
writer.WriteHeader(http.StatusOK)
|
// and discovery url contains path.
|
||||||
|
mux.HandleFunc("/c/d/bar"+openIDWellKnownWebPath, func(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
discoveryDocHandler(t, writer, httpServer.URL, issuer)
|
||||||
})
|
})
|
||||||
|
|
||||||
mux.HandleFunc(tokenWebPath, func(writer http.ResponseWriter, request *http.Request) {
|
mux.HandleFunc(tokenWebPath, func(writer http.ResponseWriter, request *http.Request) {
|
||||||
@ -171,6 +159,34 @@ func BuildAndRunTestServer(t *testing.T, caPath, caKeyPath string) *TestServer {
|
|||||||
return oidcServer
|
return oidcServer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func discoveryDocHandler(t *testing.T, writer http.ResponseWriter, httpServerURL, issuer string) {
|
||||||
|
authURL, err := url.JoinPath(httpServerURL + authWebPath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
tokenURL, err := url.JoinPath(httpServerURL + tokenWebPath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
jwksURL, err := url.JoinPath(httpServerURL + jwksWebPath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
userInfoURL, err := url.JoinPath(httpServerURL + authWebPath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
writer.Header().Add("Content-Type", "application/json")
|
||||||
|
|
||||||
|
err = json.NewEncoder(writer).Encode(struct {
|
||||||
|
Issuer string `json:"issuer"`
|
||||||
|
AuthURL string `json:"authorization_endpoint"`
|
||||||
|
TokenURL string `json:"token_endpoint"`
|
||||||
|
JWKSURL string `json:"jwks_uri"`
|
||||||
|
UserInfoURL string `json:"userinfo_endpoint"`
|
||||||
|
}{
|
||||||
|
Issuer: issuer,
|
||||||
|
AuthURL: authURL,
|
||||||
|
TokenURL: tokenURL,
|
||||||
|
JWKSURL: jwksURL,
|
||||||
|
UserInfoURL: userInfoURL,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
type JosePrivateKey interface {
|
type JosePrivateKey interface {
|
||||||
*rsa.PrivateKey | *ecdsa.PrivateKey
|
*rsa.PrivateKey | *ecdsa.PrivateKey
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user