feat(oidc): pairwise subject identifiers (#3116)
Allows configuring clients with a sector identifier to allow pairwise subject types.pull/3129/head
parent
0a970aef8a
commit
8bb8207808
|
@ -799,6 +799,11 @@ 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
|
||||||
|
|
||||||
|
## Sector Identifiers are occasionally used to generate pairwise subject identifiers. In most cases this is not
|
||||||
|
## necessary. Read the documentation for more information.
|
||||||
|
## The subject identifier must be the host component of a URL, which is a domain name with an optional port.
|
||||||
|
# sector_identifier: example.com
|
||||||
|
|
||||||
## Sets the client to public. This should typically not be set, please see the documentation for usage.
|
## Sets the client to public. This should typically not be set, please see the documentation for usage.
|
||||||
# public: false
|
# public: false
|
||||||
|
|
||||||
|
|
|
@ -48,6 +48,7 @@ identity_providers:
|
||||||
- id: myapp
|
- id: myapp
|
||||||
description: My Application
|
description: My Application
|
||||||
secret: this_is_a_secret
|
secret: this_is_a_secret
|
||||||
|
sector_identifier: ''
|
||||||
public: false
|
public: false
|
||||||
authorization_policy: two_factor
|
authorization_policy: two_factor
|
||||||
audience: []
|
audience: []
|
||||||
|
@ -73,7 +74,6 @@ identity_providers:
|
||||||
## Options
|
## Options
|
||||||
|
|
||||||
### hmac_secret
|
### hmac_secret
|
||||||
|
|
||||||
<div markdown="1">
|
<div markdown="1">
|
||||||
type: string
|
type: string
|
||||||
{: .label .label-config .label-purple }
|
{: .label .label-config .label-purple }
|
||||||
|
@ -87,7 +87,6 @@ byte string for the purpose of meeting the required format. You must [generate t
|
||||||
Should be defined using a [secret](../secrets.md) which is the recommended for containerized deployments.
|
Should be defined using a [secret](../secrets.md) which is the recommended for containerized deployments.
|
||||||
|
|
||||||
### issuer_private_key
|
### issuer_private_key
|
||||||
|
|
||||||
<div markdown="1">
|
<div markdown="1">
|
||||||
type: string
|
type: string
|
||||||
{: .label .label-config .label-purple }
|
{: .label .label-config .label-purple }
|
||||||
|
@ -104,7 +103,6 @@ private key into your configuration.
|
||||||
Should be defined using a [secret](../secrets.md) which is the recommended for containerized deployments.
|
Should be defined using a [secret](../secrets.md) which is the recommended for containerized deployments.
|
||||||
|
|
||||||
### access_token_lifespan
|
### access_token_lifespan
|
||||||
|
|
||||||
<div markdown="1">
|
<div markdown="1">
|
||||||
type: duration
|
type: duration
|
||||||
{: .label .label-config .label-purple }
|
{: .label .label-config .label-purple }
|
||||||
|
@ -118,7 +116,6 @@ The maximum lifetime of an access token. It's generally recommended keeping this
|
||||||
For more information read these docs about [token lifespan].
|
For more information read these docs about [token lifespan].
|
||||||
|
|
||||||
### authorize_code_lifespan
|
### authorize_code_lifespan
|
||||||
|
|
||||||
<div markdown="1">
|
<div markdown="1">
|
||||||
type: duration
|
type: duration
|
||||||
{: .label .label-config .label-purple }
|
{: .label .label-config .label-purple }
|
||||||
|
@ -132,7 +129,6 @@ The maximum lifetime of an authorize code. This can be rather short, as the auth
|
||||||
obtain the other token types. For more information read these docs about [token lifespan].
|
obtain the other token types. For more information read these docs about [token lifespan].
|
||||||
|
|
||||||
### id_token_lifespan
|
### id_token_lifespan
|
||||||
|
|
||||||
<div markdown="1">
|
<div markdown="1">
|
||||||
type: duration
|
type: duration
|
||||||
{: .label .label-config .label-purple }
|
{: .label .label-config .label-purple }
|
||||||
|
@ -145,7 +141,6 @@ required: no
|
||||||
The maximum lifetime of an ID token. For more information read these docs about [token lifespan].
|
The maximum lifetime of an ID token. For more information read these docs about [token lifespan].
|
||||||
|
|
||||||
### refresh_token_lifespan
|
### refresh_token_lifespan
|
||||||
|
|
||||||
<div markdown="1">
|
<div markdown="1">
|
||||||
type: string
|
type: string
|
||||||
{: .label .label-config .label-purple }
|
{: .label .label-config .label-purple }
|
||||||
|
@ -165,7 +160,6 @@ A good starting point is 50% more or 30 minutes more (which ever is less) time t
|
||||||
token lifespan is 90 minutes.
|
token lifespan is 90 minutes.
|
||||||
|
|
||||||
### enable_client_debug_messages
|
### enable_client_debug_messages
|
||||||
|
|
||||||
<div markdown="1">
|
<div markdown="1">
|
||||||
type: boolean
|
type: boolean
|
||||||
{: .label .label-config .label-purple }
|
{: .label .label-config .label-purple }
|
||||||
|
@ -178,7 +172,6 @@ required: no
|
||||||
Allows additional debug messages to be sent to the clients.
|
Allows additional debug messages to be sent to the clients.
|
||||||
|
|
||||||
### minimum_parameter_entropy
|
### minimum_parameter_entropy
|
||||||
|
|
||||||
<div markdown="1">
|
<div markdown="1">
|
||||||
type: integer
|
type: integer
|
||||||
{: .label .label-config .label-purple }
|
{: .label .label-config .label-purple }
|
||||||
|
@ -305,7 +298,6 @@ have the scheme http or https and do not have the hostname of localhost.
|
||||||
A list of clients to configure. The options for each client are described below.
|
A list of clients to configure. The options for each client are described below.
|
||||||
|
|
||||||
#### id
|
#### id
|
||||||
|
|
||||||
<div markdown="1">
|
<div markdown="1">
|
||||||
type: string
|
type: string
|
||||||
{: .label .label-config .label-purple }
|
{: .label .label-config .label-purple }
|
||||||
|
@ -317,7 +309,6 @@ The Client ID for this client. It must exactly match the Client ID configured in
|
||||||
consuming this client.
|
consuming this client.
|
||||||
|
|
||||||
#### description
|
#### description
|
||||||
|
|
||||||
<div markdown="1">
|
<div markdown="1">
|
||||||
type: string
|
type: string
|
||||||
{: .label .label-config .label-purple }
|
{: .label .label-config .label-purple }
|
||||||
|
@ -330,7 +321,6 @@ required: no
|
||||||
A friendly description for this client shown in the UI. This defaults to the same as the ID.
|
A friendly description for this client shown in the UI. This defaults to the same as the ID.
|
||||||
|
|
||||||
#### secret
|
#### secret
|
||||||
|
|
||||||
<div markdown="1">
|
<div markdown="1">
|
||||||
type: string
|
type: string
|
||||||
{: .label .label-config .label-purple }
|
{: .label .label-config .label-purple }
|
||||||
|
@ -345,8 +335,45 @@ 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
|
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.
|
type. To set the client type to public see the [public](#public) configuration option.
|
||||||
|
|
||||||
#### public
|
#### sector_identifier
|
||||||
|
<div markdown="1">
|
||||||
|
type: string
|
||||||
|
{: .label .label-config .label-purple }
|
||||||
|
default: ''
|
||||||
|
{: .label .label-config .label-blue }
|
||||||
|
required: no
|
||||||
|
{: .label .label-config .label-red }
|
||||||
|
</div>
|
||||||
|
|
||||||
|
_**Important Note:** because adjusting this option will inevitably change the `sub` claim of all tokens generated for
|
||||||
|
the specified client, changing this should cause the relying party to detect all future authorizations as completely new
|
||||||
|
users._
|
||||||
|
|
||||||
|
Must be an empty string or the host component of a URL. This is commonly just the domain name, but may also include a
|
||||||
|
port.
|
||||||
|
|
||||||
|
Authelia utilizes UUID version 4 subject identifiers. By default the public subject identifier type is utilized for all
|
||||||
|
clients. This means the subject identifiers will be the same for all clients. This configuration option enables pairwise
|
||||||
|
for this client, and configures the sector identifier utilized for both the storage and the lookup of the subject
|
||||||
|
identifier.
|
||||||
|
|
||||||
|
1. All clients who do not have this configured will generate the same subject identifier for a particular user regardless
|
||||||
|
of which client obtains the ID token.
|
||||||
|
2. All clients which have the same sector identifier will:
|
||||||
|
1. have the same subject identifier for a particular user when compared to clients with the same sector identifier.
|
||||||
|
2. have a completely different subject identifier for a particular user whe compared to:
|
||||||
|
1. any client with the public subject identifier type.
|
||||||
|
2. any client with a differing sector identifier.
|
||||||
|
|
||||||
|
In specific but limited scenarios this option is beneficial for privacy reasons. In particular this is useful when the
|
||||||
|
party utilizing the _Authelia_ [OpenID Connect] Authorization Server is foreign and not controlled by the user. It would
|
||||||
|
prevent the third party utilizing the subject identifier with another third party in order to track the user.
|
||||||
|
|
||||||
|
Keep in mind depending on the other claims they may still be able to perform this tracking and it is not a silver bullet.
|
||||||
|
There are very few benefits when utilizing this in a homelab or business where no third party is utilizing
|
||||||
|
the server.
|
||||||
|
|
||||||
|
#### public
|
||||||
<div markdown="1">
|
<div markdown="1">
|
||||||
type: bool
|
type: bool
|
||||||
{: .label .label-config .label-purple }
|
{: .label .label-config .label-purple }
|
||||||
|
@ -364,7 +391,6 @@ 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.
|
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">
|
||||||
type: string
|
type: string
|
||||||
{: .label .label-config .label-purple }
|
{: .label .label-config .label-purple }
|
||||||
|
@ -377,7 +403,6 @@ 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`.
|
||||||
|
|
||||||
#### audience
|
#### audience
|
||||||
|
|
||||||
<div markdown="1">
|
<div markdown="1">
|
||||||
type: list(string)
|
type: list(string)
|
||||||
{: .label .label-config .label-purple }
|
{: .label .label-config .label-purple }
|
||||||
|
@ -388,7 +413,6 @@ required: no
|
||||||
A list of audiences this client is allowed to request.
|
A list of audiences this client is allowed to request.
|
||||||
|
|
||||||
#### scopes
|
#### scopes
|
||||||
|
|
||||||
<div markdown="1">
|
<div markdown="1">
|
||||||
type: list(string)
|
type: list(string)
|
||||||
{: .label .label-config .label-purple }
|
{: .label .label-config .label-purple }
|
||||||
|
@ -403,7 +427,6 @@ information. The documentation for the application you want to use with Authelia
|
||||||
you with the scopes to allow.
|
you with the scopes to allow.
|
||||||
|
|
||||||
#### redirect_uris
|
#### redirect_uris
|
||||||
|
|
||||||
<div markdown="1">
|
<div markdown="1">
|
||||||
type: list(string)
|
type: list(string)
|
||||||
{: .label .label-config .label-purple }
|
{: .label .label-config .label-purple }
|
||||||
|
@ -425,7 +448,6 @@ their redirect URIs are as follows:
|
||||||
4. The client can ignore rule 3 and use `urn:ietf:wg:oauth:2.0:oob` if it is a [public](#public) client type.
|
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">
|
||||||
type: list(string)
|
type: list(string)
|
||||||
{: .label .label-config .label-purple }
|
{: .label .label-config .label-purple }
|
||||||
|
@ -440,7 +462,6 @@ know what you're doing_. Valid options are: `implicit`, `refresh_token`, `author
|
||||||
`client_credentials`.
|
`client_credentials`.
|
||||||
|
|
||||||
#### response_types
|
#### response_types
|
||||||
|
|
||||||
<div markdown="1">
|
<div markdown="1">
|
||||||
type: list(string)
|
type: list(string)
|
||||||
{: .label .label-config .label-purple }
|
{: .label .label-config .label-purple }
|
||||||
|
@ -455,7 +476,6 @@ know what you're doing_. Valid options are: `code`, `code id_token`, `id_token`,
|
||||||
`token id_token code`.
|
`token id_token code`.
|
||||||
|
|
||||||
#### response_modes
|
#### response_modes
|
||||||
|
|
||||||
<div markdown="1">
|
<div markdown="1">
|
||||||
type: list(string)
|
type: list(string)
|
||||||
{: .label .label-config .label-purple }
|
{: .label .label-config .label-purple }
|
||||||
|
@ -469,7 +489,6 @@ A list of response modes this client can return. It is recommended that this isn
|
||||||
know what you're doing. Potential values are `form_post`, `query`, and `fragment`.
|
know what you're doing. Potential values are `form_post`, `query`, and `fragment`.
|
||||||
|
|
||||||
#### userinfo_signing_algorithm
|
#### userinfo_signing_algorithm
|
||||||
|
|
||||||
<div markdown="1">
|
<div markdown="1">
|
||||||
type: string
|
type: string
|
||||||
{: .label .label-config .label-purple }
|
{: .label .label-config .label-purple }
|
||||||
|
|
|
@ -799,6 +799,11 @@ 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
|
||||||
|
|
||||||
|
## Sector Identifiers are occasionally used to generate pairwise subject identifiers. In most cases this is not
|
||||||
|
## necessary. Read the documentation for more information.
|
||||||
|
## The subject identifier must be the host component of a URL, which is a domain name with an optional port.
|
||||||
|
# sector_identifier: example.com
|
||||||
|
|
||||||
## Sets the client to public. This should typically not be set, please see the documentation for usage.
|
## Sets the client to public. This should typically not be set, please see the documentation for usage.
|
||||||
# public: false
|
# public: false
|
||||||
|
|
||||||
|
|
|
@ -41,10 +41,11 @@ type OpenIDConnectCORSConfiguration struct {
|
||||||
|
|
||||||
// OpenIDConnectClientConfiguration configuration for an OpenID Connect client.
|
// OpenIDConnectClientConfiguration configuration for an OpenID Connect client.
|
||||||
type OpenIDConnectClientConfiguration struct {
|
type OpenIDConnectClientConfiguration struct {
|
||||||
ID string `koanf:"id"`
|
ID string `koanf:"id"`
|
||||||
Description string `koanf:"description"`
|
Description string `koanf:"description"`
|
||||||
Secret string `koanf:"secret"`
|
Secret string `koanf:"secret"`
|
||||||
Public bool `koanf:"public"`
|
SectorIdentifier url.URL `koanf:"sector_identifier"`
|
||||||
|
Public bool `koanf:"public"`
|
||||||
|
|
||||||
Policy string `koanf:"authorization_policy"`
|
Policy string `koanf:"authorization_policy"`
|
||||||
|
|
||||||
|
|
|
@ -155,6 +155,12 @@ const (
|
||||||
"'%s' but one option is configured as '%s'"
|
"'%s' but one option is configured as '%s'"
|
||||||
errFmtOIDCClientInvalidUserinfoAlgorithm = "identity_providers: oidc: client '%s': option " +
|
errFmtOIDCClientInvalidUserinfoAlgorithm = "identity_providers: oidc: client '%s': option " +
|
||||||
"'userinfo_signing_algorithm' must be one of '%s' but it is configured as '%s'"
|
"'userinfo_signing_algorithm' must be one of '%s' but it is configured as '%s'"
|
||||||
|
errFmtOIDCClientInvalidSectorIdentifier = "identity_providers: oidc: client '%s': option " +
|
||||||
|
"'sector_identifier' with value '%s': must be a URL with only the host component for example '%s' but it has a %s with the value '%s'"
|
||||||
|
errFmtOIDCClientInvalidSectorIdentifierWithoutValue = "identity_providers: oidc: client '%s': option " +
|
||||||
|
"'sector_identifier' with value '%s': must be a URL with only the host component for example '%s' but it has a %s"
|
||||||
|
errFmtOIDCClientInvalidSectorIdentifierHost = "identity_providers: oidc: client '%s': option " +
|
||||||
|
"'sector_identifier' with value '%s': must be a URL with only the host component but appears to be invalid"
|
||||||
errFmtOIDCServerInsecureParameterEntropy = "openid connect provider: SECURITY ISSUE - minimum parameter entropy is " +
|
errFmtOIDCServerInsecureParameterEntropy = "openid connect provider: SECURITY ISSUE - minimum parameter entropy is " +
|
||||||
"configured to an unsafe value, it should be above 8 but it's configured to %d"
|
"configured to an unsafe value, it should be above 8 but it's configured to %d"
|
||||||
)
|
)
|
||||||
|
@ -482,8 +488,9 @@ var ValidKeys = []string{
|
||||||
"identity_providers.oidc.clients",
|
"identity_providers.oidc.clients",
|
||||||
"identity_providers.oidc.clients[].id",
|
"identity_providers.oidc.clients[].id",
|
||||||
"identity_providers.oidc.clients[].description",
|
"identity_providers.oidc.clients[].description",
|
||||||
"identity_providers.oidc.clients[].public",
|
|
||||||
"identity_providers.oidc.clients[].secret",
|
"identity_providers.oidc.clients[].secret",
|
||||||
|
"identity_providers.oidc.clients[].sector_identifier",
|
||||||
|
"identity_providers.oidc.clients[].public",
|
||||||
"identity_providers.oidc.clients[].redirect_uris",
|
"identity_providers.oidc.clients[].redirect_uris",
|
||||||
"identity_providers.oidc.clients[].authorization_policy",
|
"identity_providers.oidc.clients[].authorization_policy",
|
||||||
"identity_providers.oidc.clients[].scopes",
|
"identity_providers.oidc.clients[].scopes",
|
||||||
|
|
|
@ -151,6 +151,7 @@ func validateOIDCClients(config *schema.OpenIDConnectConfiguration, validator *s
|
||||||
validator.Push(fmt.Errorf(errFmtOIDCClientInvalidPolicy, client.ID, client.Policy))
|
validator.Push(fmt.Errorf(errFmtOIDCClientInvalidPolicy, client.ID, client.Policy))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validateOIDCClientSectorIdentifier(client, validator)
|
||||||
validateOIDCClientScopes(c, config, validator)
|
validateOIDCClientScopes(c, config, validator)
|
||||||
validateOIDCClientGrantTypes(c, config, validator)
|
validateOIDCClientGrantTypes(c, config, validator)
|
||||||
validateOIDCClientResponseTypes(c, config, validator)
|
validateOIDCClientResponseTypes(c, config, validator)
|
||||||
|
@ -168,6 +169,38 @@ func validateOIDCClients(config *schema.OpenIDConnectConfiguration, validator *s
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateOIDCClientSectorIdentifier(client schema.OpenIDConnectClientConfiguration, validator *schema.StructValidator) {
|
||||||
|
if client.SectorIdentifier.String() != "" {
|
||||||
|
if client.SectorIdentifier.Scheme != "" {
|
||||||
|
validator.Push(fmt.Errorf(errFmtOIDCClientInvalidSectorIdentifier, client.ID, client.SectorIdentifier.String(), client.SectorIdentifier.Host, "scheme", client.SectorIdentifier.Scheme))
|
||||||
|
|
||||||
|
if client.SectorIdentifier.Path != "" {
|
||||||
|
validator.Push(fmt.Errorf(errFmtOIDCClientInvalidSectorIdentifier, client.ID, client.SectorIdentifier.String(), client.SectorIdentifier.Host, "path", client.SectorIdentifier.Path))
|
||||||
|
}
|
||||||
|
|
||||||
|
if client.SectorIdentifier.RawQuery != "" {
|
||||||
|
validator.Push(fmt.Errorf(errFmtOIDCClientInvalidSectorIdentifier, client.ID, client.SectorIdentifier.String(), client.SectorIdentifier.Host, "query", client.SectorIdentifier.RawQuery))
|
||||||
|
}
|
||||||
|
|
||||||
|
if client.SectorIdentifier.Fragment != "" {
|
||||||
|
validator.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() != "" {
|
||||||
|
validator.Push(fmt.Errorf(errFmtOIDCClientInvalidSectorIdentifier, client.ID, client.SectorIdentifier.String(), client.SectorIdentifier.Host, "username", client.SectorIdentifier.User.Username()))
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, set := client.SectorIdentifier.User.Password(); set {
|
||||||
|
validator.Push(fmt.Errorf(errFmtOIDCClientInvalidSectorIdentifierWithoutValue, client.ID, client.SectorIdentifier.String(), client.SectorIdentifier.Host, "password"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if client.SectorIdentifier.Host == "" {
|
||||||
|
validator.Push(fmt.Errorf(errFmtOIDCClientInvalidSectorIdentifierHost, client.ID, client.SectorIdentifier.String()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func validateOIDCClientScopes(c int, configuration *schema.OpenIDConnectConfiguration, validator *schema.StructValidator) {
|
func validateOIDCClientScopes(c int, configuration *schema.OpenIDConnectConfiguration, validator *schema.StructValidator) {
|
||||||
if len(configuration.Clients[c].Scopes) == 0 {
|
if len(configuration.Clients[c].Scopes) == 0 {
|
||||||
configuration.Clients[c].Scopes = schema.DefaultOpenIDConnectClientConfiguration.Scopes
|
configuration.Clients[c].Scopes = schema.DefaultOpenIDConnectClientConfiguration.Scopes
|
||||||
|
|
|
@ -3,6 +3,7 @@ package validator
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/url"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -151,13 +152,22 @@ func TestShouldRaiseErrorWhenOIDCServerNoClients(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) {
|
func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) {
|
||||||
|
mustParseURL := func(u string) url.URL {
|
||||||
|
out, err := url.Parse(u)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return *out
|
||||||
|
}
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
Name string
|
Name string
|
||||||
Clients []schema.OpenIDConnectClientConfiguration
|
Clients []schema.OpenIDConnectClientConfiguration
|
||||||
Errors []error
|
Errors []string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
Name: "empty",
|
Name: "EmptyIDAndSecret",
|
||||||
Clients: []schema.OpenIDConnectClientConfiguration{
|
Clients: []schema.OpenIDConnectClientConfiguration{
|
||||||
{
|
{
|
||||||
ID: "",
|
ID: "",
|
||||||
|
@ -166,13 +176,13 @@ func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) {
|
||||||
RedirectURIs: []string{},
|
RedirectURIs: []string{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Errors: []error{
|
Errors: []string{
|
||||||
fmt.Errorf(errFmtOIDCClientInvalidSecret, ""),
|
fmt.Sprintf(errFmtOIDCClientInvalidSecret, ""),
|
||||||
errors.New(errFmtOIDCClientsWithEmptyID),
|
errFmtOIDCClientsWithEmptyID,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "client-1",
|
Name: "InvalidPolicy",
|
||||||
Clients: []schema.OpenIDConnectClientConfiguration{
|
Clients: []schema.OpenIDConnectClientConfiguration{
|
||||||
{
|
{
|
||||||
ID: "client-1",
|
ID: "client-1",
|
||||||
|
@ -183,10 +193,10 @@ func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Errors: []error{fmt.Errorf(errFmtOIDCClientInvalidPolicy, "client-1", "a-policy")},
|
Errors: []string{fmt.Sprintf(errFmtOIDCClientInvalidPolicy, "client-1", "a-policy")},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "client-duplicate",
|
Name: "ClientIDDuplicated",
|
||||||
Clients: []schema.OpenIDConnectClientConfiguration{
|
Clients: []schema.OpenIDConnectClientConfiguration{
|
||||||
{
|
{
|
||||||
ID: "client-x",
|
ID: "client-x",
|
||||||
|
@ -201,10 +211,10 @@ func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) {
|
||||||
RedirectURIs: []string{},
|
RedirectURIs: []string{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Errors: []error{errors.New(errFmtOIDCClientsDuplicateID)},
|
Errors: []string{errFmtOIDCClientsDuplicateID},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "client-check-uri-parse",
|
Name: "RedirectURIInvalid",
|
||||||
Clients: []schema.OpenIDConnectClientConfiguration{
|
Clients: []schema.OpenIDConnectClientConfiguration{
|
||||||
{
|
{
|
||||||
ID: "client-check-uri-parse",
|
ID: "client-check-uri-parse",
|
||||||
|
@ -215,12 +225,12 @@ func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Errors: []error{
|
Errors: []string{
|
||||||
fmt.Errorf(errFmtOIDCClientRedirectURICantBeParsed, "client-check-uri-parse", "http://abc@%two", errors.New("parse \"http://abc@%two\": invalid URL escape \"%tw\"")),
|
fmt.Sprintf(errFmtOIDCClientRedirectURICantBeParsed, "client-check-uri-parse", "http://abc@%two", errors.New("parse \"http://abc@%two\": invalid URL escape \"%tw\"")),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "client-check-uri-abs",
|
Name: "RedirectURINotAbsolute",
|
||||||
Clients: []schema.OpenIDConnectClientConfiguration{
|
Clients: []schema.OpenIDConnectClientConfiguration{
|
||||||
{
|
{
|
||||||
ID: "client-check-uri-abs",
|
ID: "client-check-uri-abs",
|
||||||
|
@ -231,8 +241,47 @@ func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Errors: []error{
|
Errors: []string{
|
||||||
fmt.Errorf(errFmtOIDCClientRedirectURIAbsolute, "client-check-uri-abs", "google.com"),
|
fmt.Sprintf(errFmtOIDCClientRedirectURIAbsolute, "client-check-uri-abs", "google.com"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "InvalidSectorIdentifierInvalidURL",
|
||||||
|
Clients: []schema.OpenIDConnectClientConfiguration{
|
||||||
|
{
|
||||||
|
ID: "client-invalid-sector",
|
||||||
|
Secret: "a-secret",
|
||||||
|
Policy: policyTwoFactor,
|
||||||
|
RedirectURIs: []string{
|
||||||
|
"https://google.com",
|
||||||
|
},
|
||||||
|
SectorIdentifier: mustParseURL("https://user:pass@example.com/path?query=abc#fragment"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Errors: []string{
|
||||||
|
fmt.Sprintf(errFmtOIDCClientInvalidSectorIdentifier, "client-invalid-sector", "https://user:pass@example.com/path?query=abc#fragment", "example.com", "scheme", "https"),
|
||||||
|
fmt.Sprintf(errFmtOIDCClientInvalidSectorIdentifier, "client-invalid-sector", "https://user:pass@example.com/path?query=abc#fragment", "example.com", "path", "/path"),
|
||||||
|
fmt.Sprintf(errFmtOIDCClientInvalidSectorIdentifier, "client-invalid-sector", "https://user:pass@example.com/path?query=abc#fragment", "example.com", "query", "query=abc"),
|
||||||
|
fmt.Sprintf(errFmtOIDCClientInvalidSectorIdentifier, "client-invalid-sector", "https://user:pass@example.com/path?query=abc#fragment", "example.com", "fragment", "fragment"),
|
||||||
|
fmt.Sprintf(errFmtOIDCClientInvalidSectorIdentifier, "client-invalid-sector", "https://user:pass@example.com/path?query=abc#fragment", "example.com", "username", "user"),
|
||||||
|
fmt.Sprintf(errFmtOIDCClientInvalidSectorIdentifierWithoutValue, "client-invalid-sector", "https://user:pass@example.com/path?query=abc#fragment", "example.com", "password"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "InvalidSectorIdentifierInvalidHost",
|
||||||
|
Clients: []schema.OpenIDConnectClientConfiguration{
|
||||||
|
{
|
||||||
|
ID: "client-invalid-sector",
|
||||||
|
Secret: "a-secret",
|
||||||
|
Policy: policyTwoFactor,
|
||||||
|
RedirectURIs: []string{
|
||||||
|
"https://google.com",
|
||||||
|
},
|
||||||
|
SectorIdentifier: mustParseURL("example.com/path?query=abc#fragment"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Errors: []string{
|
||||||
|
fmt.Sprintf(errFmtOIDCClientInvalidSectorIdentifierHost, "client-invalid-sector", "example.com/path?query=abc#fragment"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -250,7 +299,14 @@ func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) {
|
||||||
|
|
||||||
ValidateIdentityProviders(config, validator)
|
ValidateIdentityProviders(config, validator)
|
||||||
|
|
||||||
assert.ElementsMatch(t, validator.Errors(), tc.Errors)
|
errs := validator.Errors()
|
||||||
|
|
||||||
|
require.Len(t, errs, len(tc.Errors))
|
||||||
|
for i, errStr := range tc.Errors {
|
||||||
|
t.Run(fmt.Sprintf("Error%d", i+1), func(t *testing.T) {
|
||||||
|
assert.EqualError(t, errs[i], errStr)
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,10 +12,11 @@ import (
|
||||||
// NewClient creates a new Client.
|
// NewClient creates a new Client.
|
||||||
func NewClient(config schema.OpenIDConnectClientConfiguration) (client *Client) {
|
func NewClient(config schema.OpenIDConnectClientConfiguration) (client *Client) {
|
||||||
client = &Client{
|
client = &Client{
|
||||||
ID: config.ID,
|
ID: config.ID,
|
||||||
Description: config.Description,
|
Description: config.Description,
|
||||||
Secret: []byte(config.Secret),
|
Secret: []byte(config.Secret),
|
||||||
Public: config.Public,
|
SectorIdentifier: config.SectorIdentifier.String(),
|
||||||
|
Public: config.Public,
|
||||||
|
|
||||||
Policy: authorization.PolicyToLevel(config.Policy),
|
Policy: authorization.PolicyToLevel(config.Policy),
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package oidc
|
package oidc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/url"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
@ -27,6 +28,39 @@ func TestOpenIDConnectProvider_NewOpenIDConnectProvider_BadIssuerKey(t *testing.
|
||||||
assert.Error(t, err, "abc")
|
assert.Error(t, err, "abc")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNewOpenIDConnectProvider_ShouldEnableOptionalDiscoveryValues(t *testing.T) {
|
||||||
|
provider, err := NewOpenIDConnectProvider(&schema.OpenIDConnectConfiguration{
|
||||||
|
IssuerPrivateKey: exampleIssuerPrivateKey,
|
||||||
|
EnablePKCEPlainChallenge: true,
|
||||||
|
HMACSecret: "asbdhaaskmdlkamdklasmdlkams",
|
||||||
|
Clients: []schema.OpenIDConnectClientConfiguration{
|
||||||
|
{
|
||||||
|
ID: "a-client",
|
||||||
|
Secret: "a-client-secret",
|
||||||
|
SectorIdentifier: url.URL{Host: "google.com"},
|
||||||
|
Policy: "one_factor",
|
||||||
|
RedirectURIs: []string{
|
||||||
|
"https://google.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.True(t, provider.Pairwise())
|
||||||
|
|
||||||
|
disco := provider.GetOpenIDConnectWellKnownConfiguration("https://example.com")
|
||||||
|
|
||||||
|
assert.Len(t, disco.SubjectTypesSupported, 2)
|
||||||
|
assert.Contains(t, disco.SubjectTypesSupported, "public")
|
||||||
|
assert.Contains(t, disco.SubjectTypesSupported, "pairwise")
|
||||||
|
|
||||||
|
assert.Len(t, disco.CodeChallengeMethodsSupported, 2)
|
||||||
|
assert.Contains(t, disco.CodeChallengeMethodsSupported, "S256")
|
||||||
|
assert.Contains(t, disco.CodeChallengeMethodsSupported, "S256")
|
||||||
|
}
|
||||||
|
|
||||||
func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GoodConfiguration(t *testing.T) {
|
func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GoodConfiguration(t *testing.T) {
|
||||||
provider, err := NewOpenIDConnectProvider(&schema.OpenIDConnectConfiguration{
|
provider, err := NewOpenIDConnectProvider(&schema.OpenIDConnectConfiguration{
|
||||||
IssuerPrivateKey: exampleIssuerPrivateKey,
|
IssuerPrivateKey: exampleIssuerPrivateKey,
|
||||||
|
|
|
@ -97,9 +97,9 @@ type OpenIDConnectStore struct {
|
||||||
// Client represents the client internally.
|
// Client represents the client internally.
|
||||||
type Client struct {
|
type Client struct {
|
||||||
ID string
|
ID string
|
||||||
SectorIdentifier string
|
|
||||||
Description string
|
Description string
|
||||||
Secret []byte
|
Secret []byte
|
||||||
|
SectorIdentifier string
|
||||||
Public bool
|
Public bool
|
||||||
|
|
||||||
Policy authorization.Level
|
Policy authorization.Level
|
||||||
|
|
Loading…
Reference in New Issue