@@ -296,14 +321,35 @@ A friendly description for this client shown in the UI. This defaults to the sam
type: string
{: .label .label-config .label-purple }
-required: yes
-{: .label .label-config .label-red }
+required: situational
+{: .label .label-config .label-yellow }
The shared secret between Authelia and the application consuming this client. This secret must
match the secret configured in the application. Currently this is stored in plain text.
You must [generate this option yourself](#generating-a-random-secret).
+This must be provided when the client is a confidential client type, and must be blank when using the public client
+type. To set the client type to public see the [public](#public) configuration option.
+
+#### public
+
+
+type: bool
+{: .label .label-config .label-purple }
+default: false
+{: .label .label-config .label-blue }
+required: no
+{: .label .label-config .label-green }
+
+
+This enables the public client type for this client. This is for clients that are not capable of maintaining
+confidentiality of credentials, you can read more about client types in [RFC6749](https://datatracker.ietf.org/doc/html/rfc6749#section-2.1).
+This is particularly useful for SPA's and CLI tools. This option requires setting the [client secret](#secret) to a
+blank string.
+
+In addition to the standard rules for redirect URIs, public clients can use the `urn:ietf:wg:oauth:2.0:oob` redirect URI.
+
#### authorization_policy
@@ -317,18 +363,16 @@ required: no
The authorization policy for this client: either `one_factor` or `two_factor`.
-#### redirect_uris
+#### audience
type: list(string)
-{: .label .label-config .label-purple }
-required: yes
-{: .label .label-config .label-red }
+{: .label .label-config .label-purple }
+required: no
+{: .label .label-config .label-green }
-A list of valid callback URL´s this client will redirect to. All other callbacks will be considered
-unsafe. The URL's are case-sensitive and they differ from application to application - the community has
-provided [a list of URL´s for common applications](../../community/oidc-integrations.md).
+A list of audiences this client is allowed to request.
#### scopes
@@ -345,6 +389,28 @@ A list of scopes to allow this client to consume. See [scope definitions](#scope
information. The documentation for the application you want to use with Authelia will most-likely provide
you with the scopes to allow.
+#### redirect_uris
+
+
+type: list(string)
+{: .label .label-config .label-purple }
+required: yes
+{: .label .label-config .label-red }
+
+
+A list of valid callback URIs this client will redirect to. All other callbacks will be considered
+unsafe. The URIs are case-sensitive and they differ from application to application - the community has
+provided [a list of URL´s for common applications](../../community/oidc-integrations.md).
+
+Some restrictions that have been placed on clients and
+their redirect URIs are as follows:
+
+1. If a client attempts to authorize with Authelia and its redirect URI is not listed in the client configuration the
+ attempt to authorize wil fail and an error will be generated.
+2. The redirect URIs are case-sensitive.
+3. The URI must include a scheme and that scheme must be one of `http` or `https`.
+4. The client can ignore rule 3 and use `urn:ietf:wg:oauth:2.0:oob` if it is a [public](#public) client type.
+
#### grant_types
diff --git a/internal/configuration/config.template.yml b/internal/configuration/config.template.yml
index 6c6601435..71919fe07 100644
--- a/internal/configuration/config.template.yml
+++ b/internal/configuration/config.template.yml
@@ -595,7 +595,7 @@ notifier:
## Enables additional debug messages.
# enable_client_debug_messages: false
- ## SECURITY NOTICE: It's not recommended to change this option, and highly discouraged to have it below 8 for
+ ## SECURITY NOTICE: It's not recommended changing this option, and highly discouraged to have it below 8 for
## security reasons.
# minimum_parameter_entropy: 8
@@ -611,36 +611,42 @@ notifier:
## The client secret is a shared secret between Authelia and the consumer of this client.
# secret: this_is_a_secret
+ ## Sets the client to public. This should typically not be set, please see the documentation for usage.
+ # public: false
+
## The policy to require for this client; one_factor or two_factor.
# authorization_policy: two_factor
+ ## Audience this client is allowed to request.
+ # audience: []
+
+ ## Scopes this client is allowed to request.
+ # scopes:
+ # - openid
+ # - groups
+ # - email
+ # - profile
+
## Redirect URI's specifies a list of valid case-sensitive callbacks for this client.
# redirect_uris:
# - https://oidc.example.com:8080/oauth2/callback
- ## Scopes defines the valid scopes this client can request
- # scopes:
- # - openid
- # - groups
- # - email
- # - profile
-
## Grant Types configures which grants this client can obtain.
## It's not recommended to define this unless you know what you're doing.
# grant_types:
- # - refresh_token
- # - authorization_code
+ # - refresh_token
+ # - authorization_code
## Response Types configures which responses this client can be sent.
## It's not recommended to define this unless you know what you're doing.
# response_types:
- # - code
+ # - code
## Response Modes configures which response modes this client supports.
# response_modes:
- # - form_post
- # - query
- # - fragment
+ # - form_post
+ # - query
+ # - fragment
## The algorithm used to sign userinfo endpoint responses for this client, either none or RS256.
# userinfo_signing_algorithm: none
diff --git a/internal/configuration/schema/identity_providers.go b/internal/configuration/schema/identity_providers.go
index faeaffb77..90a1de667 100644
--- a/internal/configuration/schema/identity_providers.go
+++ b/internal/configuration/schema/identity_providers.go
@@ -25,12 +25,16 @@ type OpenIDConnectConfiguration struct {
// OpenIDConnectClientConfiguration configuration for an OpenID Connect client.
type OpenIDConnectClientConfiguration struct {
- ID string `mapstructure:"id"`
- Description string `mapstructure:"description"`
- Secret string `mapstructure:"secret"`
- RedirectURIs []string `mapstructure:"redirect_uris"`
- Policy string `mapstructure:"authorization_policy"`
+ ID string `mapstructure:"id"`
+ Description string `mapstructure:"description"`
+ Secret string `mapstructure:"secret"`
+ Public bool `mapstructure:"public"`
+
+ Policy string `mapstructure:"authorization_policy"`
+
+ Audience []string `mapstructure:"audience"`
Scopes []string `mapstructure:"scopes"`
+ RedirectURIs []string `mapstructure:"redirect_uris"`
GrantTypes []string `mapstructure:"grant_types"`
ResponseTypes []string `mapstructure:"response_types"`
ResponseModes []string `mapstructure:"response_modes"`
diff --git a/internal/configuration/validator/access_control_test.go b/internal/configuration/validator/access_control_test.go
index 2e37f45d0..00c24cc37 100644
--- a/internal/configuration/validator/access_control_test.go
+++ b/internal/configuration/validator/access_control_test.go
@@ -192,7 +192,7 @@ func TestShouldReturnCorrectResultsForValidNetworkGroups(t *testing.T) {
}
validNetwork := IsNetworkGroupValid(config, "internal")
- invalidNetwork := IsNetworkGroupValid(config, "127.0.0.1")
+ invalidNetwork := IsNetworkGroupValid(config, loopback)
assert.True(t, validNetwork)
assert.False(t, invalidNetwork)
diff --git a/internal/configuration/validator/authentication_test.go b/internal/configuration/validator/authentication_test.go
index 5b4477e35..f38ea9c1f 100644
--- a/internal/configuration/validator/authentication_test.go
+++ b/internal/configuration/validator/authentication_test.go
@@ -443,7 +443,7 @@ func (suite *LDAPAuthenticationBackendSuite) TestShouldHelpDetectNoInputPlacehol
}
func (suite *LDAPAuthenticationBackendSuite) TestShouldAdaptLDAPURL() {
- suite.Assert().Equal("", validateLDAPURLSimple("127.0.0.1", suite.validator))
+ suite.Assert().Equal("", validateLDAPURLSimple(loopback, suite.validator))
suite.Assert().False(suite.validator.HasWarnings())
suite.Require().Len(suite.validator.Errors(), 1)
diff --git a/internal/configuration/validator/configuration_test.go b/internal/configuration/validator/configuration_test.go
index 5e4f4db1b..76c439b32 100644
--- a/internal/configuration/validator/configuration_test.go
+++ b/internal/configuration/validator/configuration_test.go
@@ -12,7 +12,7 @@ import (
func newDefaultConfig() schema.Configuration {
config := schema.Configuration{}
- config.Host = "127.0.0.1"
+ config.Host = loopback
config.Port = 9090
config.Logging.Level = "info"
config.Logging.Format = "text"
diff --git a/internal/configuration/validator/const.go b/internal/configuration/validator/const.go
index 24f597fe7..f6e33cd3b 100644
--- a/internal/configuration/validator/const.go
+++ b/internal/configuration/validator/const.go
@@ -1,5 +1,10 @@
package validator
+const (
+ loopback = "127.0.0.1"
+ oauth2InstalledApp = "urn:ietf:wg:oauth:2.0:oob"
+)
+
const (
errFmtDeprecatedConfigurationKey = "[DEPRECATED] The %s configuration option is deprecated and will be " +
"removed in %s, please use %s instead"
@@ -14,11 +19,16 @@ const (
errFmtOIDCServerClientRedirectURI = "OIDC client with ID '%s' redirect URI %s has an invalid scheme '%s', " +
"should be http or https"
+ errFmtOIDCClientRedirectURIPublic = "openid connect provider: client with ID '%s' redirect URI '%s' is " +
+ "only valid for the public client type, not the confidential client type"
+ errFmtOIDCClientRedirectURIAbsolute = "openid connect provider: client with ID '%s' redirect URI '%s' is invalid " +
+ "because it has no scheme when it should be http or https"
errFmtOIDCServerClientRedirectURICantBeParsed = "OIDC client with ID '%s' has an invalid redirect URI '%s' " +
"could not be parsed: %v"
errFmtOIDCServerClientInvalidPolicy = "OIDC client with ID '%s' has an invalid policy '%s', " +
"should be either 'one_factor' or 'two_factor'"
- errFmtOIDCServerClientInvalidSecret = "OIDC client with ID '%s' has an empty secret" //nolint:gosec
+ errFmtOIDCServerClientInvalidSecret = "OIDC client with ID '%s' has an empty secret" //nolint:gosec
+ errFmtOIDCClientPublicInvalidSecret = "openid connect provider: client with ID '%s' is public but does not have an empty secret" //nolint:gosec
errFmtOIDCServerClientInvalidScope = "OIDC client with ID '%s' has an invalid scope '%s', " +
"must be one of: '%s'"
errFmtOIDCServerClientInvalidGrantType = "OIDC client with ID '%s' has an invalid grant type '%s', " +
diff --git a/internal/configuration/validator/identity_providers.go b/internal/configuration/validator/identity_providers.go
index 211eefdf6..736b01cfb 100644
--- a/internal/configuration/validator/identity_providers.go
+++ b/internal/configuration/validator/identity_providers.go
@@ -68,8 +68,14 @@ func validateOIDCClients(configuration *schema.OpenIDConnectConfiguration, valid
ids = append(ids, client.ID)
}
- if client.Secret == "" {
- validator.Push(fmt.Errorf(errFmtOIDCServerClientInvalidSecret, client.ID))
+ if client.Public {
+ if client.Secret != "" {
+ validator.Push(fmt.Errorf(errFmtOIDCClientPublicInvalidSecret, client.ID))
+ }
+ } else {
+ if client.Secret == "" {
+ validator.Push(fmt.Errorf(errFmtOIDCServerClientInvalidSecret, client.ID))
+ }
}
if client.Policy == "" {
@@ -163,15 +169,29 @@ func validateOIDDClientUserinfoAlgorithm(c int, configuration *schema.OpenIDConn
func validateOIDCClientRedirectURIs(client schema.OpenIDConnectClientConfiguration, validator *schema.StructValidator) {
for _, redirectURI := range client.RedirectURIs {
- parsedURI, err := url.Parse(redirectURI)
+ if redirectURI == oauth2InstalledApp {
+ if client.Public {
+ continue
+ }
- if err != nil {
- validator.Push(fmt.Errorf(errFmtOIDCServerClientRedirectURICantBeParsed, client.ID, redirectURI, err))
- break
+ validator.Push(fmt.Errorf(errFmtOIDCClientRedirectURIPublic, client.ID, redirectURI))
+
+ continue
}
- if parsedURI.Scheme != schemeHTTPS && parsedURI.Scheme != schemeHTTP {
- validator.Push(fmt.Errorf(errFmtOIDCServerClientRedirectURI, client.ID, redirectURI, parsedURI.Scheme))
+ parsedURL, err := url.Parse(redirectURI)
+ if err != nil {
+ validator.Push(fmt.Errorf(errFmtOIDCServerClientRedirectURICantBeParsed, client.ID, redirectURI, err))
+ continue
+ }
+
+ if !parsedURL.IsAbs() {
+ validator.Push(fmt.Errorf(errFmtOIDCClientRedirectURIAbsolute, client.ID, redirectURI))
+ return
+ }
+
+ if parsedURL.Scheme != schemeHTTPS && parsedURL.Scheme != schemeHTTP {
+ validator.Push(fmt.Errorf(errFmtOIDCServerClientRedirectURI, client.ID, redirectURI, parsedURL.Scheme))
}
}
}
diff --git a/internal/configuration/validator/identity_providers_test.go b/internal/configuration/validator/identity_providers_test.go
index 44e8e977d..ac51ca40f 100644
--- a/internal/configuration/validator/identity_providers_test.go
+++ b/internal/configuration/validator/identity_providers_test.go
@@ -84,13 +84,21 @@ func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) {
"http://abc@%two",
},
},
+ {
+ ID: "client-check-uri-abs",
+ Secret: "a-secret",
+ Policy: twoFactorPolicy,
+ RedirectURIs: []string{
+ "google.com",
+ },
+ },
},
},
}
ValidateIdentityProviders(config, validator)
- require.Len(t, validator.Errors(), 7)
+ require.Len(t, validator.Errors(), 8)
assert.Equal(t, schema.DefaultOpenIDConnectClientConfiguration.Policy, config.OIDC.Clients[0].Policy)
assert.EqualError(t, validator.Errors()[0], fmt.Sprintf(errFmtOIDCServerClientInvalidSecret, ""))
@@ -98,8 +106,9 @@ func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) {
assert.EqualError(t, validator.Errors()[2], fmt.Sprintf(errFmtOIDCServerClientInvalidPolicy, "a-client", "a-policy"))
assert.EqualError(t, validator.Errors()[3], fmt.Sprintf(errFmtOIDCServerClientInvalidPolicy, "a-client", "a-policy"))
assert.EqualError(t, validator.Errors()[4], fmt.Sprintf(errFmtOIDCServerClientRedirectURICantBeParsed, "client-check-uri-parse", "http://abc@%two", errors.New("parse \"http://abc@%two\": invalid URL escape \"%tw\"")))
- assert.EqualError(t, validator.Errors()[5], "OIDC Server has one or more clients with an empty ID")
- assert.EqualError(t, validator.Errors()[6], "OIDC Server has clients with duplicate ID's")
+ assert.EqualError(t, validator.Errors()[5], fmt.Sprintf(errFmtOIDCClientRedirectURIAbsolute, "client-check-uri-abs", "google.com"))
+ assert.EqualError(t, validator.Errors()[6], "OIDC Server has one or more clients with an empty ID")
+ assert.EqualError(t, validator.Errors()[7], "OIDC Server has clients with duplicate ID's")
}
func TestShouldRaiseErrorWhenOIDCClientConfiguredWithBadScopes(t *testing.T) {
@@ -239,6 +248,85 @@ func TestValidateIdentityProvidersShouldRaiseWarningOnSecurityIssue(t *testing.T
assert.EqualError(t, validator.Warnings()[0], "SECURITY ISSUE: OIDC minimum parameter entropy is configured to an unsafe value, it should be above 8 but it's configured to 1.")
}
+func TestValidateIdentityProvidersShouldRaiseErrorsOnInvalidClientTypes(t *testing.T) {
+ validator := schema.NewStructValidator()
+ config := &schema.IdentityProvidersConfiguration{
+ OIDC: &schema.OpenIDConnectConfiguration{
+ HMACSecret: "hmac1",
+ IssuerPrivateKey: "key2",
+ Clients: []schema.OpenIDConnectClientConfiguration{
+ {
+ ID: "client-with-invalid-secret",
+ Secret: "a-secret",
+ Public: true,
+ Policy: "two_factor",
+ RedirectURIs: []string{
+ "https://localhost",
+ },
+ },
+ {
+ ID: "client-with-bad-redirect-uri",
+ Secret: "a-secret",
+ Public: false,
+ Policy: "two_factor",
+ RedirectURIs: []string{
+ oauth2InstalledApp,
+ },
+ },
+ },
+ },
+ }
+
+ ValidateIdentityProviders(config, validator)
+
+ require.Len(t, validator.Errors(), 2)
+ assert.Len(t, validator.Warnings(), 0)
+
+ assert.EqualError(t, validator.Errors()[0], fmt.Sprintf(errFmtOIDCClientPublicInvalidSecret, "client-with-invalid-secret"))
+ assert.EqualError(t, validator.Errors()[1], fmt.Sprintf(errFmtOIDCClientRedirectURIPublic, "client-with-bad-redirect-uri", oauth2InstalledApp))
+}
+
+func TestValidateIdentityProvidersShouldNotRaiseErrorsOnValidPublicClients(t *testing.T) {
+ validator := schema.NewStructValidator()
+ config := &schema.IdentityProvidersConfiguration{
+ OIDC: &schema.OpenIDConnectConfiguration{
+ HMACSecret: "hmac1",
+ IssuerPrivateKey: "key2",
+ Clients: []schema.OpenIDConnectClientConfiguration{
+ {
+ ID: "installed-app-client",
+ Public: true,
+ Policy: "two_factor",
+ RedirectURIs: []string{
+ oauth2InstalledApp,
+ },
+ },
+ {
+ ID: "client-with-https-scheme",
+ Public: true,
+ Policy: "two_factor",
+ RedirectURIs: []string{
+ "https://localhost:9000",
+ },
+ },
+ {
+ ID: "client-with-loopback",
+ Public: true,
+ Policy: "two_factor",
+ RedirectURIs: []string{
+ "http://127.0.0.1",
+ },
+ },
+ },
+ },
+ }
+
+ ValidateIdentityProviders(config, validator)
+
+ assert.Len(t, validator.Errors(), 0)
+ assert.Len(t, validator.Warnings(), 0)
+}
+
func TestValidateIdentityProvidersShouldSetDefaultValues(t *testing.T) {
validator := schema.NewStructValidator()
config := &schema.IdentityProvidersConfiguration{
diff --git a/internal/oidc/client.go b/internal/oidc/client.go
index 8369e9f40..f4e984bd3 100644
--- a/internal/oidc/client.go
+++ b/internal/oidc/client.go
@@ -12,20 +12,21 @@ import (
// NewClient creates a new InternalClient.
func NewClient(config schema.OpenIDConnectClientConfiguration) (client *InternalClient) {
client = &InternalClient{
- ID: config.ID,
- Description: config.Description,
- Policy: authorization.PolicyToLevel(config.Policy),
- Secret: []byte(config.Secret),
+ ID: config.ID,
+ Description: config.Description,
+ Secret: []byte(config.Secret),
+ Public: config.Public,
+
+ Policy: authorization.PolicyToLevel(config.Policy),
+
+ Audience: config.Audience,
+ Scopes: config.Scopes,
RedirectURIs: config.RedirectURIs,
GrantTypes: config.GrantTypes,
ResponseTypes: config.ResponseTypes,
- Scopes: config.Scopes,
+ ResponseModes: []fosite.ResponseModeType{fosite.ResponseModeDefault},
UserinfoSigningAlgorithm: config.UserinfoSigningAlgorithm,
-
- ResponseModes: []fosite.ResponseModeType{
- fosite.ResponseModeDefault,
- },
}
for _, mode := range config.ResponseModes {
diff --git a/internal/oidc/types.go b/internal/oidc/types.go
index 355caa3bb..b503dc81d 100644
--- a/internal/oidc/types.go
+++ b/internal/oidc/types.go
@@ -33,21 +33,21 @@ type OpenIDConnectStore struct {
// InternalClient represents the client internally.
type InternalClient struct {
- ID string `json:"id"`
- Description string `json:"-"`
- Secret []byte `json:"client_secret,omitempty"`
- RedirectURIs []string `json:"redirect_uris"`
- GrantTypes []string `json:"grant_types"`
- ResponseTypes []string `json:"response_types"`
- Scopes []string `json:"scopes"`
- Audience []string `json:"audience"`
- Public bool `json:"public"`
+ ID string `json:"id"`
+ Description string `json:"-"`
+ Secret []byte `json:"client_secret,omitempty"`
+ Public bool `json:"public"`
+ Policy authorization.Level `json:"-"`
+
+ Audience []string `json:"audience"`
+ Scopes []string `json:"scopes"`
+ RedirectURIs []string `json:"redirect_uris"`
+ GrantTypes []string `json:"grant_types"`
+ ResponseTypes []string `json:"response_types"`
ResponseModes []fosite.ResponseModeType `json:"response_modes"`
UserinfoSigningAlgorithm string `json:"userinfo_signed_response_alg,omitempty"`
-
- Policy authorization.Level `json:"-"`
}
// KeyManager keeps track of all of the active/inactive rsa keys and provides them to services requiring them.