diff options
Diffstat (limited to 'internal/configuration/validator')
| -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 |
7 files changed, 625 insertions, 115 deletions
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 { |
