diff options
Diffstat (limited to 'internal/configuration')
| -rw-r--r-- | internal/configuration/config.template.yml | 40 | ||||
| -rw-r--r-- | internal/configuration/deprecation.go | 20 | ||||
| -rw-r--r-- | internal/configuration/helpers_test.go | 11 | ||||
| -rw-r--r-- | internal/configuration/schema/const.go | 28 | ||||
| -rw-r--r-- | internal/configuration/schema/identity_providers.go | 5 | ||||
| -rw-r--r-- | internal/configuration/schema/keys.go | 1 | ||||
| -rw-r--r-- | internal/configuration/schema/server.go | 42 | ||||
| -rw-r--r-- | internal/configuration/schema/types.go | 2 | ||||
| -rw-r--r-- | internal/configuration/validator/access_control.go | 2 | ||||
| -rw-r--r-- | internal/configuration/validator/const.go | 56 | ||||
| -rw-r--r-- | internal/configuration/validator/identity_providers.go | 158 | ||||
| -rw-r--r-- | internal/configuration/validator/identity_providers_test.go | 388 | ||||
| -rw-r--r-- | internal/configuration/validator/server.go | 53 | ||||
| -rw-r--r-- | internal/configuration/validator/server_test.go | 81 | ||||
| -rw-r--r-- | internal/configuration/validator/util.go | 2 |
15 files changed, 709 insertions, 180 deletions
diff --git a/internal/configuration/config.template.yml b/internal/configuration/config.template.yml index 62bf2b15e..d759a9ae5 100644 --- a/internal/configuration/config.template.yml +++ b/internal/configuration/config.template.yml @@ -1292,46 +1292,6 @@ notifier: # 1oAPwIHNaJJwC4z6oG9E_DO_NOT_USE= # -----END CERTIFICATE----- - ## The issuer_private_key is used to sign the JWT forged by OpenID Connect. This is in addition to the - ## issuer_private_keys option. Assumed to use the RS256 algorithm, and must not be specified if any of the - ## keys in issuer_private_keys also has the algorithm RS256 or are an RSA key without an algorithm. - ## Issuer Private Key can also be set using a secret: https://www.authelia.com/c/secrets - # issuer_private_key: | - # -----BEGIN RSA PRIVATE KEY----- - # MIIBPAIBAAJBAK2i7RlJEYo/Xa6mQmv9zmT0XUj3DcEhRJGPVw2qMyadUFxNg/ZF - # p7aTcToHMf00z6T3b7mwdBkCFQOL3Kb7WRcCAwEAAQJBAJdpB0+RQ9ZFwy9Uk38P - # 5zZpUB8cL8ZFeEFluQeVbt0vyNa+cPLvDLouY87onduXtMz5AKIatLaTOjuG2thh - # SKECIQDY6G8gvsYJdXCE9UJ7ukoLrRHxt/frhAtmSY5lVAPuMwIhAMzuDrJo73LH - # ZyEaqIXc5pIiX3Sag43csPDHfuXdtT2NAiEAhyRKGJzDxiDlefFU+sGWYK/z/iYg - # 0Rvz/kbV8UvnJwECIQDAYN6VJ6NZmc27qv33JIejOfdoTEEhZMMKVg1PlxE0ZQIg - # HFpJiFxZES3QvVPr8deBXORPurqD5uU85NKsf61AdRs_DO_NOT_USE= - # -----END RSA PRIVATE KEY----- - - ## Optional matching certificate chain in PEM DER form that matches the issuer_private_key. All certificates within - ## the chain must be valid and current, and from top to bottom each certificate must be signed by the next - ## certificate in the chain if provided. - # issuer_certificate_chain: | - # -----BEGIN CERTIFICATE----- - # MIIBWzCCAQWgAwIBAgIQYAKsXhJOXKfyySlmpKicTzANBgkqhkiG9w0BAQsFADAT - # MREwDwYDVQQKEwhBdXRoZWxpYTAeFw0yMzA0MjEwMDA3NDRaFw0yNDA0MjAwMDA3 - # NDRaMBMxETAPBgNVBAoTCEF1dGhlbGlhMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJB - # AK2i7RlJEYo/Xa6mQmv9zmT0XUj3DcEhRJGPVw2qMyadUFxNg/ZFp7aTcToHMf00 - # z6T3b7mwdBkCFQOL3Kb7WRcCAwEAAaM1MDMwDgYDVR0PAQH/BAQDAgWgMBMGA1Ud - # JQQMMAoGCCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADQQB8 - # Of2iM7fPadmtChCMna8lYWH+lEplj6BxOJlRuGRawxszLwi78bnq0sCR33LU6xMx - # 1oAPwIHNaJJwC4z6oG9E_DO_NOT_USE= - # -----END CERTIFICATE----- - # -----BEGIN CERTIFICATE----- - # MIIBWzCCAQWgAwIBAgIQYAKsXhJOXKfyySlmpKicTzANBgkqhkiG9w0BAQsFADAT - # MREwDwYDVQQKEwhBdXRoZWxpYTAeFw0yMzA0MjEwMDA3NDRaFw0yNDA0MjAwMDA3 - # NDRaMBMxETAPBgNVBAoTCEF1dGhlbGlhMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJB - # AK2i7RlJEYo/Xa6mQmv9zmT0XUj3DcEhRJGPVw2qMyadUFxNg/ZFp7aTcToHMf00 - # z6T3b7mwdBkCFQOL3Kb7WRcCAwEAAaM1MDMwDgYDVR0PAQH/BAQDAgWgMBMGA1Ud - # JQQMMAoGCCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADQQB8 - # Of2iM7fPadmtChCMna8lYWH+lEplj6BxOJlRuGRawxszLwi78bnq0sCR33LU6xMx - # 1oAPwIHNaJJwC4z6oG9E_DO_NOT_USE= - # -----END CERTIFICATE----- - ## Enables additional debug messages. # enable_client_debug_messages: false diff --git a/internal/configuration/deprecation.go b/internal/configuration/deprecation.go index 420acd194..ad48456ba 100644 --- a/internal/configuration/deprecation.go +++ b/internal/configuration/deprecation.go @@ -358,6 +358,26 @@ var deprecations = map[string]Deprecation{ MapFunc: nil, ErrFunc: nil, }, + "identity_providers.oidc.issuer_private_key": { + Version: model.SemanticVersion{Major: 4, Minor: 38}, + Key: "identity_providers.oidc.issuer_private_key", + NewKey: "identity_providers.oidc.issuer_private_keys", + AutoMap: false, + MapFunc: nil, + ErrFunc: func(d Deprecation, keysFinal map[string]any, value any, val *schema.StructValidator) { + val.PushWarning(fmt.Errorf("configuration key '%s' is deprecated in %s and should be configured using the new configuration key '%s': this has been automatically mapped for you but you will need to adjust your configuration (see https://www.authelia.com/c/oidc) to remove this message", d.Key, d.Version, d.NewKey)) + }, + }, + "identity_providers.oidc.issuer_certificate_chain": { + Version: model.SemanticVersion{Major: 4, Minor: 38}, + Key: "identity_providers.oidc.issuer_certificate_chain", + NewKey: "identity_providers.oidc.issuer_private_keys", + AutoMap: false, + MapFunc: nil, + ErrFunc: func(d Deprecation, keysFinal map[string]any, value any, val *schema.StructValidator) { + val.PushWarning(fmt.Errorf("configuration key '%s' is deprecated in %s and should be configured using the new configuration key '%s': this has been automatically mapped for you but you will need to adjust your configuration (see https://www.authelia.com/c/oidc) to remove this message", d.Key, d.Version, d.NewKey)) + }, + }, "jwt_secret": { Version: model.SemanticVersion{Major: 4, Minor: 38}, Key: "jwt_secret", diff --git a/internal/configuration/helpers_test.go b/internal/configuration/helpers_test.go index fcb7e343d..678b8535c 100644 --- a/internal/configuration/helpers_test.go +++ b/internal/configuration/helpers_test.go @@ -48,11 +48,12 @@ func TestGetEnvConfigMaps(t *testing.T) { assert.True(t, ok) assert.Equal(t, "mysecret.password", key) - assert.Len(t, ignoredKeys, 4) - assert.Contains(t, ignoredKeys, DefaultEnvPrefix+"MYOTHER_CONFIGKEY_FILE") - assert.Contains(t, ignoredKeys, DefaultEnvPrefix+"MYSECRET_PASSWORD_FILE") - assert.Contains(t, ignoredKeys, DefaultEnvPrefix+"MYSECRET_USER_PASSWORD_FILE") - assert.Contains(t, ignoredKeys, DefaultEnvPrefix+"JWT_SECRET_FILE") + assert.Len(t, ignoredKeys, 6) + assert.Contains(t, ignoredKeys, DefaultEnvPrefix+MYOTHER_CONFIGKEY_FILE) + assert.Contains(t, ignoredKeys, DefaultEnvPrefix+MYSECRET_PASSWORD_FILE) + assert.Contains(t, ignoredKeys, DefaultEnvPrefix+MYSECRET_USER_PASSWORD_FILE) + assert.Contains(t, ignoredKeys, DefaultEnvPrefix+"IDENTITY_PROVIDERS_OIDC_ISSUER_PRIVATE_KEY_FILE") + assert.Contains(t, ignoredKeys, DefaultEnvPrefix+"IDENTITY_PROVIDERS_OIDC_ISSUER_CERTIFICATE_CHAIN_FILE") } func TestGetSecretConfigMapMockInput(t *testing.T) { diff --git a/internal/configuration/schema/const.go b/internal/configuration/schema/const.go index 52c92d591..d5de64444 100644 --- a/internal/configuration/schema/const.go +++ b/internal/configuration/schema/const.go @@ -125,8 +125,32 @@ const ( ) const ( - blockCERTIFICATE = "CERTIFICATE" - blockRSAPRIVATEKEY = "RSA PRIVATE KEY" + blockCERTIFICATE = "CERTIFICATE" +) + +// Authorization Schemes. +const ( + SchemeBasic = "basic" + SchemeBearer = "bearer" +) + +// Authz values. +const ( + AuthzEndpointNameLegacy = "legacy" + AuthzEndpointNameAuthRequest = "auth-request" + AuthzEndpointNameExtAuthz = "ext-authz" + AuthzEndpointNameForwardAuth = "forward-auth" + + AuthzImplementationLegacy = "Legacy" + AuthzImplementationAuthRequest = "AuthRequest" + AuthzImplementationExtAuthz = "ExtAuthz" + AuthzImplementationForwardAuth = "ForwardAuth" + + AuthzStrategyHeaderCookieSession = "CookieSession" + AuthzStrategyHeaderAuthorization = "HeaderAuthorization" + AuthzStrategyHeaderProxyAuthorization = "HeaderProxyAuthorization" + AuthzStrategyHeaderAuthRequestProxyAuthorization = "HeaderAuthRequestProxyAuthorization" + AuthzStrategyHeaderLegacy = "HeaderLegacy" ) const ( diff --git a/internal/configuration/schema/identity_providers.go b/internal/configuration/schema/identity_providers.go index e755ba45b..2d1f6fcad 100644 --- a/internal/configuration/schema/identity_providers.go +++ b/internal/configuration/schema/identity_providers.go @@ -16,8 +16,8 @@ type IdentityProvidersOpenIDConnect struct { HMACSecret string `koanf:"hmac_secret" json:"hmac_secret" jsonschema:"title=HMAC Secret" jsonschema_description:"The HMAC Secret used to sign Access Tokens."` IssuerPrivateKeys []JWK `koanf:"issuer_private_keys" json:"issuer_private_keys" jsonschema:"title=Issuer Private Keys" jsonschema_description:"The Private Keys used to sign ID Tokens."` - IssuerCertificateChain X509CertificateChain `koanf:"issuer_certificate_chain" json:"issuer_certificate_chain" jsonschema:"title=Issuer Certificate Chain" jsonschema_description:"The Issuer Certificate Chain with an RSA Public Key used to sign ID Tokens."` - IssuerPrivateKey *rsa.PrivateKey `koanf:"issuer_private_key" json:"issuer_private_key" jsonschema:"title=Issuer Private Key" jsonschema_description:"The Issuer Private Key with an RSA Private Key used to sign ID Tokens."` + IssuerCertificateChain X509CertificateChain `koanf:"issuer_certificate_chain" json:"issuer_certificate_chain" jsonschema:"title=Issuer Certificate Chain,deprecated" jsonschema_description:"The Issuer Certificate Chain with an RSA Public Key used to sign ID Tokens."` + IssuerPrivateKey *rsa.PrivateKey `koanf:"issuer_private_key" json:"issuer_private_key" jsonschema:"title=Issuer Private Key,deprecated" jsonschema_description:"The Issuer Private Key with an RSA Private Key used to sign ID Tokens."` EnableClientDebugMessages bool `koanf:"enable_client_debug_messages" json:"enable_client_debug_messages" jsonschema:"default=false,title=Enable Client Debug Messages" jsonschema_description:"Enables additional debug messages for clients."` MinimumParameterEntropy int `koanf:"minimum_parameter_entropy" json:"minimum_parameter_entropy" jsonschema:"default=8,minimum=-1,title=Minimum Parameter Entropy" jsonschema_description:"The minimum entropy of the nonce parameter."` @@ -61,6 +61,7 @@ type IdentityProvidersOpenIDConnectDiscovery struct { ResponseObjectSigningAlgs []string RequestObjectSigningAlgs []string JWTResponseAccessTokens bool + BearerAuthorization bool } type IdentityProvidersOpenIDConnectLifespans struct { diff --git a/internal/configuration/schema/keys.go b/internal/configuration/schema/keys.go index de96acdb3..52a5e5a4a 100644 --- a/internal/configuration/schema/keys.go +++ b/internal/configuration/schema/keys.go @@ -318,6 +318,7 @@ var Keys = []string{ "server.endpoints.authz.*.implementation", "server.endpoints.authz.*.authn_strategies", "server.endpoints.authz.*.authn_strategies[].name", + "server.endpoints.authz.*.authn_strategies[].schemes", "server.buffers.read", "server.buffers.write", "server.timeouts.read", diff --git a/internal/configuration/schema/server.go b/internal/configuration/schema/server.go index bc9069c43..2a5e4f299 100644 --- a/internal/configuration/schema/server.go +++ b/internal/configuration/schema/server.go @@ -45,7 +45,8 @@ type ServerEndpointsAuthz struct { // ServerEndpointsAuthzAuthnStrategy is the Authz endpoints configuration for the HTTP server. type ServerEndpointsAuthzAuthnStrategy struct { - Name string `koanf:"name" json:"name" jsonschema:"enum=HeaderAuthorization,enum=HeaderProxyAuthorization,enum=HeaderAuthRequestProxyAuthorization,enum=HeaderLegacy,enum=CookieSession,title=Name" jsonschema_description:"The name of the Authorization strategy to use."` + Name string `koanf:"name" json:"name" jsonschema:"enum=HeaderAuthorization,enum=HeaderProxyAuthorization,enum=HeaderAuthRequestProxyAuthorization,enum=HeaderLegacy,enum=CookieSession,title=Name" jsonschema_description:"The name of the Authorization strategy to use."` + Schemes []string `koanf:"schemes" json:"schemes" jsonschema:"enum=basic,enum=bearer,default=basic,title=Authorization Schemes" jsonschema_description:"The name of the authorization schemes to allow with the header strategies."` } // ServerTLS represents the configuration of the http servers TLS options. @@ -74,39 +75,50 @@ var DefaultServerConfiguration = Server{ }, Endpoints: ServerEndpoints{ Authz: map[string]ServerEndpointsAuthz{ - "legacy": { - Implementation: "Legacy", + AuthzEndpointNameLegacy: { + Implementation: AuthzImplementationLegacy, + AuthnStrategies: []ServerEndpointsAuthzAuthnStrategy{ + { + Name: AuthzStrategyHeaderLegacy, + }, + { + Name: AuthzStrategyHeaderCookieSession, + }, + }, }, - "auth-request": { - Implementation: "AuthRequest", + AuthzEndpointNameAuthRequest: { + Implementation: AuthzImplementationAuthRequest, AuthnStrategies: []ServerEndpointsAuthzAuthnStrategy{ { - Name: "HeaderAuthRequestProxyAuthorization", + Name: AuthzStrategyHeaderAuthorization, + Schemes: []string{SchemeBasic}, }, { - Name: "CookieSession", + Name: AuthzStrategyHeaderCookieSession, }, }, }, - "forward-auth": { - Implementation: "ForwardAuth", + AuthzEndpointNameExtAuthz: { + Implementation: AuthzImplementationExtAuthz, AuthnStrategies: []ServerEndpointsAuthzAuthnStrategy{ { - Name: "HeaderProxyAuthorization", + Name: AuthzStrategyHeaderAuthorization, + Schemes: []string{SchemeBasic}, }, { - Name: "CookieSession", + Name: AuthzStrategyHeaderCookieSession, }, }, }, - "ext-authz": { - Implementation: "ExtAuthz", + AuthzEndpointNameForwardAuth: { + Implementation: AuthzImplementationForwardAuth, AuthnStrategies: []ServerEndpointsAuthzAuthnStrategy{ { - Name: "HeaderProxyAuthorization", + Name: AuthzStrategyHeaderAuthorization, + Schemes: []string{SchemeBasic}, }, { - Name: "CookieSession", + Name: AuthzStrategyHeaderCookieSession, }, }, }, diff --git a/internal/configuration/schema/types.go b/internal/configuration/schema/types.go index 7f0497c51..a7705db1f 100644 --- a/internal/configuration/schema/types.go +++ b/internal/configuration/schema/types.go @@ -604,7 +604,7 @@ var jsonschemaACLNetwork = jsonschema.Schema{ var jsonschemaACLSubject = jsonschema.Schema{ Type: jsonschema.TypeString, - Pattern: "^(user|group):.+$", + Pattern: "^(user|group|oauth2:client:):.+$", } var jsonschemaACLMethod = jsonschema.Schema{ diff --git a/internal/configuration/validator/access_control.go b/internal/configuration/validator/access_control.go index 9b92eff80..9b7db2bea 100644 --- a/internal/configuration/validator/access_control.go +++ b/internal/configuration/validator/access_control.go @@ -18,7 +18,7 @@ func IsPolicyValid(policy string) (isValid bool) { // IsSubjectValid check if a subject is valid. func IsSubjectValid(subject string) (isValid bool) { - return subject == "" || strings.HasPrefix(subject, "user:") || strings.HasPrefix(subject, "group:") + return subject == "" || strings.HasPrefix(subject, "user:") || strings.HasPrefix(subject, "group:") || strings.HasPrefix(subject, "oauth2:client:") } // IsNetworkGroupValid check if a network group is valid. diff --git a/internal/configuration/validator/const.go b/internal/configuration/validator/const.go index a2c636ece..d212ab9ce 100644 --- a/internal/configuration/validator/const.go +++ b/internal/configuration/validator/const.go @@ -159,7 +159,7 @@ const ( const ( errFmtOIDCProviderNoClientsConfigured = "identity_providers: oidc: option 'clients' must have one or " + "more clients configured" - errFmtOIDCProviderNoPrivateKey = "identity_providers: oidc: option `issuer_private_keys` or 'issuer_private_key' is required" + errFmtOIDCProviderNoPrivateKey = "identity_providers: oidc: option `issuer_private_keys` is required" errFmtOIDCProviderEnforcePKCEInvalidValue = "identity_providers: oidc: option 'enforce_pkce' must be 'never', " + "'public_clients_only' or 'always', but it's configured as '%s'" errFmtOIDCProviderInsecureParameterEntropy = "identity_providers: oidc: option 'minimum_parameter_entropy' is " @@ -196,6 +196,10 @@ const ( errFmtOIDCClientsWithEmptyID = "identity_providers: oidc: clients: option 'id' is required but was absent on the clients in positions %s" errFmtOIDCClientsDeprecated = "identity_providers: oidc: clients: warnings for clients above indicate deprecated functionality and it's strongly suggested these issues are checked and fixed if they're legitimate issues or reported if they are not as in a future version these warnings will become errors" + errFmtMustOnlyHaveValues = "'%s' must only have the values %s " + errFmtMustBeConfiguredAs = "'%s' must be configured as %s " + errFmtOIDCClientOption = "identity_providers: oidc: clients: client '%s': option " + errFmtOIDCWhenScope = "when configured with scope '%s'" errFmtOIDCClientInvalidSecretIs = errFmtOIDCClientOption + "'secret' is " errFmtOIDCClientInvalidSecret = errFmtOIDCClientInvalidSecretIs + "required" errFmtOIDCClientInvalidSecretPlainText = errFmtOIDCClientInvalidSecretIs + "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" @@ -204,7 +208,9 @@ const ( "required to be empty when option 'public' is true" errFmtOIDCClientPublicInvalidSecretClientAuthMethod = errFmtOIDCClientInvalidSecretIs + "required to be empty when option 'token_endpoint_auth_method' is configured as '%s'" - errFmtOIDCClientOption = "identity_providers: oidc: clients: client '%s': option " + errFmtOIDCClientIDTooLong = errFmtOIDCClientOption + "'id' must not be more than 100 characters but it has %d characters" + errFmtOIDCClientIDInvalidCharacters = errFmtOIDCClientOption + "'id' must only contain RFC3986 unreserved characters" + errFmtOIDCClientRedirectURIHas = errFmtOIDCClientOption + "'redirect_uris' has " errFmtOIDCClientRedirectURICantBeParsed = errFmtOIDCClientRedirectURIHas + "an invalid value: redirect uri '%s' could not be parsed: %v" @@ -215,10 +221,19 @@ const ( "an invalid value: redirect uri '%s' must have a scheme but it's absent" errFmtOIDCClientInvalidConsentMode = "identity_providers: oidc: clients: client '%s': consent: option 'mode' must be one of " + "%s but it's configured as '%s'" - errFmtOIDCClientInvalidEntries = errFmtOIDCClientOption + "'%s' must only have the values " + - "%s but the values %s are present" + errFmtOIDCClientInvalidEntries = errFmtOIDCClientOption + errFmtMustOnlyHaveValues + + "but the values %s are present" + errFmtOIDCClientUnknownScopeEntries = errFmtOIDCClientOption + "'%s' only expects the values " + + "%s but the unknown values %s are present and should generally only be used if a particular client requires a scope outside of our standard scopes" + errFmtOIDCClientInvalidEntriesScope = errFmtOIDCClientOption + errFmtMustOnlyHaveValues + + errFmtOIDCWhenScope + " but the values %s are present" + errFmtOIDCClientEmptyEntriesScope = errFmtOIDCClientOption + errFmtMustOnlyHaveValues + + errFmtOIDCWhenScope + " but it's not configured" + errFmtOIDCClientOptionRequiredScope = errFmtOIDCClientOption + "'%s' must be configured " + errFmtOIDCWhenScope + " but it's absent" + errFmtOIDCClientOptionMustScope = errFmtOIDCClientOption + errFmtMustBeConfiguredAs + errFmtOIDCWhenScope + " but it's configured as '%s'" + errFmtOIDCClientOptionMustScopeClientType = errFmtOIDCClientOption + errFmtMustBeConfiguredAs + errFmtOIDCWhenScope + " and the '%s' client type but it's configured as '%s'" errFmtOIDCClientInvalidEntriesClientCredentials = errFmtOIDCClientOption + "'scopes' has the values " + - "%s however when exclusively utilizing the 'client_credentials' value for the 'grant_types' the values %s are not allowed" + "%s however when utilizing the 'client_credentials' value for the 'grant_types' the values %s are not allowed" errFmtOIDCClientInvalidEntryDuplicates = errFmtOIDCClientOption + "'%s' must have unique values but the values %s are duplicated" errFmtOIDCClientInvalidValue = errFmtOIDCClientOption + "'%s' must be one of %s but it's configured as '%s'" @@ -367,11 +382,14 @@ const ( errFmtServerPathNotEndForwardSlash = "server: option 'address' must not and with a forward slash but it's configured as '%s'" errFmtServerPathAlphaNum = "server: option 'path' must only contain alpha numeric characters" - errFmtServerEndpointsAuthzImplementation = "server: endpoints: authz: %s: option 'implementation' must be one of %s but it's configured as '%s'" - errFmtServerEndpointsAuthzStrategy = "server: endpoints: authz: %s: authn_strategies: option 'name' must be one of %s but it's configured as '%s'" - errFmtServerEndpointsAuthzStrategyDuplicate = "server: endpoints: authz: %s: authn_strategies: duplicate strategy name detected with name '%s'" - errFmtServerEndpointsAuthzPrefixDuplicate = "server: endpoints: authz: %s: endpoint starts with the same prefix as the '%s' endpoint with the '%s' implementation which accepts prefixes as part of its implementation" - errFmtServerEndpointsAuthzInvalidName = "server: endpoints: authz: %s: contains invalid characters" + errFmtServerEndpointsAuthzImplementation = "server: endpoints: authz: %s: option 'implementation' must be one of %s but it's configured as '%s'" + errFmtServerEndpointsAuthzStrategy = "server: endpoints: authz: %s: authn_strategies: option 'name' must be one of %s but it's configured as '%s'" + errFmtServerEndpointsAuthzSchemes = "server: endpoints: authz: %s: authn_strategies: strategy #%d (%s): option 'schemes' must only include the values %s but has '%s'" + errFmtServerEndpointsAuthzSchemesInvalidForStrategy = "server: endpoints: authz: %s: authn_strategies: strategy #%d (%s): option 'schemes' is not valid for the strategy" + errFmtServerEndpointsAuthzStrategyNoName = "server: endpoints: authz: %s: authn_strategies: strategy #%d: option 'name' must be configured" + errFmtServerEndpointsAuthzStrategyDuplicate = "server: endpoints: authz: %s: authn_strategies: duplicate strategy name detected with name '%s'" + errFmtServerEndpointsAuthzPrefixDuplicate = "server: endpoints: authz: %s: endpoint starts with the same prefix as the '%s' endpoint with the '%s' implementation which accepts prefixes as part of its implementation" + errFmtServerEndpointsAuthzInvalidName = "server: endpoints: authz: %s: contains invalid characters" errFmtServerEndpointsAuthzLegacyInvalidImplementation = "server: endpoints: authz: %s: option 'implementation' is invalid: the endpoint with the name 'legacy' must use the 'Legacy' implementation" ) @@ -421,9 +439,7 @@ const ( ) const ( - legacy = "legacy" - authzImplementationLegacy = "Legacy" - authzImplementationExtAuthz = "ExtAuthz" + legacy = "legacy" ) const ( @@ -431,8 +447,10 @@ const ( ) var ( - validAuthzImplementations = []string{"AuthRequest", "ForwardAuth", authzImplementationExtAuthz, authzImplementationLegacy} - validAuthzAuthnStrategies = []string{"CookieSession", "HeaderAuthorization", "HeaderProxyAuthorization", "HeaderAuthRequestProxyAuthorization", "HeaderLegacy"} + validAuthzImplementations = []string{schema.AuthzImplementationAuthRequest, schema.AuthzImplementationForwardAuth, schema.AuthzImplementationExtAuthz, schema.AuthzImplementationLegacy} + validAuthzAuthnStrategies = []string{schema.AuthzStrategyHeaderCookieSession, schema.AuthzStrategyHeaderAuthorization, schema.AuthzStrategyHeaderProxyAuthorization, schema.AuthzStrategyHeaderAuthRequestProxyAuthorization, schema.AuthzStrategyHeaderLegacy} + validAuthzAuthnHeaderStrategies = []string{schema.AuthzStrategyHeaderAuthorization, schema.AuthzStrategyHeaderProxyAuthorization, schema.AuthzStrategyHeaderAuthRequestProxyAuthorization} + validAuthzAuthnStrategySchemes = []string{schema.SchemeBasic, schema.SchemeBearer} ) var ( @@ -514,7 +532,7 @@ var ( var ( validOIDCCORSEndpoints = []string{oidc.EndpointAuthorization, oidc.EndpointPushedAuthorizationRequest, oidc.EndpointToken, oidc.EndpointIntrospection, oidc.EndpointRevocation, oidc.EndpointUserinfo} - validOIDCClientScopes = []string{oidc.ScopeOpenID, oidc.ScopeEmail, oidc.ScopeProfile, oidc.ScopeGroups, oidc.ScopeOfflineAccess} + validOIDCClientScopes = []string{oidc.ScopeOpenID, oidc.ScopeEmail, oidc.ScopeProfile, oidc.ScopeGroups, oidc.ScopeOfflineAccess, oidc.ScopeOffline, oidc.ScopeAutheliaBearerAuthz} validOIDCClientConsentModes = []string{auto, oidc.ClientConsentModeImplicit.String(), oidc.ClientConsentModeExplicit.String(), oidc.ClientConsentModePreConfigured.String()} validOIDCClientResponseModes = []string{oidc.ResponseModeFormPost, oidc.ResponseModeQuery, oidc.ResponseModeFragment, oidc.ResponseModeJWT, oidc.ResponseModeFormPostJWT, oidc.ResponseModeQueryJWT, oidc.ResponseModeFragmentJWT} validOIDCClientResponseTypes = []string{oidc.ResponseTypeAuthorizationCodeFlow, oidc.ResponseTypeImplicitFlowIDToken, oidc.ResponseTypeImplicitFlowToken, oidc.ResponseTypeImplicitFlowBoth, oidc.ResponseTypeHybridFlowIDToken, oidc.ResponseTypeHybridFlowToken, oidc.ResponseTypeHybridFlowBoth} @@ -527,6 +545,11 @@ var ( validOIDCClientTokenEndpointAuthMethodsConfidential = []string{oidc.ClientAuthMethodClientSecretPost, oidc.ClientAuthMethodClientSecretBasic, oidc.ClientAuthMethodPrivateKeyJWT} validOIDCClientTokenEndpointAuthSigAlgsClientSecretJWT = []string{oidc.SigningAlgHMACUsingSHA256, oidc.SigningAlgHMACUsingSHA384, oidc.SigningAlgHMACUsingSHA512} validOIDCIssuerJWKSigningAlgs = []string{oidc.SigningAlgRSAUsingSHA256, oidc.SigningAlgRSAPSSUsingSHA256, oidc.SigningAlgECDSAUsingP256AndSHA256, oidc.SigningAlgRSAUsingSHA384, oidc.SigningAlgRSAPSSUsingSHA384, oidc.SigningAlgECDSAUsingP384AndSHA384, oidc.SigningAlgRSAUsingSHA512, oidc.SigningAlgRSAPSSUsingSHA512, oidc.SigningAlgECDSAUsingP521AndSHA512} + + validOIDCClientScopesBearerAuthz = []string{oidc.ScopeOfflineAccess, oidc.ScopeOffline, oidc.ScopeAutheliaBearerAuthz} + validOIDCClientResponseModesBearerAuthz = []string{oidc.ResponseModeFormPost, oidc.ResponseModeFormPostJWT} + validOIDCClientResponseTypesBearerAuthz = []string{oidc.ResponseTypeAuthorizationCodeFlow} + validOIDCClientGrantTypesBearerAuthz = []string{oidc.GrantTypeAuthorizationCode, oidc.GrantTypeRefreshToken, oidc.GrantTypeClientCredentials} ) var ( @@ -534,6 +557,7 @@ var ( reDomainCharacters = regexp.MustCompile(`^[a-z0-9-]+(\.[a-z0-9-]+)+[a-z0-9]$`) reAuthzEndpointName = regexp.MustCompile(`^[a-zA-Z](([a-zA-Z0-9/._-]*)([a-zA-Z]))?$`) reOpenIDConnectKID = regexp.MustCompile(`^([a-zA-Z0-9](([a-zA-Z0-9._~-]*)([a-zA-Z0-9]))?)?$`) + reRFC3986Unreserved = regexp.MustCompile(`^[a-zA-Z0-9._~-]+$`) ) var replacedKeys = map[string]string{ diff --git a/internal/configuration/validator/identity_providers.go b/internal/configuration/validator/identity_providers.go index 7b86d1b86..c28bbce8d 100644 --- a/internal/configuration/validator/identity_providers.go +++ b/internal/configuration/validator/identity_providers.go @@ -7,7 +7,6 @@ import ( "net/url" "sort" "strconv" - "strings" "github.com/ory/fosite" @@ -120,6 +119,8 @@ func validateOIDCLifespans(config *schema.IdentityProvidersOpenIDConnect, _ *sch func validateOIDCIssuer(config *schema.IdentityProvidersOpenIDConnect, validator *schema.StructValidator) { switch { + case len(config.IssuerPrivateKeys) != 0 && (config.IssuerPrivateKey != nil || config.IssuerCertificateChain.HasCertificates()): + validator.Push(fmt.Errorf("identity_providers: oidc: option `issuer_private_keys` must not be configured at the same time as 'issuer_private_key' or 'issuer_certificate_chain'")) case config.IssuerPrivateKey != nil: validateOIDCIssuerPrivateKey(config) @@ -347,19 +348,26 @@ func validateOIDCClients(config *schema.IdentityProvidersOpenIDConnect, validato errDeprecatedFunc := func() { errDeprecated = true } for c, client := range config.Clients { - if client.ID == "" { + n := len(client.ID) + + switch { + case n == 0: blankClientIDs = append(blankClientIDs, "#"+strconv.Itoa(c+1)) - } else { + case n > 100: + validator.Push(fmt.Errorf(errFmtOIDCClientIDTooLong, client.ID, n)) + case !reRFC3986Unreserved.MatchString(client.ID): + validator.Push(fmt.Errorf(errFmtOIDCClientIDInvalidCharacters, client.ID)) + default: if client.Description == "" { config.Clients[c].Description = client.ID } - if id := strings.ToLower(client.ID); utils.IsStringInSlice(id, clientIDs) { - if !utils.IsStringInSlice(id, duplicateClientIDs) { - duplicateClientIDs = append(duplicateClientIDs, id) + if utils.IsStringInSlice(client.ID, clientIDs) { + if !utils.IsStringInSlice(client.ID, duplicateClientIDs) { + duplicateClientIDs = append(duplicateClientIDs, client.ID) } } else { - clientIDs = append(clientIDs, id) + clientIDs = append(clientIDs, client.ID) } } @@ -380,7 +388,15 @@ func validateOIDCClients(config *schema.IdentityProvidersOpenIDConnect, validato } func validateOIDCClient(c int, config *schema.IdentityProvidersOpenIDConnect, validator *schema.StructValidator, errDeprecatedFunc func()) { + ccg := utils.IsStringInSlice(oidc.GrantTypeClientCredentials, config.Clients[c].GrantTypes) + switch { + case ccg: + if config.Clients[c].AuthorizationPolicy == "" { + config.Clients[c].AuthorizationPolicy = policyOneFactor + } else if config.Clients[c].AuthorizationPolicy != policyOneFactor { + validator.Push(fmt.Errorf(errFmtOIDCClientInvalidValue, config.Clients[c].ID, "authorization_policy", strJoinOr([]string{policyOneFactor}), config.Clients[c].AuthorizationPolicy)) + } case config.Clients[c].AuthorizationPolicy == "": config.Clients[c].AuthorizationPolicy = schema.DefaultOpenIDConnectClientConfiguration.AuthorizationPolicy case utils.IsStringInSlice(config.Clients[c].AuthorizationPolicy, config.Discovery.AuthorizationPolicies): @@ -416,12 +432,14 @@ func validateOIDCClient(c int, config *schema.IdentityProvidersOpenIDConnect, va validator.Push(fmt.Errorf(errFmtOIDCClientInvalidValue, config.Clients[c].ID, attrOIDCRequestedAudienceMode, strJoinOr([]string{oidc.ClientRequestedAudienceModeExplicit.String(), oidc.ClientRequestedAudienceModeImplicit.String()}), config.Clients[c].RequestedAudienceMode)) } - validateOIDCClientConsentMode(c, config, validator) + setDefaults := validateOIDCClientScopesSpecialBearerAuthz(c, config, ccg, validator) - validateOIDCClientScopes(c, config, validator, errDeprecatedFunc) - validateOIDCClientResponseTypes(c, config, validator, errDeprecatedFunc) - validateOIDCClientResponseModes(c, config, validator, errDeprecatedFunc) - validateOIDCClientGrantTypes(c, config, validator, errDeprecatedFunc) + validateOIDCClientConsentMode(c, config, validator, setDefaults) + + validateOIDCClientScopes(c, config, validator, ccg, errDeprecatedFunc) + validateOIDCClientResponseTypes(c, config, validator, setDefaults, errDeprecatedFunc) + validateOIDCClientResponseModes(c, config, validator, setDefaults, errDeprecatedFunc) + validateOIDCClientGrantTypes(c, config, validator, setDefaults, errDeprecatedFunc) validateOIDCClientRedirectURIs(c, config, validator, errDeprecatedFunc) validateOIDDClientSigningAlgs(c, config, validator) @@ -569,9 +587,13 @@ func validateOIDCClientSectorIdentifier(c int, config *schema.IdentityProvidersO } } -func validateOIDCClientConsentMode(c int, config *schema.IdentityProvidersOpenIDConnect, validator *schema.StructValidator) { +func validateOIDCClientConsentMode(c int, config *schema.IdentityProvidersOpenIDConnect, validator *schema.StructValidator, setDefaults bool) { switch { case utils.IsStringInSlice(config.Clients[c].ConsentMode, []string{"", auto}): + if !setDefaults { + break + } + if config.Clients[c].ConsentPreConfiguredDuration != nil { config.Clients[c].ConsentMode = oidc.ClientConsentModePreConfigured.String() } else { @@ -588,8 +610,8 @@ func validateOIDCClientConsentMode(c int, config *schema.IdentityProvidersOpenID } } -func validateOIDCClientScopes(c int, config *schema.IdentityProvidersOpenIDConnect, validator *schema.StructValidator, errDeprecatedFunc func()) { - if len(config.Clients[c].Scopes) == 0 { +func validateOIDCClientScopes(c int, config *schema.IdentityProvidersOpenIDConnect, validator *schema.StructValidator, ccg bool, errDeprecatedFunc func()) { + if len(config.Clients[c].Scopes) == 0 && !ccg { config.Clients[c].Scopes = schema.DefaultOpenIDConnectClientConfiguration.Scopes } @@ -601,16 +623,10 @@ func validateOIDCClientScopes(c int, config *schema.IdentityProvidersOpenIDConne validator.PushWarning(fmt.Errorf(errFmtOIDCClientInvalidEntryDuplicates, config.Clients[c].ID, attrOIDCScopes, strJoinAnd(duplicates))) } - if utils.IsStringInSlice(oidc.GrantTypeClientCredentials, config.Clients[c].GrantTypes) { + if ccg { validateOIDCClientScopesClientCredentialsGrant(c, config, validator) - } else { - if !utils.IsStringInSlice(oidc.ScopeOpenID, config.Clients[c].Scopes) { - config.Clients[c].Scopes = append([]string{oidc.ScopeOpenID}, config.Clients[c].Scopes...) - } - - if len(invalid) != 0 { - validator.Push(fmt.Errorf(errFmtOIDCClientInvalidEntries, config.Clients[c].ID, attrOIDCScopes, strJoinOr(validOIDCClientScopes), strJoinAnd(invalid))) - } + } else if len(invalid) != 0 { + validator.PushWarning(fmt.Errorf(errFmtOIDCClientUnknownScopeEntries, config.Clients[c].ID, attrOIDCScopes, strJoinOr(validOIDCClientScopes), strJoinAnd(invalid))) } if utils.IsStringSliceContainsAny([]string{oidc.ScopeOfflineAccess, oidc.ScopeOffline}, config.Clients[c].Scopes) && @@ -625,11 +641,81 @@ func validateOIDCClientScopes(c int, config *schema.IdentityProvidersOpenIDConne } } -func validateOIDCClientScopesClientCredentialsGrant(c int, config *schema.IdentityProvidersOpenIDConnect, validator *schema.StructValidator) { - if len(config.Clients[c].GrantTypes) != 1 { - return +//nolint:gocyclo +func validateOIDCClientScopesSpecialBearerAuthz(c int, config *schema.IdentityProvidersOpenIDConnect, ccg bool, validator *schema.StructValidator) bool { + if !utils.IsStringInSlice(oidc.ScopeAutheliaBearerAuthz, config.Clients[c].Scopes) { + return true + } + + if !config.Discovery.BearerAuthorization { + config.Discovery.BearerAuthorization = true + } + + if !utils.IsStringSliceContainsAll(config.Clients[c].Scopes, validOIDCClientScopesBearerAuthz) { + validator.Push(fmt.Errorf(errFmtOIDCClientInvalidEntriesScope, config.Clients[c].ID, attrOIDCScopes, strJoinAnd(validOIDCClientScopesBearerAuthz), oidc.ScopeAutheliaBearerAuthz, strJoinAnd(config.Clients[c].Scopes))) } + if len(config.Clients[c].GrantTypes) == 0 { + validator.Push(fmt.Errorf(errFmtOIDCClientEmptyEntriesScope, config.Clients[c].ID, attrOIDCGrantTypes, strJoinAnd(validOIDCClientGrantTypesBearerAuthz), oidc.ScopeAutheliaBearerAuthz)) + } else { + invalid, _ := validateList(config.Clients[c].GrantTypes, validOIDCClientGrantTypesBearerAuthz, false) + + if len(invalid) != 0 { + validator.Push(fmt.Errorf(errFmtOIDCClientInvalidEntriesScope, config.Clients[c].ID, attrOIDCGrantTypes, strJoinAnd(validOIDCClientGrantTypesBearerAuthz), oidc.ScopeAutheliaBearerAuthz, strJoinAnd(invalid))) + } + } + + if len(config.Clients[c].Audience) == 0 { + validator.Push(fmt.Errorf(errFmtOIDCClientOptionRequiredScope, config.Clients[c].ID, "audience", oidc.ScopeAutheliaBearerAuthz)) + } + + if !ccg { + if !config.Clients[c].EnforcePAR { + validator.Push(fmt.Errorf(errFmtOIDCClientOptionMustScope, config.Clients[c].ID, "enforce_par", "'true'", oidc.ScopeAutheliaBearerAuthz, "false")) + } + + if !config.Clients[c].EnforcePKCE { + validator.Push(fmt.Errorf(errFmtOIDCClientOptionMustScope, config.Clients[c].ID, "enforce_pkce", "'true'", oidc.ScopeAutheliaBearerAuthz, "false")) + } else if config.Clients[c].PKCEChallengeMethod != oidc.PKCEChallengeMethodSHA256 { + validator.Push(fmt.Errorf(errFmtOIDCClientOptionMustScope, config.Clients[c].ID, attrOIDCPKCEChallengeMethod, "'"+oidc.PKCEChallengeMethodSHA256+"'", oidc.ScopeAutheliaBearerAuthz, config.Clients[c].PKCEChallengeMethod)) + } + + if config.Clients[c].ConsentMode != oidc.ClientConsentModeExplicit.String() { + validator.Push(fmt.Errorf(errFmtOIDCClientOptionMustScope, config.Clients[c].ID, "consent_mode", "'"+oidc.ClientConsentModeExplicit.String()+"'", oidc.ScopeAutheliaBearerAuthz, config.Clients[c].ConsentMode)) + } + + if len(config.Clients[c].ResponseTypes) == 0 { + validator.Push(fmt.Errorf(errFmtOIDCClientEmptyEntriesScope, config.Clients[c].ID, attrOIDCResponseTypes, strJoinAnd(validOIDCClientResponseTypesBearerAuthz), oidc.ScopeAutheliaBearerAuthz)) + } else if !utils.IsStringSliceContainsAll(config.Clients[c].ResponseTypes, validOIDCClientResponseTypesBearerAuthz) || + !utils.IsStringSliceContainsAny(config.Clients[c].ResponseTypes, validOIDCClientResponseTypesBearerAuthz) { + validator.Push(fmt.Errorf(errFmtOIDCClientInvalidEntriesScope, config.Clients[c].ID, attrOIDCResponseTypes, strJoinAnd(validOIDCClientResponseTypesBearerAuthz), oidc.ScopeAutheliaBearerAuthz, strJoinAnd(config.Clients[c].ResponseTypes))) + } + + if len(config.Clients[c].ResponseModes) == 0 { + validator.Push(fmt.Errorf(errFmtOIDCClientEmptyEntriesScope, config.Clients[c].ID, attrOIDCResponseModes, strJoinAnd(validOIDCClientResponseModesBearerAuthz), oidc.ScopeAutheliaBearerAuthz)) + } else if !utils.IsStringSliceContainsAll(config.Clients[c].ResponseModes, validOIDCClientResponseModesBearerAuthz) || + !utils.IsStringSliceContainsAny(config.Clients[c].ResponseModes, validOIDCClientResponseModesBearerAuthz) { + validator.Push(fmt.Errorf(errFmtOIDCClientInvalidEntriesScope, config.Clients[c].ID, attrOIDCResponseModes, strJoinAnd(validOIDCClientResponseModesBearerAuthz), oidc.ScopeAutheliaBearerAuthz, strJoinAnd(config.Clients[c].ResponseModes))) + } + } + + if config.Clients[c].Public { + if config.Clients[c].TokenEndpointAuthMethod != oidc.ClientAuthMethodNone { + validator.Push(fmt.Errorf(errFmtOIDCClientOptionMustScopeClientType, config.Clients[c].ID, attrOIDCTokenAuthMethod, "'"+oidc.ClientAuthMethodNone+"'", oidc.ScopeAutheliaBearerAuthz, "public", config.Clients[c].TokenEndpointAuthMethod)) + } + } else { + switch config.Clients[c].TokenEndpointAuthMethod { + case oidc.ClientAuthMethodClientSecretPost, oidc.ClientAuthMethodClientSecretJWT, oidc.ClientAuthMethodPrivateKeyJWT: + break + default: + validator.Push(fmt.Errorf(errFmtOIDCClientOptionMustScopeClientType, config.Clients[c].ID, attrOIDCTokenAuthMethod, strJoinOr([]string{oidc.ClientAuthMethodClientSecretPost, oidc.ClientAuthMethodClientSecretJWT, oidc.ClientAuthMethodPrivateKeyJWT}), oidc.ScopeAutheliaBearerAuthz, "confidential", config.Clients[c].TokenEndpointAuthMethod)) + } + } + + return false +} + +func validateOIDCClientScopesClientCredentialsGrant(c int, config *schema.IdentityProvidersOpenIDConnect, validator *schema.StructValidator) { invalid := validateListNotAllowed(config.Clients[c].Scopes, []string{oidc.ScopeOpenID, oidc.ScopeOffline, oidc.ScopeOfflineAccess}) if len(invalid) > 0 { @@ -637,8 +723,12 @@ func validateOIDCClientScopesClientCredentialsGrant(c int, config *schema.Identi } } -func validateOIDCClientResponseTypes(c int, config *schema.IdentityProvidersOpenIDConnect, validator *schema.StructValidator, errDeprecatedFunc func()) { +func validateOIDCClientResponseTypes(c int, config *schema.IdentityProvidersOpenIDConnect, validator *schema.StructValidator, setDefaults bool, errDeprecatedFunc func()) { if len(config.Clients[c].ResponseTypes) == 0 { + if !setDefaults { + return + } + config.Clients[c].ResponseTypes = schema.DefaultOpenIDConnectClientConfiguration.ResponseTypes } @@ -655,8 +745,12 @@ func validateOIDCClientResponseTypes(c int, config *schema.IdentityProvidersOpen } } -func validateOIDCClientResponseModes(c int, config *schema.IdentityProvidersOpenIDConnect, validator *schema.StructValidator, errDeprecatedFunc func()) { +func validateOIDCClientResponseModes(c int, config *schema.IdentityProvidersOpenIDConnect, validator *schema.StructValidator, setDefaults bool, errDeprecatedFunc func()) { if len(config.Clients[c].ResponseModes) == 0 { + if !setDefaults { + return + } + config.Clients[c].ResponseModes = schema.DefaultOpenIDConnectClientConfiguration.ResponseModes for _, responseType := range config.Clients[c].ResponseTypes { @@ -687,8 +781,12 @@ func validateOIDCClientResponseModes(c int, config *schema.IdentityProvidersOpen } } -func validateOIDCClientGrantTypes(c int, config *schema.IdentityProvidersOpenIDConnect, validator *schema.StructValidator, errDeprecatedFunc func()) { +func validateOIDCClientGrantTypes(c int, config *schema.IdentityProvidersOpenIDConnect, validator *schema.StructValidator, setDefaults bool, errDeprecatedFunc func()) { if len(config.Clients[c].GrantTypes) == 0 { + if !setDefaults { + return + } + validateOIDCClientGrantTypesSetDefaults(c, config) } diff --git a/internal/configuration/validator/identity_providers_test.go b/internal/configuration/validator/identity_providers_test.go index b3e276488..a5af2a01d 100644 --- a/internal/configuration/validator/identity_providers_test.go +++ b/internal/configuration/validator/identity_providers_test.go @@ -34,7 +34,31 @@ func TestShouldRaiseErrorWhenInvalidOIDCServerConfiguration(t *testing.T) { require.Len(t, validator.Errors(), 2) - assert.EqualError(t, validator.Errors()[0], "identity_providers: oidc: option `issuer_private_keys` or 'issuer_private_key' is required") + assert.EqualError(t, validator.Errors()[0], "identity_providers: oidc: option `issuer_private_keys` is required") + assert.EqualError(t, validator.Errors()[1], "identity_providers: oidc: option 'clients' must have one or more clients configured") +} + +func TestShouldRaiseErrorWhenInvalidOIDCServerConfigurationBothKeyTypesSpecified(t *testing.T) { + validator := schema.NewStructValidator() + config := &schema.IdentityProviders{ + OIDC: &schema.IdentityProvidersOpenIDConnect{ + HMACSecret: "abc", + IssuerPrivateKey: keyRSA2048, + IssuerPrivateKeys: []schema.JWK{ + { + Use: "sig", + Algorithm: "RS256", + Key: keyRSA4096, + }, + }, + }, + } + + ValidateIdentityProviders(config, validator) + + require.Len(t, validator.Errors(), 2) + + assert.EqualError(t, validator.Errors()[0], "identity_providers: oidc: option `issuer_private_keys` must not be configured at the same time as 'issuer_private_key' or 'issuer_certificate_chain'") assert.EqualError(t, validator.Errors()[1], "identity_providers: oidc: option 'clients' must have one or more clients configured") } @@ -188,6 +212,30 @@ func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) { }, }, { + name: "BadIDTooLong", + clients: []schema.IdentityProvidersOpenIDConnectClient{ + { + ID: "abc123abc123abc123abc123abc123abc123abc123abc123abc123abc123abc123abc123abc123abc123abc123abc123abc123abc123", + Secret: tOpenIDConnectPlainTextClientSecret, + }, + }, + errors: []string{ + "identity_providers: oidc: clients: client 'abc123abc123abc123abc123abc123abc123abc123abc123abc123abc123abc123abc123abc123abc123abc123abc123abc123abc123': option 'id' must not be more than 100 characters but it has 108 characters", + }, + }, + { + name: "BadIDInvalidCharacters", + clients: []schema.IdentityProvidersOpenIDConnectClient{ + { + ID: "@!#!@$!@#*()!&@%*(!^@#*()!&@^%!(_@#&", + Secret: tOpenIDConnectPlainTextClientSecret, + }, + }, + errors: []string{ + "identity_providers: oidc: clients: client '@!#!@$!@#*()!&@%*(!^@#*()!&@^%!(_@#&': option 'id' must only contain RFC3986 unreserved characters", + }, + }, + { name: "InvalidPolicy", clients: []schema.IdentityProvidersOpenIDConnectClient{ { @@ -204,6 +252,25 @@ func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) { }, }, { + name: "InvalidPolicyCCG", + clients: []schema.IdentityProvidersOpenIDConnectClient{ + { + ID: "client-1", + Secret: tOpenIDConnectPlainTextClientSecret, + AuthorizationPolicy: "a-policy", + RedirectURIs: []string{ + "https://google.com", + }, + GrantTypes: []string{ + oidc.GrantTypeClientCredentials, + }, + }, + }, + errors: []string{ + "identity_providers: oidc: clients: client 'client-1': option 'authorization_policy' must be one of 'one_factor' but it's configured as 'a-policy'", + }, + }, + { name: "ClientIDDuplicated", clients: []schema.IdentityProvidersOpenIDConnectClient{ { @@ -453,32 +520,6 @@ func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) { } } -func TestShouldRaiseErrorWhenOIDCClientConfiguredWithBadScopes(t *testing.T) { - validator := schema.NewStructValidator() - config := &schema.IdentityProviders{ - OIDC: &schema.IdentityProvidersOpenIDConnect{ - HMACSecret: "rLABDrx87et5KvRHVUgTm3pezWWd8LMN", - IssuerPrivateKey: keyRSA2048, - Clients: []schema.IdentityProvidersOpenIDConnectClient{ - { - ID: "good_id", - Secret: tOpenIDConnectPlainTextClientSecret, - AuthorizationPolicy: "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: clients: 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.IdentityProviders{ @@ -858,7 +899,7 @@ func TestValidateOIDCClients(t *testing.T) { nil, }, { - "ShouldIncludeMinimalScope", + "ShouldNotIncludeOldMinimalScope", nil, nil, tcv{ @@ -868,7 +909,7 @@ func TestValidateOIDCClients(t *testing.T) { nil, }, tcv{ - []string{oidc.ScopeOpenID, oidc.ScopeEmail}, + []string{oidc.ScopeEmail}, []string{oidc.ResponseTypeAuthorizationCodeFlow}, []string{oidc.ResponseModeFormPost, oidc.ResponseModeQuery}, []string{oidc.GrantTypeAuthorizationCode}, @@ -1031,7 +1072,7 @@ func TestValidateOIDCClients(t *testing.T) { nil, }, { - "ShouldRaiseErrorOnInvalidScopes", + "ShouldRaiseWarningOnInvalidScopes", nil, nil, tcv{ @@ -1046,10 +1087,10 @@ func TestValidateOIDCClients(t *testing.T) { []string{oidc.ResponseModeFormPost, oidc.ResponseModeQuery}, []string{oidc.GrantTypeAuthorizationCode}, }, - nil, []string{ - "identity_providers: oidc: clients: client 'test': option 'scopes' must only have the values 'openid', 'email', 'profile', 'groups', or 'offline_access' but the values 'group' are present", + "identity_providers: oidc: clients: client 'test': option 'scopes' only expects the values 'openid', 'email', 'profile', 'groups', 'offline_access', 'offline', or 'authelia.bearer.authz' but the unknown values 'group' are present and should generally only be used if a particular client requires a scope outside of our standard scopes", }, + nil, }, { "ShouldRaiseErrorOnMissingAuthorizationCodeFlowResponseTypeWithRefreshTokenValues", @@ -1290,33 +1331,8 @@ func TestValidateOIDCClients(t *testing.T) { "identity_providers: oidc: clients: 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", }, []string{ - "identity_providers: oidc: clients: client 'test': option 'scopes' has the values 'openid', 'offline', and 'offline_access' however when exclusively utilizing the 'client_credentials' value for the 'grant_types' the values 'openid', 'offline', or 'offline_access' are not allowed", - }, - }, - { - "ShouldNotRestrictRefreshOpenIDScopesWithMultipleGrantTypesAndAllowCustomClientCredentials", - func(have *schema.IdentityProvidersOpenIDConnect) { - have.Clients[0].Public = false - have.Clients[0].Scopes = []string{oidc.ScopeOpenID, oidc.ScopeOffline, oidc.ScopeOfflineAccess, "custom"} - }, - nil, - tcv{ - nil, - nil, - nil, - []string{oidc.GrantTypeClientCredentials, oidc.GrantTypeImplicit}, - }, - tcv{ - []string{oidc.ScopeOpenID, oidc.ScopeOffline, oidc.ScopeOfflineAccess, "custom"}, - []string{oidc.ResponseTypeAuthorizationCodeFlow}, - []string{oidc.ResponseModeFormPost, oidc.ResponseModeQuery}, - []string{oidc.GrantTypeClientCredentials, oidc.GrantTypeImplicit}, - }, - []string{ - "identity_providers: oidc: clients: 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: clients: 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'", + "identity_providers: oidc: clients: client 'test': option 'scopes' has the values 'openid', 'offline', and 'offline_access' however when utilizing the 'client_credentials' value for the 'grant_types' the values 'openid', 'offline', or 'offline_access' are not allowed", }, - nil, }, { "ShouldRaiseErrorOnGrantTypeRefreshTokenWithoutScopeOfflineAccess", @@ -2062,6 +2078,262 @@ func TestValidateOIDCClients(t *testing.T) { }, }, { + "ShouldHandleBearerErrorsMisconfiguredPublicClientType", + func(have *schema.IdentityProvidersOpenIDConnect) { + have.Clients[0] = schema.IdentityProvidersOpenIDConnectClient{ + ID: "abc", + Secret: nil, + Public: true, + RedirectURIs: []string{"http://localhost"}, + Audience: nil, + Scopes: []string{oidc.ScopeAutheliaBearerAuthz, oidc.ScopeOpenID}, + GrantTypes: []string{oidc.GrantTypeImplicit}, + ResponseTypes: []string{oidc.ResponseTypeImplicitFlowBoth}, + ResponseModes: []string{oidc.ResponseModeQuery}, + AuthorizationPolicy: "", + RequestedAudienceMode: "", + ConsentMode: oidc.ClientConsentModeImplicit.String(), + EnforcePAR: false, + EnforcePKCE: false, + PKCEChallengeMethod: "", + TokenEndpointAuthMethod: "", + } + }, + nil, + tcv{ + nil, + nil, + nil, + nil, + }, + tcv{ + []string{oidc.ScopeAutheliaBearerAuthz, oidc.ScopeOpenID}, + []string{oidc.ResponseTypeImplicitFlowBoth}, + []string{oidc.ResponseModeQuery}, + []string{oidc.GrantTypeImplicit}, + }, + nil, + []string{ + "identity_providers: oidc: clients: client 'abc': option 'scopes' must only have the values 'offline_access', 'offline', and 'authelia.bearer.authz' when configured with scope 'authelia.bearer.authz' but the values 'authelia.bearer.authz' and 'openid' are present", + "identity_providers: oidc: clients: client 'abc': option 'grant_types' must only have the values 'authorization_code', 'refresh_token', and 'client_credentials' when configured with scope 'authelia.bearer.authz' but the values 'implicit' are present", + "identity_providers: oidc: clients: client 'abc': option 'audience' must be configured when configured with scope 'authelia.bearer.authz' but it's absent", + "identity_providers: oidc: clients: client 'abc': option 'enforce_par' must be configured as 'true' when configured with scope 'authelia.bearer.authz' but it's configured as 'false'", + "identity_providers: oidc: clients: client 'abc': option 'enforce_pkce' must be configured as 'true' when configured with scope 'authelia.bearer.authz' but it's configured as 'false'", + "identity_providers: oidc: clients: client 'abc': option 'consent_mode' must be configured as 'explicit' when configured with scope 'authelia.bearer.authz' but it's configured as 'implicit'", + "identity_providers: oidc: clients: client 'abc': option 'response_types' must only have the values 'code' when configured with scope 'authelia.bearer.authz' but the values 'id_token token' are present", + "identity_providers: oidc: clients: client 'abc': option 'response_modes' must only have the values 'form_post' and 'form_post.jwt' when configured with scope 'authelia.bearer.authz' but the values 'query' are present", + "identity_providers: oidc: clients: client 'abc': option 'token_endpoint_auth_method' must be configured as 'none' when configured with scope 'authelia.bearer.authz' and the 'public' client type but it's configured as ''", + }, + }, + { + "ShouldHandleBearerErrorsMisconfiguredConfidentialClientType", + func(have *schema.IdentityProvidersOpenIDConnect) { + have.Clients[0] = schema.IdentityProvidersOpenIDConnectClient{ + ID: "abc", + Secret: tOpenIDConnectPBKDF2ClientSecret, + Public: false, + RedirectURIs: []string{"http://localhost"}, + Audience: nil, + Scopes: []string{oidc.ScopeAutheliaBearerAuthz, oidc.ScopeOpenID}, + GrantTypes: []string{oidc.GrantTypeImplicit}, + ResponseTypes: []string{oidc.ResponseTypeImplicitFlowBoth}, + ResponseModes: []string{oidc.ResponseModeQuery}, + AuthorizationPolicy: "", + RequestedAudienceMode: "", + ConsentMode: oidc.ClientConsentModeImplicit.String(), + EnforcePAR: false, + EnforcePKCE: true, + PKCEChallengeMethod: "", + TokenEndpointAuthMethod: "", + } + }, + nil, + tcv{ + nil, + nil, + nil, + nil, + }, + tcv{ + []string{oidc.ScopeAutheliaBearerAuthz, oidc.ScopeOpenID}, + []string{oidc.ResponseTypeImplicitFlowBoth}, + []string{oidc.ResponseModeQuery}, + []string{oidc.GrantTypeImplicit}, + }, + nil, + []string{ + "identity_providers: oidc: clients: client 'abc': option 'scopes' must only have the values 'offline_access', 'offline', and 'authelia.bearer.authz' when configured with scope 'authelia.bearer.authz' but the values 'authelia.bearer.authz' and 'openid' are present", + "identity_providers: oidc: clients: client 'abc': option 'grant_types' must only have the values 'authorization_code', 'refresh_token', and 'client_credentials' when configured with scope 'authelia.bearer.authz' but the values 'implicit' are present", + "identity_providers: oidc: clients: client 'abc': option 'audience' must be configured when configured with scope 'authelia.bearer.authz' but it's absent", + "identity_providers: oidc: clients: client 'abc': option 'enforce_par' must be configured as 'true' when configured with scope 'authelia.bearer.authz' but it's configured as 'false'", + "identity_providers: oidc: clients: client 'abc': option 'pkce_challenge_method' must be configured as 'S256' when configured with scope 'authelia.bearer.authz' but it's configured as ''", + "identity_providers: oidc: clients: client 'abc': option 'consent_mode' must be configured as 'explicit' when configured with scope 'authelia.bearer.authz' but it's configured as 'implicit'", + "identity_providers: oidc: clients: client 'abc': option 'response_types' must only have the values 'code' when configured with scope 'authelia.bearer.authz' but the values 'id_token token' are present", + "identity_providers: oidc: clients: client 'abc': option 'response_modes' must only have the values 'form_post' and 'form_post.jwt' when configured with scope 'authelia.bearer.authz' but the values 'query' are present", + "identity_providers: oidc: clients: client 'abc': option 'token_endpoint_auth_method' must be configured as 'client_secret_post', 'client_secret_jwt', or 'private_key_jwt' when configured with scope 'authelia.bearer.authz' and the 'confidential' client type but it's configured as ''", + }, + }, + { + "ShouldHandleBearerErrorsMisconfiguredConfidentialClientTypeClientCredentials", + func(have *schema.IdentityProvidersOpenIDConnect) { + have.Clients[0] = schema.IdentityProvidersOpenIDConnectClient{ + ID: "abc", + Secret: tOpenIDConnectPBKDF2ClientSecret, + Public: false, + RedirectURIs: []string{"http://localhost"}, + Audience: nil, + Scopes: []string{oidc.ScopeAutheliaBearerAuthz, oidc.ScopeOpenID}, + GrantTypes: []string{oidc.GrantTypeClientCredentials}, + ResponseTypes: nil, + ResponseModes: nil, + AuthorizationPolicy: "", + RequestedAudienceMode: "", + ConsentMode: oidc.ClientConsentModeImplicit.String(), + EnforcePAR: false, + EnforcePKCE: true, + PKCEChallengeMethod: "", + TokenEndpointAuthMethod: "", + } + }, + nil, + tcv{ + nil, + nil, + nil, + nil, + }, + tcv{ + []string{oidc.ScopeAutheliaBearerAuthz, oidc.ScopeOpenID}, + []string(nil), + []string(nil), + []string{oidc.GrantTypeClientCredentials}, + }, + nil, + []string{ + "identity_providers: oidc: clients: client 'abc': option 'scopes' must only have the values 'offline_access', 'offline', and 'authelia.bearer.authz' when configured with scope 'authelia.bearer.authz' but the values 'authelia.bearer.authz' and 'openid' are present", + "identity_providers: oidc: clients: client 'abc': option 'audience' must be configured when configured with scope 'authelia.bearer.authz' but it's absent", + "identity_providers: oidc: clients: client 'abc': option 'token_endpoint_auth_method' must be configured as 'client_secret_post', 'client_secret_jwt', or 'private_key_jwt' when configured with scope 'authelia.bearer.authz' and the 'confidential' client type but it's configured as ''", + "identity_providers: oidc: clients: client 'abc': option 'scopes' has the values 'authelia.bearer.authz' and 'openid' however when utilizing the 'client_credentials' value for the 'grant_types' the values 'openid' are not allowed", + }, + }, + { + "ShouldHandleBearerErrorsNotExplicit", + func(have *schema.IdentityProvidersOpenIDConnect) { + have.Clients[0] = schema.IdentityProvidersOpenIDConnectClient{ + ID: "abc", + Secret: nil, + Public: true, + RedirectURIs: []string{"http://localhost"}, + Audience: nil, + Scopes: []string{oidc.ScopeAutheliaBearerAuthz}, + EnforcePAR: false, + EnforcePKCE: false, + PKCEChallengeMethod: "", + TokenEndpointAuthMethod: "", + } + }, + nil, + tcv{ + nil, + nil, + nil, + nil, + }, + tcv{ + []string{oidc.ScopeAutheliaBearerAuthz}, + nil, + nil, + nil, + }, + nil, + []string{ + "identity_providers: oidc: clients: client 'abc': option 'grant_types' must only have the values 'authorization_code', 'refresh_token', and 'client_credentials' when configured with scope 'authelia.bearer.authz' but it's not configured", + "identity_providers: oidc: clients: client 'abc': option 'audience' must be configured when configured with scope 'authelia.bearer.authz' but it's absent", + "identity_providers: oidc: clients: client 'abc': option 'enforce_par' must be configured as 'true' when configured with scope 'authelia.bearer.authz' but it's configured as 'false'", + "identity_providers: oidc: clients: client 'abc': option 'enforce_pkce' must be configured as 'true' when configured with scope 'authelia.bearer.authz' but it's configured as 'false'", + "identity_providers: oidc: clients: client 'abc': option 'consent_mode' must be configured as 'explicit' when configured with scope 'authelia.bearer.authz' but it's configured as ''", + "identity_providers: oidc: clients: client 'abc': option 'response_types' must only have the values 'code' when configured with scope 'authelia.bearer.authz' but it's not configured", + "identity_providers: oidc: clients: client 'abc': option 'response_modes' must only have the values 'form_post' and 'form_post.jwt' when configured with scope 'authelia.bearer.authz' but it's not configured", + "identity_providers: oidc: clients: client 'abc': option 'token_endpoint_auth_method' must be configured as 'none' when configured with scope 'authelia.bearer.authz' and the 'public' client type but it's configured as ''", + }, + }, + { + "ShouldHandleBearerValidConfidentialClientType", + func(have *schema.IdentityProvidersOpenIDConnect) { + have.Clients[0] = schema.IdentityProvidersOpenIDConnectClient{ + ID: "abc", + Secret: tOpenIDConnectPBKDF2ClientSecret, + Public: false, + RedirectURIs: []string{"http://localhost"}, + Audience: []string{"https://app.example.com"}, + Scopes: []string{oidc.ScopeAutheliaBearerAuthz, oidc.ScopeOfflineAccess}, + GrantTypes: []string{oidc.GrantTypeAuthorizationCode, oidc.GrantTypeRefreshToken}, + ResponseTypes: []string{oidc.ResponseTypeAuthorizationCodeFlow}, + ResponseModes: []string{oidc.ResponseModeFormPost, oidc.ResponseModeFormPostJWT}, + AuthorizationPolicy: "", + RequestedAudienceMode: "", + ConsentMode: oidc.ClientConsentModeExplicit.String(), + EnforcePAR: true, + EnforcePKCE: true, + PKCEChallengeMethod: oidc.PKCEChallengeMethodSHA256, + TokenEndpointAuthMethod: oidc.ClientAuthMethodClientSecretPost, + } + }, + nil, + tcv{ + nil, + nil, + nil, + nil, + }, + tcv{ + []string{oidc.ScopeAutheliaBearerAuthz, oidc.ScopeOfflineAccess}, + []string{oidc.ResponseTypeAuthorizationCodeFlow}, + []string{oidc.ResponseModeFormPost, oidc.ResponseModeFormPostJWT}, + []string{oidc.GrantTypeAuthorizationCode, oidc.GrantTypeRefreshToken}, + }, + nil, + nil, + }, + { + "ShouldHandleBearerValidPublicClientType", + func(have *schema.IdentityProvidersOpenIDConnect) { + have.Clients[0] = schema.IdentityProvidersOpenIDConnectClient{ + ID: "abc", + Secret: nil, + Public: true, + RedirectURIs: []string{"http://localhost"}, + Audience: []string{"https://app.example.com"}, + Scopes: []string{oidc.ScopeAutheliaBearerAuthz, oidc.ScopeOfflineAccess}, + GrantTypes: []string{oidc.GrantTypeAuthorizationCode, oidc.GrantTypeRefreshToken}, + ResponseTypes: []string{oidc.ResponseTypeAuthorizationCodeFlow}, + ResponseModes: []string{oidc.ResponseModeFormPost}, + AuthorizationPolicy: "", + RequestedAudienceMode: "", + ConsentMode: oidc.ClientConsentModeExplicit.String(), + EnforcePAR: true, + EnforcePKCE: true, + PKCEChallengeMethod: oidc.PKCEChallengeMethodSHA256, + TokenEndpointAuthMethod: oidc.ClientAuthMethodNone, + } + }, + nil, + tcv{ + nil, + nil, + nil, + nil, + }, + tcv{ + []string{oidc.ScopeAutheliaBearerAuthz, oidc.ScopeOfflineAccess}, + []string{oidc.ResponseTypeAuthorizationCodeFlow}, + []string{oidc.ResponseModeFormPost}, + []string{oidc.GrantTypeAuthorizationCode, oidc.GrantTypeRefreshToken}, + }, + nil, + nil, + }, + { "ShouldSetDefaultConsentMode", nil, func(t *testing.T, have *schema.IdentityProvidersOpenIDConnect) { diff --git a/internal/configuration/validator/server.go b/internal/configuration/validator/server.go index 90c4ab744..89b96d6ab 100644 --- a/internal/configuration/validator/server.go +++ b/internal/configuration/validator/server.go @@ -174,7 +174,7 @@ func ValidateServerEndpoints(config *schema.Configuration, validator *schema.Str } switch oEndpoint.Implementation { - case authzImplementationLegacy, authzImplementationExtAuthz: + case schema.AuthzImplementationLegacy, schema.AuthzImplementationExtAuthz: if strings.HasPrefix(name, oName+"/") { validator.Push(fmt.Errorf(errFmtServerEndpointsAuthzPrefixDuplicate, name, oName, oEndpoint.Implementation)) } @@ -183,17 +183,17 @@ func ValidateServerEndpoints(config *schema.Configuration, validator *schema.Str } } - validateServerEndpointsAuthzStrategies(name, endpoint.AuthnStrategies, validator) + validateServerEndpointsAuthzStrategies(name, endpoint.Implementation, endpoint.AuthnStrategies, validator) } } func validateServerEndpointsAuthzEndpoint(config *schema.Configuration, name string, endpoint schema.ServerEndpointsAuthz, validator *schema.StructValidator) { if name == legacy { switch endpoint.Implementation { - case authzImplementationLegacy: + case schema.AuthzImplementationLegacy: break case "": - endpoint.Implementation = authzImplementationLegacy + endpoint.Implementation = schema.AuthzImplementationLegacy config.Server.Endpoints.Authz[name] = endpoint default: @@ -212,18 +212,55 @@ func validateServerEndpointsAuthzEndpoint(config *schema.Configuration, name str } } -func validateServerEndpointsAuthzStrategies(name string, strategies []schema.ServerEndpointsAuthzAuthnStrategy, validator *schema.StructValidator) { +//nolint:gocyclo +func validateServerEndpointsAuthzStrategies(name, implementation string, strategies []schema.ServerEndpointsAuthzAuthnStrategy, validator *schema.StructValidator) { + var defaults []schema.ServerEndpointsAuthzAuthnStrategy + + switch implementation { + case schema.AuthzImplementationLegacy: + defaults = schema.DefaultServerConfiguration.Endpoints.Authz[schema.AuthzEndpointNameLegacy].AuthnStrategies + case schema.AuthzImplementationAuthRequest: + defaults = schema.DefaultServerConfiguration.Endpoints.Authz[schema.AuthzEndpointNameAuthRequest].AuthnStrategies + case schema.AuthzImplementationExtAuthz: + defaults = schema.DefaultServerConfiguration.Endpoints.Authz[schema.AuthzEndpointNameExtAuthz].AuthnStrategies + case schema.AuthzImplementationForwardAuth: + defaults = schema.DefaultServerConfiguration.Endpoints.Authz[schema.AuthzEndpointNameForwardAuth].AuthnStrategies + } + + if len(strategies) == 0 { + copy(strategies, defaults) + + return + } + names := make([]string, len(strategies)) - for _, strategy := range strategies { - if utils.IsStringInSlice(strategy.Name, names) { + for i, strategy := range strategies { + if strategy.Name != "" && utils.IsStringInSlice(strategy.Name, names) { validator.Push(fmt.Errorf(errFmtServerEndpointsAuthzStrategyDuplicate, name, strategy.Name)) } names = append(names, strategy.Name) - if !utils.IsStringInSlice(strategy.Name, validAuthzAuthnStrategies) { + switch { + case strategy.Name == "": + validator.Push(fmt.Errorf(errFmtServerEndpointsAuthzStrategyNoName, name, i+1)) + case !utils.IsStringInSlice(strategy.Name, validAuthzAuthnStrategies): validator.Push(fmt.Errorf(errFmtServerEndpointsAuthzStrategy, name, strJoinOr(validAuthzAuthnStrategies), strategy.Name)) + default: + if utils.IsStringInSlice(strategy.Name, validAuthzAuthnHeaderStrategies) { + if len(strategy.Schemes) == 0 { + strategies[i].Schemes = defaults[0].Schemes + } else { + for _, scheme := range strategy.Schemes { + if !utils.IsStringInSliceFold(scheme, validAuthzAuthnStrategySchemes) { + validator.Push(fmt.Errorf(errFmtServerEndpointsAuthzSchemes, name, i+1, strategy.Name, strJoinOr(validAuthzAuthnStrategySchemes), scheme)) + } + } + } + } else if len(strategy.Schemes) != 0 { + validator.Push(fmt.Errorf(errFmtServerEndpointsAuthzSchemesInvalidForStrategy, name, i+1, strategy.Name)) + } } } } diff --git a/internal/configuration/validator/server_test.go b/internal/configuration/validator/server_test.go index c425bb420..231efe9c2 100644 --- a/internal/configuration/validator/server_test.go +++ b/internal/configuration/validator/server_test.go @@ -489,6 +489,38 @@ func TestServerAuthzEndpointErrors(t *testing.T) { []string{"server: endpoints: authz: example: authn_strategies: duplicate strategy name detected with name 'CookieSession'"}, }, { + "ShouldErrorOnSchemesForInvalidStrategy", + map[string]schema.ServerEndpointsAuthz{ + "example": {Implementation: "ForwardAuth", AuthnStrategies: []schema.ServerEndpointsAuthzAuthnStrategy{{Name: "CookieSession", Schemes: []string{"basic"}}}}, + }, + []string{"server: endpoints: authz: example: authn_strategies: strategy #1 (CookieSession): option 'schemes' is not valid for the strategy"}, + }, + { + "ShouldNotErrorOnSchemeCase", + map[string]schema.ServerEndpointsAuthz{ + "example": {Implementation: "ForwardAuth", AuthnStrategies: []schema.ServerEndpointsAuthzAuthnStrategy{{Name: "HeaderAuthorization", Schemes: []string{"basIc"}}}}, + }, + nil, + }, + { + "ShouldErrorOnInvalidStrategySchemesAndUnnamedStrategy", + map[string]schema.ServerEndpointsAuthz{ + "example": {Implementation: "ForwardAuth", AuthnStrategies: []schema.ServerEndpointsAuthzAuthnStrategy{{Name: "HeaderAuthorization", Schemes: []string{"basic", "bearer", "abc"}}}}, + }, + []string{ + "server: endpoints: authz: example: authn_strategies: strategy #1 (HeaderAuthorization): option 'schemes' must only include the values 'basic' or 'bearer' but has 'abc'", + }, + }, + { + "ShouldErrorOnUnnamedStrategy", + map[string]schema.ServerEndpointsAuthz{ + "example": {Implementation: "ForwardAuth", AuthnStrategies: []schema.ServerEndpointsAuthzAuthnStrategy{{Name: "", Schemes: []string{"basic", "bearer", "abc"}}}}, + }, + []string{ + "server: endpoints: authz: example: authn_strategies: strategy #1: option 'name' must be configured", + }, + }, + { "ShouldErrorOnInvalidChars", map[string]schema.ServerEndpointsAuthz{ "/abc": {Implementation: "ForwardAuth"}, @@ -567,6 +599,53 @@ func TestServerAuthzEndpointErrors(t *testing.T) { } } +func TestServerAuthzEndpointDefaults(t *testing.T) { + testCases := []struct { + name string + have map[string]schema.ServerEndpointsAuthz + expected map[string]schema.ServerEndpointsAuthz + }{ + { + "ShouldSetDefaultSchemes", + map[string]schema.ServerEndpointsAuthz{ + "example": {Implementation: "ForwardAuth", AuthnStrategies: []schema.ServerEndpointsAuthzAuthnStrategy{ + { + Name: "HeaderAuthorization", + Schemes: []string{}, + }, + }}, + }, + map[string]schema.ServerEndpointsAuthz{ + "example": {Implementation: "ForwardAuth", AuthnStrategies: []schema.ServerEndpointsAuthzAuthnStrategy{ + { + Name: "HeaderAuthorization", + Schemes: []string{"basic"}, + }, + }}, + }, + }, + } + + validator := schema.NewStructValidator() + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + validator.Clear() + + config := newDefaultConfig() + + config.Server.Endpoints.Authz = tc.have + + ValidateServerEndpoints(&config, validator) + + assert.Len(t, validator.Warnings(), 0) + assert.Len(t, validator.Errors(), 0) + + assert.Equal(t, tc.expected, config.Server.Endpoints.Authz) + }) + } +} + func TestServerAuthzEndpointLegacyAsImplementationLegacyWhenBlank(t *testing.T) { have := map[string]schema.ServerEndpointsAuthz{ "legacy": {}, @@ -583,7 +662,7 @@ func TestServerAuthzEndpointLegacyAsImplementationLegacyWhenBlank(t *testing.T) assert.Len(t, validator.Warnings(), 0) assert.Len(t, validator.Errors(), 0) - assert.Equal(t, authzImplementationLegacy, config.Server.Endpoints.Authz[legacy].Implementation) + assert.Equal(t, schema.AuthzImplementationLegacy, config.Server.Endpoints.Authz[legacy].Implementation) } func TestValidateTLSPathStatInvalidArgument(t *testing.T) { diff --git a/internal/configuration/validator/util.go b/internal/configuration/validator/util.go index 67403dd90..0d6955570 100644 --- a/internal/configuration/validator/util.go +++ b/internal/configuration/validator/util.go @@ -92,7 +92,7 @@ func validateListNotAllowed(values, filter []string) (invalid []string) { return invalid } -func validateList(values, valid []string, chkDuplicate bool) (invalid, duplicates []string) { //nolint:unparam +func validateList(values, valid []string, chkDuplicate bool) (invalid, duplicates []string) { chkValid := len(valid) != 0 for i, value := range values { |
