authelia/internal/configuration/validator/identity_providers.go

862 lines
36 KiB
Go
Raw Normal View History

package validator
import (
"crypto/ecdsa"
"crypto/rsa"
"fmt"
"net/url"
"sort"
"strconv"
"strings"
"time"
"github.com/ory/fosite"
"github.com/authelia/authelia/v4/internal/configuration/schema"
"github.com/authelia/authelia/v4/internal/oidc"
"github.com/authelia/authelia/v4/internal/utils"
)
// ValidateIdentityProviders validates and updates the IdentityProviders configuration.
func ValidateIdentityProviders(config *schema.IdentityProviders, val *schema.StructValidator) {
validateOIDC(config.OIDC, val)
}
func validateOIDC(config *schema.OpenIDConnect, val *schema.StructValidator) {
if config == nil {
return
}
setOIDCDefaults(config)
validateOIDCIssuer(config, val)
validateOIDCPolicies(config, val)
sort.Sort(oidc.SortedSigningAlgs(config.Discovery.ResponseObjectSigningAlgs))
switch {
case config.MinimumParameterEntropy == -1:
val.PushWarning(fmt.Errorf(errFmtOIDCProviderInsecureDisabledParameterEntropy))
case config.MinimumParameterEntropy <= 0:
config.MinimumParameterEntropy = fosite.MinParameterEntropy
case config.MinimumParameterEntropy < fosite.MinParameterEntropy:
val.PushWarning(fmt.Errorf(errFmtOIDCProviderInsecureParameterEntropy, fosite.MinParameterEntropy, config.MinimumParameterEntropy))
}
switch config.EnforcePKCE {
case "always", "never", "public_clients_only":
break
default:
val.Push(fmt.Errorf(errFmtOIDCProviderEnforcePKCEInvalidValue, config.EnforcePKCE))
}
validateOIDCOptionsCORS(config, val)
if len(config.Clients) == 0 {
val.Push(fmt.Errorf(errFmtOIDCProviderNoClientsConfigured))
} else {
validateOIDCClients(config, val)
}
}
func validateOIDCPolicies(config *schema.OpenIDConnect, val *schema.StructValidator) {
config.Discovery.Policies = []string{policyOneFactor, policyTwoFactor}
for name, policy := range config.Policies {
switch name {
case "":
val.Push(fmt.Errorf("policy must have a name"))
case policyOneFactor, policyTwoFactor:
val.Push(fmt.Errorf("policy names can't be named one_factor or two_factor"))
default:
break
}
switch policy.DefaultPolicy {
case "":
policy.DefaultPolicy = policyTwoFactor
case policyOneFactor, policyTwoFactor:
break
default:
val.Push(fmt.Errorf("policy must be one of one_factor or two_factor"))
}
if len(policy.Rules) == 0 {
val.Push(fmt.Errorf("policy must include at least one rule"))
}
for i, rule := range policy.Rules {
switch rule.Policy {
case "":
policy.Rules[i].Policy = policyTwoFactor
case policyOneFactor, policyTwoFactor:
break
default:
val.Push(fmt.Errorf("policy must be one of one_factor or two_factor"))
}
if len(rule.Subjects) == 0 {
val.Push(fmt.Errorf("policy must include at least one criteria"))
}
}
config.Policies[name] = policy
config.Discovery.Policies = append(config.Discovery.Policies, name)
}
}
func validateOIDCIssuer(config *schema.OpenIDConnect, val *schema.StructValidator) {
switch {
case config.IssuerPrivateKey != nil:
validateOIDCIssuerPrivateKey(config)
fallthrough
case len(config.IssuerPrivateKeys) != 0:
validateOIDCIssuerPrivateKeys(config, val)
default:
val.Push(fmt.Errorf(errFmtOIDCProviderNoPrivateKey))
}
}
func validateOIDCIssuerPrivateKey(config *schema.OpenIDConnect) {
config.IssuerPrivateKeys = append([]schema.JWK{{
Algorithm: oidc.SigningAlgRSAUsingSHA256,
Use: oidc.KeyUseSignature,
Key: config.IssuerPrivateKey,
CertificateChain: config.IssuerCertificateChain,
}}, config.IssuerPrivateKeys...)
}
func validateOIDCIssuerPrivateKeys(config *schema.OpenIDConnect, val *schema.StructValidator) {
var (
props *JWKProperties
err error
)
config.Discovery.ResponseObjectSigningKeyIDs = make([]string, len(config.IssuerPrivateKeys))
config.Discovery.DefaultKeyIDs = map[string]string{}
for i := 0; i < len(config.IssuerPrivateKeys); i++ {
if key, ok := config.IssuerPrivateKeys[i].Key.(*rsa.PrivateKey); ok && key.PublicKey.N == nil {
val.Push(fmt.Errorf(errFmtOIDCProviderPrivateKeysInvalid, i+1))
continue
}
switch n := len(config.IssuerPrivateKeys[i].KeyID); {
case n == 0:
if config.IssuerPrivateKeys[i].KeyID, err = jwkCalculateThumbprint(config.IssuerPrivateKeys[i].Key); err != nil {
val.Push(fmt.Errorf(errFmtOIDCProviderPrivateKeysCalcThumbprint, i+1, err))
continue
}
case n > 100:
val.Push(fmt.Errorf(errFmtOIDCProviderPrivateKeysKeyIDLength, i+1, config.IssuerPrivateKeys[i].KeyID))
}
if config.IssuerPrivateKeys[i].KeyID != "" && utils.IsStringInSlice(config.IssuerPrivateKeys[i].KeyID, config.Discovery.ResponseObjectSigningKeyIDs) {
val.Push(fmt.Errorf(errFmtOIDCProviderPrivateKeysAttributeNotUnique, i+1, config.IssuerPrivateKeys[i].KeyID, attrOIDCKeyID))
}
config.Discovery.ResponseObjectSigningKeyIDs[i] = config.IssuerPrivateKeys[i].KeyID
if !reOpenIDConnectKID.MatchString(config.IssuerPrivateKeys[i].KeyID) {
val.Push(fmt.Errorf(errFmtOIDCProviderPrivateKeysKeyIDNotValid, i+1, config.IssuerPrivateKeys[i].KeyID))
}
if props, err = schemaJWKGetProperties(config.IssuerPrivateKeys[i]); err != nil {
val.Push(fmt.Errorf(errFmtOIDCProviderPrivateKeysProperties, i+1, config.IssuerPrivateKeys[i].KeyID, err))
continue
}
validateOIDCIssuerPrivateKeysUseAlg(i, props, config, val)
validateOIDCIssuerPrivateKeyPair(i, config, val)
}
if len(config.Discovery.ResponseObjectSigningAlgs) != 0 && !utils.IsStringInSlice(oidc.SigningAlgRSAUsingSHA256, config.Discovery.ResponseObjectSigningAlgs) {
val.Push(fmt.Errorf(errFmtOIDCProviderPrivateKeysNoRS256, oidc.SigningAlgRSAUsingSHA256, strJoinAnd(config.Discovery.ResponseObjectSigningAlgs)))
}
}
func validateOIDCIssuerPrivateKeysUseAlg(i int, props *JWKProperties, config *schema.OpenIDConnect, val *schema.StructValidator) {
switch config.IssuerPrivateKeys[i].Use {
case "":
config.IssuerPrivateKeys[i].Use = props.Use
case oidc.KeyUseSignature:
break
default:
val.Push(fmt.Errorf(errFmtOIDCProviderPrivateKeysInvalidOptionOneOf, i+1, config.IssuerPrivateKeys[i].KeyID, attrOIDCKeyUse, strJoinOr([]string{oidc.KeyUseSignature}), config.IssuerPrivateKeys[i].Use))
}
switch {
case config.IssuerPrivateKeys[i].Algorithm == "":
config.IssuerPrivateKeys[i].Algorithm = props.Algorithm
fallthrough
case utils.IsStringInSlice(config.IssuerPrivateKeys[i].Algorithm, validOIDCIssuerJWKSigningAlgs):
if config.IssuerPrivateKeys[i].KeyID != "" && config.IssuerPrivateKeys[i].Algorithm != "" {
if _, ok := config.Discovery.DefaultKeyIDs[config.IssuerPrivateKeys[i].Algorithm]; !ok {
config.Discovery.DefaultKeyIDs[config.IssuerPrivateKeys[i].Algorithm] = config.IssuerPrivateKeys[i].KeyID
}
}
default:
val.Push(fmt.Errorf(errFmtOIDCProviderPrivateKeysInvalidOptionOneOf, i+1, config.IssuerPrivateKeys[i].KeyID, attrOIDCAlgorithm, strJoinOr(validOIDCIssuerJWKSigningAlgs), config.IssuerPrivateKeys[i].Algorithm))
}
if config.IssuerPrivateKeys[i].Algorithm != "" {
if !utils.IsStringInSlice(config.IssuerPrivateKeys[i].Algorithm, config.Discovery.ResponseObjectSigningAlgs) {
config.Discovery.ResponseObjectSigningAlgs = append(config.Discovery.ResponseObjectSigningAlgs, config.IssuerPrivateKeys[i].Algorithm)
}
}
}
func validateOIDCIssuerPrivateKeyPair(i int, config *schema.OpenIDConnect, val *schema.StructValidator) {
var (
checkEqualKey bool
err error
)
switch key := config.IssuerPrivateKeys[i].Key.(type) {
case *rsa.PrivateKey:
checkEqualKey = true
if key.Size() < 256 {
checkEqualKey = false
val.Push(fmt.Errorf(errFmtOIDCProviderPrivateKeysRSAKeyLessThan2048Bits, i+1, config.IssuerPrivateKeys[i].KeyID, key.Size()*8))
}
case *ecdsa.PrivateKey:
checkEqualKey = true
default:
val.Push(fmt.Errorf(errFmtOIDCProviderPrivateKeysKeyNotRSAOrECDSA, i+1, config.IssuerPrivateKeys[i].KeyID, key))
}
if config.IssuerPrivateKeys[i].CertificateChain.HasCertificates() {
if checkEqualKey && !config.IssuerPrivateKeys[i].CertificateChain.EqualKey(config.IssuerPrivateKeys[i].Key) {
val.Push(fmt.Errorf(errFmtOIDCProviderPrivateKeysKeyCertificateMismatch, i+1, config.IssuerPrivateKeys[i].KeyID))
}
if err = config.IssuerPrivateKeys[i].CertificateChain.Validate(); err != nil {
val.Push(fmt.Errorf(errFmtOIDCProviderPrivateKeysCertificateChainInvalid, i+1, config.IssuerPrivateKeys[i].KeyID, err))
}
}
}
func setOIDCDefaults(config *schema.OpenIDConnect) {
if config.AccessTokenLifespan == time.Duration(0) {
config.AccessTokenLifespan = schema.DefaultOpenIDConnectConfiguration.AccessTokenLifespan
}
if config.AuthorizeCodeLifespan == time.Duration(0) {
config.AuthorizeCodeLifespan = schema.DefaultOpenIDConnectConfiguration.AuthorizeCodeLifespan
}
if config.IDTokenLifespan == time.Duration(0) {
config.IDTokenLifespan = schema.DefaultOpenIDConnectConfiguration.IDTokenLifespan
}
if config.RefreshTokenLifespan == time.Duration(0) {
config.RefreshTokenLifespan = schema.DefaultOpenIDConnectConfiguration.RefreshTokenLifespan
}
if config.EnforcePKCE == "" {
config.EnforcePKCE = schema.DefaultOpenIDConnectConfiguration.EnforcePKCE
}
}
func validateOIDCOptionsCORS(config *schema.OpenIDConnect, validator *schema.StructValidator) {
validateOIDCOptionsCORSAllowedOrigins(config, validator)
if config.CORS.AllowedOriginsFromClientRedirectURIs {
validateOIDCOptionsCORSAllowedOriginsFromClientRedirectURIs(config)
}
validateOIDCOptionsCORSEndpoints(config, validator)
}
func validateOIDCOptionsCORSAllowedOrigins(config *schema.OpenIDConnect, val *schema.StructValidator) {
for _, origin := range config.CORS.AllowedOrigins {
if origin.String() == "*" {
if len(config.CORS.AllowedOrigins) != 1 {
val.Push(fmt.Errorf(errFmtOIDCCORSInvalidOriginWildcard))
}
if config.CORS.AllowedOriginsFromClientRedirectURIs {
val.Push(fmt.Errorf(errFmtOIDCCORSInvalidOriginWildcardWithClients))
}
continue
}
if origin.Path != "" {
val.Push(fmt.Errorf(errFmtOIDCCORSInvalidOrigin, origin.String(), "path"))
}
if origin.RawQuery != "" {
val.Push(fmt.Errorf(errFmtOIDCCORSInvalidOrigin, origin.String(), "query string"))
}
}
}
func validateOIDCOptionsCORSAllowedOriginsFromClientRedirectURIs(config *schema.OpenIDConnect) {
for _, client := range config.Clients {
for _, redirectURI := range client.RedirectURIs {
uri, err := url.ParseRequestURI(redirectURI)
if err != nil || (uri.Scheme != schemeHTTP && uri.Scheme != schemeHTTPS) || uri.Hostname() == "localhost" {
continue
}
origin := utils.OriginFromURL(uri)
if !utils.IsURLInSlice(*origin, config.CORS.AllowedOrigins) {
config.CORS.AllowedOrigins = append(config.CORS.AllowedOrigins, *origin)
}
}
}
}
func validateOIDCOptionsCORSEndpoints(config *schema.OpenIDConnect, val *schema.StructValidator) {
for _, endpoint := range config.CORS.Endpoints {
if !utils.IsStringInSlice(endpoint, validOIDCCORSEndpoints) {
val.Push(fmt.Errorf(errFmtOIDCCORSInvalidEndpoint, endpoint, strJoinOr(validOIDCCORSEndpoints)))
}
}
}
func validateOIDCClients(config *schema.OpenIDConnect, val *schema.StructValidator) {
var (
errDeprecated bool
clientIDs, duplicateClientIDs, blankClientIDs []string
)
errDeprecatedFunc := func() { errDeprecated = true }
for c, client := range config.Clients {
if client.ID == "" {
blankClientIDs = append(blankClientIDs, "#"+strconv.Itoa(c+1))
} else {
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)
}
} else {
clientIDs = append(clientIDs, id)
}
}
validateOIDCClient(c, config, val, errDeprecatedFunc)
}
if errDeprecated {
val.PushWarning(fmt.Errorf(errFmtOIDCClientsDeprecated))
}
if len(blankClientIDs) != 0 {
val.Push(fmt.Errorf(errFmtOIDCClientsWithEmptyID, buildJoinedString(", ", "or", "", blankClientIDs)))
}
if len(duplicateClientIDs) != 0 {
val.Push(fmt.Errorf(errFmtOIDCClientsDuplicateID, strJoinOr(duplicateClientIDs)))
}
}
func validateOIDCClient(c int, config *schema.OpenIDConnect, val *schema.StructValidator, errDeprecatedFunc func()) {
if config.Clients[c].Public {
if config.Clients[c].Secret != nil {
val.Push(fmt.Errorf(errFmtOIDCClientPublicInvalidSecret, config.Clients[c].ID))
}
} else {
if config.Clients[c].Secret == nil {
val.Push(fmt.Errorf(errFmtOIDCClientInvalidSecret, config.Clients[c].ID))
} else {
switch {
case config.Clients[c].Secret.IsPlainText() && config.Clients[c].TokenEndpointAuthMethod != oidc.ClientAuthMethodClientSecretJWT:
val.PushWarning(fmt.Errorf(errFmtOIDCClientInvalidSecretPlainText, config.Clients[c].ID))
case !config.Clients[c].Secret.IsPlainText() && config.Clients[c].TokenEndpointAuthMethod == oidc.ClientAuthMethodClientSecretJWT:
val.Push(fmt.Errorf(errFmtOIDCClientInvalidSecretNotPlainText, config.Clients[c].ID))
}
}
}
switch {
case config.Clients[c].Policy == "":
config.Clients[c].Policy = schema.DefaultOpenIDConnectClientConfiguration.Policy
case utils.IsStringInSlice(config.Clients[c].Policy, config.Discovery.Policies):
break
default:
val.Push(fmt.Errorf(errFmtOIDCClientInvalidValue, config.Clients[c].ID, "authorization_policy", strJoinOr(config.Discovery.Policies), config.Clients[c].Policy))
}
switch config.Clients[c].PKCEChallengeMethod {
case "", oidc.PKCEChallengeMethodPlain, oidc.PKCEChallengeMethodSHA256:
break
default:
val.Push(fmt.Errorf(errFmtOIDCClientInvalidValue, config.Clients[c].ID, attrOIDCPKCEChallengeMethod, strJoinOr([]string{oidc.PKCEChallengeMethodPlain, oidc.PKCEChallengeMethodSHA256}), config.Clients[c].PKCEChallengeMethod))
}
validateOIDCClientConsentMode(c, config, val)
validateOIDCClientScopes(c, config, val, errDeprecatedFunc)
validateOIDCClientResponseTypes(c, config, val, errDeprecatedFunc)
validateOIDCClientResponseModes(c, config, val, errDeprecatedFunc)
validateOIDCClientGrantTypes(c, config, val, errDeprecatedFunc)
validateOIDCClientRedirectURIs(c, config, val, errDeprecatedFunc)
validateOIDDClientSigningAlgs(c, config, val)
validateOIDCClientSectorIdentifier(c, config, val)
validateOIDCClientPublicKeys(c, config, val)
validateOIDCClientTokenEndpointAuth(c, config, val)
}
func validateOIDCClientPublicKeys(c int, config *schema.OpenIDConnect, val *schema.StructValidator) {
switch {
case config.Clients[c].PublicKeys.URI != nil && len(config.Clients[c].PublicKeys.Values) != 0:
val.Push(fmt.Errorf(errFmtOIDCClientPublicKeysBothURIAndValuesConfigured, config.Clients[c].ID))
case config.Clients[c].PublicKeys.URI != nil:
if config.Clients[c].PublicKeys.URI.Scheme != schemeHTTPS {
val.Push(fmt.Errorf(errFmtOIDCClientPublicKeysURIInvalidScheme, config.Clients[c].ID, config.Clients[c].PublicKeys.URI.Scheme))
}
case len(config.Clients[c].PublicKeys.Values) != 0:
validateOIDCClientJSONWebKeysList(c, config, val)
}
}
func validateOIDCClientJSONWebKeysList(c int, config *schema.OpenIDConnect, val *schema.StructValidator) {
var (
props *JWKProperties
err error
)
for i := 0; i < len(config.Clients[c].PublicKeys.Values); i++ {
if config.Clients[c].PublicKeys.Values[i].KeyID == "" {
val.Push(fmt.Errorf(errFmtOIDCClientPublicKeysInvalidOptionMissingOneOf, config.Clients[c].ID, i+1, attrOIDCKeyID))
}
if props, err = schemaJWKGetProperties(config.Clients[c].PublicKeys.Values[i]); err != nil {
val.Push(fmt.Errorf(errFmtOIDCClientPublicKeysProperties, config.Clients[c].ID, i+1, config.Clients[c].PublicKeys.Values[i].KeyID, err))
continue
}
validateOIDCClientJSONWebKeysListKeyUseAlg(c, i, props, config, val)
var checkEqualKey bool
switch key := config.Clients[c].PublicKeys.Values[i].Key.(type) {
case nil:
val.Push(fmt.Errorf(errFmtOIDCClientPublicKeysInvalidOptionMissingOneOf, config.Clients[c].ID, i+1, attrOIDCKey))
case *rsa.PublicKey:
checkEqualKey = true
if key.N == nil {
checkEqualKey = false
val.Push(fmt.Errorf(errFmtOIDCClientPublicKeysKeyMalformed, config.Clients[c].ID, i+1))
} else if key.Size() < 256 {
checkEqualKey = false
val.Push(fmt.Errorf(errFmtOIDCClientPublicKeysRSAKeyLessThan2048Bits, config.Clients[c].ID, i+1, config.Clients[c].PublicKeys.Values[i].KeyID, key.Size()*8))
}
case *ecdsa.PublicKey:
checkEqualKey = true
default:
val.Push(fmt.Errorf(errFmtOIDCClientPublicKeysKeyNotRSAOrECDSA, config.Clients[c].ID, i+1, config.Clients[c].PublicKeys.Values[i].KeyID, key))
}
if config.Clients[c].PublicKeys.Values[i].CertificateChain.HasCertificates() {
if checkEqualKey && !config.Clients[c].PublicKeys.Values[i].CertificateChain.EqualKey(config.Clients[c].PublicKeys.Values[i].Key) {
val.Push(fmt.Errorf(errFmtOIDCClientPublicKeysCertificateChainKeyMismatch, config.Clients[c].ID, i+1, config.Clients[c].PublicKeys.Values[i].KeyID))
}
if err = config.Clients[c].PublicKeys.Values[i].CertificateChain.Validate(); err != nil {
val.Push(fmt.Errorf(errFmtOIDCClientPublicKeysCertificateChainInvalid, config.Clients[c].ID, i+1, config.Clients[c].PublicKeys.Values[i].KeyID, err))
}
}
}
if config.Clients[c].RequestObjectSigningAlg != "" && !utils.IsStringInSlice(config.Clients[c].RequestObjectSigningAlg, config.Clients[c].Discovery.RequestObjectSigningAlgs) {
val.Push(fmt.Errorf(errFmtOIDCClientPublicKeysROSAMissingAlgorithm, config.Clients[c].ID, strJoinOr(config.Clients[c].Discovery.RequestObjectSigningAlgs)))
}
}
func validateOIDCClientJSONWebKeysListKeyUseAlg(c, i int, props *JWKProperties, config *schema.OpenIDConnect, val *schema.StructValidator) {
switch config.Clients[c].PublicKeys.Values[i].Use {
case "":
config.Clients[c].PublicKeys.Values[i].Use = props.Use
case oidc.KeyUseSignature:
break
default:
val.Push(fmt.Errorf(errFmtOIDCClientPublicKeysInvalidOptionOneOf, config.Clients[c].ID, i+1, config.Clients[c].PublicKeys.Values[i].KeyID, attrOIDCKeyUse, strJoinOr([]string{oidc.KeyUseSignature}), config.Clients[c].PublicKeys.Values[i].Use))
}
switch {
case config.Clients[c].PublicKeys.Values[i].Algorithm == "":
config.Clients[c].PublicKeys.Values[i].Algorithm = props.Algorithm
case utils.IsStringInSlice(config.Clients[c].PublicKeys.Values[i].Algorithm, validOIDCIssuerJWKSigningAlgs):
break
default:
val.Push(fmt.Errorf(errFmtOIDCClientPublicKeysInvalidOptionOneOf, config.Clients[c].ID, i+1, config.Clients[c].PublicKeys.Values[i].KeyID, attrOIDCAlgorithm, strJoinOr(validOIDCIssuerJWKSigningAlgs), config.Clients[c].PublicKeys.Values[i].Algorithm))
}
if config.Clients[c].PublicKeys.Values[i].Algorithm != "" {
if !utils.IsStringInSlice(config.Clients[c].PublicKeys.Values[i].Algorithm, config.Discovery.RequestObjectSigningAlgs) {
config.Discovery.RequestObjectSigningAlgs = append(config.Discovery.RequestObjectSigningAlgs, config.Clients[c].PublicKeys.Values[i].Algorithm)
}
if !utils.IsStringInSlice(config.Clients[c].PublicKeys.Values[i].Algorithm, config.Clients[c].Discovery.RequestObjectSigningAlgs) {
config.Clients[c].Discovery.RequestObjectSigningAlgs = append(config.Clients[c].Discovery.RequestObjectSigningAlgs, config.Clients[c].PublicKeys.Values[i].Algorithm)
}
}
}
func validateOIDCClientSectorIdentifier(c int, config *schema.OpenIDConnect, val *schema.StructValidator) {
if config.Clients[c].SectorIdentifier.String() != "" {
if utils.IsURLHostComponent(config.Clients[c].SectorIdentifier) || utils.IsURLHostComponentWithPort(config.Clients[c].SectorIdentifier) {
return
}
if config.Clients[c].SectorIdentifier.Scheme != "" {
val.Push(fmt.Errorf(errFmtOIDCClientInvalidSectorIdentifier, config.Clients[c].ID, config.Clients[c].SectorIdentifier.String(), config.Clients[c].SectorIdentifier.Host, "scheme", config.Clients[c].SectorIdentifier.Scheme))
if config.Clients[c].SectorIdentifier.Path != "" {
val.Push(fmt.Errorf(errFmtOIDCClientInvalidSectorIdentifier, config.Clients[c].ID, config.Clients[c].SectorIdentifier.String(), config.Clients[c].SectorIdentifier.Host, "path", config.Clients[c].SectorIdentifier.Path))
}
if config.Clients[c].SectorIdentifier.RawQuery != "" {
val.Push(fmt.Errorf(errFmtOIDCClientInvalidSectorIdentifier, config.Clients[c].ID, config.Clients[c].SectorIdentifier.String(), config.Clients[c].SectorIdentifier.Host, "query", config.Clients[c].SectorIdentifier.RawQuery))
}
if config.Clients[c].SectorIdentifier.Fragment != "" {
val.Push(fmt.Errorf(errFmtOIDCClientInvalidSectorIdentifier, config.Clients[c].ID, config.Clients[c].SectorIdentifier.String(), config.Clients[c].SectorIdentifier.Host, "fragment", config.Clients[c].SectorIdentifier.Fragment))
}
if config.Clients[c].SectorIdentifier.User != nil {
if config.Clients[c].SectorIdentifier.User.Username() != "" {
val.Push(fmt.Errorf(errFmtOIDCClientInvalidSectorIdentifier, config.Clients[c].ID, config.Clients[c].SectorIdentifier.String(), config.Clients[c].SectorIdentifier.Host, "username", config.Clients[c].SectorIdentifier.User.Username()))
}
if _, set := config.Clients[c].SectorIdentifier.User.Password(); set {
val.Push(fmt.Errorf(errFmtOIDCClientInvalidSectorIdentifierWithoutValue, config.Clients[c].ID, config.Clients[c].SectorIdentifier.String(), config.Clients[c].SectorIdentifier.Host, "password"))
}
}
} else if config.Clients[c].SectorIdentifier.Host == "" {
val.Push(fmt.Errorf(errFmtOIDCClientInvalidSectorIdentifierHost, config.Clients[c].ID, config.Clients[c].SectorIdentifier.String()))
}
}
}
func validateOIDCClientConsentMode(c int, config *schema.OpenIDConnect, val *schema.StructValidator) {
switch {
case utils.IsStringInSlice(config.Clients[c].ConsentMode, []string{"", auto}):
if config.Clients[c].ConsentPreConfiguredDuration != nil {
config.Clients[c].ConsentMode = oidc.ClientConsentModePreConfigured.String()
} else {
config.Clients[c].ConsentMode = oidc.ClientConsentModeExplicit.String()
}
case utils.IsStringInSlice(config.Clients[c].ConsentMode, validOIDCClientConsentModes):
break
default:
val.Push(fmt.Errorf(errFmtOIDCClientInvalidConsentMode, config.Clients[c].ID, strJoinOr(append(validOIDCClientConsentModes, auto)), config.Clients[c].ConsentMode))
}
if config.Clients[c].ConsentMode == oidc.ClientConsentModePreConfigured.String() && config.Clients[c].ConsentPreConfiguredDuration == nil {
config.Clients[c].ConsentPreConfiguredDuration = schema.DefaultOpenIDConnectClientConfiguration.ConsentPreConfiguredDuration
}
}
func validateOIDCClientScopes(c int, config *schema.OpenIDConnect, val *schema.StructValidator, errDeprecatedFunc func()) {
if len(config.Clients[c].Scopes) == 0 {
config.Clients[c].Scopes = schema.DefaultOpenIDConnectClientConfiguration.Scopes
}
if !utils.IsStringInSlice(oidc.ScopeOpenID, config.Clients[c].Scopes) {
config.Clients[c].Scopes = append([]string{oidc.ScopeOpenID}, config.Clients[c].Scopes...)
}
invalid, duplicates := validateList(config.Clients[c].Scopes, validOIDCClientScopes, true)
if len(invalid) != 0 {
val.Push(fmt.Errorf(errFmtOIDCClientInvalidEntries, config.Clients[c].ID, attrOIDCScopes, strJoinOr(validOIDCClientScopes), strJoinAnd(invalid)))
}
if len(duplicates) != 0 {
errDeprecatedFunc()
val.PushWarning(fmt.Errorf(errFmtOIDCClientInvalidEntryDuplicates, config.Clients[c].ID, attrOIDCScopes, strJoinAnd(duplicates)))
}
if utils.IsStringSliceContainsAny([]string{oidc.ScopeOfflineAccess, oidc.ScopeOffline}, config.Clients[c].Scopes) &&
!utils.IsStringSliceContainsAny(validOIDCClientResponseTypesRefreshToken, config.Clients[c].ResponseTypes) {
errDeprecatedFunc()
val.PushWarning(fmt.Errorf(errFmtOIDCClientInvalidRefreshTokenOptionWithoutCodeResponseType,
config.Clients[c].ID, attrOIDCScopes,
strJoinOr([]string{oidc.ScopeOfflineAccess, oidc.ScopeOffline}),
strJoinOr(validOIDCClientResponseTypesRefreshToken)),
)
}
}
func validateOIDCClientResponseTypes(c int, config *schema.OpenIDConnect, val *schema.StructValidator, errDeprecatedFunc func()) {
if len(config.Clients[c].ResponseTypes) == 0 {
config.Clients[c].ResponseTypes = schema.DefaultOpenIDConnectClientConfiguration.ResponseTypes
}
invalid, duplicates := validateList(config.Clients[c].ResponseTypes, validOIDCClientResponseTypes, true)
if len(invalid) != 0 {
val.PushWarning(fmt.Errorf(errFmtOIDCClientInvalidEntries, config.Clients[c].ID, attrOIDCResponseTypes, strJoinOr(validOIDCClientResponseTypes), strJoinAnd(invalid)))
}
if len(duplicates) != 0 {
errDeprecatedFunc()
val.PushWarning(fmt.Errorf(errFmtOIDCClientInvalidEntryDuplicates, config.Clients[c].ID, attrOIDCResponseTypes, strJoinAnd(duplicates)))
}
}
func validateOIDCClientResponseModes(c int, config *schema.OpenIDConnect, val *schema.StructValidator, errDeprecatedFunc func()) {
if len(config.Clients[c].ResponseModes) == 0 {
config.Clients[c].ResponseModes = schema.DefaultOpenIDConnectClientConfiguration.ResponseModes
for _, responseType := range config.Clients[c].ResponseTypes {
switch responseType {
case oidc.ResponseTypeAuthorizationCodeFlow:
if !utils.IsStringInSlice(oidc.ResponseModeQuery, config.Clients[c].ResponseModes) {
config.Clients[c].ResponseModes = append(config.Clients[c].ResponseModes, oidc.ResponseModeQuery)
}
case oidc.ResponseTypeImplicitFlowIDToken, oidc.ResponseTypeImplicitFlowToken, oidc.ResponseTypeImplicitFlowBoth,
oidc.ResponseTypeHybridFlowIDToken, oidc.ResponseTypeHybridFlowToken, oidc.ResponseTypeHybridFlowBoth:
if !utils.IsStringInSlice(oidc.ResponseModeFragment, config.Clients[c].ResponseModes) {
config.Clients[c].ResponseModes = append(config.Clients[c].ResponseModes, oidc.ResponseModeFragment)
}
}
}
}
invalid, duplicates := validateList(config.Clients[c].ResponseModes, validOIDCClientResponseModes, true)
if len(invalid) != 0 {
val.Push(fmt.Errorf(errFmtOIDCClientInvalidEntries, config.Clients[c].ID, attrOIDCResponseModes, strJoinOr(validOIDCClientResponseModes), strJoinAnd(invalid)))
}
if len(duplicates) != 0 {
errDeprecatedFunc()
val.PushWarning(fmt.Errorf(errFmtOIDCClientInvalidEntryDuplicates, config.Clients[c].ID, attrOIDCResponseModes, strJoinAnd(duplicates)))
}
}
func validateOIDCClientGrantTypes(c int, config *schema.OpenIDConnect, val *schema.StructValidator, errDeprecatedFunc func()) {
if len(config.Clients[c].GrantTypes) == 0 {
validateOIDCClientGrantTypesSetDefaults(c, config)
}
validateOIDCClientGrantTypesCheckRelated(c, config, val, errDeprecatedFunc)
invalid, duplicates := validateList(config.Clients[c].GrantTypes, validOIDCClientGrantTypes, true)
if len(invalid) != 0 {
val.Push(fmt.Errorf(errFmtOIDCClientInvalidEntries, config.Clients[c].ID, attrOIDCGrantTypes, strJoinOr(validOIDCClientGrantTypes), strJoinAnd(invalid)))
}
if len(duplicates) != 0 {
errDeprecatedFunc()
val.PushWarning(fmt.Errorf(errFmtOIDCClientInvalidEntryDuplicates, config.Clients[c].ID, attrOIDCGrantTypes, strJoinAnd(duplicates)))
}
}
func validateOIDCClientGrantTypesSetDefaults(c int, config *schema.OpenIDConnect) {
for _, responseType := range config.Clients[c].ResponseTypes {
switch responseType {
case oidc.ResponseTypeAuthorizationCodeFlow:
if !utils.IsStringInSlice(oidc.GrantTypeAuthorizationCode, config.Clients[c].GrantTypes) {
config.Clients[c].GrantTypes = append(config.Clients[c].GrantTypes, oidc.GrantTypeAuthorizationCode)
}
case oidc.ResponseTypeImplicitFlowIDToken, oidc.ResponseTypeImplicitFlowToken, oidc.ResponseTypeImplicitFlowBoth:
if !utils.IsStringInSlice(oidc.GrantTypeImplicit, config.Clients[c].GrantTypes) {
config.Clients[c].GrantTypes = append(config.Clients[c].GrantTypes, oidc.GrantTypeImplicit)
}
case oidc.ResponseTypeHybridFlowIDToken, oidc.ResponseTypeHybridFlowToken, oidc.ResponseTypeHybridFlowBoth:
if !utils.IsStringInSlice(oidc.GrantTypeAuthorizationCode, config.Clients[c].GrantTypes) {
config.Clients[c].GrantTypes = append(config.Clients[c].GrantTypes, oidc.GrantTypeAuthorizationCode)
}
if !utils.IsStringInSlice(oidc.GrantTypeImplicit, config.Clients[c].GrantTypes) {
config.Clients[c].GrantTypes = append(config.Clients[c].GrantTypes, oidc.GrantTypeImplicit)
}
}
}
}
func validateOIDCClientGrantTypesCheckRelated(c int, config *schema.OpenIDConnect, val *schema.StructValidator, errDeprecatedFunc func()) {
for _, grantType := range config.Clients[c].GrantTypes {
switch grantType {
case oidc.GrantTypeImplicit:
if !utils.IsStringSliceContainsAny(validOIDCClientResponseTypesImplicitFlow, config.Clients[c].ResponseTypes) && !utils.IsStringSliceContainsAny(validOIDCClientResponseTypesHybridFlow, config.Clients[c].ResponseTypes) {
errDeprecatedFunc()
val.PushWarning(fmt.Errorf(errFmtOIDCClientInvalidGrantTypeMatch, config.Clients[c].ID, grantType, "for either the implicit or hybrid flow", strJoinOr(append(append([]string{}, validOIDCClientResponseTypesImplicitFlow...), validOIDCClientResponseTypesHybridFlow...)), strJoinAnd(config.Clients[c].ResponseTypes)))
}
case oidc.GrantTypeAuthorizationCode:
if !utils.IsStringInSlice(oidc.ResponseTypeAuthorizationCodeFlow, config.Clients[c].ResponseTypes) && !utils.IsStringSliceContainsAny(validOIDCClientResponseTypesHybridFlow, config.Clients[c].ResponseTypes) {
errDeprecatedFunc()
val.PushWarning(fmt.Errorf(errFmtOIDCClientInvalidGrantTypeMatch, config.Clients[c].ID, grantType, "for either the authorization code or hybrid flow", strJoinOr(append([]string{oidc.ResponseTypeAuthorizationCodeFlow}, validOIDCClientResponseTypesHybridFlow...)), strJoinAnd(config.Clients[c].ResponseTypes)))
}
case oidc.GrantTypeRefreshToken:
if !utils.IsStringSliceContainsAny([]string{oidc.ScopeOfflineAccess, oidc.ScopeOffline}, config.Clients[c].Scopes) {
errDeprecatedFunc()
val.PushWarning(fmt.Errorf(errFmtOIDCClientInvalidGrantTypeRefresh, config.Clients[c].ID))
}
if !utils.IsStringSliceContainsAny(validOIDCClientResponseTypesRefreshToken, config.Clients[c].ResponseTypes) {
errDeprecatedFunc()
val.PushWarning(fmt.Errorf(errFmtOIDCClientInvalidRefreshTokenOptionWithoutCodeResponseType,
config.Clients[c].ID, attrOIDCGrantTypes,
strJoinOr([]string{oidc.GrantTypeRefreshToken}),
strJoinOr(validOIDCClientResponseTypesRefreshToken)),
)
}
}
}
}
func validateOIDCClientRedirectURIs(c int, config *schema.OpenIDConnect, val *schema.StructValidator, errDeprecatedFunc func()) {
var (
parsedRedirectURI *url.URL
err error
)
for _, redirectURI := range config.Clients[c].RedirectURIs {
if redirectURI == oauth2InstalledApp {
if config.Clients[c].Public {
continue
}
val.Push(fmt.Errorf(errFmtOIDCClientRedirectURIPublic, config.Clients[c].ID, oauth2InstalledApp))
continue
}
if parsedRedirectURI, err = url.Parse(redirectURI); err != nil {
val.Push(fmt.Errorf(errFmtOIDCClientRedirectURICantBeParsed, config.Clients[c].ID, redirectURI, err))
continue
}
if !parsedRedirectURI.IsAbs() || (!config.Clients[c].Public && parsedRedirectURI.Scheme == "") {
val.Push(fmt.Errorf(errFmtOIDCClientRedirectURIAbsolute, config.Clients[c].ID, redirectURI))
return
}
}
_, duplicates := validateList(config.Clients[c].RedirectURIs, nil, true)
if len(duplicates) != 0 {
errDeprecatedFunc()
val.PushWarning(fmt.Errorf(errFmtOIDCClientInvalidEntryDuplicates, config.Clients[c].ID, attrOIDCRedirectURIs, strJoinAnd(duplicates)))
}
}
func validateOIDCClientTokenEndpointAuth(c int, config *schema.OpenIDConnect, val *schema.StructValidator) {
implcit := len(config.Clients[c].ResponseTypes) != 0 && utils.IsStringSliceContainsAll(config.Clients[c].ResponseTypes, validOIDCClientResponseTypesImplicitFlow)
switch {
case config.Clients[c].TokenEndpointAuthMethod == "":
break
case !utils.IsStringInSlice(config.Clients[c].TokenEndpointAuthMethod, validOIDCClientTokenEndpointAuthMethods):
val.Push(fmt.Errorf(errFmtOIDCClientInvalidValue,
config.Clients[c].ID, attrOIDCTokenAuthMethod, strJoinOr(validOIDCClientTokenEndpointAuthMethods), config.Clients[c].TokenEndpointAuthMethod))
case config.Clients[c].TokenEndpointAuthMethod == oidc.ClientAuthMethodNone && !config.Clients[c].Public && !implcit:
val.Push(fmt.Errorf(errFmtOIDCClientInvalidTokenEndpointAuthMethod,
config.Clients[c].ID, strJoinOr(validOIDCClientTokenEndpointAuthMethodsConfidential), strJoinAnd(validOIDCClientResponseTypesImplicitFlow), config.Clients[c].TokenEndpointAuthMethod))
case config.Clients[c].TokenEndpointAuthMethod != oidc.ClientAuthMethodNone && config.Clients[c].Public:
val.Push(fmt.Errorf(errFmtOIDCClientInvalidTokenEndpointAuthMethodPublic,
config.Clients[c].ID, config.Clients[c].TokenEndpointAuthMethod))
}
switch config.Clients[c].TokenEndpointAuthMethod {
case "":
break
case oidc.ClientAuthMethodClientSecretJWT:
validateOIDCClientTokenEndpointAuthClientSecretJWT(c, config, val)
case oidc.ClientAuthMethodPrivateKeyJWT:
validateOIDCClientTokenEndpointAuthPublicKeyJWT(config.Clients[c], val)
}
}
func validateOIDCClientTokenEndpointAuthClientSecretJWT(c int, config *schema.OpenIDConnect, val *schema.StructValidator) {
switch {
case config.Clients[c].TokenEndpointAuthSigningAlg == "":
config.Clients[c].TokenEndpointAuthSigningAlg = oidc.SigningAlgHMACUsingSHA256
case !utils.IsStringInSlice(config.Clients[c].TokenEndpointAuthSigningAlg, validOIDCClientTokenEndpointAuthSigAlgsClientSecretJWT):
val.Push(fmt.Errorf(errFmtOIDCClientInvalidTokenEndpointAuthSigAlg, config.Clients[c].ID, strJoinOr(validOIDCClientTokenEndpointAuthSigAlgsClientSecretJWT), config.Clients[c].TokenEndpointAuthMethod))
}
}
func validateOIDCClientTokenEndpointAuthPublicKeyJWT(config schema.OpenIDConnectClient, val *schema.StructValidator) {
switch {
case config.TokenEndpointAuthSigningAlg == "":
val.Push(fmt.Errorf(errFmtOIDCClientInvalidTokenEndpointAuthSigAlgMissingPrivateKeyJWT, config.ID))
case !utils.IsStringInSlice(config.TokenEndpointAuthSigningAlg, validOIDCIssuerJWKSigningAlgs):
val.Push(fmt.Errorf(errFmtOIDCClientInvalidTokenEndpointAuthSigAlg, config.ID, strJoinOr(validOIDCIssuerJWKSigningAlgs), config.TokenEndpointAuthMethod))
}
if config.PublicKeys.URI == nil {
if len(config.PublicKeys.Values) == 0 {
val.Push(fmt.Errorf(errFmtOIDCClientInvalidPublicKeysPrivateKeyJWT, config.ID))
} else if len(config.Discovery.RequestObjectSigningAlgs) != 0 && !utils.IsStringInSlice(config.TokenEndpointAuthSigningAlg, config.Discovery.RequestObjectSigningAlgs) {
val.Push(fmt.Errorf(errFmtOIDCClientInvalidTokenEndpointAuthSigAlgReg, config.ID, strJoinOr(config.Discovery.RequestObjectSigningAlgs), config.TokenEndpointAuthMethod))
}
}
}
func validateOIDDClientSigningAlgs(c int, config *schema.OpenIDConnect, val *schema.StructValidator) {
switch config.Clients[c].UserinfoSigningKeyID {
case "":
if config.Clients[c].UserinfoSigningAlg == "" {
config.Clients[c].UserinfoSigningAlg = schema.DefaultOpenIDConnectClientConfiguration.UserinfoSigningAlg
} else if config.Clients[c].UserinfoSigningAlg != oidc.SigningAlgNone && !utils.IsStringInSlice(config.Clients[c].UserinfoSigningAlg, config.Discovery.ResponseObjectSigningAlgs) {
val.Push(fmt.Errorf(errFmtOIDCClientInvalidValue,
config.Clients[c].ID, attrOIDCUsrSigAlg, strJoinOr(append(config.Discovery.ResponseObjectSigningAlgs, oidc.SigningAlgNone)), config.Clients[c].UserinfoSigningAlg))
}
default:
if !utils.IsStringInSlice(config.Clients[c].UserinfoSigningKeyID, config.Discovery.ResponseObjectSigningKeyIDs) {
val.Push(fmt.Errorf(errFmtOIDCClientInvalidValue,
config.Clients[c].ID, attrOIDCUsrSigKID, strJoinOr(config.Discovery.ResponseObjectSigningKeyIDs), config.Clients[c].UserinfoSigningKeyID))
} else {
config.Clients[c].UserinfoSigningAlg = getResponseObjectAlgFromKID(config, config.Clients[c].UserinfoSigningKeyID, config.Clients[c].UserinfoSigningAlg)
}
}
switch config.Clients[c].IDTokenSigningKeyID {
case "":
if config.Clients[c].IDTokenSigningAlg == "" {
config.Clients[c].IDTokenSigningAlg = schema.DefaultOpenIDConnectClientConfiguration.IDTokenSigningAlg
} else if !utils.IsStringInSlice(config.Clients[c].IDTokenSigningAlg, config.Discovery.ResponseObjectSigningAlgs) {
val.Push(fmt.Errorf(errFmtOIDCClientInvalidValue,
config.Clients[c].ID, attrOIDCIDTokenSigAlg, strJoinOr(config.Discovery.ResponseObjectSigningAlgs), config.Clients[c].IDTokenSigningAlg))
}
default:
if !utils.IsStringInSlice(config.Clients[c].IDTokenSigningKeyID, config.Discovery.ResponseObjectSigningKeyIDs) {
val.Push(fmt.Errorf(errFmtOIDCClientInvalidValue,
config.Clients[c].ID, attrOIDCIDTokenSigKID, strJoinOr(config.Discovery.ResponseObjectSigningKeyIDs), config.Clients[c].IDTokenSigningKeyID))
} else {
config.Clients[c].IDTokenSigningAlg = getResponseObjectAlgFromKID(config, config.Clients[c].IDTokenSigningKeyID, config.Clients[c].IDTokenSigningAlg)
}
}
}