authelia/internal/configuration/validator/identity_providers_test.go

2504 lines
82 KiB
Go

package validator
import (
"crypto/ecdsa"
"crypto/ed25519"
"crypto/rsa"
"crypto/x509"
"errors"
"fmt"
"net/url"
"os"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/authelia/authelia/v4/internal/configuration/schema"
"github.com/authelia/authelia/v4/internal/oidc"
"github.com/authelia/authelia/v4/internal/utils"
)
func TestShouldRaiseErrorWhenInvalidOIDCServerConfiguration(t *testing.T) {
validator := schema.NewStructValidator()
config := &schema.IdentityProvidersConfiguration{
OIDC: &schema.OpenIDConnectConfiguration{
HMACSecret: "abc",
},
}
ValidateIdentityProviders(config, validator)
require.Len(t, validator.Errors(), 2)
assert.EqualError(t, validator.Errors()[0], "identity_providers: oidc: option 'issuer_private_key' or `issuer_jwks` is required")
assert.EqualError(t, validator.Errors()[1], "identity_providers: oidc: option 'clients' must have one or more clients configured")
}
func TestShouldNotRaiseErrorWhenCORSEndpointsValid(t *testing.T) {
validator := schema.NewStructValidator()
config := &schema.IdentityProvidersConfiguration{
OIDC: &schema.OpenIDConnectConfiguration{
HMACSecret: "rLABDrx87et5KvRHVUgTm3pezWWd8LMN",
IssuerPrivateKey: keyRSA2048,
CORS: schema.OpenIDConnectCORSConfiguration{
Endpoints: []string{oidc.EndpointAuthorization, oidc.EndpointToken, oidc.EndpointIntrospection, oidc.EndpointRevocation, oidc.EndpointUserinfo},
},
Clients: []schema.OpenIDConnectClientConfiguration{
{
ID: "example",
Secret: tOpenIDConnectPlainTextClientSecret,
},
},
},
}
ValidateIdentityProviders(config, validator)
assert.Len(t, validator.Errors(), 0)
}
func TestShouldRaiseErrorWhenCORSEndpointsNotValid(t *testing.T) {
validator := schema.NewStructValidator()
config := &schema.IdentityProvidersConfiguration{
OIDC: &schema.OpenIDConnectConfiguration{
HMACSecret: "rLABDrx87et5KvRHVUgTm3pezWWd8LMN",
IssuerPrivateKey: keyRSA2048,
CORS: schema.OpenIDConnectCORSConfiguration{
Endpoints: []string{oidc.EndpointAuthorization, oidc.EndpointToken, oidc.EndpointIntrospection, oidc.EndpointRevocation, oidc.EndpointUserinfo, "invalid_endpoint"},
},
Clients: []schema.OpenIDConnectClientConfiguration{
{
ID: "example",
Secret: tOpenIDConnectPlainTextClientSecret,
},
},
},
}
ValidateIdentityProviders(config, validator)
require.Len(t, validator.Errors(), 1)
assert.EqualError(t, validator.Errors()[0], "identity_providers: oidc: cors: option 'endpoints' contains an invalid value 'invalid_endpoint': must be one of 'authorization', 'pushed-authorization-request', 'token', 'introspection', 'revocation', or 'userinfo'")
}
func TestShouldRaiseErrorWhenOIDCPKCEEnforceValueInvalid(t *testing.T) {
validator := schema.NewStructValidator()
config := &schema.IdentityProvidersConfiguration{
OIDC: &schema.OpenIDConnectConfiguration{
HMACSecret: "rLABDrx87et5KvRHVUgTm3pezWWd8LMN",
IssuerPrivateKey: keyRSA2048,
EnforcePKCE: testInvalid,
},
}
ValidateIdentityProviders(config, validator)
require.Len(t, validator.Errors(), 2)
assert.EqualError(t, validator.Errors()[0], "identity_providers: oidc: option 'enforce_pkce' must be 'never', 'public_clients_only' or 'always', but it's configured as 'invalid'")
assert.EqualError(t, validator.Errors()[1], "identity_providers: oidc: option 'clients' must have one or more clients configured")
}
func TestShouldRaiseErrorWhenOIDCCORSOriginsHasInvalidValues(t *testing.T) {
validator := schema.NewStructValidator()
config := &schema.IdentityProvidersConfiguration{
OIDC: &schema.OpenIDConnectConfiguration{
HMACSecret: "rLABDrx87et5KvRHVUgTm3pezWWd8LMN",
IssuerPrivateKey: keyRSA2048,
CORS: schema.OpenIDConnectCORSConfiguration{
AllowedOrigins: utils.URLsFromStringSlice([]string{"https://example.com/", "https://site.example.com/subpath", "https://site.example.com?example=true", "*"}),
AllowedOriginsFromClientRedirectURIs: true,
},
Clients: []schema.OpenIDConnectClientConfiguration{
{
ID: "myclient",
Secret: tOpenIDConnectPlainTextClientSecret,
Policy: "two_factor",
RedirectURIs: []string{"https://example.com/oauth2_callback", "https://localhost:566/callback", "http://an.example.com/callback", "file://a/file"},
},
},
},
}
ValidateIdentityProviders(config, validator)
require.Len(t, validator.Errors(), 5)
assert.EqualError(t, validator.Errors()[0], "identity_providers: oidc: cors: option 'allowed_origins' contains an invalid value 'https://example.com/' as it has a path: origins must only be scheme, hostname, and an optional port")
assert.EqualError(t, validator.Errors()[1], "identity_providers: oidc: cors: option 'allowed_origins' contains an invalid value 'https://site.example.com/subpath' as it has a path: origins must only be scheme, hostname, and an optional port")
assert.EqualError(t, validator.Errors()[2], "identity_providers: oidc: cors: option 'allowed_origins' contains an invalid value 'https://site.example.com?example=true' as it has a query string: origins must only be scheme, hostname, and an optional port")
assert.EqualError(t, validator.Errors()[3], "identity_providers: oidc: cors: option 'allowed_origins' contains the wildcard origin '*' with more than one origin but the wildcard origin must be defined by itself")
assert.EqualError(t, validator.Errors()[4], "identity_providers: oidc: cors: option 'allowed_origins' contains the wildcard origin '*' cannot be specified with option 'allowed_origins_from_client_redirect_uris' enabled")
require.Len(t, config.OIDC.CORS.AllowedOrigins, 6)
assert.Equal(t, "*", config.OIDC.CORS.AllowedOrigins[3].String())
assert.Equal(t, "https://example.com", config.OIDC.CORS.AllowedOrigins[4].String())
}
func TestShouldRaiseErrorWhenOIDCServerNoClients(t *testing.T) {
validator := schema.NewStructValidator()
config := &schema.IdentityProvidersConfiguration{
OIDC: &schema.OpenIDConnectConfiguration{
HMACSecret: "rLABDrx87et5KvRHVUgTm3pezWWd8LMN",
IssuerPrivateKey: keyRSA2048,
},
}
ValidateIdentityProviders(config, validator)
require.Len(t, validator.Errors(), 1)
assert.EqualError(t, validator.Errors()[0], "identity_providers: oidc: option 'clients' must have one or more clients configured")
}
func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) {
mustParseURL := func(u string) url.URL {
out, err := url.Parse(u)
if err != nil {
panic(err)
}
return *out
}
testCases := []struct {
Name string
Clients []schema.OpenIDConnectClientConfiguration
Errors []string
}{
{
Name: "EmptyIDAndSecret",
Clients: []schema.OpenIDConnectClientConfiguration{
{
ID: "",
Secret: nil,
Policy: "",
RedirectURIs: []string{},
},
},
Errors: []string{
"identity_providers: oidc: client '': option 'secret' is required",
"identity_providers: oidc: clients: option 'id' is required but was absent on the clients in positions #1",
},
},
{
Name: "InvalidPolicy",
Clients: []schema.OpenIDConnectClientConfiguration{
{
ID: "client-1",
Secret: tOpenIDConnectPlainTextClientSecret,
Policy: "a-policy",
RedirectURIs: []string{
"https://google.com",
},
},
},
Errors: []string{
"identity_providers: oidc: client 'client-1': option 'policy' must be one of 'one_factor' or 'two_factor' but it's configured as 'a-policy'",
},
},
{
Name: "ClientIDDuplicated",
Clients: []schema.OpenIDConnectClientConfiguration{
{
ID: "client-x",
Secret: tOpenIDConnectPlainTextClientSecret,
Policy: policyTwoFactor,
RedirectURIs: []string{},
},
{
ID: "client-x",
Secret: tOpenIDConnectPlainTextClientSecret,
Policy: policyTwoFactor,
RedirectURIs: []string{},
},
},
Errors: []string{
"identity_providers: oidc: clients: option 'id' must be unique for every client but one or more clients share the following 'id' values 'client-x'",
},
},
{
Name: "RedirectURIInvalid",
Clients: []schema.OpenIDConnectClientConfiguration{
{
ID: "client-check-uri-parse",
Secret: tOpenIDConnectPlainTextClientSecret,
Policy: policyTwoFactor,
RedirectURIs: []string{
"http://abc@%two",
},
},
},
Errors: []string{
"identity_providers: oidc: client 'client-check-uri-parse': option 'redirect_uris' has an invalid value: redirect uri 'http://abc@%two' could not be parsed: parse \"http://abc@%two\": invalid URL escape \"%tw\"",
},
},
{
Name: "RedirectURINotAbsolute",
Clients: []schema.OpenIDConnectClientConfiguration{
{
ID: "client-check-uri-abs",
Secret: tOpenIDConnectPlainTextClientSecret,
Policy: policyTwoFactor,
RedirectURIs: []string{
"google.com",
},
},
},
Errors: []string{
"identity_providers: oidc: client 'client-check-uri-abs': option 'redirect_uris' has an invalid value: redirect uri 'google.com' must have a scheme but it's absent",
},
},
{
Name: "ValidSectorIdentifier",
Clients: []schema.OpenIDConnectClientConfiguration{
{
ID: "client-valid-sector",
Secret: tOpenIDConnectPlainTextClientSecret,
Policy: policyTwoFactor,
RedirectURIs: []string{
"https://google.com",
},
SectorIdentifier: mustParseURL(exampleDotCom),
},
},
},
{
Name: "ValidSectorIdentifierWithPort",
Clients: []schema.OpenIDConnectClientConfiguration{
{
ID: "client-valid-sector",
Secret: tOpenIDConnectPlainTextClientSecret,
Policy: policyTwoFactor,
RedirectURIs: []string{
"https://google.com",
},
SectorIdentifier: mustParseURL("example.com:2000"),
},
},
},
{
Name: "InvalidSectorIdentifierInvalidURL",
Clients: []schema.OpenIDConnectClientConfiguration{
{
ID: "client-invalid-sector",
Secret: tOpenIDConnectPlainTextClientSecret,
Policy: policyTwoFactor,
RedirectURIs: []string{
"https://google.com",
},
SectorIdentifier: mustParseURL("https://user:pass@example.com/path?query=abc#fragment"),
},
},
Errors: []string{
"identity_providers: oidc: client 'client-invalid-sector': option 'sector_identifier' with value 'https://user:pass@example.com/path?query=abc#fragment': must be a URL with only the host component for example 'example.com' but it has a scheme with the value 'https'",
"identity_providers: oidc: client 'client-invalid-sector': option 'sector_identifier' with value 'https://user:pass@example.com/path?query=abc#fragment': must be a URL with only the host component for example 'example.com' but it has a path with the value '/path'",
"identity_providers: oidc: client 'client-invalid-sector': option 'sector_identifier' with value 'https://user:pass@example.com/path?query=abc#fragment': must be a URL with only the host component for example 'example.com' but it has a query with the value 'query=abc'",
"identity_providers: oidc: client 'client-invalid-sector': option 'sector_identifier' with value 'https://user:pass@example.com/path?query=abc#fragment': must be a URL with only the host component for example 'example.com' but it has a fragment with the value 'fragment'",
"identity_providers: oidc: client 'client-invalid-sector': option 'sector_identifier' with value 'https://user:pass@example.com/path?query=abc#fragment': must be a URL with only the host component for example 'example.com' but it has a username with the value 'user'",
"identity_providers: oidc: client 'client-invalid-sector': option 'sector_identifier' with value 'https://user:pass@example.com/path?query=abc#fragment': must be a URL with only the host component for example 'example.com' but it has a password",
},
},
{
Name: "InvalidSectorIdentifierInvalidHost",
Clients: []schema.OpenIDConnectClientConfiguration{
{
ID: "client-invalid-sector",
Secret: tOpenIDConnectPlainTextClientSecret,
Policy: policyTwoFactor,
RedirectURIs: []string{
"https://google.com",
},
SectorIdentifier: mustParseURL("example.com/path?query=abc#fragment"),
},
},
Errors: []string{
"identity_providers: oidc: client 'client-invalid-sector': option 'sector_identifier' with value 'example.com/path?query=abc#fragment': must be a URL with only the host component but appears to be invalid",
},
},
{
Name: "InvalidConsentMode",
Clients: []schema.OpenIDConnectClientConfiguration{
{
ID: "client-bad-consent-mode",
Secret: tOpenIDConnectPlainTextClientSecret,
Policy: policyTwoFactor,
RedirectURIs: []string{
"https://google.com",
},
ConsentMode: "cap",
},
},
Errors: []string{
"identity_providers: oidc: client 'client-bad-consent-mode': consent: option 'mode' must be one of 'auto', 'implicit', 'explicit', 'pre-configured', or 'auto' but it's configured as 'cap'",
},
},
{
Name: "InvalidPKCEChallengeMethod",
Clients: []schema.OpenIDConnectClientConfiguration{
{
ID: "client-bad-pkce-mode",
Secret: tOpenIDConnectPlainTextClientSecret,
Policy: policyTwoFactor,
RedirectURIs: []string{
"https://google.com",
},
PKCEChallengeMethod: "abc",
},
},
Errors: []string{
"identity_providers: oidc: client 'client-bad-pkce-mode': option 'pkce_challenge_method' must be one of 'plain' or 'S256' but it's configured as 'abc'",
},
},
{
Name: "InvalidPKCEChallengeMethodLowerCaseS256",
Clients: []schema.OpenIDConnectClientConfiguration{
{
ID: "client-bad-pkce-mode-s256",
Secret: tOpenIDConnectPlainTextClientSecret,
Policy: policyTwoFactor,
RedirectURIs: []string{
"https://google.com",
},
PKCEChallengeMethod: "s256",
},
},
Errors: []string{
"identity_providers: oidc: client 'client-bad-pkce-mode-s256': option 'pkce_challenge_method' must be one of 'plain' or 'S256' but it's configured as 's256'",
},
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
validator := schema.NewStructValidator()
config := &schema.IdentityProvidersConfiguration{
OIDC: &schema.OpenIDConnectConfiguration{
HMACSecret: "rLABDrx87et5KvRHVUgTm3pezWWd8LMN",
IssuerPrivateKey: keyRSA2048,
Clients: tc.Clients,
},
}
ValidateIdentityProviders(config, validator)
errs := validator.Errors()
require.Len(t, errs, len(tc.Errors))
for i, errStr := range tc.Errors {
t.Run(fmt.Sprintf("Error%d", i+1), func(t *testing.T) {
assert.EqualError(t, errs[i], errStr)
})
}
})
}
}
func TestShouldRaiseErrorWhenOIDCClientConfiguredWithBadScopes(t *testing.T) {
validator := schema.NewStructValidator()
config := &schema.IdentityProvidersConfiguration{
OIDC: &schema.OpenIDConnectConfiguration{
HMACSecret: "rLABDrx87et5KvRHVUgTm3pezWWd8LMN",
IssuerPrivateKey: keyRSA2048,
Clients: []schema.OpenIDConnectClientConfiguration{
{
ID: "good_id",
Secret: tOpenIDConnectPlainTextClientSecret,
Policy: "two_factor",
Scopes: []string{"openid", "bad_scope"},
RedirectURIs: []string{
"https://google.com/callback",
},
},
},
},
}
ValidateIdentityProviders(config, validator)
require.Len(t, validator.Errors(), 1)
assert.EqualError(t, validator.Errors()[0], "identity_providers: oidc: client 'good_id': option 'scopes' must only have the values 'openid', 'email', 'profile', 'groups', or 'offline_access' but the values 'bad_scope' are present")
}
func TestShouldRaiseErrorWhenOIDCClientConfiguredWithBadGrantTypes(t *testing.T) {
validator := schema.NewStructValidator()
config := &schema.IdentityProvidersConfiguration{
OIDC: &schema.OpenIDConnectConfiguration{
HMACSecret: "rLABDrx87et5KvRHVUgTm3pezWWd8LMN",
IssuerPrivateKey: keyRSA2048,
Clients: []schema.OpenIDConnectClientConfiguration{
{
ID: "good_id",
Secret: tOpenIDConnectPBKDF2ClientSecret,
Policy: "two_factor",
GrantTypes: []string{"bad_grant_type"},
RedirectURIs: []string{
"https://google.com/callback",
},
},
},
},
}
ValidateIdentityProviders(config, validator)
require.Len(t, validator.Errors(), 1)
assert.EqualError(t, validator.Errors()[0], "identity_providers: oidc: client 'good_id': option 'grant_types' must only have the values 'implicit', 'refresh_token', or 'authorization_code' but the values 'bad_grant_type' are present")
}
func TestShouldNotErrorOnCertificateValid(t *testing.T) {
validator := schema.NewStructValidator()
config := &schema.IdentityProvidersConfiguration{
OIDC: &schema.OpenIDConnectConfiguration{
HMACSecret: "rLABDrx87et5KvRHVUgTm3pezWWd8LMN",
IssuerCertificateChain: certRSA2048,
IssuerPrivateKey: keyRSA2048,
Clients: []schema.OpenIDConnectClientConfiguration{
{
ID: "good_id",
Secret: tOpenIDConnectPBKDF2ClientSecret,
Policy: "two_factor",
RedirectURIs: []string{
"https://google.com/callback",
},
},
},
},
}
ValidateIdentityProviders(config, validator)
assert.Len(t, validator.Warnings(), 0)
assert.Len(t, validator.Errors(), 0)
}
func TestShouldRaiseErrorOnCertificateNotValid(t *testing.T) {
validator := schema.NewStructValidator()
config := &schema.IdentityProvidersConfiguration{
OIDC: &schema.OpenIDConnectConfiguration{
HMACSecret: "rLABDrx87et5KvRHVUgTm3pezWWd8LMN",
IssuerCertificateChain: certRSA2048,
IssuerPrivateKey: keyRSA4096,
Clients: []schema.OpenIDConnectClientConfiguration{
{
ID: "good_id",
Secret: tOpenIDConnectPBKDF2ClientSecret,
Policy: "two_factor",
RedirectURIs: []string{
"https://google.com/callback",
},
},
},
},
}
ValidateIdentityProviders(config, validator)
assert.Len(t, validator.Warnings(), 0)
require.Len(t, validator.Errors(), 1)
assert.EqualError(t, validator.Errors()[0], "identity_providers: oidc: issuer_jwks: key #1 with key id '9c7423': option 'key' does not appear to be the private key the certificate provided by option 'certificate_chain'")
}
func TestValidateIdentityProvidersShouldRaiseWarningOnSecurityIssue(t *testing.T) {
validator := schema.NewStructValidator()
config := &schema.IdentityProvidersConfiguration{
OIDC: &schema.OpenIDConnectConfiguration{
HMACSecret: "abc",
IssuerPrivateKey: keyRSA2048,
MinimumParameterEntropy: 1,
Clients: []schema.OpenIDConnectClientConfiguration{
{
ID: "good_id",
Secret: tOpenIDConnectPBKDF2ClientSecret,
Policy: "two_factor",
RedirectURIs: []string{
"https://google.com/callback",
},
},
},
},
}
ValidateIdentityProviders(config, validator)
assert.Len(t, validator.Errors(), 0)
require.Len(t, validator.Warnings(), 1)
assert.EqualError(t, validator.Warnings()[0], "openid connect provider: SECURITY ISSUE - minimum parameter entropy is configured to an unsafe value, it should be above 8 but it's configured to 1")
}
func TestValidateIdentityProvidersShouldRaiseErrorsOnInvalidClientTypes(t *testing.T) {
validator := schema.NewStructValidator()
config := &schema.IdentityProvidersConfiguration{
OIDC: &schema.OpenIDConnectConfiguration{
HMACSecret: "hmac1",
IssuerPrivateKey: keyRSA2048,
Clients: []schema.OpenIDConnectClientConfiguration{
{
ID: "client-with-invalid-secret",
Secret: tOpenIDConnectPlainTextClientSecret,
Public: true,
Policy: "two_factor",
RedirectURIs: []string{
"https://localhost",
},
},
{
ID: "client-with-bad-redirect-uri",
Secret: tOpenIDConnectPBKDF2ClientSecret,
Public: false,
Policy: "two_factor",
RedirectURIs: []string{
oauth2InstalledApp,
},
},
},
},
}
ValidateIdentityProviders(config, validator)
require.Len(t, validator.Errors(), 2)
assert.Len(t, validator.Warnings(), 0)
assert.EqualError(t, validator.Errors()[0], "identity_providers: oidc: client 'client-with-invalid-secret': option 'secret' is required to be empty when option 'public' is true")
assert.EqualError(t, validator.Errors()[1], "identity_providers: oidc: client 'client-with-bad-redirect-uri': option 'redirect_uris' has the redirect uri 'urn:ietf:wg:oauth:2.0:oob' when option 'public' is false but this is invalid as this uri is not valid for the openid connect confidential client type")
}
func TestValidateIdentityProvidersShouldNotRaiseErrorsOnValidClientOptions(t *testing.T) {
validator := schema.NewStructValidator()
config := &schema.IdentityProvidersConfiguration{
OIDC: &schema.OpenIDConnectConfiguration{
HMACSecret: "hmac1",
IssuerPrivateKey: keyRSA2048,
Clients: []schema.OpenIDConnectClientConfiguration{
{
ID: "installed-app-client",
Public: true,
Policy: "two_factor",
RedirectURIs: []string{
oauth2InstalledApp,
},
},
{
ID: "client-with-https-scheme",
Public: true,
Policy: "two_factor",
RedirectURIs: []string{
"https://localhost:9000",
},
},
{
ID: "client-with-loopback",
Public: true,
Policy: "two_factor",
RedirectURIs: []string{
"http://127.0.0.1",
},
},
{
ID: "client-with-pkce-mode-plain",
Public: true,
Policy: "two_factor",
RedirectURIs: []string{
"https://pkce.com",
},
PKCEChallengeMethod: "plain",
},
{
ID: "client-with-pkce-mode-S256",
Public: true,
Policy: "two_factor",
RedirectURIs: []string{
"https://pkce.com",
},
PKCEChallengeMethod: "S256",
},
},
},
}
ValidateIdentityProviders(config, validator)
assert.Len(t, validator.Errors(), 0)
assert.Len(t, validator.Warnings(), 0)
}
func TestValidateIdentityProvidersShouldRaiseWarningOnPlainTextClients(t *testing.T) {
validator := schema.NewStructValidator()
config := &schema.IdentityProvidersConfiguration{
OIDC: &schema.OpenIDConnectConfiguration{
HMACSecret: "hmac1",
IssuerPrivateKey: keyRSA2048,
Clients: []schema.OpenIDConnectClientConfiguration{
{
ID: "client-with-invalid-secret_standard",
Secret: tOpenIDConnectPlainTextClientSecret,
Policy: "two_factor",
RedirectURIs: []string{
"https://localhost",
},
},
},
},
}
ValidateIdentityProviders(config, validator)
assert.Len(t, validator.Errors(), 0)
require.Len(t, validator.Warnings(), 1)
assert.EqualError(t, validator.Warnings()[0], "identity_providers: oidc: client 'client-with-invalid-secret_standard': option 'secret' is plaintext but for clients not using the 'token_endpoint_auth_method' of 'client_secret_jwt' it should be a hashed value as plaintext values are deprecated with the exception of 'client_secret_jwt' and will be removed when oidc becomes stable")
}
// All valid schemes are supported as defined in https://datatracker.ietf.org/doc/html/rfc8252#section-7.1
func TestValidateOIDCClientRedirectURIsSupportingPrivateUseURISchemes(t *testing.T) {
have := &schema.OpenIDConnectConfiguration{
Clients: []schema.OpenIDConnectClientConfiguration{
{
ID: "owncloud",
RedirectURIs: []string{
"https://www.mywebsite.com",
"http://www.mywebsite.com",
"oc://ios.owncloud.com",
// example given in the RFC https://datatracker.ietf.org/doc/html/rfc8252#section-7.1
"com.example.app:/oauth2redirect/example-provider",
oauth2InstalledApp,
},
},
},
}
t.Run("public", func(t *testing.T) {
validator := schema.NewStructValidator()
have.Clients[0].Public = true
validateOIDCClientRedirectURIs(0, have, validator, nil)
assert.Len(t, validator.Warnings(), 0)
assert.Len(t, validator.Errors(), 0)
})
t.Run("not public", func(t *testing.T) {
validator := schema.NewStructValidator()
have.Clients[0].Public = false
validateOIDCClientRedirectURIs(0, have, validator, nil)
assert.Len(t, validator.Warnings(), 0)
assert.Len(t, validator.Errors(), 1)
assert.ElementsMatch(t, validator.Errors(), []error{
errors.New("identity_providers: oidc: client 'owncloud': option 'redirect_uris' has the redirect uri 'urn:ietf:wg:oauth:2.0:oob' when option 'public' is false but this is invalid as this uri is not valid for the openid connect confidential client type"),
})
})
}
func TestValidateOIDCClients(t *testing.T) {
type tcv struct {
Scopes []string
ResponseTypes []string
ResponseModes []string
GrantTypes []string
}
testCasses := []struct {
name string
setup func(have *schema.OpenIDConnectConfiguration)
validate func(t *testing.T, have *schema.OpenIDConnectConfiguration)
have tcv
expected tcv
serrs []string // Soft errors which will be warnings before GA.
errs []string
}{
{
"ShouldSetDefaultResponseTypeAndResponseModes",
nil,
nil,
tcv{
nil,
nil,
nil,
nil,
},
tcv{
[]string{oidc.ScopeOpenID, oidc.ScopeGroups, oidc.ScopeProfile, oidc.ScopeEmail},
[]string{oidc.ResponseTypeAuthorizationCodeFlow},
[]string{oidc.ResponseModeFormPost, oidc.ResponseModeQuery},
[]string{oidc.GrantTypeAuthorizationCode},
},
nil,
nil,
},
{
"ShouldIncludeMinimalScope",
nil,
nil,
tcv{
[]string{oidc.ScopeEmail},
nil,
nil,
nil,
},
tcv{
[]string{oidc.ScopeOpenID, oidc.ScopeEmail},
[]string{oidc.ResponseTypeAuthorizationCodeFlow},
[]string{oidc.ResponseModeFormPost, oidc.ResponseModeQuery},
[]string{oidc.GrantTypeAuthorizationCode},
},
nil,
nil,
},
{
"ShouldSetDefaultResponseModesFlowAuthorizeCode",
nil,
nil,
tcv{
nil,
[]string{oidc.ResponseTypeAuthorizationCodeFlow},
nil,
nil,
},
tcv{
[]string{oidc.ScopeOpenID, oidc.ScopeGroups, oidc.ScopeProfile, oidc.ScopeEmail},
[]string{oidc.ResponseTypeAuthorizationCodeFlow},
[]string{oidc.ResponseModeFormPost, oidc.ResponseModeQuery},
[]string{oidc.GrantTypeAuthorizationCode},
},
nil,
nil,
},
{
"ShouldSetDefaultResponseModesFlowImplicit",
nil,
nil,
tcv{
nil,
[]string{oidc.ResponseTypeImplicitFlowBoth},
nil,
nil,
},
tcv{
[]string{oidc.ScopeOpenID, oidc.ScopeGroups, oidc.ScopeProfile, oidc.ScopeEmail},
[]string{oidc.ResponseTypeImplicitFlowBoth},
[]string{oidc.ResponseModeFormPost, oidc.ResponseModeFragment},
[]string{oidc.GrantTypeImplicit},
},
nil,
nil,
},
{
"ShouldSetDefaultResponseModesFlowHybrid",
nil,
nil,
tcv{
nil,
[]string{oidc.ResponseTypeHybridFlowBoth},
nil,
nil,
},
tcv{
[]string{oidc.ScopeOpenID, oidc.ScopeGroups, oidc.ScopeProfile, oidc.ScopeEmail},
[]string{oidc.ResponseTypeHybridFlowBoth},
[]string{oidc.ResponseModeFormPost, oidc.ResponseModeFragment},
[]string{oidc.GrantTypeAuthorizationCode, oidc.GrantTypeImplicit},
},
nil,
nil,
},
{
"ShouldSetDefaultResponseModesFlowMixedAuthorizeCodeHybrid",
nil,
nil,
tcv{
nil,
[]string{oidc.ResponseTypeAuthorizationCodeFlow, oidc.ResponseTypeHybridFlowBoth},
nil,
nil,
},
tcv{
[]string{oidc.ScopeOpenID, oidc.ScopeGroups, oidc.ScopeProfile, oidc.ScopeEmail},
[]string{oidc.ResponseTypeAuthorizationCodeFlow, oidc.ResponseTypeHybridFlowBoth},
[]string{oidc.ResponseModeFormPost, oidc.ResponseModeQuery, oidc.ResponseModeFragment},
[]string{oidc.GrantTypeAuthorizationCode, oidc.GrantTypeImplicit},
},
nil,
nil,
},
{
"ShouldSetDefaultResponseModesFlowMixedAuthorizeCodeImplicit",
nil,
nil,
tcv{
nil,
[]string{oidc.ResponseTypeAuthorizationCodeFlow, oidc.ResponseTypeImplicitFlowBoth},
nil,
nil,
},
tcv{
[]string{oidc.ScopeOpenID, oidc.ScopeGroups, oidc.ScopeProfile, oidc.ScopeEmail},
[]string{oidc.ResponseTypeAuthorizationCodeFlow, oidc.ResponseTypeImplicitFlowBoth},
[]string{oidc.ResponseModeFormPost, oidc.ResponseModeQuery, oidc.ResponseModeFragment},
[]string{oidc.GrantTypeAuthorizationCode, oidc.GrantTypeImplicit},
},
nil,
nil,
},
{
"ShouldSetDefaultResponseModesFlowMixedAll",
nil,
nil,
tcv{
nil,
[]string{oidc.ResponseTypeAuthorizationCodeFlow, oidc.ResponseTypeImplicitFlowBoth, oidc.ResponseTypeHybridFlowBoth},
nil,
nil,
},
tcv{
[]string{oidc.ScopeOpenID, oidc.ScopeGroups, oidc.ScopeProfile, oidc.ScopeEmail},
[]string{oidc.ResponseTypeAuthorizationCodeFlow, oidc.ResponseTypeImplicitFlowBoth, oidc.ResponseTypeHybridFlowBoth},
[]string{oidc.ResponseModeFormPost, oidc.ResponseModeQuery, oidc.ResponseModeFragment},
[]string{oidc.GrantTypeAuthorizationCode, oidc.GrantTypeImplicit},
},
nil,
nil,
},
{
"ShouldNotOverrideValues",
nil,
nil,
tcv{
[]string{oidc.ScopeOpenID, oidc.ScopeGroups},
[]string{oidc.ResponseTypeAuthorizationCodeFlow, oidc.ResponseTypeImplicitFlowBoth, oidc.ResponseTypeHybridFlowBoth},
[]string{oidc.ResponseModeFormPost},
[]string{oidc.GrantTypeAuthorizationCode},
},
tcv{
[]string{oidc.ScopeOpenID, oidc.ScopeGroups},
[]string{oidc.ResponseTypeAuthorizationCodeFlow, oidc.ResponseTypeImplicitFlowBoth, oidc.ResponseTypeHybridFlowBoth},
[]string{oidc.ResponseModeFormPost},
[]string{oidc.GrantTypeAuthorizationCode},
},
nil,
nil,
},
{
"ShouldRaiseErrorOnDuplicateScopes",
nil,
nil,
tcv{
[]string{oidc.ScopeOpenID, oidc.ScopeProfile, oidc.ScopeOpenID},
nil,
nil,
nil,
},
tcv{
[]string{oidc.ScopeOpenID, oidc.ScopeProfile, oidc.ScopeOpenID},
[]string{oidc.ResponseTypeAuthorizationCodeFlow},
[]string{oidc.ResponseModeFormPost, oidc.ResponseModeQuery},
[]string{oidc.GrantTypeAuthorizationCode},
},
[]string{
"identity_providers: oidc: client 'test': option 'scopes' must have unique values but the values 'openid' are duplicated",
},
nil,
},
{
"ShouldRaiseErrorOnInvalidScopes",
nil,
nil,
tcv{
[]string{oidc.ScopeOpenID, oidc.ScopeProfile, "group"},
nil,
nil,
nil,
},
tcv{
[]string{oidc.ScopeOpenID, oidc.ScopeProfile, "group"},
[]string{oidc.ResponseTypeAuthorizationCodeFlow},
[]string{oidc.ResponseModeFormPost, oidc.ResponseModeQuery},
[]string{oidc.GrantTypeAuthorizationCode},
},
nil,
[]string{
"identity_providers: oidc: client 'test': option 'scopes' must only have the values 'openid', 'email', 'profile', 'groups', or 'offline_access' but the values 'group' are present",
},
},
{
"ShouldRaiseErrorOnMissingAuthorizationCodeFlowResponseTypeWithRefreshTokenValues",
nil,
nil,
tcv{
[]string{oidc.ScopeOpenID, oidc.ScopeProfile, oidc.ScopeOfflineAccess},
[]string{oidc.ResponseTypeImplicitFlowBoth},
nil,
[]string{oidc.GrantTypeImplicit, oidc.GrantTypeRefreshToken},
},
tcv{
[]string{oidc.ScopeOpenID, oidc.ScopeProfile, oidc.ScopeOfflineAccess},
[]string{oidc.ResponseTypeImplicitFlowBoth},
[]string{oidc.ResponseModeFormPost, oidc.ResponseModeFragment},
[]string{oidc.GrantTypeImplicit, oidc.GrantTypeRefreshToken},
},
[]string{
"identity_providers: oidc: client 'test': option 'scopes' should only have the values 'offline_access' or 'offline' if the client is also configured with a 'response_type' such as 'code', 'code id_token', 'code token', or 'code id_token token' which respond with authorization codes",
"identity_providers: oidc: client 'test': option 'grant_types' should only have the values 'refresh_token' if the client is also configured with a 'response_type' such as 'code', 'code id_token', 'code token', or 'code id_token token' which respond with authorization codes",
},
nil,
},
{
"ShouldRaiseErrorOnDuplicateResponseTypes",
nil,
nil,
tcv{
nil,
[]string{oidc.ResponseTypeAuthorizationCodeFlow, oidc.ResponseTypeImplicitFlowBoth, oidc.ResponseTypeAuthorizationCodeFlow},
nil,
nil,
},
tcv{
[]string{oidc.ScopeOpenID, oidc.ScopeGroups, oidc.ScopeProfile, oidc.ScopeEmail},
[]string{oidc.ResponseTypeAuthorizationCodeFlow, oidc.ResponseTypeImplicitFlowBoth, oidc.ResponseTypeAuthorizationCodeFlow},
[]string{oidc.ResponseModeFormPost, oidc.ResponseModeQuery, oidc.ResponseModeFragment},
[]string{oidc.GrantTypeAuthorizationCode, oidc.GrantTypeImplicit},
},
[]string{
"identity_providers: oidc: client 'test': option 'response_types' must have unique values but the values 'code' are duplicated",
},
nil,
},
{
"ShouldRaiseErrorOnInvalidResponseTypesOrder",
nil,
nil,
tcv{
nil,
[]string{oidc.ResponseTypeImplicitFlowBoth, "token id_token"},
nil,
nil,
},
tcv{
[]string{oidc.ScopeOpenID, oidc.ScopeGroups, oidc.ScopeProfile, oidc.ScopeEmail},
[]string{oidc.ResponseTypeImplicitFlowBoth, "token id_token"},
[]string{"form_post", "fragment"},
[]string{"implicit"},
},
[]string{
"identity_providers: oidc: client 'test': option 'response_types' must only have the values 'code', 'id_token', 'token', 'id_token token', 'code id_token', 'code token', or 'code id_token token' but the values 'token id_token' are present",
},
nil,
},
{
"ShouldRaiseErrorOnInvalidResponseTypes",
nil,
nil,
tcv{
nil,
[]string{"not_valid"},
nil,
nil,
},
tcv{
[]string{oidc.ScopeOpenID, oidc.ScopeGroups, oidc.ScopeProfile, oidc.ScopeEmail},
[]string{"not_valid"},
[]string{oidc.ResponseModeFormPost},
nil,
},
[]string{
"identity_providers: oidc: client 'test': option 'response_types' must only have the values 'code', 'id_token', 'token', 'id_token token', 'code id_token', 'code token', or 'code id_token token' but the values 'not_valid' are present",
},
nil,
},
{
"ShouldRaiseErrorOnInvalidResponseModes",
nil,
nil,
tcv{
nil,
nil,
[]string{"not_valid"},
nil,
},
tcv{
[]string{oidc.ScopeOpenID, oidc.ScopeGroups, oidc.ScopeProfile, oidc.ScopeEmail},
[]string{oidc.ResponseTypeAuthorizationCodeFlow},
[]string{"not_valid"},
[]string{oidc.GrantTypeAuthorizationCode},
},
nil,
[]string{
"identity_providers: oidc: client 'test': option 'response_modes' must only have the values 'form_post', 'query', or 'fragment' but the values 'not_valid' are present",
},
},
{
"ShouldRaiseErrorOnDuplicateResponseModes",
nil,
nil,
tcv{
nil,
nil,
[]string{oidc.ResponseModeQuery, oidc.ResponseModeQuery},
nil,
},
tcv{
[]string{oidc.ScopeOpenID, oidc.ScopeGroups, oidc.ScopeProfile, oidc.ScopeEmail},
[]string{oidc.ResponseTypeAuthorizationCodeFlow},
[]string{oidc.ResponseModeQuery, oidc.ResponseModeQuery},
[]string{oidc.GrantTypeAuthorizationCode},
},
[]string{
"identity_providers: oidc: client 'test': option 'response_modes' must have unique values but the values 'query' are duplicated",
},
nil,
},
{
"ShouldRaiseErrorOnInvalidGrantTypes",
nil,
nil,
tcv{
nil,
nil,
nil,
[]string{"invalid"},
},
tcv{
[]string{oidc.ScopeOpenID, oidc.ScopeGroups, oidc.ScopeProfile, oidc.ScopeEmail},
[]string{oidc.ResponseTypeAuthorizationCodeFlow},
[]string{oidc.ResponseModeFormPost, oidc.ResponseModeQuery},
[]string{"invalid"},
},
nil,
[]string{
"identity_providers: oidc: client 'test': option 'grant_types' must only have the values 'implicit', 'refresh_token', or 'authorization_code' but the values 'invalid' are present",
},
},
{
"ShouldRaiseErrorOnDuplicateGrantTypes",
nil,
nil,
tcv{
nil,
nil,
nil,
[]string{oidc.GrantTypeAuthorizationCode, oidc.GrantTypeAuthorizationCode},
},
tcv{
[]string{oidc.ScopeOpenID, oidc.ScopeGroups, oidc.ScopeProfile, oidc.ScopeEmail},
[]string{oidc.ResponseTypeAuthorizationCodeFlow},
[]string{oidc.ResponseModeFormPost, oidc.ResponseModeQuery},
[]string{oidc.GrantTypeAuthorizationCode, oidc.GrantTypeAuthorizationCode},
},
[]string{
"identity_providers: oidc: client 'test': option 'grant_types' must have unique values but the values 'authorization_code' are duplicated",
},
nil,
},
{
"ShouldRaiseErrorOnGrantTypeRefreshTokenWithoutScopeOfflineAccess",
nil,
nil,
tcv{
nil,
nil,
nil,
[]string{oidc.GrantTypeAuthorizationCode, oidc.GrantTypeRefreshToken},
},
tcv{
[]string{oidc.ScopeOpenID, oidc.ScopeGroups, oidc.ScopeProfile, oidc.ScopeEmail},
[]string{oidc.ResponseTypeAuthorizationCodeFlow},
[]string{oidc.ResponseModeFormPost, oidc.ResponseModeQuery},
[]string{oidc.GrantTypeAuthorizationCode, oidc.GrantTypeRefreshToken},
},
[]string{
"identity_providers: oidc: client 'test': option 'grant_types' should only have the 'refresh_token' value if the client is also configured with the 'offline_access' scope",
},
nil,
},
{
"ShouldRaiseErrorOnGrantTypeAuthorizationCodeWithoutAuthorizationCodeOrHybridFlow",
nil,
nil,
tcv{
nil,
[]string{oidc.ResponseTypeImplicitFlowBoth},
nil,
[]string{oidc.GrantTypeAuthorizationCode},
},
tcv{
[]string{oidc.ScopeOpenID, oidc.ScopeGroups, oidc.ScopeProfile, oidc.ScopeEmail},
[]string{oidc.ResponseTypeImplicitFlowBoth},
[]string{"form_post", "fragment"},
[]string{oidc.GrantTypeAuthorizationCode},
},
[]string{
"identity_providers: oidc: client 'test': option 'grant_types' should only have grant type values which are valid with the configured 'response_types' for the client but 'authorization_code' expects a response type for either the authorization code or hybrid flow such as 'code', 'code id_token', 'code token', or 'code id_token token' but the response types are 'id_token token'",
},
nil,
},
{
"ShouldRaiseErrorOnGrantTypeImplicitWithoutImplicitOrHybridFlow",
nil,
nil,
tcv{
nil,
[]string{oidc.ResponseTypeAuthorizationCodeFlow},
nil,
[]string{oidc.GrantTypeImplicit},
},
tcv{
[]string{oidc.ScopeOpenID, oidc.ScopeGroups, oidc.ScopeProfile, oidc.ScopeEmail},
[]string{oidc.ResponseTypeAuthorizationCodeFlow},
[]string{oidc.ResponseModeFormPost, oidc.ResponseModeQuery},
[]string{oidc.GrantTypeImplicit},
},
[]string{
"identity_providers: oidc: client 'test': option 'grant_types' should only have grant type values which are valid with the configured 'response_types' for the client but 'implicit' expects a response type for either the implicit or hybrid flow such as 'id_token', 'token', 'id_token token', 'code id_token', 'code token', or 'code id_token token' but the response types are 'code'",
},
nil,
},
{
"ShouldValidateCorrectRedirectURIsConfidentialClientType",
func(have *schema.OpenIDConnectConfiguration) {
have.Clients[0].RedirectURIs = []string{
"https://google.com",
}
},
func(t *testing.T, have *schema.OpenIDConnectConfiguration) {
assert.Equal(t, []string{"https://google.com"}, have.Clients[0].RedirectURIs)
},
tcv{
nil,
nil,
nil,
nil,
},
tcv{
[]string{oidc.ScopeOpenID, oidc.ScopeGroups, oidc.ScopeProfile, oidc.ScopeEmail},
[]string{oidc.ResponseTypeAuthorizationCodeFlow},
[]string{oidc.ResponseModeFormPost, oidc.ResponseModeQuery},
[]string{oidc.GrantTypeAuthorizationCode},
},
nil,
nil,
},
{
"ShouldValidateCorrectRedirectURIsPublicClientType",
func(have *schema.OpenIDConnectConfiguration) {
have.Clients[0].Public = true
have.Clients[0].Secret = nil
have.Clients[0].RedirectURIs = []string{
oauth2InstalledApp,
}
},
func(t *testing.T, have *schema.OpenIDConnectConfiguration) {
assert.Equal(t, []string{oauth2InstalledApp}, have.Clients[0].RedirectURIs)
},
tcv{
nil,
nil,
nil,
nil,
},
tcv{
[]string{oidc.ScopeOpenID, oidc.ScopeGroups, oidc.ScopeProfile, oidc.ScopeEmail},
[]string{oidc.ResponseTypeAuthorizationCodeFlow},
[]string{oidc.ResponseModeFormPost, oidc.ResponseModeQuery},
[]string{oidc.GrantTypeAuthorizationCode},
},
nil,
nil,
},
{
"ShouldRaiseErrorOnInvalidRedirectURIsPublicOnly",
func(have *schema.OpenIDConnectConfiguration) {
have.Clients[0].RedirectURIs = []string{
"urn:ietf:wg:oauth:2.0:oob",
}
},
func(t *testing.T, have *schema.OpenIDConnectConfiguration) {
assert.Equal(t, []string{oauth2InstalledApp}, have.Clients[0].RedirectURIs)
},
tcv{
nil,
nil,
nil,
nil,
},
tcv{
[]string{oidc.ScopeOpenID, oidc.ScopeGroups, oidc.ScopeProfile, oidc.ScopeEmail},
[]string{oidc.ResponseTypeAuthorizationCodeFlow},
[]string{oidc.ResponseModeFormPost, oidc.ResponseModeQuery},
[]string{oidc.GrantTypeAuthorizationCode},
},
nil,
[]string{
"identity_providers: oidc: client 'test': option 'redirect_uris' has the redirect uri 'urn:ietf:wg:oauth:2.0:oob' when option 'public' is false but this is invalid as this uri is not valid for the openid connect confidential client type",
},
},
{
"ShouldRaiseErrorOnInvalidRedirectURIsMalformedURI",
func(have *schema.OpenIDConnectConfiguration) {
have.Clients[0].RedirectURIs = []string{
"http://abc@%two",
}
},
func(t *testing.T, have *schema.OpenIDConnectConfiguration) {
assert.Equal(t, []string{"http://abc@%two"}, have.Clients[0].RedirectURIs)
},
tcv{
nil,
nil,
nil,
nil,
},
tcv{
[]string{oidc.ScopeOpenID, oidc.ScopeGroups, oidc.ScopeProfile, oidc.ScopeEmail},
[]string{oidc.ResponseTypeAuthorizationCodeFlow},
[]string{oidc.ResponseModeFormPost, oidc.ResponseModeQuery},
[]string{oidc.GrantTypeAuthorizationCode},
},
nil,
[]string{
"identity_providers: oidc: client 'test': option 'redirect_uris' has an invalid value: redirect uri 'http://abc@%two' could not be parsed: parse \"http://abc@%two\": invalid URL escape \"%tw\"",
},
},
{
"ShouldRaiseErrorOnInvalidRedirectURIsNotAbsolute",
func(have *schema.OpenIDConnectConfiguration) {
have.Clients[0].RedirectURIs = []string{
"google.com",
}
},
func(t *testing.T, have *schema.OpenIDConnectConfiguration) {
assert.Equal(t, []string{"google.com"}, have.Clients[0].RedirectURIs)
},
tcv{
nil,
nil,
nil,
nil,
},
tcv{
[]string{oidc.ScopeOpenID, oidc.ScopeGroups, oidc.ScopeProfile, oidc.ScopeEmail},
[]string{oidc.ResponseTypeAuthorizationCodeFlow},
[]string{oidc.ResponseModeFormPost, oidc.ResponseModeQuery},
[]string{oidc.GrantTypeAuthorizationCode},
},
nil,
[]string{
"identity_providers: oidc: client 'test': option 'redirect_uris' has an invalid value: redirect uri 'google.com' must have a scheme but it's absent",
},
},
{
"ShouldRaiseErrorOnDuplicateRedirectURI",
func(have *schema.OpenIDConnectConfiguration) {
have.Clients[0].RedirectURIs = []string{
"https://google.com",
"https://google.com",
}
},
func(t *testing.T, have *schema.OpenIDConnectConfiguration) {
assert.Equal(t, []string{"https://google.com", "https://google.com"}, have.Clients[0].RedirectURIs)
},
tcv{
nil,
nil,
nil,
nil,
},
tcv{
[]string{oidc.ScopeOpenID, oidc.ScopeGroups, oidc.ScopeProfile, oidc.ScopeEmail},
[]string{oidc.ResponseTypeAuthorizationCodeFlow},
[]string{oidc.ResponseModeFormPost, oidc.ResponseModeQuery},
[]string{oidc.GrantTypeAuthorizationCode},
},
[]string{
"identity_providers: oidc: client 'test': option 'redirect_uris' must have unique values but the values 'https://google.com' are duplicated",
},
nil,
},
{
"ShouldNotSetDefaultTokenEndpointClientAuthMethodConfidentialClientType",
nil,
func(t *testing.T, have *schema.OpenIDConnectConfiguration) {
assert.Equal(t, "", have.Clients[0].TokenEndpointAuthMethod)
},
tcv{
nil,
nil,
nil,
nil,
},
tcv{
[]string{oidc.ScopeOpenID, oidc.ScopeGroups, oidc.ScopeProfile, oidc.ScopeEmail},
[]string{oidc.ResponseTypeAuthorizationCodeFlow},
[]string{oidc.ResponseModeFormPost, oidc.ResponseModeQuery},
[]string{oidc.GrantTypeAuthorizationCode},
},
nil,
nil,
},
{
"ShouldNotOverrideValidClientAuthMethod",
func(have *schema.OpenIDConnectConfiguration) {
have.Clients[0].TokenEndpointAuthMethod = oidc.ClientAuthMethodClientSecretPost
},
func(t *testing.T, have *schema.OpenIDConnectConfiguration) {
assert.Equal(t, oidc.ClientAuthMethodClientSecretPost, have.Clients[0].TokenEndpointAuthMethod)
},
tcv{
nil,
nil,
nil,
nil,
},
tcv{
[]string{oidc.ScopeOpenID, oidc.ScopeGroups, oidc.ScopeProfile, oidc.ScopeEmail},
[]string{oidc.ResponseTypeAuthorizationCodeFlow},
[]string{oidc.ResponseModeFormPost, oidc.ResponseModeQuery},
[]string{oidc.GrantTypeAuthorizationCode},
},
nil,
nil,
},
{
"ShouldRaiseErrorOnInvalidClientAuthMethod",
func(have *schema.OpenIDConnectConfiguration) {
have.Clients[0].TokenEndpointAuthMethod = "client_credentials"
},
func(t *testing.T, have *schema.OpenIDConnectConfiguration) {
assert.Equal(t, "client_credentials", have.Clients[0].TokenEndpointAuthMethod)
},
tcv{
nil,
nil,
nil,
nil,
},
tcv{
[]string{oidc.ScopeOpenID, oidc.ScopeGroups, oidc.ScopeProfile, oidc.ScopeEmail},
[]string{oidc.ResponseTypeAuthorizationCodeFlow},
[]string{oidc.ResponseModeFormPost, oidc.ResponseModeQuery},
[]string{oidc.GrantTypeAuthorizationCode},
},
nil,
[]string{
"identity_providers: oidc: client 'test': option 'token_endpoint_auth_method' must be one of 'none', 'client_secret_post', 'client_secret_basic', or 'client_secret_jwt' but it's configured as 'client_credentials'",
},
},
{
"ShouldRaiseErrorOnInvalidClientAuthMethodForPublicClientType",
func(have *schema.OpenIDConnectConfiguration) {
have.Clients[0].TokenEndpointAuthMethod = oidc.ClientAuthMethodClientSecretBasic
have.Clients[0].Public = true
have.Clients[0].Secret = nil
},
func(t *testing.T, have *schema.OpenIDConnectConfiguration) {
assert.Equal(t, oidc.ClientAuthMethodClientSecretBasic, have.Clients[0].TokenEndpointAuthMethod)
},
tcv{
nil,
nil,
nil,
nil,
},
tcv{
[]string{oidc.ScopeOpenID, oidc.ScopeGroups, oidc.ScopeProfile, oidc.ScopeEmail},
[]string{oidc.ResponseTypeAuthorizationCodeFlow},
[]string{oidc.ResponseModeFormPost, oidc.ResponseModeQuery},
[]string{oidc.GrantTypeAuthorizationCode},
},
nil,
[]string{
"identity_providers: oidc: client 'test': option 'token_endpoint_auth_method' must be 'none' when configured as the public client type but it's configured as 'client_secret_basic'",
},
},
{
"ShouldRaiseErrorOnInvalidClientAuthMethodForConfidentialClientTypeAuthorizationCodeFlow",
func(have *schema.OpenIDConnectConfiguration) {
have.Clients[0].TokenEndpointAuthMethod = oidc.ClientAuthMethodNone
},
func(t *testing.T, have *schema.OpenIDConnectConfiguration) {
assert.Equal(t, oidc.ClientAuthMethodNone, have.Clients[0].TokenEndpointAuthMethod)
},
tcv{
nil,
nil,
nil,
nil,
},
tcv{
[]string{oidc.ScopeOpenID, oidc.ScopeGroups, oidc.ScopeProfile, oidc.ScopeEmail},
[]string{oidc.ResponseTypeAuthorizationCodeFlow},
[]string{oidc.ResponseModeFormPost, oidc.ResponseModeQuery},
[]string{oidc.GrantTypeAuthorizationCode},
},
nil,
[]string{
"identity_providers: oidc: client 'test': option 'token_endpoint_auth_method' must be one of 'client_secret_post' or 'client_secret_basic' when configured as the confidential client type unless it only includes implicit flow response types such as 'id_token', 'token', and 'id_token token' but it's configured as 'none'",
},
},
{
"ShouldRaiseErrorOnInvalidClientAuthMethodForConfidentialClientTypeHybridFlow",
func(have *schema.OpenIDConnectConfiguration) {
have.Clients[0].TokenEndpointAuthMethod = oidc.ClientAuthMethodNone
},
func(t *testing.T, have *schema.OpenIDConnectConfiguration) {
assert.Equal(t, oidc.ClientAuthMethodNone, have.Clients[0].TokenEndpointAuthMethod)
},
tcv{
nil,
[]string{oidc.ResponseTypeHybridFlowToken},
nil,
nil,
},
tcv{
[]string{oidc.ScopeOpenID, oidc.ScopeGroups, oidc.ScopeProfile, oidc.ScopeEmail},
[]string{oidc.ResponseTypeHybridFlowToken},
[]string{oidc.ResponseModeFormPost, oidc.ResponseModeFragment},
[]string{oidc.GrantTypeAuthorizationCode, oidc.GrantTypeImplicit},
},
nil,
[]string{
"identity_providers: oidc: client 'test': option 'token_endpoint_auth_method' must be one of 'client_secret_post' or 'client_secret_basic' when configured as the confidential client type unless it only includes implicit flow response types such as 'id_token', 'token', and 'id_token token' but it's configured as 'none'",
},
},
{
"ShouldSetDefaultUserInfoAlg",
nil,
func(t *testing.T, have *schema.OpenIDConnectConfiguration) {
assert.Equal(t, oidc.SigningAlgNone, have.Clients[0].UserinfoSigningAlg)
},
tcv{
nil,
nil,
nil,
nil,
},
tcv{
[]string{oidc.ScopeOpenID, oidc.ScopeGroups, oidc.ScopeProfile, oidc.ScopeEmail},
[]string{oidc.ResponseTypeAuthorizationCodeFlow},
[]string{oidc.ResponseModeFormPost, oidc.ResponseModeQuery},
[]string{oidc.GrantTypeAuthorizationCode},
},
nil,
nil,
},
{
"ShouldNotOverrideUserInfoAlg",
func(have *schema.OpenIDConnectConfiguration) {
have.Clients[0].UserinfoSigningAlg = oidc.SigningAlgRSAUsingSHA256
},
func(t *testing.T, have *schema.OpenIDConnectConfiguration) {
assert.Equal(t, oidc.SigningAlgRSAUsingSHA256, have.Clients[0].UserinfoSigningAlg)
},
tcv{
nil,
nil,
nil,
nil,
},
tcv{
[]string{oidc.ScopeOpenID, oidc.ScopeGroups, oidc.ScopeProfile, oidc.ScopeEmail},
[]string{oidc.ResponseTypeAuthorizationCodeFlow},
[]string{oidc.ResponseModeFormPost, oidc.ResponseModeQuery},
[]string{oidc.GrantTypeAuthorizationCode},
},
nil,
nil,
},
{
"ShouldRaiseErrorOnInvalidUserInfoAlg",
func(have *schema.OpenIDConnectConfiguration) {
have.Clients[0].UserinfoSigningAlg = "rs256"
},
func(t *testing.T, have *schema.OpenIDConnectConfiguration) {
assert.Equal(t, "rs256", have.Clients[0].UserinfoSigningAlg)
},
tcv{
nil,
nil,
nil,
nil,
},
tcv{
[]string{oidc.ScopeOpenID, oidc.ScopeGroups, oidc.ScopeProfile, oidc.ScopeEmail},
[]string{oidc.ResponseTypeAuthorizationCodeFlow},
[]string{oidc.ResponseModeFormPost, oidc.ResponseModeQuery},
[]string{oidc.GrantTypeAuthorizationCode},
},
nil,
[]string{
"identity_providers: oidc: client 'test': option 'userinfo_signing_algorithm' must be one of 'RS256' or 'none' but it's configured as 'rs256'",
},
},
{
"ShouldSetDefaultConsentMode",
nil,
func(t *testing.T, have *schema.OpenIDConnectConfiguration) {
assert.Equal(t, "explicit", have.Clients[0].ConsentMode)
},
tcv{
nil,
nil,
nil,
nil,
},
tcv{
[]string{oidc.ScopeOpenID, oidc.ScopeGroups, oidc.ScopeProfile, oidc.ScopeEmail},
[]string{oidc.ResponseTypeAuthorizationCodeFlow},
[]string{oidc.ResponseModeFormPost, oidc.ResponseModeQuery},
[]string{oidc.GrantTypeAuthorizationCode},
},
nil,
nil,
},
{
"ShouldSetDefaultConsentModeAuto",
func(have *schema.OpenIDConnectConfiguration) {
have.Clients[0].ConsentMode = auto
},
func(t *testing.T, have *schema.OpenIDConnectConfiguration) {
assert.Equal(t, "explicit", have.Clients[0].ConsentMode)
},
tcv{
nil,
nil,
nil,
nil,
},
tcv{
[]string{oidc.ScopeOpenID, oidc.ScopeGroups, oidc.ScopeProfile, oidc.ScopeEmail},
[]string{oidc.ResponseTypeAuthorizationCodeFlow},
[]string{oidc.ResponseModeFormPost, oidc.ResponseModeQuery},
[]string{oidc.GrantTypeAuthorizationCode},
},
nil,
nil,
},
{
"ShouldSetDefaultConsentModePreConfigured",
func(have *schema.OpenIDConnectConfiguration) {
d := time.Minute
have.Clients[0].ConsentMode = ""
have.Clients[0].ConsentPreConfiguredDuration = &d
},
func(t *testing.T, have *schema.OpenIDConnectConfiguration) {
assert.Equal(t, "pre-configured", have.Clients[0].ConsentMode)
},
tcv{
nil,
nil,
nil,
nil,
},
tcv{
[]string{oidc.ScopeOpenID, oidc.ScopeGroups, oidc.ScopeProfile, oidc.ScopeEmail},
[]string{oidc.ResponseTypeAuthorizationCodeFlow},
[]string{oidc.ResponseModeFormPost, oidc.ResponseModeQuery},
[]string{oidc.GrantTypeAuthorizationCode},
},
nil,
nil,
},
{
"ShouldSetDefaultConsentModeAutoPreConfigured",
func(have *schema.OpenIDConnectConfiguration) {
d := time.Minute
have.Clients[0].ConsentMode = auto
have.Clients[0].ConsentPreConfiguredDuration = &d
},
func(t *testing.T, have *schema.OpenIDConnectConfiguration) {
assert.Equal(t, "pre-configured", have.Clients[0].ConsentMode)
},
tcv{
nil,
nil,
nil,
nil,
},
tcv{
[]string{oidc.ScopeOpenID, oidc.ScopeGroups, oidc.ScopeProfile, oidc.ScopeEmail},
[]string{oidc.ResponseTypeAuthorizationCodeFlow},
[]string{oidc.ResponseModeFormPost, oidc.ResponseModeQuery},
[]string{oidc.GrantTypeAuthorizationCode},
},
nil,
nil,
},
{
"ShouldNotOverrideConsentMode",
func(have *schema.OpenIDConnectConfiguration) {
have.Clients[0].ConsentMode = "implicit"
},
func(t *testing.T, have *schema.OpenIDConnectConfiguration) {
assert.Equal(t, "implicit", have.Clients[0].ConsentMode)
},
tcv{
nil,
nil,
nil,
nil,
},
tcv{
[]string{oidc.ScopeOpenID, oidc.ScopeGroups, oidc.ScopeProfile, oidc.ScopeEmail},
[]string{oidc.ResponseTypeAuthorizationCodeFlow},
[]string{oidc.ResponseModeFormPost, oidc.ResponseModeQuery},
[]string{oidc.GrantTypeAuthorizationCode},
},
nil,
nil,
},
{
"ShouldSentConsentPreConfiguredDefaultDuration",
func(have *schema.OpenIDConnectConfiguration) {
have.Clients[0].ConsentMode = "pre-configured"
},
func(t *testing.T, have *schema.OpenIDConnectConfiguration) {
assert.Equal(t, "pre-configured", have.Clients[0].ConsentMode)
assert.Equal(t, schema.DefaultOpenIDConnectClientConfiguration.ConsentPreConfiguredDuration, have.Clients[0].ConsentPreConfiguredDuration)
},
tcv{
nil,
nil,
nil,
nil,
},
tcv{
[]string{oidc.ScopeOpenID, oidc.ScopeGroups, oidc.ScopeProfile, oidc.ScopeEmail},
[]string{oidc.ResponseTypeAuthorizationCodeFlow},
[]string{oidc.ResponseModeFormPost, oidc.ResponseModeQuery},
[]string{oidc.GrantTypeAuthorizationCode},
},
nil,
nil,
},
{
"ShouldRaiseErrorOnIncorrectlyConfiguredTokenEndpointClientAuthMethodClientSecretJWT",
func(have *schema.OpenIDConnectConfiguration) {
have.Clients[0].TokenEndpointAuthMethod = oidc.ClientAuthMethodClientSecretJWT
have.Clients[0].Secret = tOpenIDConnectPBKDF2ClientSecret
},
nil,
tcv{
nil,
nil,
nil,
nil,
},
tcv{
[]string{oidc.ScopeOpenID, oidc.ScopeGroups, oidc.ScopeProfile, oidc.ScopeEmail},
[]string{oidc.ResponseTypeAuthorizationCodeFlow},
[]string{oidc.ResponseModeFormPost, oidc.ResponseModeQuery},
[]string{oidc.GrantTypeAuthorizationCode},
},
nil,
[]string{
"identity_providers: oidc: client 'test': option 'secret' must be plaintext with option 'token_endpoint_auth_method' with a value of 'client_secret_jwt'",
},
},
{
"ShouldNotRaiseWarningOrErrorOnCorrectlyConfiguredTokenEndpointClientAuthMethodClientSecretJWT",
func(have *schema.OpenIDConnectConfiguration) {
have.Clients[0].TokenEndpointAuthMethod = oidc.ClientAuthMethodClientSecretJWT
have.Clients[0].Secret = tOpenIDConnectPlainTextClientSecret
},
nil,
tcv{
nil,
nil,
nil,
nil,
},
tcv{
[]string{oidc.ScopeOpenID, oidc.ScopeGroups, oidc.ScopeProfile, oidc.ScopeEmail},
[]string{oidc.ResponseTypeAuthorizationCodeFlow},
[]string{oidc.ResponseModeFormPost, oidc.ResponseModeQuery},
[]string{oidc.GrantTypeAuthorizationCode},
},
nil,
nil,
},
{
"ShouldRaiseErrorOnIncorrectlyConfiguredTokenEndpointClientAuthMethodClientSecretJWT",
func(have *schema.OpenIDConnectConfiguration) {
have.Clients[0].TokenEndpointAuthMethod = oidc.ClientAuthMethodClientSecretJWT
have.Clients[0].Secret = MustDecodeSecret("$pbkdf2-sha512$310000$c8p78n7pUMln0jzvd4aK4Q$JNRBzwAo0ek5qKn50cFzzvE9RXV88h1wJn5KGiHrD0YKtZaR/nCb2CJPOsKaPK0hjf.9yHxzQGZziziccp6Yng")
},
nil,
tcv{
nil,
nil,
nil,
nil,
},
tcv{
[]string{oidc.ScopeOpenID, oidc.ScopeGroups, oidc.ScopeProfile, oidc.ScopeEmail},
[]string{oidc.ResponseTypeAuthorizationCodeFlow},
[]string{oidc.ResponseModeFormPost, oidc.ResponseModeQuery},
[]string{oidc.GrantTypeAuthorizationCode},
},
nil,
[]string{
"identity_providers: oidc: client 'test': option 'secret' must be plaintext with option 'token_endpoint_auth_method' with a value of 'client_secret_jwt'",
},
},
{
"ShouldNotRaiseWarningOrErrorOnCorrectlyConfiguredTokenEndpointClientAuthMethodClientSecretJWT",
func(have *schema.OpenIDConnectConfiguration) {
have.Clients[0].TokenEndpointAuthMethod = oidc.ClientAuthMethodClientSecretJWT
have.Clients[0].Secret = MustDecodeSecret("$plaintext$abc123")
},
nil,
tcv{
nil,
nil,
nil,
nil,
},
tcv{
[]string{oidc.ScopeOpenID, oidc.ScopeGroups, oidc.ScopeProfile, oidc.ScopeEmail},
[]string{oidc.ResponseTypeAuthorizationCodeFlow},
[]string{oidc.ResponseModeFormPost, oidc.ResponseModeQuery},
[]string{oidc.GrantTypeAuthorizationCode},
},
nil,
nil,
},
{
"ShouldSetDefaultTokenEndpointAuthSigAlg",
func(have *schema.OpenIDConnectConfiguration) {
have.Clients[0].TokenEndpointAuthMethod = oidc.ClientAuthMethodClientSecretJWT
have.Clients[0].Secret = tOpenIDConnectPlainTextClientSecret
},
func(t *testing.T, have *schema.OpenIDConnectConfiguration) {
assert.Equal(t, oidc.SigningAlgHMACUsingSHA256, have.Clients[0].TokenEndpointAuthSigningAlg)
},
tcv{
nil,
nil,
nil,
nil,
},
tcv{
[]string{oidc.ScopeOpenID, oidc.ScopeGroups, oidc.ScopeProfile, oidc.ScopeEmail},
[]string{oidc.ResponseTypeAuthorizationCodeFlow},
[]string{oidc.ResponseModeFormPost, oidc.ResponseModeQuery},
[]string{oidc.GrantTypeAuthorizationCode},
},
nil,
nil,
},
{
"ShouldRaiseErrorOnInvalidPublicTokenAuthAlg",
func(have *schema.OpenIDConnectConfiguration) {
have.Clients[0].TokenEndpointAuthMethod = oidc.ClientAuthMethodClientSecretJWT
have.Clients[0].TokenEndpointAuthSigningAlg = oidc.SigningAlgHMACUsingSHA256
have.Clients[0].Secret = nil
have.Clients[0].Public = true
},
func(t *testing.T, have *schema.OpenIDConnectConfiguration) {
assert.Equal(t, oidc.SigningAlgHMACUsingSHA256, have.Clients[0].TokenEndpointAuthSigningAlg)
},
tcv{
nil,
nil,
nil,
nil,
},
tcv{
[]string{oidc.ScopeOpenID, oidc.ScopeGroups, oidc.ScopeProfile, oidc.ScopeEmail},
[]string{oidc.ResponseTypeAuthorizationCodeFlow},
[]string{oidc.ResponseModeFormPost, oidc.ResponseModeQuery},
[]string{oidc.GrantTypeAuthorizationCode},
},
nil,
[]string{
"identity_providers: oidc: client 'test': option 'token_endpoint_auth_method' must be 'none' when configured as the public client type but it's configured as 'client_secret_jwt'",
},
},
{
"ShouldRaiseErrorOnInvalidTokenAuthAlgClientTypeConfidential",
func(have *schema.OpenIDConnectConfiguration) {
have.Clients[0].TokenEndpointAuthMethod = oidc.ClientAuthMethodClientSecretJWT
have.Clients[0].TokenEndpointAuthSigningAlg = oidc.EndpointToken
have.Clients[0].Secret = tOpenIDConnectPlainTextClientSecret
},
func(t *testing.T, have *schema.OpenIDConnectConfiguration) {
assert.Equal(t, oidc.EndpointToken, have.Clients[0].TokenEndpointAuthSigningAlg)
},
tcv{
nil,
nil,
nil,
nil,
},
tcv{
[]string{oidc.ScopeOpenID, oidc.ScopeGroups, oidc.ScopeProfile, oidc.ScopeEmail},
[]string{oidc.ResponseTypeAuthorizationCodeFlow},
[]string{oidc.ResponseModeFormPost, oidc.ResponseModeQuery},
[]string{oidc.GrantTypeAuthorizationCode},
},
nil,
[]string{
"identity_providers: oidc: client 'test': option 'token_endpoint_auth_signing_alg' must be 'HS256', 'HS384', or 'HS512' when option 'token_endpoint_auth_method' is client_secret_jwt",
},
},
}
errDeprecatedFunc := func() {}
for _, tc := range testCasses {
t.Run(tc.name, func(t *testing.T) {
have := &schema.OpenIDConnectConfiguration{
Discovery: schema.OpenIDConnectDiscovery{
RegisteredJWKSigningAlgs: []string{oidc.SigningAlgRSAUsingSHA256},
},
Clients: []schema.OpenIDConnectClientConfiguration{
{
ID: "test",
Secret: tOpenIDConnectPBKDF2ClientSecret,
Scopes: tc.have.Scopes,
ResponseModes: tc.have.ResponseModes,
ResponseTypes: tc.have.ResponseTypes,
GrantTypes: tc.have.GrantTypes,
},
},
}
if tc.setup != nil {
tc.setup(have)
}
val := schema.NewStructValidator()
validateOIDCClient(0, have, val, errDeprecatedFunc)
t.Run("General", func(t *testing.T) {
assert.Equal(t, tc.expected.Scopes, have.Clients[0].Scopes)
assert.Equal(t, tc.expected.ResponseTypes, have.Clients[0].ResponseTypes)
assert.Equal(t, tc.expected.ResponseModes, have.Clients[0].ResponseModes)
assert.Equal(t, tc.expected.GrantTypes, have.Clients[0].GrantTypes)
if tc.validate != nil {
tc.validate(t, have)
}
})
t.Run("Warnings", func(t *testing.T) {
require.Len(t, val.Warnings(), len(tc.serrs))
for i, err := range tc.serrs {
assert.EqualError(t, val.Warnings()[i], err)
}
})
t.Run("Errors", func(t *testing.T) {
require.Len(t, val.Errors(), len(tc.errs))
for i, err := range tc.errs {
assert.EqualError(t, val.Errors()[i], err)
}
})
})
}
}
func TestValidateOIDCClientTokenEndpointAuthMethod(t *testing.T) {
testCasses := []struct {
name string
have string
public bool
expected string
errs []string
}{
{"ShouldSetDefaultValueConfidential", "", false, "", nil},
{"ShouldErrorOnInvalidValue", "abc", false, "abc",
[]string{
"identity_providers: oidc: client 'test': option 'token_endpoint_auth_method' must be one of 'none', 'client_secret_post', 'client_secret_basic', or 'client_secret_jwt' but it's configured as 'abc'",
},
},
{"ShouldErrorOnInvalidValueForPublicClient", "client_secret_post", true, "client_secret_post",
[]string{
"identity_providers: oidc: client 'test': option 'token_endpoint_auth_method' must be 'none' when configured as the public client type but it's configured as 'client_secret_post'",
},
},
{"ShouldErrorOnInvalidValueForConfidentialClient", "none", false, "none",
[]string{
"identity_providers: oidc: client 'test': option 'token_endpoint_auth_method' must be one of 'client_secret_post' or 'client_secret_basic' when configured as the confidential client type unless it only includes implicit flow response types such as 'id_token', 'token', and 'id_token token' but it's configured as 'none'",
},
},
}
for _, tc := range testCasses {
t.Run(tc.name, func(t *testing.T) {
have := &schema.OpenIDConnectConfiguration{
Clients: []schema.OpenIDConnectClientConfiguration{
{
ID: "test",
Public: tc.public,
TokenEndpointAuthMethod: tc.have,
},
},
}
val := schema.NewStructValidator()
validateOIDCClientTokenEndpointAuth(0, have, val)
assert.Equal(t, tc.expected, have.Clients[0].TokenEndpointAuthMethod)
assert.Len(t, val.Warnings(), 0)
require.Len(t, val.Errors(), len(tc.errs))
if tc.errs != nil {
for i, err := range tc.errs {
assert.EqualError(t, val.Errors()[i], err)
}
}
})
}
}
func TestValidateOIDCIssuer(t *testing.T) {
frankenchain := schema.NewX509CertificateChainFromCerts([]*x509.Certificate{certRSA2048.Leaf(), certRSA1024.Leaf()})
frankenkey := &rsa.PrivateKey{}
*frankenkey = *keyRSA2048
frankenkey.PublicKey.N = nil
testCases := []struct {
name string
have schema.OpenIDConnectConfiguration
expected schema.OpenIDConnectConfiguration
errs []string
}{
{
"ShouldMapLegacyConfiguration",
schema.OpenIDConnectConfiguration{
IssuerPrivateKey: keyRSA2048,
},
schema.OpenIDConnectConfiguration{
IssuerPrivateKey: keyRSA2048,
IssuerJWKS: []schema.JWK{
{KeyID: "e7dfdc", Key: keyRSA2048, Algorithm: oidc.SigningAlgRSAUsingSHA256, Use: oidc.KeyUseSignature},
},
Discovery: schema.OpenIDConnectDiscovery{
DefaultKeyID: "e7dfdc",
RegisteredJWKSigningAlgs: []string{oidc.SigningAlgRSAUsingSHA256},
},
},
nil,
},
{
"ShouldSetDefaultKeyValues",
schema.OpenIDConnectConfiguration{
IssuerJWKS: []schema.JWK{
{Key: keyRSA2048, CertificateChain: certRSA2048},
{Key: keyECDSAP256, CertificateChain: certECDSAP256},
{Key: keyECDSAP384, CertificateChain: certECDSAP384},
{Key: keyECDSAP521, CertificateChain: certECDSAP521},
},
},
schema.OpenIDConnectConfiguration{
IssuerJWKS: []schema.JWK{
{Key: keyRSA2048, CertificateChain: certRSA2048, Algorithm: oidc.SigningAlgRSAUsingSHA256, Use: oidc.KeyUseSignature, KeyID: "e7dfdc"},
{Key: keyECDSAP256, CertificateChain: certECDSAP256, Algorithm: oidc.SigningAlgECDSAUsingP256AndSHA256, Use: oidc.KeyUseSignature, KeyID: "29b3f2"},
{Key: keyECDSAP384, CertificateChain: certECDSAP384, Algorithm: oidc.SigningAlgECDSAUsingP384AndSHA384, Use: oidc.KeyUseSignature, KeyID: "e968b4"},
{Key: keyECDSAP521, CertificateChain: certECDSAP521, Algorithm: oidc.SigningAlgECDSAUsingP521AndSHA512, Use: oidc.KeyUseSignature, KeyID: "6b20c3"},
},
Discovery: schema.OpenIDConnectDiscovery{
DefaultKeyID: "e7dfdc",
RegisteredJWKSigningAlgs: []string{oidc.SigningAlgRSAUsingSHA256, oidc.SigningAlgECDSAUsingP256AndSHA256, oidc.SigningAlgECDSAUsingP384AndSHA384, oidc.SigningAlgECDSAUsingP521AndSHA512},
},
},
nil,
},
{
"ShouldRaiseErrorsDuplicateRSA256Keys",
schema.OpenIDConnectConfiguration{
IssuerJWKS: []schema.JWK{
{Key: keyRSA2048, CertificateChain: certRSA2048},
{Key: keyRSA4096, CertificateChain: certRSA4096},
},
},
schema.OpenIDConnectConfiguration{
IssuerJWKS: []schema.JWK{
{Key: keyRSA2048, CertificateChain: certRSA2048, Algorithm: oidc.SigningAlgRSAUsingSHA256, Use: oidc.KeyUseSignature, KeyID: "e7dfdc"},
{Key: keyRSA4096, CertificateChain: certRSA4096, Algorithm: oidc.SigningAlgRSAUsingSHA256, Use: oidc.KeyUseSignature, KeyID: "9c7423"},
},
Discovery: schema.OpenIDConnectDiscovery{
DefaultKeyID: "e7dfdc",
RegisteredJWKSigningAlgs: []string{oidc.SigningAlgRSAUsingSHA256},
},
},
[]string{
"identity_providers: oidc: issuer_jwks: key #2 with key id '9c7423': option 'algorithm' must be unique but another key is using it",
},
},
{
"ShouldRaiseErrorsDuplicateRSA256Keys",
schema.OpenIDConnectConfiguration{
IssuerJWKS: []schema.JWK{
{Key: keyRSA4096, CertificateChain: certRSA4096, Algorithm: oidc.SigningAlgRSAUsingSHA512},
},
},
schema.OpenIDConnectConfiguration{
IssuerJWKS: []schema.JWK{
{Key: keyRSA4096, CertificateChain: certRSA4096, Algorithm: oidc.SigningAlgRSAUsingSHA512, Use: oidc.KeyUseSignature, KeyID: "9c7423"},
},
Discovery: schema.OpenIDConnectDiscovery{
DefaultKeyID: "",
RegisteredJWKSigningAlgs: []string{oidc.SigningAlgRSAUsingSHA512},
},
},
[]string{
"identity_providers: oidc: issuer_jwks: keys: must at least have one key supporting the 'RS256' algorithm but only has 'RS512'",
},
},
{
"ShouldRaiseErrorOnBadCurve",
schema.OpenIDConnectConfiguration{
IssuerJWKS: []schema.JWK{
{Key: keyRSA4096, CertificateChain: certRSA4096},
{Key: keyECDSAP224, CertificateChain: certECDSAP224},
},
},
schema.OpenIDConnectConfiguration{
IssuerJWKS: []schema.JWK{
{Key: keyRSA4096, CertificateChain: certRSA4096, Algorithm: oidc.SigningAlgRSAUsingSHA256, Use: oidc.KeyUseSignature, KeyID: "9c7423"},
{Key: keyECDSAP224, CertificateChain: certECDSAP224},
},
Discovery: schema.OpenIDConnectDiscovery{
DefaultKeyID: "9c7423",
RegisteredJWKSigningAlgs: []string{oidc.SigningAlgRSAUsingSHA256},
},
},
[]string{
"identity_providers: oidc: issuer_jwks: key #2: option 'key' failed to calculate thumbprint to configure key id value: square/go-jose: unsupported/unknown elliptic curve",
},
},
{
"ShouldRaiseErrorOnBadRSAKey",
schema.OpenIDConnectConfiguration{
IssuerJWKS: []schema.JWK{
{Key: keyRSA1024, CertificateChain: certRSA1024},
},
},
schema.OpenIDConnectConfiguration{
IssuerJWKS: []schema.JWK{
{Key: keyRSA1024, CertificateChain: certRSA1024, Algorithm: oidc.SigningAlgRSAUsingSHA256, Use: oidc.KeyUseSignature, KeyID: "a9c018"},
},
Discovery: schema.OpenIDConnectDiscovery{
DefaultKeyID: "a9c018",
RegisteredJWKSigningAlgs: []string{oidc.SigningAlgRSAUsingSHA256},
},
},
[]string{
"identity_providers: oidc: issuer_jwks: key #1 with key id 'a9c018': option 'key' is an RSA 1024 bit private key but it must be a RSA 2048 bit private key",
},
},
{
"ShouldRaiseErrorOnBadAlg",
schema.OpenIDConnectConfiguration{
IssuerJWKS: []schema.JWK{
{Key: keyRSA4096, CertificateChain: certRSA4096, Algorithm: "invalid"},
},
},
schema.OpenIDConnectConfiguration{
IssuerJWKS: []schema.JWK{
{Key: keyRSA4096, CertificateChain: certRSA4096, Algorithm: "invalid", Use: oidc.KeyUseSignature, KeyID: "9c7423"},
},
Discovery: schema.OpenIDConnectDiscovery{
DefaultKeyID: "",
RegisteredJWKSigningAlgs: []string{"invalid"},
},
},
[]string{
"identity_providers: oidc: issuer_jwks: key #1 with key id '9c7423': option 'algorithm' must be one of 'RS256', 'PS256', 'ES256', 'RS384', 'PS384', 'ES384', 'RS512', 'PS512', or 'ES512' but it's configured as 'invalid'",
"identity_providers: oidc: issuer_jwks: keys: must at least have one key supporting the 'RS256' algorithm but only has 'invalid'",
},
},
{
"ShouldRaiseErrorOnBadUse",
schema.OpenIDConnectConfiguration{
IssuerJWKS: []schema.JWK{
{Key: keyRSA4096, CertificateChain: certRSA4096, Use: "invalid"},
},
},
schema.OpenIDConnectConfiguration{
IssuerJWKS: []schema.JWK{
{Key: keyRSA4096, CertificateChain: certRSA4096, Algorithm: oidc.SigningAlgRSAUsingSHA256, Use: "invalid", KeyID: "9c7423"},
},
Discovery: schema.OpenIDConnectDiscovery{
DefaultKeyID: "9c7423",
RegisteredJWKSigningAlgs: []string{oidc.SigningAlgRSAUsingSHA256},
},
},
[]string{
"identity_providers: oidc: issuer_jwks: key #1 with key id '9c7423': option 'use' must be one of 'sig' but it's configured as 'invalid'",
},
},
{
"ShouldRaiseErrorOnBadKeyIDLength",
schema.OpenIDConnectConfiguration{
IssuerJWKS: []schema.JWK{
{Key: keyRSA4096, CertificateChain: certRSA4096, KeyID: "thisistoolong"},
},
},
schema.OpenIDConnectConfiguration{
IssuerJWKS: []schema.JWK{
{Key: keyRSA4096, CertificateChain: certRSA4096, Algorithm: oidc.SigningAlgRSAUsingSHA256, Use: oidc.KeyUseSignature, KeyID: "thisistoolong"},
},
Discovery: schema.OpenIDConnectDiscovery{
DefaultKeyID: "thisistoolong",
RegisteredJWKSigningAlgs: []string{oidc.SigningAlgRSAUsingSHA256},
},
},
[]string{
"identity_providers: oidc: issuer_jwks: key #1 with key id 'thisistoolong': option `key_id`` must be 7 characters or less",
},
},
{
"ShouldRaiseErrorOnBadKeyIDCharacters",
schema.OpenIDConnectConfiguration{
IssuerJWKS: []schema.JWK{
{Key: keyRSA4096, CertificateChain: certRSA4096, KeyID: "x@x"},
},
},
schema.OpenIDConnectConfiguration{
IssuerJWKS: []schema.JWK{
{Key: keyRSA4096, CertificateChain: certRSA4096, Algorithm: oidc.SigningAlgRSAUsingSHA256, Use: oidc.KeyUseSignature, KeyID: "x@x"},
},
Discovery: schema.OpenIDConnectDiscovery{
DefaultKeyID: "x@x",
RegisteredJWKSigningAlgs: []string{oidc.SigningAlgRSAUsingSHA256},
},
},
[]string{
"identity_providers: oidc: issuer_jwks: key #1 with key id 'x@x': option 'key_id' must only have alphanumeric characters",
},
},
{
"ShouldRaiseErrorOnBadKeyIDDuplicates",
schema.OpenIDConnectConfiguration{
IssuerJWKS: []schema.JWK{
{Key: keyRSA4096, CertificateChain: certRSA4096, KeyID: "x"},
{Key: keyRSA2048, CertificateChain: certRSA2048, Algorithm: oidc.SigningAlgRSAPSSUsingSHA256, KeyID: "x"},
},
},
schema.OpenIDConnectConfiguration{
IssuerJWKS: []schema.JWK{
{Key: keyRSA4096, CertificateChain: certRSA4096, Algorithm: oidc.SigningAlgRSAUsingSHA256, Use: oidc.KeyUseSignature, KeyID: "x"},
{Key: keyRSA2048, CertificateChain: certRSA2048, Algorithm: oidc.SigningAlgRSAPSSUsingSHA256, Use: oidc.KeyUseSignature, KeyID: "x"},
},
Discovery: schema.OpenIDConnectDiscovery{
DefaultKeyID: "x",
RegisteredJWKSigningAlgs: []string{oidc.SigningAlgRSAUsingSHA256, oidc.SigningAlgRSAPSSUsingSHA256},
},
},
[]string{
"identity_providers: oidc: issuer_jwks: key #2 with key id 'x': option 'key_id' must be unique",
},
},
{
"ShouldRaiseErrorOnEd25519Keys",
schema.OpenIDConnectConfiguration{
IssuerJWKS: []schema.JWK{
{Key: keyEd2519, CertificateChain: certEd15519},
},
},
schema.OpenIDConnectConfiguration{
IssuerJWKS: []schema.JWK{
{Key: keyEd2519, CertificateChain: certEd15519, KeyID: "d2dd94"},
},
Discovery: schema.OpenIDConnectDiscovery{
DefaultKeyID: "",
RegisteredJWKSigningAlgs: []string(nil),
},
},
[]string{
"identity_providers: oidc: issuer_jwks: key #1 with key id 'd2dd94': option 'key' must be a *rsa.PrivateKey or *ecdsa.PrivateKey but it's a ed25519.PrivateKey",
},
},
{
"ShouldRaiseErrorOnCertificateAsKey",
schema.OpenIDConnectConfiguration{
IssuerJWKS: []schema.JWK{
{Key: publicRSA2048Pair},
},
},
schema.OpenIDConnectConfiguration{
IssuerJWKS: []schema.JWK{
{Key: publicRSA2048Pair, Algorithm: oidc.SigningAlgRSAUsingSHA256, Use: oidc.KeyUseSignature, KeyID: "904c62"},
},
Discovery: schema.OpenIDConnectDiscovery{
DefaultKeyID: "904c62",
RegisteredJWKSigningAlgs: []string{oidc.SigningAlgRSAUsingSHA256},
},
},
[]string{
"identity_providers: oidc: issuer_jwks: key #1 with key id '904c62': option 'key' must be a *rsa.PrivateKey or *ecdsa.PrivateKey but it's a *rsa.PublicKey",
},
},
{
"ShouldRaiseErrorOnInvalidChain",
schema.OpenIDConnectConfiguration{
IssuerJWKS: []schema.JWK{
{Key: keyRSA2048, CertificateChain: frankenchain},
},
},
schema.OpenIDConnectConfiguration{
IssuerJWKS: []schema.JWK{
{Key: keyRSA2048, CertificateChain: frankenchain, Algorithm: oidc.SigningAlgRSAUsingSHA256, Use: oidc.KeyUseSignature, KeyID: "e7dfdc"},
},
Discovery: schema.OpenIDConnectDiscovery{
DefaultKeyID: "e7dfdc",
RegisteredJWKSigningAlgs: []string{oidc.SigningAlgRSAUsingSHA256},
},
},
[]string{
"identity_providers: oidc: issuer_jwks: key #1 with key id 'e7dfdc': option 'certificate_chain' produced an error during validation of the chain: certificate #1 in chain is not signed properly by certificate #2 in chain: x509: invalid signature: parent certificate cannot sign this kind of certificate",
},
},
{
"ShouldRaiseErrorOnInvalidPrivateKeyN",
schema.OpenIDConnectConfiguration{
IssuerJWKS: []schema.JWK{
{Key: frankenkey},
},
},
schema.OpenIDConnectConfiguration{
IssuerJWKS: []schema.JWK{
{Key: frankenkey},
},
Discovery: schema.OpenIDConnectDiscovery{
DefaultKeyID: "",
RegisteredJWKSigningAlgs: []string(nil),
},
},
[]string{
"identity_providers: oidc: issuer_jwks: key #1: option 'key' must be a valid RSA private key but the provided data is malformed as it's missing the public key bits",
},
},
{
"ShouldRaiseErrorOnCertForKey",
schema.OpenIDConnectConfiguration{
IssuerJWKS: []schema.JWK{
{Key: certRSA2048},
},
},
schema.OpenIDConnectConfiguration{
IssuerJWKS: []schema.JWK{
{Key: certRSA2048},
},
Discovery: schema.OpenIDConnectDiscovery{
DefaultKeyID: "",
RegisteredJWKSigningAlgs: []string(nil),
},
},
[]string{
"identity_providers: oidc: issuer_jwks: key #1 with key id '': option 'key' failed to get key properties: the key type 'schema.X509CertificateChain' is unknown or not valid for the configuration",
},
},
}
var n int
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
val := schema.NewStructValidator()
validateOIDCIssuer(&tc.have, val)
assert.Equal(t, tc.expected.Discovery.DefaultKeyID, tc.have.Discovery.DefaultKeyID)
assert.Equal(t, tc.expected.Discovery.RegisteredJWKSigningAlgs, tc.have.Discovery.RegisteredJWKSigningAlgs)
assert.Equal(t, tc.expected.IssuerPrivateKey, tc.have.IssuerPrivateKey)
assert.Equal(t, tc.expected.IssuerCertificateChain, tc.have.IssuerCertificateChain)
n = len(tc.expected.IssuerJWKS)
require.Len(t, tc.have.IssuerJWKS, n)
for i := 0; i < n; i++ {
t.Run(fmt.Sprintf("Key%d", i), func(t *testing.T) {
assert.Equal(t, tc.expected.IssuerJWKS[i].Algorithm, tc.have.IssuerJWKS[i].Algorithm)
assert.Equal(t, tc.expected.IssuerJWKS[i].Use, tc.have.IssuerJWKS[i].Use)
assert.Equal(t, tc.expected.IssuerJWKS[i].KeyID, tc.have.IssuerJWKS[i].KeyID)
assert.Equal(t, tc.expected.IssuerJWKS[i].Key, tc.have.IssuerJWKS[i].Key)
assert.Equal(t, tc.expected.IssuerJWKS[i].CertificateChain, tc.have.IssuerJWKS[i].CertificateChain)
})
}
n = len(tc.errs)
require.Len(t, val.Errors(), n)
for i := 0; i < n; i++ {
assert.EqualError(t, val.Errors()[i], tc.errs[i])
}
})
}
}
func MustDecodeSecret(value string) *schema.PasswordDigest {
if secret, err := schema.DecodePasswordDigest(value); err != nil {
panic(err)
} else {
return secret
}
}
func MustLoadCrypto(alg, mod, ext string, extra ...string) any {
fparts := []string{alg, mod}
if len(extra) != 0 {
fparts = append(fparts, extra...)
}
var (
data []byte
decoded any
err error
)
if data, err = os.ReadFile(fmt.Sprintf(pathCrypto, strings.Join(fparts, "_"), ext)); err != nil {
panic(err)
}
if decoded, err = utils.ParseX509FromPEMRecursive(data); err != nil {
panic(err)
}
return decoded
}
func MustLoadCertificateChain(alg, op string) schema.X509CertificateChain {
decoded := MustLoadCrypto(alg, op, "crt")
switch cert := decoded.(type) {
case *x509.Certificate:
return schema.NewX509CertificateChainFromCerts([]*x509.Certificate{cert})
case []*x509.Certificate:
return schema.NewX509CertificateChainFromCerts(cert)
default:
panic(fmt.Errorf("the key was not a *x509.Certificate or []*x509.Certificate, it's a %T", cert))
}
}
func MustLoadCertificate(alg, op string) *x509.Certificate {
decoded := MustLoadCrypto(alg, op, "crt")
cert, ok := decoded.(*x509.Certificate)
if !ok {
panic(fmt.Errorf("the key was not a *x509.Certificate, it's a %T", cert))
}
return cert
}
func MustLoadEd15519PrivateKey(curve string, extra ...string) ed25519.PrivateKey {
decoded := MustLoadCrypto("ED25519", curve, "pem", extra...)
key, ok := decoded.(ed25519.PrivateKey)
if !ok {
panic(fmt.Errorf("the key was not a ed25519.PrivateKey, it's a %T", key))
}
return key
}
func MustLoadECDSAPrivateKey(curve string, extra ...string) *ecdsa.PrivateKey {
decoded := MustLoadCrypto("ECDSA", curve, "pem", extra...)
key, ok := decoded.(*ecdsa.PrivateKey)
if !ok {
panic(fmt.Errorf("the key was not a *ecdsa.PrivateKey, it's a %T", key))
}
return key
}
func MustLoadRSAPublicKey(bits string, extra ...string) *rsa.PublicKey {
decoded := MustLoadCrypto("RSA", bits, "pem", extra...)
key, ok := decoded.(*rsa.PublicKey)
if !ok {
panic(fmt.Errorf("the key was not a *rsa.PublicKey, it's a %T", key))
}
return key
}
func MustLoadRSAPrivateKey(bits string, extra ...string) *rsa.PrivateKey {
decoded := MustLoadCrypto("RSA", bits, "pem", extra...)
key, ok := decoded.(*rsa.PrivateKey)
if !ok {
panic(fmt.Errorf("the key was not a *rsa.PrivateKey, it's a %T", key))
}
return key
}
const (
pathCrypto = "../test_resources/crypto/%s.%s"
)
//nolint:unused
var (
tOpenIDConnectPBKDF2ClientSecret, tOpenIDConnectPlainTextClientSecret *schema.PasswordDigest
// Standard RSA key pair.
publicRSA2048Pair *rsa.PublicKey
privateRSA2048Pair *rsa.PrivateKey
// Standard RSA key / certificate pairs.
keyRSA1024, keyRSA2048, keyRSA2048PKCS8, keyRSA4096 *rsa.PrivateKey
certRSA1024, certRSA2048, certRSA4096 schema.X509CertificateChain
// Standard ECDSA key / certificate pairs.
keyECDSAP224, keyECDSAP256, keyECDSAP384, keyECDSAP521 *ecdsa.PrivateKey
certECDSAP224, certECDSAP256, certECDSAP384, certECDSAP521 schema.X509CertificateChain
// Standard ECDSA key / certificate pairs.
keyECDSAP256PKCS8, keyECDSAP384PKCS8, keyECDSAP521PKCS8 *ecdsa.PrivateKey
certECDSAP224PKCS8, certECDSAP256PKCS8, certECDSAP384PKCS8, certECDSAP521PKCS8 schema.X509CertificateChain
// Ed15519 key / certificate pair.
keyEd2519 ed25519.PrivateKey
certEd15519 schema.X509CertificateChain
)
func init() {
tOpenIDConnectPBKDF2ClientSecret = MustDecodeSecret("$pbkdf2-sha512$310000$c8p78n7pUMln0jzvd4aK4Q$JNRBzwAo0ek5qKn50cFzzvE9RXV88h1wJn5KGiHrD0YKtZaR/nCb2CJPOsKaPK0hjf.9yHxzQGZziziccp6Yng")
tOpenIDConnectPlainTextClientSecret = MustDecodeSecret("$plaintext$example")
publicRSA2048Pair = MustLoadRSAPublicKey("2048", "PAIR", "PUBLIC")
privateRSA2048Pair = MustLoadRSAPrivateKey("2048", "PAIR", "PRIVATE")
keyRSA1024 = MustLoadRSAPrivateKey("1024")
keyRSA2048 = MustLoadRSAPrivateKey("2048")
keyRSA4096 = MustLoadRSAPrivateKey("4096")
keyECDSAP224 = MustLoadECDSAPrivateKey("P224")
keyECDSAP256 = MustLoadECDSAPrivateKey("P256")
keyECDSAP384 = MustLoadECDSAPrivateKey("P384")
keyECDSAP521 = MustLoadECDSAPrivateKey("P521")
keyEd2519 = MustLoadEd15519PrivateKey("PKCS8")
keyRSA2048PKCS8 = MustLoadRSAPrivateKey("2048", "PKCS8")
keyECDSAP256PKCS8 = MustLoadECDSAPrivateKey("P256", "PKCS8")
keyECDSAP384PKCS8 = MustLoadECDSAPrivateKey("P384", "PKCS8")
keyECDSAP521PKCS8 = MustLoadECDSAPrivateKey("P521", "PKCS8")
certRSA1024 = MustLoadCertificateChain("RSA", "1024")
certRSA2048 = MustLoadCertificateChain("RSA", "2048")
certRSA4096 = MustLoadCertificateChain("RSA", "4096")
certECDSAP224 = MustLoadCertificateChain("ECDSA", "P224")
certECDSAP256 = MustLoadCertificateChain("ECDSA", "P256")
certECDSAP384 = MustLoadCertificateChain("ECDSA", "P384")
certECDSAP521 = MustLoadCertificateChain("ECDSA", "P521")
certEd15519 = MustLoadCertificateChain("ED25519", "PKCS8")
}