feat(oidc): add additional config options, accurate token times, and refactoring (#1991)
* This gives admins more control over their OIDC installation exposing options that had defaults before. Things like lifespans for authorize codes, access tokens, id tokens, refresh tokens, a option to enable the debug client messages, minimum parameter entropy. It also allows admins to configure the response modes. * Additionally this records specific values about a users session indicating when they performed a specific authz factor so this is represented in the token accurately. * Lastly we also implemented a OIDC key manager which calculates the kid for jwk's using the SHA1 digest instead of being static, or more specifically the first 7 chars. As per https://datatracker.ietf.org/doc/html/draft-ietf-jose-json-web-key#section-8.1.1 the kid should not exceed 8 chars. While it's allowed to exceed 8 chars, it must only be done so with a compelling reason, which we do not have.pull/2141/head^2
parent
2dbd7ed219
commit
ef549f851d
|
@ -133,8 +133,8 @@ func startServer() {
|
||||||
authorizer := authorization.NewAuthorizer(config)
|
authorizer := authorization.NewAuthorizer(config)
|
||||||
sessionProvider := session.NewProvider(config.Session, autheliaCertPool)
|
sessionProvider := session.NewProvider(config.Session, autheliaCertPool)
|
||||||
regulator := regulation.NewRegulator(config.Regulation, storageProvider, clock)
|
regulator := regulation.NewRegulator(config.Regulation, storageProvider, clock)
|
||||||
oidcProvider, err := oidc.NewOpenIDConnectProvider(config.IdentityProviders.OIDC)
|
|
||||||
|
|
||||||
|
oidcProvider, err := oidc.NewOpenIDConnectProvider(config.IdentityProviders.OIDC)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatalf("Error initializing OpenID Connect Provider: %+v", err)
|
logger.Fatalf("Error initializing OpenID Connect Provider: %+v", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@ port: 9091
|
||||||
## They should be in base64 format, and have one of the following extensions: *.cer, *.crt, *.pem.
|
## They should be in base64 format, and have one of the following extensions: *.cer, *.crt, *.pem.
|
||||||
# certificates_directory: /config/certificates
|
# certificates_directory: /config/certificates
|
||||||
|
|
||||||
## The theme to display: light, dark, grey.
|
## The theme to display: light, dark, grey, auto.
|
||||||
theme: light
|
theme: light
|
||||||
|
|
||||||
##
|
##
|
||||||
|
@ -65,7 +65,7 @@ jwt_secret: a_very_important_secret
|
||||||
## in such a case.
|
## in such a case.
|
||||||
##
|
##
|
||||||
## Note: this parameter is optional. If not provided, user won't be redirected upon successful authentication.
|
## Note: this parameter is optional. If not provided, user won't be redirected upon successful authentication.
|
||||||
default_redirection_url: https://home.example.com:8080/
|
default_redirection_url: https://home.example.com/
|
||||||
|
|
||||||
##
|
##
|
||||||
## TOTP Configuration
|
## TOTP Configuration
|
||||||
|
@ -586,6 +586,19 @@ notifier:
|
||||||
# --- KEY START
|
# --- KEY START
|
||||||
# --- KEY END
|
# --- KEY END
|
||||||
|
|
||||||
|
## The lifespans configure the expiration for these token types.
|
||||||
|
# access_token_lifespan: 1h
|
||||||
|
# authorize_code_lifespan: 1m
|
||||||
|
# id_token_lifespan: 1h
|
||||||
|
# refresh_token_lifespan: 90m
|
||||||
|
|
||||||
|
## 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 reasons.
|
||||||
|
# minimum_parameter_entropy: 8
|
||||||
|
|
||||||
## Clients is a list of known clients and their configuration.
|
## Clients is a list of known clients and their configuration.
|
||||||
# clients:
|
# clients:
|
||||||
# -
|
# -
|
||||||
|
@ -616,10 +629,16 @@ notifier:
|
||||||
## 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:
|
||||||
# - refresh_token
|
# - refresh_token
|
||||||
# - "authorization_code
|
# - authorization_code
|
||||||
|
|
||||||
## Response Types configures which responses this client can be sent.
|
## Response Types configures which responses this client can be sent.
|
||||||
## 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.
|
||||||
# response_types:
|
# response_types:
|
||||||
# - code
|
# - code
|
||||||
|
|
||||||
|
## Response Modes configures which response modes this client supports.
|
||||||
|
# response_modes:
|
||||||
|
# - form_post
|
||||||
|
# - query
|
||||||
|
# - fragment
|
||||||
...
|
...
|
||||||
|
|
|
@ -102,6 +102,11 @@ identity_providers:
|
||||||
issuer_private_key: |
|
issuer_private_key: |
|
||||||
--- KEY START
|
--- KEY START
|
||||||
--- KEY END
|
--- KEY END
|
||||||
|
access_token_lifespan: 1h
|
||||||
|
authorize_code_lifespan: 1m
|
||||||
|
id_token_lifespan: 1h
|
||||||
|
refresh_token_lifespan: 720h
|
||||||
|
enable_client_debug_messages: false
|
||||||
clients:
|
clients:
|
||||||
- id: myapp
|
- id: myapp
|
||||||
description: My Application
|
description: My Application
|
||||||
|
@ -119,11 +124,21 @@ identity_providers:
|
||||||
- authorization_code
|
- authorization_code
|
||||||
response_types:
|
response_types:
|
||||||
- code
|
- code
|
||||||
|
response_modes:
|
||||||
|
- form_post
|
||||||
|
- query
|
||||||
|
- fragment
|
||||||
```
|
```
|
||||||
|
|
||||||
## Options
|
## Options
|
||||||
|
|
||||||
### hmac_secret
|
### hmac_secret
|
||||||
|
<div markdown="1">
|
||||||
|
type: string
|
||||||
|
{: .label .label-config .label-purple }
|
||||||
|
required: yes
|
||||||
|
{: .label .label-config .label-red }
|
||||||
|
</div>
|
||||||
|
|
||||||
The HMAC secret used to sign the [OpenID Connect] JWT's. The provided string is hashed to a SHA256 byte string for
|
The HMAC secret used to sign the [OpenID Connect] JWT's. The provided string is hashed to a SHA256 byte string for
|
||||||
the purpose of meeting the required format.
|
the purpose of meeting the required format.
|
||||||
|
@ -131,49 +146,213 @@ the purpose of meeting the required format.
|
||||||
Can also be defined using a [secret](../secrets.md) which is the recommended for containerized deployments.
|
Can also be defined using a [secret](../secrets.md) which is the recommended for containerized deployments.
|
||||||
|
|
||||||
### issuer_private_key
|
### issuer_private_key
|
||||||
|
<div markdown="1">
|
||||||
|
type: string
|
||||||
|
{: .label .label-config .label-purple }
|
||||||
|
required: yes
|
||||||
|
{: .label .label-config .label-red }
|
||||||
|
</div>
|
||||||
|
|
||||||
The private key in DER base64 encoded PEM format used to encrypt the [OpenID Connect] JWT's.
|
The private key in DER base64 encoded PEM format used to encrypt the [OpenID Connect] JWT's. This can easily be
|
||||||
|
generated using the Authelia binary using the following syntax:
|
||||||
|
|
||||||
|
```console
|
||||||
|
authelia rsa generate --dir /config
|
||||||
|
```
|
||||||
|
|
||||||
Can also be defined using a [secret](../secrets.md) which is the recommended for containerized deployments.
|
Can also be defined using a [secret](../secrets.md) which is the recommended for containerized deployments.
|
||||||
|
|
||||||
|
### access_token_lifespan
|
||||||
|
<div markdown="1">
|
||||||
|
type: duration
|
||||||
|
{: .label .label-config .label-purple }
|
||||||
|
default: 1h
|
||||||
|
{: .label .label-config .label-blue }
|
||||||
|
required: no
|
||||||
|
{: .label .label-config .label-green }
|
||||||
|
</div>
|
||||||
|
|
||||||
|
The maximum lifetime of an access token. It's generally recommended keeping this short similar to the default.
|
||||||
|
For more information read these docs about [token lifespan].
|
||||||
|
|
||||||
|
### authorize_code_lifespan
|
||||||
|
<div markdown="1">
|
||||||
|
type: duration
|
||||||
|
{: .label .label-config .label-purple }
|
||||||
|
default: 1m
|
||||||
|
{: .label .label-config .label-blue }
|
||||||
|
required: no
|
||||||
|
{: .label .label-config .label-green }
|
||||||
|
</div>
|
||||||
|
|
||||||
|
The maximum lifetime of an authorize code. This can be rather short, as the authorize code should only be needed to
|
||||||
|
obtain the other token types. For more information read these docs about [token lifespan].
|
||||||
|
|
||||||
|
### id_token_lifespan
|
||||||
|
<div markdown="1">
|
||||||
|
type: duration
|
||||||
|
{: .label .label-config .label-purple }
|
||||||
|
default: 1h
|
||||||
|
{: .label .label-config .label-blue }
|
||||||
|
required: no
|
||||||
|
{: .label .label-config .label-green }
|
||||||
|
</div>
|
||||||
|
|
||||||
|
The maximum lifetime of an ID token. For more information read these docs about [token lifespan].
|
||||||
|
|
||||||
|
### refresh_token_lifespan
|
||||||
|
<div markdown="1">
|
||||||
|
type: string
|
||||||
|
{: .label .label-config .label-purple }
|
||||||
|
default: 30d
|
||||||
|
{: .label .label-config .label-blue }
|
||||||
|
required: no
|
||||||
|
{: .label .label-config .label-green }
|
||||||
|
</div>
|
||||||
|
|
||||||
|
The maximum lifetime of a refresh token. This should typically be slightly more the other token lifespans. This is
|
||||||
|
because the 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].
|
||||||
|
|
||||||
|
### enable_client_debug_messages
|
||||||
|
<div markdown="1">
|
||||||
|
type: boolean
|
||||||
|
{: .label .label-config .label-purple }
|
||||||
|
default: false
|
||||||
|
{: .label .label-config .label-blue }
|
||||||
|
required: no
|
||||||
|
{: .label .label-config .label-green }
|
||||||
|
</div>
|
||||||
|
|
||||||
|
Allows additional debug messages to be sent to the clients.
|
||||||
|
|
||||||
|
### minimum_parameter_entropy
|
||||||
|
<div markdown="1">
|
||||||
|
type: integer
|
||||||
|
{: .label .label-config .label-purple }
|
||||||
|
default: 8
|
||||||
|
{: .label .label-config .label-blue }
|
||||||
|
required: no
|
||||||
|
{: .label .label-config .label-green }
|
||||||
|
</div>
|
||||||
|
|
||||||
|
This controls the minimum length of the `nonce` and `state` parameters.
|
||||||
|
|
||||||
|
***Security Notice:*** Changing this value is generally discouraged, reducing it from the default can theoretically make
|
||||||
|
certain scenarios less secure. It highly encouraged that if your OpenID Connect RP does not send these parameters or
|
||||||
|
sends parameters with a lower length than the default that they implement a change rather than changing this value.
|
||||||
|
|
||||||
### clients
|
### clients
|
||||||
|
|
||||||
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">
|
||||||
|
type: string
|
||||||
|
{: .label .label-config .label-purple }
|
||||||
|
required: yes
|
||||||
|
{: .label .label-config .label-red }
|
||||||
|
</div>
|
||||||
|
|
||||||
The Client ID for this client. Must be configured in the application consuming this client.
|
The Client ID for this client. Must be configured in the application consuming this client.
|
||||||
|
|
||||||
#### description
|
#### description
|
||||||
|
<div markdown="1">
|
||||||
|
type: string
|
||||||
|
{: .label .label-config .label-purple }
|
||||||
|
default: *same as id*
|
||||||
|
{: .label .label-config .label-blue }
|
||||||
|
required: no
|
||||||
|
{: .label .label-config .label-green }
|
||||||
|
</div>
|
||||||
|
|
||||||
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">
|
||||||
|
type: string
|
||||||
|
{: .label .label-config .label-purple }
|
||||||
|
required: yes
|
||||||
|
{: .label .label-config .label-red }
|
||||||
|
</div>
|
||||||
|
|
||||||
The shared secret between Authelia and the application consuming this client. Currently this is stored in plain text.
|
The shared secret between Authelia and the application consuming this client. Currently this is stored in plain text.
|
||||||
|
|
||||||
#### authorization_policy
|
#### authorization_policy
|
||||||
|
<div markdown="1">
|
||||||
|
type: string
|
||||||
|
{: .label .label-config .label-purple }
|
||||||
|
default: two_factor
|
||||||
|
{: .label .label-config .label-blue }
|
||||||
|
required: no
|
||||||
|
{: .label .label-config .label-green }
|
||||||
|
</div>
|
||||||
|
|
||||||
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
|
#### 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 URL's this client will redirect to. All other callbacks will be considered unsafe. The URL's
|
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.
|
are case-sensitive.
|
||||||
|
|
||||||
#### scopes
|
#### scopes
|
||||||
|
<div markdown="1">
|
||||||
|
type: list(string)
|
||||||
|
{: .label .label-config .label-purple }
|
||||||
|
default: openid, groups, profile, email
|
||||||
|
{: .label .label-config .label-blue }
|
||||||
|
required: no
|
||||||
|
{: .label .label-config .label-green }
|
||||||
|
</div>
|
||||||
|
|
||||||
A list of scopes to allow this client to consume. See [scope definitions](#scope-definitions) for more information.
|
A list of scopes to allow this client to consume. See [scope definitions](#scope-definitions) for more information.
|
||||||
|
|
||||||
#### grant_types
|
#### grant_types
|
||||||
|
<div markdown="1">
|
||||||
|
type: list(string)
|
||||||
|
{: .label .label-config .label-purple }
|
||||||
|
default: refresh_token, authorization_code
|
||||||
|
{: .label .label-config .label-blue }
|
||||||
|
required: no
|
||||||
|
{: .label .label-config .label-green }
|
||||||
|
</div>
|
||||||
|
|
||||||
A list of grant types this client can return. It is recommended that this isn't configured at this time unless you know
|
A list of grant types this client can return. It is recommended that this isn't configured at this time unless you know
|
||||||
what you're doing.
|
what you're doing. Valid options are: `implicit`, `refresh_token`, `authorization_code`, `password`,
|
||||||
|
`client_credentials`.
|
||||||
|
|
||||||
#### response_types
|
#### response_types
|
||||||
|
<div markdown="1">
|
||||||
|
type: list(string)
|
||||||
|
{: .label .label-config .label-purple }
|
||||||
|
default: code
|
||||||
|
{: .label .label-config .label-blue }
|
||||||
|
required: no
|
||||||
|
{: .label .label-config .label-green }
|
||||||
|
</div>
|
||||||
|
|
||||||
A list of response types this client can return. It is recommended that this isn't configured at this time unless you
|
A list of response types this client can return. It is recommended that this isn't configured at this time unless you
|
||||||
know what you're doing.
|
know what you're doing. Valid options are: `code`, `code id_token`, `id_token`, `token id_token`, `token`,
|
||||||
|
`token id_token code`.
|
||||||
|
|
||||||
|
#### response_modes
|
||||||
|
<div markdown="1">
|
||||||
|
type: list(string)
|
||||||
|
{: .label .label-config .label-purple }
|
||||||
|
default: form_post, query, fragment
|
||||||
|
{: .label .label-config .label-blue }
|
||||||
|
required: no
|
||||||
|
{: .label .label-config .label-green }
|
||||||
|
</div>
|
||||||
|
|
||||||
|
A list of response modes this client can return. It is recommended that this isn't configured at this time unless you
|
||||||
|
know what you're doing. Potential values are `form_post`, `query`, and `fragment`.
|
||||||
|
|
||||||
## Scope Definitions
|
## Scope Definitions
|
||||||
|
|
||||||
|
@ -183,17 +362,17 @@ This is the default scope for openid. This field is forced on every client by th
|
||||||
validation that Authelia does.
|
validation that Authelia does.
|
||||||
|
|
||||||
|JWT Field|JWT Type |Authelia Attribute|Description |
|
|JWT Field|JWT Type |Authelia Attribute|Description |
|
||||||
|:-------:|:-----------:|:----------------:|:--------------------------------------:|
|
|:-------:|:-----------:|:----------------:|:-------------------------------------------:|
|
||||||
|sub |string |Username |The username the user used to login with|
|
|sub |string |Username |The username the user used to login with |
|
||||||
|scope |string |scopes |Granted scopes (space delimited) |
|
|scope |string |scopes |Granted scopes (space delimited) |
|
||||||
|scp |array[string]|scopes |Granted scopes |
|
|scp |array[string]|scopes |Granted scopes |
|
||||||
|iss |string |hostname |The issuer name, determined by URL |
|
|iss |string |hostname |The issuer name, determined by URL |
|
||||||
|at_hash |string |_N/A_ |Access Token Hash |
|
|at_hash |string |_N/A_ |Access Token Hash |
|
||||||
|auth_time|number |_N/A_ |Authorize Time |
|
|
||||||
|aud |array[string]|_N/A_ |Audience |
|
|aud |array[string]|_N/A_ |Audience |
|
||||||
|exp |number |_N/A_ |Expires |
|
|exp |number |_N/A_ |Expires |
|
||||||
|iat |number |_N/A_ |Issued At |
|
|auth_time|number |_N/A_ |The time the user authenticated with Authelia|
|
||||||
|rat |number |_N/A_ |Requested At |
|
|rat |number |_N/A_ |The time when the token was requested |
|
||||||
|
|iat |number |_N/A_ |The time when the token was issued |
|
||||||
|jti |string(uuid) |_N/A_ |JWT Identifier |
|
|jti |string(uuid) |_N/A_ |JWT Identifier |
|
||||||
|
|
||||||
### groups
|
### groups
|
||||||
|
@ -208,10 +387,11 @@ This scope includes the groups the authentication backend reports the user is a
|
||||||
|
|
||||||
This scope includes the email information the authentication backend reports about the user in the token.
|
This scope includes the email information the authentication backend reports about the user in the token.
|
||||||
|
|
||||||
|JWT Field |JWT Type|Authelia Attribute|Description |
|
|JWT Field |JWT Type |Authelia Attribute|Description |
|
||||||
|:------------:|:------:|:----------------:|:-------------------------------------------------------:|
|
|:------------:|:-----------:|:----------------:|:-------------------------------------------------------:|
|
||||||
|email |string |email[0] |The first email in the list of emails |
|
|email |string |email[0] |The first email address in the list of emails |
|
||||||
|email_verified|bool |_N/A_ |If the email is verified, assumed true for the time being|
|
|email_verified|bool |_N/A_ |If the email is verified, assumed true for the time being|
|
||||||
|
|alt_emails |array[string]|email[1:] |All email addresses that are not in the email JWT field |
|
||||||
|
|
||||||
### profile
|
### profile
|
||||||
|
|
||||||
|
@ -223,3 +403,4 @@ This scope includes the profile information the authentication backend reports a
|
||||||
|
|
||||||
|
|
||||||
[OpenID Connect]: https://openid.net/connect/
|
[OpenID Connect]: https://openid.net/connect/
|
||||||
|
[token lifespan]: https://docs.apigee.com/api-platform/antipatterns/oauth-long-expiration
|
|
@ -65,7 +65,7 @@ jwt_secret: a_very_important_secret
|
||||||
## in such a case.
|
## in such a case.
|
||||||
##
|
##
|
||||||
## Note: this parameter is optional. If not provided, user won't be redirected upon successful authentication.
|
## Note: this parameter is optional. If not provided, user won't be redirected upon successful authentication.
|
||||||
default_redirection_url: https://home.example.com:8080/
|
default_redirection_url: https://home.example.com/
|
||||||
|
|
||||||
##
|
##
|
||||||
## TOTP Configuration
|
## TOTP Configuration
|
||||||
|
@ -586,6 +586,19 @@ notifier:
|
||||||
# --- KEY START
|
# --- KEY START
|
||||||
# --- KEY END
|
# --- KEY END
|
||||||
|
|
||||||
|
## The lifespans configure the expiration for these token types.
|
||||||
|
# access_token_lifespan: 1h
|
||||||
|
# authorize_code_lifespan: 1m
|
||||||
|
# id_token_lifespan: 1h
|
||||||
|
# refresh_token_lifespan: 90m
|
||||||
|
|
||||||
|
## 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 reasons.
|
||||||
|
# minimum_parameter_entropy: 8
|
||||||
|
|
||||||
## Clients is a list of known clients and their configuration.
|
## Clients is a list of known clients and their configuration.
|
||||||
# clients:
|
# clients:
|
||||||
# -
|
# -
|
||||||
|
@ -616,10 +629,16 @@ notifier:
|
||||||
## 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:
|
||||||
# - refresh_token
|
# - refresh_token
|
||||||
# - "authorization_code
|
# - authorization_code
|
||||||
|
|
||||||
## Response Types configures which responses this client can be sent.
|
## Response Types configures which responses this client can be sent.
|
||||||
## 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.
|
||||||
# response_types:
|
# response_types:
|
||||||
# - code
|
# - code
|
||||||
|
|
||||||
|
## Response Modes configures which response modes this client supports.
|
||||||
|
# response_modes:
|
||||||
|
# - form_post
|
||||||
|
# - query
|
||||||
|
# - fragment
|
||||||
...
|
...
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package schema
|
package schema
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
// IdentityProvidersConfiguration represents the IdentityProviders 2.0 configuration for Authelia.
|
// IdentityProvidersConfiguration represents the IdentityProviders 2.0 configuration for Authelia.
|
||||||
type IdentityProvidersConfiguration struct {
|
type IdentityProvidersConfiguration struct {
|
||||||
OIDC *OpenIDConnectConfiguration `mapstructure:"oidc"`
|
OIDC *OpenIDConnectConfiguration `mapstructure:"oidc"`
|
||||||
|
@ -11,6 +13,13 @@ type OpenIDConnectConfiguration struct {
|
||||||
HMACSecret string `mapstructure:"hmac_secret"`
|
HMACSecret string `mapstructure:"hmac_secret"`
|
||||||
IssuerPrivateKey string `mapstructure:"issuer_private_key"`
|
IssuerPrivateKey string `mapstructure:"issuer_private_key"`
|
||||||
|
|
||||||
|
AccessTokenLifespan time.Duration `mapstructure:"access_token_lifespan"`
|
||||||
|
AuthorizeCodeLifespan time.Duration `mapstructure:"authorize_code_lifespan"`
|
||||||
|
IDTokenLifespan time.Duration `mapstructure:"id_token_lifespan"`
|
||||||
|
RefreshTokenLifespan time.Duration `mapstructure:"refresh_token_lifespan"`
|
||||||
|
EnableClientDebugMessages bool `mapstructure:"enable_client_debug_messages"`
|
||||||
|
MinimumParameterEntropy int `mapstructure:"minimum_parameter_entropy"`
|
||||||
|
|
||||||
Clients []OpenIDConnectClientConfiguration `mapstructure:"clients"`
|
Clients []OpenIDConnectClientConfiguration `mapstructure:"clients"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,12 +33,22 @@ type OpenIDConnectClientConfiguration struct {
|
||||||
Scopes []string `mapstructure:"scopes"`
|
Scopes []string `mapstructure:"scopes"`
|
||||||
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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultOpenIDConnectClientConfiguration contains defaults for OIDC AutheliaClients.
|
// DefaultOpenIDConnectConfiguration contains defaults for OIDC.
|
||||||
var DefaultOpenIDConnectClientConfiguration = OpenIDConnectClientConfiguration{
|
var DefaultOpenIDConnectConfiguration = OpenIDConnectConfiguration{
|
||||||
Scopes: []string{"openid", "groups", "profile", "email"},
|
AccessTokenLifespan: time.Hour,
|
||||||
ResponseTypes: []string{"code"},
|
AuthorizeCodeLifespan: time.Minute,
|
||||||
GrantTypes: []string{"refresh_token", "authorization_code"},
|
IDTokenLifespan: time.Hour,
|
||||||
Policy: "two_factor",
|
RefreshTokenLifespan: time.Minute * 90,
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultOpenIDConnectClientConfiguration contains defaults for OIDC Clients.
|
||||||
|
var DefaultOpenIDConnectClientConfiguration = OpenIDConnectClientConfiguration{
|
||||||
|
Policy: "two_factor",
|
||||||
|
Scopes: []string{"openid", "groups", "profile", "email"},
|
||||||
|
GrantTypes: []string{"refresh_token", "authorization_code"},
|
||||||
|
ResponseTypes: []string{"code"},
|
||||||
|
ResponseModes: []string{"form_post", "query", "fragment"},
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,10 +2,10 @@ package validator
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/authelia/authelia/internal/configuration/schema"
|
"github.com/authelia/authelia/internal/configuration/schema"
|
||||||
|
"github.com/authelia/authelia/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
var defaultPort = 9091
|
var defaultPort = 9091
|
||||||
|
@ -41,9 +41,9 @@ func ValidateConfiguration(configuration *schema.Configuration, validator *schem
|
||||||
}
|
}
|
||||||
|
|
||||||
if configuration.DefaultRedirectionURL != "" {
|
if configuration.DefaultRedirectionURL != "" {
|
||||||
_, err := url.ParseRequestURI(configuration.DefaultRedirectionURL)
|
err := utils.IsStringAbsURL(configuration.DefaultRedirectionURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
validator.Push(fmt.Errorf("Unable to parse default redirection url"))
|
validator.Push(fmt.Errorf("Value for \"default_redirection_url\" is invalid: %+v", err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -150,11 +150,11 @@ func TestShouldRaiseErrorWithUndefinedJWTSecretKey(t *testing.T) {
|
||||||
func TestShouldRaiseErrorWithBadDefaultRedirectionURL(t *testing.T) {
|
func TestShouldRaiseErrorWithBadDefaultRedirectionURL(t *testing.T) {
|
||||||
validator := schema.NewStructValidator()
|
validator := schema.NewStructValidator()
|
||||||
config := newDefaultConfig()
|
config := newDefaultConfig()
|
||||||
config.DefaultRedirectionURL = "abc"
|
config.DefaultRedirectionURL = "bad_default_redirection_url"
|
||||||
|
|
||||||
ValidateConfiguration(&config, validator)
|
ValidateConfiguration(&config, validator)
|
||||||
require.Len(t, validator.Errors(), 1)
|
require.Len(t, validator.Errors(), 1)
|
||||||
assert.EqualError(t, validator.Errors()[0], "Unable to parse default redirection url")
|
assert.EqualError(t, validator.Errors()[0], "Value for \"default_redirection_url\" is invalid: the url 'bad_default_redirection_url' is not absolute because it doesn't start with a scheme like 'http://' or 'https://'")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestShouldNotOverrideCertificatesDirectoryAndShouldPassWhenBlank(t *testing.T) {
|
func TestShouldNotOverrideCertificatesDirectoryAndShouldPassWhenBlank(t *testing.T) {
|
||||||
|
|
|
@ -12,14 +12,28 @@ const (
|
||||||
errFmtSessionRedisHostRequired = "The host must be provided when using the %s session provider"
|
errFmtSessionRedisHostRequired = "The host must be provided when using the %s session provider"
|
||||||
errFmtSessionRedisHostOrNodesRequired = "Either the host or a node must be provided when using the %s session provider"
|
errFmtSessionRedisHostOrNodesRequired = "Either the host or a node must be provided when using the %s session provider"
|
||||||
|
|
||||||
errOAuthOIDCServerClientRedirectURIFmt = "OIDC Server Client redirect URI %s has an invalid scheme %s, should be http or https"
|
errFmtOIDCServerClientRedirectURI = "OIDC Client with ID '%s' redirect URI %s has an invalid scheme '%s', " +
|
||||||
errOAuthOIDCServerClientRedirectURICantBeParsedFmt = "OIDC Client with ID '%s' has an invalid redirect URI '%s' could not be parsed: %v"
|
"should be http or https"
|
||||||
errIdentityProvidersOIDCServerClientInvalidPolicyFmt = "OIDC Client with ID '%s' has an invalid policy '%s', should be either 'one_factor' or 'two_factor'"
|
errFmtOIDCServerClientRedirectURICantBeParsed = "OIDC Client with ID '%s' has an invalid redirect URI '%s' " +
|
||||||
errIdentityProvidersOIDCServerClientInvalidSecFmt = "OIDC Client with ID '%s' has an empty secret"
|
"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
|
||||||
|
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', " +
|
||||||
|
"must be one of: '%s'"
|
||||||
|
errFmtOIDCServerClientInvalidResponseMode = "OIDC Client with ID '%s' has an invalid response mode '%s', " +
|
||||||
|
"must be one of: '%s'"
|
||||||
|
errFmtOIDCServerInsecureParameterEntropy = "SECURITY ISSUE: OIDC minimum parameter entropy is configured to an " +
|
||||||
|
"unsafe value, it should be above 8 but it's configured to %d."
|
||||||
|
|
||||||
errFileHashing = "config key incorrect: authentication_backend.file.hashing should be authentication_backend.file.password"
|
errFileHashing = "config key incorrect: authentication_backend.file.hashing should be " +
|
||||||
errFilePHashing = "config key incorrect: authentication_backend.file.password_hashing should be authentication_backend.file.password"
|
"authentication_backend.file.password"
|
||||||
errFilePOptions = "config key incorrect: authentication_backend.file.password_options should be authentication_backend.file.password"
|
errFilePHashing = "config key incorrect: authentication_backend.file.password_hashing should be " +
|
||||||
|
"authentication_backend.file.password"
|
||||||
|
errFilePOptions = "config key incorrect: authentication_backend.file.password_options should be " +
|
||||||
|
"authentication_backend.file.password"
|
||||||
|
|
||||||
bypassPolicy = "bypass"
|
bypassPolicy = "bypass"
|
||||||
oneFactorPolicy = "one_factor"
|
oneFactorPolicy = "one_factor"
|
||||||
|
@ -31,6 +45,8 @@ const (
|
||||||
|
|
||||||
schemeLDAP = "ldap"
|
schemeLDAP = "ldap"
|
||||||
schemeLDAPS = "ldaps"
|
schemeLDAPS = "ldaps"
|
||||||
|
schemeHTTP = "http"
|
||||||
|
schemeHTTPS = "https"
|
||||||
|
|
||||||
testBadTimer = "-1"
|
testBadTimer = "-1"
|
||||||
testInvalidPolicy = "invalid"
|
testInvalidPolicy = "invalid"
|
||||||
|
@ -43,12 +59,16 @@ const (
|
||||||
testTLSCert = "/tmp/cert.pem"
|
testTLSCert = "/tmp/cert.pem"
|
||||||
testTLSKey = "/tmp/key.pem"
|
testTLSKey = "/tmp/key.pem"
|
||||||
|
|
||||||
errAccessControlInvalidPolicyWithSubjects = "Policy [bypass] for rule #%d domain %s with subjects %s is invalid. It is " +
|
errAccessControlInvalidPolicyWithSubjects = "Policy [bypass] for rule #%d domain %s with subjects %s is invalid. " +
|
||||||
"not supported to configure both policy bypass and subjects. For more information see: " +
|
"It is not supported to configure both policy bypass and subjects. For more information see: " +
|
||||||
"https://www.authelia.com/docs/configuration/access-control.html#combining-subjects-and-the-bypass-policy"
|
"https://www.authelia.com/docs/configuration/access-control.html#combining-subjects-and-the-bypass-policy"
|
||||||
)
|
)
|
||||||
|
|
||||||
var validLoggingLevels = []string{"trace", "debug", "info", "warn", "error"}
|
var validLoggingLevels = []string{"trace", "debug", "info", "warn", "error"}
|
||||||
|
var validScopes = []string{"openid", "email", "profile", "groups", "offline_access"}
|
||||||
|
var validOIDCGrantTypes = []string{"implicit", "refresh_token", "authorization_code", "password", "client_credentials"}
|
||||||
|
var validOIDCResponseModes = []string{"form_post", "query", "fragment"}
|
||||||
|
|
||||||
var validRequestMethods = []string{"GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "TRACE", "CONNECT", "OPTIONS"}
|
var validRequestMethods = []string{"GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "TRACE", "CONNECT", "OPTIONS"}
|
||||||
|
|
||||||
// SecretNames contains a map of secret names.
|
// SecretNames contains a map of secret names.
|
||||||
|
@ -211,6 +231,11 @@ var validKeys = []string{
|
||||||
|
|
||||||
// Identity Provider Keys.
|
// Identity Provider Keys.
|
||||||
"identity_providers.oidc.clients",
|
"identity_providers.oidc.clients",
|
||||||
|
"identity_providers.oidc.id_token_lifespan",
|
||||||
|
"identity_providers.oidc.access_token_lifespan",
|
||||||
|
"identity_providers.oidc.refresh_token_lifespan",
|
||||||
|
"identity_providers.oidc.authorize_code_lifespan",
|
||||||
|
"identity_providers.oidc.enable_client_debug_messages",
|
||||||
}
|
}
|
||||||
|
|
||||||
var replacedKeys = map[string]string{
|
var replacedKeys = map[string]string{
|
||||||
|
|
|
@ -3,6 +3,8 @@ package validator
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/authelia/authelia/internal/configuration/schema"
|
"github.com/authelia/authelia/internal/configuration/schema"
|
||||||
"github.com/authelia/authelia/internal/utils"
|
"github.com/authelia/authelia/internal/utils"
|
||||||
|
@ -19,6 +21,26 @@ func validateOIDC(configuration *schema.OpenIDConnectConfiguration, validator *s
|
||||||
validator.Push(fmt.Errorf("OIDC Server issuer private key must be provided"))
|
validator.Push(fmt.Errorf("OIDC Server issuer private key must be provided"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if configuration.AccessTokenLifespan == time.Duration(0) {
|
||||||
|
configuration.AccessTokenLifespan = schema.DefaultOpenIDConnectConfiguration.AccessTokenLifespan
|
||||||
|
}
|
||||||
|
|
||||||
|
if configuration.AuthorizeCodeLifespan == time.Duration(0) {
|
||||||
|
configuration.AuthorizeCodeLifespan = schema.DefaultOpenIDConnectConfiguration.AuthorizeCodeLifespan
|
||||||
|
}
|
||||||
|
|
||||||
|
if configuration.IDTokenLifespan == time.Duration(0) {
|
||||||
|
configuration.IDTokenLifespan = schema.DefaultOpenIDConnectConfiguration.IDTokenLifespan
|
||||||
|
}
|
||||||
|
|
||||||
|
if configuration.RefreshTokenLifespan == time.Duration(0) {
|
||||||
|
configuration.RefreshTokenLifespan = schema.DefaultOpenIDConnectConfiguration.RefreshTokenLifespan
|
||||||
|
}
|
||||||
|
|
||||||
|
if configuration.MinimumParameterEntropy != 0 && configuration.MinimumParameterEntropy < 8 {
|
||||||
|
validator.PushWarning(fmt.Errorf(errFmtOIDCServerInsecureParameterEntropy, configuration.MinimumParameterEntropy))
|
||||||
|
}
|
||||||
|
|
||||||
validateOIDCClients(configuration, validator)
|
validateOIDCClients(configuration, validator)
|
||||||
|
|
||||||
if len(configuration.Clients) == 0 {
|
if len(configuration.Clients) == 0 {
|
||||||
|
@ -47,28 +69,19 @@ func validateOIDCClients(configuration *schema.OpenIDConnectConfiguration, valid
|
||||||
}
|
}
|
||||||
|
|
||||||
if client.Secret == "" {
|
if client.Secret == "" {
|
||||||
validator.Push(fmt.Errorf(errIdentityProvidersOIDCServerClientInvalidSecFmt, 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
|
||||||
} else if client.Policy != oneFactorPolicy && client.Policy != twoFactorPolicy {
|
} else if client.Policy != oneFactorPolicy && client.Policy != twoFactorPolicy {
|
||||||
validator.Push(fmt.Errorf(errIdentityProvidersOIDCServerClientInvalidPolicyFmt, client.ID, client.Policy))
|
validator.Push(fmt.Errorf(errFmtOIDCServerClientInvalidPolicy, client.ID, client.Policy))
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(client.Scopes) == 0 {
|
validateOIDCClientScopes(c, configuration, validator)
|
||||||
configuration.Clients[c].Scopes = schema.DefaultOpenIDConnectClientConfiguration.Scopes
|
validateOIDCClientGrantTypes(c, configuration, validator)
|
||||||
} else if !utils.IsStringInSlice("openid", client.Scopes) {
|
validateOIDCClientResponseTypes(c, configuration, validator)
|
||||||
configuration.Clients[c].Scopes = append(configuration.Clients[c].Scopes, "openid")
|
validateOIDCClientResponseModes(c, configuration, validator)
|
||||||
}
|
|
||||||
|
|
||||||
if len(client.GrantTypes) == 0 {
|
|
||||||
configuration.Clients[c].GrantTypes = schema.DefaultOpenIDConnectClientConfiguration.GrantTypes
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(client.ResponseTypes) == 0 {
|
|
||||||
configuration.Clients[c].ResponseTypes = schema.DefaultOpenIDConnectClientConfiguration.ResponseTypes
|
|
||||||
}
|
|
||||||
|
|
||||||
validateOIDCClientRedirectURIs(client, validator)
|
validateOIDCClientRedirectURIs(client, validator)
|
||||||
}
|
}
|
||||||
|
@ -82,17 +95,73 @@ func validateOIDCClients(configuration *schema.OpenIDConnectConfiguration, valid
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateOIDCClientScopes(c int, configuration *schema.OpenIDConnectConfiguration, validator *schema.StructValidator) {
|
||||||
|
if len(configuration.Clients[c].Scopes) == 0 {
|
||||||
|
configuration.Clients[c].Scopes = schema.DefaultOpenIDConnectClientConfiguration.Scopes
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !utils.IsStringInSlice("openid", configuration.Clients[c].Scopes) {
|
||||||
|
configuration.Clients[c].Scopes = append(configuration.Clients[c].Scopes, "openid")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, scope := range configuration.Clients[c].Scopes {
|
||||||
|
if !utils.IsStringInSlice(scope, validScopes) {
|
||||||
|
validator.Push(fmt.Errorf(
|
||||||
|
errFmtOIDCServerClientInvalidScope,
|
||||||
|
configuration.Clients[c].ID, scope, strings.Join(validScopes, "', '")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateOIDCClientGrantTypes(c int, configuration *schema.OpenIDConnectConfiguration, validator *schema.StructValidator) {
|
||||||
|
if len(configuration.Clients[c].GrantTypes) == 0 {
|
||||||
|
configuration.Clients[c].GrantTypes = schema.DefaultOpenIDConnectClientConfiguration.GrantTypes
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, grantType := range configuration.Clients[c].GrantTypes {
|
||||||
|
if !utils.IsStringInSlice(grantType, validOIDCGrantTypes) {
|
||||||
|
validator.Push(fmt.Errorf(
|
||||||
|
errFmtOIDCServerClientInvalidGrantType,
|
||||||
|
configuration.Clients[c].ID, grantType, strings.Join(validOIDCGrantTypes, "', '")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateOIDCClientResponseTypes(c int, configuration *schema.OpenIDConnectConfiguration, _ *schema.StructValidator) {
|
||||||
|
if len(configuration.Clients[c].ResponseTypes) == 0 {
|
||||||
|
configuration.Clients[c].ResponseTypes = schema.DefaultOpenIDConnectClientConfiguration.ResponseTypes
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateOIDCClientResponseModes(c int, configuration *schema.OpenIDConnectConfiguration, validator *schema.StructValidator) {
|
||||||
|
if len(configuration.Clients[c].ResponseModes) == 0 {
|
||||||
|
configuration.Clients[c].ResponseModes = schema.DefaultOpenIDConnectClientConfiguration.ResponseModes
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, responseMode := range configuration.Clients[c].ResponseModes {
|
||||||
|
if !utils.IsStringInSlice(responseMode, validOIDCResponseModes) {
|
||||||
|
validator.Push(fmt.Errorf(
|
||||||
|
errFmtOIDCServerClientInvalidResponseMode,
|
||||||
|
configuration.Clients[c].ID, responseMode, strings.Join(validOIDCResponseModes, "', '")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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)
|
parsedURI, err := url.Parse(redirectURI)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
validator.Push(fmt.Errorf(errOAuthOIDCServerClientRedirectURICantBeParsedFmt, client.ID, redirectURI, err))
|
validator.Push(fmt.Errorf(errFmtOIDCServerClientRedirectURICantBeParsed, client.ID, redirectURI, err))
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
if parsedURI.Scheme != "https" && parsedURI.Scheme != "http" {
|
if parsedURI.Scheme != schemeHTTPS && parsedURI.Scheme != schemeHTTP {
|
||||||
validator.Push(fmt.Errorf(errOAuthOIDCServerClientRedirectURIFmt, redirectURI, parsedURI.Scheme))
|
validator.Push(fmt.Errorf(errFmtOIDCServerClientRedirectURI, client.ID, redirectURI, parsedURI.Scheme))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
@ -92,16 +93,126 @@ func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) {
|
||||||
require.Len(t, validator.Errors(), 7)
|
require.Len(t, validator.Errors(), 7)
|
||||||
|
|
||||||
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(errIdentityProvidersOIDCServerClientInvalidSecFmt, ""))
|
assert.EqualError(t, validator.Errors()[0], fmt.Sprintf(errFmtOIDCServerClientInvalidSecret, ""))
|
||||||
assert.EqualError(t, validator.Errors()[1], fmt.Sprintf(errOAuthOIDCServerClientRedirectURIFmt, "tcp://google.com", "tcp"))
|
assert.EqualError(t, validator.Errors()[1], fmt.Sprintf(errFmtOIDCServerClientRedirectURI, "", "tcp://google.com", "tcp"))
|
||||||
assert.EqualError(t, validator.Errors()[2], fmt.Sprintf(errIdentityProvidersOIDCServerClientInvalidPolicyFmt, "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(errIdentityProvidersOIDCServerClientInvalidPolicyFmt, "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(errOAuthOIDCServerClientRedirectURICantBeParsedFmt, "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], "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()[6], "OIDC Server has clients with duplicate ID's")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestShouldNotRaiseErrorWhenOIDCServerConfiguredCorrectly(t *testing.T) {
|
func TestShouldRaiseErrorWhenOIDCClientConfiguredWithBadScopes(t *testing.T) {
|
||||||
|
validator := schema.NewStructValidator()
|
||||||
|
config := &schema.IdentityProvidersConfiguration{
|
||||||
|
OIDC: &schema.OpenIDConnectConfiguration{
|
||||||
|
HMACSecret: "rLABDrx87et5KvRHVUgTm3pezWWd8LMN",
|
||||||
|
IssuerPrivateKey: "key-material",
|
||||||
|
Clients: []schema.OpenIDConnectClientConfiguration{
|
||||||
|
{
|
||||||
|
ID: "good_id",
|
||||||
|
Secret: "good_secret",
|
||||||
|
Policy: "two_factor",
|
||||||
|
Scopes: []string{"openid", "bad_scope"},
|
||||||
|
RedirectURIs: []string{
|
||||||
|
"https://google.com/callback",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidateIdentityProviders(config, validator)
|
||||||
|
|
||||||
|
require.Len(t, validator.Errors(), 1)
|
||||||
|
assert.EqualError(t, validator.Errors()[0], "OIDC Client with ID 'good_id' has an invalid scope "+
|
||||||
|
"'bad_scope', must be one of: 'openid', 'email', 'profile', 'groups', 'offline_access'")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShouldRaiseErrorWhenOIDCClientConfiguredWithBadGrantTypes(t *testing.T) {
|
||||||
|
validator := schema.NewStructValidator()
|
||||||
|
config := &schema.IdentityProvidersConfiguration{
|
||||||
|
OIDC: &schema.OpenIDConnectConfiguration{
|
||||||
|
HMACSecret: "rLABDrx87et5KvRHVUgTm3pezWWd8LMN",
|
||||||
|
IssuerPrivateKey: "key-material",
|
||||||
|
Clients: []schema.OpenIDConnectClientConfiguration{
|
||||||
|
{
|
||||||
|
ID: "good_id",
|
||||||
|
Secret: "good_secret",
|
||||||
|
Policy: "two_factor",
|
||||||
|
GrantTypes: []string{"bad_grant_type"},
|
||||||
|
RedirectURIs: []string{
|
||||||
|
"https://google.com/callback",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidateIdentityProviders(config, validator)
|
||||||
|
|
||||||
|
require.Len(t, validator.Errors(), 1)
|
||||||
|
assert.EqualError(t, validator.Errors()[0], "OIDC Client with ID 'good_id' has an invalid grant type "+
|
||||||
|
"'bad_grant_type', must be one of: 'implicit', 'refresh_token', 'authorization_code', "+
|
||||||
|
"'password', 'client_credentials'")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShouldRaiseErrorWhenOIDCClientConfiguredWithBadResponseModes(t *testing.T) {
|
||||||
|
validator := schema.NewStructValidator()
|
||||||
|
config := &schema.IdentityProvidersConfiguration{
|
||||||
|
OIDC: &schema.OpenIDConnectConfiguration{
|
||||||
|
HMACSecret: "rLABDrx87et5KvRHVUgTm3pezWWd8LMN",
|
||||||
|
IssuerPrivateKey: "key-material",
|
||||||
|
Clients: []schema.OpenIDConnectClientConfiguration{
|
||||||
|
{
|
||||||
|
ID: "good_id",
|
||||||
|
Secret: "good_secret",
|
||||||
|
Policy: "two_factor",
|
||||||
|
ResponseModes: []string{"bad_responsemode"},
|
||||||
|
RedirectURIs: []string{
|
||||||
|
"https://google.com/callback",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidateIdentityProviders(config, validator)
|
||||||
|
|
||||||
|
require.Len(t, validator.Errors(), 1)
|
||||||
|
assert.EqualError(t, validator.Errors()[0], "OIDC Client with ID 'good_id' has an invalid response mode "+
|
||||||
|
"'bad_responsemode', must be one of: 'form_post', 'query', 'fragment'")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateIdentityProvidersShouldRaiseWarningOnSecurityIssue(t *testing.T) {
|
||||||
|
validator := schema.NewStructValidator()
|
||||||
|
config := &schema.IdentityProvidersConfiguration{
|
||||||
|
OIDC: &schema.OpenIDConnectConfiguration{
|
||||||
|
HMACSecret: "abc",
|
||||||
|
IssuerPrivateKey: "abc",
|
||||||
|
MinimumParameterEntropy: 1,
|
||||||
|
Clients: []schema.OpenIDConnectClientConfiguration{
|
||||||
|
{
|
||||||
|
ID: "good_id",
|
||||||
|
Secret: "good_secret",
|
||||||
|
Policy: "two_factor",
|
||||||
|
RedirectURIs: []string{
|
||||||
|
"https://google.com/callback",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidateIdentityProviders(config, validator)
|
||||||
|
|
||||||
|
assert.Len(t, validator.Errors(), 0)
|
||||||
|
require.Len(t, validator.Warnings(), 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 TestValidateIdentityProvidersShouldSetDefaultValues(t *testing.T) {
|
||||||
validator := schema.NewStructValidator()
|
validator := schema.NewStructValidator()
|
||||||
config := &schema.IdentityProvidersConfiguration{
|
config := &schema.IdentityProvidersConfiguration{
|
||||||
OIDC: &schema.OpenIDConnectConfiguration{
|
OIDC: &schema.OpenIDConnectConfiguration{
|
||||||
|
@ -111,7 +222,6 @@ func TestShouldNotRaiseErrorWhenOIDCServerConfiguredCorrectly(t *testing.T) {
|
||||||
{
|
{
|
||||||
ID: "a-client",
|
ID: "a-client",
|
||||||
Secret: "a-client-secret",
|
Secret: "a-client-secret",
|
||||||
Policy: oneFactorPolicy,
|
|
||||||
RedirectURIs: []string{
|
RedirectURIs: []string{
|
||||||
"https://google.com",
|
"https://google.com",
|
||||||
},
|
},
|
||||||
|
@ -134,6 +244,10 @@ func TestShouldNotRaiseErrorWhenOIDCServerConfiguredCorrectly(t *testing.T) {
|
||||||
"token",
|
"token",
|
||||||
"code",
|
"code",
|
||||||
},
|
},
|
||||||
|
ResponseModes: []string{
|
||||||
|
"form_post",
|
||||||
|
"fragment",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -141,32 +255,61 @@ func TestShouldNotRaiseErrorWhenOIDCServerConfiguredCorrectly(t *testing.T) {
|
||||||
|
|
||||||
ValidateIdentityProviders(config, validator)
|
ValidateIdentityProviders(config, validator)
|
||||||
|
|
||||||
|
assert.Len(t, validator.Warnings(), 0)
|
||||||
assert.Len(t, validator.Errors(), 0)
|
assert.Len(t, validator.Errors(), 0)
|
||||||
|
|
||||||
|
// Assert Clients[0] Policy is set to the default, and the default doesn't override Clients[1]'s Policy.
|
||||||
|
assert.Equal(t, config.OIDC.Clients[0].Policy, twoFactorPolicy)
|
||||||
|
assert.Equal(t, config.OIDC.Clients[1].Policy, oneFactorPolicy)
|
||||||
|
|
||||||
|
// Assert Clients[0] Description is set to the Clients[0] ID, and Clients[1]'s Description is not overridden.
|
||||||
assert.Equal(t, config.OIDC.Clients[0].ID, config.OIDC.Clients[0].Description)
|
assert.Equal(t, config.OIDC.Clients[0].ID, config.OIDC.Clients[0].Description)
|
||||||
assert.Equal(t, "Normal Description", config.OIDC.Clients[1].Description)
|
assert.Equal(t, "Normal Description", config.OIDC.Clients[1].Description)
|
||||||
|
|
||||||
|
// Assert Clients[0] ends up configured with the default Scopes.
|
||||||
require.Len(t, config.OIDC.Clients[0].Scopes, 4)
|
require.Len(t, config.OIDC.Clients[0].Scopes, 4)
|
||||||
assert.Equal(t, "openid", config.OIDC.Clients[0].Scopes[0])
|
assert.Equal(t, "openid", config.OIDC.Clients[0].Scopes[0])
|
||||||
assert.Equal(t, "groups", config.OIDC.Clients[0].Scopes[1])
|
assert.Equal(t, "groups", config.OIDC.Clients[0].Scopes[1])
|
||||||
assert.Equal(t, "profile", config.OIDC.Clients[0].Scopes[2])
|
assert.Equal(t, "profile", config.OIDC.Clients[0].Scopes[2])
|
||||||
assert.Equal(t, "email", config.OIDC.Clients[0].Scopes[3])
|
assert.Equal(t, "email", config.OIDC.Clients[0].Scopes[3])
|
||||||
|
|
||||||
require.Len(t, config.OIDC.Clients[0].GrantTypes, 2)
|
// Assert Clients[1] ends up configured with the configured Scopes and the openid Scope.
|
||||||
assert.Equal(t, "refresh_token", config.OIDC.Clients[0].GrantTypes[0])
|
|
||||||
assert.Equal(t, "authorization_code", config.OIDC.Clients[0].GrantTypes[1])
|
|
||||||
|
|
||||||
require.Len(t, config.OIDC.Clients[0].ResponseTypes, 1)
|
|
||||||
assert.Equal(t, "code", config.OIDC.Clients[0].ResponseTypes[0])
|
|
||||||
|
|
||||||
require.Len(t, config.OIDC.Clients[1].Scopes, 2)
|
require.Len(t, config.OIDC.Clients[1].Scopes, 2)
|
||||||
assert.Equal(t, "groups", config.OIDC.Clients[1].Scopes[0])
|
assert.Equal(t, "groups", config.OIDC.Clients[1].Scopes[0])
|
||||||
assert.Equal(t, "openid", config.OIDC.Clients[1].Scopes[1])
|
assert.Equal(t, "openid", config.OIDC.Clients[1].Scopes[1])
|
||||||
|
|
||||||
|
// Assert Clients[0] ends up configured with the default GrantTypes.
|
||||||
|
require.Len(t, config.OIDC.Clients[0].GrantTypes, 2)
|
||||||
|
assert.Equal(t, "refresh_token", config.OIDC.Clients[0].GrantTypes[0])
|
||||||
|
assert.Equal(t, "authorization_code", config.OIDC.Clients[0].GrantTypes[1])
|
||||||
|
|
||||||
|
// Assert Clients[1] ends up configured with only the configured GrantTypes.
|
||||||
require.Len(t, config.OIDC.Clients[1].GrantTypes, 1)
|
require.Len(t, config.OIDC.Clients[1].GrantTypes, 1)
|
||||||
assert.Equal(t, "refresh_token", config.OIDC.Clients[1].GrantTypes[0])
|
assert.Equal(t, "refresh_token", config.OIDC.Clients[1].GrantTypes[0])
|
||||||
|
|
||||||
|
// Assert Clients[0] ends up configured with the default ResponseTypes.
|
||||||
|
require.Len(t, config.OIDC.Clients[0].ResponseTypes, 1)
|
||||||
|
assert.Equal(t, "code", config.OIDC.Clients[0].ResponseTypes[0])
|
||||||
|
|
||||||
|
// Assert Clients[1] ends up configured only with the configured ResponseTypes.
|
||||||
require.Len(t, config.OIDC.Clients[1].ResponseTypes, 2)
|
require.Len(t, config.OIDC.Clients[1].ResponseTypes, 2)
|
||||||
assert.Equal(t, "token", config.OIDC.Clients[1].ResponseTypes[0])
|
assert.Equal(t, "token", config.OIDC.Clients[1].ResponseTypes[0])
|
||||||
assert.Equal(t, "code", config.OIDC.Clients[1].ResponseTypes[1])
|
assert.Equal(t, "code", config.OIDC.Clients[1].ResponseTypes[1])
|
||||||
|
|
||||||
|
// Assert Clients[0] ends up configured with the default ResponseModes.
|
||||||
|
require.Len(t, config.OIDC.Clients[0].ResponseModes, 3)
|
||||||
|
assert.Equal(t, "form_post", config.OIDC.Clients[0].ResponseModes[0])
|
||||||
|
assert.Equal(t, "query", config.OIDC.Clients[0].ResponseModes[1])
|
||||||
|
assert.Equal(t, "fragment", config.OIDC.Clients[0].ResponseModes[2])
|
||||||
|
|
||||||
|
// Assert Clients[1] ends up configured only with the configured ResponseModes.
|
||||||
|
require.Len(t, config.OIDC.Clients[1].ResponseModes, 2)
|
||||||
|
assert.Equal(t, "form_post", config.OIDC.Clients[1].ResponseModes[0])
|
||||||
|
assert.Equal(t, "fragment", config.OIDC.Clients[1].ResponseModes[1])
|
||||||
|
|
||||||
|
assert.Equal(t, false, config.OIDC.EnableClientDebugMessages)
|
||||||
|
assert.Equal(t, time.Hour, config.OIDC.AccessTokenLifespan)
|
||||||
|
assert.Equal(t, time.Minute, config.OIDC.AuthorizeCodeLifespan)
|
||||||
|
assert.Equal(t, time.Hour, config.OIDC.IDTokenLifespan)
|
||||||
|
assert.Equal(t, time.Minute*90, config.OIDC.RefreshTokenLifespan)
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,7 +63,6 @@ const msMaximumRandomDelay = int64(85)
|
||||||
|
|
||||||
// OIDC constants.
|
// OIDC constants.
|
||||||
const (
|
const (
|
||||||
oidcWellKnownPath = "/.well-known/openid-configuration"
|
|
||||||
oidcJWKsPath = "/api/oidc/jwks"
|
oidcJWKsPath = "/api/oidc/jwks"
|
||||||
oidcAuthorizePath = "/api/oidc/authorize"
|
oidcAuthorizePath = "/api/oidc/authorize"
|
||||||
oidcTokenPath = "/api/oidc/token" //nolint:gosec // This is not a hard coded credential, it's a path.
|
oidcTokenPath = "/api/oidc/token" //nolint:gosec // This is not a hard coded credential, it's a path.
|
||||||
|
@ -78,12 +77,3 @@ const (
|
||||||
accept = "accept"
|
accept = "accept"
|
||||||
reject = "reject"
|
reject = "reject"
|
||||||
)
|
)
|
||||||
|
|
||||||
var scopeDescriptions = map[string]string{
|
|
||||||
"openid": "Use OpenID to verify your identity",
|
|
||||||
"email": "Access your email addresses",
|
|
||||||
"profile": "Access your username",
|
|
||||||
"groups": "Access your group membership",
|
|
||||||
}
|
|
||||||
|
|
||||||
var audienceDescriptions = map[string]string{}
|
|
||||||
|
|
|
@ -7,7 +7,6 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/authelia/authelia/internal/authentication"
|
|
||||||
"github.com/authelia/authelia/internal/middlewares"
|
"github.com/authelia/authelia/internal/middlewares"
|
||||||
"github.com/authelia/authelia/internal/regulation"
|
"github.com/authelia/authelia/internal/regulation"
|
||||||
"github.com/authelia/authelia/internal/session"
|
"github.com/authelia/authelia/internal/session"
|
||||||
|
@ -168,22 +167,13 @@ func FirstFactorPost(msInitialDelay time.Duration, delayEnabled bool) middleware
|
||||||
|
|
||||||
ctx.Logger.Tracef("Details for user %s => groups: %s, emails %s", bodyJSON.Username, userDetails.Groups, userDetails.Emails)
|
ctx.Logger.Tracef("Details for user %s => groups: %s, emails %s", bodyJSON.Username, userDetails.Groups, userDetails.Emails)
|
||||||
|
|
||||||
// And set those information in the new session.
|
userSession.SetOneFactor(ctx.Clock.Now(), userDetails, keepMeLoggedIn)
|
||||||
userSession.Username = userDetails.Username
|
|
||||||
userSession.DisplayName = userDetails.DisplayName
|
|
||||||
userSession.Groups = userDetails.Groups
|
|
||||||
userSession.Emails = userDetails.Emails
|
|
||||||
userSession.AuthenticationLevel = authentication.OneFactor
|
|
||||||
userSession.LastActivity = time.Now().Unix()
|
|
||||||
userSession.KeepMeLoggedIn = keepMeLoggedIn
|
|
||||||
refresh, refreshInterval := getProfileRefreshSettings(ctx.Configuration.AuthenticationBackend)
|
|
||||||
|
|
||||||
if refresh {
|
if refresh, refreshInterval := getProfileRefreshSettings(ctx.Configuration.AuthenticationBackend); refresh {
|
||||||
userSession.RefreshTTL = ctx.Clock.Now().Add(refreshInterval)
|
userSession.RefreshTTL = ctx.Clock.Now().Add(refreshInterval)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = ctx.SaveSession(userSession)
|
err = ctx.SaveSession(userSession)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to save session of user %s", bodyJSON.Username), authenticationFailedMessage)
|
handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to save session of user %s", bodyJSON.Username), authenticationFailedMessage)
|
||||||
return
|
return
|
||||||
|
@ -192,7 +182,7 @@ func FirstFactorPost(msInitialDelay time.Duration, delayEnabled bool) middleware
|
||||||
successful = true
|
successful = true
|
||||||
|
|
||||||
if userSession.OIDCWorkflowSession != nil {
|
if userSession.OIDCWorkflowSession != nil {
|
||||||
HandleOIDCWorkflowResponse(ctx)
|
handleOIDCWorkflowResponse(ctx)
|
||||||
} else {
|
} else {
|
||||||
Handle1FAResponse(ctx, bodyJSON.TargetURL, bodyJSON.RequestMethod, userSession.Username, userSession.Groups)
|
Handle1FAResponse(ctx, bodyJSON.TargetURL, bodyJSON.RequestMethod, userSession.Username, userSession.Groups)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,8 +4,11 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/ory/fosite"
|
"github.com/ory/fosite"
|
||||||
|
"github.com/ory/fosite/handler/openid"
|
||||||
|
"github.com/ory/fosite/token/jwt"
|
||||||
|
|
||||||
"github.com/authelia/authelia/internal/logging"
|
"github.com/authelia/authelia/internal/logging"
|
||||||
"github.com/authelia/authelia/internal/middlewares"
|
"github.com/authelia/authelia/internal/middlewares"
|
||||||
|
@ -13,6 +16,7 @@ import (
|
||||||
"github.com/authelia/authelia/internal/session"
|
"github.com/authelia/authelia/internal/session"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
//nolint: gocyclo // TODO: Consider refactoring time permitting.
|
||||||
func oidcAuthorize(ctx *middlewares.AutheliaCtx, rw http.ResponseWriter, r *http.Request) {
|
func oidcAuthorize(ctx *middlewares.AutheliaCtx, rw http.ResponseWriter, r *http.Request) {
|
||||||
ar, err := ctx.Providers.OpenIDConnect.Fosite.NewAuthorizeRequest(ctx, r)
|
ar, err := ctx.Providers.OpenIDConnect.Fosite.NewAuthorizeRequest(ctx, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -46,14 +50,34 @@ func oidcAuthorize(ctx *middlewares.AutheliaCtx, rw http.ResponseWriter, r *http
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extraClaims := map[string]interface{}{}
|
||||||
|
|
||||||
for _, scope := range requestedScopes {
|
for _, scope := range requestedScopes {
|
||||||
ar.GrantScope(scope)
|
ar.GrantScope(scope)
|
||||||
|
|
||||||
|
switch scope {
|
||||||
|
case "groups":
|
||||||
|
extraClaims["groups"] = userSession.Groups
|
||||||
|
case "profile":
|
||||||
|
extraClaims["name"] = userSession.DisplayName
|
||||||
|
case "email":
|
||||||
|
if len(userSession.Emails) != 0 {
|
||||||
|
extraClaims["email"] = userSession.Emails[0]
|
||||||
|
if len(userSession.Emails) > 1 {
|
||||||
|
extraClaims["alt_emails"] = userSession.Emails[1:]
|
||||||
|
}
|
||||||
|
// TODO (james-d-elliott): actually verify emails and record that information.
|
||||||
|
extraClaims["email_verified"] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, a := range requestedAudience {
|
for _, a := range requestedAudience {
|
||||||
ar.GrantAudience(a)
|
ar.GrantAudience(a)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
workflowCreated := time.Unix(userSession.OIDCWorkflowSession.CreatedTimestamp, 0)
|
||||||
|
|
||||||
userSession.OIDCWorkflowSession = nil
|
userSession.OIDCWorkflowSession = nil
|
||||||
if err := ctx.SaveSession(userSession); err != nil {
|
if err := ctx.SaveSession(userSession); err != nil {
|
||||||
ctx.Logger.Errorf("%v", err)
|
ctx.Logger.Errorf("%v", err)
|
||||||
|
@ -62,15 +86,41 @@ func oidcAuthorize(ctx *middlewares.AutheliaCtx, rw http.ResponseWriter, r *http
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
oauthSession, err := newOIDCSession(ctx, ar)
|
issuer, err := ctx.ForwardedProtoHost()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Logger.Errorf("Error occurred in NewOIDCSession: %+v", err)
|
ctx.Logger.Errorf("Error occurred obtaining issuer: %+v", err)
|
||||||
ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, ar, err)
|
ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, ar, err)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
response, err := ctx.Providers.OpenIDConnect.Fosite.NewAuthorizeResponse(ctx, ar, oauthSession)
|
authTime, err := userSession.AuthenticatedTime(client.Policy)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Logger.Errorf("Error occurred obtaining authentication timestamp: %+v", err)
|
||||||
|
ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, ar, err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := ctx.Providers.OpenIDConnect.Fosite.NewAuthorizeResponse(ctx, ar, &oidc.OpenIDSession{
|
||||||
|
DefaultSession: &openid.DefaultSession{
|
||||||
|
Claims: &jwt.IDTokenClaims{
|
||||||
|
Subject: userSession.Username,
|
||||||
|
Issuer: issuer,
|
||||||
|
AuthTime: authTime,
|
||||||
|
RequestedAt: workflowCreated,
|
||||||
|
IssuedAt: time.Now(),
|
||||||
|
Nonce: ar.GetRequestForm().Get("nonce"),
|
||||||
|
Audience: []string{ar.GetClient().GetID()},
|
||||||
|
Extra: extraClaims,
|
||||||
|
},
|
||||||
|
Headers: &jwt.Headers{Extra: map[string]interface{}{
|
||||||
|
"kid": ctx.Providers.OpenIDConnect.KeyManager.GetActiveKeyID(),
|
||||||
|
}},
|
||||||
|
Subject: userSession.Username,
|
||||||
|
},
|
||||||
|
ClientID: clientID,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Logger.Errorf("Error occurred in NewAuthorizeResponse: %+v", err)
|
ctx.Logger.Errorf("Error occurred in NewAuthorizeResponse: %+v", err)
|
||||||
ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, ar, err)
|
ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, ar, err)
|
||||||
|
@ -98,13 +148,15 @@ func oidcAuthorizeHandleAuthorizationOrConsentInsufficient(
|
||||||
ctx.Logger.Debugf("User %s must consent with scopes %s",
|
ctx.Logger.Debugf("User %s must consent with scopes %s",
|
||||||
userSession.Username, strings.Join(ar.GetRequestedScopes(), ", "))
|
userSession.Username, strings.Join(ar.GetRequestedScopes(), ", "))
|
||||||
|
|
||||||
userSession.OIDCWorkflowSession = new(session.OIDCWorkflowSession)
|
userSession.OIDCWorkflowSession = &session.OIDCWorkflowSession{
|
||||||
userSession.OIDCWorkflowSession.ClientID = client.ID
|
ClientID: client.ID,
|
||||||
userSession.OIDCWorkflowSession.RequestedScopes = ar.GetRequestedScopes()
|
RequestedScopes: ar.GetRequestedScopes(),
|
||||||
userSession.OIDCWorkflowSession.RequestedAudience = ar.GetRequestedAudience()
|
RequestedAudience: ar.GetRequestedAudience(),
|
||||||
userSession.OIDCWorkflowSession.AuthURI = redirectURL
|
AuthURI: redirectURL,
|
||||||
userSession.OIDCWorkflowSession.TargetURI = ar.GetRedirectURI().String()
|
TargetURI: ar.GetRedirectURI().String(),
|
||||||
userSession.OIDCWorkflowSession.RequiredAuthorizationLevel = client.Policy
|
RequiredAuthorizationLevel: client.Policy,
|
||||||
|
CreatedTimestamp: time.Now().Unix(),
|
||||||
|
}
|
||||||
|
|
||||||
if err := ctx.SaveSession(userSession); err != nil {
|
if err := ctx.SaveSession(userSession); err != nil {
|
||||||
ctx.Logger.Errorf("%v", err)
|
ctx.Logger.Errorf("%v", err)
|
||||||
|
|
|
@ -34,13 +34,7 @@ func oidcConsent(ctx *middlewares.AutheliaCtx) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var body ConsentGetResponseBody
|
if err := ctx.SetJSONBody(client.GetConsentResponseBody(userSession.OIDCWorkflowSession)); err != nil {
|
||||||
body.Scopes = scopeNamesToScopes(userSession.OIDCWorkflowSession.RequestedScopes)
|
|
||||||
body.Audience = audienceNamesToAudience(userSession.OIDCWorkflowSession.RequestedAudience)
|
|
||||||
body.ClientID = client.ID
|
|
||||||
body.ClientDescription = client.Description
|
|
||||||
|
|
||||||
if err := ctx.SetJSONBody(body); err != nil {
|
|
||||||
ctx.Error(fmt.Errorf("Unable to set JSON body: %v", err), "Operation failed")
|
ctx.Error(fmt.Errorf("Unable to set JSON body: %v", err), "Operation failed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,14 +7,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func oidcIntrospect(ctx *middlewares.AutheliaCtx, rw http.ResponseWriter, req *http.Request) {
|
func oidcIntrospect(ctx *middlewares.AutheliaCtx, rw http.ResponseWriter, req *http.Request) {
|
||||||
oidcSession, err := newDefaultOIDCSession(ctx)
|
oidcSession := newOpenIDSession("")
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
ctx.Logger.Errorf("Error occurred in NewDefaultOIDCSession: %+v", err)
|
|
||||||
ctx.Providers.OpenIDConnect.Fosite.WriteIntrospectionError(rw, err)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ir, err := ctx.Providers.OpenIDConnect.Fosite.NewIntrospectionRequest(ctx, req, oidcSession)
|
ir, err := ctx.Providers.OpenIDConnect.Fosite.NewIntrospectionRequest(ctx, req, oidcSession)
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ import (
|
||||||
func oidcJWKs(ctx *middlewares.AutheliaCtx) {
|
func oidcJWKs(ctx *middlewares.AutheliaCtx) {
|
||||||
ctx.SetContentType("application/json")
|
ctx.SetContentType("application/json")
|
||||||
|
|
||||||
if err := json.NewEncoder(ctx).Encode(ctx.Providers.OpenIDConnect.GetKeySet()); err != nil {
|
if err := json.NewEncoder(ctx).Encode(ctx.Providers.OpenIDConnect.KeyManager.GetKeySet()); err != nil {
|
||||||
ctx.Error(err, "failed to serve jwk set")
|
ctx.Error(err, "failed to serve jwk set")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,13 +9,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func oidcToken(ctx *middlewares.AutheliaCtx, rw http.ResponseWriter, req *http.Request) {
|
func oidcToken(ctx *middlewares.AutheliaCtx, rw http.ResponseWriter, req *http.Request) {
|
||||||
oidcSession, err := newDefaultOIDCSession(ctx)
|
oidcSession := newOpenIDSession("")
|
||||||
if err != nil {
|
|
||||||
ctx.Logger.Errorf("Error occurred in NewDefaultOIDCSession: %+v", err)
|
|
||||||
ctx.Providers.OpenIDConnect.Fosite.WriteAccessError(rw, nil, err)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
accessRequest, accessReqErr := ctx.Providers.OpenIDConnect.Fosite.NewAccessRequest(ctx, req, oidcSession)
|
accessRequest, accessReqErr := ctx.Providers.OpenIDConnect.Fosite.NewAccessRequest(ctx, req, oidcSession)
|
||||||
if accessReqErr != nil {
|
if accessReqErr != nil {
|
||||||
|
|
|
@ -7,11 +7,11 @@ import (
|
||||||
"github.com/valyala/fasthttp"
|
"github.com/valyala/fasthttp"
|
||||||
|
|
||||||
"github.com/authelia/authelia/internal/middlewares"
|
"github.com/authelia/authelia/internal/middlewares"
|
||||||
|
"github.com/authelia/authelia/internal/oidc"
|
||||||
)
|
)
|
||||||
|
|
||||||
func oidcWellKnown(ctx *middlewares.AutheliaCtx) {
|
func oidcWellKnown(ctx *middlewares.AutheliaCtx) {
|
||||||
var configuration WellKnownConfigurationJSON
|
// TODO (james-d-elliott): append the server.path here for path based installs. Also check other instances in OIDC.
|
||||||
|
|
||||||
issuer, err := ctx.ForwardedProtoHost()
|
issuer, err := ctx.ForwardedProtoHost()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Logger.Errorf("Error occurred in ForwardedProtoHost: %+v", err)
|
ctx.Logger.Errorf("Error occurred in ForwardedProtoHost: %+v", err)
|
||||||
|
@ -20,22 +20,42 @@ func oidcWellKnown(ctx *middlewares.AutheliaCtx) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
configuration.Issuer = issuer
|
wellKnown := oidc.WellKnownConfiguration{
|
||||||
configuration.AuthURL = fmt.Sprintf("%s%s", issuer, oidcAuthorizePath)
|
Issuer: issuer,
|
||||||
configuration.TokenURL = fmt.Sprintf("%s%s", issuer, oidcTokenPath)
|
JWKSURI: fmt.Sprintf("%s%s", issuer, oidcJWKsPath),
|
||||||
configuration.RevocationEndpoint = fmt.Sprintf("%s%s", issuer, oidcRevokePath)
|
|
||||||
configuration.JWKSURL = fmt.Sprintf("%s%s", issuer, oidcJWKsPath)
|
AuthorizationEndpoint: fmt.Sprintf("%s%s", issuer, oidcAuthorizePath),
|
||||||
configuration.Algorithms = []string{"RS256"}
|
TokenEndpoint: fmt.Sprintf("%s%s", issuer, oidcTokenPath),
|
||||||
configuration.ScopesSupported = []string{
|
RevocationEndpoint: fmt.Sprintf("%s%s", issuer, oidcRevokePath),
|
||||||
|
|
||||||
|
Algorithms: []string{"RS256"},
|
||||||
|
|
||||||
|
SubjectTypesSupported: []string{
|
||||||
|
"public",
|
||||||
|
},
|
||||||
|
ResponseTypesSupported: []string{
|
||||||
|
"code",
|
||||||
|
"token",
|
||||||
|
"id_token",
|
||||||
|
"code token",
|
||||||
|
"code id_token",
|
||||||
|
"token id_token",
|
||||||
|
"code token id_token",
|
||||||
|
"none",
|
||||||
|
},
|
||||||
|
ResponseModesSupported: []string{
|
||||||
|
"form_post",
|
||||||
|
"query",
|
||||||
|
"fragment",
|
||||||
|
},
|
||||||
|
ScopesSupported: []string{
|
||||||
"openid",
|
"openid",
|
||||||
|
"offline_access",
|
||||||
"profile",
|
"profile",
|
||||||
"groups",
|
"groups",
|
||||||
"email",
|
"email",
|
||||||
// Determine if this is really mandatory knowing the RP can request for a refresh token through the authorize
|
},
|
||||||
// endpoint anyway.
|
ClaimsSupported: []string{
|
||||||
"offline_access",
|
|
||||||
}
|
|
||||||
configuration.ClaimsSupported = []string{
|
|
||||||
"aud",
|
"aud",
|
||||||
"exp",
|
"exp",
|
||||||
"iat",
|
"iat",
|
||||||
|
@ -47,26 +67,21 @@ func oidcWellKnown(ctx *middlewares.AutheliaCtx) {
|
||||||
"nonce",
|
"nonce",
|
||||||
"email",
|
"email",
|
||||||
"email_verified",
|
"email_verified",
|
||||||
|
"alt_emails",
|
||||||
"groups",
|
"groups",
|
||||||
"name",
|
"name",
|
||||||
}
|
},
|
||||||
configuration.SubjectTypesSupported = []string{
|
|
||||||
"public",
|
RequestURIParameterSupported: false,
|
||||||
}
|
BackChannelLogoutSupported: false,
|
||||||
configuration.ResponseTypesSupported = []string{
|
FrontChannelLogoutSupported: false,
|
||||||
"code",
|
BackChannelLogoutSessionSupported: false,
|
||||||
"token",
|
FrontChannelLogoutSessionSupported: false,
|
||||||
"id_token",
|
|
||||||
"code token",
|
|
||||||
"code id_token",
|
|
||||||
"token id_token",
|
|
||||||
"code token id_token",
|
|
||||||
"none",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.SetContentType("application/json")
|
ctx.SetContentType("application/json")
|
||||||
|
|
||||||
if err := json.NewEncoder(ctx).Encode(configuration); err != nil {
|
if err := json.NewEncoder(ctx).Encode(wellKnown); err != nil {
|
||||||
ctx.Logger.Errorf("Error occurred in json Encode: %+v", err)
|
ctx.Logger.Errorf("Error occurred in json Encode: %+v", err)
|
||||||
// TODO: Determine if this is the appropriate error code here.
|
// TODO: Determine if this is the appropriate error code here.
|
||||||
ctx.Response.SetStatusCode(fasthttp.StatusInternalServerError)
|
ctx.Response.SetStatusCode(fasthttp.StatusInternalServerError)
|
||||||
|
|
|
@ -4,7 +4,6 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
"github.com/authelia/authelia/internal/authentication"
|
|
||||||
"github.com/authelia/authelia/internal/duo"
|
"github.com/authelia/authelia/internal/duo"
|
||||||
"github.com/authelia/authelia/internal/middlewares"
|
"github.com/authelia/authelia/internal/middlewares"
|
||||||
)
|
)
|
||||||
|
@ -65,16 +64,16 @@ func SecondFactorDuoPost(duoAPI duo.API) middlewares.RequestHandler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
userSession.AuthenticationLevel = authentication.TwoFactor
|
userSession.SetTwoFactor(ctx.Clock.Now())
|
||||||
err = ctx.SaveSession(userSession)
|
|
||||||
|
|
||||||
|
err = ctx.SaveSession(userSession)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to update authentication level with Duo: %s", err), mfaValidationFailedMessage)
|
handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to update authentication level with Duo: %s", err), mfaValidationFailedMessage)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if userSession.OIDCWorkflowSession != nil {
|
if userSession.OIDCWorkflowSession != nil {
|
||||||
HandleOIDCWorkflowResponse(ctx)
|
handleOIDCWorkflowResponse(ctx)
|
||||||
} else {
|
} else {
|
||||||
Handle2FAResponse(ctx, requestBody.TargetURL)
|
Handle2FAResponse(ctx, requestBody.TargetURL)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@ package handlers
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/authelia/authelia/internal/authentication"
|
|
||||||
"github.com/authelia/authelia/internal/middlewares"
|
"github.com/authelia/authelia/internal/middlewares"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -44,16 +43,16 @@ func SecondFactorTOTPPost(totpVerifier TOTPVerifier) middlewares.RequestHandler
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
userSession.AuthenticationLevel = authentication.TwoFactor
|
userSession.SetTwoFactor(ctx.Clock.Now())
|
||||||
err = ctx.SaveSession(userSession)
|
|
||||||
|
|
||||||
|
err = ctx.SaveSession(userSession)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to update the authentication level with TOTP: %s", err), mfaValidationFailedMessage)
|
handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to update the authentication level with TOTP: %s", err), mfaValidationFailedMessage)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if userSession.OIDCWorkflowSession != nil {
|
if userSession.OIDCWorkflowSession != nil {
|
||||||
HandleOIDCWorkflowResponse(ctx)
|
handleOIDCWorkflowResponse(ctx)
|
||||||
} else {
|
} else {
|
||||||
Handle2FAResponse(ctx, requestBody.TargetURL)
|
Handle2FAResponse(ctx, requestBody.TargetURL)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@ package handlers
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/authelia/authelia/internal/authentication"
|
|
||||||
"github.com/authelia/authelia/internal/middlewares"
|
"github.com/authelia/authelia/internal/middlewares"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -47,16 +46,16 @@ func SecondFactorU2FSignPost(u2fVerifier U2FVerifier) middlewares.RequestHandler
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
userSession.AuthenticationLevel = authentication.TwoFactor
|
userSession.SetTwoFactor(ctx.Clock.Now())
|
||||||
err = ctx.SaveSession(userSession)
|
|
||||||
|
|
||||||
|
err = ctx.SaveSession(userSession)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to update authentication level with U2F: %s", err), mfaValidationFailedMessage)
|
handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to update authentication level with U2F: %s", err), mfaValidationFailedMessage)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if userSession.OIDCWorkflowSession != nil {
|
if userSession.OIDCWorkflowSession != nil {
|
||||||
HandleOIDCWorkflowResponse(ctx)
|
handleOIDCWorkflowResponse(ctx)
|
||||||
} else {
|
} else {
|
||||||
Handle2FAResponse(ctx, requestBody.TargetURL)
|
Handle2FAResponse(ctx, requestBody.TargetURL)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,10 @@
|
||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ory/fosite"
|
|
||||||
"github.com/ory/fosite/handler/openid"
|
"github.com/ory/fosite/handler/openid"
|
||||||
"github.com/ory/fosite/token/jwt"
|
"github.com/ory/fosite/token/jwt"
|
||||||
|
|
||||||
"github.com/authelia/authelia/internal/middlewares"
|
"github.com/authelia/authelia/internal/oidc"
|
||||||
"github.com/authelia/authelia/internal/session"
|
"github.com/authelia/authelia/internal/session"
|
||||||
"github.com/authelia/authelia/internal/utils"
|
"github.com/authelia/authelia/internal/utils"
|
||||||
)
|
)
|
||||||
|
@ -23,87 +20,13 @@ func isConsentMissing(workflow *session.OIDCWorkflowSession, requestedScopes, re
|
||||||
len(requestedAudience) > 0 && utils.IsStringSlicesDifferentFold(requestedAudience, workflow.GrantedAudience)
|
len(requestedAudience) > 0 && utils.IsStringSlicesDifferentFold(requestedAudience, workflow.GrantedAudience)
|
||||||
}
|
}
|
||||||
|
|
||||||
func scopeNamesToScopes(scopeSlice []string) (scopes []Scope) {
|
func newOpenIDSession(subject string) *oidc.OpenIDSession {
|
||||||
for _, name := range scopeSlice {
|
return &oidc.OpenIDSession{
|
||||||
if val, ok := scopeDescriptions[name]; ok {
|
DefaultSession: &openid.DefaultSession{
|
||||||
scopes = append(scopes, Scope{name, val})
|
Claims: new(jwt.IDTokenClaims),
|
||||||
} else {
|
Headers: new(jwt.Headers),
|
||||||
scopes = append(scopes, Scope{name, name})
|
Subject: subject,
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return scopes
|
|
||||||
}
|
|
||||||
|
|
||||||
func audienceNamesToAudience(scopeSlice []string) (audience []Audience) {
|
|
||||||
for _, name := range scopeSlice {
|
|
||||||
if val, ok := audienceDescriptions[name]; ok {
|
|
||||||
audience = append(audience, Audience{name, val})
|
|
||||||
} else {
|
|
||||||
audience = append(audience, Audience{name, name})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return audience
|
|
||||||
}
|
|
||||||
|
|
||||||
func newOIDCSession(ctx *middlewares.AutheliaCtx, ar fosite.AuthorizeRequester) (session *openid.DefaultSession, err error) {
|
|
||||||
userSession := ctx.GetSession()
|
|
||||||
|
|
||||||
scopes := ar.GetGrantedScopes()
|
|
||||||
|
|
||||||
extra := map[string]interface{}{}
|
|
||||||
|
|
||||||
if len(userSession.Emails) != 0 && scopes.Has("email") {
|
|
||||||
extra["email"] = userSession.Emails[0]
|
|
||||||
extra["email_verified"] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if scopes.Has("groups") {
|
|
||||||
extra["groups"] = userSession.Groups
|
|
||||||
}
|
|
||||||
|
|
||||||
if scopes.Has("profile") {
|
|
||||||
extra["name"] = userSession.DisplayName
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
TODO: Adjust auth backends to return more profile information.
|
|
||||||
It's probably ideal to adjust the auth providers at this time to not store 'extra' information in the session
|
|
||||||
storage, and instead create a memory only storage for them.
|
|
||||||
This is a simple design, have a map with a key of username, and a struct with the relevant information.
|
|
||||||
*/
|
|
||||||
|
|
||||||
oidcSession, err := newDefaultOIDCSession(ctx)
|
|
||||||
if oidcSession == nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
oidcSession.Claims.Extra = extra
|
|
||||||
oidcSession.Claims.Subject = userSession.Username
|
|
||||||
oidcSession.Claims.Audience = ar.GetGrantedAudience()
|
|
||||||
|
|
||||||
return oidcSession, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func newDefaultOIDCSession(ctx *middlewares.AutheliaCtx) (session *openid.DefaultSession, err error) {
|
|
||||||
issuer, err := ctx.ForwardedProtoHost()
|
|
||||||
|
|
||||||
return &openid.DefaultSession{
|
|
||||||
Claims: &jwt.IDTokenClaims{
|
|
||||||
Issuer: issuer,
|
|
||||||
// TODO(c.michaud): make this configurable
|
|
||||||
ExpiresAt: time.Now().Add(time.Hour * 6),
|
|
||||||
IssuedAt: time.Now(),
|
|
||||||
RequestedAt: time.Now(),
|
|
||||||
AuthTime: time.Now(),
|
|
||||||
Extra: make(map[string]interface{}),
|
|
||||||
},
|
},
|
||||||
Headers: &jwt.Headers{
|
Extra: map[string]interface{}{},
|
||||||
Extra: map[string]interface{}{
|
}
|
||||||
// TODO: Obtain this from the active keys when we implement key rotation.
|
|
||||||
"kid": "main-key",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, err
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import (
|
||||||
// RegisterOIDC registers the handlers with the fasthttp *router.Router. TODO: Add paths for UserInfo, Flush, Logout.
|
// RegisterOIDC registers the handlers with the fasthttp *router.Router. TODO: Add paths for UserInfo, Flush, Logout.
|
||||||
func RegisterOIDC(router *router.Router, middleware middlewares.RequestHandlerBridge) {
|
func RegisterOIDC(router *router.Router, middleware middlewares.RequestHandlerBridge) {
|
||||||
// TODO: Add OPTIONS handler.
|
// TODO: Add OPTIONS handler.
|
||||||
router.GET(oidcWellKnownPath, middleware(oidcWellKnown))
|
router.GET("/.well-known/openid-configuration", middleware(oidcWellKnown))
|
||||||
|
|
||||||
router.GET(oidcConsentPath, middleware(oidcConsent))
|
router.GET(oidcConsentPath, middleware(oidcConsent))
|
||||||
|
|
|
@ -11,8 +11,8 @@ import (
|
||||||
"github.com/authelia/authelia/internal/utils"
|
"github.com/authelia/authelia/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HandleOIDCWorkflowResponse handle the redirection upon authentication in the OIDC workflow.
|
// handleOIDCWorkflowResponse handle the redirection upon authentication in the OIDC workflow.
|
||||||
func HandleOIDCWorkflowResponse(ctx *middlewares.AutheliaCtx) {
|
func handleOIDCWorkflowResponse(ctx *middlewares.AutheliaCtx) {
|
||||||
userSession := ctx.GetSession()
|
userSession := ctx.GetSession()
|
||||||
|
|
||||||
if !authorization.IsAuthLevelSufficient(userSession.AuthenticationLevel, userSession.OIDCWorkflowSession.RequiredAuthorizationLevel) {
|
if !authorization.IsAuthLevelSufficient(userSession.AuthenticationLevel, userSession.OIDCWorkflowSession.RequiredAuthorizationLevel) {
|
||||||
|
|
|
@ -1,9 +1,5 @@
|
||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/golang-jwt/jwt"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ConsentPostRequestBody schema of the request body of the consent POST endpoint.
|
// ConsentPostRequestBody schema of the request body of the consent POST endpoint.
|
||||||
type ConsentPostRequestBody struct {
|
type ConsentPostRequestBody struct {
|
||||||
ClientID string `json:"client_id"`
|
ClientID string `json:"client_id"`
|
||||||
|
@ -14,50 +10,3 @@ type ConsentPostRequestBody struct {
|
||||||
type ConsentPostResponseBody struct {
|
type ConsentPostResponseBody struct {
|
||||||
RedirectURI string `json:"redirect_uri"`
|
RedirectURI string `json:"redirect_uri"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConsentGetResponseBody schema of the response body of the consent GET endpoint.
|
|
||||||
type ConsentGetResponseBody struct {
|
|
||||||
ClientID string `json:"client_id"`
|
|
||||||
ClientDescription string `json:"client_description"`
|
|
||||||
Scopes []Scope `json:"scopes"`
|
|
||||||
Audience []Audience `json:"audience"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scope represents the scope information.
|
|
||||||
type Scope struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Audience represents the audience information.
|
|
||||||
type Audience struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// OIDCClaims represents a set of OIDC claims.
|
|
||||||
type OIDCClaims struct {
|
|
||||||
jwt.StandardClaims
|
|
||||||
|
|
||||||
Workflow string `json:"workflow"`
|
|
||||||
Username string `json:"username,omitempty"`
|
|
||||||
RequestedScopes []string `json:"requested_scopes,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// WellKnownConfigurationJSON is the OIDC well known config struct.
|
|
||||||
type WellKnownConfigurationJSON struct {
|
|
||||||
Issuer string `json:"issuer"`
|
|
||||||
AuthURL string `json:"authorization_endpoint"`
|
|
||||||
TokenURL string `json:"token_endpoint"`
|
|
||||||
RevocationEndpoint string `json:"revocation_endpoint"`
|
|
||||||
JWKSURL string `json:"jwks_uri"`
|
|
||||||
Algorithms []string `json:"id_token_signing_alg_values_supported"`
|
|
||||||
SubjectTypesSupported []string `json:"subject_types_supported"`
|
|
||||||
ResponseTypesSupported []string `json:"response_types_supported"`
|
|
||||||
ScopesSupported []string `json:"scopes_supported"`
|
|
||||||
ClaimsSupported []string `json:"claims_supported"`
|
|
||||||
BackChannelLogoutSupported bool `json:"backchannel_logout_supported"`
|
|
||||||
BackChannelLogoutSessionSupported bool `json:"backchannel_logout_session_supported"`
|
|
||||||
FrontChannelLogoutSupported bool `json:"frontchannel_logout_supported"`
|
|
||||||
FrontChannelLogoutSessionSupported bool `json:"frontchannel_logout_session_supported"`
|
|
||||||
}
|
|
||||||
|
|
|
@ -5,20 +5,32 @@ import (
|
||||||
|
|
||||||
"github.com/authelia/authelia/internal/authentication"
|
"github.com/authelia/authelia/internal/authentication"
|
||||||
"github.com/authelia/authelia/internal/authorization"
|
"github.com/authelia/authelia/internal/authorization"
|
||||||
|
"github.com/authelia/authelia/internal/configuration/schema"
|
||||||
|
"github.com/authelia/authelia/internal/session"
|
||||||
)
|
)
|
||||||
|
|
||||||
// InternalClient represents the client internally.
|
// NewClient creates a new InternalClient.
|
||||||
type InternalClient struct {
|
func NewClient(config schema.OpenIDConnectClientConfiguration) (client *InternalClient) {
|
||||||
ID string `json:"id"`
|
client = &InternalClient{
|
||||||
Description string `json:"-"`
|
ID: config.ID,
|
||||||
Secret []byte `json:"client_secret,omitempty"`
|
Description: config.Description,
|
||||||
RedirectURIs []string `json:"redirect_uris"`
|
Policy: authorization.PolicyToLevel(config.Policy),
|
||||||
GrantTypes []string `json:"grant_types"`
|
Secret: []byte(config.Secret),
|
||||||
ResponseTypes []string `json:"response_types"`
|
RedirectURIs: config.RedirectURIs,
|
||||||
Scopes []string `json:"scopes"`
|
GrantTypes: config.GrantTypes,
|
||||||
Audience []string `json:"audience"`
|
ResponseTypes: config.ResponseTypes,
|
||||||
Public bool `json:"public"`
|
Scopes: config.Scopes,
|
||||||
Policy authorization.Level `json:"-"`
|
|
||||||
|
ResponseModes: []fosite.ResponseModeType{
|
||||||
|
fosite.ResponseModeDefault,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, mode := range config.ResponseModes {
|
||||||
|
client.ResponseModes = append(client.ResponseModes, fosite.ResponseModeType(mode))
|
||||||
|
}
|
||||||
|
|
||||||
|
return client
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsAuthenticationLevelSufficient returns if the provided authentication.Level is sufficient for the client of the AutheliaClient.
|
// IsAuthenticationLevelSufficient returns if the provided authentication.Level is sufficient for the client of the AutheliaClient.
|
||||||
|
@ -31,6 +43,21 @@ func (c InternalClient) GetID() string {
|
||||||
return c.ID
|
return c.ID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetConsentResponseBody returns the proper consent response body for this session.OIDCWorkflowSession.
|
||||||
|
func (c InternalClient) GetConsentResponseBody(session *session.OIDCWorkflowSession) ConsentGetResponseBody {
|
||||||
|
body := ConsentGetResponseBody{
|
||||||
|
ClientID: c.ID,
|
||||||
|
ClientDescription: c.Description,
|
||||||
|
}
|
||||||
|
|
||||||
|
if session != nil {
|
||||||
|
body.Scopes = scopeNamesToScopes(session.RequestedScopes)
|
||||||
|
body.Audience = audienceNamesToAudience(session.RequestedAudience)
|
||||||
|
}
|
||||||
|
|
||||||
|
return body
|
||||||
|
}
|
||||||
|
|
||||||
// GetHashedSecret returns the Secret.
|
// GetHashedSecret returns the Secret.
|
||||||
func (c InternalClient) GetHashedSecret() []byte {
|
func (c InternalClient) GetHashedSecret() []byte {
|
||||||
return c.Secret
|
return c.Secret
|
||||||
|
@ -73,3 +100,10 @@ func (c InternalClient) IsPublic() bool {
|
||||||
func (c InternalClient) GetAudience() fosite.Arguments {
|
func (c InternalClient) GetAudience() fosite.Arguments {
|
||||||
return c.Audience
|
return c.Audience
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetResponseModes returns the valid response modes for this client.
|
||||||
|
//
|
||||||
|
// Implements the fosite.ResponseModeClient.
|
||||||
|
func (c InternalClient) GetResponseModes() []fosite.ResponseModeType {
|
||||||
|
return c.ResponseModes
|
||||||
|
}
|
||||||
|
|
|
@ -3,12 +3,46 @@ package oidc
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ory/fosite"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/authelia/authelia/internal/authentication"
|
"github.com/authelia/authelia/internal/authentication"
|
||||||
"github.com/authelia/authelia/internal/authorization"
|
"github.com/authelia/authelia/internal/authorization"
|
||||||
|
"github.com/authelia/authelia/internal/configuration/schema"
|
||||||
|
"github.com/authelia/authelia/internal/session"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestNewClient(t *testing.T) {
|
||||||
|
blankConfig := schema.OpenIDConnectClientConfiguration{}
|
||||||
|
blankClient := NewClient(blankConfig)
|
||||||
|
assert.Equal(t, "", blankClient.ID)
|
||||||
|
assert.Equal(t, "", blankClient.Description)
|
||||||
|
assert.Equal(t, "", blankClient.Description)
|
||||||
|
require.Len(t, blankClient.ResponseModes, 1)
|
||||||
|
assert.Equal(t, fosite.ResponseModeDefault, blankClient.ResponseModes[0])
|
||||||
|
|
||||||
|
exampleConfig := schema.OpenIDConnectClientConfiguration{
|
||||||
|
ID: "myapp",
|
||||||
|
Description: "My App",
|
||||||
|
Policy: "two_factor",
|
||||||
|
Secret: "abcdef",
|
||||||
|
RedirectURIs: []string{"https://google.com/callback"},
|
||||||
|
Scopes: schema.DefaultOpenIDConnectClientConfiguration.Scopes,
|
||||||
|
ResponseTypes: schema.DefaultOpenIDConnectClientConfiguration.ResponseTypes,
|
||||||
|
GrantTypes: schema.DefaultOpenIDConnectClientConfiguration.GrantTypes,
|
||||||
|
ResponseModes: schema.DefaultOpenIDConnectClientConfiguration.ResponseModes,
|
||||||
|
}
|
||||||
|
|
||||||
|
exampleClient := NewClient(exampleConfig)
|
||||||
|
assert.Equal(t, "myapp", exampleClient.ID)
|
||||||
|
require.Len(t, exampleClient.ResponseModes, 4)
|
||||||
|
assert.Equal(t, fosite.ResponseModeDefault, exampleClient.ResponseModes[0])
|
||||||
|
assert.Equal(t, fosite.ResponseModeFormPost, exampleClient.ResponseModes[1])
|
||||||
|
assert.Equal(t, fosite.ResponseModeQuery, exampleClient.ResponseModes[2])
|
||||||
|
assert.Equal(t, fosite.ResponseModeFragment, exampleClient.ResponseModes[3])
|
||||||
|
}
|
||||||
|
|
||||||
func TestIsAuthenticationLevelSufficient(t *testing.T) {
|
func TestIsAuthenticationLevelSufficient(t *testing.T) {
|
||||||
c := InternalClient{}
|
c := InternalClient{}
|
||||||
|
|
||||||
|
@ -32,3 +66,154 @@ func TestIsAuthenticationLevelSufficient(t *testing.T) {
|
||||||
assert.False(t, c.IsAuthenticationLevelSufficient(authentication.OneFactor))
|
assert.False(t, c.IsAuthenticationLevelSufficient(authentication.OneFactor))
|
||||||
assert.False(t, c.IsAuthenticationLevelSufficient(authentication.TwoFactor))
|
assert.False(t, c.IsAuthenticationLevelSufficient(authentication.TwoFactor))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestInternalClient_GetConsentResponseBody(t *testing.T) {
|
||||||
|
c := InternalClient{}
|
||||||
|
|
||||||
|
consentRequestBody := c.GetConsentResponseBody(nil)
|
||||||
|
assert.Equal(t, "", consentRequestBody.ClientID)
|
||||||
|
assert.Equal(t, "", consentRequestBody.ClientDescription)
|
||||||
|
assert.Equal(t, []Scope(nil), consentRequestBody.Scopes)
|
||||||
|
assert.Equal(t, []Audience(nil), consentRequestBody.Audience)
|
||||||
|
|
||||||
|
c.ID = "myclient"
|
||||||
|
c.Description = "My Client"
|
||||||
|
|
||||||
|
workflow := &session.OIDCWorkflowSession{
|
||||||
|
RequestedAudience: []string{"https://example.com"},
|
||||||
|
RequestedScopes: []string{"openid", "groups"},
|
||||||
|
}
|
||||||
|
expectedScopes := []Scope{
|
||||||
|
{"openid", "Use OpenID to verify your identity"},
|
||||||
|
{"groups", "Access your group membership"},
|
||||||
|
}
|
||||||
|
expectedAudiences := []Audience{
|
||||||
|
{"https://example.com", "https://example.com"},
|
||||||
|
}
|
||||||
|
|
||||||
|
consentRequestBody = c.GetConsentResponseBody(workflow)
|
||||||
|
assert.Equal(t, "myclient", consentRequestBody.ClientID)
|
||||||
|
assert.Equal(t, "My Client", consentRequestBody.ClientDescription)
|
||||||
|
assert.Equal(t, expectedScopes, consentRequestBody.Scopes)
|
||||||
|
assert.Equal(t, expectedAudiences, consentRequestBody.Audience)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInternalClient_GetAudience(t *testing.T) {
|
||||||
|
c := InternalClient{}
|
||||||
|
|
||||||
|
audience := c.GetAudience()
|
||||||
|
assert.Len(t, audience, 0)
|
||||||
|
|
||||||
|
c.Audience = []string{"https://example.com"}
|
||||||
|
|
||||||
|
audience = c.GetAudience()
|
||||||
|
require.Len(t, audience, 1)
|
||||||
|
assert.Equal(t, "https://example.com", audience[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInternalClient_GetScopes(t *testing.T) {
|
||||||
|
c := InternalClient{}
|
||||||
|
|
||||||
|
scopes := c.GetScopes()
|
||||||
|
assert.Len(t, scopes, 0)
|
||||||
|
|
||||||
|
c.Scopes = []string{"openid"}
|
||||||
|
|
||||||
|
scopes = c.GetScopes()
|
||||||
|
require.Len(t, scopes, 1)
|
||||||
|
assert.Equal(t, "openid", scopes[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInternalClient_GetGrantTypes(t *testing.T) {
|
||||||
|
c := InternalClient{}
|
||||||
|
|
||||||
|
grantTypes := c.GetGrantTypes()
|
||||||
|
require.Len(t, grantTypes, 1)
|
||||||
|
assert.Equal(t, "authorization_code", grantTypes[0])
|
||||||
|
|
||||||
|
c.GrantTypes = []string{"device_code"}
|
||||||
|
|
||||||
|
grantTypes = c.GetGrantTypes()
|
||||||
|
require.Len(t, grantTypes, 1)
|
||||||
|
assert.Equal(t, "device_code", grantTypes[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInternalClient_GetHashedSecret(t *testing.T) {
|
||||||
|
c := InternalClient{}
|
||||||
|
|
||||||
|
hashedSecret := c.GetHashedSecret()
|
||||||
|
assert.Equal(t, []byte(nil), hashedSecret)
|
||||||
|
|
||||||
|
c.Secret = []byte("a_bad_secret")
|
||||||
|
|
||||||
|
hashedSecret = c.GetHashedSecret()
|
||||||
|
assert.Equal(t, []byte("a_bad_secret"), hashedSecret)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInternalClient_GetID(t *testing.T) {
|
||||||
|
c := InternalClient{}
|
||||||
|
|
||||||
|
id := c.GetID()
|
||||||
|
assert.Equal(t, "", id)
|
||||||
|
|
||||||
|
c.ID = "myid"
|
||||||
|
|
||||||
|
id = c.GetID()
|
||||||
|
assert.Equal(t, "myid", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInternalClient_GetRedirectURIs(t *testing.T) {
|
||||||
|
c := InternalClient{}
|
||||||
|
|
||||||
|
redirectURIs := c.GetRedirectURIs()
|
||||||
|
require.Len(t, redirectURIs, 0)
|
||||||
|
|
||||||
|
c.RedirectURIs = []string{"https://example.com/oauth2/callback"}
|
||||||
|
|
||||||
|
redirectURIs = c.GetRedirectURIs()
|
||||||
|
require.Len(t, redirectURIs, 1)
|
||||||
|
assert.Equal(t, "https://example.com/oauth2/callback", redirectURIs[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInternalClient_GetResponseModes(t *testing.T) {
|
||||||
|
c := InternalClient{}
|
||||||
|
|
||||||
|
responseModes := c.GetResponseModes()
|
||||||
|
require.Len(t, responseModes, 0)
|
||||||
|
|
||||||
|
c.ResponseModes = []fosite.ResponseModeType{
|
||||||
|
fosite.ResponseModeDefault, fosite.ResponseModeFormPost,
|
||||||
|
fosite.ResponseModeQuery, fosite.ResponseModeFragment,
|
||||||
|
}
|
||||||
|
|
||||||
|
responseModes = c.GetResponseModes()
|
||||||
|
require.Len(t, responseModes, 4)
|
||||||
|
assert.Equal(t, fosite.ResponseModeDefault, responseModes[0])
|
||||||
|
assert.Equal(t, fosite.ResponseModeFormPost, responseModes[1])
|
||||||
|
assert.Equal(t, fosite.ResponseModeQuery, responseModes[2])
|
||||||
|
assert.Equal(t, fosite.ResponseModeFragment, responseModes[3])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInternalClient_GetResponseTypes(t *testing.T) {
|
||||||
|
c := InternalClient{}
|
||||||
|
|
||||||
|
responseTypes := c.GetResponseTypes()
|
||||||
|
require.Len(t, responseTypes, 1)
|
||||||
|
assert.Equal(t, "code", responseTypes[0])
|
||||||
|
|
||||||
|
c.ResponseTypes = []string{"code", "id_token"}
|
||||||
|
|
||||||
|
responseTypes = c.GetResponseTypes()
|
||||||
|
require.Len(t, responseTypes, 2)
|
||||||
|
assert.Equal(t, "code", responseTypes[0])
|
||||||
|
assert.Equal(t, "id_token", responseTypes[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInternalClient_IsPublic(t *testing.T) {
|
||||||
|
c := InternalClient{}
|
||||||
|
|
||||||
|
assert.False(t, c.IsPublic())
|
||||||
|
|
||||||
|
c.Public = true
|
||||||
|
assert.True(t, c.IsPublic())
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
package oidc
|
||||||
|
|
||||||
|
var scopeDescriptions = map[string]string{
|
||||||
|
"openid": "Use OpenID to verify your identity",
|
||||||
|
"email": "Access your email addresses",
|
||||||
|
"profile": "Access your display name",
|
||||||
|
"groups": "Access your group membership",
|
||||||
|
}
|
||||||
|
|
||||||
|
var audienceDescriptions = map[string]string{}
|
|
@ -5,12 +5,8 @@ import (
|
||||||
"crypto/subtle"
|
"crypto/subtle"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AutheliaHasher implements the fosite.Hasher interface without an actual hashing algo.
|
|
||||||
type AutheliaHasher struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compare compares the hash with the data and returns an error if they don't match.
|
// Compare compares the hash with the data and returns an error if they don't match.
|
||||||
func (h AutheliaHasher) Compare(ctx context.Context, hash, data []byte) (err error) {
|
func (h AutheliaHasher) Compare(_ context.Context, hash, data []byte) (err error) {
|
||||||
if subtle.ConstantTimeCompare(hash, data) == 0 {
|
if subtle.ConstantTimeCompare(hash, data) == 0 {
|
||||||
return errPasswordsDoNotMatch
|
return errPasswordsDoNotMatch
|
||||||
}
|
}
|
||||||
|
@ -19,6 +15,6 @@ func (h AutheliaHasher) Compare(ctx context.Context, hash, data []byte) (err err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hash creates a new hash from data.
|
// Hash creates a new hash from data.
|
||||||
func (h AutheliaHasher) Hash(ctx context.Context, data []byte) (hash []byte, err error) {
|
func (h AutheliaHasher) Hash(_ context.Context, data []byte) (hash []byte, err error) {
|
||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
package oidc
|
||||||
|
|
||||||
|
func scopeNamesToScopes(scopeSlice []string) (scopes []Scope) {
|
||||||
|
for _, name := range scopeSlice {
|
||||||
|
if val, ok := scopeDescriptions[name]; ok {
|
||||||
|
scopes = append(scopes, Scope{name, val})
|
||||||
|
} else {
|
||||||
|
scopes = append(scopes, Scope{name, name})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return scopes
|
||||||
|
}
|
||||||
|
|
||||||
|
func audienceNamesToAudience(scopeSlice []string) (audience []Audience) {
|
||||||
|
for _, name := range scopeSlice {
|
||||||
|
if val, ok := audienceDescriptions[name]; ok {
|
||||||
|
audience = append(audience, Audience{name, val})
|
||||||
|
} else {
|
||||||
|
audience = append(audience, Audience{name, name})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return audience
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
package oidc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestScopeNamesToScopes(t *testing.T) {
|
||||||
|
scopeNames := []string{"openid"}
|
||||||
|
|
||||||
|
scopes := scopeNamesToScopes(scopeNames)
|
||||||
|
assert.Equal(t, "openid", scopes[0].Name)
|
||||||
|
assert.Equal(t, "Use OpenID to verify your identity", scopes[0].Description)
|
||||||
|
|
||||||
|
scopeNames = []string{"groups"}
|
||||||
|
|
||||||
|
scopes = scopeNamesToScopes(scopeNames)
|
||||||
|
assert.Equal(t, "groups", scopes[0].Name)
|
||||||
|
assert.Equal(t, "Access your group membership", scopes[0].Description)
|
||||||
|
|
||||||
|
scopeNames = []string{"profile"}
|
||||||
|
|
||||||
|
scopes = scopeNamesToScopes(scopeNames)
|
||||||
|
assert.Equal(t, "profile", scopes[0].Name)
|
||||||
|
assert.Equal(t, "Access your display name", scopes[0].Description)
|
||||||
|
|
||||||
|
scopeNames = []string{"email"}
|
||||||
|
|
||||||
|
scopes = scopeNamesToScopes(scopeNames)
|
||||||
|
assert.Equal(t, "email", scopes[0].Name)
|
||||||
|
assert.Equal(t, "Access your email addresses", scopes[0].Description)
|
||||||
|
|
||||||
|
scopeNames = []string{"another"}
|
||||||
|
|
||||||
|
scopes = scopeNamesToScopes(scopeNames)
|
||||||
|
assert.Equal(t, "another", scopes[0].Name)
|
||||||
|
assert.Equal(t, "another", scopes[0].Description)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAudienceNamesToScopes(t *testing.T) {
|
||||||
|
audienceNames := []string{"audience", "another_aud"}
|
||||||
|
|
||||||
|
audiences := audienceNamesToAudience(audienceNames)
|
||||||
|
assert.Equal(t, "audience", audiences[0].Name)
|
||||||
|
assert.Equal(t, "audience", audiences[0].Description)
|
||||||
|
assert.Equal(t, "another_aud", audiences[1].Name)
|
||||||
|
assert.Equal(t, "another_aud", audiences[1].Description)
|
||||||
|
}
|
|
@ -0,0 +1,197 @@
|
||||||
|
package oidc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto"
|
||||||
|
"crypto/rsa"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ory/fosite/token/jwt"
|
||||||
|
"gopkg.in/square/go-jose.v2"
|
||||||
|
|
||||||
|
"github.com/authelia/authelia/internal/configuration/schema"
|
||||||
|
"github.com/authelia/authelia/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewKeyManagerWithConfiguration when provided a schema.OpenIDConnectConfiguration creates a new KeyManager and adds an
|
||||||
|
// initial key to the manager.
|
||||||
|
func NewKeyManagerWithConfiguration(configuration *schema.OpenIDConnectConfiguration) (manager *KeyManager, err error) {
|
||||||
|
manager = NewKeyManager()
|
||||||
|
|
||||||
|
_, _, err = manager.AddActivePrivateKeyData(configuration.IssuerPrivateKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return manager, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewKeyManager creates a new empty KeyManager.
|
||||||
|
func NewKeyManager() (manager *KeyManager) {
|
||||||
|
manager = new(KeyManager)
|
||||||
|
manager.keys = map[string]*rsa.PrivateKey{}
|
||||||
|
manager.keySet = new(jose.JSONWebKeySet)
|
||||||
|
|
||||||
|
return manager
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strategy returns the RS256JWTStrategy.
|
||||||
|
func (m KeyManager) Strategy() (strategy *RS256JWTStrategy) {
|
||||||
|
return m.strategy
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetKeySet returns the joseJSONWebKeySet containing the rsa.PublicKey types.
|
||||||
|
func (m KeyManager) GetKeySet() (keySet *jose.JSONWebKeySet) {
|
||||||
|
return m.keySet
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetActiveWebKey obtains the currently active jose.JSONWebKey.
|
||||||
|
func (m KeyManager) GetActiveWebKey() (webKey *jose.JSONWebKey, err error) {
|
||||||
|
webKeys := m.keySet.Key(m.activeKeyID)
|
||||||
|
if len(webKeys) == 1 {
|
||||||
|
return &webKeys[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(webKeys) == 0 {
|
||||||
|
return nil, errors.New("could not find a key with the active key id")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &webKeys[0], errors.New("multiple keys with the same key id")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetActiveKeyID returns the key id of the currently active key.
|
||||||
|
func (m KeyManager) GetActiveKeyID() (keyID string) {
|
||||||
|
return m.activeKeyID
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetActiveKey returns the rsa.PublicKey of the currently active key.
|
||||||
|
func (m KeyManager) GetActiveKey() (key *rsa.PublicKey, err error) {
|
||||||
|
if key, ok := m.keys[m.activeKeyID]; ok {
|
||||||
|
return &key.PublicKey, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.New("failed to retrieve active public key")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetActivePrivateKey returns the rsa.PrivateKey of the currently active key.
|
||||||
|
func (m KeyManager) GetActivePrivateKey() (key *rsa.PrivateKey, err error) {
|
||||||
|
if key, ok := m.keys[m.activeKeyID]; ok {
|
||||||
|
return key, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.New("failed to retrieve active private key")
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddActivePrivateKeyData adds a rsa.PublicKey given the key in the PEM string format, then sets it to the active key.
|
||||||
|
func (m *KeyManager) AddActivePrivateKeyData(data string) (key *rsa.PrivateKey, webKey *jose.JSONWebKey, err error) {
|
||||||
|
key, err = utils.ParseRsaPrivateKeyFromPemStr(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
webKey, err = m.AddActivePrivateKey(key)
|
||||||
|
|
||||||
|
return key, webKey, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddActivePrivateKey adds a rsa.PublicKey, then sets it to the active key.
|
||||||
|
func (m *KeyManager) AddActivePrivateKey(key *rsa.PrivateKey) (webKey *jose.JSONWebKey, err error) {
|
||||||
|
wk := jose.JSONWebKey{
|
||||||
|
Key: &key.PublicKey,
|
||||||
|
Algorithm: "RS256",
|
||||||
|
Use: "sig",
|
||||||
|
}
|
||||||
|
|
||||||
|
keyID, err := wk.Thumbprint(crypto.SHA1)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
strKeyID := strings.ToLower(fmt.Sprintf("%x", keyID))
|
||||||
|
if len(strKeyID) >= 7 {
|
||||||
|
// Shorten the key if it's greater than 7 to a length of exactly 7.
|
||||||
|
strKeyID = strKeyID[0:6]
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := m.keys[strKeyID]; ok {
|
||||||
|
return nil, fmt.Errorf("key id %s already exists", strKeyID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Add Mutex here when implementing key rotation.
|
||||||
|
wk.KeyID = strKeyID
|
||||||
|
m.keySet.Keys = append(m.keySet.Keys, wk)
|
||||||
|
m.keys[strKeyID] = key
|
||||||
|
m.activeKeyID = strKeyID
|
||||||
|
|
||||||
|
m.strategy, err = NewRS256JWTStrategy(wk.KeyID, key)
|
||||||
|
if err != nil {
|
||||||
|
return &wk, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &wk, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRS256JWTStrategy returns a new RS256JWTStrategy.
|
||||||
|
func NewRS256JWTStrategy(id string, key *rsa.PrivateKey) (strategy *RS256JWTStrategy, err error) {
|
||||||
|
strategy = new(RS256JWTStrategy)
|
||||||
|
strategy.JWTStrategy = new(jwt.RS256JWTStrategy)
|
||||||
|
|
||||||
|
strategy.SetKey(id, key)
|
||||||
|
|
||||||
|
return strategy, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RS256JWTStrategy is a decorator struct for the fosite RS256JWTStrategy.
|
||||||
|
type RS256JWTStrategy struct {
|
||||||
|
JWTStrategy *jwt.RS256JWTStrategy
|
||||||
|
|
||||||
|
keyID string
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyID returns the key id.
|
||||||
|
func (s RS256JWTStrategy) KeyID() (id string) {
|
||||||
|
return s.keyID
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetKey sets the provided key id and key as the active key (this is what triggers fosite to use it).
|
||||||
|
func (s *RS256JWTStrategy) SetKey(id string, key *rsa.PrivateKey) {
|
||||||
|
s.keyID = id
|
||||||
|
s.JWTStrategy.PrivateKey = key
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash is a decorator func for the underlying fosite RS256JWTStrategy.
|
||||||
|
func (s *RS256JWTStrategy) Hash(ctx context.Context, in []byte) ([]byte, error) {
|
||||||
|
return s.JWTStrategy.Hash(ctx, in)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSigningMethodLength is a decorator func for the underlying fosite RS256JWTStrategy.
|
||||||
|
func (s *RS256JWTStrategy) GetSigningMethodLength() int {
|
||||||
|
return s.JWTStrategy.GetSigningMethodLength()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSignature is a decorator func for the underlying fosite RS256JWTStrategy.
|
||||||
|
func (s *RS256JWTStrategy) GetSignature(ctx context.Context, token string) (string, error) {
|
||||||
|
return s.JWTStrategy.GetSignature(ctx, token)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate is a decorator func for the underlying fosite RS256JWTStrategy.
|
||||||
|
func (s *RS256JWTStrategy) Generate(ctx context.Context, claims jwt.MapClaims, header jwt.Mapper) (string, string, error) {
|
||||||
|
return s.JWTStrategy.Generate(ctx, claims, header)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate is a decorator func for the underlying fosite RS256JWTStrategy.
|
||||||
|
func (s *RS256JWTStrategy) Validate(ctx context.Context, token string) (string, error) {
|
||||||
|
return s.JWTStrategy.Validate(ctx, token)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode is a decorator func for the underlying fosite RS256JWTStrategy.
|
||||||
|
func (s *RS256JWTStrategy) Decode(ctx context.Context, token string) (*jwt.Token, error) {
|
||||||
|
return s.JWTStrategy.Decode(ctx, token)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPublicKeyID is a decorator func for the underlying fosite RS256JWTStrategy.
|
||||||
|
func (s *RS256JWTStrategy) GetPublicKeyID(_ context.Context) (string, error) {
|
||||||
|
return s.keyID, nil
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
package oidc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestKeyManager_AddActiveKeyData(t *testing.T) {
|
||||||
|
manager := NewKeyManager()
|
||||||
|
assert.Nil(t, manager.strategy)
|
||||||
|
assert.Nil(t, manager.Strategy())
|
||||||
|
|
||||||
|
key, wk, err := manager.AddActivePrivateKeyData(exampleIssuerPrivateKey)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, key)
|
||||||
|
require.NotNil(t, wk)
|
||||||
|
require.NotNil(t, manager.strategy)
|
||||||
|
require.NotNil(t, manager.Strategy())
|
||||||
|
|
||||||
|
thumbprint, err := wk.Thumbprint(crypto.SHA1)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
kid := strings.ToLower(fmt.Sprintf("%x", thumbprint)[0:6])
|
||||||
|
assert.Equal(t, manager.activeKeyID, kid)
|
||||||
|
assert.Equal(t, kid, wk.KeyID)
|
||||||
|
assert.Len(t, manager.keys, 1)
|
||||||
|
assert.Len(t, manager.keySet.Keys, 1)
|
||||||
|
assert.Contains(t, manager.keys, kid)
|
||||||
|
|
||||||
|
keys := manager.keySet.Key(kid)
|
||||||
|
assert.Equal(t, keys[0].KeyID, kid)
|
||||||
|
|
||||||
|
privKey, err := manager.GetActivePrivateKey()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, privKey)
|
||||||
|
|
||||||
|
pubKey, err := manager.GetActiveKey()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, pubKey)
|
||||||
|
|
||||||
|
webKey, err := manager.GetActiveWebKey()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, webKey)
|
||||||
|
|
||||||
|
keySet := manager.GetKeySet()
|
||||||
|
assert.NotNil(t, keySet)
|
||||||
|
assert.Equal(t, kid, manager.GetActiveKeyID())
|
||||||
|
}
|
|
@ -1,26 +1,12 @@
|
||||||
package oidc
|
package oidc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rsa"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/ory/fosite"
|
|
||||||
"github.com/ory/fosite/compose"
|
"github.com/ory/fosite/compose"
|
||||||
"github.com/ory/fosite/token/jwt"
|
|
||||||
"gopkg.in/square/go-jose.v2"
|
|
||||||
|
|
||||||
"github.com/authelia/authelia/internal/configuration/schema"
|
"github.com/authelia/authelia/internal/configuration/schema"
|
||||||
"github.com/authelia/authelia/internal/utils"
|
"github.com/authelia/authelia/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// OpenIDConnectProvider for OpenID Connect.
|
|
||||||
type OpenIDConnectProvider struct {
|
|
||||||
privateKeys map[string]*rsa.PrivateKey
|
|
||||||
|
|
||||||
Fosite fosite.OAuth2Provider
|
|
||||||
Store *OpenIDConnectStore
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewOpenIDConnectProvider new-ups a OpenIDConnectProvider.
|
// NewOpenIDConnectProvider new-ups a OpenIDConnectProvider.
|
||||||
func NewOpenIDConnectProvider(configuration *schema.OpenIDConnectConfiguration) (provider OpenIDConnectProvider, err error) {
|
func NewOpenIDConnectProvider(configuration *schema.OpenIDConnectConfiguration) (provider OpenIDConnectProvider, err error) {
|
||||||
provider = OpenIDConnectProvider{
|
provider = OpenIDConnectProvider{
|
||||||
|
@ -31,20 +17,31 @@ func NewOpenIDConnectProvider(configuration *schema.OpenIDConnectConfiguration)
|
||||||
return provider, nil
|
return provider, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
provider.Store = NewOpenIDConnectStore(configuration)
|
provider.Store, err = NewOpenIDConnectStore(configuration)
|
||||||
|
|
||||||
composeConfiguration := new(compose.Config)
|
|
||||||
|
|
||||||
key, err := utils.ParseRsaPrivateKeyFromPemStr(configuration.IssuerPrivateKey)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return provider, fmt.Errorf("unable to parse the private key of the OpenID issuer: %w", err)
|
return provider, err
|
||||||
}
|
}
|
||||||
|
|
||||||
provider.privateKeys = make(map[string]*rsa.PrivateKey)
|
composeConfiguration := &compose.Config{
|
||||||
provider.privateKeys["main-key"] = key
|
AccessTokenLifespan: configuration.AccessTokenLifespan,
|
||||||
|
AuthorizeCodeLifespan: configuration.AuthorizeCodeLifespan,
|
||||||
|
IDTokenLifespan: configuration.IDTokenLifespan,
|
||||||
|
RefreshTokenLifespan: configuration.RefreshTokenLifespan,
|
||||||
|
SendDebugMessagesToClients: configuration.EnableClientDebugMessages,
|
||||||
|
MinParameterEntropy: configuration.MinimumParameterEntropy,
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Consider implementing RS512 as well.
|
keyManager, err := NewKeyManagerWithConfiguration(configuration)
|
||||||
jwtStrategy := &jwt.RS256JWTStrategy{PrivateKey: key}
|
if err != nil {
|
||||||
|
return provider, err
|
||||||
|
}
|
||||||
|
|
||||||
|
provider.KeyManager = keyManager
|
||||||
|
|
||||||
|
key, err := provider.KeyManager.GetActivePrivateKey()
|
||||||
|
if err != nil {
|
||||||
|
return provider, err
|
||||||
|
}
|
||||||
|
|
||||||
strategy := &compose.CommonStrategy{
|
strategy := &compose.CommonStrategy{
|
||||||
CoreStrategy: compose.NewOAuth2HMACStrategy(
|
CoreStrategy: compose.NewOAuth2HMACStrategy(
|
||||||
|
@ -54,9 +51,9 @@ func NewOpenIDConnectProvider(configuration *schema.OpenIDConnectConfiguration)
|
||||||
),
|
),
|
||||||
OpenIDConnectTokenStrategy: compose.NewOpenIDConnectStrategy(
|
OpenIDConnectTokenStrategy: compose.NewOpenIDConnectStrategy(
|
||||||
composeConfiguration,
|
composeConfiguration,
|
||||||
provider.privateKeys["main-key"],
|
key,
|
||||||
),
|
),
|
||||||
JWTStrategy: jwtStrategy,
|
JWTStrategy: provider.KeyManager.Strategy(),
|
||||||
}
|
}
|
||||||
|
|
||||||
provider.Fosite = compose.Compose(
|
provider.Fosite = compose.Compose(
|
||||||
|
@ -90,19 +87,3 @@ func NewOpenIDConnectProvider(configuration *schema.OpenIDConnectConfiguration)
|
||||||
|
|
||||||
return provider, nil
|
return provider, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetKeySet returns the jose.JSONWebKeySet for the OpenIDConnectProvider.
|
|
||||||
func (p OpenIDConnectProvider) GetKeySet() (webKeySet jose.JSONWebKeySet) {
|
|
||||||
for keyID, key := range p.privateKeys {
|
|
||||||
webKey := jose.JSONWebKey{
|
|
||||||
Key: &key.PublicKey,
|
|
||||||
KeyID: keyID,
|
|
||||||
Algorithm: "RS256",
|
|
||||||
Use: "sig",
|
|
||||||
}
|
|
||||||
|
|
||||||
webKeySet.Keys = append(webKeySet.Keys, webKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
return webKeySet
|
|
||||||
}
|
|
||||||
|
|
|
@ -26,15 +26,41 @@ func TestOpenIDConnectProvider_NewOpenIDConnectProvider_BadIssuerKey(t *testing.
|
||||||
assert.Error(t, err, "abc")
|
assert.Error(t, err, "abc")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestOpenIDConnectProvider_GetKeySet(t *testing.T) {
|
func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GoodConfiguration(t *testing.T) {
|
||||||
p, err := NewOpenIDConnectProvider(&schema.OpenIDConnectConfiguration{
|
provider, err := NewOpenIDConnectProvider(&schema.OpenIDConnectConfiguration{
|
||||||
IssuerPrivateKey: exampleIssuerPrivateKey,
|
IssuerPrivateKey: exampleIssuerPrivateKey,
|
||||||
|
HMACSecret: "asbdhaaskmdlkamdklasmdlkams",
|
||||||
|
Clients: []schema.OpenIDConnectClientConfiguration{
|
||||||
|
{
|
||||||
|
ID: "a-client",
|
||||||
|
Secret: "a-client-secret",
|
||||||
|
Policy: "one_factor",
|
||||||
|
RedirectURIs: []string{
|
||||||
|
"https://google.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "b-client",
|
||||||
|
Description: "Normal Description",
|
||||||
|
Secret: "b-client-secret",
|
||||||
|
Policy: "two_factor",
|
||||||
|
RedirectURIs: []string{
|
||||||
|
"https://google.com",
|
||||||
|
},
|
||||||
|
Scopes: []string{
|
||||||
|
"groups",
|
||||||
|
},
|
||||||
|
GrantTypes: []string{
|
||||||
|
"refresh_token",
|
||||||
|
},
|
||||||
|
ResponseTypes: []string{
|
||||||
|
"token",
|
||||||
|
"code",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
assert.NotNil(t, provider)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
assert.Len(t, p.GetKeySet().Keys, 1)
|
|
||||||
assert.Equal(t, "RS256", p.GetKeySet().Keys[0].Algorithm)
|
|
||||||
assert.Equal(t, "sig", p.GetKeySet().Keys[0].Use)
|
|
||||||
assert.Equal(t, "main-key", p.GetKeySet().Keys[0].KeyID)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,31 +14,10 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewOpenIDConnectStore returns a new OpenIDConnectStore using the provided schema.OpenIDConnectConfiguration.
|
// NewOpenIDConnectStore returns a new OpenIDConnectStore using the provided schema.OpenIDConnectConfiguration.
|
||||||
func NewOpenIDConnectStore(configuration *schema.OpenIDConnectConfiguration) (store *OpenIDConnectStore) {
|
func NewOpenIDConnectStore(configuration *schema.OpenIDConnectConfiguration) (store *OpenIDConnectStore, err error) {
|
||||||
store = &OpenIDConnectStore{}
|
store = &OpenIDConnectStore{
|
||||||
|
memory: &storage.MemoryStore{
|
||||||
store.clients = make(map[string]*InternalClient)
|
IDSessions: map[string]fosite.Requester{},
|
||||||
|
|
||||||
for _, clientConf := range configuration.Clients {
|
|
||||||
policy := authorization.PolicyToLevel(clientConf.Policy)
|
|
||||||
logging.Logger().Debugf("Registering client %s with policy %s (%v)", clientConf.ID, clientConf.Policy, policy)
|
|
||||||
|
|
||||||
client := &InternalClient{
|
|
||||||
ID: clientConf.ID,
|
|
||||||
Description: clientConf.Description,
|
|
||||||
Policy: authorization.PolicyToLevel(clientConf.Policy),
|
|
||||||
Secret: []byte(clientConf.Secret),
|
|
||||||
RedirectURIs: clientConf.RedirectURIs,
|
|
||||||
GrantTypes: clientConf.GrantTypes,
|
|
||||||
ResponseTypes: clientConf.ResponseTypes,
|
|
||||||
Scopes: clientConf.Scopes,
|
|
||||||
}
|
|
||||||
|
|
||||||
store.clients[client.ID] = client
|
|
||||||
}
|
|
||||||
|
|
||||||
store.memory = &storage.MemoryStore{
|
|
||||||
IDSessions: make(map[string]fosite.Requester),
|
|
||||||
Users: map[string]storage.MemoryUserRelation{},
|
Users: map[string]storage.MemoryUserRelation{},
|
||||||
AuthorizeCodes: map[string]storage.StoreAuthorizeCode{},
|
AuthorizeCodes: map[string]storage.StoreAuthorizeCode{},
|
||||||
AccessTokens: map[string]fosite.Requester{},
|
AccessTokens: map[string]fosite.Requester{},
|
||||||
|
@ -46,19 +25,19 @@ func NewOpenIDConnectStore(configuration *schema.OpenIDConnectConfiguration) (st
|
||||||
PKCES: map[string]fosite.Requester{},
|
PKCES: map[string]fosite.Requester{},
|
||||||
AccessTokenRequestIDs: map[string]string{},
|
AccessTokenRequestIDs: map[string]string{},
|
||||||
RefreshTokenRequestIDs: map[string]string{},
|
RefreshTokenRequestIDs: map[string]string{},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
return store
|
store.clients = make(map[string]*InternalClient)
|
||||||
}
|
|
||||||
|
|
||||||
// OpenIDConnectStore is Authelia's internal representation of the fosite.Storage interface.
|
for _, client := range configuration.Clients {
|
||||||
//
|
policy := authorization.PolicyToLevel(client.Policy)
|
||||||
// Currently it is mostly just implementing a decorator pattern other then GetInternalClient.
|
logging.Logger().Debugf("Registering client %s with policy %s (%v)", client.ID, client.Policy, policy)
|
||||||
// The long term plan is to have these methods interact with the Authelia storage and
|
|
||||||
// session providers where applicable.
|
store.clients[client.ID] = NewClient(client)
|
||||||
type OpenIDConnectStore struct {
|
}
|
||||||
clients map[string]*InternalClient
|
|
||||||
memory *storage.MemoryStore
|
return store, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetClientPolicy retrieves the policy from the client with the matching provided id.
|
// GetClientPolicy retrieves the policy from the client with the matching provided id.
|
||||||
|
|
|
@ -12,7 +12,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestOpenIDConnectStore_GetClientPolicy(t *testing.T) {
|
func TestOpenIDConnectStore_GetClientPolicy(t *testing.T) {
|
||||||
s := NewOpenIDConnectStore(&schema.OpenIDConnectConfiguration{
|
s, err := NewOpenIDConnectStore(&schema.OpenIDConnectConfiguration{
|
||||||
IssuerPrivateKey: exampleIssuerPrivateKey,
|
IssuerPrivateKey: exampleIssuerPrivateKey,
|
||||||
Clients: []schema.OpenIDConnectClientConfiguration{
|
Clients: []schema.OpenIDConnectClientConfiguration{
|
||||||
{
|
{
|
||||||
|
@ -32,6 +32,8 @@ func TestOpenIDConnectStore_GetClientPolicy(t *testing.T) {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
policyOne := s.GetClientPolicy("myclient")
|
policyOne := s.GetClientPolicy("myclient")
|
||||||
assert.Equal(t, authorization.OneFactor, policyOne)
|
assert.Equal(t, authorization.OneFactor, policyOne)
|
||||||
|
|
||||||
|
@ -43,7 +45,7 @@ func TestOpenIDConnectStore_GetClientPolicy(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestOpenIDConnectStore_GetInternalClient(t *testing.T) {
|
func TestOpenIDConnectStore_GetInternalClient(t *testing.T) {
|
||||||
s := NewOpenIDConnectStore(&schema.OpenIDConnectConfiguration{
|
s, err := NewOpenIDConnectStore(&schema.OpenIDConnectConfiguration{
|
||||||
IssuerPrivateKey: exampleIssuerPrivateKey,
|
IssuerPrivateKey: exampleIssuerPrivateKey,
|
||||||
Clients: []schema.OpenIDConnectClientConfiguration{
|
Clients: []schema.OpenIDConnectClientConfiguration{
|
||||||
{
|
{
|
||||||
|
@ -56,6 +58,8 @@ func TestOpenIDConnectStore_GetInternalClient(t *testing.T) {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
client, err := s.GetClient(context.Background(), "myinvalidclient")
|
client, err := s.GetClient(context.Background(), "myinvalidclient")
|
||||||
assert.EqualError(t, err, "not_found")
|
assert.EqualError(t, err, "not_found")
|
||||||
assert.Nil(t, client)
|
assert.Nil(t, client)
|
||||||
|
@ -74,11 +78,13 @@ func TestOpenIDConnectStore_GetInternalClient_ValidClient(t *testing.T) {
|
||||||
Scopes: []string{"openid", "profile"},
|
Scopes: []string{"openid", "profile"},
|
||||||
Secret: "mysecret",
|
Secret: "mysecret",
|
||||||
}
|
}
|
||||||
s := NewOpenIDConnectStore(&schema.OpenIDConnectConfiguration{
|
s, err := NewOpenIDConnectStore(&schema.OpenIDConnectConfiguration{
|
||||||
IssuerPrivateKey: exampleIssuerPrivateKey,
|
IssuerPrivateKey: exampleIssuerPrivateKey,
|
||||||
Clients: []schema.OpenIDConnectClientConfiguration{c1},
|
Clients: []schema.OpenIDConnectClientConfiguration{c1},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
client, err := s.GetInternalClient(c1.ID)
|
client, err := s.GetInternalClient(c1.ID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, client)
|
require.NotNil(t, client)
|
||||||
|
@ -100,18 +106,20 @@ func TestOpenIDConnectStore_GetInternalClient_InvalidClient(t *testing.T) {
|
||||||
Scopes: []string{"openid", "profile"},
|
Scopes: []string{"openid", "profile"},
|
||||||
Secret: "mysecret",
|
Secret: "mysecret",
|
||||||
}
|
}
|
||||||
s := NewOpenIDConnectStore(&schema.OpenIDConnectConfiguration{
|
s, err := NewOpenIDConnectStore(&schema.OpenIDConnectConfiguration{
|
||||||
IssuerPrivateKey: exampleIssuerPrivateKey,
|
IssuerPrivateKey: exampleIssuerPrivateKey,
|
||||||
Clients: []schema.OpenIDConnectClientConfiguration{c1},
|
Clients: []schema.OpenIDConnectClientConfiguration{c1},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
client, err := s.GetInternalClient("another-client")
|
client, err := s.GetInternalClient("another-client")
|
||||||
assert.Nil(t, client)
|
assert.Nil(t, client)
|
||||||
assert.EqualError(t, err, "not_found")
|
assert.EqualError(t, err, "not_found")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestOpenIDConnectStore_IsValidClientID(t *testing.T) {
|
func TestOpenIDConnectStore_IsValidClientID(t *testing.T) {
|
||||||
s := NewOpenIDConnectStore(&schema.OpenIDConnectConfiguration{
|
s, err := NewOpenIDConnectStore(&schema.OpenIDConnectConfiguration{
|
||||||
IssuerPrivateKey: exampleIssuerPrivateKey,
|
IssuerPrivateKey: exampleIssuerPrivateKey,
|
||||||
Clients: []schema.OpenIDConnectClientConfiguration{
|
Clients: []schema.OpenIDConnectClientConfiguration{
|
||||||
{
|
{
|
||||||
|
@ -124,6 +132,8 @@ func TestOpenIDConnectStore_IsValidClientID(t *testing.T) {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
validClient := s.IsValidClientID("myclient")
|
validClient := s.IsValidClientID("myclient")
|
||||||
invalidClient := s.IsValidClientID("myinvalidclient")
|
invalidClient := s.IsValidClientID("myinvalidclient")
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,112 @@
|
||||||
|
package oidc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rsa"
|
||||||
|
|
||||||
|
"github.com/ory/fosite"
|
||||||
|
"github.com/ory/fosite/handler/openid"
|
||||||
|
"github.com/ory/fosite/storage"
|
||||||
|
"gopkg.in/square/go-jose.v2"
|
||||||
|
|
||||||
|
"github.com/authelia/authelia/internal/authorization"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OpenIDConnectProvider for OpenID Connect.
|
||||||
|
type OpenIDConnectProvider struct {
|
||||||
|
Fosite fosite.OAuth2Provider
|
||||||
|
Store *OpenIDConnectStore
|
||||||
|
KeyManager *KeyManager
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenIDConnectStore is Authelia's internal representation of the fosite.Storage interface.
|
||||||
|
//
|
||||||
|
// Currently it is mostly just implementing a decorator pattern other then GetInternalClient.
|
||||||
|
// The long term plan is to have these methods interact with the Authelia storage and
|
||||||
|
// session providers where applicable.
|
||||||
|
type OpenIDConnectStore struct {
|
||||||
|
clients map[string]*InternalClient
|
||||||
|
memory *storage.MemoryStore
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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"`
|
||||||
|
Policy authorization.Level `json:"-"`
|
||||||
|
|
||||||
|
// These are the OpenIDConnect Client props.
|
||||||
|
ResponseModes []fosite.ResponseModeType `json:"response_modes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyManager keeps track of all of the active/inactive rsa keys and provides them to services requiring them.
|
||||||
|
// It additionally allows us to add keys for the purpose of key rotation in the future.
|
||||||
|
type KeyManager struct {
|
||||||
|
activeKeyID string
|
||||||
|
keys map[string]*rsa.PrivateKey
|
||||||
|
keySet *jose.JSONWebKeySet
|
||||||
|
strategy *RS256JWTStrategy
|
||||||
|
}
|
||||||
|
|
||||||
|
// AutheliaHasher implements the fosite.Hasher interface without an actual hashing algo.
|
||||||
|
type AutheliaHasher struct{}
|
||||||
|
|
||||||
|
// ConsentGetResponseBody schema of the response body of the consent GET endpoint.
|
||||||
|
type ConsentGetResponseBody struct {
|
||||||
|
ClientID string `json:"client_id"`
|
||||||
|
ClientDescription string `json:"client_description"`
|
||||||
|
Scopes []Scope `json:"scopes"`
|
||||||
|
Audience []Audience `json:"audience"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scope represents the scope information.
|
||||||
|
type Scope struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Audience represents the audience information.
|
||||||
|
type Audience struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// WellKnownConfiguration is the OIDC well known config struct.
|
||||||
|
//
|
||||||
|
// See https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata
|
||||||
|
type WellKnownConfiguration struct {
|
||||||
|
Issuer string `json:"issuer"`
|
||||||
|
JWKSURI string `json:"jwks_uri"`
|
||||||
|
|
||||||
|
AuthorizationEndpoint string `json:"authorization_endpoint"`
|
||||||
|
TokenEndpoint string `json:"token_endpoint"`
|
||||||
|
RevocationEndpoint string `json:"revocation_endpoint"`
|
||||||
|
|
||||||
|
Algorithms []string `json:"id_token_signing_alg_values_supported"`
|
||||||
|
|
||||||
|
SubjectTypesSupported []string `json:"subject_types_supported"`
|
||||||
|
ResponseTypesSupported []string `json:"response_types_supported"`
|
||||||
|
ResponseModesSupported []string `json:"response_modes_supported"`
|
||||||
|
ScopesSupported []string `json:"scopes_supported"`
|
||||||
|
ClaimsSupported []string `json:"claims_supported"`
|
||||||
|
|
||||||
|
RequestURIParameterSupported bool `json:"request_uri_parameter_supported"`
|
||||||
|
BackChannelLogoutSupported bool `json:"backchannel_logout_supported"`
|
||||||
|
FrontChannelLogoutSupported bool `json:"frontchannel_logout_supported"`
|
||||||
|
BackChannelLogoutSessionSupported bool `json:"backchannel_logout_session_supported"`
|
||||||
|
FrontChannelLogoutSessionSupported bool `json:"frontchannel_logout_session_supported"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenIDSession holds OIDC Session information.
|
||||||
|
type OpenIDSession struct {
|
||||||
|
*openid.DefaultSession `json:"idToken"`
|
||||||
|
|
||||||
|
Extra map[string]interface{} `json:"extra"`
|
||||||
|
ClientID string
|
||||||
|
}
|
|
@ -2,12 +2,14 @@ package session
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/valyala/fasthttp"
|
"github.com/valyala/fasthttp"
|
||||||
|
|
||||||
"github.com/authelia/authelia/internal/authentication"
|
"github.com/authelia/authelia/internal/authentication"
|
||||||
|
"github.com/authelia/authelia/internal/authorization"
|
||||||
"github.com/authelia/authelia/internal/configuration/schema"
|
"github.com/authelia/authelia/internal/configuration/schema"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -27,6 +29,7 @@ func TestShouldInitializerSession(t *testing.T) {
|
||||||
|
|
||||||
func TestShouldUpdateSession(t *testing.T) {
|
func TestShouldUpdateSession(t *testing.T) {
|
||||||
ctx := &fasthttp.RequestCtx{}
|
ctx := &fasthttp.RequestCtx{}
|
||||||
|
|
||||||
configuration := schema.SessionConfiguration{}
|
configuration := schema.SessionConfiguration{}
|
||||||
configuration.Domain = testDomain
|
configuration.Domain = testDomain
|
||||||
configuration.Name = testName
|
configuration.Name = testName
|
||||||
|
@ -50,6 +53,77 @@ func TestShouldUpdateSession(t *testing.T) {
|
||||||
}, session)
|
}, session)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestShouldSetSessionAuthenticationLevels(t *testing.T) {
|
||||||
|
ctx := &fasthttp.RequestCtx{}
|
||||||
|
configuration := schema.SessionConfiguration{}
|
||||||
|
|
||||||
|
timeOneFactor := time.Unix(1625048140, 0)
|
||||||
|
timeTwoFactor := time.Unix(1625048150, 0)
|
||||||
|
timeZeroFactor := time.Unix(0, 0)
|
||||||
|
|
||||||
|
configuration.Domain = testDomain
|
||||||
|
configuration.Name = testName
|
||||||
|
configuration.Expiration = testExpiration
|
||||||
|
|
||||||
|
provider := NewProvider(configuration, nil)
|
||||||
|
session, _ := provider.GetSession(ctx)
|
||||||
|
|
||||||
|
session.SetOneFactor(timeOneFactor, &authentication.UserDetails{Username: testUsername}, false)
|
||||||
|
|
||||||
|
err := provider.SaveSession(ctx, session)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
session, err = provider.GetSession(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
authAt, err := session.AuthenticatedTime(authorization.OneFactor)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, timeOneFactor, authAt)
|
||||||
|
|
||||||
|
authAt, err = session.AuthenticatedTime(authorization.TwoFactor)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, timeZeroFactor, authAt)
|
||||||
|
|
||||||
|
authAt, err = session.AuthenticatedTime(authorization.Denied)
|
||||||
|
assert.EqualError(t, err, "invalid authorization level")
|
||||||
|
assert.Equal(t, timeZeroFactor, authAt)
|
||||||
|
|
||||||
|
assert.Equal(t, UserSession{
|
||||||
|
Username: testUsername,
|
||||||
|
AuthenticationLevel: authentication.OneFactor,
|
||||||
|
LastActivity: timeOneFactor.Unix(),
|
||||||
|
FirstFactorAuthnTimestamp: timeOneFactor.Unix(),
|
||||||
|
}, session)
|
||||||
|
|
||||||
|
session.SetTwoFactor(timeTwoFactor)
|
||||||
|
|
||||||
|
err = provider.SaveSession(ctx, session)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
session, err = provider.GetSession(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, UserSession{
|
||||||
|
Username: testUsername,
|
||||||
|
AuthenticationLevel: authentication.TwoFactor,
|
||||||
|
LastActivity: timeTwoFactor.Unix(),
|
||||||
|
FirstFactorAuthnTimestamp: timeOneFactor.Unix(),
|
||||||
|
SecondFactorAuthnTimestamp: timeTwoFactor.Unix(),
|
||||||
|
}, session)
|
||||||
|
|
||||||
|
authAt, err = session.AuthenticatedTime(authorization.OneFactor)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, timeOneFactor, authAt)
|
||||||
|
|
||||||
|
authAt, err = session.AuthenticatedTime(authorization.TwoFactor)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, timeTwoFactor, authAt)
|
||||||
|
|
||||||
|
authAt, err = session.AuthenticatedTime(authorization.Denied)
|
||||||
|
assert.EqualError(t, err, "invalid authorization level")
|
||||||
|
assert.Equal(t, timeZeroFactor, authAt)
|
||||||
|
}
|
||||||
|
|
||||||
func TestShouldDestroySessionAndWipeSessionData(t *testing.T) {
|
func TestShouldDestroySessionAndWipeSessionData(t *testing.T) {
|
||||||
ctx := &fasthttp.RequestCtx{}
|
ctx := &fasthttp.RequestCtx{}
|
||||||
configuration := schema.SessionConfiguration{}
|
configuration := schema.SessionConfiguration{}
|
||||||
|
|
|
@ -37,6 +37,9 @@ type UserSession struct {
|
||||||
AuthenticationLevel authentication.Level
|
AuthenticationLevel authentication.Level
|
||||||
LastActivity int64
|
LastActivity int64
|
||||||
|
|
||||||
|
FirstFactorAuthnTimestamp int64
|
||||||
|
SecondFactorAuthnTimestamp int64
|
||||||
|
|
||||||
// The challenge generated in first step of U2F registration (after identity verification) or authentication.
|
// The challenge generated in first step of U2F registration (after identity verification) or authentication.
|
||||||
// This is used reused in the second phase to check that the challenge has been completed.
|
// This is used reused in the second phase to check that the challenge has been completed.
|
||||||
U2FChallenge *u2f.Challenge
|
U2FChallenge *u2f.Challenge
|
||||||
|
@ -70,4 +73,5 @@ type OIDCWorkflowSession struct {
|
||||||
TargetURI string
|
TargetURI string
|
||||||
AuthURI string
|
AuthURI string
|
||||||
RequiredAuthorizationLevel authorization.Level
|
RequiredAuthorizationLevel authorization.Level
|
||||||
|
CreatedTimestamp int64
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
package session
|
package session
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/authelia/authelia/internal/authentication"
|
"github.com/authelia/authelia/internal/authentication"
|
||||||
|
"github.com/authelia/authelia/internal/authorization"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewDefaultUserSession create a default user session.
|
// NewDefaultUserSession create a default user session.
|
||||||
|
@ -12,3 +16,36 @@ func NewDefaultUserSession() UserSession {
|
||||||
LastActivity: 0,
|
LastActivity: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetOneFactor sets the expected property values for one factor authentication.
|
||||||
|
func (s *UserSession) SetOneFactor(now time.Time, details *authentication.UserDetails, keepMeLoggedIn bool) {
|
||||||
|
s.FirstFactorAuthnTimestamp = now.Unix()
|
||||||
|
s.LastActivity = now.Unix()
|
||||||
|
s.AuthenticationLevel = authentication.OneFactor
|
||||||
|
|
||||||
|
s.KeepMeLoggedIn = keepMeLoggedIn
|
||||||
|
|
||||||
|
s.Username = details.Username
|
||||||
|
s.DisplayName = details.DisplayName
|
||||||
|
s.Groups = details.Groups
|
||||||
|
s.Emails = details.Emails
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTwoFactor sets the expected property values for two factor authentication.
|
||||||
|
func (s *UserSession) SetTwoFactor(now time.Time) {
|
||||||
|
s.SecondFactorAuthnTimestamp = now.Unix()
|
||||||
|
s.LastActivity = now.Unix()
|
||||||
|
s.AuthenticationLevel = authentication.TwoFactor
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthenticatedTime returns the unix timestamp this session authenticated successfully at the given level.
|
||||||
|
func (s UserSession) AuthenticatedTime(level authorization.Level) (authenticatedTime time.Time, err error) {
|
||||||
|
switch level {
|
||||||
|
case authorization.OneFactor:
|
||||||
|
return time.Unix(s.FirstFactorAuthnTimestamp, 0), nil
|
||||||
|
case authorization.TwoFactor:
|
||||||
|
return time.Unix(s.SecondFactorAuthnTimestamp, 0), nil
|
||||||
|
default:
|
||||||
|
return time.Unix(0, 0), errors.New("invalid authorization level")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -19,5 +19,9 @@ func (s *OIDCSuite) TestOIDCScenario() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestOIDCSuite(t *testing.T) {
|
func TestOIDCSuite(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("skipping suite test in short mode")
|
||||||
|
}
|
||||||
|
|
||||||
suite.Run(t, NewOIDCSuite())
|
suite.Run(t, NewOIDCSuite())
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,5 +19,9 @@ func (s *OIDCTraefikSuite) TestOIDCScenario() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestOIDCTraefikSuite(t *testing.T) {
|
func TestOIDCTraefikSuite(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("skipping suite test in short mode")
|
||||||
|
}
|
||||||
|
|
||||||
suite.Run(t, NewOIDCTraefikSuite())
|
suite.Run(t, NewOIDCTraefikSuite())
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,29 @@
|
||||||
package utils
|
package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
"unicode"
|
"unicode"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// IsStringAbsURL checks a string can be parsed as a URL and that is IsAbs and if it can't it returns an error
|
||||||
|
// describing why.
|
||||||
|
func IsStringAbsURL(input string) (err error) {
|
||||||
|
parsedURL, err := url.Parse(input)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not parse '%s' as a URL", input)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !parsedURL.IsAbs() {
|
||||||
|
return fmt.Errorf("the url '%s' is not absolute because it doesn't start with a scheme like 'http://' or 'https://'", input)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// IsStringAlphaNumeric returns false if any rune in the string is not alpha-numeric.
|
// IsStringAlphaNumeric returns false if any rune in the string is not alpha-numeric.
|
||||||
func IsStringAlphaNumeric(input string) bool {
|
func IsStringAlphaNumeric(input string) bool {
|
||||||
for _, r := range input {
|
for _, r := range input {
|
||||||
|
|
Loading…
Reference in New Issue