mirror of
https://github.com/0rangebananaspy/authelia.git
synced 2024-09-14 22:47:21 +07:00
feature(oidc): add support for OpenID Connect
OpenID connect has become a standard when it comes to authentication and in order to fix a security concern around forwarding authentication and authorization information it has been decided to add support for it. This feature is in beta version and only enabled when there is a configuration for it. Before enabling it in production, please consider that it's in beta with potential bugs and that there are several production critical features still missing such as all OIDC related data is stored in configuration or memory. This means you are potentially going to experience issues with HA deployments, or when restarting a single instance specifically related to OIDC. We are still working on adding the remaining set of features before making it GA as soon as possible. Related to #189 Co-authored-by: Clement Michaud <clement.michaud34@gmail.com>
This commit is contained in:
parent
48d8e1e541
commit
ddea31193b
|
@ -50,7 +50,7 @@ Here is what Authelia's portal looks like
|
|||
|
||||
Here is the list of the main available features:
|
||||
|
||||
* Several kind of second factor:
|
||||
* Several second factor methods:
|
||||
* **[Security Key (U2F)](https://www.authelia.com/docs/features/2fa/security-key)** with [Yubikey].
|
||||
* **[Time-based One-Time password](https://www.authelia.com/docs/features/2fa/one-time-password)**
|
||||
with [Google Authenticator].
|
||||
|
@ -61,6 +61,7 @@ Here is the list of the main available features:
|
|||
* Access restriction after too many authentication attempts.
|
||||
* Fine-grained access control per subdomain, user, resource and network.
|
||||
* Support of basic authentication for endpoints protected by single factor.
|
||||
* Beta support for [OpenID Connect](https://www.authelia.com/docs/configuration/identity-providers/oidc.html).
|
||||
* Highly available using a remote database and Redis as a highly available KV store.
|
||||
* Compatible with Kubernetes [ingress-nginx](https://github.com/kubernetes/ingress-nginx) controller out of the box.
|
||||
|
||||
|
|
|
@ -59,6 +59,9 @@ var hostEntries = []HostEntry{
|
|||
|
||||
// Kubernetes dashboard.
|
||||
{Domain: "kubernetes.example.com", IP: "192.168.240.110"},
|
||||
// OIDC tester app
|
||||
{Domain: "oidc.example.com", IP: "192.168.240.100"},
|
||||
{Domain: "oidc-public.example.com", IP: "192.168.240.100"},
|
||||
}
|
||||
|
||||
func runCommand(cmd string, args ...string) {
|
||||
|
|
|
@ -59,7 +59,7 @@ var Commands = []AutheliaCommandDefinition{
|
|||
},
|
||||
{
|
||||
Name: "suites",
|
||||
Short: "Compute hash of a password for creating a file-based users database",
|
||||
Short: "Commands related to suites management",
|
||||
SubCommands: CobraCommands{
|
||||
SuitesTestCmd,
|
||||
SuitesListCmd,
|
||||
|
@ -135,7 +135,7 @@ func main() {
|
|||
cobraCommands = append(cobraCommands, command)
|
||||
}
|
||||
|
||||
cobraCommands = append(cobraCommands, commands.HashPasswordCmd)
|
||||
cobraCommands = append(cobraCommands, commands.HashPasswordCmd, commands.CertificatesCmd, commands.RSACmd)
|
||||
|
||||
rootCmd.PersistentFlags().StringVar(&logLevel, "log-level", "info", "Set the log level for the command")
|
||||
rootCmd.AddCommand(cobraCommands...)
|
||||
|
|
|
@ -83,7 +83,7 @@ func setupSuite(cmd *cobra.Command, args []string) {
|
|||
|
||||
suiteResourcePath := cwd + "/internal/suites/" + suiteName
|
||||
|
||||
exist, err := utils.FileExists(suiteResourcePath)
|
||||
exist, err := utils.PathExists(suiteResourcePath)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
|
|
|
@ -14,6 +14,7 @@ import (
|
|||
"github.com/authelia/authelia/internal/logging"
|
||||
"github.com/authelia/authelia/internal/middlewares"
|
||||
"github.com/authelia/authelia/internal/notification"
|
||||
"github.com/authelia/authelia/internal/oidc"
|
||||
"github.com/authelia/authelia/internal/regulation"
|
||||
"github.com/authelia/authelia/internal/server"
|
||||
"github.com/authelia/authelia/internal/session"
|
||||
|
@ -117,15 +118,22 @@ func startServer() {
|
|||
authorizer := authorization.NewAuthorizer(config.AccessControl)
|
||||
sessionProvider := session.NewProvider(config.Session, autheliaCertPool)
|
||||
regulator := regulation.NewRegulator(config.Regulation, storageProvider, clock)
|
||||
oidcProvider, err := oidc.NewOpenIDConnectProvider(config.IdentityProviders.OIDC)
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
providers := middlewares.Providers{
|
||||
Authorizer: authorizer,
|
||||
UserProvider: userProvider,
|
||||
Regulator: regulator,
|
||||
OpenIDConnect: oidcProvider,
|
||||
StorageProvider: storageProvider,
|
||||
Notifier: notifier,
|
||||
SessionProvider: sessionProvider,
|
||||
}
|
||||
|
||||
server.StartServer(*config, providers)
|
||||
}
|
||||
|
||||
|
@ -149,7 +157,8 @@ func main() {
|
|||
}
|
||||
|
||||
rootCmd.AddCommand(versionCmd, commands.HashPasswordCmd,
|
||||
commands.ValidateConfigCmd, commands.CertificatesCmd)
|
||||
commands.ValidateConfigCmd, commands.CertificatesCmd,
|
||||
commands.RSACmd)
|
||||
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
logger.Fatal(err)
|
||||
|
|
|
@ -553,4 +553,62 @@ notifier:
|
|||
# sender: admin@example.com
|
||||
# host: smtp.gmail.com
|
||||
# port: 587
|
||||
|
||||
##
|
||||
## Identity Providers
|
||||
##
|
||||
# identity_providers:
|
||||
|
||||
##
|
||||
## OpenID Connect (Identity Provider)
|
||||
##
|
||||
## It's recommended you read the documentation before configuration of this section:
|
||||
## https://www.authelia.com/docs/configuration/identity-providers/oidc.html
|
||||
# oidc:
|
||||
## The hmac_secret is used to sign OAuth2 tokens (authorization code, access tokens and refresh tokens).
|
||||
## HMAC Secret can also be set using a secret: https://www.authelia.com/docs/configuration/secrets.html
|
||||
# hmac_secret: this_is_a_secret_abc123abc123abc
|
||||
|
||||
## The issuer_private_key is used to sign the JWT forged by OpenID Connect.
|
||||
## Issuer Private Key can also be set using a secret: https://docs.authelia.com/configuration/secrets.html
|
||||
# issuer_private_key: |
|
||||
# --- KEY START
|
||||
# --- KEY END
|
||||
|
||||
## Clients is a list of known clients and their configuration.
|
||||
# clients:
|
||||
# -
|
||||
## The ID is the OpenID Connect ClientID which is used to link an application to a configuration.
|
||||
# id: myapp
|
||||
|
||||
## The description to show to users when they end up on the consent screen. Defaults to the ID above.
|
||||
# description: My Application
|
||||
|
||||
## The client secret is a shared secret between Authelia and the consumer of this client.
|
||||
# secret: this_is_a_secret
|
||||
|
||||
## The policy to require for this client; one_factor or two_factor.
|
||||
# authorization_policy: two_factor
|
||||
|
||||
## Redirect URI's specifies a list of valid case-sensitive callbacks for this client.
|
||||
# redirect_uris:
|
||||
# - https://oidc.example.com:8080/oauth2/callback
|
||||
|
||||
## Scopes defines the valid scopes this client can request
|
||||
# scopes:
|
||||
# - openid
|
||||
# - groups
|
||||
# - email
|
||||
# - profile
|
||||
|
||||
## Grant Types configures which grants this client can obtain.
|
||||
## It's not recommended to define this unless you know what you're doing.
|
||||
# grant_types:
|
||||
# - refresh_token
|
||||
# - "authorization_code
|
||||
|
||||
## Response Types configures which responses this client can be sent.
|
||||
## It's not recommended to define this unless you know what you're doing.
|
||||
# response_types:
|
||||
# - code
|
||||
...
|
||||
|
|
|
@ -1,3 +1,10 @@
|
|||
.label.label-config {
|
||||
text-transform: none;
|
||||
}
|
||||
.tbl-header {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
.tbl-beta-stage {
|
||||
border-bottom-width: 3px !important;
|
||||
}
|
12
docs/configuration/identity-providers/index.md
Normal file
12
docs/configuration/identity-providers/index.md
Normal file
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
layout: default
|
||||
title: Identity Providers
|
||||
parent: Configuration
|
||||
nav_order: 12
|
||||
has_children: true
|
||||
---
|
||||
|
||||
# Identity Providers
|
||||
|
||||
This section covers configuration of the identity server characteristics of Authelia. Currently the only identity server
|
||||
supported is OpenID Connect.
|
225
docs/configuration/identity-providers/oidc.md
Normal file
225
docs/configuration/identity-providers/oidc.md
Normal file
|
@ -0,0 +1,225 @@
|
|||
---
|
||||
layout: default
|
||||
title: OpenID Connect
|
||||
parent: Identity Providers
|
||||
grand_parent: Configuration
|
||||
nav_order: 2
|
||||
---
|
||||
|
||||
# OpenID Connect
|
||||
|
||||
**Authelia** currently supports the [OpenID Connect] OP role as a [beta](#beta) feature. The OP role is the
|
||||
[OpenID Connect] Provider role, not the Relaying Party or RP role. This means other applications that implement the
|
||||
[OpenID Connect] RP role can use Authelia as an authentication and authorization backend similar to how you may use
|
||||
social media or development platforms for login.
|
||||
|
||||
The Relaying Party role is the role which allows an application to use GitHub, Google, or other [OpenID Connect]
|
||||
providers for authentication and authorization. We do not intend to support this functionality at this moment in time.
|
||||
|
||||
## Beta
|
||||
|
||||
We have decided to implement [OpenID Connect] as a beta feature, it's suggested you only utilize it for testing and
|
||||
providing feedback, and should take caution in relying on it in production. [OpenID Connect] and it's related endpoints
|
||||
are not enabled by default unless you specifically configure the [OpenID Connect] section.
|
||||
|
||||
The beta will be broken up into stages. Each stage will bring additional features. The following table is a *rough* plan
|
||||
for which stage will have each feature, and may evolve over time:
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="tbl-header">Stage</th>
|
||||
<th class="tbl-header">Feature Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td rowspan="7" class="tbl-header tbl-beta-stage">beta1</td>
|
||||
<td><a href="https://openid.net/specs/openid-connect-core-1_0.html#Consent" target="_blank" rel="noopener noreferrer">User Consent</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowSteps" target="_blank" rel="noopener noreferrer">Authorization Code Flow</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://openid.net/specs/openid-connect-discovery-1_0.html" target="_blank" rel="noopener noreferrer">OpenID Connect Discovery</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>RS256 Signature Strategy</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Per Client Scope/Grant Type/Response Type Restriction</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Per Client Authorization Policy (1FA/2FA)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="tbl-beta-stage">Per Client List of Valid Redirection URI's</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="2" class="tbl-header tbl-beta-stage">beta2 <sup>1</sup></td>
|
||||
<td>Token Storage</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="tbl-beta-stage">Audit Storage</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="4" class="tbl-header tbl-beta-stage">beta3 <sup>1</sup></td>
|
||||
<td><a href="https://openid.net/specs/openid-connect-backchannel-1_0.html" target="_blank" rel="noopener noreferrer">Back-Channel Logout</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Deny Refresh on Session Expiration</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://openid.net/specs/openid-connect-messages-1_0-20.html#rotate.sig.keys" target="_blank" rel="noopener noreferrer">Signing Key Rotation Policy</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="tbl-beta-stage">Client Secrets Hashed in Configuration</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="tbl-header tbl-beta-stage">GA <sup>1</sup></td>
|
||||
<td class="tbl-beta-stage">General Availability after previous stages are vetted for bug fixes</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="2" class="tbl-header">misc</td>
|
||||
<td>List of other features that may be implemented</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="tbl-beta-stage"><a href="https://openid.net/specs/openid-connect-frontchannel-1_0.html" target="_blank" rel="noopener noreferrer">Front-Channel Logout</a> <sup>2</sup></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
*<sup>1</sup> this stage has not been implemented as of yet*
|
||||
|
||||
*<sup>2</sup> this individual feature has not been implemented as of yet*
|
||||
|
||||
## Configuration
|
||||
|
||||
```yaml
|
||||
identity_providers:
|
||||
oidc:
|
||||
hmac_secret: this_is_a_secret_abc123abc123abc
|
||||
issuer_private_key: |
|
||||
--- KEY START
|
||||
--- KEY END
|
||||
clients:
|
||||
- id: myapp
|
||||
description: My Application
|
||||
secret: this_is_a_secret
|
||||
authorization_policy: two_factor
|
||||
redirect_uris:
|
||||
- https://oidc.example.com:8080/oauth2/callback
|
||||
scopes:
|
||||
- openid
|
||||
- groups
|
||||
- email
|
||||
- profile
|
||||
grant_types:
|
||||
- refresh_token
|
||||
- authorization_code
|
||||
response_types:
|
||||
- code
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
### hmac_secret
|
||||
|
||||
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.
|
||||
|
||||
Can also be defined using a [secret](../secrets.md) which is the recommended for containerized deployments.
|
||||
|
||||
### issuer_private_key
|
||||
|
||||
The private key in DER base64 encoded PEM format used to encrypt the [OpenID Connect] JWT's.
|
||||
|
||||
Can also be defined using a [secret](../secrets.md) which is the recommended for containerized deployments.
|
||||
|
||||
### clients
|
||||
|
||||
A list of clients to configure. The options for each client are described below.
|
||||
|
||||
#### id
|
||||
|
||||
The Client ID for this client. Must be configured in the application consuming this client.
|
||||
|
||||
#### description
|
||||
|
||||
A friendly description for this client shown in the UI. This defaults to the same as the ID.
|
||||
|
||||
#### secret
|
||||
|
||||
The shared secret between Authelia and the application consuming this client. Currently this is stored in plain text.
|
||||
|
||||
#### authorization_policy
|
||||
|
||||
The authorization policy for this client. Either `one_factor` or `two_factor`.
|
||||
|
||||
#### redirect_uris
|
||||
|
||||
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.
|
||||
|
||||
#### scopes
|
||||
|
||||
A list of scopes to allow this client to consume. See [scope definitions](#scope-definitions) for more information.
|
||||
|
||||
#### grant_types
|
||||
|
||||
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.
|
||||
|
||||
#### response_types
|
||||
|
||||
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.
|
||||
|
||||
## Scope Definitions
|
||||
|
||||
### openid
|
||||
|
||||
This is the default scope for openid. This field is forced on every client by the configuration
|
||||
validation that Authelia does.
|
||||
|
||||
|JWT Field|JWT Type |Authelia Attribute|Description |
|
||||
|:-------:|:-----------:|:----------------:|:--------------------------------------:|
|
||||
|sub |string |Username |The username the user used to login with|
|
||||
|scope |string |scopes |Granted scopes (space delimited) |
|
||||
|scp |array[string]|scopes |Granted scopes |
|
||||
|iss |string |hostname |The issuer name, determined by URL |
|
||||
|at_hash |string |_N/A_ |Access Token Hash |
|
||||
|auth_time|number |_N/A_ |Authorize Time |
|
||||
|aud |array[string]|_N/A_ |Audience |
|
||||
|exp |number |_N/A_ |Expires |
|
||||
|iat |number |_N/A_ |Issued At |
|
||||
|rat |number |_N/A_ |Requested At |
|
||||
|jti |string(uuid) |_N/A_ |JWT Identifier |
|
||||
|
||||
### groups
|
||||
|
||||
This scope includes the groups the authentication backend reports the user is a member of in the token.
|
||||
|
||||
|JWT Field|JWT Type |Authelia Attribute|Description |
|
||||
|:-------:|:-----------:|:----------------:|:--------------------:|
|
||||
|groups |array[string]|Groups |The users display name|
|
||||
|
||||
### email
|
||||
|
||||
This scope includes the email information the authentication backend reports about the user in the token.
|
||||
|
||||
|JWT Field |JWT Type|Authelia Attribute|Description |
|
||||
|:------------:|:------:|:----------------:|:-------------------------------------------------------:|
|
||||
|email |string |email[0] |The first email in the list of emails |
|
||||
|email_verified|bool |_N/A_ |If the email is verified, assumed true for the time being|
|
||||
|
||||
### profile
|
||||
|
||||
This scope includes the profile information the authentication backend reports about the user in the token.
|
||||
|
||||
|JWT Field|JWT Type|Authelia Attribute|Description |
|
||||
|:-------:|:------:|:----------------:|:--------------------:|
|
||||
|name |string | display_name |The users display name|
|
||||
|
||||
|
||||
[OpenID Connect]: https://openid.net/connect/
|
|
@ -40,7 +40,7 @@ so that you can test it in minutes. Let's begin with the
|
|||
|
||||
## However, Authelia...
|
||||
|
||||
* is not an OAuth or OpenID Connect provider yet (planned in the [roadmap](./roadmap.md))
|
||||
* [OpenID Connect](./configuration/identity-providers/oidc.md) is still in preview.
|
||||
* is not a SAML provider yet.
|
||||
* does not support authentication against an OAuth or OpenID Connect provider yet.
|
||||
* does not support authentication against a SAML provider yet.
|
||||
|
|
|
@ -14,7 +14,9 @@ ideas and plans with you.
|
|||
|
||||
Below are the prioritised roadmap items:
|
||||
|
||||
1. [Authelia acts as an OpenID Connect Provider](https://github.com/authelia/authelia/issues/189). This is a high
|
||||
1. **[In Preview](./configuration/identity-providers/oidc.md)** *this roadmap item is in preview status, more
|
||||
information can be found in the docs*.
|
||||
[Authelia acts as an OpenID Connect Provider](https://github.com/authelia/authelia/issues/189). This is a high
|
||||
priority because currently the only way to pass authentication information back to the protected app is through the
|
||||
use of HTTP headers as described
|
||||
[here](https://www.authelia.com/docs/deployment/supported-proxies/#how-can-the-backend-be-aware-of-the-authenticated-users)
|
||||
|
|
5
go.mod
5
go.mod
|
@ -17,9 +17,9 @@ require (
|
|||
github.com/go-sql-driver/mysql v1.6.0
|
||||
github.com/golang/mock v1.5.0
|
||||
github.com/jackc/pgx/v4 v4.11.0
|
||||
github.com/mattn/go-sqlite3 v1.14.7
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible
|
||||
github.com/ory/fosite v0.39.0
|
||||
github.com/otiai10/copy v1.5.1
|
||||
github.com/pelletier/go-toml v1.4.0 // indirect
|
||||
github.com/pquerna/otp v1.3.0
|
||||
github.com/simia-tech/crypt v0.5.0
|
||||
github.com/sirupsen/logrus v1.8.1
|
||||
|
@ -30,5 +30,6 @@ require (
|
|||
github.com/tstranex/u2f v1.0.0
|
||||
github.com/valyala/fasthttp v1.24.0
|
||||
golang.org/x/text v0.3.6
|
||||
gopkg.in/square/go-jose.v2 v2.5.1
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
)
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/authelia/authelia/internal/authentication"
|
||||
"github.com/authelia/authelia/internal/configuration/schema"
|
||||
)
|
||||
|
||||
|
@ -175,3 +176,17 @@ func domainToPrefixSuffix(domain string) (prefix, suffix string) {
|
|||
|
||||
return parts[0], strings.Join(parts[1:], ".")
|
||||
}
|
||||
|
||||
// IsAuthLevelSufficient returns true if the current authenticationLevel is above the authorizationLevel.
|
||||
func IsAuthLevelSufficient(authenticationLevel authentication.Level, authorizationLevel Level) bool {
|
||||
switch authorizationLevel {
|
||||
case Denied:
|
||||
return false
|
||||
case OneFactor:
|
||||
return authenticationLevel >= authentication.OneFactor
|
||||
case TwoFactor:
|
||||
return authenticationLevel >= authentication.TwoFactor
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/authelia/authelia/internal/authentication"
|
||||
"github.com/authelia/authelia/internal/configuration/schema"
|
||||
)
|
||||
|
||||
|
@ -182,3 +183,15 @@ func TestShouldParseACLNetworks(t *testing.T) {
|
|||
assert.Equal(t, fourthNetwork, networksCacheMap["fec0::1"])
|
||||
assert.Equal(t, fourthNetwork, networksCacheMap["fec0::1/128"])
|
||||
}
|
||||
|
||||
func TestShouldReturnCorrectValidationLevel(t *testing.T) {
|
||||
assert.True(t, IsAuthLevelSufficient(authentication.NotAuthenticated, Bypass))
|
||||
assert.True(t, IsAuthLevelSufficient(authentication.OneFactor, Bypass))
|
||||
assert.True(t, IsAuthLevelSufficient(authentication.TwoFactor, Bypass))
|
||||
assert.False(t, IsAuthLevelSufficient(authentication.NotAuthenticated, OneFactor))
|
||||
assert.True(t, IsAuthLevelSufficient(authentication.OneFactor, OneFactor))
|
||||
assert.True(t, IsAuthLevelSufficient(authentication.TwoFactor, OneFactor))
|
||||
assert.False(t, IsAuthLevelSufficient(authentication.NotAuthenticated, TwoFactor))
|
||||
assert.False(t, IsAuthLevelSufficient(authentication.OneFactor, TwoFactor))
|
||||
assert.True(t, IsAuthLevelSufficient(authentication.TwoFactor, TwoFactor))
|
||||
}
|
||||
|
|
|
@ -21,14 +21,14 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
host string
|
||||
validFrom string
|
||||
validFor time.Duration
|
||||
isCA bool
|
||||
rsaBits int
|
||||
ecdsaCurve string
|
||||
ed25519Key bool
|
||||
targetDirectory string
|
||||
host string
|
||||
validFrom string
|
||||
validFor time.Duration
|
||||
isCA bool
|
||||
rsaBits int
|
||||
ecdsaCurve string
|
||||
ed25519Key bool
|
||||
certificateTargetDirectory string
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
@ -45,7 +45,7 @@ func init() {
|
|||
CertificatesGenerateCmd.PersistentFlags().IntVar(&rsaBits, "rsa-bits", 2048, "Size of RSA key to generate. Ignored if --ecdsa-curve is set")
|
||||
CertificatesGenerateCmd.PersistentFlags().StringVar(&ecdsaCurve, "ecdsa-curve", "", "ECDSA curve to use to generate a key. Valid values are P224, P256 (recommended), P384, P521")
|
||||
CertificatesGenerateCmd.PersistentFlags().BoolVar(&ed25519Key, "ed25519", false, "Generate an Ed25519 key")
|
||||
CertificatesGenerateCmd.PersistentFlags().StringVar(&targetDirectory, "dir", "", "Target directory where the certificate and keys will be stored")
|
||||
CertificatesGenerateCmd.PersistentFlags().StringVar(&certificateTargetDirectory, "dir", "", "Target directory where the certificate and keys will be stored")
|
||||
|
||||
CertificatesCmd.AddCommand(CertificatesGenerateCmd)
|
||||
}
|
||||
|
@ -144,7 +144,7 @@ func generateSelfSignedCertificate(cmd *cobra.Command, args []string) {
|
|||
log.Fatalf("Failed to create certificate: %v", err)
|
||||
}
|
||||
|
||||
certPath := path.Join(targetDirectory, "cert.pem")
|
||||
certPath := path.Join(certificateTargetDirectory, "cert.pem")
|
||||
certOut, err := os.Create(certPath)
|
||||
|
||||
if err != nil {
|
||||
|
@ -161,7 +161,7 @@ func generateSelfSignedCertificate(cmd *cobra.Command, args []string) {
|
|||
|
||||
log.Printf("wrote %s\n", certPath)
|
||||
|
||||
keyPath := path.Join(targetDirectory, "key.pem")
|
||||
keyPath := path.Join(certificateTargetDirectory, "key.pem")
|
||||
keyOut, err := os.OpenFile(keyPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
|
||||
if err != nil {
|
||||
|
|
79
internal/commands/rsa.go
Normal file
79
internal/commands/rsa.go
Normal file
|
@ -0,0 +1,79 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/authelia/authelia/internal/utils"
|
||||
)
|
||||
|
||||
var rsaTargetDirectory string
|
||||
|
||||
func init() {
|
||||
RSAGenerateCmd.PersistentFlags().StringVar(&rsaTargetDirectory, "dir", "", "Target directory where the keypair will be stored")
|
||||
|
||||
RSACmd.AddCommand(RSAGenerateCmd)
|
||||
}
|
||||
|
||||
func generateRSAKeypair(cmd *cobra.Command, args []string) {
|
||||
privateKey, publicKey := utils.GenerateRsaKeyPair(2048)
|
||||
|
||||
keyPath := path.Join(rsaTargetDirectory, "key.pem")
|
||||
keyOut, err := os.OpenFile(keyPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to open %s for writing: %v", keyPath, err)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = keyOut.WriteString(utils.ExportRsaPrivateKeyAsPemStr(privateKey))
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to write private key: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := keyOut.Close(); err != nil {
|
||||
log.Fatalf("Unable to close private key file: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
keyPath = path.Join(rsaTargetDirectory, "key.pub")
|
||||
keyOut, err = os.OpenFile(keyPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to open %s for writing: %v", keyPath, err)
|
||||
return
|
||||
}
|
||||
|
||||
publicPem, err := utils.ExportRsaPublicKeyAsPemStr(publicKey)
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to marshal public key: %v", err)
|
||||
}
|
||||
|
||||
_, err = keyOut.WriteString(publicPem)
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to write private key: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := keyOut.Close(); err != nil {
|
||||
log.Fatalf("Unable to close public key file: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// RSACmd RSA helper command.
|
||||
var RSACmd = &cobra.Command{
|
||||
Use: "rsa",
|
||||
Short: "Commands related to rsa keypair generation",
|
||||
}
|
||||
|
||||
// RSAGenerateCmd certificate generation command.
|
||||
var RSAGenerateCmd = &cobra.Command{
|
||||
Use: "generate",
|
||||
Short: "Generate a RSA keypair",
|
||||
Run: generateRSAKeypair,
|
||||
}
|
|
@ -553,4 +553,62 @@ notifier:
|
|||
# sender: admin@example.com
|
||||
# host: smtp.gmail.com
|
||||
# port: 587
|
||||
|
||||
##
|
||||
## Identity Providers
|
||||
##
|
||||
# identity_providers:
|
||||
|
||||
##
|
||||
## OpenID Connect (Identity Provider)
|
||||
##
|
||||
## It's recommended you read the documentation before configuration of this section:
|
||||
## https://www.authelia.com/docs/configuration/identity-providers/oidc.html
|
||||
# oidc:
|
||||
## The hmac_secret is used to sign OAuth2 tokens (authorization code, access tokens and refresh tokens).
|
||||
## HMAC Secret can also be set using a secret: https://www.authelia.com/docs/configuration/secrets.html
|
||||
# hmac_secret: this_is_a_secret_abc123abc123abc
|
||||
|
||||
## The issuer_private_key is used to sign the JWT forged by OpenID Connect.
|
||||
## Issuer Private Key can also be set using a secret: https://docs.authelia.com/configuration/secrets.html
|
||||
# issuer_private_key: |
|
||||
# --- KEY START
|
||||
# --- KEY END
|
||||
|
||||
## Clients is a list of known clients and their configuration.
|
||||
# clients:
|
||||
# -
|
||||
## The ID is the OpenID Connect ClientID which is used to link an application to a configuration.
|
||||
# id: myapp
|
||||
|
||||
## The description to show to users when they end up on the consent screen. Defaults to the ID above.
|
||||
# description: My Application
|
||||
|
||||
## The client secret is a shared secret between Authelia and the consumer of this client.
|
||||
# secret: this_is_a_secret
|
||||
|
||||
## The policy to require for this client; one_factor or two_factor.
|
||||
# authorization_policy: two_factor
|
||||
|
||||
## Redirect URI's specifies a list of valid case-sensitive callbacks for this client.
|
||||
# redirect_uris:
|
||||
# - https://oidc.example.com:8080/oauth2/callback
|
||||
|
||||
## Scopes defines the valid scopes this client can request
|
||||
# scopes:
|
||||
# - openid
|
||||
# - groups
|
||||
# - email
|
||||
# - profile
|
||||
|
||||
## Grant Types configures which grants this client can obtain.
|
||||
## It's not recommended to define this unless you know what you're doing.
|
||||
# grant_types:
|
||||
# - refresh_token
|
||||
# - "authorization_code
|
||||
|
||||
## Response Types configures which responses this client can be sent.
|
||||
## It's not recommended to define this unless you know what you're doing.
|
||||
# response_types:
|
||||
# - code
|
||||
...
|
||||
|
|
|
@ -14,6 +14,7 @@ type Configuration struct {
|
|||
JWTSecret string `mapstructure:"jwt_secret"`
|
||||
DefaultRedirectionURL string `mapstructure:"default_redirection_url"`
|
||||
|
||||
IdentityProviders IdentityProvidersConfiguration `mapstructure:"identity_providers"`
|
||||
AuthenticationBackend AuthenticationBackendConfiguration `mapstructure:"authentication_backend"`
|
||||
Session SessionConfiguration `mapstructure:"session"`
|
||||
TOTP *TOTPConfiguration `mapstructure:"totp"`
|
||||
|
|
35
internal/configuration/schema/identity_providers.go
Normal file
35
internal/configuration/schema/identity_providers.go
Normal file
|
@ -0,0 +1,35 @@
|
|||
package schema
|
||||
|
||||
// IdentityProvidersConfiguration represents the IdentityProviders 2.0 configuration for Authelia.
|
||||
type IdentityProvidersConfiguration struct {
|
||||
OIDC *OpenIDConnectConfiguration `mapstructure:"oidc"`
|
||||
}
|
||||
|
||||
// OpenIDConnectConfiguration configuration for OpenID Connect.
|
||||
type OpenIDConnectConfiguration struct {
|
||||
// This secret must be 32 bytes long
|
||||
HMACSecret string `mapstructure:"hmac_secret"`
|
||||
IssuerPrivateKey string `mapstructure:"issuer_private_key"`
|
||||
|
||||
Clients []OpenIDConnectClientConfiguration `mapstructure:"clients"`
|
||||
}
|
||||
|
||||
// OpenIDConnectClientConfiguration configuration for an OpenID Connect client.
|
||||
type OpenIDConnectClientConfiguration struct {
|
||||
ID string `mapstructure:"id"`
|
||||
Description string `mapstructure:"description"`
|
||||
Secret string `mapstructure:"secret"`
|
||||
RedirectURIs []string `mapstructure:"redirect_uris"`
|
||||
Policy string `mapstructure:"authorization_policy"`
|
||||
Scopes []string `mapstructure:"scopes"`
|
||||
GrantTypes []string `mapstructure:"grant_types"`
|
||||
ResponseTypes []string `mapstructure:"response_types"`
|
||||
}
|
||||
|
||||
// DefaultOpenIDConnectClientConfiguration contains defaults for OIDC AutheliaClients.
|
||||
var DefaultOpenIDConnectClientConfiguration = OpenIDConnectClientConfiguration{
|
||||
Scopes: []string{"openid", "groups", "profile", "email"},
|
||||
ResponseTypes: []string{"code"},
|
||||
GrantTypes: []string{"refresh_token", "authorization_code"},
|
||||
Policy: "two_factor",
|
||||
}
|
|
@ -91,4 +91,6 @@ func ValidateConfiguration(configuration *schema.Configuration, validator *schem
|
|||
} else {
|
||||
ValidateNotifier(configuration.Notifier, validator)
|
||||
}
|
||||
|
||||
ValidateIdentityProviders(&configuration.IdentityProviders, validator)
|
||||
}
|
||||
|
|
|
@ -7,6 +7,11 @@ const (
|
|||
errFmtSessionRedisHostOrNodesRequired = "Either the host or a node must be provided when using the %s session provider"
|
||||
errFmtReplacedConfigurationKey = "invalid configuration key '%s' was replaced by '%s'"
|
||||
|
||||
errOAuthOIDCServerClientRedirectURIFmt = "OIDC Server Client redirect URI %s has an invalid scheme %s, should be http or https"
|
||||
errOAuthOIDCServerClientRedirectURICantBeParsedFmt = "OIDC Client with ID '%s' has an invalid redirect URI '%s' could not be parsed: %v"
|
||||
errIdentityProvidersOIDCServerClientInvalidPolicyFmt = "OIDC Client with ID '%s' has an invalid policy '%s', should be either 'one_factor' or 'two_factor'"
|
||||
errIdentityProvidersOIDCServerClientInvalidSecFmt = "OIDC Client with ID '%s' has an empty secret"
|
||||
|
||||
errFileHashing = "config key incorrect: authentication_backend.file.hashing 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"
|
||||
|
@ -42,15 +47,17 @@ var validRequestMethods = []string{"GET", "HEAD", "POST", "PUT", "PATCH", "DELET
|
|||
|
||||
// SecretNames contains a map of secret names.
|
||||
var SecretNames = map[string]string{
|
||||
"JWTSecret": "jwt_secret",
|
||||
"SessionSecret": "session.secret",
|
||||
"DUOSecretKey": "duo_api.secret_key",
|
||||
"RedisPassword": "session.redis.password",
|
||||
"RedisSentinelPassword": "session.redis.high_availability.sentinel_password",
|
||||
"LDAPPassword": "authentication_backend.ldap.password",
|
||||
"SMTPPassword": "notifier.smtp.password",
|
||||
"MySQLPassword": "storage.mysql.password",
|
||||
"PostgreSQLPassword": "storage.postgres.password",
|
||||
"JWTSecret": "jwt_secret",
|
||||
"SessionSecret": "session.secret",
|
||||
"DUOSecretKey": "duo_api.secret_key",
|
||||
"RedisPassword": "session.redis.password",
|
||||
"RedisSentinelPassword": "session.redis.high_availability.sentinel_password",
|
||||
"LDAPPassword": "authentication_backend.ldap.password",
|
||||
"SMTPPassword": "notifier.smtp.password",
|
||||
"MySQLPassword": "storage.mysql.password",
|
||||
"PostgreSQLPassword": "storage.postgres.password",
|
||||
"OpenIDConnectHMACSecret": "identity_providers.oidc.hmac_secret",
|
||||
"OpenIDConnectIssuerPrivateKey": "identity_providers.oidc.issuer_private_key",
|
||||
}
|
||||
|
||||
// validKeys is a list of valid keys that are not secret names. For the sake of consistency please place any secret in
|
||||
|
@ -184,6 +191,9 @@ var validKeys = []string{
|
|||
"authentication_backend.file.password.salt_length",
|
||||
"authentication_backend.file.password.memory",
|
||||
"authentication_backend.file.password.parallelism",
|
||||
|
||||
// Identity Provider Keys.
|
||||
"identity_providers.oidc.clients",
|
||||
}
|
||||
|
||||
var replacedKeys = map[string]string{
|
||||
|
|
98
internal/configuration/validator/identity_providers.go
Normal file
98
internal/configuration/validator/identity_providers.go
Normal file
|
@ -0,0 +1,98 @@
|
|||
package validator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/authelia/authelia/internal/configuration/schema"
|
||||
"github.com/authelia/authelia/internal/utils"
|
||||
)
|
||||
|
||||
// ValidateIdentityProviders validates and update IdentityProviders configuration.
|
||||
func ValidateIdentityProviders(configuration *schema.IdentityProvidersConfiguration, validator *schema.StructValidator) {
|
||||
validateOIDC(configuration.OIDC, validator)
|
||||
}
|
||||
|
||||
func validateOIDC(configuration *schema.OpenIDConnectConfiguration, validator *schema.StructValidator) {
|
||||
if configuration != nil {
|
||||
if configuration.IssuerPrivateKey == "" {
|
||||
validator.Push(fmt.Errorf("OIDC Server issuer private key must be provided"))
|
||||
}
|
||||
|
||||
validateOIDCClients(configuration, validator)
|
||||
|
||||
if len(configuration.Clients) == 0 {
|
||||
validator.Push(fmt.Errorf("OIDC Server has no clients defined"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func validateOIDCClients(configuration *schema.OpenIDConnectConfiguration, validator *schema.StructValidator) {
|
||||
invalidID, duplicateIDs := false, false
|
||||
|
||||
var ids []string
|
||||
|
||||
for c, client := range configuration.Clients {
|
||||
if client.ID == "" {
|
||||
invalidID = true
|
||||
} else {
|
||||
if client.Description == "" {
|
||||
configuration.Clients[c].Description = client.ID
|
||||
}
|
||||
|
||||
if utils.IsStringInSliceFold(client.ID, ids) {
|
||||
duplicateIDs = true
|
||||
}
|
||||
ids = append(ids, client.ID)
|
||||
}
|
||||
|
||||
if client.Secret == "" {
|
||||
validator.Push(fmt.Errorf(errIdentityProvidersOIDCServerClientInvalidSecFmt, client.ID))
|
||||
}
|
||||
|
||||
if client.Policy == "" {
|
||||
configuration.Clients[c].Policy = schema.DefaultOpenIDConnectClientConfiguration.Policy
|
||||
} else if client.Policy != oneFactorPolicy && client.Policy != twoFactorPolicy {
|
||||
validator.Push(fmt.Errorf(errIdentityProvidersOIDCServerClientInvalidPolicyFmt, client.ID, client.Policy))
|
||||
}
|
||||
|
||||
if len(client.Scopes) == 0 {
|
||||
configuration.Clients[c].Scopes = schema.DefaultOpenIDConnectClientConfiguration.Scopes
|
||||
} else if !utils.IsStringInSlice("openid", client.Scopes) {
|
||||
configuration.Clients[c].Scopes = append(configuration.Clients[c].Scopes, "openid")
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
if invalidID {
|
||||
validator.Push(fmt.Errorf("OIDC Server has one or more clients with an empty ID"))
|
||||
}
|
||||
|
||||
if duplicateIDs {
|
||||
validator.Push(fmt.Errorf("OIDC Server has clients with duplicate ID's"))
|
||||
}
|
||||
}
|
||||
|
||||
func validateOIDCClientRedirectURIs(client schema.OpenIDConnectClientConfiguration, validator *schema.StructValidator) {
|
||||
for _, redirectURI := range client.RedirectURIs {
|
||||
parsedURI, err := url.Parse(redirectURI)
|
||||
|
||||
if err != nil {
|
||||
validator.Push(fmt.Errorf(errOAuthOIDCServerClientRedirectURICantBeParsedFmt, client.ID, redirectURI, err))
|
||||
break
|
||||
}
|
||||
|
||||
if parsedURI.Scheme != "https" && parsedURI.Scheme != "http" {
|
||||
validator.Push(fmt.Errorf(errOAuthOIDCServerClientRedirectURIFmt, redirectURI, parsedURI.Scheme))
|
||||
}
|
||||
}
|
||||
}
|
172
internal/configuration/validator/identity_providers_test.go
Normal file
172
internal/configuration/validator/identity_providers_test.go
Normal file
|
@ -0,0 +1,172 @@
|
|||
package validator
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/authelia/authelia/internal/configuration/schema"
|
||||
)
|
||||
|
||||
func TestShouldRaiseErrorWhenInvalidOIDCServerConfiguration(t *testing.T) {
|
||||
validator := schema.NewStructValidator()
|
||||
config := &schema.IdentityProvidersConfiguration{
|
||||
OIDC: &schema.OpenIDConnectConfiguration{
|
||||
HMACSecret: "abc",
|
||||
IssuerPrivateKey: "",
|
||||
},
|
||||
}
|
||||
|
||||
ValidateIdentityProviders(config, validator)
|
||||
|
||||
require.Len(t, validator.Errors(), 2)
|
||||
|
||||
assert.EqualError(t, validator.Errors()[0], "OIDC Server issuer private key must be provided")
|
||||
assert.EqualError(t, validator.Errors()[1], "OIDC Server has no clients defined")
|
||||
}
|
||||
|
||||
func TestShouldRaiseErrorWhenOIDCServerIssuerPrivateKeyPathInvalid(t *testing.T) {
|
||||
validator := schema.NewStructValidator()
|
||||
config := &schema.IdentityProvidersConfiguration{
|
||||
OIDC: &schema.OpenIDConnectConfiguration{
|
||||
HMACSecret: "rLABDrx87et5KvRHVUgTm3pezWWd8LMN",
|
||||
IssuerPrivateKey: "key-material",
|
||||
},
|
||||
}
|
||||
|
||||
ValidateIdentityProviders(config, validator)
|
||||
|
||||
require.Len(t, validator.Errors(), 1)
|
||||
|
||||
assert.EqualError(t, validator.Errors()[0], "OIDC Server has no clients defined")
|
||||
}
|
||||
|
||||
func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) {
|
||||
validator := schema.NewStructValidator()
|
||||
config := &schema.IdentityProvidersConfiguration{
|
||||
OIDC: &schema.OpenIDConnectConfiguration{
|
||||
HMACSecret: "rLABDrx87et5KvRHVUgTm3pezWWd8LMN",
|
||||
IssuerPrivateKey: "key-material",
|
||||
Clients: []schema.OpenIDConnectClientConfiguration{
|
||||
{
|
||||
ID: "",
|
||||
Secret: "",
|
||||
Policy: "",
|
||||
RedirectURIs: []string{
|
||||
"tcp://google.com",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "a-client",
|
||||
Secret: "a-secret",
|
||||
Policy: "a-policy",
|
||||
RedirectURIs: []string{
|
||||
"https://google.com",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "a-client",
|
||||
Secret: "a-secret",
|
||||
Policy: "a-policy",
|
||||
RedirectURIs: []string{
|
||||
"https://google.com",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "client-check-uri-parse",
|
||||
Secret: "a-secret",
|
||||
Policy: twoFactorPolicy,
|
||||
RedirectURIs: []string{
|
||||
"http://abc@%two",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ValidateIdentityProviders(config, validator)
|
||||
|
||||
require.Len(t, validator.Errors(), 7)
|
||||
|
||||
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()[1], fmt.Sprintf(errOAuthOIDCServerClientRedirectURIFmt, "tcp://google.com", "tcp"))
|
||||
assert.EqualError(t, validator.Errors()[2], fmt.Sprintf(errIdentityProvidersOIDCServerClientInvalidPolicyFmt, "a-client", "a-policy"))
|
||||
assert.EqualError(t, validator.Errors()[3], fmt.Sprintf(errIdentityProvidersOIDCServerClientInvalidPolicyFmt, "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()[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")
|
||||
}
|
||||
|
||||
func TestShouldNotRaiseErrorWhenOIDCServerConfiguredCorrectly(t *testing.T) {
|
||||
validator := schema.NewStructValidator()
|
||||
config := &schema.IdentityProvidersConfiguration{
|
||||
OIDC: &schema.OpenIDConnectConfiguration{
|
||||
HMACSecret: "rLABDrx87et5KvRHVUgTm3pezWWd8LMN",
|
||||
IssuerPrivateKey: "../../../README.md",
|
||||
Clients: []schema.OpenIDConnectClientConfiguration{
|
||||
{
|
||||
ID: "a-client",
|
||||
Secret: "a-client-secret",
|
||||
Policy: oneFactorPolicy,
|
||||
RedirectURIs: []string{
|
||||
"https://google.com",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "b-client",
|
||||
Description: "Normal Description",
|
||||
Secret: "b-client-secret",
|
||||
Policy: oneFactorPolicy,
|
||||
RedirectURIs: []string{
|
||||
"https://google.com",
|
||||
},
|
||||
Scopes: []string{
|
||||
"groups",
|
||||
},
|
||||
GrantTypes: []string{
|
||||
"refresh_token",
|
||||
},
|
||||
ResponseTypes: []string{
|
||||
"token",
|
||||
"code",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ValidateIdentityProviders(config, validator)
|
||||
|
||||
assert.Len(t, validator.Errors(), 0)
|
||||
|
||||
assert.Equal(t, config.OIDC.Clients[0].ID, config.OIDC.Clients[0].Description)
|
||||
assert.Equal(t, "Normal Description", config.OIDC.Clients[1].Description)
|
||||
|
||||
require.Len(t, config.OIDC.Clients[0].Scopes, 4)
|
||||
assert.Equal(t, "openid", config.OIDC.Clients[0].Scopes[0])
|
||||
assert.Equal(t, "groups", config.OIDC.Clients[0].Scopes[1])
|
||||
assert.Equal(t, "profile", config.OIDC.Clients[0].Scopes[2])
|
||||
assert.Equal(t, "email", config.OIDC.Clients[0].Scopes[3])
|
||||
|
||||
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])
|
||||
|
||||
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)
|
||||
assert.Equal(t, "groups", config.OIDC.Clients[1].Scopes[0])
|
||||
assert.Equal(t, "openid", config.OIDC.Clients[1].Scopes[1])
|
||||
|
||||
require.Len(t, config.OIDC.Clients[1].GrantTypes, 1)
|
||||
assert.Equal(t, "refresh_token", config.OIDC.Clients[1].GrantTypes[0])
|
||||
|
||||
require.Len(t, config.OIDC.Clients[1].ResponseTypes, 2)
|
||||
assert.Equal(t, "token", config.OIDC.Clients[1].ResponseTypes[0])
|
||||
assert.Equal(t, "code", config.OIDC.Clients[1].ResponseTypes[1])
|
||||
}
|
|
@ -58,6 +58,11 @@ func ValidateSecrets(configuration *schema.Configuration, validator *schema.Stru
|
|||
if configuration.Storage.PostgreSQL != nil {
|
||||
configuration.Storage.PostgreSQL.Password = getSecretValue(SecretNames["PostgreSQLPassword"], validator, viper)
|
||||
}
|
||||
|
||||
if configuration.IdentityProviders.OIDC != nil {
|
||||
configuration.IdentityProviders.OIDC.HMACSecret = getSecretValue(SecretNames["OpenIDConnectHMACSecret"], validator, viper)
|
||||
configuration.IdentityProviders.OIDC.IssuerPrivateKey = getSecretValue(SecretNames["OpenIDConnectIssuerPrivateKey"], validator, viper)
|
||||
}
|
||||
}
|
||||
|
||||
func getSecretValue(name string, validator *schema.StructValidator, viper *viper.Viper) string {
|
||||
|
@ -75,7 +80,8 @@ func getSecretValue(name string, validator *schema.StructValidator, viper *viper
|
|||
if err != nil {
|
||||
validator.Push(fmt.Errorf("error loading secret file (%s): %s", name, err))
|
||||
} else {
|
||||
return strings.ReplaceAll(string(content), "\n", "")
|
||||
// TODO: Test this functionality.
|
||||
return strings.TrimRight(string(content), "\n")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -25,8 +25,6 @@ const remoteNameHeader = "Remote-Name"
|
|||
const remoteEmailHeader = "Remote-Email"
|
||||
const remoteGroupsHeader = "Remote-Groups"
|
||||
|
||||
var protoHostSeparator = []byte("://")
|
||||
|
||||
const (
|
||||
// Forbidden means the user is forbidden the access to a resource.
|
||||
Forbidden authorizationMatching = iota
|
||||
|
@ -57,3 +55,30 @@ const testUsername = "john"
|
|||
const movingAverageWindow = 10
|
||||
const msMinimumDelay1FA = float64(250)
|
||||
const msMaximumRandomDelay = int64(85)
|
||||
|
||||
// OIDC constants.
|
||||
const (
|
||||
oidcWellKnownPath = "/.well-known/openid-configuration"
|
||||
oidcJWKsPath = "/api/oidc/jwks"
|
||||
oidcAuthorizePath = "/api/oidc/authorize"
|
||||
oidcTokenPath = "/api/oidc/token" //nolint:gosec // This is not a hard coded credential, it's a path.
|
||||
oidcIntrospectPath = "/api/oidc/introspect"
|
||||
oidcRevokePath = "/api/oidc/revoke"
|
||||
|
||||
// Note: If you change this const you must also do so in the frontend at web/src/services/Api.ts.
|
||||
oidcConsentPath = "/api/oidc/consent"
|
||||
)
|
||||
|
||||
const (
|
||||
accept = "accept"
|
||||
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{}
|
||||
|
|
|
@ -127,8 +127,12 @@ func FirstFactorPost(msInitialDelay time.Duration, delayEnabled bool) middleware
|
|||
|
||||
ctx.Logger.Debugf("Credentials validation of user %s is ok", bodyJSON.Username)
|
||||
|
||||
// Reset all values from previous session before regenerating the cookie.
|
||||
err = ctx.SaveSession(session.NewDefaultUserSession())
|
||||
userSession := ctx.GetSession()
|
||||
newSession := session.NewDefaultUserSession()
|
||||
newSession.OIDCWorkflowSession = userSession.OIDCWorkflowSession
|
||||
|
||||
// Reset all values from previous session except OIDC workflow before regenerating the cookie.
|
||||
err = ctx.SaveSession(newSession)
|
||||
|
||||
if err != nil {
|
||||
handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to reset the session for user %s: %s", bodyJSON.Username, err.Error()), authenticationFailedMessage)
|
||||
|
@ -165,7 +169,6 @@ 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)
|
||||
|
||||
// And set those information in the new session.
|
||||
userSession := ctx.GetSession()
|
||||
userSession.Username = userDetails.Username
|
||||
userSession.DisplayName = userDetails.DisplayName
|
||||
userSession.Groups = userDetails.Groups
|
||||
|
@ -188,6 +191,10 @@ func FirstFactorPost(msInitialDelay time.Duration, delayEnabled bool) middleware
|
|||
|
||||
successful = true
|
||||
|
||||
Handle1FAResponse(ctx, bodyJSON.TargetURL, bodyJSON.RequestMethod, userSession.Username, userSession.Groups)
|
||||
if userSession.OIDCWorkflowSession != nil {
|
||||
HandleOIDCWorkflowResponse(ctx)
|
||||
} else {
|
||||
Handle1FAResponse(ctx, bodyJSON.TargetURL, bodyJSON.RequestMethod, userSession.Username, userSession.Groups)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
129
internal/handlers/handler_oidc_authorize.go
Normal file
129
internal/handlers/handler_oidc_authorize.go
Normal file
|
@ -0,0 +1,129 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/ory/fosite"
|
||||
|
||||
"github.com/authelia/authelia/internal/logging"
|
||||
"github.com/authelia/authelia/internal/middlewares"
|
||||
"github.com/authelia/authelia/internal/oidc"
|
||||
"github.com/authelia/authelia/internal/session"
|
||||
)
|
||||
|
||||
func oidcAuthorize(ctx *middlewares.AutheliaCtx, rw http.ResponseWriter, r *http.Request) {
|
||||
ar, err := ctx.Providers.OpenIDConnect.Fosite.NewAuthorizeRequest(ctx, r)
|
||||
if err != nil {
|
||||
logging.Logger().Errorf("Error occurred in NewAuthorizeRequest: %+v", err)
|
||||
ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, ar, err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
clientID := ar.GetClient().GetID()
|
||||
client, err := ctx.Providers.OpenIDConnect.Store.GetInternalClient(clientID)
|
||||
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Unable to find related client configuration with name '%s': %v", ar.GetID(), err)
|
||||
ctx.Logger.Error(err)
|
||||
ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, ar, err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
userSession := ctx.GetSession()
|
||||
|
||||
requestedScopes := ar.GetRequestedScopes()
|
||||
requestedAudience := ar.GetRequestedAudience()
|
||||
|
||||
isAuthInsufficient := !client.IsAuthenticationLevelSufficient(userSession.AuthenticationLevel)
|
||||
|
||||
if isAuthInsufficient || (isConsentMissing(userSession.OIDCWorkflowSession, requestedScopes, requestedAudience)) {
|
||||
oidcAuthorizeHandleAuthorizationOrConsentInsufficient(ctx, userSession, client, isAuthInsufficient, rw, r, ar)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
for _, scope := range requestedScopes {
|
||||
ar.GrantScope(scope)
|
||||
}
|
||||
|
||||
for _, a := range requestedAudience {
|
||||
ar.GrantAudience(a)
|
||||
}
|
||||
|
||||
userSession.OIDCWorkflowSession = nil
|
||||
if err := ctx.SaveSession(userSession); err != nil {
|
||||
ctx.Logger.Errorf("%v", err)
|
||||
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
oauthSession, err := newOIDCSession(ctx, ar)
|
||||
if err != nil {
|
||||
ctx.Logger.Errorf("Error occurred in NewOIDCSession: %+v", err)
|
||||
ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, ar, err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
response, err := ctx.Providers.OpenIDConnect.Fosite.NewAuthorizeResponse(ctx, ar, oauthSession)
|
||||
if err != nil {
|
||||
ctx.Logger.Errorf("Error occurred in NewAuthorizeResponse: %+v", err)
|
||||
ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, ar, err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeResponse(rw, ar, response)
|
||||
}
|
||||
|
||||
func oidcAuthorizeHandleAuthorizationOrConsentInsufficient(
|
||||
ctx *middlewares.AutheliaCtx, userSession session.UserSession, client *oidc.InternalClient, isAuthInsufficient bool,
|
||||
rw http.ResponseWriter, r *http.Request,
|
||||
ar fosite.AuthorizeRequester) {
|
||||
forwardedProtoHost, err := ctx.ForwardedProtoHost()
|
||||
if err != nil {
|
||||
ctx.Logger.Errorf("%v", err)
|
||||
http.Error(rw, err.Error(), http.StatusBadRequest)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
redirectURL := fmt.Sprintf("%s%s", forwardedProtoHost, string(ctx.Request.RequestURI()))
|
||||
|
||||
ctx.Logger.Debugf("User %s must consent with scopes %s",
|
||||
userSession.Username, strings.Join(ar.GetRequestedScopes(), ", "))
|
||||
|
||||
userSession.OIDCWorkflowSession = new(session.OIDCWorkflowSession)
|
||||
userSession.OIDCWorkflowSession.ClientID = client.ID
|
||||
userSession.OIDCWorkflowSession.RequestedScopes = ar.GetRequestedScopes()
|
||||
userSession.OIDCWorkflowSession.RequestedAudience = ar.GetRequestedAudience()
|
||||
userSession.OIDCWorkflowSession.AuthURI = redirectURL
|
||||
userSession.OIDCWorkflowSession.TargetURI = ar.GetRedirectURI().String()
|
||||
userSession.OIDCWorkflowSession.RequiredAuthorizationLevel = client.Policy
|
||||
|
||||
if err := ctx.SaveSession(userSession); err != nil {
|
||||
ctx.Logger.Errorf("%v", err)
|
||||
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
uri, err := ctx.ForwardedProtoHost()
|
||||
if err != nil {
|
||||
ctx.Logger.Errorf("%v", err)
|
||||
http.Error(rw, err.Error(), http.StatusBadRequest)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if isAuthInsufficient {
|
||||
http.Redirect(rw, r, uri, http.StatusFound)
|
||||
} else {
|
||||
http.Redirect(rw, r, fmt.Sprintf("%s/consent", uri), http.StatusFound)
|
||||
}
|
||||
}
|
124
internal/handlers/handler_oidc_consent.go
Normal file
124
internal/handlers/handler_oidc_consent.go
Normal file
|
@ -0,0 +1,124 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/authelia/authelia/internal/middlewares"
|
||||
)
|
||||
|
||||
func oidcConsent(ctx *middlewares.AutheliaCtx) {
|
||||
userSession := ctx.GetSession()
|
||||
|
||||
if userSession.OIDCWorkflowSession == nil {
|
||||
ctx.Logger.Debugf("Cannot consent for user %s when OIDC workflow has not been initiated", userSession.Username)
|
||||
ctx.ReplyForbidden()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
clientID := userSession.OIDCWorkflowSession.ClientID
|
||||
client, err := ctx.Providers.OpenIDConnect.Store.GetInternalClient(clientID)
|
||||
|
||||
if err != nil {
|
||||
ctx.Logger.Debugf("Unable to find related client configuration with name '%s': %v", clientID, err)
|
||||
ctx.ReplyForbidden()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if !client.IsAuthenticationLevelSufficient(userSession.AuthenticationLevel) {
|
||||
ctx.Logger.Debugf("Insufficient permissions to give consent v2 %d -> %d", userSession.AuthenticationLevel, userSession.OIDCWorkflowSession.RequiredAuthorizationLevel)
|
||||
ctx.ReplyForbidden()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var body ConsentGetResponseBody
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
func oidcConsentPOST(ctx *middlewares.AutheliaCtx) {
|
||||
userSession := ctx.GetSession()
|
||||
|
||||
if userSession.OIDCWorkflowSession == nil {
|
||||
ctx.Logger.Debugf("Cannot consent for user %s when OIDC workflow has not been initiated", userSession.Username)
|
||||
ctx.ReplyForbidden()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
client, err := ctx.Providers.OpenIDConnect.Store.GetInternalClient(userSession.OIDCWorkflowSession.ClientID)
|
||||
|
||||
if err != nil {
|
||||
ctx.Logger.Debugf("Unable to find related client configuration with name '%s': %v", userSession.OIDCWorkflowSession.ClientID, err)
|
||||
ctx.ReplyForbidden()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if !client.IsAuthenticationLevelSufficient(userSession.AuthenticationLevel) {
|
||||
ctx.Logger.Debugf("Insufficient permissions to give consent v1 %d -> %d", userSession.AuthenticationLevel, userSession.OIDCWorkflowSession.RequiredAuthorizationLevel)
|
||||
ctx.ReplyForbidden()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var body ConsentPostRequestBody
|
||||
err = json.Unmarshal(ctx.Request.Body(), &body)
|
||||
|
||||
if err != nil {
|
||||
ctx.Error(fmt.Errorf("Unable to unmarshal body: %v", err), "Operation failed")
|
||||
return
|
||||
}
|
||||
|
||||
if body.AcceptOrReject != accept && body.AcceptOrReject != reject {
|
||||
ctx.Logger.Infof("User %s tried to reply to consent with an unexpected verb", userSession.Username)
|
||||
ctx.ReplyBadRequest()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if userSession.OIDCWorkflowSession.ClientID != body.ClientID {
|
||||
ctx.Logger.Infof("User %s consented to scopes of another client (%s) than expected (%s). Beware this can be a sign of attack",
|
||||
userSession.Username, body.ClientID, userSession.OIDCWorkflowSession.ClientID)
|
||||
ctx.ReplyBadRequest()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var redirectionURL string
|
||||
|
||||
if body.AcceptOrReject == accept {
|
||||
redirectionURL = userSession.OIDCWorkflowSession.AuthURI
|
||||
userSession.OIDCWorkflowSession.GrantedScopes = userSession.OIDCWorkflowSession.RequestedScopes
|
||||
userSession.OIDCWorkflowSession.GrantedAudience = userSession.OIDCWorkflowSession.RequestedAudience
|
||||
|
||||
if err := ctx.SaveSession(userSession); err != nil {
|
||||
ctx.Error(fmt.Errorf("Unable to write session: %v", err), "Operation failed")
|
||||
return
|
||||
}
|
||||
} else if body.AcceptOrReject == reject {
|
||||
redirectionURL = fmt.Sprintf("%s?error=access_denied&error_description=%s",
|
||||
userSession.OIDCWorkflowSession.TargetURI, "User has rejected the scopes")
|
||||
userSession.OIDCWorkflowSession = nil
|
||||
|
||||
if err := ctx.SaveSession(userSession); err != nil {
|
||||
ctx.Error(fmt.Errorf("Unable to write session: %v", err), "Operation failed")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
response := ConsentPostResponseBody{RedirectURI: redirectionURL}
|
||||
|
||||
if err := ctx.SetJSONBody(response); err != nil {
|
||||
ctx.Error(fmt.Errorf("Unable to set JSON body in response"), "Operation failed")
|
||||
}
|
||||
}
|
29
internal/handlers/handler_oidc_introspect.go
Normal file
29
internal/handlers/handler_oidc_introspect.go
Normal file
|
@ -0,0 +1,29 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/authelia/authelia/internal/middlewares"
|
||||
)
|
||||
|
||||
func oidcIntrospect(ctx *middlewares.AutheliaCtx, rw http.ResponseWriter, req *http.Request) {
|
||||
oidcSession, err := newDefaultOIDCSession(ctx)
|
||||
|
||||
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)
|
||||
|
||||
if err != nil {
|
||||
ctx.Logger.Errorf("Error occurred in NewIntrospectionRequest: %+v", err)
|
||||
ctx.Providers.OpenIDConnect.Fosite.WriteIntrospectionError(rw, err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Providers.OpenIDConnect.Fosite.WriteIntrospectionResponse(rw, ir)
|
||||
}
|
15
internal/handlers/handler_oidc_jwks.go
Normal file
15
internal/handlers/handler_oidc_jwks.go
Normal file
|
@ -0,0 +1,15 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/authelia/authelia/internal/middlewares"
|
||||
)
|
||||
|
||||
func oidcJWKs(ctx *middlewares.AutheliaCtx) {
|
||||
ctx.SetContentType("application/json")
|
||||
|
||||
if err := json.NewEncoder(ctx).Encode(ctx.Providers.OpenIDConnect.GetKeySet()); err != nil {
|
||||
ctx.Error(err, "failed to serve jwk set")
|
||||
}
|
||||
}
|
13
internal/handlers/handler_oidc_revoke.go
Normal file
13
internal/handlers/handler_oidc_revoke.go
Normal file
|
@ -0,0 +1,13 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/authelia/authelia/internal/middlewares"
|
||||
)
|
||||
|
||||
func oidcRevoke(ctx *middlewares.AutheliaCtx, rw http.ResponseWriter, req *http.Request) {
|
||||
err := ctx.Providers.OpenIDConnect.Fosite.NewRevocationRequest(ctx, req)
|
||||
|
||||
ctx.Providers.OpenIDConnect.Fosite.WriteRevocationResponse(rw, err)
|
||||
}
|
46
internal/handlers/handler_oidc_token.go
Normal file
46
internal/handlers/handler_oidc_token.go
Normal file
|
@ -0,0 +1,46 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/ory/fosite"
|
||||
|
||||
"github.com/authelia/authelia/internal/middlewares"
|
||||
)
|
||||
|
||||
func oidcToken(ctx *middlewares.AutheliaCtx, rw http.ResponseWriter, req *http.Request) {
|
||||
oidcSession, err := newDefaultOIDCSession(ctx)
|
||||
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)
|
||||
if accessReqErr != nil {
|
||||
ctx.Logger.Errorf("Error occurred in NewAccessRequest: %+v", accessRequest)
|
||||
ctx.Providers.OpenIDConnect.Fosite.WriteAccessError(rw, accessRequest, accessReqErr)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// If this is a client_credentials grant, grant all scopes the client is allowed to perform.
|
||||
if accessRequest.GetGrantTypes().ExactOne("client_credentials") {
|
||||
for _, scope := range accessRequest.GetRequestedScopes() {
|
||||
if fosite.HierarchicScopeStrategy(accessRequest.GetClient().GetScopes(), scope) {
|
||||
accessRequest.GrantScope(scope)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
response, err := ctx.Providers.OpenIDConnect.Fosite.NewAccessResponse(ctx, accessRequest)
|
||||
if err != nil {
|
||||
ctx.Logger.Errorf("Error occurred in NewAccessResponse: %+v", err)
|
||||
ctx.Providers.OpenIDConnect.Fosite.WriteAccessError(rw, accessRequest, err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Providers.OpenIDConnect.Fosite.WriteAccessResponse(rw, accessRequest, response)
|
||||
}
|
73
internal/handlers/handler_oidc_wellknown.go
Normal file
73
internal/handlers/handler_oidc_wellknown.go
Normal file
|
@ -0,0 +1,73 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/valyala/fasthttp"
|
||||
|
||||
"github.com/authelia/authelia/internal/middlewares"
|
||||
)
|
||||
|
||||
func oidcWellKnown(ctx *middlewares.AutheliaCtx) {
|
||||
var configuration WellKnownConfigurationJSON
|
||||
|
||||
issuer, err := ctx.ForwardedProtoHost()
|
||||
if err != nil {
|
||||
ctx.Logger.Errorf("Error occurred in ForwardedProtoHost: %+v", err)
|
||||
ctx.Response.SetStatusCode(fasthttp.StatusBadRequest)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
configuration.Issuer = issuer
|
||||
configuration.AuthURL = fmt.Sprintf("%s%s", issuer, oidcAuthorizePath)
|
||||
configuration.TokenURL = fmt.Sprintf("%s%s", issuer, oidcTokenPath)
|
||||
configuration.RevocationEndpoint = fmt.Sprintf("%s%s", issuer, oidcRevokePath)
|
||||
configuration.JWKSURL = fmt.Sprintf("%s%s", issuer, oidcJWKsPath)
|
||||
configuration.Algorithms = []string{"RS256"}
|
||||
configuration.ScopesSupported = []string{
|
||||
"openid",
|
||||
"profile",
|
||||
"groups",
|
||||
"email",
|
||||
// Determine if this is really mandatory knowing the RP can request for a refresh token through the authorize
|
||||
// endpoint anyway.
|
||||
"offline_access",
|
||||
}
|
||||
configuration.ClaimsSupported = []string{
|
||||
"aud",
|
||||
"exp",
|
||||
"iat",
|
||||
"iss",
|
||||
"jti",
|
||||
"rat",
|
||||
"sub",
|
||||
"auth_time",
|
||||
"nonce",
|
||||
"email",
|
||||
"email_verified",
|
||||
"groups",
|
||||
"name",
|
||||
}
|
||||
configuration.ResponseTypesSupported = []string{
|
||||
"code",
|
||||
"token",
|
||||
"id_token",
|
||||
"code token",
|
||||
"code id_token",
|
||||
"token id_token",
|
||||
"code token id_token",
|
||||
"none",
|
||||
}
|
||||
|
||||
ctx.SetContentType("application/json")
|
||||
|
||||
if err := json.NewEncoder(ctx).Encode(configuration); err != nil {
|
||||
ctx.Logger.Errorf("Error occurred in json Encode: %+v", err)
|
||||
// TODO: Determine if this is the appropriate error code here.
|
||||
ctx.Response.SetStatusCode(fasthttp.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
|
@ -73,6 +73,10 @@ func SecondFactorDuoPost(duoAPI duo.API) middlewares.RequestHandler {
|
|||
return
|
||||
}
|
||||
|
||||
Handle2FAResponse(ctx, requestBody.TargetURL)
|
||||
if userSession.OIDCWorkflowSession != nil {
|
||||
HandleOIDCWorkflowResponse(ctx)
|
||||
} else {
|
||||
Handle2FAResponse(ctx, requestBody.TargetURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,8 +10,8 @@ import (
|
|||
// SecondFactorTOTPPost validate the TOTP passcode provided by the user.
|
||||
func SecondFactorTOTPPost(totpVerifier TOTPVerifier) middlewares.RequestHandler {
|
||||
return func(ctx *middlewares.AutheliaCtx) {
|
||||
bodyJSON := signTOTPRequestBody{}
|
||||
err := ctx.ParseBody(&bodyJSON)
|
||||
requestBody := signTOTPRequestBody{}
|
||||
err := ctx.ParseBody(&requestBody)
|
||||
|
||||
if err != nil {
|
||||
handleAuthenticationUnauthorized(ctx, err, mfaValidationFailedMessage)
|
||||
|
@ -26,7 +26,7 @@ func SecondFactorTOTPPost(totpVerifier TOTPVerifier) middlewares.RequestHandler
|
|||
return
|
||||
}
|
||||
|
||||
isValid, err := totpVerifier.Verify(bodyJSON.Token, secret)
|
||||
isValid, err := totpVerifier.Verify(requestBody.Token, secret)
|
||||
if err != nil {
|
||||
handleAuthenticationUnauthorized(ctx, fmt.Errorf("Error occurred during OTP validation for user %s: %s", userSession.Username, err), mfaValidationFailedMessage)
|
||||
return
|
||||
|
@ -52,6 +52,10 @@ func SecondFactorTOTPPost(totpVerifier TOTPVerifier) middlewares.RequestHandler
|
|||
return
|
||||
}
|
||||
|
||||
Handle2FAResponse(ctx, bodyJSON.TargetURL)
|
||||
if userSession.OIDCWorkflowSession != nil {
|
||||
HandleOIDCWorkflowResponse(ctx)
|
||||
} else {
|
||||
Handle2FAResponse(ctx, requestBody.TargetURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -55,6 +55,10 @@ func SecondFactorU2FSignPost(u2fVerifier U2FVerifier) middlewares.RequestHandler
|
|||
return
|
||||
}
|
||||
|
||||
Handle2FAResponse(ctx, requestBody.TargetURL)
|
||||
if userSession.OIDCWorkflowSession != nil {
|
||||
HandleOIDCWorkflowResponse(ctx)
|
||||
} else {
|
||||
Handle2FAResponse(ctx, requestBody.TargetURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,49 +31,6 @@ func isSchemeWSS(url *url.URL) bool {
|
|||
return url.Scheme == "wss"
|
||||
}
|
||||
|
||||
// getOriginalURL extract the URL from the request headers (X-Original-URI or X-Forwarded-* headers).
|
||||
func getOriginalURL(ctx *middlewares.AutheliaCtx) (*url.URL, error) {
|
||||
originalURL := ctx.XOriginalURL()
|
||||
if originalURL != nil {
|
||||
url, err := url.ParseRequestURI(string(originalURL))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Unable to parse URL extracted from X-Original-URL header: %v", err)
|
||||
}
|
||||
|
||||
ctx.Logger.Trace("Using X-Original-URL header content as targeted site URL")
|
||||
|
||||
return url, nil
|
||||
}
|
||||
|
||||
forwardedProto := ctx.XForwardedProto()
|
||||
forwardedHost := ctx.XForwardedHost()
|
||||
forwardedURI := ctx.XForwardedURI()
|
||||
|
||||
if forwardedProto == nil {
|
||||
return nil, errMissingXForwardedProto
|
||||
}
|
||||
|
||||
if forwardedHost == nil {
|
||||
return nil, errMissingXForwardedHost
|
||||
}
|
||||
|
||||
var requestURI string
|
||||
|
||||
scheme := append(forwardedProto, protoHostSeparator...)
|
||||
requestURI = string(append(scheme,
|
||||
append(forwardedHost, forwardedURI...)...))
|
||||
|
||||
url, err := url.ParseRequestURI(requestURI)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Unable to parse URL %s: %v", requestURI, err)
|
||||
}
|
||||
|
||||
ctx.Logger.Tracef("Using X-Forwarded-Proto, X-Forwarded-Host and X-Forwarded-URI headers " +
|
||||
"to construct targeted site URL")
|
||||
|
||||
return url, nil
|
||||
}
|
||||
|
||||
// parseBasicAuth parses an HTTP Basic Authentication string.
|
||||
// "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==" returns ("Aladdin", "open sesame", true).
|
||||
func parseBasicAuth(header, auth string) (username, password string, err error) {
|
||||
|
@ -468,7 +425,7 @@ func VerifyGet(cfg schema.AuthenticationBackendConfiguration) middlewares.Reques
|
|||
|
||||
return func(ctx *middlewares.AutheliaCtx) {
|
||||
ctx.Logger.Tracef("Headers=%s", ctx.Request.Header.String())
|
||||
targetURL, err := getOriginalURL(ctx)
|
||||
targetURL, err := ctx.GetOriginalURL()
|
||||
|
||||
if err != nil {
|
||||
ctx.Error(fmt.Errorf("Unable to parse target URL: %s", err), operationFailedMessage)
|
||||
|
|
|
@ -26,49 +26,13 @@ var verifyGetCfg = schema.AuthenticationBackendConfiguration{
|
|||
LDAP: &schema.LDAPAuthenticationBackendConfiguration{},
|
||||
}
|
||||
|
||||
// Test getOriginalURL.
|
||||
func TestShouldGetOriginalURLFromOriginalURLHeader(t *testing.T) {
|
||||
mock := mocks.NewMockAutheliaCtx(t)
|
||||
defer mock.Close()
|
||||
|
||||
mock.Ctx.Request.Header.Set("X-Original-URL", "https://home.example.com")
|
||||
originalURL, err := getOriginalURL(mock.Ctx)
|
||||
assert.NoError(t, err)
|
||||
|
||||
expectedURL, err := url.ParseRequestURI("https://home.example.com")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedURL, originalURL)
|
||||
}
|
||||
|
||||
func TestShouldGetOriginalURLFromForwardedHeadersWithoutURI(t *testing.T) {
|
||||
mock := mocks.NewMockAutheliaCtx(t)
|
||||
defer mock.Close()
|
||||
mock.Ctx.Request.Header.Set("X-Forwarded-Proto", "https")
|
||||
mock.Ctx.Request.Header.Set("X-Forwarded-Host", "home.example.com")
|
||||
originalURL, err := getOriginalURL(mock.Ctx)
|
||||
assert.NoError(t, err)
|
||||
|
||||
expectedURL, err := url.ParseRequestURI("https://home.example.com")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedURL, originalURL)
|
||||
}
|
||||
|
||||
func TestShouldGetOriginalURLFromForwardedHeadersWithURI(t *testing.T) {
|
||||
mock := mocks.NewMockAutheliaCtx(t)
|
||||
defer mock.Close()
|
||||
mock.Ctx.Request.Header.Set("X-Original-URL", "htt-ps//home?-.example.com")
|
||||
_, err := getOriginalURL(mock.Ctx)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "Unable to parse URL extracted from X-Original-URL header: parse \"htt-ps//home?-.example.com\": invalid URI for request", err.Error())
|
||||
}
|
||||
|
||||
func TestShouldRaiseWhenTargetUrlIsMalformed(t *testing.T) {
|
||||
mock := mocks.NewMockAutheliaCtx(t)
|
||||
defer mock.Close()
|
||||
mock.Ctx.Request.Header.Set("X-Forwarded-Proto", "https")
|
||||
mock.Ctx.Request.Header.Set("X-Forwarded-Host", "home.example.com")
|
||||
mock.Ctx.Request.Header.Set("X-Forwarded-URI", "/abc")
|
||||
originalURL, err := getOriginalURL(mock.Ctx)
|
||||
originalURL, err := mock.Ctx.GetOriginalURL()
|
||||
assert.NoError(t, err)
|
||||
|
||||
expectedURL, err := url.ParseRequestURI("https://home.example.com/abc")
|
||||
|
@ -79,7 +43,7 @@ func TestShouldRaiseWhenTargetUrlIsMalformed(t *testing.T) {
|
|||
func TestShouldRaiseWhenNoHeaderProvidedToDetectTargetURL(t *testing.T) {
|
||||
mock := mocks.NewMockAutheliaCtx(t)
|
||||
defer mock.Close()
|
||||
_, err := getOriginalURL(mock.Ctx)
|
||||
_, err := mock.Ctx.GetOriginalURL()
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "Missing header X-Forwarded-Proto", err.Error())
|
||||
}
|
||||
|
@ -89,7 +53,7 @@ func TestShouldRaiseWhenNoXForwardedHostHeaderProvidedToDetectTargetURL(t *testi
|
|||
defer mock.Close()
|
||||
|
||||
mock.Ctx.Request.Header.Set("X-Forwarded-Proto", "https")
|
||||
_, err := getOriginalURL(mock.Ctx)
|
||||
_, err := mock.Ctx.GetOriginalURL()
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "Missing header X-Forwarded-Host", err.Error())
|
||||
}
|
||||
|
@ -101,7 +65,7 @@ func TestShouldRaiseWhenXForwardedProtoIsNotParsable(t *testing.T) {
|
|||
mock.Ctx.Request.Header.Set("X-Forwarded-Proto", "!:;;:,")
|
||||
mock.Ctx.Request.Header.Set("X-Forwarded-Host", "myhost.local")
|
||||
|
||||
_, err := getOriginalURL(mock.Ctx)
|
||||
_, err := mock.Ctx.GetOriginalURL()
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "Unable to parse URL !:;;:,://myhost.local: parse \"!:;;:,://myhost.local\": invalid URI for request", err.Error())
|
||||
}
|
||||
|
@ -114,7 +78,7 @@ func TestShouldRaiseWhenXForwardedURIIsNotParsable(t *testing.T) {
|
|||
mock.Ctx.Request.Header.Set("X-Forwarded-Host", "myhost.local")
|
||||
mock.Ctx.Request.Header.Set("X-Forwarded-URI", "!:;;:,")
|
||||
|
||||
_, err := getOriginalURL(mock.Ctx)
|
||||
_, err := mock.Ctx.GetOriginalURL()
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, "Unable to parse URL https://myhost.local!:;;:,: parse \"https://myhost.local!:;;:,\": invalid port \":,\" after host", err.Error())
|
||||
}
|
||||
|
|
106
internal/handlers/oidc.go
Normal file
106
internal/handlers/oidc.go
Normal file
|
@ -0,0 +1,106 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/ory/fosite"
|
||||
"github.com/ory/fosite/handler/openid"
|
||||
"github.com/ory/fosite/token/jwt"
|
||||
|
||||
"github.com/authelia/authelia/internal/middlewares"
|
||||
"github.com/authelia/authelia/internal/session"
|
||||
"github.com/authelia/authelia/internal/utils"
|
||||
)
|
||||
|
||||
// isConsentMissing compares the requestedScopes and requestedAudience to the workflows
|
||||
// GrantedScopes and GrantedAudience and returns true if they do not match or the workflow is nil.
|
||||
func isConsentMissing(workflow *session.OIDCWorkflowSession, requestedScopes, requestedAudience []string) (isMissing bool) {
|
||||
if workflow == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
return len(requestedScopes) > 0 && utils.IsStringSlicesDifferent(requestedScopes, workflow.GrantedScopes) ||
|
||||
len(requestedAudience) > 0 && utils.IsStringSlicesDifferentFold(requestedAudience, workflow.GrantedAudience)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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: make(map[string]interface{}),
|
||||
},
|
||||
}, err
|
||||
}
|
33
internal/handlers/oidc_test.go
Normal file
33
internal/handlers/oidc_test.go
Normal file
|
@ -0,0 +1,33 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/authelia/authelia/internal/session"
|
||||
)
|
||||
|
||||
func TestShouldDetectIfConsentIsMissing(t *testing.T) {
|
||||
var workflow *session.OIDCWorkflowSession
|
||||
|
||||
requestedScopes := []string{"openid", "profile"}
|
||||
requestedAudience := []string{"https://authelia.com"}
|
||||
|
||||
assert.True(t, isConsentMissing(workflow, requestedScopes, requestedAudience))
|
||||
|
||||
workflow = &session.OIDCWorkflowSession{
|
||||
GrantedScopes: []string{"openid", "profile"},
|
||||
GrantedAudience: []string{"https://authelia.com"},
|
||||
}
|
||||
|
||||
assert.False(t, isConsentMissing(workflow, requestedScopes, requestedAudience))
|
||||
|
||||
requestedScopes = []string{"openid", "profile", "group"}
|
||||
|
||||
assert.True(t, isConsentMissing(workflow, requestedScopes, requestedAudience))
|
||||
|
||||
requestedScopes = []string{"openid", "profile"}
|
||||
requestedAudience = []string{"https://not.authelia.com"}
|
||||
assert.True(t, isConsentMissing(workflow, requestedScopes, requestedAudience))
|
||||
}
|
29
internal/handlers/register_oidc.go
Normal file
29
internal/handlers/register_oidc.go
Normal file
|
@ -0,0 +1,29 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"github.com/fasthttp/router"
|
||||
|
||||
"github.com/authelia/authelia/internal/middlewares"
|
||||
)
|
||||
|
||||
// RegisterOIDC registers the handlers with the fasthttp *router.Router. TODO: Add paths for UserInfo, Flush, Logout.
|
||||
func RegisterOIDC(router *router.Router, middleware middlewares.RequestHandlerBridge) {
|
||||
// TODO: Add OPTIONS handler.
|
||||
router.GET(oidcWellKnownPath, middleware(oidcWellKnown))
|
||||
|
||||
router.GET(oidcConsentPath, middleware(oidcConsent))
|
||||
|
||||
router.POST(oidcConsentPath, middleware(oidcConsentPOST))
|
||||
|
||||
router.GET(oidcJWKsPath, middleware(oidcJWKs))
|
||||
|
||||
router.GET(oidcAuthorizePath, middleware(middlewares.NewHTTPToAutheliaHandlerAdaptor(oidcAuthorize)))
|
||||
|
||||
// TODO: Add OPTIONS handler.
|
||||
router.POST(oidcTokenPath, middleware(middlewares.NewHTTPToAutheliaHandlerAdaptor(oidcToken)))
|
||||
|
||||
router.POST(oidcIntrospectPath, middleware(middlewares.NewHTTPToAutheliaHandlerAdaptor(oidcIntrospect)))
|
||||
|
||||
// TODO: Add OPTIONS handler.
|
||||
router.POST(oidcRevokePath, middleware(middlewares.NewHTTPToAutheliaHandlerAdaptor(oidcRevoke)))
|
||||
}
|
|
@ -11,6 +11,42 @@ import (
|
|||
"github.com/authelia/authelia/internal/utils"
|
||||
)
|
||||
|
||||
// HandleOIDCWorkflowResponse handle the redirection upon authentication in the OIDC workflow.
|
||||
func HandleOIDCWorkflowResponse(ctx *middlewares.AutheliaCtx) {
|
||||
userSession := ctx.GetSession()
|
||||
|
||||
if !authorization.IsAuthLevelSufficient(userSession.AuthenticationLevel, userSession.OIDCWorkflowSession.RequiredAuthorizationLevel) {
|
||||
ctx.Logger.Warn("OIDC requires 2FA, cannot be redirected yet")
|
||||
ctx.ReplyOK()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
uri, err := ctx.ForwardedProtoHost()
|
||||
if err != nil {
|
||||
ctx.Logger.Errorf("%v", err)
|
||||
handleAuthenticationUnauthorized(ctx, fmt.Errorf("Unable to get forward facing URI"), authenticationFailedMessage)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if isConsentMissing(
|
||||
userSession.OIDCWorkflowSession,
|
||||
userSession.OIDCWorkflowSession.RequestedScopes,
|
||||
userSession.OIDCWorkflowSession.RequestedAudience) {
|
||||
err := ctx.SetJSONBody(redirectResponse{Redirect: fmt.Sprintf("%s/consent", uri)})
|
||||
|
||||
if err != nil {
|
||||
ctx.Logger.Errorf("Unable to set default redirection URL in body: %s", err)
|
||||
}
|
||||
} else {
|
||||
err := ctx.SetJSONBody(redirectResponse{Redirect: userSession.OIDCWorkflowSession.AuthURI})
|
||||
if err != nil {
|
||||
ctx.Logger.Errorf("Unable to set default redirection URL in body: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle1FAResponse handle the redirection upon 1FA authentication.
|
||||
func Handle1FAResponse(ctx *middlewares.AutheliaCtx, targetURI, requestMethod string, username string, groups []string) {
|
||||
if targetURI == "" {
|
||||
|
|
62
internal/handlers/types_oidc.go
Normal file
62
internal/handlers/types_oidc.go
Normal file
|
@ -0,0 +1,62 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
)
|
||||
|
||||
// ConsentPostRequestBody schema of the request body of the consent POST endpoint.
|
||||
type ConsentPostRequestBody struct {
|
||||
ClientID string `json:"client_id"`
|
||||
AcceptOrReject string `json:"accept_or_reject"`
|
||||
}
|
||||
|
||||
// ConsentPostResponseBody schema of the response body of the consent POST endpoint.
|
||||
type ConsentPostResponseBody struct {
|
||||
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"`
|
||||
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"`
|
||||
}
|
|
@ -4,6 +4,7 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
|
@ -37,7 +38,7 @@ func NewAutheliaCtx(ctx *fasthttp.RequestCtx, configuration schema.Configuration
|
|||
}
|
||||
|
||||
// AutheliaMiddleware is wrapping the RequestCtx into an AutheliaCtx providing Authelia related objects.
|
||||
func AutheliaMiddleware(configuration schema.Configuration, providers Providers) func(next RequestHandler) fasthttp.RequestHandler {
|
||||
func AutheliaMiddleware(configuration schema.Configuration, providers Providers) RequestHandlerBridge {
|
||||
return func(next RequestHandler) fasthttp.RequestHandler {
|
||||
return func(ctx *fasthttp.RequestCtx) {
|
||||
autheliaCtx, err := NewAutheliaCtx(ctx, configuration, providers)
|
||||
|
@ -87,6 +88,11 @@ func (c *AutheliaCtx) ReplyForbidden() {
|
|||
c.RequestCtx.Error(fasthttp.StatusMessage(fasthttp.StatusForbidden), fasthttp.StatusForbidden)
|
||||
}
|
||||
|
||||
// ReplyBadRequest response sent when bad request has been sent.
|
||||
func (c *AutheliaCtx) ReplyBadRequest() {
|
||||
c.RequestCtx.Error(fasthttp.StatusMessage(fasthttp.StatusBadRequest), fasthttp.StatusBadRequest)
|
||||
}
|
||||
|
||||
// XForwardedProto return the content of the X-Forwarded-Proto header.
|
||||
func (c *AutheliaCtx) XForwardedProto() []byte {
|
||||
return c.RequestCtx.Request.Header.Peek(xForwardedProtoHeader)
|
||||
|
@ -107,6 +113,24 @@ func (c *AutheliaCtx) XForwardedURI() []byte {
|
|||
return c.RequestCtx.Request.Header.Peek(xForwardedURIHeader)
|
||||
}
|
||||
|
||||
// ForwardedProtoHost gets the X-Forwarded-Proto and X-Forwarded-Host headers and forms them into a URL.
|
||||
func (c AutheliaCtx) ForwardedProtoHost() (string, error) {
|
||||
XForwardedProto := c.XForwardedProto()
|
||||
|
||||
if XForwardedProto == nil {
|
||||
return "", errMissingXForwardedProto
|
||||
}
|
||||
|
||||
XForwardedHost := c.XForwardedHost()
|
||||
|
||||
if XForwardedHost == nil {
|
||||
return "", errMissingXForwardedHost
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s://%s", XForwardedProto,
|
||||
XForwardedHost), nil
|
||||
}
|
||||
|
||||
// XOriginalURL return the content of the X-Original-URL header.
|
||||
func (c *AutheliaCtx) XOriginalURL() []byte {
|
||||
return c.RequestCtx.Request.Header.Peek(xOriginalURLHeader)
|
||||
|
@ -181,3 +205,46 @@ func (c *AutheliaCtx) RemoteIP() net.IP {
|
|||
|
||||
return c.RequestCtx.RemoteIP()
|
||||
}
|
||||
|
||||
// GetOriginalURL extract the URL from the request headers (X-Original-URI or X-Forwarded-* headers).
|
||||
func (c *AutheliaCtx) GetOriginalURL() (*url.URL, error) {
|
||||
originalURL := c.XOriginalURL()
|
||||
if originalURL != nil {
|
||||
parsedURL, err := url.ParseRequestURI(string(originalURL))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Unable to parse URL extracted from X-Original-URL header: %v", err)
|
||||
}
|
||||
|
||||
c.Logger.Trace("Using X-Original-URL header content as targeted site URL")
|
||||
|
||||
return parsedURL, nil
|
||||
}
|
||||
|
||||
forwardedProto := c.XForwardedProto()
|
||||
forwardedHost := c.XForwardedHost()
|
||||
forwardedURI := c.XForwardedURI()
|
||||
|
||||
if forwardedProto == nil {
|
||||
return nil, errMissingXForwardedProto
|
||||
}
|
||||
|
||||
if forwardedHost == nil {
|
||||
return nil, errMissingXForwardedHost
|
||||
}
|
||||
|
||||
var requestURI string
|
||||
|
||||
scheme := append(forwardedProto, protoHostSeparator...)
|
||||
requestURI = string(append(scheme,
|
||||
append(forwardedHost, forwardedURI...)...))
|
||||
|
||||
parsedURL, err := url.ParseRequestURI(requestURI)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Unable to parse URL %s: %v", requestURI, err)
|
||||
}
|
||||
|
||||
c.Logger.Tracef("Using X-Fowarded-Proto, X-Forwarded-Host and X-Forwarded-URI headers " +
|
||||
"to construct targeted site URL")
|
||||
|
||||
return parsedURL, nil
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package middlewares_test
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
|
@ -33,3 +34,39 @@ func TestShouldCallNextWithAutheliaCtx(t *testing.T) {
|
|||
|
||||
assert.True(t, nextCalled)
|
||||
}
|
||||
|
||||
// Test getOriginalURL.
|
||||
func TestShouldGetOriginalURLFromOriginalURLHeader(t *testing.T) {
|
||||
mock := mocks.NewMockAutheliaCtx(t)
|
||||
defer mock.Close()
|
||||
|
||||
mock.Ctx.Request.Header.Set("X-Original-URL", "https://home.example.com")
|
||||
originalURL, err := mock.Ctx.GetOriginalURL()
|
||||
assert.NoError(t, err)
|
||||
|
||||
expectedURL, err := url.ParseRequestURI("https://home.example.com")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedURL, originalURL)
|
||||
}
|
||||
|
||||
func TestShouldGetOriginalURLFromForwardedHeadersWithoutURI(t *testing.T) {
|
||||
mock := mocks.NewMockAutheliaCtx(t)
|
||||
defer mock.Close()
|
||||
mock.Ctx.Request.Header.Set("X-Forwarded-Proto", "https")
|
||||
mock.Ctx.Request.Header.Set("X-Forwarded-Host", "home.example.com")
|
||||
originalURL, err := mock.Ctx.GetOriginalURL()
|
||||
assert.NoError(t, err)
|
||||
|
||||
expectedURL, err := url.ParseRequestURI("https://home.example.com")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedURL, originalURL)
|
||||
}
|
||||
|
||||
func TestShouldGetOriginalURLFromForwardedHeadersWithURI(t *testing.T) {
|
||||
mock := mocks.NewMockAutheliaCtx(t)
|
||||
defer mock.Close()
|
||||
mock.Ctx.Request.Header.Set("X-Original-URL", "htt-ps//home?-.example.com")
|
||||
_, err := mock.Ctx.GetOriginalURL()
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "Unable to parse URL extracted from X-Original-URL header: parse \"htt-ps//home?-.example.com\": invalid URI for request", err.Error())
|
||||
}
|
||||
|
|
|
@ -16,3 +16,5 @@ var okMessageBytes = []byte("{\"status\":\"OK\"}")
|
|||
const operationFailedMessage = "Operation failed"
|
||||
const identityVerificationTokenAlreadyUsedMessage = "The identity verification token has already been used"
|
||||
const identityVerificationTokenHasExpiredMessage = "The identity verification token has expired"
|
||||
|
||||
var protoHostSeparator = []byte("://")
|
||||
|
|
125
internal/middlewares/http_to_authelia_handler_adaptor.go
Normal file
125
internal/middlewares/http_to_authelia_handler_adaptor.go
Normal file
|
@ -0,0 +1,125 @@
|
|||
package middlewares
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
// AutheliaHandlerFunc is used with the NewHTTPToAutheliaHandlerAdaptor to encapsulate a func.
|
||||
type AutheliaHandlerFunc func(ctx *AutheliaCtx, rw http.ResponseWriter, r *http.Request)
|
||||
|
||||
type netHTTPBody struct {
|
||||
b []byte
|
||||
}
|
||||
|
||||
// Read reads the body.
|
||||
func (r *netHTTPBody) Read(p []byte) (int, error) {
|
||||
if len(r.b) == 0 {
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
n := copy(p, r.b)
|
||||
r.b = r.b[n:]
|
||||
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// Close closes the body.
|
||||
func (r *netHTTPBody) Close() error {
|
||||
r.b = r.b[:0]
|
||||
return nil
|
||||
}
|
||||
|
||||
type netHTTPResponseWriter struct {
|
||||
statusCode int
|
||||
h http.Header
|
||||
body []byte
|
||||
}
|
||||
|
||||
// StatusCode returns the status code.
|
||||
func (w *netHTTPResponseWriter) StatusCode() int {
|
||||
if w.statusCode == 0 {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
return w.statusCode
|
||||
}
|
||||
|
||||
// Header returns the http.Header.
|
||||
func (w *netHTTPResponseWriter) Header() http.Header {
|
||||
if w.h == nil {
|
||||
w.h = make(http.Header)
|
||||
}
|
||||
|
||||
return w.h
|
||||
}
|
||||
|
||||
// WriteHeader needs to be documented TODO: document it.
|
||||
func (w *netHTTPResponseWriter) WriteHeader(statusCode int) {
|
||||
w.statusCode = statusCode
|
||||
}
|
||||
|
||||
// Write writes to the body.
|
||||
func (w *netHTTPResponseWriter) Write(p []byte) (int, error) {
|
||||
w.body = append(w.body, p...)
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// NewHTTPToAutheliaHandlerAdaptor creates a new adaptor given the AutheliaHandlerFunc.
|
||||
func NewHTTPToAutheliaHandlerAdaptor(h AutheliaHandlerFunc) RequestHandler {
|
||||
return func(ctx *AutheliaCtx) {
|
||||
var r http.Request
|
||||
|
||||
body := ctx.PostBody()
|
||||
r.Method = string(ctx.Method())
|
||||
r.Proto = "HTTP/1.1"
|
||||
r.ProtoMajor = 1
|
||||
r.ProtoMinor = 1
|
||||
r.RequestURI = string(ctx.RequestURI())
|
||||
r.ContentLength = int64(len(body))
|
||||
r.Host = string(ctx.Host())
|
||||
r.RemoteAddr = ctx.RemoteAddr().String()
|
||||
|
||||
hdr := make(http.Header)
|
||||
ctx.Request.Header.VisitAll(func(k, v []byte) {
|
||||
sk := string(k)
|
||||
sv := string(v)
|
||||
switch sk {
|
||||
case "Transfer-Encoding":
|
||||
r.TransferEncoding = append(r.TransferEncoding, sv)
|
||||
default:
|
||||
hdr.Set(sk, sv)
|
||||
}
|
||||
})
|
||||
|
||||
r.Header = hdr
|
||||
r.Body = &netHTTPBody{body}
|
||||
rURL, err := url.ParseRequestURI(r.RequestURI)
|
||||
|
||||
if err != nil {
|
||||
ctx.Logger.Errorf("cannot parse requestURI %q: %s", r.RequestURI, err)
|
||||
ctx.RequestCtx.Error("Internal Server Error", fasthttp.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
r.URL = rURL
|
||||
|
||||
var w netHTTPResponseWriter
|
||||
|
||||
h(ctx, &w, r.WithContext(ctx))
|
||||
|
||||
ctx.SetStatusCode(w.StatusCode())
|
||||
|
||||
for k, vv := range w.Header() {
|
||||
for _, v := range vv {
|
||||
ctx.Response.Header.Set(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Write(w.body) //nolint:errcheck
|
||||
}
|
||||
}
|
|
@ -6,7 +6,7 @@ import (
|
|||
"fmt"
|
||||
"time"
|
||||
|
||||
jwt "github.com/dgrijalva/jwt-go"
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
|
||||
"github.com/authelia/authelia/internal/templates"
|
||||
)
|
||||
|
@ -51,18 +51,13 @@ func IdentityVerificationStart(args IdentityVerificationStartArgs) RequestHandle
|
|||
return
|
||||
}
|
||||
|
||||
if ctx.XForwardedProto() == nil {
|
||||
ctx.Error(errMissingXForwardedProto, operationFailedMessage)
|
||||
uri, err := ctx.ForwardedProtoHost()
|
||||
if err != nil {
|
||||
ctx.Error(err, operationFailedMessage)
|
||||
return
|
||||
}
|
||||
|
||||
if ctx.XForwardedHost() == nil {
|
||||
ctx.Error(errMissingXForwardedHost, operationFailedMessage)
|
||||
return
|
||||
}
|
||||
|
||||
link := fmt.Sprintf("%s://%s%s%s?token=%s", ctx.XForwardedProto(),
|
||||
ctx.XForwardedHost(), ctx.Configuration.Server.Path, args.TargetEndpoint, ss)
|
||||
link := fmt.Sprintf("%s%s%s?token=%s", uri, ctx.Configuration.Server.Path, args.TargetEndpoint, ss)
|
||||
|
||||
bufHTML := new(bytes.Buffer)
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package middlewares
|
||||
|
||||
import (
|
||||
jwt "github.com/dgrijalva/jwt-go"
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/valyala/fasthttp"
|
||||
|
||||
|
@ -9,6 +9,7 @@ import (
|
|||
"github.com/authelia/authelia/internal/authorization"
|
||||
"github.com/authelia/authelia/internal/configuration/schema"
|
||||
"github.com/authelia/authelia/internal/notification"
|
||||
"github.com/authelia/authelia/internal/oidc"
|
||||
"github.com/authelia/authelia/internal/regulation"
|
||||
"github.com/authelia/authelia/internal/session"
|
||||
"github.com/authelia/authelia/internal/storage"
|
||||
|
@ -31,6 +32,7 @@ type Providers struct {
|
|||
Authorizer *authorization.Authorizer
|
||||
SessionProvider *session.Provider
|
||||
Regulator *regulation.Regulator
|
||||
OpenIDConnect oidc.OpenIDConnectProvider
|
||||
|
||||
UserProvider authentication.UserProvider
|
||||
StorageProvider storage.Provider
|
||||
|
@ -43,6 +45,9 @@ type RequestHandler = func(*AutheliaCtx)
|
|||
// Middleware represent an Authelia middleware.
|
||||
type Middleware = func(RequestHandler) RequestHandler
|
||||
|
||||
// RequestHandlerBridge bridge a AutheliaCtx handle to a RequestHandler handler.
|
||||
type RequestHandlerBridge = func(RequestHandler) fasthttp.RequestHandler
|
||||
|
||||
// IdentityVerificationStartArgs represent the arguments used to customize the starting phase
|
||||
// of the identity verification process.
|
||||
type IdentityVerificationStartArgs struct {
|
||||
|
|
75
internal/oidc/client.go
Normal file
75
internal/oidc/client.go
Normal file
|
@ -0,0 +1,75 @@
|
|||
package oidc
|
||||
|
||||
import (
|
||||
"github.com/ory/fosite"
|
||||
|
||||
"github.com/authelia/authelia/internal/authentication"
|
||||
"github.com/authelia/authelia/internal/authorization"
|
||||
)
|
||||
|
||||
// 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:"-"`
|
||||
}
|
||||
|
||||
// IsAuthenticationLevelSufficient returns if the provided authentication.Level is sufficient for the client of the AutheliaClient.
|
||||
func (c InternalClient) IsAuthenticationLevelSufficient(level authentication.Level) bool {
|
||||
return authorization.IsAuthLevelSufficient(level, c.Policy)
|
||||
}
|
||||
|
||||
// GetID returns the ID.
|
||||
func (c InternalClient) GetID() string {
|
||||
return c.ID
|
||||
}
|
||||
|
||||
// GetHashedSecret returns the Secret.
|
||||
func (c InternalClient) GetHashedSecret() []byte {
|
||||
return c.Secret
|
||||
}
|
||||
|
||||
// GetRedirectURIs returns the RedirectURIs.
|
||||
func (c InternalClient) GetRedirectURIs() []string {
|
||||
return c.RedirectURIs
|
||||
}
|
||||
|
||||
// GetGrantTypes returns the GrantTypes.
|
||||
func (c InternalClient) GetGrantTypes() fosite.Arguments {
|
||||
if len(c.GrantTypes) == 0 {
|
||||
return fosite.Arguments{"authorization_code"}
|
||||
}
|
||||
|
||||
return c.GrantTypes
|
||||
}
|
||||
|
||||
// GetResponseTypes returns the ResponseTypes.
|
||||
func (c InternalClient) GetResponseTypes() fosite.Arguments {
|
||||
if len(c.ResponseTypes) == 0 {
|
||||
return fosite.Arguments{"code"}
|
||||
}
|
||||
|
||||
return c.ResponseTypes
|
||||
}
|
||||
|
||||
// GetScopes returns the Scopes.
|
||||
func (c InternalClient) GetScopes() fosite.Arguments {
|
||||
return c.Scopes
|
||||
}
|
||||
|
||||
// IsPublic returns the value of the Public property.
|
||||
func (c InternalClient) IsPublic() bool {
|
||||
return c.Public
|
||||
}
|
||||
|
||||
// GetAudience returns the Audience.
|
||||
func (c InternalClient) GetAudience() fosite.Arguments {
|
||||
return c.Audience
|
||||
}
|
34
internal/oidc/client_test.go
Normal file
34
internal/oidc/client_test.go
Normal file
|
@ -0,0 +1,34 @@
|
|||
package oidc
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/authelia/authelia/internal/authentication"
|
||||
"github.com/authelia/authelia/internal/authorization"
|
||||
)
|
||||
|
||||
func TestIsAuthenticationLevelSufficient(t *testing.T) {
|
||||
c := InternalClient{}
|
||||
|
||||
c.Policy = authorization.Bypass
|
||||
assert.True(t, c.IsAuthenticationLevelSufficient(authentication.NotAuthenticated))
|
||||
assert.True(t, c.IsAuthenticationLevelSufficient(authentication.OneFactor))
|
||||
assert.True(t, c.IsAuthenticationLevelSufficient(authentication.TwoFactor))
|
||||
|
||||
c.Policy = authorization.OneFactor
|
||||
assert.False(t, c.IsAuthenticationLevelSufficient(authentication.NotAuthenticated))
|
||||
assert.True(t, c.IsAuthenticationLevelSufficient(authentication.OneFactor))
|
||||
assert.True(t, c.IsAuthenticationLevelSufficient(authentication.TwoFactor))
|
||||
|
||||
c.Policy = authorization.TwoFactor
|
||||
assert.False(t, c.IsAuthenticationLevelSufficient(authentication.NotAuthenticated))
|
||||
assert.False(t, c.IsAuthenticationLevelSufficient(authentication.OneFactor))
|
||||
assert.True(t, c.IsAuthenticationLevelSufficient(authentication.TwoFactor))
|
||||
|
||||
c.Policy = authorization.Denied
|
||||
assert.False(t, c.IsAuthenticationLevelSufficient(authentication.NotAuthenticated))
|
||||
assert.False(t, c.IsAuthenticationLevelSufficient(authentication.OneFactor))
|
||||
assert.False(t, c.IsAuthenticationLevelSufficient(authentication.TwoFactor))
|
||||
}
|
5
internal/oidc/errors.go
Normal file
5
internal/oidc/errors.go
Normal file
|
@ -0,0 +1,5 @@
|
|||
package oidc
|
||||
|
||||
import "errors"
|
||||
|
||||
var errPasswordsDoNotMatch = errors.New("the passwords don't match")
|
24
internal/oidc/hasher.go
Normal file
24
internal/oidc/hasher.go
Normal file
|
@ -0,0 +1,24 @@
|
|||
package oidc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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.
|
||||
func (h AutheliaHasher) Compare(ctx context.Context, hash, data []byte) (err error) {
|
||||
if subtle.ConstantTimeCompare(hash, data) == 0 {
|
||||
return errPasswordsDoNotMatch
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Hash creates a new hash from data.
|
||||
func (h AutheliaHasher) Hash(ctx context.Context, data []byte) (hash []byte, err error) {
|
||||
return data, nil
|
||||
}
|
47
internal/oidc/hasher_test.go
Normal file
47
internal/oidc/hasher_test.go
Normal file
|
@ -0,0 +1,47 @@
|
|||
package oidc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestShouldNotRaiseErrorOnEqualPasswordsPlainText(t *testing.T) {
|
||||
hasher := AutheliaHasher{}
|
||||
|
||||
a := []byte("abc")
|
||||
b := []byte("abc")
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
err := hasher.Compare(ctx, a, b)
|
||||
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestShouldRaiseErrorOnNonEqualPasswordsPlainText(t *testing.T) {
|
||||
hasher := AutheliaHasher{}
|
||||
|
||||
a := []byte("abc")
|
||||
b := []byte("abcd")
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
err := hasher.Compare(ctx, a, b)
|
||||
|
||||
assert.Equal(t, errPasswordsDoNotMatch, err)
|
||||
}
|
||||
|
||||
func TestShouldHashPassword(t *testing.T) {
|
||||
hasher := AutheliaHasher{}
|
||||
|
||||
data := []byte("abc")
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
hash, err := hasher.Hash(ctx, data)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, data, hash)
|
||||
}
|
108
internal/oidc/provider.go
Normal file
108
internal/oidc/provider.go
Normal file
|
@ -0,0 +1,108 @@
|
|||
package oidc
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"fmt"
|
||||
|
||||
"github.com/ory/fosite"
|
||||
"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/utils"
|
||||
)
|
||||
|
||||
// OpenIDConnectProvider for OpenID Connect.
|
||||
type OpenIDConnectProvider struct {
|
||||
privateKeys map[string]*rsa.PrivateKey
|
||||
|
||||
Fosite fosite.OAuth2Provider
|
||||
Store *OpenIDConnectStore
|
||||
}
|
||||
|
||||
// NewOpenIDConnectProvider new-ups a OpenIDConnectProvider.
|
||||
func NewOpenIDConnectProvider(configuration *schema.OpenIDConnectConfiguration) (provider OpenIDConnectProvider, err error) {
|
||||
provider = OpenIDConnectProvider{
|
||||
Fosite: nil,
|
||||
}
|
||||
|
||||
if configuration == nil {
|
||||
return provider, nil
|
||||
}
|
||||
|
||||
provider.Store = NewOpenIDConnectStore(configuration)
|
||||
|
||||
composeConfiguration := new(compose.Config)
|
||||
|
||||
key, err := utils.ParseRsaPrivateKeyFromPemStr(configuration.IssuerPrivateKey)
|
||||
if err != nil {
|
||||
return provider, fmt.Errorf("unable to parse the private key of the OpenID issuer: %w", err)
|
||||
}
|
||||
|
||||
provider.privateKeys = make(map[string]*rsa.PrivateKey)
|
||||
provider.privateKeys["main-key"] = key
|
||||
|
||||
// TODO: Consider implementing RS512 as well.
|
||||
jwtStrategy := &jwt.RS256JWTStrategy{PrivateKey: key}
|
||||
|
||||
strategy := &compose.CommonStrategy{
|
||||
CoreStrategy: compose.NewOAuth2HMACStrategy(
|
||||
composeConfiguration,
|
||||
[]byte(utils.HashSHA256FromString(configuration.HMACSecret)),
|
||||
nil,
|
||||
),
|
||||
OpenIDConnectTokenStrategy: compose.NewOpenIDConnectStrategy(
|
||||
composeConfiguration,
|
||||
provider.privateKeys["main-key"],
|
||||
),
|
||||
JWTStrategy: jwtStrategy,
|
||||
}
|
||||
|
||||
provider.Fosite = compose.Compose(
|
||||
composeConfiguration,
|
||||
provider.Store,
|
||||
strategy,
|
||||
AutheliaHasher{},
|
||||
|
||||
/*
|
||||
These are the OAuth2 and OpenIDConnect factories. Order is important (the OAuth2 factories at the top must
|
||||
be before the OpenIDConnect factories) and taken directly from fosite.compose.ComposeAllEnabled. The
|
||||
commented factories are not enabled as we don't yet use them but are still here for reference purposes.
|
||||
*/
|
||||
compose.OAuth2AuthorizeExplicitFactory,
|
||||
compose.OAuth2AuthorizeImplicitFactory,
|
||||
compose.OAuth2ClientCredentialsGrantFactory,
|
||||
compose.OAuth2RefreshTokenGrantFactory,
|
||||
compose.OAuth2ResourceOwnerPasswordCredentialsFactory,
|
||||
// compose.RFC7523AssertionGrantFactory,
|
||||
|
||||
compose.OpenIDConnectExplicitFactory,
|
||||
compose.OpenIDConnectImplicitFactory,
|
||||
compose.OpenIDConnectHybridFactory,
|
||||
compose.OpenIDConnectRefreshFactory,
|
||||
|
||||
compose.OAuth2TokenIntrospectionFactory,
|
||||
compose.OAuth2TokenRevocationFactory,
|
||||
|
||||
// compose.OAuth2PKCEFactory,
|
||||
)
|
||||
|
||||
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
|
||||
}
|
40
internal/oidc/provider_test.go
Normal file
40
internal/oidc/provider_test.go
Normal file
|
@ -0,0 +1,40 @@
|
|||
package oidc
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/authelia/authelia/internal/configuration/schema"
|
||||
)
|
||||
|
||||
var exampleIssuerPrivateKey = "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAvcMVMB2vEbqI6PlSNJ4HmUyMxBDJ5iY7FS+zDDAHOZBg9S3S\nKcAn1CZcnyL0VvJ7wcdhR6oTnOwR94eKvzUyJZ+GL2hTMm27dubEYsNdhoCl6N3X\nyEEohNfoxiiCYraVauX8X3M9jFzbEz9+pacaDbHB2syaJ1qFmMNR+HSu2jPzOo7M\nlqKIOgUzA0741MaYNt47AEVg4XU5ORLdolbAkItmYg1QbyFndg9H5IvwKkYaXTGE\nlgDBcPUC0yVjAC15Mguquq+jZeQay+6PSbHTD8PQMOkLjyChI2xEhVNbdCXe676R\ncMW2R/gjrcK23zmtmTWRfdC1iZLSlHO+bJj9vQIDAQABAoIBAEZvkP/JJOCJwqPn\nV3IcbmmilmV4bdi1vByDFgyiDyx4wOSA24+PubjvfFW9XcCgRPuKjDtTj/AhWBHv\nB7stfa2lZuNV7/u562mZArA+IAr62Zp0LdIxDV8x3T8gbjVB3HhPYbv0RJZDKTYd\nzV6jhfIrVu9mHpoY6ZnodhapCPYIyk/d49KBIHZuAc25CUjMXgTeaVtf0c996036\nUxW6ef33wAOJAvW0RCvbXAJfmBeEq2qQlkjTIlpYx71fhZWexHifi8Ouv3Zonc+1\n/P2Adq5uzYVBT92f9RKHg9QxxNzVrLjSMaxyvUtWQCAQfW0tFIRdqBGsHYsQrFtI\nF4yzv8ECgYEA7ntpyN9HD9Z9lYQzPCR73sFCLM+ID99aVij0wHuxK97bkSyyvkLd\n7MyTaym3lg1UEqWNWBCLvFULZx7F0Ah6qCzD4ymm3Bj/ADpWWPgljBI0AFml+HHs\nhcATmXUrj5QbLyhiP2gmJjajp1o/rgATx6ED66seSynD6JOH8wUhhZUCgYEAy7OA\n06PF8GfseNsTqlDjNF0K7lOqd21S0prdwrsJLiVzUlfMM25MLE0XLDUutCnRheeh\nIlcuDoBsVTxz6rkvFGD74N+pgXlN4CicsBq5ofK060PbqCQhSII3fmHobrZ9Cr75\nHmBjAxHx998SKaAAGbBbcYGUAp521i1pH5CEPYkCgYEAkUd1Zf0+2RMdZhwm6hh/\nrW+l1I6IoMK70YkZsLipccRNld7Y9LbfYwYtODcts6di9AkOVfueZJiaXbONZfIE\nZrb+jkAteh9wGL9xIrnohbABJcV3Kiaco84jInUSmGDtPokncOENfHIEuEpuSJ2b\nbx1TuhmAVuGWivR0+ULC7RECgYEAgS0cDRpWc9Xzh9Cl7+PLsXEvdWNpPsL9OsEq\n0Ep7z9+/+f/jZtoTRCS/BTHUpDvAuwHglT5j3p5iFMt5VuiIiovWLwynGYwrbnNS\nqfrIrYKUaH1n1oDS+oBZYLQGCe9/7EifAjxtjYzbvSyg//SPG7tSwfBCREbpZXj2\nqSWkNsECgYA/mCDzCTlrrWPuiepo6kTmN+4TnFA+hJI6NccDVQ+jvbqEdoJ4SW4L\nzqfZSZRFJMNpSgIqkQNRPJqMP0jQ5KRtJrjMWBnYxktwKz9fDg2R2MxdFgMF2LH2\nHEMMhFHlv8NDjVOXh1KwRoltNGVWYsSrD9wKU9GhRCEfmNCGrvBcEg==\n-----END RSA PRIVATE KEY-----"
|
||||
|
||||
func TestOpenIDConnectProvider_NewOpenIDConnectProvider_NotConfigured(t *testing.T) {
|
||||
provider, err := NewOpenIDConnectProvider(nil)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, provider.Fosite)
|
||||
assert.Nil(t, provider.Store)
|
||||
}
|
||||
|
||||
func TestOpenIDConnectProvider_NewOpenIDConnectProvider_BadIssuerKey(t *testing.T) {
|
||||
_, err := NewOpenIDConnectProvider(&schema.OpenIDConnectConfiguration{
|
||||
IssuerPrivateKey: "BAD KEY",
|
||||
})
|
||||
|
||||
assert.Error(t, err, "abc")
|
||||
}
|
||||
|
||||
func TestOpenIDConnectProvider_GetKeySet(t *testing.T) {
|
||||
p, err := NewOpenIDConnectProvider(&schema.OpenIDConnectConfiguration{
|
||||
IssuerPrivateKey: exampleIssuerPrivateKey,
|
||||
})
|
||||
|
||||
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)
|
||||
}
|
219
internal/oidc/store.go
Normal file
219
internal/oidc/store.go
Normal file
|
@ -0,0 +1,219 @@
|
|||
package oidc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/ory/fosite"
|
||||
"github.com/ory/fosite/storage"
|
||||
"gopkg.in/square/go-jose.v2"
|
||||
|
||||
"github.com/authelia/authelia/internal/authorization"
|
||||
"github.com/authelia/authelia/internal/configuration/schema"
|
||||
"github.com/authelia/authelia/internal/logging"
|
||||
)
|
||||
|
||||
// NewOpenIDConnectStore returns a new OpenIDConnectStore using the provided schema.OpenIDConnectConfiguration.
|
||||
func NewOpenIDConnectStore(configuration *schema.OpenIDConnectConfiguration) (store *OpenIDConnectStore) {
|
||||
store = &OpenIDConnectStore{}
|
||||
|
||||
store.clients = make(map[string]*InternalClient)
|
||||
|
||||
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{},
|
||||
AuthorizeCodes: map[string]storage.StoreAuthorizeCode{},
|
||||
AccessTokens: map[string]fosite.Requester{},
|
||||
RefreshTokens: map[string]storage.StoreRefreshToken{},
|
||||
PKCES: map[string]fosite.Requester{},
|
||||
AccessTokenRequestIDs: map[string]string{},
|
||||
RefreshTokenRequestIDs: map[string]string{},
|
||||
}
|
||||
|
||||
return store
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// GetClientPolicy retrieves the policy from the client with the matching provided id.
|
||||
func (s OpenIDConnectStore) GetClientPolicy(id string) (level authorization.Level) {
|
||||
client, err := s.GetInternalClient(id)
|
||||
if err != nil {
|
||||
return authorization.TwoFactor
|
||||
}
|
||||
|
||||
return client.Policy
|
||||
}
|
||||
|
||||
// GetInternalClient returns a fosite.Client asserted as an InternalClient matching the provided id.
|
||||
func (s OpenIDConnectStore) GetInternalClient(id string) (client *InternalClient, err error) {
|
||||
client, ok := s.clients[id]
|
||||
if !ok {
|
||||
return nil, fosite.ErrNotFound
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// IsValidClientID returns true if the provided id exists in the OpenIDConnectProvider.Clients map.
|
||||
func (s OpenIDConnectStore) IsValidClientID(id string) (valid bool) {
|
||||
_, err := s.GetInternalClient(id)
|
||||
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// CreateOpenIDConnectSession decorates fosite's storage.MemoryStore CreateOpenIDConnectSession method.
|
||||
func (s *OpenIDConnectStore) CreateOpenIDConnectSession(ctx context.Context, authorizeCode string, requester fosite.Requester) error {
|
||||
return s.memory.CreateOpenIDConnectSession(ctx, authorizeCode, requester)
|
||||
}
|
||||
|
||||
// GetOpenIDConnectSession decorates fosite's storage.MemoryStore GetOpenIDConnectSession method.
|
||||
func (s *OpenIDConnectStore) GetOpenIDConnectSession(ctx context.Context, authorizeCode string, requester fosite.Requester) (fosite.Requester, error) {
|
||||
return s.memory.GetOpenIDConnectSession(ctx, authorizeCode, requester)
|
||||
}
|
||||
|
||||
// DeleteOpenIDConnectSession decorates fosite's storage.MemoryStore DeleteOpenIDConnectSession method.
|
||||
func (s *OpenIDConnectStore) DeleteOpenIDConnectSession(ctx context.Context, authorizeCode string) error {
|
||||
return s.memory.DeleteOpenIDConnectSession(ctx, authorizeCode)
|
||||
}
|
||||
|
||||
// GetClient decorates fosite's storage.MemoryStore GetClient method.
|
||||
func (s *OpenIDConnectStore) GetClient(_ context.Context, id string) (fosite.Client, error) {
|
||||
return s.GetInternalClient(id)
|
||||
}
|
||||
|
||||
// ClientAssertionJWTValid decorates fosite's storage.MemoryStore ClientAssertionJWTValid method.
|
||||
func (s *OpenIDConnectStore) ClientAssertionJWTValid(ctx context.Context, jti string) error {
|
||||
return s.memory.ClientAssertionJWTValid(ctx, jti)
|
||||
}
|
||||
|
||||
// SetClientAssertionJWT decorates fosite's storage.MemoryStore SetClientAssertionJWT method.
|
||||
func (s *OpenIDConnectStore) SetClientAssertionJWT(ctx context.Context, jti string, exp time.Time) error {
|
||||
return s.memory.SetClientAssertionJWT(ctx, jti, exp)
|
||||
}
|
||||
|
||||
// CreateAuthorizeCodeSession decorates fosite's storage.MemoryStore CreateAuthorizeCodeSession method.
|
||||
func (s *OpenIDConnectStore) CreateAuthorizeCodeSession(ctx context.Context, code string, req fosite.Requester) error {
|
||||
return s.memory.CreateAuthorizeCodeSession(ctx, code, req)
|
||||
}
|
||||
|
||||
// GetAuthorizeCodeSession decorates fosite's storage.MemoryStore GetAuthorizeCodeSession method.
|
||||
func (s *OpenIDConnectStore) GetAuthorizeCodeSession(ctx context.Context, code string, session fosite.Session) (fosite.Requester, error) {
|
||||
return s.memory.GetAuthorizeCodeSession(ctx, code, session)
|
||||
}
|
||||
|
||||
// InvalidateAuthorizeCodeSession decorates fosite's storage.MemoryStore InvalidateAuthorizeCodeSession method.
|
||||
func (s *OpenIDConnectStore) InvalidateAuthorizeCodeSession(ctx context.Context, code string) error {
|
||||
return s.memory.InvalidateAuthorizeCodeSession(ctx, code)
|
||||
}
|
||||
|
||||
// CreatePKCERequestSession decorates fosite's storage.MemoryStore CreatePKCERequestSession method.
|
||||
func (s *OpenIDConnectStore) CreatePKCERequestSession(ctx context.Context, code string, req fosite.Requester) error {
|
||||
return s.memory.CreatePKCERequestSession(ctx, code, req)
|
||||
}
|
||||
|
||||
// GetPKCERequestSession decorates fosite's storage.MemoryStore GetPKCERequestSession method.
|
||||
func (s *OpenIDConnectStore) GetPKCERequestSession(ctx context.Context, code string, session fosite.Session) (fosite.Requester, error) {
|
||||
return s.memory.GetPKCERequestSession(ctx, code, session)
|
||||
}
|
||||
|
||||
// DeletePKCERequestSession decorates fosite's storage.MemoryStore DeletePKCERequestSession method.
|
||||
func (s *OpenIDConnectStore) DeletePKCERequestSession(ctx context.Context, code string) error {
|
||||
return s.memory.DeletePKCERequestSession(ctx, code)
|
||||
}
|
||||
|
||||
// CreateAccessTokenSession decorates fosite's storage.MemoryStore CreateAccessTokenSession method.
|
||||
func (s *OpenIDConnectStore) CreateAccessTokenSession(ctx context.Context, signature string, req fosite.Requester) error {
|
||||
return s.memory.CreateAccessTokenSession(ctx, signature, req)
|
||||
}
|
||||
|
||||
// GetAccessTokenSession decorates fosite's storage.MemoryStore GetAccessTokenSession method.
|
||||
func (s *OpenIDConnectStore) GetAccessTokenSession(ctx context.Context, signature string, session fosite.Session) (fosite.Requester, error) {
|
||||
return s.memory.GetAccessTokenSession(ctx, signature, session)
|
||||
}
|
||||
|
||||
// DeleteAccessTokenSession decorates fosite's storage.MemoryStore DeleteAccessTokenSession method.
|
||||
func (s *OpenIDConnectStore) DeleteAccessTokenSession(ctx context.Context, signature string) error {
|
||||
return s.memory.DeleteAccessTokenSession(ctx, signature)
|
||||
}
|
||||
|
||||
// CreateRefreshTokenSession decorates fosite's storage.MemoryStore CreateRefreshTokenSession method.
|
||||
func (s *OpenIDConnectStore) CreateRefreshTokenSession(ctx context.Context, signature string, req fosite.Requester) error {
|
||||
return s.memory.CreateRefreshTokenSession(ctx, signature, req)
|
||||
}
|
||||
|
||||
// GetRefreshTokenSession decorates fosite's storage.MemoryStore GetRefreshTokenSession method.
|
||||
func (s *OpenIDConnectStore) GetRefreshTokenSession(ctx context.Context, signature string, session fosite.Session) (fosite.Requester, error) {
|
||||
return s.memory.GetRefreshTokenSession(ctx, signature, session)
|
||||
}
|
||||
|
||||
// DeleteRefreshTokenSession decorates fosite's storage.MemoryStore DeleteRefreshTokenSession method.
|
||||
func (s *OpenIDConnectStore) DeleteRefreshTokenSession(ctx context.Context, signature string) error {
|
||||
return s.memory.DeleteRefreshTokenSession(ctx, signature)
|
||||
}
|
||||
|
||||
// Authenticate decorates fosite's storage.MemoryStore Authenticate method.
|
||||
func (s *OpenIDConnectStore) Authenticate(ctx context.Context, name string, secret string) error {
|
||||
return s.memory.Authenticate(ctx, name, secret)
|
||||
}
|
||||
|
||||
// RevokeRefreshToken decorates fosite's storage.MemoryStore RevokeRefreshToken method.
|
||||
func (s *OpenIDConnectStore) RevokeRefreshToken(ctx context.Context, requestID string) error {
|
||||
return s.memory.RevokeRefreshToken(ctx, requestID)
|
||||
}
|
||||
|
||||
// RevokeAccessToken decorates fosite's storage.MemoryStore RevokeAccessToken method.
|
||||
func (s *OpenIDConnectStore) RevokeAccessToken(ctx context.Context, requestID string) error {
|
||||
return s.memory.RevokeAccessToken(ctx, requestID)
|
||||
}
|
||||
|
||||
// GetPublicKey decorates fosite's storage.MemoryStore GetPublicKey method.
|
||||
func (s *OpenIDConnectStore) GetPublicKey(ctx context.Context, issuer string, subject string, keyID string) (*jose.JSONWebKey, error) {
|
||||
return s.memory.GetPublicKey(ctx, issuer, subject, keyID)
|
||||
}
|
||||
|
||||
// GetPublicKeys decorates fosite's storage.MemoryStore GetPublicKeys method.
|
||||
func (s *OpenIDConnectStore) GetPublicKeys(ctx context.Context, issuer string, subject string) (*jose.JSONWebKeySet, error) {
|
||||
return s.memory.GetPublicKeys(ctx, issuer, subject)
|
||||
}
|
||||
|
||||
// GetPublicKeyScopes decorates fosite's storage.MemoryStore GetPublicKeyScopes method.
|
||||
func (s *OpenIDConnectStore) GetPublicKeyScopes(ctx context.Context, issuer string, subject string, keyID string) ([]string, error) {
|
||||
return s.memory.GetPublicKeyScopes(ctx, issuer, subject, keyID)
|
||||
}
|
||||
|
||||
// IsJWTUsed decorates fosite's storage.MemoryStore IsJWTUsed method.
|
||||
func (s *OpenIDConnectStore) IsJWTUsed(ctx context.Context, jti string) (bool, error) {
|
||||
return s.memory.IsJWTUsed(ctx, jti)
|
||||
}
|
||||
|
||||
// MarkJWTUsedForTime decorates fosite's storage.MemoryStore MarkJWTUsedForTime method.
|
||||
func (s *OpenIDConnectStore) MarkJWTUsedForTime(ctx context.Context, jti string, exp time.Time) error {
|
||||
return s.memory.MarkJWTUsedForTime(ctx, jti, exp)
|
||||
}
|
132
internal/oidc/store_test.go
Normal file
132
internal/oidc/store_test.go
Normal file
|
@ -0,0 +1,132 @@
|
|||
package oidc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/authelia/authelia/internal/authorization"
|
||||
"github.com/authelia/authelia/internal/configuration/schema"
|
||||
)
|
||||
|
||||
func TestOpenIDConnectStore_GetClientPolicy(t *testing.T) {
|
||||
s := NewOpenIDConnectStore(&schema.OpenIDConnectConfiguration{
|
||||
IssuerPrivateKey: exampleIssuerPrivateKey,
|
||||
Clients: []schema.OpenIDConnectClientConfiguration{
|
||||
{
|
||||
ID: "myclient",
|
||||
Description: "myclient desc",
|
||||
Policy: "one_factor",
|
||||
Scopes: []string{"openid", "profile"},
|
||||
Secret: "mysecret",
|
||||
},
|
||||
{
|
||||
ID: "myotherclient",
|
||||
Description: "myclient desc",
|
||||
Policy: "two_factor",
|
||||
Scopes: []string{"openid", "profile"},
|
||||
Secret: "mysecret",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
policyOne := s.GetClientPolicy("myclient")
|
||||
assert.Equal(t, authorization.OneFactor, policyOne)
|
||||
|
||||
policyTwo := s.GetClientPolicy("myotherclient")
|
||||
assert.Equal(t, authorization.TwoFactor, policyTwo)
|
||||
|
||||
policyInvalid := s.GetClientPolicy("invalidclient")
|
||||
assert.Equal(t, authorization.TwoFactor, policyInvalid)
|
||||
}
|
||||
|
||||
func TestOpenIDConnectStore_GetInternalClient(t *testing.T) {
|
||||
s := NewOpenIDConnectStore(&schema.OpenIDConnectConfiguration{
|
||||
IssuerPrivateKey: exampleIssuerPrivateKey,
|
||||
Clients: []schema.OpenIDConnectClientConfiguration{
|
||||
{
|
||||
ID: "myclient",
|
||||
Description: "myclient desc",
|
||||
Policy: "one_factor",
|
||||
Scopes: []string{"openid", "profile"},
|
||||
Secret: "mysecret",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client, err := s.GetClient(context.Background(), "myinvalidclient")
|
||||
assert.EqualError(t, err, "not_found")
|
||||
assert.Nil(t, client)
|
||||
|
||||
client, err = s.GetClient(context.Background(), "myclient")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, client)
|
||||
assert.Equal(t, "myclient", client.GetID())
|
||||
}
|
||||
|
||||
func TestOpenIDConnectStore_GetInternalClient_ValidClient(t *testing.T) {
|
||||
c1 := schema.OpenIDConnectClientConfiguration{
|
||||
ID: "myclient",
|
||||
Description: "myclient desc",
|
||||
Policy: "one_factor",
|
||||
Scopes: []string{"openid", "profile"},
|
||||
Secret: "mysecret",
|
||||
}
|
||||
s := NewOpenIDConnectStore(&schema.OpenIDConnectConfiguration{
|
||||
IssuerPrivateKey: exampleIssuerPrivateKey,
|
||||
Clients: []schema.OpenIDConnectClientConfiguration{c1},
|
||||
})
|
||||
|
||||
client, err := s.GetInternalClient(c1.ID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, client)
|
||||
assert.Equal(t, client.ID, c1.ID)
|
||||
assert.Equal(t, client.Description, c1.Description)
|
||||
assert.Equal(t, client.Scopes, c1.Scopes)
|
||||
assert.Equal(t, client.GrantTypes, c1.GrantTypes)
|
||||
assert.Equal(t, client.ResponseTypes, c1.ResponseTypes)
|
||||
assert.Equal(t, client.RedirectURIs, c1.RedirectURIs)
|
||||
assert.Equal(t, client.Policy, authorization.OneFactor)
|
||||
assert.Equal(t, client.Secret, []byte(c1.Secret))
|
||||
}
|
||||
|
||||
func TestOpenIDConnectStore_GetInternalClient_InvalidClient(t *testing.T) {
|
||||
c1 := schema.OpenIDConnectClientConfiguration{
|
||||
ID: "myclient",
|
||||
Description: "myclient desc",
|
||||
Policy: "one_factor",
|
||||
Scopes: []string{"openid", "profile"},
|
||||
Secret: "mysecret",
|
||||
}
|
||||
s := NewOpenIDConnectStore(&schema.OpenIDConnectConfiguration{
|
||||
IssuerPrivateKey: exampleIssuerPrivateKey,
|
||||
Clients: []schema.OpenIDConnectClientConfiguration{c1},
|
||||
})
|
||||
|
||||
client, err := s.GetInternalClient("another-client")
|
||||
assert.Nil(t, client)
|
||||
assert.EqualError(t, err, "not_found")
|
||||
}
|
||||
|
||||
func TestOpenIDConnectStore_IsValidClientID(t *testing.T) {
|
||||
s := NewOpenIDConnectStore(&schema.OpenIDConnectConfiguration{
|
||||
IssuerPrivateKey: exampleIssuerPrivateKey,
|
||||
Clients: []schema.OpenIDConnectClientConfiguration{
|
||||
{
|
||||
ID: "myclient",
|
||||
Description: "myclient desc",
|
||||
Policy: "one_factor",
|
||||
Scopes: []string{"openid", "profile"},
|
||||
Secret: "mysecret",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
validClient := s.IsValidClientID("myclient")
|
||||
invalidClient := s.IsValidClientID("myinvalidclient")
|
||||
|
||||
assert.True(t, validClient)
|
||||
assert.False(t, invalidClient)
|
||||
}
|
|
@ -28,9 +28,7 @@ import (
|
|||
//go:embed public_html
|
||||
var assets embed.FS
|
||||
|
||||
// StartServer start Authelia server with the given configuration and providers.
|
||||
func StartServer(configuration schema.Configuration, providers middlewares.Providers) {
|
||||
logger := logging.Logger()
|
||||
func registerRoutes(configuration schema.Configuration, providers middlewares.Providers) fasthttp.RequestHandler {
|
||||
autheliaMiddleware := middlewares.AutheliaMiddleware(configuration, providers)
|
||||
rememberMe := strconv.FormatBool(configuration.Session.RememberMeDuration != "0")
|
||||
resetPassword := strconv.FormatBool(!configuration.AuthenticationBackend.DisableResetPassword)
|
||||
|
@ -142,6 +140,19 @@ func StartServer(configuration schema.Configuration, providers middlewares.Provi
|
|||
handler = middlewares.StripPathMiddleware(handler)
|
||||
}
|
||||
|
||||
if providers.OpenIDConnect.Fosite != nil {
|
||||
handlers.RegisterOIDC(r, autheliaMiddleware)
|
||||
}
|
||||
|
||||
return handler
|
||||
}
|
||||
|
||||
// StartServer start Authelia server with the given configuration and providers.
|
||||
func StartServer(configuration schema.Configuration, providers middlewares.Providers) {
|
||||
logger := logging.Logger()
|
||||
|
||||
handler := registerRoutes(configuration, providers)
|
||||
|
||||
server := &fasthttp.Server{
|
||||
ErrorHandler: autheliaErrorHandler,
|
||||
Handler: handler,
|
||||
|
@ -157,6 +168,7 @@ func StartServer(configuration schema.Configuration, providers middlewares.Provi
|
|||
logger.Fatalf("Error initializing listener: %s", err)
|
||||
}
|
||||
|
||||
// TODO(clems4ever): move that piece to a more related location, probably in the configuration package.
|
||||
if configuration.AuthenticationBackend.File != nil && configuration.AuthenticationBackend.File.Password.Algorithm == "argon2id" && runtime.GOOS == "linux" {
|
||||
f, err := ioutil.ReadFile("/sys/fs/cgroup/memory/memory.limit_in_bytes")
|
||||
if err != nil {
|
||||
|
|
|
@ -87,6 +87,7 @@ func (p *Provider) GetSession(ctx *fasthttp.RequestCtx) (UserSession, error) {
|
|||
// and save it in the store.
|
||||
if !ok {
|
||||
userSession := NewDefaultUserSession()
|
||||
|
||||
store.Set(userSessionStorerKey, userSession)
|
||||
|
||||
return userSession, nil
|
||||
|
@ -130,6 +131,7 @@ func (p *Provider) SaveSession(ctx *fasthttp.RequestCtx, userSession UserSession
|
|||
// RegenerateSession regenerate a session ID.
|
||||
func (p *Provider) RegenerateSession(ctx *fasthttp.RequestCtx) error {
|
||||
err := p.sessionHolder.Regenerate(ctx)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"github.com/tstranex/u2f"
|
||||
|
||||
"github.com/authelia/authelia/internal/authentication"
|
||||
"github.com/authelia/authelia/internal/authorization"
|
||||
)
|
||||
|
||||
// ProviderConfig is the configuration used to create the session provider.
|
||||
|
@ -43,6 +44,9 @@ type UserSession struct {
|
|||
// This is used in second phase of a U2F authentication.
|
||||
U2FRegistration *U2FRegistration
|
||||
|
||||
// Represent an OIDC workflow session initiated by the client if not null.
|
||||
OIDCWorkflowSession *OIDCWorkflowSession
|
||||
|
||||
// This boolean is set to true after identity verification and checked
|
||||
// while doing the query actually updating the password.
|
||||
PasswordResetUsername *string
|
||||
|
@ -55,3 +59,15 @@ type Identity struct {
|
|||
Username string
|
||||
Email string
|
||||
}
|
||||
|
||||
// OIDCWorkflowSession represent an OIDC workflow session.
|
||||
type OIDCWorkflowSession struct {
|
||||
ClientID string
|
||||
RequestedScopes []string
|
||||
GrantedScopes []string
|
||||
RequestedAudience []string
|
||||
GrantedAudience []string
|
||||
TargetURI string
|
||||
AuthURI string
|
||||
RequiredAuthorizationLevel authorization.Level
|
||||
}
|
||||
|
|
99
internal/suites/OIDC/configuration.yml
Normal file
99
internal/suites/OIDC/configuration.yml
Normal file
|
@ -0,0 +1,99 @@
|
|||
---
|
||||
port: 9091
|
||||
tls_cert: /config/ssl/cert.pem
|
||||
tls_key: /config/ssl/key.pem
|
||||
|
||||
log_level: debug
|
||||
|
||||
jwt_secret: unsecure_secret
|
||||
|
||||
authentication_backend:
|
||||
file:
|
||||
path: /config/users.yml
|
||||
|
||||
session:
|
||||
secret: unsecure_session_secret
|
||||
domain: example.com
|
||||
expiration: 3600 # 1 hour
|
||||
inactivity: 300 # 5 minutes
|
||||
remember_me_duration: 1y
|
||||
# We use redis here to keep the users authenticated when Authelia restarts
|
||||
# It eases development.
|
||||
redis:
|
||||
host: redis
|
||||
port: 6379
|
||||
|
||||
storage:
|
||||
local:
|
||||
path: /config/db.sqlite
|
||||
|
||||
access_control:
|
||||
default_policy: deny
|
||||
rules:
|
||||
- domain: "home.example.com"
|
||||
policy: bypass
|
||||
- domain: "public.example.com"
|
||||
policy: bypass
|
||||
- domain: "admin.example.com"
|
||||
policy: two_factor
|
||||
- domain: "secure.example.com"
|
||||
policy: two_factor
|
||||
- domain: "singlefactor.example.com"
|
||||
policy: one_factor
|
||||
- domain: "oidc.example.com"
|
||||
policy: two_factor
|
||||
- domain: "oidc-public.example.com"
|
||||
policy: bypass
|
||||
|
||||
notifier:
|
||||
smtp:
|
||||
host: smtp
|
||||
port: 1025
|
||||
sender: admin@example.com
|
||||
disable_require_tls: true
|
||||
|
||||
identity_providers:
|
||||
oidc:
|
||||
hmac_secret: IVPWBkAdJHje3uz7LtFTDU2pFUfh39Xm
|
||||
issuer_private_key: |
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEogIBAAKCAQEAvOFmoEJFt1JkfdlwM3vJFg5rrY9d6LyyqezjZkBZDQ4qdEEU
|
||||
dCrbW8ISFTtg9sfbrS3qingUzVP9VOfYPMC3r0ugjJXjhvJdBSaoLlzL3saeyrXk
|
||||
frOOvkcWKzeOynqUNPhKy9dchmuLALFfd/Jy7Wzq0y7XxGeNidEmFjMAf9dwf6/+
|
||||
PjQjbG7zBFu/XSajITPHlDXPVDd0j2qw2wu5Z9iqn4LRXnAFnC438hZZKZU/+JxU
|
||||
2ezr6Sefiy8XTC2kDiq3cgLeEjSywlJOs+4TLjVS/3h75sh2Wk0xVaSwjPEjCOgm
|
||||
a+2E3GJrGdQBiAjMSu101VBVwHUHaLDCn1T4NwIDAQABAoIBADWkupXnXI99Ogc4
|
||||
GxK0JF88Rz6qyhwQg5mZKthejCwWCt6roRiBF33O933KOHa+OljMAqHDCv1pzjgw
|
||||
BIz0mvaRPw7OfylTajHNUdShDFHADVc7I6MMcgz+eYBarhY5jCAjKHMOPjv7DSZs
|
||||
OdYCKLvfxC2oTyV714n9uZhyccDcvQpkgZuBDL0oxPom1GOI8TGhPjxvFOovEHWA
|
||||
Q8q9XY4cUVNDikZmvpgeUkJHWYHYb+11vKeSupnYD03yJ3sDy+F6+m+3/XmzFbXb
|
||||
1p43ermHQsMfDlxPyulUUI0viSo2UhlMC/moAb9FusOv+dTl2lt0gGqzDJ9gg1z1
|
||||
XpHRnwkCgYEA5x48dyxd4lydtVYef9sBmbLJEYozsYyOwLcnrLSNaZxeCza1exyR
|
||||
QIRogswoLDacxrYvO8FY6LtAEMkisv732M29zthBPm5wyoSZiM1X2YfQXKsmyh2h
|
||||
x1/yCWv/BQjj68A8IAxToaXxSG4WAr/X00RGUkXgkgw122FxcmGuFyUCgYEA0TcR
|
||||
dnt/oRMK4aCZHcBgTknzDfxKlJh4S0C9WjxKgr8IlW4LTeVSBuuqOObOQYImEhtw
|
||||
TRTKZIViL0roDF79cioQSp1Tk5h6uy8wr6VyhWRnWfTz2/azoTHnmQ780rtAuEI/
|
||||
NvE6FiqwikJLjma1YJoRfr/bfmgMdxcYbJI1MSsCgYAEZ5Yda1IKu1siFpcUNrdM
|
||||
F5UvaWPc0WHzGEqARxye06UTL6K7yuqVwTBAteVaGlxYiSZTTDcGkHMDHuIzaRqO
|
||||
HjWs2IA90VsC8Q4ABnHTKnx1F6nwlin8I774IP/GN8ooNwyuS63YWdJEYBy5RrC1
|
||||
TQrODJjgD62DFdNUq7nmpQKBgFMJEzI+Q+KPJ0NztTG8t7x61y/W0Vb2yM+9Syn0
|
||||
QfJwlZyRR4VMHelHQZFB8dzIJgoLv9+n/8gztEtm5IB8dwUHst2aYaBz5UpDqYQd
|
||||
Gz3cIrTuZpcH7DVvFCeIbknJLh+zk1lgFpjTqqvFMi27kANeQtFWnmwmKcRec0As
|
||||
K1ZvAoGAV/3YB44/zIoB590+yhpx2HTmDPVHH+J+5O71Pi1D9W13ClBFLrE69wo+
|
||||
IQLIstBI5tGOGeuQNjXhDKJ1U30xppZXcnebrkA+oOo+6dy20zghFR2maAGXfWFU
|
||||
pM4GsSnSTm0bXPebVouQFqhj7LqcQQzCqRDThmw/Lp1tJUmu40g=
|
||||
-----END RSA PRIVATE KEY-----
|
||||
clients:
|
||||
- id: oidc-tester-app
|
||||
secret: foobar
|
||||
policy: two_factor
|
||||
redirect_uris:
|
||||
- https://oidc.example.com:8080/oauth2/callback
|
||||
# This client is used for testing purpose. As of now, the app must be protected by ACLs
|
||||
# otherwise it won't work properly.
|
||||
- id: oidc-tester-app-public
|
||||
secret: foobar
|
||||
authorization_policy: one_factor
|
||||
redirect_uris:
|
||||
- https://oidc-public.example.com:8080/oauth2/callback
|
||||
...
|
10
internal/suites/OIDC/docker-compose.yml
Normal file
10
internal/suites/OIDC/docker-compose.yml
Normal file
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
version: '3'
|
||||
services:
|
||||
authelia-backend:
|
||||
volumes:
|
||||
- './OIDC/configuration.yml:/config/configuration.yml:ro'
|
||||
- './OIDC/users.yml:/config/users.yml'
|
||||
- './OIDC/keypair/key.pem:/config/issuer.pem:ro'
|
||||
- './common/ssl:/config/ssl:ro'
|
||||
...
|
27
internal/suites/OIDC/keypair/key.pem
Normal file
27
internal/suites/OIDC/keypair/key.pem
Normal file
|
@ -0,0 +1,27 @@
|
|||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEogIBAAKCAQEAvOFmoEJFt1JkfdlwM3vJFg5rrY9d6LyyqezjZkBZDQ4qdEEU
|
||||
dCrbW8ISFTtg9sfbrS3qingUzVP9VOfYPMC3r0ugjJXjhvJdBSaoLlzL3saeyrXk
|
||||
frOOvkcWKzeOynqUNPhKy9dchmuLALFfd/Jy7Wzq0y7XxGeNidEmFjMAf9dwf6/+
|
||||
PjQjbG7zBFu/XSajITPHlDXPVDd0j2qw2wu5Z9iqn4LRXnAFnC438hZZKZU/+JxU
|
||||
2ezr6Sefiy8XTC2kDiq3cgLeEjSywlJOs+4TLjVS/3h75sh2Wk0xVaSwjPEjCOgm
|
||||
a+2E3GJrGdQBiAjMSu101VBVwHUHaLDCn1T4NwIDAQABAoIBADWkupXnXI99Ogc4
|
||||
GxK0JF88Rz6qyhwQg5mZKthejCwWCt6roRiBF33O933KOHa+OljMAqHDCv1pzjgw
|
||||
BIz0mvaRPw7OfylTajHNUdShDFHADVc7I6MMcgz+eYBarhY5jCAjKHMOPjv7DSZs
|
||||
OdYCKLvfxC2oTyV714n9uZhyccDcvQpkgZuBDL0oxPom1GOI8TGhPjxvFOovEHWA
|
||||
Q8q9XY4cUVNDikZmvpgeUkJHWYHYb+11vKeSupnYD03yJ3sDy+F6+m+3/XmzFbXb
|
||||
1p43ermHQsMfDlxPyulUUI0viSo2UhlMC/moAb9FusOv+dTl2lt0gGqzDJ9gg1z1
|
||||
XpHRnwkCgYEA5x48dyxd4lydtVYef9sBmbLJEYozsYyOwLcnrLSNaZxeCza1exyR
|
||||
QIRogswoLDacxrYvO8FY6LtAEMkisv732M29zthBPm5wyoSZiM1X2YfQXKsmyh2h
|
||||
x1/yCWv/BQjj68A8IAxToaXxSG4WAr/X00RGUkXgkgw122FxcmGuFyUCgYEA0TcR
|
||||
dnt/oRMK4aCZHcBgTknzDfxKlJh4S0C9WjxKgr8IlW4LTeVSBuuqOObOQYImEhtw
|
||||
TRTKZIViL0roDF79cioQSp1Tk5h6uy8wr6VyhWRnWfTz2/azoTHnmQ780rtAuEI/
|
||||
NvE6FiqwikJLjma1YJoRfr/bfmgMdxcYbJI1MSsCgYAEZ5Yda1IKu1siFpcUNrdM
|
||||
F5UvaWPc0WHzGEqARxye06UTL6K7yuqVwTBAteVaGlxYiSZTTDcGkHMDHuIzaRqO
|
||||
HjWs2IA90VsC8Q4ABnHTKnx1F6nwlin8I774IP/GN8ooNwyuS63YWdJEYBy5RrC1
|
||||
TQrODJjgD62DFdNUq7nmpQKBgFMJEzI+Q+KPJ0NztTG8t7x61y/W0Vb2yM+9Syn0
|
||||
QfJwlZyRR4VMHelHQZFB8dzIJgoLv9+n/8gztEtm5IB8dwUHst2aYaBz5UpDqYQd
|
||||
Gz3cIrTuZpcH7DVvFCeIbknJLh+zk1lgFpjTqqvFMi27kANeQtFWnmwmKcRec0As
|
||||
K1ZvAoGAV/3YB44/zIoB590+yhpx2HTmDPVHH+J+5O71Pi1D9W13ClBFLrE69wo+
|
||||
IQLIstBI5tGOGeuQNjXhDKJ1U30xppZXcnebrkA+oOo+6dy20zghFR2maAGXfWFU
|
||||
pM4GsSnSTm0bXPebVouQFqhj7LqcQQzCqRDThmw/Lp1tJUmu40g=
|
||||
-----END RSA PRIVATE KEY-----
|
9
internal/suites/OIDC/keypair/key.pub
Normal file
9
internal/suites/OIDC/keypair/key.pub
Normal file
|
@ -0,0 +1,9 @@
|
|||
-----BEGIN RSA PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvOFmoEJFt1JkfdlwM3vJ
|
||||
Fg5rrY9d6LyyqezjZkBZDQ4qdEEUdCrbW8ISFTtg9sfbrS3qingUzVP9VOfYPMC3
|
||||
r0ugjJXjhvJdBSaoLlzL3saeyrXkfrOOvkcWKzeOynqUNPhKy9dchmuLALFfd/Jy
|
||||
7Wzq0y7XxGeNidEmFjMAf9dwf6/+PjQjbG7zBFu/XSajITPHlDXPVDd0j2qw2wu5
|
||||
Z9iqn4LRXnAFnC438hZZKZU/+JxU2ezr6Sefiy8XTC2kDiq3cgLeEjSywlJOs+4T
|
||||
LjVS/3h75sh2Wk0xVaSwjPEjCOgma+2E3GJrGdQBiAjMSu101VBVwHUHaLDCn1T4
|
||||
NwIDAQAB
|
||||
-----END RSA PUBLIC KEY-----
|
35
internal/suites/OIDC/users.yml
Normal file
35
internal/suites/OIDC/users.yml
Normal file
|
@ -0,0 +1,35 @@
|
|||
---
|
||||
###############################################################
|
||||
# Users Database #
|
||||
###############################################################
|
||||
|
||||
# This file can be used if you do not have an LDAP set up.
|
||||
|
||||
# List of users
|
||||
users:
|
||||
john:
|
||||
displayname: "John Doe"
|
||||
password: "$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/" # yamllint disable-line rule:line-length
|
||||
email: john.doe@authelia.com
|
||||
groups:
|
||||
- admins
|
||||
- dev
|
||||
|
||||
harry:
|
||||
displayname: "Harry Potter"
|
||||
password: "$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/" # yamllint disable-line rule:line-length
|
||||
email: harry.potter@authelia.com
|
||||
groups: []
|
||||
|
||||
bob:
|
||||
displayname: "Bob Dylan"
|
||||
password: "$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/" # yamllint disable-line rule:line-length
|
||||
email: bob.dylan@authelia.com
|
||||
groups:
|
||||
- dev
|
||||
|
||||
james:
|
||||
displayname: "James Dean"
|
||||
password: "$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/" # yamllint disable-line rule:line-length
|
||||
email: james.dean@authelia.com
|
||||
...
|
101
internal/suites/OIDCTraefik/configuration.yml
Normal file
101
internal/suites/OIDCTraefik/configuration.yml
Normal file
|
@ -0,0 +1,101 @@
|
|||
---
|
||||
port: 9091
|
||||
tls_cert: /config/ssl/cert.pem
|
||||
tls_key: /config/ssl/key.pem
|
||||
|
||||
log_level: debug
|
||||
|
||||
jwt_secret: unsecure_secret
|
||||
|
||||
authentication_backend:
|
||||
file:
|
||||
path: /config/users.yml
|
||||
|
||||
session:
|
||||
secret: unsecure_session_secret
|
||||
domain: example.com
|
||||
expiration: 3600 # 1 hour
|
||||
inactivity: 300 # 5 minutes
|
||||
remember_me_duration: 1y
|
||||
# We use redis here to keep the users authenticated when Authelia restarts
|
||||
# It eases development.
|
||||
redis:
|
||||
host: redis
|
||||
port: 6379
|
||||
|
||||
storage:
|
||||
local:
|
||||
path: /config/db.sqlite
|
||||
|
||||
access_control:
|
||||
default_policy: deny
|
||||
rules:
|
||||
- domain: "home.example.com"
|
||||
policy: bypass
|
||||
- domain: "public.example.com"
|
||||
policy: bypass
|
||||
- domain: "admin.example.com"
|
||||
policy: two_factor
|
||||
- domain: "secure.example.com"
|
||||
policy: two_factor
|
||||
- domain: "singlefactor.example.com"
|
||||
policy: one_factor
|
||||
- domain: "oidc.example.com"
|
||||
policy: two_factor
|
||||
- domain: "oidc-public.example.com"
|
||||
policy: bypass
|
||||
- domain: "traefik.example.com"
|
||||
policy: bypass
|
||||
|
||||
notifier:
|
||||
smtp:
|
||||
host: smtp
|
||||
port: 1025
|
||||
sender: admin@example.com
|
||||
disable_require_tls: true
|
||||
|
||||
identity_providers:
|
||||
oidc:
|
||||
hmac_secret: IVPWBkAdJHje3uz7LtFTDU2pFUfh39Xm
|
||||
issuer_private_key: |
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEogIBAAKCAQEAvOFmoEJFt1JkfdlwM3vJFg5rrY9d6LyyqezjZkBZDQ4qdEEU
|
||||
dCrbW8ISFTtg9sfbrS3qingUzVP9VOfYPMC3r0ugjJXjhvJdBSaoLlzL3saeyrXk
|
||||
frOOvkcWKzeOynqUNPhKy9dchmuLALFfd/Jy7Wzq0y7XxGeNidEmFjMAf9dwf6/+
|
||||
PjQjbG7zBFu/XSajITPHlDXPVDd0j2qw2wu5Z9iqn4LRXnAFnC438hZZKZU/+JxU
|
||||
2ezr6Sefiy8XTC2kDiq3cgLeEjSywlJOs+4TLjVS/3h75sh2Wk0xVaSwjPEjCOgm
|
||||
a+2E3GJrGdQBiAjMSu101VBVwHUHaLDCn1T4NwIDAQABAoIBADWkupXnXI99Ogc4
|
||||
GxK0JF88Rz6qyhwQg5mZKthejCwWCt6roRiBF33O933KOHa+OljMAqHDCv1pzjgw
|
||||
BIz0mvaRPw7OfylTajHNUdShDFHADVc7I6MMcgz+eYBarhY5jCAjKHMOPjv7DSZs
|
||||
OdYCKLvfxC2oTyV714n9uZhyccDcvQpkgZuBDL0oxPom1GOI8TGhPjxvFOovEHWA
|
||||
Q8q9XY4cUVNDikZmvpgeUkJHWYHYb+11vKeSupnYD03yJ3sDy+F6+m+3/XmzFbXb
|
||||
1p43ermHQsMfDlxPyulUUI0viSo2UhlMC/moAb9FusOv+dTl2lt0gGqzDJ9gg1z1
|
||||
XpHRnwkCgYEA5x48dyxd4lydtVYef9sBmbLJEYozsYyOwLcnrLSNaZxeCza1exyR
|
||||
QIRogswoLDacxrYvO8FY6LtAEMkisv732M29zthBPm5wyoSZiM1X2YfQXKsmyh2h
|
||||
x1/yCWv/BQjj68A8IAxToaXxSG4WAr/X00RGUkXgkgw122FxcmGuFyUCgYEA0TcR
|
||||
dnt/oRMK4aCZHcBgTknzDfxKlJh4S0C9WjxKgr8IlW4LTeVSBuuqOObOQYImEhtw
|
||||
TRTKZIViL0roDF79cioQSp1Tk5h6uy8wr6VyhWRnWfTz2/azoTHnmQ780rtAuEI/
|
||||
NvE6FiqwikJLjma1YJoRfr/bfmgMdxcYbJI1MSsCgYAEZ5Yda1IKu1siFpcUNrdM
|
||||
F5UvaWPc0WHzGEqARxye06UTL6K7yuqVwTBAteVaGlxYiSZTTDcGkHMDHuIzaRqO
|
||||
HjWs2IA90VsC8Q4ABnHTKnx1F6nwlin8I774IP/GN8ooNwyuS63YWdJEYBy5RrC1
|
||||
TQrODJjgD62DFdNUq7nmpQKBgFMJEzI+Q+KPJ0NztTG8t7x61y/W0Vb2yM+9Syn0
|
||||
QfJwlZyRR4VMHelHQZFB8dzIJgoLv9+n/8gztEtm5IB8dwUHst2aYaBz5UpDqYQd
|
||||
Gz3cIrTuZpcH7DVvFCeIbknJLh+zk1lgFpjTqqvFMi27kANeQtFWnmwmKcRec0As
|
||||
K1ZvAoGAV/3YB44/zIoB590+yhpx2HTmDPVHH+J+5O71Pi1D9W13ClBFLrE69wo+
|
||||
IQLIstBI5tGOGeuQNjXhDKJ1U30xppZXcnebrkA+oOo+6dy20zghFR2maAGXfWFU
|
||||
pM4GsSnSTm0bXPebVouQFqhj7LqcQQzCqRDThmw/Lp1tJUmu40g=
|
||||
-----END RSA PRIVATE KEY-----
|
||||
clients:
|
||||
- id: oidc-tester-app
|
||||
secret: foobar
|
||||
policy: two_factor
|
||||
redirect_uris:
|
||||
- https://oidc.example.com:8080/oauth2/callback
|
||||
# This client is used for testing purpose. As of now, the app must be protected by ACLs
|
||||
# otherwise it won't work properly.
|
||||
- id: oidc-tester-app-public
|
||||
secret: foobar
|
||||
authorization_policy: one_factor
|
||||
redirect_uris:
|
||||
- https://oidc-public.example.com:8080/oauth2/callback
|
||||
...
|
10
internal/suites/OIDCTraefik/docker-compose.yml
Normal file
10
internal/suites/OIDCTraefik/docker-compose.yml
Normal file
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
version: '3'
|
||||
services:
|
||||
authelia-backend:
|
||||
volumes:
|
||||
- './OIDCTraefik/configuration.yml:/config/configuration.yml:ro'
|
||||
- './OIDCTraefik/users.yml:/config/users.yml'
|
||||
- './OIDCTraefik/keypair/key.pem:/config/issuer.pem:ro'
|
||||
- './common/ssl:/config/ssl:ro'
|
||||
...
|
27
internal/suites/OIDCTraefik/keypair/key.pem
Normal file
27
internal/suites/OIDCTraefik/keypair/key.pem
Normal file
|
@ -0,0 +1,27 @@
|
|||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEogIBAAKCAQEAvOFmoEJFt1JkfdlwM3vJFg5rrY9d6LyyqezjZkBZDQ4qdEEU
|
||||
dCrbW8ISFTtg9sfbrS3qingUzVP9VOfYPMC3r0ugjJXjhvJdBSaoLlzL3saeyrXk
|
||||
frOOvkcWKzeOynqUNPhKy9dchmuLALFfd/Jy7Wzq0y7XxGeNidEmFjMAf9dwf6/+
|
||||
PjQjbG7zBFu/XSajITPHlDXPVDd0j2qw2wu5Z9iqn4LRXnAFnC438hZZKZU/+JxU
|
||||
2ezr6Sefiy8XTC2kDiq3cgLeEjSywlJOs+4TLjVS/3h75sh2Wk0xVaSwjPEjCOgm
|
||||
a+2E3GJrGdQBiAjMSu101VBVwHUHaLDCn1T4NwIDAQABAoIBADWkupXnXI99Ogc4
|
||||
GxK0JF88Rz6qyhwQg5mZKthejCwWCt6roRiBF33O933KOHa+OljMAqHDCv1pzjgw
|
||||
BIz0mvaRPw7OfylTajHNUdShDFHADVc7I6MMcgz+eYBarhY5jCAjKHMOPjv7DSZs
|
||||
OdYCKLvfxC2oTyV714n9uZhyccDcvQpkgZuBDL0oxPom1GOI8TGhPjxvFOovEHWA
|
||||
Q8q9XY4cUVNDikZmvpgeUkJHWYHYb+11vKeSupnYD03yJ3sDy+F6+m+3/XmzFbXb
|
||||
1p43ermHQsMfDlxPyulUUI0viSo2UhlMC/moAb9FusOv+dTl2lt0gGqzDJ9gg1z1
|
||||
XpHRnwkCgYEA5x48dyxd4lydtVYef9sBmbLJEYozsYyOwLcnrLSNaZxeCza1exyR
|
||||
QIRogswoLDacxrYvO8FY6LtAEMkisv732M29zthBPm5wyoSZiM1X2YfQXKsmyh2h
|
||||
x1/yCWv/BQjj68A8IAxToaXxSG4WAr/X00RGUkXgkgw122FxcmGuFyUCgYEA0TcR
|
||||
dnt/oRMK4aCZHcBgTknzDfxKlJh4S0C9WjxKgr8IlW4LTeVSBuuqOObOQYImEhtw
|
||||
TRTKZIViL0roDF79cioQSp1Tk5h6uy8wr6VyhWRnWfTz2/azoTHnmQ780rtAuEI/
|
||||
NvE6FiqwikJLjma1YJoRfr/bfmgMdxcYbJI1MSsCgYAEZ5Yda1IKu1siFpcUNrdM
|
||||
F5UvaWPc0WHzGEqARxye06UTL6K7yuqVwTBAteVaGlxYiSZTTDcGkHMDHuIzaRqO
|
||||
HjWs2IA90VsC8Q4ABnHTKnx1F6nwlin8I774IP/GN8ooNwyuS63YWdJEYBy5RrC1
|
||||
TQrODJjgD62DFdNUq7nmpQKBgFMJEzI+Q+KPJ0NztTG8t7x61y/W0Vb2yM+9Syn0
|
||||
QfJwlZyRR4VMHelHQZFB8dzIJgoLv9+n/8gztEtm5IB8dwUHst2aYaBz5UpDqYQd
|
||||
Gz3cIrTuZpcH7DVvFCeIbknJLh+zk1lgFpjTqqvFMi27kANeQtFWnmwmKcRec0As
|
||||
K1ZvAoGAV/3YB44/zIoB590+yhpx2HTmDPVHH+J+5O71Pi1D9W13ClBFLrE69wo+
|
||||
IQLIstBI5tGOGeuQNjXhDKJ1U30xppZXcnebrkA+oOo+6dy20zghFR2maAGXfWFU
|
||||
pM4GsSnSTm0bXPebVouQFqhj7LqcQQzCqRDThmw/Lp1tJUmu40g=
|
||||
-----END RSA PRIVATE KEY-----
|
9
internal/suites/OIDCTraefik/keypair/key.pub
Normal file
9
internal/suites/OIDCTraefik/keypair/key.pub
Normal file
|
@ -0,0 +1,9 @@
|
|||
-----BEGIN RSA PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvOFmoEJFt1JkfdlwM3vJ
|
||||
Fg5rrY9d6LyyqezjZkBZDQ4qdEEUdCrbW8ISFTtg9sfbrS3qingUzVP9VOfYPMC3
|
||||
r0ugjJXjhvJdBSaoLlzL3saeyrXkfrOOvkcWKzeOynqUNPhKy9dchmuLALFfd/Jy
|
||||
7Wzq0y7XxGeNidEmFjMAf9dwf6/+PjQjbG7zBFu/XSajITPHlDXPVDd0j2qw2wu5
|
||||
Z9iqn4LRXnAFnC438hZZKZU/+JxU2ezr6Sefiy8XTC2kDiq3cgLeEjSywlJOs+4T
|
||||
LjVS/3h75sh2Wk0xVaSwjPEjCOgma+2E3GJrGdQBiAjMSu101VBVwHUHaLDCn1T4
|
||||
NwIDAQAB
|
||||
-----END RSA PUBLIC KEY-----
|
35
internal/suites/OIDCTraefik/users.yml
Normal file
35
internal/suites/OIDCTraefik/users.yml
Normal file
|
@ -0,0 +1,35 @@
|
|||
---
|
||||
###############################################################
|
||||
# Users Database #
|
||||
###############################################################
|
||||
|
||||
# This file can be used if you do not have an LDAP set up.
|
||||
|
||||
# List of users
|
||||
users:
|
||||
john:
|
||||
displayname: "John Doe"
|
||||
password: "$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/" # yamllint disable-line rule:line-length
|
||||
email: john.doe@authelia.com
|
||||
groups:
|
||||
- admins
|
||||
- dev
|
||||
|
||||
harry:
|
||||
displayname: "Harry Potter"
|
||||
password: "$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/" # yamllint disable-line rule:line-length
|
||||
email: harry.potter@authelia.com
|
||||
groups: []
|
||||
|
||||
bob:
|
||||
displayname: "Bob Dylan"
|
||||
password: "$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/" # yamllint disable-line rule:line-length
|
||||
email: bob.dylan@authelia.com
|
||||
groups:
|
||||
- dev
|
||||
|
||||
james:
|
||||
displayname: "James Dean"
|
||||
password: "$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/" # yamllint disable-line rule:line-length
|
||||
email: james.dean@authelia.com
|
||||
...
|
|
@ -41,6 +41,9 @@ var MX1MailBaseURL = fmt.Sprintf("https://mx1.mail.%s", BaseDomain)
|
|||
// MX2MailBaseURL the base URL of the mx2.mail domain.
|
||||
var MX2MailBaseURL = fmt.Sprintf("https://mx2.mail.%s", BaseDomain)
|
||||
|
||||
// OIDCBaseURL the base URL of the oidc domain.
|
||||
var OIDCBaseURL = fmt.Sprintf("https://oidc.%s", BaseDomain)
|
||||
|
||||
// DuoBaseURL the base URL of the Duo configuration API.
|
||||
var DuoBaseURL = "https://duo.example.com"
|
||||
|
||||
|
|
|
@ -45,6 +45,11 @@ func (de *DockerEnvironment) createCommand(cmd string) *exec.Cmd {
|
|||
return utils.Command("bash", "-c", dockerCmdLine)
|
||||
}
|
||||
|
||||
// Pull pull all images of needed in the environment.
|
||||
func (de *DockerEnvironment) Pull(images ...string) error {
|
||||
return de.createCommandWithStdout(fmt.Sprintf("pull %s", strings.Join(images, " "))).Run()
|
||||
}
|
||||
|
||||
// Up spawn a docker environment.
|
||||
func (de *DockerEnvironment) Up() error {
|
||||
return de.createCommandWithStdout("up --build -d").Run()
|
||||
|
|
|
@ -24,7 +24,7 @@ services:
|
|||
- 'traefik.frontend.rule=Host:login.example.com;PathPrefix:/api'
|
||||
- 'traefik.protocol=https'
|
||||
# Traefik 2.x
|
||||
- 'traefik.http.routers.authelia_backend.rule=Host(`login.example.com`) && PathPrefix(`/api`) || Host(`login.example.com`) && PathPrefix(`${PathPrefix}/api/`)' # yamllint disable-line rule:line-length
|
||||
- 'traefik.http.routers.authelia_backend.rule=Host(`login.example.com`) && PathPrefix(`/.well-known/openid-configuration`) || Host(`login.example.com`) && PathPrefix(`/api`) || Host(`login.example.com`) && PathPrefix(`${PathPrefix}/api/`)' # yamllint disable-line rule:line-length
|
||||
- 'traefik.http.routers.authelia_backend.entrypoints=https'
|
||||
- 'traefik.http.routers.authelia_backend.tls=true'
|
||||
- 'traefik.http.services.authelia_backend.loadbalancer.server.scheme=https'
|
||||
|
|
|
@ -8,10 +8,11 @@ services:
|
|||
- 'traefik.frontend.rule=Host:login.example.com;PathPrefix:/api'
|
||||
- 'traefik.protocol=https'
|
||||
# Traefik 2.x
|
||||
- 'traefik.http.routers.authelia_backend.rule=Host(`login.example.com`) && PathPrefix(`/api`) || Host(`login.example.com`) && PathPrefix(`${PathPrefix}/api/`)' # yamllint disable-line rule:line-length
|
||||
- 'traefik.http.routers.authelia_backend.rule=Host(`login.example.com`) && PathPrefix(`/.well-known/openid-configuration`) || Host(`login.example.com`) && PathPrefix(`/api`) || Host(`login.example.com`) && PathPrefix(`${PathPrefix}/api/`)' # yamllint disable-line rule:line-length
|
||||
- 'traefik.http.routers.authelia_backend.entrypoints=https'
|
||||
- 'traefik.http.routers.authelia_backend.tls=true'
|
||||
- 'traefik.http.services.authelia_backend.loadbalancer.server.scheme=https'
|
||||
- 'traefik.http.services.authelia_backend.passHostHeader=true'
|
||||
volumes:
|
||||
- '../..:/authelia'
|
||||
environment:
|
||||
|
|
|
@ -58,6 +58,9 @@
|
|||
<li>
|
||||
mx2.main.example.com <a href="https://mx2.mail.example.com:8080/secret.html"> / secret.html</a>
|
||||
</li>
|
||||
<li>
|
||||
oidc.example.com <a href="https://oidc.example.com:8080/">/</a> (only in OIDC suite).
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
You can also log off by visiting the following <a
|
||||
|
|
|
@ -34,6 +34,25 @@ http {
|
|||
|
||||
# Required by Authelia to build correct links for identity validation.
|
||||
proxy_set_header X-Forwarded-Host $http_host;
|
||||
proxy_set_header X-Forwarded-URI $request_uri;
|
||||
|
||||
# Needed for network ACLs to work. It appends the IP of the client to the list of IPs
|
||||
# and allows Authelia to use it to match the network-based ACLs.
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
||||
proxy_intercept_errors on;
|
||||
|
||||
proxy_pass $backend_endpoint;
|
||||
}
|
||||
|
||||
location /.well-known/openid-configuration {
|
||||
# Required by Authelia because "trust proxy" option is used.
|
||||
# See https://expressjs.com/en/guide/behind-proxies.html
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# Required by Authelia to build correct links for identity validation.
|
||||
proxy_set_header X-Forwarded-Host $http_host;
|
||||
proxy_set_header X-Forwarded-URI $request_uri;
|
||||
|
||||
# Needed for network ACLs to work. It appends the IP of the client to the list of IPs
|
||||
# and allows Authelia to use it to match the network-based ACLs.
|
||||
|
@ -191,6 +210,74 @@ http {
|
|||
}
|
||||
}
|
||||
|
||||
# Example configuration of domains protected by Authelia.
|
||||
server {
|
||||
listen 8080 ssl;
|
||||
server_name oidc.example.com
|
||||
oidc-public.example.com;
|
||||
|
||||
resolver 127.0.0.11 ipv6=off;
|
||||
set $upstream_verify https://authelia-backend:9091/api/verify;
|
||||
set $upstream_endpoint http://oidc-client:8080;
|
||||
|
||||
ssl_certificate /etc/ssl/server.cert;
|
||||
ssl_certificate_key /etc/ssl/server.key;
|
||||
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
add_header X-Frame-Options "SAMEORIGIN";
|
||||
|
||||
error_page 497 301 =307 https://$host:$server_port$request_uri;
|
||||
|
||||
# Reverse proxy to the backend. It is protected by Authelia by forwarding authorization checks
|
||||
# to the virtual endpoint introduced by nginx and declared in the next block.
|
||||
location / {
|
||||
auth_request /auth_verify;
|
||||
|
||||
# Route the request to the correct virtual host in the backend.
|
||||
proxy_set_header Host $http_host;
|
||||
|
||||
# mitigate HTTPoxy Vulnerability
|
||||
# https://www.nginx.com/blog/mitigating-the-httpoxy-vulnerability-with-nginx/
|
||||
proxy_set_header Proxy "";
|
||||
|
||||
# Set the `target_url` variable based on the request. It will be used to build the portal
|
||||
# URL with the correct redirection parameter.
|
||||
set $target_url $scheme://$http_host$request_uri;
|
||||
error_page 401 =302 https://login.example.com:8080/?rd=$target_url;
|
||||
|
||||
proxy_pass $upstream_endpoint;
|
||||
}
|
||||
|
||||
# Virtual endpoint forwarding requests to Authelia server.
|
||||
location /auth_verify {
|
||||
internal;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
|
||||
# Provide either X-Original-URL and X-Forwarded-Proto or
|
||||
# X-Forwarded-Proto, X-Forwarded-Host and X-Forwarded-URI or both.
|
||||
# Those headers will be used by Authelia to deduce the target url of the user.
|
||||
#
|
||||
# X-Forwarded-Proto is mandatory since Authelia uses the "trust proxy" option.
|
||||
# See https://expressjs.com/en/guide/behind-proxies.html
|
||||
proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
|
||||
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $http_host;
|
||||
proxy_set_header X-Forwarded-URI $request_uri;
|
||||
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
||||
# Authelia can receive Proxy-Authorization to authenticate however most of the clients
|
||||
# support Authorization instead. Therefore we rewrite Authorization into Proxy-Authorization.
|
||||
proxy_set_header Proxy-Authorization $http_authorization;
|
||||
|
||||
proxy_pass_request_body off;
|
||||
proxy_set_header Content-Length "";
|
||||
|
||||
proxy_pass $upstream_verify;
|
||||
}
|
||||
}
|
||||
|
||||
# Fake Web Mail used to receive emails sent by Authelia.
|
||||
server {
|
||||
listen 8080 ssl;
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
version: '3'
|
||||
services:
|
||||
oidc-client:
|
||||
image: ghcr.io/authelia/oidc-tester-app:master-2a82ab3
|
||||
command: /entrypoint.sh
|
||||
depends_on:
|
||||
- authelia-backend
|
||||
volumes:
|
||||
- ./example/compose/oidc-client/entrypoint.sh:/entrypoint.sh
|
||||
expose:
|
||||
- 8080
|
||||
labels:
|
||||
- 'traefik.http.routers.oidc.rule=Host(`oidc.example.com`)'
|
||||
- 'traefik.http.routers.oidc.priority=150'
|
||||
- 'traefik.http.routers.oidc.tls=true'
|
||||
- 'traefik.http.routers.oidc.middlewares=authelia@docker'
|
||||
networks:
|
||||
- authelianet
|
||||
...
|
7
internal/suites/example/compose/oidc-client/entrypoint.sh
Executable file
7
internal/suites/example/compose/oidc-client/entrypoint.sh
Executable file
|
@ -0,0 +1,7 @@
|
|||
#!/bin/bash
|
||||
|
||||
while true;
|
||||
do
|
||||
oidc-tester-app --issuer https://login.example.com:8080 --id oidc-tester-app --secret foobar --scopes openid,profile,email --redirect-domain oidc.example.com
|
||||
sleep 5
|
||||
done
|
|
@ -26,6 +26,10 @@ services:
|
|||
- '--serversTransport.insecureSkipVerify=true'
|
||||
networks:
|
||||
authelianet:
|
||||
aliases:
|
||||
- public.example.com
|
||||
- secure.example.com
|
||||
- login.example.com
|
||||
# Set the IP to be able to query on port 8080
|
||||
ipv4_address: 192.168.240.100
|
||||
...
|
||||
|
|
117
internal/suites/scenario_oidc_test.go
Normal file
117
internal/suites/scenario_oidc_test.go
Normal file
|
@ -0,0 +1,117 @@
|
|||
package suites
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type OIDCScenario struct {
|
||||
*SeleniumSuite
|
||||
secret string
|
||||
}
|
||||
|
||||
func NewOIDCScenario() *OIDCScenario {
|
||||
return &OIDCScenario{
|
||||
SeleniumSuite: new(SeleniumSuite),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *OIDCScenario) SetupSuite() {
|
||||
wds, err := StartWebDriver()
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
s.WebDriverSession = wds
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||
defer cancel()
|
||||
|
||||
s.secret = s.doRegisterAndLogin2FA(ctx, s.T(), "john", "password", false, AdminBaseURL)
|
||||
}
|
||||
|
||||
func (s *OIDCScenario) TearDownSuite() {
|
||||
err := s.WebDriverSession.Stop()
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *OIDCScenario) SetupTest() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
s.doVisit(s.T(), fmt.Sprintf("%s/logout", OIDCBaseURL))
|
||||
s.doLogout(ctx, s.T())
|
||||
s.doVisit(s.T(), HomeBaseURL)
|
||||
s.verifyIsHome(ctx, s.T())
|
||||
}
|
||||
|
||||
func (s *OIDCScenario) TestShouldAuthorizeAccessToOIDCApp() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
s.doVisit(s.T(), OIDCBaseURL)
|
||||
s.verifyIsFirstFactorPage(ctx, s.T())
|
||||
s.doFillLoginPageAndClick(ctx, s.T(), "john", "password", false)
|
||||
s.verifyIsSecondFactorPage(ctx, s.T())
|
||||
s.doValidateTOTP(ctx, s.T(), s.secret)
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
s.waitBodyContains(ctx, s.T(), "Not logged yet...")
|
||||
|
||||
// this href represents the 'login' link
|
||||
err := s.WaitElementLocatedByTagName(ctx, s.T(), "a").Click()
|
||||
assert.NoError(s.T(), err)
|
||||
|
||||
s.verifyIsConsentPage(ctx, s.T())
|
||||
|
||||
err = s.WaitElementLocatedByID(ctx, s.T(), "accept-button").Click()
|
||||
assert.NoError(s.T(), err)
|
||||
|
||||
// Verify that the app is showing the info related to the user stored in the JWT token
|
||||
time.Sleep(2 * time.Second)
|
||||
s.waitBodyContains(ctx, s.T(), "Logged in as john!")
|
||||
}
|
||||
|
||||
func (s *OIDCScenario) TestShouldDenyConsent() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
s.doVisit(s.T(), OIDCBaseURL)
|
||||
s.verifyIsFirstFactorPage(ctx, s.T())
|
||||
s.doFillLoginPageAndClick(ctx, s.T(), "john", "password", false)
|
||||
s.verifyIsSecondFactorPage(ctx, s.T())
|
||||
s.doValidateTOTP(ctx, s.T(), s.secret)
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
s.waitBodyContains(ctx, s.T(), "Not logged yet...")
|
||||
|
||||
// this href represents the 'login' link
|
||||
err := s.WaitElementLocatedByTagName(ctx, s.T(), "a").Click()
|
||||
assert.NoError(s.T(), err)
|
||||
|
||||
s.verifyIsConsentPage(ctx, s.T())
|
||||
|
||||
err = s.WaitElementLocatedByID(ctx, s.T(), "deny-button").Click()
|
||||
assert.NoError(s.T(), err)
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
s.verifyURLIs(ctx, s.T(), "https://oidc.example.com:8080/oauth2/callback?error=access_denied&error_description=User%20has%20rejected%20the%20scopes")
|
||||
}
|
||||
|
||||
func TestRunOIDCScenario(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping suite test in short mode")
|
||||
}
|
||||
|
||||
suite.Run(t, NewOIDCSuite())
|
||||
}
|
80
internal/suites/suite_oidc.go
Normal file
80
internal/suites/suite_oidc.go
Normal file
|
@ -0,0 +1,80 @@
|
|||
package suites
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
var oidcSuiteName = "OIDC"
|
||||
|
||||
func init() {
|
||||
dockerEnvironment := NewDockerEnvironment([]string{
|
||||
"internal/suites/docker-compose.yml",
|
||||
"internal/suites/OIDC/docker-compose.yml",
|
||||
"internal/suites/example/compose/authelia/docker-compose.backend.{}.yml",
|
||||
"internal/suites/example/compose/authelia/docker-compose.frontend.{}.yml",
|
||||
"internal/suites/example/compose/nginx/backend/docker-compose.yml",
|
||||
"internal/suites/example/compose/nginx/portal/docker-compose.yml",
|
||||
"internal/suites/example/compose/smtp/docker-compose.yml",
|
||||
"internal/suites/example/compose/oidc-client/docker-compose.yml",
|
||||
"internal/suites/example/compose/redis/docker-compose.yml",
|
||||
})
|
||||
|
||||
setup := func(suitePath string) error {
|
||||
// TODO(c.michaud): use version in tags for oidc-client but in the meantime we pull the image to make sure it's
|
||||
// up to date.
|
||||
err := dockerEnvironment.Pull("oidc-client")
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = dockerEnvironment.Up()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return waitUntilAutheliaIsReady(dockerEnvironment, oidcSuiteName)
|
||||
}
|
||||
|
||||
displayAutheliaLogs := func() error {
|
||||
backendLogs, err := dockerEnvironment.Logs("authelia-backend", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println(backendLogs)
|
||||
|
||||
frontendLogs, err := dockerEnvironment.Logs("authelia-frontend", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println(frontendLogs)
|
||||
|
||||
oidcClientLogs, err := dockerEnvironment.Logs("oidc-client", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println(oidcClientLogs)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
teardown := func(suitePath string) error {
|
||||
err := dockerEnvironment.Down()
|
||||
return err
|
||||
}
|
||||
|
||||
GlobalRegistry.Register(oidcSuiteName, Suite{
|
||||
SetUp: setup,
|
||||
SetUpTimeout: 5 * time.Minute,
|
||||
OnSetupTimeout: displayAutheliaLogs,
|
||||
OnError: displayAutheliaLogs,
|
||||
TestTimeout: 2 * time.Minute,
|
||||
TearDown: teardown,
|
||||
TearDownTimeout: 2 * time.Minute,
|
||||
})
|
||||
}
|
23
internal/suites/suite_oidc_test.go
Normal file
23
internal/suites/suite_oidc_test.go
Normal file
|
@ -0,0 +1,23 @@
|
|||
package suites
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type OIDCSuite struct {
|
||||
*SeleniumSuite
|
||||
}
|
||||
|
||||
func NewOIDCSuite() *OIDCSuite {
|
||||
return &OIDCSuite{SeleniumSuite: new(SeleniumSuite)}
|
||||
}
|
||||
|
||||
func (s *OIDCSuite) TestOIDCScenario() {
|
||||
suite.Run(s.T(), NewOIDCScenario())
|
||||
}
|
||||
|
||||
func TestOIDCSuite(t *testing.T) {
|
||||
suite.Run(t, NewOIDCSuite())
|
||||
}
|
80
internal/suites/suite_oidc_traefik.go
Normal file
80
internal/suites/suite_oidc_traefik.go
Normal file
|
@ -0,0 +1,80 @@
|
|||
package suites
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
var oidcTraefikSuiteName = "OIDCTraefik"
|
||||
|
||||
func init() {
|
||||
dockerEnvironment := NewDockerEnvironment([]string{
|
||||
"internal/suites/docker-compose.yml",
|
||||
"internal/suites/OIDCTraefik/docker-compose.yml",
|
||||
"internal/suites/example/compose/authelia/docker-compose.backend.{}.yml",
|
||||
"internal/suites/example/compose/authelia/docker-compose.frontend.{}.yml",
|
||||
"internal/suites/example/compose/nginx/backend/docker-compose.yml",
|
||||
"internal/suites/example/compose/traefik2/docker-compose.yml",
|
||||
"internal/suites/example/compose/smtp/docker-compose.yml",
|
||||
"internal/suites/example/compose/oidc-client/docker-compose.yml",
|
||||
"internal/suites/example/compose/redis/docker-compose.yml",
|
||||
})
|
||||
|
||||
setup := func(suitePath string) error {
|
||||
// TODO(c.michaud): use version in tags for oidc-client but in the meantime we pull the image to make sure it's
|
||||
// up to date.
|
||||
err := dockerEnvironment.Pull("oidc-client")
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = dockerEnvironment.Up()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return waitUntilAutheliaIsReady(dockerEnvironment, oidcTraefikSuiteName)
|
||||
}
|
||||
|
||||
displayAutheliaLogs := func() error {
|
||||
backendLogs, err := dockerEnvironment.Logs("authelia-backend", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println(backendLogs)
|
||||
|
||||
frontendLogs, err := dockerEnvironment.Logs("authelia-frontend", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println(frontendLogs)
|
||||
|
||||
oidcClientLogs, err := dockerEnvironment.Logs("oidc-client", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println(oidcClientLogs)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
teardown := func(suitePath string) error {
|
||||
err := dockerEnvironment.Down()
|
||||
return err
|
||||
}
|
||||
|
||||
GlobalRegistry.Register(oidcTraefikSuiteName, Suite{
|
||||
SetUp: setup,
|
||||
SetUpTimeout: 5 * time.Minute,
|
||||
OnSetupTimeout: displayAutheliaLogs,
|
||||
OnError: displayAutheliaLogs,
|
||||
TestTimeout: 2 * time.Minute,
|
||||
TearDown: teardown,
|
||||
TearDownTimeout: 2 * time.Minute,
|
||||
})
|
||||
}
|
23
internal/suites/suite_oidc_traefik_test.go
Normal file
23
internal/suites/suite_oidc_traefik_test.go
Normal file
|
@ -0,0 +1,23 @@
|
|||
package suites
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type OIDCTraefikSuite struct {
|
||||
*SeleniumSuite
|
||||
}
|
||||
|
||||
func NewOIDCTraefikSuite() *OIDCTraefikSuite {
|
||||
return &OIDCTraefikSuite{SeleniumSuite: new(SeleniumSuite)}
|
||||
}
|
||||
|
||||
func (s *OIDCTraefikSuite) TestOIDCScenario() {
|
||||
suite.Run(s.T(), NewOIDCScenario())
|
||||
}
|
||||
|
||||
func TestOIDCTraefikSuite(t *testing.T) {
|
||||
suite.Run(t, NewOIDCTraefikSuite())
|
||||
}
|
10
internal/suites/verify_is_consent_page.go
Normal file
10
internal/suites/verify_is_consent_page.go
Normal file
|
@ -0,0 +1,10 @@
|
|||
package suites
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func (wds *WebDriverSession) verifyIsConsentPage(ctx context.Context, t *testing.T) {
|
||||
wds.WaitElementLocatedByID(ctx, t, "consent-stage")
|
||||
}
|
|
@ -229,3 +229,16 @@ func (wds *WebDriverSession) WaitElementTextContains(ctx context.Context, t *tes
|
|||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func (wds *WebDriverSession) waitBodyContains(ctx context.Context, t *testing.T, pattern string) {
|
||||
err := wds.Wait(ctx, func(driver selenium.WebDriver) (bool, error) {
|
||||
text, err := wds.WaitElementLocatedByTagName(ctx, t, "body").Text()
|
||||
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return strings.Contains(text, pattern), nil
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
|
|
@ -6,46 +6,47 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
windows = "windows"
|
||||
testStringInput = "abcdefghijkl"
|
||||
|
||||
// RFC3339Zero is the default value for time.Time.Unix().
|
||||
RFC3339Zero = int64(-62135596800)
|
||||
|
||||
// TLS13 is the textual representation of TLS 1.3.
|
||||
TLS13 = "1.3"
|
||||
|
||||
// TLS12 is the textual representation of TLS 1.2.
|
||||
TLS12 = "1.2"
|
||||
|
||||
// TLS11 is the textual representation of TLS 1.1.
|
||||
TLS11 = "1.1"
|
||||
|
||||
// TLS10 is the textual representation of TLS 1.0.
|
||||
TLS10 = "1.0"
|
||||
|
||||
// Hour is an int based representation of the time unit.
|
||||
Hour = time.Minute * 60
|
||||
|
||||
// Day is an int based representation of the time unit.
|
||||
Day = Hour * 24
|
||||
|
||||
// Week is an int based representation of the time unit.
|
||||
Week = Day * 7
|
||||
|
||||
// Year is an int based representation of the time unit.
|
||||
Year = Day * 365
|
||||
|
||||
// Month is an int based representation of the time unit.
|
||||
Month = Year / 12
|
||||
)
|
||||
|
||||
// ErrTimeoutReached error thrown when a timeout is reached.
|
||||
var ErrTimeoutReached = errors.New("timeout reached")
|
||||
var parseDurationRegexp = regexp.MustCompile(`^(?P<Duration>[1-9]\d*?)(?P<Unit>[smhdwMy])?$`)
|
||||
|
||||
// Hour is an int based representation of the time unit.
|
||||
const Hour = time.Minute * 60
|
||||
|
||||
// Day is an int based representation of the time unit.
|
||||
const Day = Hour * 24
|
||||
|
||||
// Week is an int based representation of the time unit.
|
||||
const Week = Day * 7
|
||||
|
||||
// Year is an int based representation of the time unit.
|
||||
const Year = Day * 365
|
||||
|
||||
// Month is an int based representation of the time unit.
|
||||
const Month = Year / 12
|
||||
|
||||
const windows = "windows"
|
||||
|
||||
// RFC3339Zero is the default value for time.Time.Unix().
|
||||
const RFC3339Zero = int64(-62135596800)
|
||||
|
||||
const testStringInput = "abcdefghijkl"
|
||||
|
||||
// AlphaNumericCharacters are literally just valid alphanumeric chars.
|
||||
var AlphaNumericCharacters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
|
||||
|
||||
// ErrTLSVersionNotSupported returned when an unknown TLS version supplied.
|
||||
var ErrTLSVersionNotSupported = errors.New("supplied TLS version isn't supported")
|
||||
|
||||
// TLS13 is the textual representation of TLS 1.3.
|
||||
const TLS13 = "1.3"
|
||||
|
||||
// TLS12 is the textual representation of TLS 1.2.
|
||||
const TLS12 = "1.2"
|
||||
|
||||
// TLS11 is the textual representation of TLS 1.1.
|
||||
const TLS11 = "1.1"
|
||||
|
||||
// TLS10 is the textual representation of TLS 1.0.
|
||||
const TLS10 = "1.0"
|
||||
|
|
|
@ -1,12 +1,49 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
)
|
||||
|
||||
// FileExists returns whether the given file or directory exists.
|
||||
func FileExists(path string) (bool, error) {
|
||||
_, err := os.Stat(path)
|
||||
// FileExists returns true if the given path exists and is a file.
|
||||
func FileExists(path string) (exists bool, err error) {
|
||||
info, err := os.Stat(path)
|
||||
if err == nil {
|
||||
if info.IsDir() {
|
||||
return false, errors.New("path is a directory")
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if os.IsNotExist(err) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return false, err
|
||||
}
|
||||
|
||||
// DirectoryExists returns true if the given path exists and is a directory.
|
||||
func DirectoryExists(path string) (exists bool, err error) {
|
||||
info, err := os.Stat(path)
|
||||
if err == nil {
|
||||
if info.IsDir() {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, errors.New("path is a file")
|
||||
}
|
||||
|
||||
if os.IsNotExist(err) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return false, err
|
||||
}
|
||||
|
||||
// PathExists returns true if the given path exists.
|
||||
func PathExists(path string) (exists bool, err error) {
|
||||
_, err = os.Stat(path)
|
||||
if err == nil {
|
||||
return true, nil
|
||||
}
|
||||
|
|
56
internal/utils/files_test.go
Normal file
56
internal/utils/files_test.go
Normal file
|
@ -0,0 +1,56 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestShouldCheckIfFileExists(t *testing.T) {
|
||||
exists, err := FileExists("../../README.md")
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, exists)
|
||||
|
||||
exists, err = FileExists("../../")
|
||||
assert.EqualError(t, err, "path is a directory")
|
||||
assert.False(t, exists)
|
||||
|
||||
exists, err = FileExists("../../NOTAFILE.md")
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, exists)
|
||||
}
|
||||
|
||||
func TestShouldCheckIfDirectoryExists(t *testing.T) {
|
||||
exists, err := DirectoryExists("../../")
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, exists)
|
||||
|
||||
exists, err = DirectoryExists("../../README.md")
|
||||
assert.EqualError(t, err, "path is a file")
|
||||
assert.False(t, exists)
|
||||
|
||||
exists, err = DirectoryExists("../../NOTADIRECTORY/")
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, exists)
|
||||
}
|
||||
|
||||
func TestShouldCheckIfPathExists(t *testing.T) {
|
||||
exists, err := PathExists("../../README.md")
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, exists)
|
||||
|
||||
exists, err = PathExists("../../")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, exists)
|
||||
|
||||
exists, err = PathExists("../../NOTAFILE.md")
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, exists)
|
||||
|
||||
exists, err = PathExists("../../NOTADIRECTORY/")
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, exists)
|
||||
}
|
13
internal/utils/hashing.go
Normal file
13
internal/utils/hashing.go
Normal file
|
@ -0,0 +1,13 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// HashSHA256FromString takes an input string and calculates the SHA256 checksum returning it as a base16 hash string.
|
||||
func HashSHA256FromString(input string) (output string) {
|
||||
sum := sha256.Sum256([]byte(input))
|
||||
|
||||
return fmt.Sprintf("%x", sum)
|
||||
}
|
83
internal/utils/rsa.go
Normal file
83
internal/utils/rsa.go
Normal file
|
@ -0,0 +1,83 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
)
|
||||
|
||||
// GenerateRsaKeyPair generate an RSA key pair.
|
||||
// bits can be 2048 or 4096.
|
||||
func GenerateRsaKeyPair(bits int) (*rsa.PrivateKey, *rsa.PublicKey) {
|
||||
privkey, _ := rsa.GenerateKey(rand.Reader, bits)
|
||||
return privkey, &privkey.PublicKey
|
||||
}
|
||||
|
||||
// ExportRsaPrivateKeyAsPemStr marshal a rsa private key into PEM string.
|
||||
func ExportRsaPrivateKeyAsPemStr(privkey *rsa.PrivateKey) string {
|
||||
privkeyBytes := x509.MarshalPKCS1PrivateKey(privkey)
|
||||
privkeyPem := pem.EncodeToMemory(
|
||||
&pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Bytes: privkeyBytes,
|
||||
},
|
||||
)
|
||||
|
||||
return string(privkeyPem)
|
||||
}
|
||||
|
||||
// ParseRsaPrivateKeyFromPemStr parse a RSA private key from PEM string.
|
||||
func ParseRsaPrivateKeyFromPemStr(privPEM string) (*rsa.PrivateKey, error) {
|
||||
block, _ := pem.Decode([]byte(privPEM))
|
||||
if block == nil {
|
||||
return nil, errors.New("failed to parse PEM block containing the key")
|
||||
}
|
||||
|
||||
priv, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return priv, nil
|
||||
}
|
||||
|
||||
// ExportRsaPublicKeyAsPemStr marshal a RSA public into a PEM string.
|
||||
func ExportRsaPublicKeyAsPemStr(pubkey *rsa.PublicKey) (string, error) {
|
||||
pubkeyBytes, err := x509.MarshalPKIXPublicKey(pubkey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
pubkeyPem := pem.EncodeToMemory(
|
||||
&pem.Block{
|
||||
Type: "RSA PUBLIC KEY",
|
||||
Bytes: pubkeyBytes,
|
||||
},
|
||||
)
|
||||
|
||||
return string(pubkeyPem), nil
|
||||
}
|
||||
|
||||
// ParseRsaPublicKeyFromPemStr parse RSA public key from a PEM string.
|
||||
func ParseRsaPublicKeyFromPemStr(pubPEM string) (*rsa.PublicKey, error) {
|
||||
block, _ := pem.Decode([]byte(pubPEM))
|
||||
if block == nil {
|
||||
return nil, errors.New("failed to parse PEM block containing the key")
|
||||
}
|
||||
|
||||
pub, err := x509.ParsePKIXPublicKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch pub := pub.(type) {
|
||||
case *rsa.PublicKey:
|
||||
return pub, nil
|
||||
default:
|
||||
break // fall through
|
||||
}
|
||||
|
||||
return nil, errors.New("Key type is not RSA")
|
||||
}
|
|
@ -68,15 +68,13 @@ func SliceString(s string, d int) (array []string) {
|
|||
return
|
||||
}
|
||||
|
||||
// IsStringSlicesDifferent checks two slices of strings and on the first occurrence of a string item not existing in the
|
||||
// other slice returns true, otherwise returns false.
|
||||
func IsStringSlicesDifferent(a, b []string) (different bool) {
|
||||
func isStringSlicesDifferent(a, b []string, method func(s string, b []string) bool) (different bool) {
|
||||
if len(a) != len(b) {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, s := range a {
|
||||
if !IsStringInSlice(s, b) {
|
||||
if !method(s, b) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
@ -84,6 +82,18 @@ func IsStringSlicesDifferent(a, b []string) (different bool) {
|
|||
return false
|
||||
}
|
||||
|
||||
// IsStringSlicesDifferent checks two slices of strings and on the first occurrence of a string item not existing in the
|
||||
// other slice returns true, otherwise returns false.
|
||||
func IsStringSlicesDifferent(a, b []string) (different bool) {
|
||||
return isStringSlicesDifferent(a, b, IsStringInSlice)
|
||||
}
|
||||
|
||||
// IsStringSlicesDifferentFold checks two slices of strings and on the first occurrence of a string item not existing in
|
||||
// the other slice (case insensitive) returns true, otherwise returns false.
|
||||
func IsStringSlicesDifferentFold(a, b []string) (different bool) {
|
||||
return isStringSlicesDifferent(a, b, IsStringInSliceFold)
|
||||
}
|
||||
|
||||
// StringSlicesDelta takes a before and after []string and compares them returning a added and removed []string.
|
||||
func StringSlicesDelta(before, after []string) (added, removed []string) {
|
||||
for _, s := range before {
|
||||
|
|
|
@ -7,6 +7,12 @@ import (
|
|||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestShouldDetectAlphaNumericString(t *testing.T) {
|
||||
assert.True(t, IsStringAlphaNumeric("abc"))
|
||||
assert.True(t, IsStringAlphaNumeric("abc123"))
|
||||
assert.False(t, IsStringAlphaNumeric("abc123@"))
|
||||
}
|
||||
|
||||
func TestShouldSplitIntoEvenStringsOfFour(t *testing.T) {
|
||||
input := testStringInput
|
||||
|
||||
|
@ -70,6 +76,12 @@ func TestShouldFindSliceDifferences(t *testing.T) {
|
|||
b := []string{"abc", "xyz"}
|
||||
|
||||
assert.True(t, IsStringSlicesDifferent(a, b))
|
||||
assert.True(t, IsStringSlicesDifferentFold(a, b))
|
||||
|
||||
c := []string{"Abc", "xyz"}
|
||||
|
||||
assert.True(t, IsStringSlicesDifferent(b, c))
|
||||
assert.False(t, IsStringSlicesDifferentFold(b, c))
|
||||
}
|
||||
|
||||
func TestShouldNotFindSliceDifferences(t *testing.T) {
|
||||
|
@ -77,6 +89,7 @@ func TestShouldNotFindSliceDifferences(t *testing.T) {
|
|||
b := []string{"abc", "onetwothree"}
|
||||
|
||||
assert.False(t, IsStringSlicesDifferent(a, b))
|
||||
assert.False(t, IsStringSlicesDifferentFold(a, b))
|
||||
}
|
||||
|
||||
func TestShouldFindSliceDifferenceWhenDifferentLength(t *testing.T) {
|
||||
|
@ -84,6 +97,7 @@ func TestShouldFindSliceDifferenceWhenDifferentLength(t *testing.T) {
|
|||
b := []string{"abc", "onetwothree", "more"}
|
||||
|
||||
assert.True(t, IsStringSlicesDifferent(a, b))
|
||||
assert.True(t, IsStringSlicesDifferentFold(a, b))
|
||||
}
|
||||
|
||||
func TestShouldFindStringInSliceContains(t *testing.T) {
|
||||
|
|
|
@ -14,12 +14,14 @@ import {
|
|||
RegisterSecurityKeyRoute,
|
||||
RegisterOneTimePasswordRoute,
|
||||
LogoutRoute,
|
||||
ConsentRoute,
|
||||
} from "./Routes";
|
||||
import * as themes from "./themes";
|
||||
import { getBasePath } from "./utils/BasePath";
|
||||
import { getRememberMe, getResetPassword, getTheme } from "./utils/Configuration";
|
||||
import RegisterOneTimePassword from "./views/DeviceRegistration/RegisterOneTimePassword";
|
||||
import RegisterSecurityKey from "./views/DeviceRegistration/RegisterSecurityKey";
|
||||
import ConsentView from "./views/LoginPortal/ConsentView/ConsentView";
|
||||
import LoginPortal from "./views/LoginPortal/LoginPortal";
|
||||
import SignOut from "./views/LoginPortal/SignOut/SignOut";
|
||||
import ResetPasswordStep1 from "./views/ResetPassword/ResetPasswordStep1";
|
||||
|
@ -65,6 +67,9 @@ const App: React.FC = () => {
|
|||
<Route path={LogoutRoute} exact>
|
||||
<SignOut />
|
||||
</Route>
|
||||
<Route path={ConsentRoute} exact>
|
||||
<ConsentView />
|
||||
</Route>
|
||||
<Route path={FirstFactorRoute}>
|
||||
<LoginPortal rememberMe={getRememberMe()} resetPassword={getResetPassword()} />
|
||||
</Route>
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
export const FirstFactorRoute = "/";
|
||||
export const AuthenticatedRoute = "/authenticated";
|
||||
export const FirstFactorRoute: string = "/";
|
||||
export const AuthenticatedRoute: string = "/authenticated";
|
||||
export const ConsentRoute: string = "/consent";
|
||||
|
||||
export const SecondFactorRoute = "/2fa";
|
||||
export const SecondFactorU2FRoute = "/2fa/security-key";
|
||||
export const SecondFactorTOTPRoute = "/2fa/one-time-password";
|
||||
export const SecondFactorPushRoute = "/2fa/push-notification";
|
||||
export const SecondFactorRoute: string = "/2fa";
|
||||
export const SecondFactorU2FRoute: string = "/2fa/security-key";
|
||||
export const SecondFactorTOTPRoute: string = "/2fa/one-time-password";
|
||||
export const SecondFactorPushRoute: string = "/2fa/push-notification";
|
||||
|
||||
export const ResetPasswordStep1Route = "/reset-password/step1";
|
||||
export const ResetPasswordStep2Route = "/reset-password/step2";
|
||||
export const RegisterSecurityKeyRoute = "/security-key/register";
|
||||
export const RegisterOneTimePasswordRoute = "/one-time-password/register";
|
||||
export const LogoutRoute = "/logout";
|
||||
export const ResetPasswordStep1Route: string = "/reset-password/step1";
|
||||
export const ResetPasswordStep2Route: string = "/reset-password/step2";
|
||||
export const RegisterSecurityKeyRoute: string = "/security-key/register";
|
||||
export const RegisterOneTimePasswordRoute: string = "/one-time-password/register";
|
||||
export const LogoutRoute: string = "/logout";
|
||||
|
|
|
@ -18,7 +18,7 @@ const NotificationBar = function (props: Props) {
|
|||
if (notification && notification !== null) {
|
||||
setTmpNotification(notification);
|
||||
}
|
||||
}, [notification]);
|
||||
}, [notification, setTmpNotification]);
|
||||
|
||||
const shouldSnackbarBeOpen = notification !== undefined && notification !== null;
|
||||
|
||||
|
|
6
web/src/hooks/Consent.ts
Normal file
6
web/src/hooks/Consent.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
import { getRequestedScopes } from "../services/Consent";
|
||||
import { useRemoteCall } from "./RemoteCall";
|
||||
|
||||
export function useRequestedScopes() {
|
||||
return useRemoteCall(getRequestedScopes, []);
|
||||
}
|
5
web/src/hooks/Redirector.ts
Normal file
5
web/src/hooks/Redirector.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
export function useRedirector() {
|
||||
return (url: string) => {
|
||||
window.location.href = url;
|
||||
};
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user