authelia/internal/configuration/validator/identity_providers.go

352 lines
12 KiB
Go
Raw Normal View History

package validator
import (
"fmt"
"net/url"
"strings"
"time"
"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.IdentityProvidersConfiguration, val *schema.StructValidator) {
validateOIDC(config.OIDC, val)
}
func validateOIDC(config *schema.OpenIDConnectConfiguration, val *schema.StructValidator) {
if config == nil {
return
}
setOIDCDefaults(config)
switch {
case config.IssuerPrivateKey == nil:
val.Push(fmt.Errorf(errFmtOIDCNoPrivateKey))
default:
if config.IssuerCertificateChain.HasCertificates() {
if !config.IssuerCertificateChain.EqualKey(config.IssuerPrivateKey) {
val.Push(fmt.Errorf(errFmtOIDCCertificateMismatch))
}
if err := config.IssuerCertificateChain.Validate(); err != nil {
val.Push(fmt.Errorf(errFmtOIDCCertificateChain, err))
}
}
if config.IssuerPrivateKey.PublicKey.N == nil {
val.Push(fmt.Errorf(errFmtOIDCInvalidPrivateKeyMalformedMissingPublicKey))
} else if config.IssuerPrivateKey.Size()*8 < 2048 {
val.Push(fmt.Errorf(errFmtOIDCInvalidPrivateKeyBitSize, 2048, config.IssuerPrivateKey.Size()*8))
}
}
if config.MinimumParameterEntropy != 0 && config.MinimumParameterEntropy < 8 {
val.PushWarning(fmt.Errorf(errFmtOIDCServerInsecureParameterEntropy, config.MinimumParameterEntropy))
}
if config.EnforcePKCE != "never" && config.EnforcePKCE != "public_clients_only" && config.EnforcePKCE != "always" {
val.Push(fmt.Errorf(errFmtOIDCEnforcePKCEInvalidValue, config.EnforcePKCE))
}
validateOIDCOptionsCORS(config, val)
if len(config.Clients) == 0 {
val.Push(fmt.Errorf(errFmtOIDCNoClientsConfigured))
} else {
validateOIDCClients(config, val)
}
}
func setOIDCDefaults(config *schema.OpenIDConnectConfiguration) {
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.OpenIDConnectConfiguration, validator *schema.StructValidator) {
validateOIDCOptionsCORSAllowedOrigins(config, validator)
if config.CORS.AllowedOriginsFromClientRedirectURIs {
validateOIDCOptionsCORSAllowedOriginsFromClientRedirectURIs(config)
}
validateOIDCOptionsCORSEndpoints(config, validator)
}
func validateOIDCOptionsCORSAllowedOrigins(config *schema.OpenIDConnectConfiguration, 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.OpenIDConnectConfiguration) {
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.OpenIDConnectConfiguration, val *schema.StructValidator) {
for _, endpoint := range config.CORS.Endpoints {
if !utils.IsStringInSlice(endpoint, validOIDCCORSEndpoints) {
val.Push(fmt.Errorf(errFmtOIDCCORSInvalidEndpoint, endpoint, strings.Join(validOIDCCORSEndpoints, "', '")))
}
}
}
func validateOIDCClients(config *schema.OpenIDConnectConfiguration, val *schema.StructValidator) {
invalidID, duplicateIDs := false, false
var ids []string
for c, client := range config.Clients {
if client.ID == "" {
invalidID = true
} else {
if client.Description == "" {
config.Clients[c].Description = client.ID
}
if utils.IsStringInSliceFold(client.ID, ids) {
duplicateIDs = true
}
ids = append(ids, client.ID)
}
if client.Public {
if client.Secret != nil {
val.Push(fmt.Errorf(errFmtOIDCClientPublicInvalidSecret, client.ID))
}
} else {
if client.Secret == nil {
val.Push(fmt.Errorf(errFmtOIDCClientInvalidSecret, client.ID))
} else if client.Secret.IsPlainText() {
val.PushWarning(fmt.Errorf(errFmtOIDCClientInvalidSecretPlainText, client.ID))
}
}
if client.Policy == "" {
config.Clients[c].Policy = schema.DefaultOpenIDConnectClientConfiguration.Policy
} else if client.Policy != policyOneFactor && client.Policy != policyTwoFactor {
val.Push(fmt.Errorf(errFmtOIDCClientInvalidPolicy, client.ID, client.Policy))
}
switch client.PKCEChallengeMethod {
case "", "plain", "S256":
break
default:
val.Push(fmt.Errorf(errFmtOIDCClientInvalidPKCEChallengeMethod, client.ID, client.PKCEChallengeMethod))
}
validateOIDCClientConsentMode(c, config, val)
validateOIDCClientSectorIdentifier(client, val)
validateOIDCClientScopes(c, config, val)
validateOIDCClientGrantTypes(c, config, val)
validateOIDCClientResponseTypes(c, config, val)
validateOIDCClientResponseModes(c, config, val)
validateOIDDClientUserinfoAlgorithm(c, config, val)
validateOIDCClientRedirectURIs(client, val)
}
if invalidID {
val.Push(fmt.Errorf(errFmtOIDCClientsWithEmptyID))
}
if duplicateIDs {
val.Push(fmt.Errorf(errFmtOIDCClientsDuplicateID))
}
}
func validateOIDCClientSectorIdentifier(client schema.OpenIDConnectClientConfiguration, val *schema.StructValidator) {
if client.SectorIdentifier.String() != "" {
if utils.IsURLHostComponent(client.SectorIdentifier) || utils.IsURLHostComponentWithPort(client.SectorIdentifier) {
return
}
if client.SectorIdentifier.Scheme != "" {
val.Push(fmt.Errorf(errFmtOIDCClientInvalidSectorIdentifier, client.ID, client.SectorIdentifier.String(), client.SectorIdentifier.Host, "scheme", client.SectorIdentifier.Scheme))
if client.SectorIdentifier.Path != "" {
val.Push(fmt.Errorf(errFmtOIDCClientInvalidSectorIdentifier, client.ID, client.SectorIdentifier.String(), client.SectorIdentifier.Host, "path", client.SectorIdentifier.Path))
}
if client.SectorIdentifier.RawQuery != "" {
val.Push(fmt.Errorf(errFmtOIDCClientInvalidSectorIdentifier, client.ID, client.SectorIdentifier.String(), client.SectorIdentifier.Host, "query", client.SectorIdentifier.RawQuery))
}
if client.SectorIdentifier.Fragment != "" {
val.Push(fmt.Errorf(errFmtOIDCClientInvalidSectorIdentifier, client.ID, client.SectorIdentifier.String(), client.SectorIdentifier.Host, "fragment", client.SectorIdentifier.Fragment))
}
if client.SectorIdentifier.User != nil {
if client.SectorIdentifier.User.Username() != "" {
val.Push(fmt.Errorf(errFmtOIDCClientInvalidSectorIdentifier, client.ID, client.SectorIdentifier.String(), client.SectorIdentifier.Host, "username", client.SectorIdentifier.User.Username()))
}
if _, set := client.SectorIdentifier.User.Password(); set {
val.Push(fmt.Errorf(errFmtOIDCClientInvalidSectorIdentifierWithoutValue, client.ID, client.SectorIdentifier.String(), client.SectorIdentifier.Host, "password"))
}
}
} else if client.SectorIdentifier.Host == "" {
val.Push(fmt.Errorf(errFmtOIDCClientInvalidSectorIdentifierHost, client.ID, client.SectorIdentifier.String()))
}
}
}
func validateOIDCClientConsentMode(c int, config *schema.OpenIDConnectConfiguration, 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, strings.Join(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.OpenIDConnectConfiguration, val *schema.StructValidator) {
if len(config.Clients[c].Scopes) == 0 {
config.Clients[c].Scopes = schema.DefaultOpenIDConnectClientConfiguration.Scopes
return
}
if !utils.IsStringInSlice(oidc.ScopeOpenID, config.Clients[c].Scopes) {
config.Clients[c].Scopes = append(config.Clients[c].Scopes, oidc.ScopeOpenID)
}
for _, scope := range config.Clients[c].Scopes {
if !utils.IsStringInSlice(scope, validOIDCScopes) {
val.Push(fmt.Errorf(
errFmtOIDCClientInvalidEntry,
config.Clients[c].ID, "scopes", strings.Join(validOIDCScopes, "', '"), scope))
}
}
}
func validateOIDCClientGrantTypes(c int, config *schema.OpenIDConnectConfiguration, val *schema.StructValidator) {
if len(config.Clients[c].GrantTypes) == 0 {
config.Clients[c].GrantTypes = schema.DefaultOpenIDConnectClientConfiguration.GrantTypes
return
}
for _, grantType := range config.Clients[c].GrantTypes {
if !utils.IsStringInSlice(grantType, validOIDCGrantTypes) {
val.Push(fmt.Errorf(
errFmtOIDCClientInvalidEntry,
config.Clients[c].ID, "grant_types", strings.Join(validOIDCGrantTypes, "', '"), grantType))
}
}
}
func validateOIDCClientResponseTypes(c int, config *schema.OpenIDConnectConfiguration, _ *schema.StructValidator) {
if len(config.Clients[c].ResponseTypes) == 0 {
config.Clients[c].ResponseTypes = schema.DefaultOpenIDConnectClientConfiguration.ResponseTypes
return
}
}
func validateOIDCClientResponseModes(c int, config *schema.OpenIDConnectConfiguration, validator *schema.StructValidator) {
if len(config.Clients[c].ResponseModes) == 0 {
config.Clients[c].ResponseModes = schema.DefaultOpenIDConnectClientConfiguration.ResponseModes
return
}
for _, responseMode := range config.Clients[c].ResponseModes {
if !utils.IsStringInSlice(responseMode, validOIDCResponseModes) {
validator.Push(fmt.Errorf(
errFmtOIDCClientInvalidEntry,
config.Clients[c].ID, "response_modes", strings.Join(validOIDCResponseModes, "', '"), responseMode))
}
}
}
func validateOIDDClientUserinfoAlgorithm(c int, config *schema.OpenIDConnectConfiguration, val *schema.StructValidator) {
if config.Clients[c].UserinfoSigningAlgorithm == "" {
config.Clients[c].UserinfoSigningAlgorithm = schema.DefaultOpenIDConnectClientConfiguration.UserinfoSigningAlgorithm
} else if !utils.IsStringInSlice(config.Clients[c].UserinfoSigningAlgorithm, validOIDCUserinfoAlgorithms) {
val.Push(fmt.Errorf(errFmtOIDCClientInvalidUserinfoAlgorithm,
config.Clients[c].ID, strings.Join(validOIDCUserinfoAlgorithms, ", "), config.Clients[c].UserinfoSigningAlgorithm))
}
}
func validateOIDCClientRedirectURIs(client schema.OpenIDConnectClientConfiguration, val *schema.StructValidator) {
for _, redirectURI := range client.RedirectURIs {
if redirectURI == oauth2InstalledApp {
if client.Public {
continue
}
val.Push(fmt.Errorf(errFmtOIDCClientRedirectURIPublic, client.ID, oauth2InstalledApp))
continue
}
parsedURL, err := url.Parse(redirectURI)
if err != nil {
val.Push(fmt.Errorf(errFmtOIDCClientRedirectURICantBeParsed, client.ID, redirectURI, err))
continue
}
if !parsedURL.IsAbs() || (!client.Public && parsedURL.Scheme == "") {
val.Push(fmt.Errorf(errFmtOIDCClientRedirectURIAbsolute, client.ID, redirectURI))
return
}
}
}