Skip to content

Commit

Permalink
Add DiscoveryURL to AuthenticationConfiguration
Browse files Browse the repository at this point in the history
Signed-off-by: Anish Ramasekar <anish.ramasekar@gmail.com>
  • Loading branch information
aramase committed Mar 4, 2024
1 parent 6d2ee13 commit 84852ff
Show file tree
Hide file tree
Showing 6 changed files with 212 additions and 14 deletions.
38 changes: 36 additions & 2 deletions staging/src/k8s.io/apiserver/pkg/apis/apiserver/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,9 +175,43 @@ type JWTAuthenticator struct {
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 {
URL string
// 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
// 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
Audiences []string
AudienceMatchPolicy AudienceMatchPolicyType
Expand Down
34 changes: 31 additions & 3 deletions staging/src/k8s.io/apiserver/pkg/apis/apiserver/v1alpha1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,17 +209,45 @@ type JWTAuthenticator struct {
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 {
// 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.
// Used to fetch discovery information unless overridden by discoveryURL.
// Required to be unique.
// 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 `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
// used to validate the connection when fetching discovery information.
// If unset, the system verifier is used.
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -100,21 +100,40 @@ func validateJWTAuthenticator(authenticator api.JWTAuthenticator, fldPath *field
func validateIssuer(issuer api.Issuer, fldPath *field.Path) 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, validateCertificateAuthority(issuer.CertificateAuthority, fldPath.Child("certificateAuthority"))...)

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

if len(issuerURL) == 0 {
allErrs = append(allErrs, field.Required(fldPath, "URL is required"))
return allErrs
if len(issuerDiscoveryURL) == 0 {
return nil
}

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)
if err != nil {
allErrs = append(allErrs, field.Invalid(fldPath, issuerURL, err.Error()))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ func TestValidateAuthenticationConfiguration(t *testing.T) {
}
}

func TestValidateURL(t *testing.T) {
func TestValidateIssuerURL(t *testing.T) {
fldPath := field.NewPath("issuer", "url")

testCases := []struct {
Expand Down Expand Up @@ -259,7 +259,92 @@ func TestValidateURL(t *testing.T) {

for _, tt := range testCases {
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 != "" {
t.Fatalf("URL validation mismatch (-want +got):\n%s", d)
}
Expand Down

0 comments on commit 84852ff

Please sign in to comment.