feat(oidc): implement client type public (#2171)
This implements the public option for clients which allows using Authelia as an OpenID Connect Provider for cli applications and SPA's where the client secret cannot be considered secure.pull/2187/head
parent
0da770d900
commit
8342a46ba1
|
@ -595,7 +595,7 @@ notifier:
|
||||||
## Enables additional debug messages.
|
## Enables additional debug messages.
|
||||||
# enable_client_debug_messages: false
|
# 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.
|
## security reasons.
|
||||||
# minimum_parameter_entropy: 8
|
# minimum_parameter_entropy: 8
|
||||||
|
|
||||||
|
@ -611,20 +611,26 @@ notifier:
|
||||||
## The client secret is a shared secret between Authelia and the consumer of this client.
|
## The client secret is a shared secret between Authelia and the consumer of this client.
|
||||||
# secret: this_is_a_secret
|
# 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.
|
## The policy to require for this client; one_factor or two_factor.
|
||||||
# authorization_policy: two_factor
|
# authorization_policy: two_factor
|
||||||
|
|
||||||
## Redirect URI's specifies a list of valid case-sensitive callbacks for this client.
|
## Audience this client is allowed to request.
|
||||||
# redirect_uris:
|
# audience: []
|
||||||
# - https://oidc.example.com:8080/oauth2/callback
|
|
||||||
|
|
||||||
## Scopes defines the valid scopes this client can request
|
## Scopes this client is allowed to request.
|
||||||
# scopes:
|
# scopes:
|
||||||
# - openid
|
# - openid
|
||||||
# - groups
|
# - groups
|
||||||
# - email
|
# - email
|
||||||
# - profile
|
# - profile
|
||||||
|
|
||||||
|
## Redirect URI's specifies a list of valid case-sensitive callbacks for this client.
|
||||||
|
# redirect_uris:
|
||||||
|
# - https://oidc.example.com:8080/oauth2/callback
|
||||||
|
|
||||||
## Grant Types configures which grants this client can obtain.
|
## Grant Types configures which grants this client can obtain.
|
||||||
## It's not recommended to define this unless you know what you're doing.
|
## It's not recommended to define this unless you know what you're doing.
|
||||||
# grant_types:
|
# grant_types:
|
||||||
|
|
|
@ -34,7 +34,7 @@ for which stage will have each feature, and may evolve over time:
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td rowspan="7" class="tbl-header tbl-beta-stage">beta1</td>
|
<td rowspan="8" class="tbl-header tbl-beta-stage">beta1 (4.29.0)</td>
|
||||||
<td><a href="https://openid.net/specs/openid-connect-core-1_0.html#Consent" target="_blank" rel="noopener noreferrer">User Consent</a></td>
|
<td><a href="https://openid.net/specs/openid-connect-core-1_0.html#Consent" target="_blank" rel="noopener noreferrer">User Consent</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -56,9 +56,27 @@ for which stage will have each feature, and may evolve over time:
|
||||||
<td class="tbl-beta-stage">Per Client List of Valid Redirection URI's</td>
|
<td class="tbl-beta-stage">Per Client List of Valid Redirection URI's</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td rowspan="1" class="tbl-header tbl-beta-stage">beta2 <sup>1</sup></td>
|
<td class="tbl-beta-stage"><a href="https://datatracker.ietf.org/doc/html/rfc6749#section-2.1" target="_blank"rel="noopener noreferrer">Confidential Client Type</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="6" class="tbl-header tbl-beta-stage">beta2 (4.30.0) <sup>1</sup></td>
|
||||||
<td class="tbl-beta-stage"><a href="https://openid.net/specs/openid-connect-core-1_0.html#UserInfo" target="_blank" rel="noopener noreferrer">Userinfo Endpoint</a> (missed in beta1)</td>
|
<td class="tbl-beta-stage"><a href="https://openid.net/specs/openid-connect-core-1_0.html#UserInfo" target="_blank" rel="noopener noreferrer">Userinfo Endpoint</a> (missed in beta1)</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="tbl-beta-stage">Parameter Entropy Configuration</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="tbl-beta-stage">Token/Code Lifespan Configuration</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="tbl-beta-stage">Client Debug Messages</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="tbl-beta-stage">Client Audience</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="tbl-beta-stage"><a href="https://datatracker.ietf.org/doc/html/rfc6749#section-2.1" target="_blank"rel="noopener noreferrer">Public Client Type</a></td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td rowspan="2" class="tbl-header tbl-beta-stage">beta3 <sup>1</sup></td>
|
<td rowspan="2" class="tbl-header tbl-beta-stage">beta3 <sup>1</sup></td>
|
||||||
<td>Token Storage</td>
|
<td>Token Storage</td>
|
||||||
|
@ -117,20 +135,22 @@ identity_providers:
|
||||||
access_token_lifespan: 1h
|
access_token_lifespan: 1h
|
||||||
authorize_code_lifespan: 1m
|
authorize_code_lifespan: 1m
|
||||||
id_token_lifespan: 1h
|
id_token_lifespan: 1h
|
||||||
refresh_token_lifespan: 720h
|
refresh_token_lifespan: 90m
|
||||||
enable_client_debug_messages: false
|
enable_client_debug_messages: false
|
||||||
clients:
|
clients:
|
||||||
- id: myapp
|
- id: myapp
|
||||||
description: My Application
|
description: My Application
|
||||||
secret: this_is_a_secret
|
secret: this_is_a_secret
|
||||||
|
public: false
|
||||||
authorization_policy: two_factor
|
authorization_policy: two_factor
|
||||||
redirect_uris:
|
audience: []
|
||||||
- https://oidc.example.com:8080/oauth2/callback
|
|
||||||
scopes:
|
scopes:
|
||||||
- openid
|
- openid
|
||||||
- groups
|
- groups
|
||||||
- email
|
- email
|
||||||
- profile
|
- profile
|
||||||
|
redirect_uris:
|
||||||
|
- https://oidc.example.com:8080/oauth2/callback
|
||||||
grant_types:
|
grant_types:
|
||||||
- refresh_token
|
- refresh_token
|
||||||
- authorization_code
|
- authorization_code
|
||||||
|
@ -222,7 +242,7 @@ The maximum lifetime of an ID token. For more information read these docs about
|
||||||
<div markdown="1">
|
<div markdown="1">
|
||||||
type: string
|
type: string
|
||||||
{: .label .label-config .label-purple }
|
{: .label .label-config .label-purple }
|
||||||
default: 30d
|
default: 90m
|
||||||
{: .label .label-config .label-blue }
|
{: .label .label-config .label-blue }
|
||||||
required: no
|
required: no
|
||||||
{: .label .label-config .label-green }
|
{: .label .label-config .label-green }
|
||||||
|
@ -232,6 +252,11 @@ The maximum lifetime of a refresh token. The
|
||||||
refresh token can be used to obtain new refresh tokens as well as access tokens or id tokens with an
|
refresh token can be used to obtain new refresh tokens as well as access tokens or id tokens with an
|
||||||
up-to-date expiration. For more information read these docs about [token lifespan].
|
up-to-date expiration. For more information read these docs about [token lifespan].
|
||||||
|
|
||||||
|
A good starting point is 50% more or 30 minutes more (which ever is less) time than the highest lifespan out of the
|
||||||
|
[access token lifespan](#access_token_lifespan), the [authorize code lifespan](#authorize_code_lifespan), and the
|
||||||
|
[id token lifespan](#id_token_lifespan). For instance the default for all of these is 60 minutes, so the default refresh
|
||||||
|
token lifespan is 90 minutes.
|
||||||
|
|
||||||
### enable_client_debug_messages
|
### enable_client_debug_messages
|
||||||
|
|
||||||
<div markdown="1">
|
<div markdown="1">
|
||||||
|
@ -296,14 +321,35 @@ A friendly description for this client shown in the UI. This defaults to the sam
|
||||||
<div markdown="1">
|
<div markdown="1">
|
||||||
type: string
|
type: string
|
||||||
{: .label .label-config .label-purple }
|
{: .label .label-config .label-purple }
|
||||||
required: yes
|
required: situational
|
||||||
{: .label .label-config .label-red }
|
{: .label .label-config .label-yellow }
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
The shared secret between Authelia and the application consuming this client. This secret must
|
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.
|
match the secret configured in the application. Currently this is stored in plain text.
|
||||||
You must [generate this option yourself](#generating-a-random-secret).
|
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
|
||||||
|
|
||||||
|
<div markdown="1">
|
||||||
|
type: bool
|
||||||
|
{: .label .label-config .label-purple }
|
||||||
|
default: false
|
||||||
|
{: .label .label-config .label-blue }
|
||||||
|
required: no
|
||||||
|
{: .label .label-config .label-green }
|
||||||
|
</div>
|
||||||
|
|
||||||
|
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
|
#### authorization_policy
|
||||||
|
|
||||||
<div markdown="1">
|
<div markdown="1">
|
||||||
|
@ -317,18 +363,16 @@ required: no
|
||||||
|
|
||||||
The authorization policy for this client: either `one_factor` or `two_factor`.
|
The authorization policy for this client: either `one_factor` or `two_factor`.
|
||||||
|
|
||||||
#### redirect_uris
|
#### audience
|
||||||
|
|
||||||
<div markdown="1">
|
<div markdown="1">
|
||||||
type: list(string)
|
type: list(string)
|
||||||
{: .label .label-config .label-purple }
|
{: .label .label-config .label-purple }
|
||||||
required: yes
|
required: no
|
||||||
{: .label .label-config .label-red }
|
{: .label .label-config .label-green }
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
A list of valid callback URL´s this client will redirect to. All other callbacks will be considered
|
A list of audiences this client is allowed to request.
|
||||||
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).
|
|
||||||
|
|
||||||
#### scopes
|
#### 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
|
information. The documentation for the application you want to use with Authelia will most-likely provide
|
||||||
you with the scopes to allow.
|
you with the scopes to allow.
|
||||||
|
|
||||||
|
#### redirect_uris
|
||||||
|
|
||||||
|
<div markdown="1">
|
||||||
|
type: list(string)
|
||||||
|
{: .label .label-config .label-purple }
|
||||||
|
required: yes
|
||||||
|
{: .label .label-config .label-red }
|
||||||
|
</div>
|
||||||
|
|
||||||
|
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
|
#### grant_types
|
||||||
|
|
||||||
<div markdown="1">
|
<div markdown="1">
|
||||||
|
|
|
@ -595,7 +595,7 @@ notifier:
|
||||||
## Enables additional debug messages.
|
## Enables additional debug messages.
|
||||||
# enable_client_debug_messages: false
|
# 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.
|
## security reasons.
|
||||||
# minimum_parameter_entropy: 8
|
# minimum_parameter_entropy: 8
|
||||||
|
|
||||||
|
@ -611,20 +611,26 @@ notifier:
|
||||||
## The client secret is a shared secret between Authelia and the consumer of this client.
|
## The client secret is a shared secret between Authelia and the consumer of this client.
|
||||||
# secret: this_is_a_secret
|
# 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.
|
## The policy to require for this client; one_factor or two_factor.
|
||||||
# authorization_policy: two_factor
|
# authorization_policy: two_factor
|
||||||
|
|
||||||
## Redirect URI's specifies a list of valid case-sensitive callbacks for this client.
|
## Audience this client is allowed to request.
|
||||||
# redirect_uris:
|
# audience: []
|
||||||
# - https://oidc.example.com:8080/oauth2/callback
|
|
||||||
|
|
||||||
## Scopes defines the valid scopes this client can request
|
## Scopes this client is allowed to request.
|
||||||
# scopes:
|
# scopes:
|
||||||
# - openid
|
# - openid
|
||||||
# - groups
|
# - groups
|
||||||
# - email
|
# - email
|
||||||
# - profile
|
# - profile
|
||||||
|
|
||||||
|
## Redirect URI's specifies a list of valid case-sensitive callbacks for this client.
|
||||||
|
# redirect_uris:
|
||||||
|
# - https://oidc.example.com:8080/oauth2/callback
|
||||||
|
|
||||||
## Grant Types configures which grants this client can obtain.
|
## Grant Types configures which grants this client can obtain.
|
||||||
## It's not recommended to define this unless you know what you're doing.
|
## It's not recommended to define this unless you know what you're doing.
|
||||||
# grant_types:
|
# grant_types:
|
||||||
|
|
|
@ -28,9 +28,13 @@ type OpenIDConnectClientConfiguration struct {
|
||||||
ID string `mapstructure:"id"`
|
ID string `mapstructure:"id"`
|
||||||
Description string `mapstructure:"description"`
|
Description string `mapstructure:"description"`
|
||||||
Secret string `mapstructure:"secret"`
|
Secret string `mapstructure:"secret"`
|
||||||
RedirectURIs []string `mapstructure:"redirect_uris"`
|
Public bool `mapstructure:"public"`
|
||||||
|
|
||||||
Policy string `mapstructure:"authorization_policy"`
|
Policy string `mapstructure:"authorization_policy"`
|
||||||
|
|
||||||
|
Audience []string `mapstructure:"audience"`
|
||||||
Scopes []string `mapstructure:"scopes"`
|
Scopes []string `mapstructure:"scopes"`
|
||||||
|
RedirectURIs []string `mapstructure:"redirect_uris"`
|
||||||
GrantTypes []string `mapstructure:"grant_types"`
|
GrantTypes []string `mapstructure:"grant_types"`
|
||||||
ResponseTypes []string `mapstructure:"response_types"`
|
ResponseTypes []string `mapstructure:"response_types"`
|
||||||
ResponseModes []string `mapstructure:"response_modes"`
|
ResponseModes []string `mapstructure:"response_modes"`
|
||||||
|
|
|
@ -192,7 +192,7 @@ func TestShouldReturnCorrectResultsForValidNetworkGroups(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
validNetwork := IsNetworkGroupValid(config, "internal")
|
validNetwork := IsNetworkGroupValid(config, "internal")
|
||||||
invalidNetwork := IsNetworkGroupValid(config, "127.0.0.1")
|
invalidNetwork := IsNetworkGroupValid(config, loopback)
|
||||||
|
|
||||||
assert.True(t, validNetwork)
|
assert.True(t, validNetwork)
|
||||||
assert.False(t, invalidNetwork)
|
assert.False(t, invalidNetwork)
|
||||||
|
|
|
@ -443,7 +443,7 @@ func (suite *LDAPAuthenticationBackendSuite) TestShouldHelpDetectNoInputPlacehol
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *LDAPAuthenticationBackendSuite) TestShouldAdaptLDAPURL() {
|
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.Assert().False(suite.validator.HasWarnings())
|
||||||
suite.Require().Len(suite.validator.Errors(), 1)
|
suite.Require().Len(suite.validator.Errors(), 1)
|
||||||
|
|
|
@ -12,7 +12,7 @@ import (
|
||||||
|
|
||||||
func newDefaultConfig() schema.Configuration {
|
func newDefaultConfig() schema.Configuration {
|
||||||
config := schema.Configuration{}
|
config := schema.Configuration{}
|
||||||
config.Host = "127.0.0.1"
|
config.Host = loopback
|
||||||
config.Port = 9090
|
config.Port = 9090
|
||||||
config.Logging.Level = "info"
|
config.Logging.Level = "info"
|
||||||
config.Logging.Format = "text"
|
config.Logging.Format = "text"
|
||||||
|
|
|
@ -1,5 +1,10 @@
|
||||||
package validator
|
package validator
|
||||||
|
|
||||||
|
const (
|
||||||
|
loopback = "127.0.0.1"
|
||||||
|
oauth2InstalledApp = "urn:ietf:wg:oauth:2.0:oob"
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
errFmtDeprecatedConfigurationKey = "[DEPRECATED] The %s configuration option is deprecated and will be " +
|
errFmtDeprecatedConfigurationKey = "[DEPRECATED] The %s configuration option is deprecated and will be " +
|
||||||
"removed in %s, please use %s instead"
|
"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', " +
|
errFmtOIDCServerClientRedirectURI = "OIDC client with ID '%s' redirect URI %s has an invalid scheme '%s', " +
|
||||||
"should be http or https"
|
"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' " +
|
errFmtOIDCServerClientRedirectURICantBeParsed = "OIDC client with ID '%s' has an invalid redirect URI '%s' " +
|
||||||
"could not be parsed: %v"
|
"could not be parsed: %v"
|
||||||
errFmtOIDCServerClientInvalidPolicy = "OIDC client with ID '%s' has an invalid policy '%s', " +
|
errFmtOIDCServerClientInvalidPolicy = "OIDC client with ID '%s' has an invalid policy '%s', " +
|
||||||
"should be either 'one_factor' or 'two_factor'"
|
"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', " +
|
errFmtOIDCServerClientInvalidScope = "OIDC client with ID '%s' has an invalid scope '%s', " +
|
||||||
"must be one of: '%s'"
|
"must be one of: '%s'"
|
||||||
errFmtOIDCServerClientInvalidGrantType = "OIDC client with ID '%s' has an invalid grant type '%s', " +
|
errFmtOIDCServerClientInvalidGrantType = "OIDC client with ID '%s' has an invalid grant type '%s', " +
|
||||||
|
|
|
@ -68,9 +68,15 @@ func validateOIDCClients(configuration *schema.OpenIDConnectConfiguration, valid
|
||||||
ids = append(ids, client.ID)
|
ids = append(ids, client.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if client.Public {
|
||||||
|
if client.Secret != "" {
|
||||||
|
validator.Push(fmt.Errorf(errFmtOIDCClientPublicInvalidSecret, client.ID))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
if client.Secret == "" {
|
if client.Secret == "" {
|
||||||
validator.Push(fmt.Errorf(errFmtOIDCServerClientInvalidSecret, client.ID))
|
validator.Push(fmt.Errorf(errFmtOIDCServerClientInvalidSecret, client.ID))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if client.Policy == "" {
|
if client.Policy == "" {
|
||||||
configuration.Clients[c].Policy = schema.DefaultOpenIDConnectClientConfiguration.Policy
|
configuration.Clients[c].Policy = schema.DefaultOpenIDConnectClientConfiguration.Policy
|
||||||
|
@ -163,15 +169,29 @@ func validateOIDDClientUserinfoAlgorithm(c int, configuration *schema.OpenIDConn
|
||||||
|
|
||||||
func validateOIDCClientRedirectURIs(client schema.OpenIDConnectClientConfiguration, validator *schema.StructValidator) {
|
func validateOIDCClientRedirectURIs(client schema.OpenIDConnectClientConfiguration, validator *schema.StructValidator) {
|
||||||
for _, redirectURI := range client.RedirectURIs {
|
for _, redirectURI := range client.RedirectURIs {
|
||||||
parsedURI, err := url.Parse(redirectURI)
|
if redirectURI == oauth2InstalledApp {
|
||||||
|
if client.Public {
|
||||||
if err != nil {
|
continue
|
||||||
validator.Push(fmt.Errorf(errFmtOIDCServerClientRedirectURICantBeParsed, client.ID, redirectURI, err))
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if parsedURI.Scheme != schemeHTTPS && parsedURI.Scheme != schemeHTTP {
|
validator.Push(fmt.Errorf(errFmtOIDCClientRedirectURIPublic, client.ID, redirectURI))
|
||||||
validator.Push(fmt.Errorf(errFmtOIDCServerClientRedirectURI, client.ID, redirectURI, parsedURI.Scheme))
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -84,13 +84,21 @@ func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) {
|
||||||
"http://abc@%two",
|
"http://abc@%two",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
ID: "client-check-uri-abs",
|
||||||
|
Secret: "a-secret",
|
||||||
|
Policy: twoFactorPolicy,
|
||||||
|
RedirectURIs: []string{
|
||||||
|
"google.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
ValidateIdentityProviders(config, validator)
|
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.Equal(t, schema.DefaultOpenIDConnectClientConfiguration.Policy, config.OIDC.Clients[0].Policy)
|
||||||
assert.EqualError(t, validator.Errors()[0], fmt.Sprintf(errFmtOIDCServerClientInvalidSecret, ""))
|
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()[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()[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()[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()[5], fmt.Sprintf(errFmtOIDCClientRedirectURIAbsolute, "client-check-uri-abs", "google.com"))
|
||||||
assert.EqualError(t, validator.Errors()[6], "OIDC Server has clients with duplicate ID's")
|
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) {
|
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.")
|
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) {
|
func TestValidateIdentityProvidersShouldSetDefaultValues(t *testing.T) {
|
||||||
validator := schema.NewStructValidator()
|
validator := schema.NewStructValidator()
|
||||||
config := &schema.IdentityProvidersConfiguration{
|
config := &schema.IdentityProvidersConfiguration{
|
||||||
|
|
|
@ -14,18 +14,19 @@ func NewClient(config schema.OpenIDConnectClientConfiguration) (client *Internal
|
||||||
client = &InternalClient{
|
client = &InternalClient{
|
||||||
ID: config.ID,
|
ID: config.ID,
|
||||||
Description: config.Description,
|
Description: config.Description,
|
||||||
Policy: authorization.PolicyToLevel(config.Policy),
|
|
||||||
Secret: []byte(config.Secret),
|
Secret: []byte(config.Secret),
|
||||||
|
Public: config.Public,
|
||||||
|
|
||||||
|
Policy: authorization.PolicyToLevel(config.Policy),
|
||||||
|
|
||||||
|
Audience: config.Audience,
|
||||||
|
Scopes: config.Scopes,
|
||||||
RedirectURIs: config.RedirectURIs,
|
RedirectURIs: config.RedirectURIs,
|
||||||
GrantTypes: config.GrantTypes,
|
GrantTypes: config.GrantTypes,
|
||||||
ResponseTypes: config.ResponseTypes,
|
ResponseTypes: config.ResponseTypes,
|
||||||
Scopes: config.Scopes,
|
ResponseModes: []fosite.ResponseModeType{fosite.ResponseModeDefault},
|
||||||
|
|
||||||
UserinfoSigningAlgorithm: config.UserinfoSigningAlgorithm,
|
UserinfoSigningAlgorithm: config.UserinfoSigningAlgorithm,
|
||||||
|
|
||||||
ResponseModes: []fosite.ResponseModeType{
|
|
||||||
fosite.ResponseModeDefault,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, mode := range config.ResponseModes {
|
for _, mode := range config.ResponseModes {
|
||||||
|
|
|
@ -36,18 +36,18 @@ type InternalClient struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Description string `json:"-"`
|
Description string `json:"-"`
|
||||||
Secret []byte `json:"client_secret,omitempty"`
|
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"`
|
RedirectURIs []string `json:"redirect_uris"`
|
||||||
GrantTypes []string `json:"grant_types"`
|
GrantTypes []string `json:"grant_types"`
|
||||||
ResponseTypes []string `json:"response_types"`
|
ResponseTypes []string `json:"response_types"`
|
||||||
Scopes []string `json:"scopes"`
|
|
||||||
Audience []string `json:"audience"`
|
|
||||||
Public bool `json:"public"`
|
|
||||||
|
|
||||||
ResponseModes []fosite.ResponseModeType `json:"response_modes"`
|
ResponseModes []fosite.ResponseModeType `json:"response_modes"`
|
||||||
|
|
||||||
UserinfoSigningAlgorithm string `json:"userinfo_signed_response_alg,omitempty"`
|
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.
|
// KeyManager keeps track of all of the active/inactive rsa keys and provides them to services requiring them.
|
||||||
|
|
Loading…
Reference in New Issue