Merge origin/master into feat-settings-ui

feat-otp-verification
James Elliott 2023-01-25 22:03:30 +11:00
commit 7d17c39c52
No known key found for this signature in database
GPG Key ID: 0F1C4A096E857E49
210 changed files with 9271 additions and 3508 deletions

View File

@ -20,7 +20,9 @@ tags:
- name: State
description: Configuration, health and state endpoints
- name: Authentication
description: Authentication and verification endpoints
description: Authentication endpoints
- name: Authorization
description: Authorization endpoints
{{- if .PasswordReset }}
- name: Password Reset
description: Password reset endpoints
@ -101,18 +103,58 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/handlers.StateResponse'
/api/verify:
{{- range $name, $config := .EndpointsAuthz }}
{{- $uri := printf "/api/authz/%s" $name }}
{{- if (eq $name "legacy") }}{{ $uri = "/api/verify" }}{{ end }}
{{ $uri }}:
{{- if (eq $config.Implementation "Legacy") }}
{{- range $method := list "get" "head" "options" "post" "put" "patch" "delete" "trace" }}
{{ $method }}:
tags:
- Authentication
summary: Verification
- Authorization
summary: Authorization Verification (Legacy)
description: >
The verify endpoint provides the ability to verify if a user has the necessary permissions to access a specified
domain.
The legacy authorization verification endpoint provides the ability to verify if a user has the necessary
permissions to access a specified domain with several proxies. It's generally recommended users use a proxy
specific endpoint instead.
parameters:
- $ref: '#/components/parameters/originalURLParam'
- name: X-Original-URL
in: header
description: Redirection URL
required: false
style: simple
explode: true
schema:
type: string
- $ref: '#/components/parameters/forwardedMethodParam'
- name: X-Forwarded-Proto
in: header
description: Redirection URL (Scheme / Protocol)
required: false
style: simple
explode: true
example: "https"
schema:
type: string
- name: X-Forwarded-Host
in: header
description: Redirection URL (Host)
required: false
style: simple
explode: true
example: "example.com"
schema:
type: string
- name: X-Forwarded-Uri
in: header
description: Redirection URL (URI)
required: false
style: simple
explode: true
example: "/path/example"
schema:
type: string
- $ref: '#/components/parameters/forwardedForParam'
- $ref: '#/components/parameters/authParam'
responses:
"200":
@ -143,6 +185,136 @@ paths:
security:
- authelia_auth: []
{{- end }}
{{- else if (eq $config.Implementation "ExtAuthz") }}
{{- range $method := list "get" "head" "options" "post" "put" "patch" "delete" "trace" }}
{{ $method }}:
tags:
- Authorization
summary: Authorization Verification (ExtAuthz)
description: >
The ExtAuthz authorization verification endpoint provides the ability to verify if a user has the necessary
permissions to access a specified resource with the Envoy proxy.
parameters:
- $ref: '#/components/parameters/forwardedMethodParam'
- $ref: '#/components/parameters/forwardedHostParam'
- $ref: '#/components/parameters/forwardedURIParam'
- $ref: '#/components/parameters/forwardedForParam'
- $ref: '#/components/parameters/autheliaURLParam'
responses:
"200":
description: Successful Operation
headers:
remote-user:
description: Username
schema:
type: string
example: john
remote-name:
description: Name
schema:
type: string
example: John Doe
remote-email:
description: Email
schema:
type: string
example: john.doe@authelia.com
remote-groups:
description: Comma separated list of Groups
schema:
type: string
example: admin,devs
"401":
description: Unauthorized
security:
- authelia_auth: []
{{- end }}
{{- else if (eq $config.Implementation "ForwardAuth") }}
{{- range $method := list "get" "head" }}
{{ $method }}:
tags:
- Authorization
summary: Authorization Verification (ForwardAuth)
description: >
The ForwardAuth authorization verification endpoint provides the ability to verify if a user has the necessary
permissions to access a specified resource with the Traefik, Caddy, or Skipper proxies.
parameters:
- $ref: '#/components/parameters/forwardedMethodParam'
- $ref: '#/components/parameters/forwardedHostParam'
- $ref: '#/components/parameters/forwardedURIParam'
- $ref: '#/components/parameters/forwardedForParam'
responses:
"200":
description: Successful Operation
headers:
remote-user:
description: Username
schema:
type: string
example: john
remote-name:
description: Name
schema:
type: string
example: John Doe
remote-email:
description: Email
schema:
type: string
example: john.doe@authelia.com
remote-groups:
description: Comma separated list of Groups
schema:
type: string
example: admin,devs
"401":
description: Unauthorized
security:
- authelia_auth: []
{{- end }}
{{- else if (eq $config.Implementation "AuthRequest") }}
{{- range $method := list "get" "head" }}
{{ $method }}:
tags:
- Authorization
summary: Authorization Verification (AuthRequest)
description: >
The AuthRequest authorization verification endpoint provides the ability to verify if a user has the necessary
permissions to access a specified resource with the HAPROXY, NGINX, or NGINX-based proxies.
parameters:
- $ref: '#/components/parameters/originalMethodParam'
- $ref: '#/components/parameters/originalURLParam'
responses:
"200":
description: Successful Operation
headers:
remote-user:
description: Username
schema:
type: string
example: john
remote-name:
description: Name
schema:
type: string
example: John Doe
remote-email:
description: Email
schema:
type: string
example: john.doe@authelia.com
remote-groups:
description: Comma separated list of Groups
schema:
type: string
example: admin,devs
"401":
description: Unauthorized
security:
- authelia_auth: []
{{- end }}
{{- end }}
{{- end }}
/api/firstfactor:
post:
tags:
@ -1181,6 +1353,32 @@ components:
type: integer
required: true
description: Numeric Webauthn Device ID
originalMethodParam:
name: X-Original-Method
in: header
description: Request Method
required: true
style: simple
explode: true
schema:
type: string
enum:
- "GET"
- "HEAD"
- "POST"
- "PUT"
- "PATCH"
- "DELETE"
- "TRACE"
- "CONNECT"
- "OPTIONS"
- "COPY"
- "LOCK"
- "MKCOL"
- "MOVE"
- "PROPFIND"
- "PROPPATCH"
- "UNLOCK"
originalURLParam:
name: X-Original-URL
in: header
@ -1216,6 +1414,56 @@ components:
- "PROPFIND"
- "PROPPATCH"
- "UNLOCK"
forwardedProtoParam:
name: X-Forwarded-Proto
in: header
description: Redirection URL (Scheme / Protocol)
required: true
style: simple
explode: true
example: "https"
schema:
type: string
forwardedHostParam:
name: X-Forwarded-Host
in: header
description: Redirection URL (Host)
required: true
style: simple
explode: true
example: "example.com"
schema:
type: string
forwardedURIParam:
name: X-Forwarded-Uri
in: header
description: Redirection URL (URI)
required: true
style: simple
explode: true
example: "/path/example"
schema:
type: string
forwardedForParam:
name: X-Forwarded-For
in: header
description: Clients IP address or IP address chain
required: false
style: simple
explode: true
example: "192.168.0.55,192.168.0.20"
schema:
type: string
autheliaURLParam:
name: X-Authelia-URL
in: header
description: Authelia Portal URL
required: false
style: simple
explode: true
example: "https://auth.example.com"
schema:
type: string
authParam:
name: auth
in: query

View File

@ -1,18 +1,13 @@
package main
import (
"crypto/ecdsa"
"crypto/rsa"
"encoding/json"
"fmt"
"io"
"net/http"
"net/mail"
"net/url"
"os"
"path/filepath"
"reflect"
"regexp"
"strings"
"time"
@ -215,116 +210,3 @@ func codeKeysRunE(cmd *cobra.Command, args []string) (err error) {
return nil
}
var decodedTypes = []reflect.Type{
reflect.TypeOf(mail.Address{}),
reflect.TypeOf(regexp.Regexp{}),
reflect.TypeOf(url.URL{}),
reflect.TypeOf(time.Duration(0)),
reflect.TypeOf(schema.Address{}),
reflect.TypeOf(rsa.PrivateKey{}),
reflect.TypeOf(ecdsa.PrivateKey{}),
}
func containsType(needle reflect.Type, haystack []reflect.Type) (contains bool) {
for _, t := range haystack {
if needle.Kind() == reflect.Ptr {
if needle.Elem() == t {
return true
}
} else if needle == t {
return true
}
}
return false
}
//nolint:gocyclo
func readTags(prefix string, t reflect.Type) (tags []string) {
tags = make([]string, 0)
if t.Kind() != reflect.Struct {
if t.Kind() == reflect.Slice {
tags = append(tags, readTags(getKeyNameFromTagAndPrefix(prefix, "", true), t.Elem())...)
}
return
}
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
tag := field.Tag.Get("koanf")
if tag == "" {
tags = append(tags, prefix)
continue
}
switch field.Type.Kind() {
case reflect.Struct:
if !containsType(field.Type, decodedTypes) {
tags = append(tags, readTags(getKeyNameFromTagAndPrefix(prefix, tag, false), field.Type)...)
continue
}
case reflect.Slice:
switch field.Type.Elem().Kind() {
case reflect.Struct:
if !containsType(field.Type.Elem(), decodedTypes) {
tags = append(tags, getKeyNameFromTagAndPrefix(prefix, tag, false))
tags = append(tags, readTags(getKeyNameFromTagAndPrefix(prefix, tag, true), field.Type.Elem())...)
continue
}
case reflect.Slice:
tags = append(tags, readTags(getKeyNameFromTagAndPrefix(prefix, tag, true), field.Type.Elem())...)
}
case reflect.Ptr:
switch field.Type.Elem().Kind() {
case reflect.Struct:
if !containsType(field.Type.Elem(), decodedTypes) {
tags = append(tags, readTags(getKeyNameFromTagAndPrefix(prefix, tag, false), field.Type.Elem())...)
continue
}
case reflect.Slice:
if field.Type.Elem().Elem().Kind() == reflect.Struct {
if !containsType(field.Type.Elem(), decodedTypes) {
tags = append(tags, readTags(getKeyNameFromTagAndPrefix(prefix, tag, true), field.Type.Elem())...)
continue
}
}
}
}
tags = append(tags, getKeyNameFromTagAndPrefix(prefix, tag, false))
}
return tags
}
func getKeyNameFromTagAndPrefix(prefix, name string, slice bool) string {
nameParts := strings.SplitN(name, ",", 2)
if prefix == "" {
return nameParts[0]
}
if len(nameParts) == 2 && nameParts[1] == "squash" {
return prefix
}
if slice {
if name == "" {
return fmt.Sprintf("%s[]", prefix)
}
return fmt.Sprintf("%s.%s[]", prefix, nameParts[0])
}
return fmt.Sprintf("%s.%s", prefix, nameParts[0])
}

View File

@ -1,11 +1,20 @@
package main
import (
"crypto/ecdsa"
"crypto/rsa"
"fmt"
"net/mail"
"net/url"
"path/filepath"
"reflect"
"regexp"
"strings"
"time"
"github.com/spf13/pflag"
"github.com/authelia/authelia/v4/internal/configuration/schema"
)
func getPFlagPath(flags *pflag.FlagSet, flagNames ...string) (fullPath string, err error) {
@ -46,3 +55,125 @@ func buildCSP(defaultSrc string, ruleSets ...[]CSPValue) string {
return strings.Join(rules, "; ")
}
var decodedTypes = []reflect.Type{
reflect.TypeOf(mail.Address{}),
reflect.TypeOf(regexp.Regexp{}),
reflect.TypeOf(url.URL{}),
reflect.TypeOf(time.Duration(0)),
reflect.TypeOf(schema.Address{}),
reflect.TypeOf(schema.X509CertificateChain{}),
reflect.TypeOf(schema.PasswordDigest{}),
reflect.TypeOf(rsa.PrivateKey{}),
reflect.TypeOf(ecdsa.PrivateKey{}),
}
func containsType(needle reflect.Type, haystack []reflect.Type) (contains bool) {
for _, t := range haystack {
if needle.Kind() == reflect.Ptr {
if needle.Elem() == t {
return true
}
} else if needle == t {
return true
}
}
return false
}
//nolint:gocyclo
func readTags(prefix string, t reflect.Type) (tags []string) {
tags = make([]string, 0)
if t.Kind() != reflect.Struct {
if t.Kind() == reflect.Slice {
tags = append(tags, readTags(getKeyNameFromTagAndPrefix(prefix, "", true, false), t.Elem())...)
}
return
}
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
tag := field.Tag.Get("koanf")
if tag == "" {
tags = append(tags, prefix)
continue
}
switch kind := field.Type.Kind(); kind {
case reflect.Struct:
if !containsType(field.Type, decodedTypes) {
tags = append(tags, readTags(getKeyNameFromTagAndPrefix(prefix, tag, false, false), field.Type)...)
continue
}
case reflect.Slice, reflect.Map:
switch field.Type.Elem().Kind() {
case reflect.Struct:
if !containsType(field.Type.Elem(), decodedTypes) {
tags = append(tags, getKeyNameFromTagAndPrefix(prefix, tag, false, false))
tags = append(tags, readTags(getKeyNameFromTagAndPrefix(prefix, tag, kind == reflect.Slice, kind == reflect.Map), field.Type.Elem())...)
continue
}
case reflect.Slice:
tags = append(tags, readTags(getKeyNameFromTagAndPrefix(prefix, tag, kind == reflect.Slice, kind == reflect.Map), field.Type.Elem())...)
}
case reflect.Ptr:
switch field.Type.Elem().Kind() {
case reflect.Struct:
if !containsType(field.Type.Elem(), decodedTypes) {
tags = append(tags, readTags(getKeyNameFromTagAndPrefix(prefix, tag, false, false), field.Type.Elem())...)
continue
}
case reflect.Slice:
if field.Type.Elem().Elem().Kind() == reflect.Struct {
if !containsType(field.Type.Elem(), decodedTypes) {
tags = append(tags, readTags(getKeyNameFromTagAndPrefix(prefix, tag, true, false), field.Type.Elem())...)
continue
}
}
}
}
tags = append(tags, getKeyNameFromTagAndPrefix(prefix, tag, false, false))
}
return tags
}
func getKeyNameFromTagAndPrefix(prefix, name string, isSlice, isMap bool) string {
nameParts := strings.SplitN(name, ",", 2)
if prefix == "" {
return nameParts[0]
}
if len(nameParts) == 2 && nameParts[1] == "squash" {
return prefix
}
switch {
case isMap:
if name == "" {
return fmt.Sprintf("%s.*", prefix)
}
return fmt.Sprintf("%s.%s.*", prefix, nameParts[0])
case isSlice:
if name == "" {
return fmt.Sprintf("%s[]", prefix)
}
return fmt.Sprintf("%s.%s[]", prefix, nameParts[0])
default:
return fmt.Sprintf("%s.%s", prefix, nameParts[0])
}
}

View File

@ -348,6 +348,8 @@ func runSuiteTests(suiteName string, withEnv bool) error {
cmd.Env = append(cmd.Env, "HEADLESS=y")
}
cmd.Env = append(cmd.Env, "SUITES_LOG_LEVEL="+log.GetLevel().String())
testErr := cmd.Run()
// If the tests failed, run the error hook.

View File

@ -140,9 +140,7 @@ func setupSuite(cmd *cobra.Command, args []string) {
log.Fatal(err)
}
err = s.SetUp(suiteTmpDirectory)
if err != nil {
if err = s.SetUp(suiteTmpDirectory); err != nil {
log.Error("Failure during environment deployment.")
teardownSuite(nil, args)
log.Fatal(err)

View File

@ -51,12 +51,6 @@ server:
## Useful to allow overriding of specific static assets.
# asset_path: /config/assets/
## Enables the pprof endpoint.
enable_pprof: false
## Enables the expvars endpoint.
enable_expvars: false
## Disables writing the health check vars to /app/.healthcheck.env which makes healthcheck.sh return exit code 0.
## This is disabled by default if either /app/.healthcheck.env or /app/healthcheck.sh do not exist.
disable_healthcheck: false
@ -104,6 +98,30 @@ server:
## Idle timeout.
# idle: 30s
## Server Endpoints configuration.
## This section is considered advanced and it SHOULD NOT be configured unless you've read the relevant documentation.
# endpoints:
## Enables the pprof endpoint.
# enable_pprof: false
## Enables the expvars endpoint.
# enable_expvars: false
## Configure the authz endpoints.
# authz:
# forward-auth:
# implementation: ForwardAuth
# authn_strategies: []
# ext-authz:
# implementation: ExtAuthz
# authn_strategies: []
# auth-request:
# implementation: AuthRequest
# authn_strategies: []
# legacy:
# implementation: Legacy
# authn_strategies: []
##
## Log Configuration
##
@ -505,7 +523,6 @@ authentication_backend:
# variant: standard
# cost: 12
##
## Password Policy Configuration.
##
@ -540,6 +557,23 @@ password_policy:
## Configures the minimum score allowed.
min_score: 3
##
## Privacy Policy Configuration
##
## Parameters used for displaying the privacy policy link and drawer.
privacy_policy:
## Enables the display of the privacy policy using the policy_url.
enabled: false
## Enables the display of the privacy policy drawer which requires users accept the privacy policy
## on a per-browser basis.
require_user_acceptance: false
## The URL of the privacy policy document. Must be an absolute URL and must have the 'https://' scheme.
## If the privacy policy enabled option is true, this MUST be provided.
policy_url: ''
##
## Access Control Configuration
##

View File

@ -0,0 +1,209 @@
---
title: "4.38: Pre-Release Notes"
description: "Authelia 4.38 is just around the corner. This version has several additional features and improvements to existing features. In this blog post we'll discuss the new features and roughly what it means for users."
lead: "Pre-Release Notes for 4.38"
excerpt: "Authelia 4.38 is just around the corner. This version has several additional features and improvements to existing features. In this blog post we'll discuss the new features and roughly what it means for users."
date: 2023-01-18T19:47:09+10:00
draft: false
images: []
categories: ["News", "Release Notes"]
tags: ["releases", "pre-release-notes"]
contributors: ["James Elliott"]
pinned: false
homepage: false
---
Authelia [4.38](https://github.com/authelia/authelia/milestone/17) is just around the corner. This version has several
additional features and improvements to existing features. In this blog post we'll discuss the new features and roughly
what it means for users.
Overall this release adds several major roadmap items. It's quite a big release. We expect a few bugs here and there but
nothing major. It's one of our biggest releases to date, so while it's taken a longer time than usual it's for good
reason we think.
We understand it's taking a bit longer than usual and people are getting anxious for their particular feature of
interest. We're trying to ensure that we sufficiently add automated tests to all of the new features in both the backend
and in the frontend via automated browser-based testing in Chromium to ensure a high quality user experience.
As this is a larger release we're probably going to ask users to help with some experimentation. If you're comfortable
backing up your database then please keep your eyes peeled in the [chat](../../information/contact.md#chat).
_**Note:** These features discussed in this blog post are still subject to change however they represent the most likely
outcome._
_**Important Note:** There are some changes in this release which deprecate older configurations. The changes should be
backwards compatible, however mistakes happen. In addition we advise making the adjustments to your configuration as
necessary as several new features will not be available or even possible without making the necessary adjustments. We
will be publishing some guides on making these adjustments on the blog in the near future, including an FAQ catered to
specific scenarios._
## OpenID Connect 1.0
As part of our ongoing effort for comprehensive support for [OpenID Connect 1.0] we'll be introducing several important
features. Please see the [roadmap](../../roadmap/active/openid-connect.md) for more information.
##### OAuth 2.0 Pushed Authorization Requests
Support for [RFC9126] known as [Pushed Authorization Requests] is one of the main features being added to our
[OpenID Connect 1.0] implementation in this release.
[Pushed Authorization Requests] allows for relying parties / clients to send the Authorization Request parameters over a
back-channel and receive an opaque URI to be used as the `redirect_uri` on the standard Authorization endpoint in place
of the standard Authorization Request parameters.
The endpoint used by this mechanism requires the relying party provides the Token Endpoint authentication parameters.
This means the actual Authorization Request parameters are never sent in the clear over the front-channel. This helps
mitigate a few things:
1. Enhanced privacy. This is the primary focus of this specification.
2. Part of conforming to the [OpenID Connect 1.0] specification [Financial-grade API Security Profile 1.0 (Advanced)].
3. Reduces the attack surface by preventing an attacker from adjusting request parameters prior to the Authorization
Server receiving them.
4. Reduces the attack surface marginally as less information is available over the front-channel which is the most
likely location where an attacker would have access to information. While reducing access to information is not
a reasonable primary security method, when combined with other mechanisms present in [OpenID Connect 1.0] it is
meaningful.
Even if an attacker gets the [Authorization Code], they are unlikely to have the `client_id` for example, and this is
required to exchange the [Authorization Code] for an [Access Token] and ID Token.
This option can be enforced globally for users who only use relying parties which support
[Pushed Authorization Requests], or can be individually enforced for each relying party which has support.
##### Proof Key for Code Exchange by OAuth Public Clients
While we already support [RFC7636] commonly known as [Proof Key for Code Exchange], and support enforcement at a global
level for either public clients or all clients, we're adding a feature where administrators will be able to enforce
[Proof Key for Code Exchange] on individual clients.
It should also be noted that [Proof Key for Code Exchange] can be used at the same time as
[OAuth 2.0 Pushed Authorization Requests](#oauth-20-pushed-authorization-requests).
These features combined with our requirement for the HTTPS scheme are very powerful security measures.
[RFC7636]: https://datatracker.ietf.org/doc/html/rfc7636
[RFC9126]: https://datatracker.ietf.org/doc/html/rfc9126
[Proof Key for Code Exchange]: https://oauth.net/2/pkce/
[Access Token]: https://oauth.net/2/access-tokens/
[Authorization Code]: https://oauth.net/2/grant-types/authorization-code/
[Financial-grade API Security Profile 1.0 (Advanced)]: https://openid.net/specs/openid-financial-api-part-2-1_0.html
[OpenID Connect 1.0]: https://openid.net/
[OpenID Connect 1.0]: https://openid.net/
[Pushed Authorization Requests]: https://oauth.net/2/pushed-authorization-requests/
## Multi-Domain Protection
In this release we are releasing the main implementation of the Multi-Domain Protection roadmap item.
Please see the [roadmap](../../roadmap/active/openid-connect.md) for more information.
##### Initial Implementation
_**Important Note:** This feature at the time of this writing, will not work well with Webauthn. Steps are being taken
to address this however it will not specifically delay the release of this feature._
This release see's the initial implementation of multi-domain protection. Users will be able to configure more than a
single root domain for cookies provided none of them are a subdomain of another domain configured. In addition each
domain can have individual settings.
This does not allow single sign-on between these distinct domains. When surveyed users had very low interest in this
feature and technically speaking it's not trivial to implement such a feature as a lot of critical security
considerations need to be addressed.
In addition this feature will allow configuration based detection of the Authelia Portal URI on proxies other than
NGINX/NGINX Proxy Manager/SWAG/HAProxy with the use of the new
[Customizable Authorization Endpoints](#customizable-authorization-endpoints). This is important as it means you only
need to configure a single middleware or helper to perform automatic redirection.
## Webauthn
As part of our ongoing effort for comprehensive support for Webauthn we'll be introducing several important
features. Please see the [roadmap](../../roadmap/active/webauthn.md) for more information.
##### Multiple Webauthn Credentials Per-User
In this release we see full support for multiple Webauthn credentials. This is a fairly basic feature but getting the
frontend experience right is important to us. This is going to be supported via the
[User Control Panel](#user-dashboard--control-panel).
## Customizable Authorization Endpoints
For the longest time we've managed to have the `/api/verify` endpoint perform all authorization verification. This has
served us well however we've been growing out of it. This endpoint is being deprecated in favor of new customizable
per-implementation endpoints. Each existing proxy we support uses one of these distinct implementations.
The old endpoint will still work, in fact you can technically configure an additional endpoint using the methodology of
it via the `Legacy` implementation. However this is strongly discouraged and will not intentionally have new features or
fixes (excluding security fixes) going forward.
In addition to being able to customize them you can create your own, and completely disable support for all other
implementations in the process. Use of these new endpoints will require reconfiguration of your proxy, we plan to
release a guide for each proxy.
## User Dashboard / Control Panel
As part of our ongoing effort for comprehensive support for a User Dashboard / Control Panel we'll be introducing
several important features. Please see the [roadmap](../../roadmap/active/dashboard-control-panel.md) for more
information.
##### Device Registration OTP
Instead of the current link, in this release users will instead be sent a One Time Password, cryptographically randomly
generated by Authelia. This One Time Password will grant users a duration to perform security sensitive tasks.
The motivation for this is that it works in more situations, and is slightly less prone to phishing.
##### TOTP Registration
Instead of just assuming that users have successfully registered their TOTP application, we will require users to enter
the TOTP code prior to it being saved to the database.
## Configuration
Several enhancements are landing for the configuration.
##### Directories
Users will now be able to configure a directory where all `.yml` and `.yaml` files will be loaded in lexical order.
This will not allow combining lists of items, but it will allow you to split portions of the configuration easily.
##### Discovery
Environment variables are being added to assist with configuration discovery, and this will be the default method for
our containers. The advantage is that since the variable will be available when execing into the container, even if
the configuration paths have changed or you've defined additional paths, the `authelia` command will know where the
files are if you properly use this variables.
##### Templating
The file based configuration will have access to several experimental templating filters which will assist in creating
configuration templates. The initial one will just expand *most* environment variables into the configuration. The
second will use the go template engine in a very similar way to how Helm operates.
As these features are experimental they may break, be removed, or otherwise not operate as expected. However most of our
testing indicates they're incredibly solid.
##### LDAP Implementation
Several new LDAP implementations which provide defaults are being introduced in this version to assist users in
integrating their LDAP server with Authelia.
## Miscellaneous
Some miscellaneous notes about this release.
##### Email Notifications
Events triggered by users will generate new notifications sent to their inbox, for example adding a new 2FA device.
##### Storage Import/Export
Utility functions to assist in exporting and subsequently importing the important values in Authelia are being added and
unified in this release.
##### Privacy Policy
We'll be introducing a feature which allows administrators to more easily comply with the GDPR which optionally shows a
link to their individual privacy policy on the frontend, and optionally requires users to accept it before using
Authelia.

View File

@ -0,0 +1,72 @@
---
title: "Privacy Policy"
description: "Privacy Policy Configuration."
lead: "This describes a section of the configuration for enabling a Privacy Policy link display."
date: 2020-02-29T01:43:59+01:00
draft: false
images: []
menu:
configuration:
parent: "miscellaneous"
weight: 199100
toc: true
---
## Configuration
```yaml
privacy_policy:
enabled: false
require_user_acceptance: false
policy_url: ''
```
## Options
### enabled
{{< confkey type="boolean" default="false" required="no" >}}
Enables the display of the Privacy Policy link.
### require_user_acceptance
{{< confkey type="boolean" default="false" required="no" >}}
Requires users accept per-browser the Privacy Policy via a Dialog Drawer at the bottom of the page. The fact they have
accepted is recorded and checked in the browser
[localStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage).
If the user has not accepted the policy they should not be able to interact with the Authelia UI via normal means.
Administrators who are required to abide by the [GDPR] or other privacy laws should be advised that
[OpenID Connect 1.0](../identity-providers/open-id-connect.md) clients configured with the `implicit` consent mode are
unlikely to trigger the display of the Authelia UI if the user is already authenticated.
We wont be adding checks like this to the `implicit` consent mode when that mode in particular is unlikely to be
compliant with those laws, and that mode is not strictly compliant with the OpenID Connect 1.0 specifications. It is
therefore recommended if `require_user_acceptance` is enabled then administrators should avoid using the `implicit`
consent mode or do so at their own risk.
### policy_url
{{< confkey type="string" required="situational" >}}
The privacy policy URL is a URL which optionally is displayed in the frontend linking users to the administrators
privacy policy. This is useful for users who wish to abide by laws such as the [GDPR].
Administrators can view the particulars of what _Authelia_ collects out of the box with our
[Privacy Policy](https://www.authelia.com/privacy/#application).
This value must be an absolute URL, and must have the `https://` scheme.
This option is required if the [enabled](#enabled) option is true.
[GDPR]: https://gdpr-info.eu/
_**Example:**_
```yaml
privacy_policy:
enabled: true
policy_url: 'https://www.example.com/privacy-policy'
```

View File

@ -0,0 +1,72 @@
---
title: "Server Authz Endpoints"
description: "Configuring the Server Authz Endpoint Settings."
lead: "Authelia supports several authorization endpoints on the internal webserver. This section describes how to configure and tune them."
date: 2022-10-31T09:33:39+11:00
draft: false
images: []
menu:
configuration:
parent: "miscellaneous"
weight: 199210
toc: true
aliases:
- /c/authz
---
## Configuration
```yaml
server:
endpoints:
authz:
forward-auth:
implementation: ForwardAuth
authn_strategies: []
ext-authz:
implementation: ExtAuthz
authn_strategies: []
auth-request:
implementation: AuthRequest
authn_strategies: []
legacy:
implementation: Legacy
authn_strategies: []
```
## Name
{{< confkey type="string" required="yes" >}}
The first level under the `authz` directive is the name of the endpoint. In the example these names are `forward-auth`,
`ext-authz`, `auth-request`, and `legacy`.
The name correlates with the path of the endpoint. All endpoints start with `/api/authz/`, and end with the name. In the
example the `forward-auth` endpoint has a full path of `/api/authz/forward-auth`.
Valid characters for the name are alphanumeric as well as `-` and `_`. They MUST start AND end with an
alphanumeric character.
### implementation
{{< confkey type="string" required="yes" >}}
The underlying implementation for the endpoint. Valid case-sensitive values are `ForwardAuth`, `ExtAuthz`,
`AuthRequest`, and `Legacy`. Read more about the implementations in the
[reference guide](../../reference/guides/proxy-authorization.md#implementations).
### authn_strategies
{{< confkey type="list" required="no" >}}
A list of authentication strategies and their configuration options. These strategies are in order, and the first one
which succeeds is used. Failures other than lacking the sufficient information in the request to perform the strategy
immediately short-circuit the authentication, otherwise the next strategy in the list is attempted.
#### name
{{< confkey type="string" required="yes" >}}
The name of the strategy. Valid case-sensitive values are `CookieSession`, `HeaderAuthorization`,
`HeaderProxyAuthorization`, `HeaderAuthRequestProxyAuthorization`, and `HeaderLegacy`. Read more about the strategies in
the [reference guide](../../reference/guides/proxy-authorization.md#authn-strategies).

View File

@ -22,8 +22,6 @@ server:
host: 0.0.0.0
port: 9091
path: ""
enable_pprof: false
enable_expvars: false
disable_healthcheck: false
tls:
key: ""
@ -38,6 +36,22 @@ server:
read: 6s
write: 6s
idle: 30s
endpoints:
enable_pprof: false
enable_expvars: false
authz:
forward-auth:
implementation: ForwardAuth
authn_strategies: []
ext-authz:
implementation: ExtAuthz
authn_strategies: []
auth-request:
implementation: AuthRequest
authn_strategies: []
legacy:
implementation: Legacy
authn_strategies: []
```
## Options
@ -100,18 +114,6 @@ assets that can be overridden must be placed in the `asset_path`. The structure
can be overriden is documented in the
[Sever Asset Overrides Reference Guide](../../reference/guides/server-asset-overrides.md).
### enable_pprof
{{< confkey type="boolean" default="false" required="no" >}}
Enables the go pprof endpoints.
### enable_expvars
{{< confkey type="boolean" default="false" required="no" >}}
Enables the go expvars endpoints.
### disable_healthcheck
{{< confkey type="boolean" default="false" required="no" >}}
@ -177,6 +179,32 @@ information.
Configures the server timeouts. See the [Server Timeouts](../prologue/common.md#server-timeouts) documentation for more
information.
### endpoints
#### enable_pprof
{{< confkey type="boolean" default="false" required="no" >}}
*__Security Note:__ This is a developer endpoint. __DO NOT__ enable it unless you know why you're enabling it.
__DO NOT__ enable this in production.*
Enables the go [pprof](https://pkg.go.dev/net/http/pprof) endpoints.
#### enable_expvars
*__Security Note:__ This is a developer endpoint. __DO NOT__ enable it unless you know why you're enabling it.
__DO NOT__ enable this in production.*
{{< confkey type="boolean" default="false" required="no" >}}
Enables the go [expvar](https://pkg.go.dev/expvar) endpoints.
#### authz
This is an *__advanced__* option allowing configuration of the authorization endpoints and has its own section.
Generally this does not need to be configured for most use cases. See the
[authz configuration](./server-endpoints-authz.md) for more information.
## Additional Notes
### Buffer Sizes

View File

@ -204,4 +204,4 @@ Configures the server write timeout.
*__Note:__ This setting uses the [duration notation format](#duration-notation-format). Please see the
[common options](#duration-notation-format) documentation for information on this format.*
Configures the server write timeout.
Configures the server idle timeout.

View File

@ -18,18 +18,25 @@ __Authelia__ and its development workflow can be tested with [Docker] and [Docke
In order to build and contribute to __Authelia__, you need to make sure the following are installed in your environment:
* [go] *(v1.18 or greater)*
* [Docker]
* [Docker Compose]
* [Node.js] *(v16 or greater)*
* [pnpm]
* General:
* [git]
* Backend Development:
* [go] *(v1.19 or greater)*
* [gcc]
* Frontend Development
* [Node.js] *(v18 or greater)*
* [pnpm]
* Integration Suites:
* [Docker]
* [Docker Compose]
* [chromium]
The additional tools are recommended:
* [golangci-lint]
* [goimports-reviser]
* [yamllint]
* Either the [VSCodium] or [GoLand] IDE
* [VSCodium] or [GoLand]
## Scripts
@ -80,3 +87,6 @@ listed subdomains from your browser, and they will be served by the reverse prox
[yamllint]: https://yamllint.readthedocs.io/en/stable/quickstart.html
[VSCodium]: https://vscodium.com/
[GoLand]: https://www.jetbrains.com/go/
[chromium]: https://www.chromium.org/
[git]: https://git-scm.com/
[gcc]: https://gcc.gnu.org/

View File

@ -39,27 +39,23 @@ spec:
envoyExtAuthzHttp:
service: 'authelia.default.svc.cluster.local'
port: 80
pathPrefix: '/api/verify/'
pathPrefix: '/api/authz/ext-authz/'
includeRequestHeadersInCheck:
- accept
- cookie
- proxy-authorization
- 'accept'
- 'cookie'
- 'authorization'
- 'proxy-authorization'
headersToUpstreamOnAllow:
- 'authorization'
- 'proxy-authorization'
- 'remote-*'
- 'authelia-*'
includeAdditionalHeadersInCheck:
X-Authelia-URL: 'https://auth.example.com/'
X-Forwarded-Method: '%REQ(:METHOD)%'
X-Forwarded-Proto: '%REQ(:SCHEME)%'
X-Forwarded-Host: '%REQ(:AUTHORITY)%'
X-Forwarded-URI: '%REQ(:PATH)%'
X-Forwarded-For: '%DOWNSTREAM_REMOTE_ADDRESS_WITHOUT_PORT%'
headersToDownstreamOnDeny:
- set-cookie
- 'set-cookie'
headersToDownstreamOnAllow:
- set-cookie
- 'set-cookie'
```
### Authorization Policy

View File

@ -41,11 +41,9 @@ be applied to the Authelia Ingress itself.*
```yaml
annotations:
nginx.ingress.kubernetes.io/auth-method: GET
nginx.ingress.kubernetes.io/auth-url: http://authelia.default.svc.cluster.local/api/verify
nginx.ingress.kubernetes.io/auth-url: http://authelia.default.svc.cluster.local/api/authz/auth-request
nginx.ingress.kubernetes.io/auth-signin: https://auth.example.com?rm=$request_method
nginx.ingress.kubernetes.io/auth-response-headers: Remote-User,Remote-Name,Remote-Groups,Remote-Email
nginx.ingress.kubernetes.io/auth-snippet: |
proxy_set_header X-Forwarded-Method $request_method;
nginx.ingress.kubernetes.io/auth-response-headers: Authorization,Proxy-Authorization,Remote-User,Remote-Name,Remote-Groups,Remote-Email
```
[ingress-nginx]: https://kubernetes.github.io/ingress-nginx/

View File

@ -61,12 +61,17 @@ metadata:
app.kubernetes.io/name: authelia
spec:
forwardAuth:
address: http://authelia.default.svc.cluster.local/api/verify?rd=https%3A%2F%2Fauth.example.com%2F
address: 'http://authelia.default.svc.cluster.local/api/authz/forward-auth'
## The following commented line is for configuring the Authelia URL in the proxy. We strongly suggest this is
## configured in the Session Cookies section of the Authelia configuration.
# address: 'http://authelia.default.svc.cluster.local/api/authz/forward-auth?authelia_url=https%3A%2F%2Fauth.example.com%2F'
authResponseHeaders:
- Remote-User
- Remote-Name
- Remote-Email
- Remote-Groups
- 'Authorization'
- 'Proxy-Authorization'
- 'Remote-User'
- 'Remote-Groups'
- 'Remote-Email'
- 'Remote-Name'
...
```
{{< /details >}}

View File

@ -98,7 +98,10 @@ auth.example.com {
# Protected Endpoint.
nextcloud.example.com {
forward_auth authelia:9091 {
uri /api/verify?rd=https://auth.example.com/
uri /api/authz/forward-auth
## The following commented line is for configuring the Authelia URL in the proxy. We strongly suggest
## this is configured in the Session Cookies section of the Authelia configuration.
# uri /api/authz/forward-auth?authelia_url=https://auth.example.com/
copy_headers Remote-User Remote-Groups Remote-Name Remote-Email
## This import needs to be included if you're relying on a trusted proxies configuration.
@ -137,7 +140,7 @@ example.com {
@nextcloud path /nextcloud /nextcloud/*
handle @nextcloud {
forward_auth authelia:9091 {
uri /api/verify?rd=https://example.com/authelia/
uri /api/authz/forward-auth?authelia_url=https://example.com/authelia/
copy_headers Remote-User Remote-Groups Remote-Name Remote-Email
## This import needs to be included if you're relying on a trusted proxies configuration.
@ -183,7 +186,7 @@ nextcloud.example.com {
import trusted_proxy_list
method GET
rewrite "/api/verify?rd=https://auth.example.com/"
rewrite "/api/authz/forward-auth?authelia_url=https://auth.example.com/"
header_up X-Forwarded-Method {method}
header_up X-Forwarded-Uri {uri}

View File

@ -169,7 +169,7 @@ static_resources:
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz
http_service:
path_prefix: '/api/verify/'
path_prefix: /api/authz/ext-authz/
server_uri:
uri: authelia:9091
cluster: authelia
@ -181,18 +181,12 @@ static_resources:
- exact: cookie
- exact: proxy-authorization
headers_to_add:
- key: X-Authelia-URL
value: 'https://auth.example.com/'
- key: X-Forwarded-Method
value: '%REQ(:METHOD)%'
- key: X-Forwarded-Proto
value: '%REQ(:SCHEME)%'
- key: X-Forwarded-Host
value: '%REQ(:AUTHORITY)%'
- key: X-Forwarded-Uri
value: '%REQ(:PATH)%'
- key: X-Forwarded-For
value: '%DOWNSTREAM_REMOTE_ADDRESS_WITHOUT_PORT%'
## The following commented lines are for configuring the Authelia URL in the proxy. We
## strongly suggest this is configured in the Session Cookies section of the Authelia configuration.
# - key: X-Authelia-URL
# value: https://auth.example.com
authorization_response:
allowed_upstream_headers:
patterns:

View File

@ -193,13 +193,11 @@ frontend fe_http
# Required headers
http-request set-header X-Real-IP %[src]
http-request set-header X-Forwarded-Method %[var(req.method)]
http-request set-header X-Forwarded-Proto %[var(req.scheme)]
http-request set-header X-Forwarded-Host %[req.hdr(Host)]
http-request set-header X-Forwarded-Uri %[path]%[var(req.questionmark)]%[query]
http-request set-header X-Original-Method %[var(req.method)]
http-request set-header X-Original-URL %[var(req.scheme)]://%[req.hdr(Host)]%[path]%[var(req.questionmark)]%[query]
# Protect endpoints with haproxy-auth-request and Authelia
http-request lua.auth-request be_authelia /api/verify if protected-frontends
http-request lua.auth-request be_authelia /api/authz/auth-request if protected-frontends
# Force `Authorization` header via query arg to /api/verify
http-request lua.auth-request be_authelia /api/verify?auth=basic if protected-frontends-basic
@ -293,12 +291,11 @@ frontend fe_http
# Required headers
http-request set-header X-Real-IP %[src]
http-request set-header X-Forwarded-Proto %[var(req.scheme)]
http-request set-header X-Forwarded-Host %[req.hdr(Host)]
http-request set-header X-Forwarded-Uri %[path]%[var(req.questionmark)]%[query]
http-request set-header X-Original-Method %[var(req.method)]
http-request set-header X-Original-URL %[var(req.scheme)]://%[req.hdr(Host)]%[path]%[var(req.questionmark)]%[query]
# Protect endpoints with haproxy-auth-request and Authelia
http-request lua.auth-request be_authelia_proxy /api/verify if protected-frontends
http-request lua.auth-request be_authelia_proxy /api/authz/auth-request if protected-frontends
# Force `Authorization` header via query arg to /api/verify
http-request lua.auth-request be_authelia_proxy /api/verify?auth=basic if protected-frontends-basic

View File

@ -31,21 +31,22 @@ See [support](support.md) for support information.
## Integration Implementation
Authelia is capable of being integrated into many proxies due to the decisions regarding the implementation. We handle
requests to the `/api/verify` endpoint with specific headers and return standardized responses based on the headers and
requests to the authz endpoints with specific headers and return standardized responses based on the headers and
the policy engines determination about what must be done.
### Destination Identification
The method to identify the destination of a request relies on metadata headers which need to be set by your reverse
proxy. The headers we rely on are as follows:
Broadly speaking, the method to identify the destination of a request relies on metadata headers which need to be set by
your reverse proxy. The headers we rely on at the authz endpoints are as follows:
* [X-Forwarded-Proto](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto)
* [X-Forwarded-Host](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Host)
* X-Forwarded-Uri
* [X-Forwarded-For](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For)
* X-Forwarded-Method
* X-Forwarded-Method / X-Original-Method
* X-Original-URL
Alternatively we utilize `X-Original-URL` header which is expected to contain a fully formatted URL.
The specifics however are dictated by the specific [Authorization Implementation](../../reference/guides/proxy-authorization.md) used.
### User Identification

View File

@ -197,6 +197,10 @@ server {
location /api/verify {
proxy_pass $upstream;
}
location /api/authz/ {
proxy_pass $upstream;
}
}
```
{{< /details >}}
@ -376,7 +380,7 @@ proxy_set_header X-Forwarded-For $remote_addr;
{{< details "/config/nginx/snippets/authelia-location.conf" >}}
```nginx
set $upstream_authelia http://authelia:9091/api/verify;
set $upstream_authelia http://authelia:9091/api/authz/auth-request;
## Virtual endpoint created by nginx to forward auth requests.
location /authelia {
@ -386,12 +390,8 @@ location /authelia {
## Headers
## The headers starting with X-* are required.
proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
proxy_set_header X-Original-Method $request_method;
proxy_set_header X-Forwarded-Method $request_method;
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-Original-URL $scheme://$http_host$request_uri;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header Content-Length "";
proxy_set_header Connection "";
@ -458,9 +458,12 @@ snippet is rarely required. It's only used if you want to only allow
[HTTP Basic Authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication) for a particular
endpoint. It's recommended to use [authelia-location.conf](#authelia-locationconf) instead.*
_**Note:** This example assumes you configured an authz endpoint with the name `auth-request/basic` and the
implementation `AuthRequest` which contains the `HeaderAuthorization` and `HeaderProxyAuthorization` strategies._
{{< details "/config/nginx/snippets/authelia-location-basic.conf" >}}
```nginx
set $upstream_authelia http://authelia:9091/api/verify?auth=basic;
set $upstream_authelia http://authelia:9091/api/authz/auth-request/basic;
# Virtual endpoint created by nginx to forward auth requests.
location /authelia-basic {
@ -470,6 +473,7 @@ location /authelia-basic {
## Headers
## The headers starting with X-* are required.
proxy_set_header X-Original-Method $request_method;
proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
proxy_set_header X-Original-Method $request_method;
proxy_set_header X-Forwarded-Method $request_method;

View File

@ -15,19 +15,24 @@ aliases:
- /docs/home/supported-proxies.html
---
| Proxy | [Standard](#standard) | [Kubernetes](#kubernetes) | [XHR Redirect](#xhr-redirect) | [Request Method](#request-method) |
|:---------------------:|:------------------------------------------------------------------:|:-------------------------------------------------------------------------------------:|:---------------------------------:|:---------------------------------:|
| [Traefik] | {{% support support="full" link="traefik.md" %}} | {{% support support="full" link="../../integration/kubernetes/traefik-ingress.md" %}} | {{% support support="full" %}} | {{% support support="full" %}} |
| [Caddy] | {{% support support="full" link="caddy.md" %}} | {{% support support="unknown" %}} | {{% support support="full" %}} | {{% support support="full" %}} |
| [Envoy] | {{% support support="full" link="envoy.md" %}} | {{% support support="full" link="../../integration/kubernetes/istio.md" %}} | {{% support support="unknown" %}} | {{% support support="full" %}} |
| [NGINX] | {{% support support="full" link="nginx.md" %}} | {{% support support="full" link="../../integration/kubernetes/nginx-ingress.md" %}} | {{% support %}} | {{% support support="full" %}} |
| [NGINX Proxy Manager] | {{% support support="full" link="nginx-proxy-manager/index.md" %}} | {{% support support="unknown" %}} | {{% support %}} | {{% support support="full" %}} |
| [SWAG] | {{% support support="full" link="swag.md" %}} | {{% support support="unknown" %}} | {{% support %}} | {{% support support="full" %}} |
| [HAProxy] | {{% support support="full" link="haproxy.md" %}} | {{% support support="unknown" %}} | {{% support support="unknown" %}} | {{% support support="full" %}} |
| [Skipper] | {{% support support="full" link="skipper.md" %}} | {{% support support="unknown" %}} | {{% support support="unknown" %}} | {{% support support="unknown" %}} |
| [Traefik] 1.x | {{% support support="full" link="traefikv1.md" %}} | {{% support support="unknown" %}} | {{% support support="full" %}} | {{% support support="full" %}} |
| [Apache] | {{% support link="#apache" %}} | {{% support %}} | {{% support %}} | {{% support %}} |
| [IIS] | {{% support link="#iis" %}} | {{% support %}} | {{% support %}} | {{% support %}} |
| Proxy | [Implementation] | [Standard](#standard) | [Kubernetes](#kubernetes) | [XHR Redirect](#xhr-redirect) | [Request Method](#request-method) |
|:---------------------:|:----------------:|:------------------------------------------------------------------:|:-------------------------------------------------------------------------------------:|:---------------------------------:|:---------------------------------:|
| [Traefik] | [ForwardAuth] | {{% support support="full" link="traefik.md" %}} | {{% support support="full" link="../../integration/kubernetes/traefik-ingress.md" %}} | {{% support support="full" %}} | {{% support support="full" %}} |
| [Caddy] | [ForwardAuth] | {{% support support="full" link="caddy.md" %}} | {{% support support="unknown" %}} | {{% support support="full" %}} | {{% support support="full" %}} |
| [Envoy] | [ExtAuthz] | {{% support support="full" link="envoy.md" %}} | {{% support support="full" link="../../integration/kubernetes/istio.md" %}} | {{% support support="unknown" %}} | {{% support support="full" %}} |
| [NGINX] | [AuthRequest] | {{% support support="full" link="nginx.md" %}} | {{% support support="full" link="../../integration/kubernetes/nginx-ingress.md" %}} | {{% support %}} | {{% support support="full" %}} |
| [NGINX Proxy Manager] | [AuthRequest] | {{% support support="full" link="nginx-proxy-manager/index.md" %}} | {{% support support="unknown" %}} | {{% support %}} | {{% support support="full" %}} |
| [SWAG] | [AuthRequest] | {{% support support="full" link="swag.md" %}} | {{% support support="unknown" %}} | {{% support %}} | {{% support support="full" %}} |
| [HAProxy] | [AuthRequest] | {{% support support="full" link="haproxy.md" %}} | {{% support support="unknown" %}} | {{% support support="unknown" %}} | {{% support support="full" %}} |
| [Skipper] | [ForwardAuth] | {{% support support="full" link="skipper.md" %}} | {{% support support="unknown" %}} | {{% support support="unknown" %}} | {{% support support="unknown" %}} |
| [Traefik] 1.x | [ForwardAuth] | {{% support support="full" link="traefikv1.md" %}} | {{% support support="unknown" %}} | {{% support support="full" %}} | {{% support support="full" %}} |
| [Apache] | N/A | {{% support link="#apache" %}} | {{% support %}} | {{% support %}} | {{% support %}} |
| [IIS] | N/A | {{% support link="#iis" %}} | {{% support %}} | {{% support %}} | {{% support %}} |
[ForwardAuth]: ../../reference/guides/proxy-authorization.md#forwardauth
[AuthRequest]: ../../reference/guides/proxy-authorization.md#authrequest
[ExtAuthz]: ../../reference/guides/proxy-authorization.md#extauthz
[Implementation]: ../../reference/guides/proxy-authorization.md#implementations
Legend:

View File

@ -152,12 +152,12 @@ services:
- 'traefik.http.routers.authelia.rule=Host(`auth.example.com`)'
- 'traefik.http.routers.authelia.entryPoints=https'
- 'traefik.http.routers.authelia.tls=true'
- 'traefik.http.middlewares.authelia.forwardAuth.address=http://authelia:9091/api/verify?rd=https%3A%2F%2Fauth.example.com%2F'
- 'traefik.http.middlewares.authelia.forwardAuth.address=http://authelia:9091/api/authz/forward-auth'
## The following commented line is for configuring the Authelia URL in the proxy. We strongly suggest this is
## configured in the Session Cookies section of the Authelia configuration.
# - 'traefik.http.middlewares.authelia.forwardAuth.address=http://authelia:9091/api/authz/forward-auth?authelia_url=https%3A%2F%2Fauth.example.com%2F'
- 'traefik.http.middlewares.authelia.forwardAuth.trustForwardHeader=true'
- 'traefik.http.middlewares.authelia.forwardAuth.authResponseHeaders=Remote-User,Remote-Groups,Remote-Name,Remote-Email'
- 'traefik.http.middlewares.authelia-basic.forwardAuth.address=http://authelia:9091/api/verify?auth=basic'
- 'traefik.http.middlewares.authelia-basic.forwardAuth.trustForwardHeader=true'
- 'traefik.http.middlewares.authelia-basic.forwardAuth.authResponseHeaders=Remote-User,Remote-Groups,Remote-Name,Remote-Email'
- 'traefik.http.middlewares.authelia.forwardAuth.authResponseHeaders=Authorization,Proxy-Authorization,Remote-User,Remote-Groups,Remote-Name,Remote-Email'
nextcloud:
container_name: nextcloud
image: linuxserver/nextcloud
@ -364,26 +364,30 @@ http:
middlewares:
authelia:
forwardAuth:
address: https://authelia:9091/api/verify?rd=https%3A%2F%2Fauth.example.com%2F
address: 'https://authelia:9091/api/authz/forward-auth?authelia_url=https%3A%2F%2Fauth.example.com%2F'
trustForwardHeader: true
authResponseHeaders:
- "Remote-User"
- "Remote-Groups"
- "Remote-Email"
- "Remote-Name"
- 'Authorization'
- 'Proxy-Authorization'
- 'Remote-User'
- 'Remote-Groups'
- 'Remote-Email'
- 'Remote-Name'
tls:
ca: /certificates/ca.public.crt
cert: /certificates/traefik.public.crt
key: /certificates/traefik.private.pem
authelia-basic:
forwardAuth:
address: https://authelia:9091/api/verify?auth=basic
address: 'https://authelia:9091/api/verify?auth=basic'
trustForwardHeader: true
authResponseHeaders:
- "Remote-User"
- "Remote-Groups"
- "Remote-Email"
- "Remote-Name"
- 'Authorization'
- 'Proxy-Authorization'
- 'Remote-User'
- 'Remote-Groups'
- 'Remote-Email'
- 'Remote-Name'
tls:
ca: /certificates/ca.public.crt
cert: /certificates/traefik.public.crt
@ -491,9 +495,12 @@ This can be avoided a couple different ways:
2. Define the __Authelia__ middleware on your [Traefik] container. See the below example.
```yaml
- 'traefik.http.middlewares.authelia.forwardAuth.address=http://authelia:9091/api/verify?rd=https%3A%2F%2Fauth.example.com%2F'
- 'traefik.http.middlewares.authelia.forwardAuth.address=http://authelia:9091/api/authz/forward-auth'
## The following commented line is for configuring the Authelia URL in the proxy. We strongly suggest this is
## configured in the Session Cookies section of the Authelia configuration.
# - 'traefik.http.middlewares.authelia.forwardAuth.address=http://authelia:9091/api/authz/forward-auth?authelia_url=https%3A%2F%2Fauth.example.com%2F'
- 'traefik.http.middlewares.authelia.forwardAuth.trustForwardHeader=true'
- 'traefik.http.middlewares.authelia.forwardAuth.authResponseHeaders=Remote-User,Remote-Groups,Remote-Name,Remote-Email'
- 'traefik.http.middlewares.authelia.forwardAuth.authResponseHeaders=Authorization,Proxy-Authorization,Remote-User,Remote-Groups,Remote-Name,Remote-Email'
```
## See Also

View File

@ -90,9 +90,9 @@ services:
- 'traefik.frontend.rule=Host:traefik.example.com'
- 'traefik.port=8081'
ports:
- 80:80
- 443:443
- 8081:8081
- '80:80'
- '443:443'
- '8081:8081'
restart: unless-stopped
command:
- '--api'
@ -132,9 +132,12 @@ services:
- net
labels:
- 'traefik.frontend.rule=Host:nextcloud.example.com'
- 'traefik.frontend.auth.forward.address=http://authelia:9091/api/verify?rd=https://auth.example.com/'
- 'traefik.frontend.auth.forward.address=http://authelia:9091/api/authz/forward-auth'
## The following commented line is for configuring the Authelia URL in the proxy. We strongly suggest this is
## configured in the Session Cookies section of the Authelia configuration.
# - 'traefik.frontend.auth.forward.address=http://authelia:9091/api/authz/forward-auth?authelia_url=https%3A%2F%2Fauth.example.com%2F'
- 'traefik.frontend.auth.forward.trustForwardHeader=true'
- 'traefik.frontend.auth.forward.authResponseHeaders=Remote-User,Remote-Groups,Remote-Name,Remote-Email'
- 'traefik.frontend.auth.forward.authResponseHeaders=Authorization,Proxy-Authorization,Remote-User,Remote-Groups,Remote-Name,Remote-Email'
expose:
- 443
restart: unless-stopped
@ -151,9 +154,9 @@ services:
- net
labels:
- 'traefik.frontend.rule=Host:heimdall.example.com'
- 'traefik.frontend.auth.forward.address=http://authelia:9091/api/verify?auth=basic'
- 'traefik.frontend.auth.forward.address=http://authelia:9091/api/authz/forward-auth/basic'
- 'traefik.frontend.auth.forward.trustForwardHeader=true'
- 'traefik.frontend.auth.forward.authResponseHeaders=Remote-User,Remote-Groups,Remote-Name,Remote-Email'
- 'traefik.frontend.auth.forward.authResponseHeaders=Authorization,Proxy-Authorization,Remote-User,Remote-Groups,Remote-Name,Remote-Email'
expose:
- 443
restart: unless-stopped

View File

@ -25,22 +25,21 @@ when configured. If metrics are enabled the metrics listener listens on `0.0.0.0
#### Recorded Metrics
##### Vectored Histograms
| Name | Vectors | Buckets |
|:-----------------------:|:-------:|:-------------------------------------------------------------------------------------------------------------:|
| authentication_duration | success | .0005, .00075, .001, .005, .01, .025, .05, .075, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.8, 0.9, 1, 5, 10, 15, 30, 60 |
| request_duration | code | .005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10, 15, 20, 30, 40, 50, 60 |
##### Vectored Counters
| Name | Vectors |
|:----------------------------:|:---------------------:|
| request | code, method |
| verify_request | code |
| authentication_first_factor | success, banned |
| authentication_second_factor | success, banned, type |
| Name | Vectors | Description |
|:-------------------:|:---------------------:|:--------------------:|
| request | code, method | All Requests |
| authz | code | Authz Requests |
| authn | success, banned | Authn Requests (1FA) |
| authn_second_factor | success, banned, type | Authn Requests (2FA) |
##### Vectored Histograms
| Name | Vectors | Buckets |
|:----------------:|:-------:|:-------------------------------------------------------------------------------------------------------------:|
| authn_duration | success | .0005, .00075, .001, .005, .01, .025, .05, .075, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.8, 0.9, 1, 5, 10, 15, 30, 60 |
| request_duration | code | .005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10, 15, 20, 30, 40, 50, 60 |
#### Vector Definitions

View File

@ -0,0 +1,247 @@
---
title: "Proxy Authorization"
description: "A reference guide on Proxy Authorization implementations"
lead: "This section contains reference guide on Proxy Authorization implementations Authelia supports."
date: 2022-10-31T09:33:39+11:00
draft: false
images: []
menu:
reference:
parent: "guides"
weight: 220
toc: true
aliases:
- /r/proxy-authz
---
Proxies can integrate with Authelia via several authorization endpoints. These endpoints are by default configured
appropriately for most use cases; however they can be individually configured, removed, added, etc.
They are currently divided into two sections:
- [Implementations](#implementations)
- [Authn Strategies](#authn-strategies)
These endpoints are meant to collect important information from these requests via headers to determine both
metadata about the request (such as the resource and IP address of the user) which is determined via the
[Implementations](#implementations), and the identity of the user which is determined via the
[Authn Strategies](#authn-strategies).
## Default Endpoints
| Name | Path | [Implementation] | [Authn Strategies] |
|:------------:|:-----------------------:|:----------------:|:------------------------------------------------------:|
| forward-auth | /api/authz/forward-auth | [ForwardAuth] | [HeaderProxyAuthorization], [CookieSession] |
| ext-authz | /api/authz/ext-authz | [ExtAuthz] | [HeaderProxyAuthorization], [CookieSession] |
| auth-request | /api/authz/auth-request | [AuthRequest] | [HeaderAuthRequestProxyAuthorization], [CookieSession] |
| legacy | /api/verify | [Legacy] | [HeaderLegacy], [CookieSession] |
## Metadata
Various metadata is collected from the request made to the Authelia authorization server. This table describes the
metadata collected. All of this metadata is utilized for the purpose of determining if the user is authorized to a
particular resource.
| Name | Description |
|:------------:|:-----------------------------------------------:|
| Method | The Method Verb of the Request |
| Scheme | The URI Scheme of the Request |
| Hostname | The URI Hostname of the Request |
| Path | The URI Path of the Request |
| IP | The IP address of the client making the Request |
| Authelia URL | The URL of the Authelia Portal |
Some values may have either fallbacks or override values. If they exist they will be in the alternatives table which
will be below the main metadata table.
The metadata table contains the recommended source of this information and this source is often times automatic
depending on the proxy implementation. The difference between an override and a fallback is an override values will
take precedence over the metadata values, and fallbacks only take effect if the override values or metadata values are
completely unset.
## Implementations
### ForwardAuth
This is the implementation which supports [Traefik] via the [ForwardAuth Middleware], [Caddy] via the
[forward_auth directive], and [Skipper] via the [webhook auth filter].
#### ForwardAuth Metadata
| Metadata | Source | Key |
|:------------:|:----------------------------:|:--------------------:|
| Method | [Header] | `X-Forwarded-Method` |
| Scheme | [Header] | [X-Forwarded-Proto] |
| Hostname | [Header] | [X-Forwarded-Host] |
| Path | [Header] | `X-Forwarded-URI` |
| IP | [Header] | [X-Forwarded-For] |
| Authelia URL | Session Cookie Configuration | `authelia_url` |
#### ForwardAuth Metadata Alternatives
| Metadata | Alternative Type | Source | Key |
|:------------:|:----------------:|:--------------:|:--------------:|
| Scheme | Fallback | [Header] | Server Scheme |
| IP | Fallback | TCP Packet | Source IP |
| Authelia URL | Override | Query Argument | `authelia_url` |
### ExtAuthz
This is the implementation which supports [Envoy] via the [ExtAuthz Extension Filter].
#### ExtAuthz Metadata
| Metadata | Source | Key |
|:------------:|:----------------------------:|:-------------------:|
| Method | _[Start Line]_ | [HTTP Method] |
| Scheme | [Header] | [X-Forwarded-Proto] |
| Hostname | [Header] | [Host] |
| Path | [Header] | Endpoint Sub-Path |
| IP | [Header] | [X-Forwarded-For] |
| Authelia URL | Session Cookie Configuration | `authelia_url` |
#### ExtAuthz Metadata Alternatives
| Metadata | Alternative Type | Source | Key |
|:------------:|:----------------:|:----------:|:------------------:|
| Scheme | Fallback | [Header] | Server Scheme |
| IP | Fallback | TCP Packet | Source IP |
| Authelia URL | Override | [Header] | `X-Authelia-URL` |
### AuthRequest
This is the implementation which supports [NGINX] via the [auth_request HTTP module] and [HAProxy] via the
[auth-request lua plugin].
| Metadata | Source | Key |
|:------------:|:--------:|:-------------------:|
| Method | [Header] | `X-Original-Method` |
| Scheme | [Header] | `X-Original-URL` |
| Hostname | [Header] | `X-Original-URL` |
| Path | [Header] | `X-Original-URL` |
| IP | [Header] | [X-Forwarded-For] |
| Authelia URL | _N/A_ | _N/A_ |
_**Note:** This endpoint does not support automatic redirection. This is because there is no support on NGINX's side to
achieve this with `ngx_http_auth_request_module` and the redirection must be performed within the NGINX configuration._
#### AuthRequest Metadata Alternatives
| Metadata | Alternative Type | Source | Key |
|:--------:|:----------------:|:----------:|:---------:|
| IP | Fallback | TCP Packet | Source IP |
### Legacy
This is the legacy implementation which used to operate similar to both the [ForwardAuth](#forwardauth) and
[AuthRequest](#authrequest) implementations.
*__Note:__ This implementation has duplicate entries for metadata. This is due to the fact this implementation used to
cater for the AuthRequest and ForwardAuth implementations. The table is in order of precedence where if a header higher
in the list exists it is used over those lower in the list.*
| Metadata | Source | Key |
|:------------:|:--------------:|:--------------------:|
| Method | [Header] | `X-Original-Method` |
| Scheme | [Header] | `X-Original-URL` |
| Hostname | [Header] | `X-Original-URL` |
| Path | [Header] | `X-Original-URL` |
| Method | [Header] | `X-Forwarded-Method` |
| Scheme | [Header] | [X-Forwarded-Proto] |
| Hostname | [Header] | [X-Forwarded-Host] |
| Path | [Header] | `X-Forwarded-URI` |
| IP | [Header] | [X-Forwarded-For] |
| Authelia URL | Query Argument | `rd` |
| Authelia URL | [Header] | `X-Authelia-URL` |
## Authn Strategies
Authentication strategies are used to determine the users identity which is essential to determining if they are
authorized to visit a particular resource. Authentication strategies are executed in order, and have three potential
results.
1. Successful Authentication
2. No Authentication
3. Unsuccessful Authentication
Result 2 is the only result in which the next strategy is attempted, this occurs when there is not enough
information in the request to perform authentication. Both result 1 and 2 result in a short-circuit, i.e. no other
strategy will be attempted.
Result 1 occurs when the strategy requirements (i.e. a particular header) are present and the details are sufficient to
authenticate them and the details are correct. Result 2 occurs when the strategy requirements are present and either the
details are incomplete (i.e. malformed header) or the details are incorrect (i.e. bad password).
### CookieSession
This strategy uses a cookie which links the user to a session to determine the users identity. This is the default
strategy for end-users.
If this strategy if included in an endpoint will redirect the user the Authelia Authorization Portal on supported
proxies when they are not authorized and can potentially be authorized provided no other strategies have critical
errors.
### HeaderAuthorization
This strategy uses the [Authorization] header to determine the users' identity. If the user credentials are wrong, or
the header is malformed it will respond with the [WWW-Authenticate] header and a [401 Unauthorized] status code.
### HeaderProxyAuthorization
This strategy uses the [Proxy-Authorization] header to determine the users' identity. If the user credentials are wrong,
or the header is malformed it will respond with the [Proxy-Authenticate] header and a
[407 Proxy Authentication Required] status code.
### HeaderAuthRequestProxyAuthorization
This strategy uses the [Proxy-Authorization] header to determine the users' identity. If the user credentials are wrong,
or the header is malformed it will respond with the [WWW-Authenticate] header and a [401 Unauthorized] status code. It
is specifically intended for use with the [AuthRequest] implementation.
### HeaderLegacy
This strategy uses the [Proxy-Authorization] header to determine the users' identity. If the user credentials are wrong,
or the header is malformed it will respond with the [WWW-Authenticate] header.
[401 Unauthorized]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401
[407 Proxy Authentication Required]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/407
[NGINX]: https://www.nginx.com/
[Traefik]: https://traefik.io/traefik/
[Envoy]: https://www.envoyproxy.io/
[Caddy]: https://caddyserver.com/
[Skipper]: https://opensource.zalando.com/skipper/
[HAProxy]: http://www.haproxy.org/
[ExtAuthz Extension Filter]: https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/http/ext_authz/v3/ext_authz.proto#envoy-v3-api-msg-extensions-filters-http-ext-authz-v3-extauthz
[auth_request HTTP module]: https://nginx.org/en/docs/http/ngx_http_auth_request_module.html
[auth-request lua plugin]: https://github.com/TimWolla/haproxy-auth-request
[ForwardAuth Middleware]: https://doc.traefik.io/traefik/middlewares/http/forwardauth/
[forward_auth directive]: https://caddyserver.com/docs/caddyfile/directives/forward_auth
[webhook auth filter]: https://opensource.zalando.com/skipper/reference/filters/#webhook
[Implementation]: #implementations
[Authn Strategies]: #authn-strategies
[ForwardAuth]: #forwardauth
[ExtAuthz]: #extauthz
[AuthRequest]: #authrequest
[Legacy]: #legacy
[HeaderProxyAuthorization]: #headerproxyauthorization
[HeaderAuthRequestProxyAuthorization]: #headerauthrequestproxyauthorization
[HeaderLegacy]: #headerlegacy
[CookieSession]: #cookiesession
[Authorization]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization
[WWW-Authenticate]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/WWW-Authenticate
[Proxy-Authorization]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Proxy-Authorization
[Proxy-Authenticate]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Proxy-Authenticate
[X-Forwarded-Proto]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto
[X-Forwarded-Host]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Host
[X-Forwarded-For]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For
[Host]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Host
[HTTP Method]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods
[HTTP Method]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods
[Start Line]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages#start_line
[Header]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers

View File

@ -43,7 +43,7 @@ has many drawbacks that just are not satisfactory in order to easily facilitate
### Initial Implementation
{{< roadmap-status stage="waiting" >}}
{{< roadmap-status stage="in-progress" version="v4.38.0" >}}
This stage is waiting on the choice to handle sessions. Initial implementation will involve just a basic cookie
implementation where users will be required to sign in to each root domain and no SSO functionality will exist.

File diff suppressed because one or more lines are too long

View File

@ -19,7 +19,7 @@ services:
- 'traefik.http.routers.authelia.entrypoints=https'
- 'traefik.http.routers.authelia.tls=true'
- 'traefik.http.routers.authelia.tls.certresolver=letsencrypt'
- 'traefik.http.middlewares.authelia.forwardauth.address=http://authelia:9091/api/verify?rd=https://authelia.example.com' # yamllint disable-line rule:line-length
- 'traefik.http.middlewares.authelia.forwardauth.address=http://authelia:9091/api/authz/forward-auth?authelia_url=https://authelia.example.com' # yamllint disable-line rule:line-length
- 'traefik.http.middlewares.authelia.forwardauth.trustForwardHeader=true'
- 'traefik.http.middlewares.authelia.forwardauth.authResponseHeaders=Remote-User,Remote-Groups,Remote-Name,Remote-Email' # yamllint disable-line rule:line-length
expose:
@ -61,8 +61,8 @@ services:
- 'traefik.http.routers.api.tls.certresolver=letsencrypt'
- 'traefik.http.routers.api.middlewares=authelia@docker'
ports:
- 80:80
- 443:443
- '80:80'
- '443:443'
command:
- '--api'
- '--providers.docker=true'

View File

@ -19,7 +19,7 @@ services:
- 'traefik.http.routers.authelia.entrypoints=https'
- 'traefik.http.routers.authelia.tls=true'
- 'traefik.http.routers.authelia.tls.options=default'
- 'traefik.http.middlewares.authelia.forwardauth.address=http://authelia:9091/api/verify?rd=https://authelia.example.com' # yamllint disable-line rule:line-length
- 'traefik.http.middlewares.authelia.forwardauth.address=http://authelia:9091/api/authz/forward-auth?authelia-url=https://authelia.example.com' # yamllint disable-line rule:line-length
- 'traefik.http.middlewares.authelia.forwardauth.trustForwardHeader=true'
- 'traefik.http.middlewares.authelia.forwardauth.authResponseHeaders=Remote-User,Remote-Groups,Remote-Name,Remote-Email' # yamllint disable-line rule:line-length
expose:
@ -48,8 +48,8 @@ services:
- 'traefik.http.routers.api.tls.options=default'
- 'traefik.http.routers.api.middlewares=authelia@docker'
ports:
- 80:80
- 443:443
- '80:80'
- '443:443'
command:
- '--api'
- '--providers.docker=true'

View File

@ -10,8 +10,10 @@ type Level int
const (
// NotAuthenticated if the user is not authenticated yet.
NotAuthenticated Level = iota
// OneFactor if the user has passed first factor only.
OneFactor
// TwoFactor if the user has passed two factors.
TwoFactor
)

View File

@ -53,12 +53,12 @@ func NewAuthorizer(config *schema.Configuration) (authorizer *Authorizer) {
}
// IsSecondFactorEnabled return true if at least one policy is set to second factor.
func (p Authorizer) IsSecondFactorEnabled() bool {
func (p *Authorizer) IsSecondFactorEnabled() bool {
return p.mfa
}
// GetRequiredLevel retrieve the required level of authorization to access the object.
func (p Authorizer) GetRequiredLevel(subject Subject, object Object) (hasSubjects bool, level Level) {
func (p *Authorizer) GetRequiredLevel(subject Subject, object Object) (hasSubjects bool, level Level) {
p.log.Debugf("Check authorization of subject %s and object %s (method %s).",
subject.String(), object.String(), object.Method)
@ -78,7 +78,7 @@ func (p Authorizer) GetRequiredLevel(subject Subject, object Object) (hasSubject
}
// GetRuleMatchResults iterates through the rules and produces a list of RuleMatchResult provided a subject and object.
func (p Authorizer) GetRuleMatchResults(subject Subject, object Object) (results []RuleMatchResult) {
func (p *Authorizer) GetRuleMatchResults(subject Subject, object Object) (results []RuleMatchResult) {
skipped := false
results = make([]RuleMatchResult, len(p.rules))

View File

@ -6,10 +6,13 @@ type Level int
const (
// Bypass bypass level.
Bypass Level = iota
// OneFactor one factor level.
OneFactor
// TwoFactor two factor level.
TwoFactor
// Denied denied level.
Denied
)

View File

@ -117,7 +117,7 @@ func runServices(ctx *CmdCtx) {
ctx.group.Go(func() (err error) {
defer func() {
if r := recover(); r != nil {
ctx.log.WithError(recoverErr(r)).Errorf("Critical error in server caught (recovered)")
ctx.log.WithError(recoverErr(r)).Errorf("Server (main) critical error caught (recovered)")
}
}()
@ -143,7 +143,7 @@ func runServices(ctx *CmdCtx) {
defer func() {
if r := recover(); r != nil {
ctx.log.WithError(recoverErr(r)).Errorf("Critical error in metrics server caught (recovered)")
ctx.log.WithError(recoverErr(r)).Errorf("Server (metrics) critical error caught (recovered)")
}
}()
@ -165,11 +165,11 @@ func runServices(ctx *CmdCtx) {
if ctx.config.AuthenticationBackend.File != nil && ctx.config.AuthenticationBackend.File.Watch {
provider := ctx.providers.UserProvider.(*authentication.FileUserProvider)
if watcher, err := runServiceFileWatcher(ctx, ctx.config.AuthenticationBackend.File.Path, provider); err != nil {
ctx.log.WithError(err).Errorf("Error opening file watcher")
ctx.log.WithError(err).Errorf("File Watcher (user database) start returned error")
} else {
defer func(watcher *fsnotify.Watcher) {
if err := watcher.Close(); err != nil {
ctx.log.WithError(err).Errorf("Error closing file watcher")
ctx.log.WithError(err).Errorf("File Watcher (user database) close returned error")
}
}(watcher)
}

View File

@ -51,12 +51,6 @@ server:
## Useful to allow overriding of specific static assets.
# asset_path: /config/assets/
## Enables the pprof endpoint.
enable_pprof: false
## Enables the expvars endpoint.
enable_expvars: false
## Disables writing the health check vars to /app/.healthcheck.env which makes healthcheck.sh return exit code 0.
## This is disabled by default if either /app/.healthcheck.env or /app/healthcheck.sh do not exist.
disable_healthcheck: false
@ -104,6 +98,30 @@ server:
## Idle timeout.
# idle: 30s
## Server Endpoints configuration.
## This section is considered advanced and it SHOULD NOT be configured unless you've read the relevant documentation.
# endpoints:
## Enables the pprof endpoint.
# enable_pprof: false
## Enables the expvars endpoint.
# enable_expvars: false
## Configure the authz endpoints.
# authz:
# forward-auth:
# implementation: ForwardAuth
# authn_strategies: []
# ext-authz:
# implementation: ExtAuthz
# authn_strategies: []
# auth-request:
# implementation: AuthRequest
# authn_strategies: []
# legacy:
# implementation: Legacy
# authn_strategies: []
##
## Log Configuration
##
@ -505,7 +523,6 @@ authentication_backend:
# variant: standard
# cost: 12
##
## Password Policy Configuration.
##
@ -540,6 +557,23 @@ password_policy:
## Configures the minimum score allowed.
min_score: 3
##
## Privacy Policy Configuration
##
## Parameters used for displaying the privacy policy link and drawer.
privacy_policy:
## Enables the display of the privacy policy using the policy_url.
enabled: false
## Enables the display of the privacy policy drawer which requires users accept the privacy policy
## on a per-browser basis.
require_user_acceptance: false
## The URL of the privacy policy document. Must be an absolute URL and must have the 'https://' scheme.
## If the privacy policy enabled option is true, this MUST be provided.
policy_url: ''
##
## Access Control Configuration
##

View File

@ -141,4 +141,18 @@ var deprecations = map[string]Deprecation{
AutoMap: true,
MapFunc: nil,
},
"server.enable_pprof": {
Version: model.SemanticVersion{Major: 4, Minor: 38},
Key: "server.enable_pprof",
NewKey: "server.endpoints.enable_pprof",
AutoMap: true,
MapFunc: nil,
},
"server.enable_expvars": {
Version: model.SemanticVersion{Major: 4, Minor: 38},
Key: "server.enable_expvars",
NewKey: "server.endpoints.enable_expvars",
AutoMap: true,
MapFunc: nil,
},
}

View File

@ -23,4 +23,5 @@ type Configuration struct {
Telemetry TelemetryConfig `koanf:"telemetry"`
Webauthn WebauthnConfiguration `koanf:"webauthn"`
PasswordPolicy PasswordPolicyConfiguration `koanf:"password_policy"`
PrivacyPolicy PrivacyPolicy `koanf:"privacy_policy"`
}

View File

@ -235,13 +235,17 @@ var Keys = []string{
"server.port",
"server.path",
"server.asset_path",
"server.enable_pprof",
"server.enable_expvars",
"server.disable_healthcheck",
"server.tls.certificate",
"server.tls.key",
"server.tls.client_certificates",
"server.headers.csp_template",
"server.endpoints.enable_pprof",
"server.endpoints.enable_expvars",
"server.endpoints.authz",
"server.endpoints.authz.*.implementation",
"server.endpoints.authz.*.authn_strategies",
"server.endpoints.authz.*.authn_strategies[].name",
"server.buffers.read",
"server.buffers.write",
"server.timeouts.read",
@ -268,4 +272,7 @@ var Keys = []string{
"password_policy.standard.require_special",
"password_policy.zxcvbn.enabled",
"password_policy.zxcvbn.min_score",
"privacy_policy.enabled",
"privacy_policy.require_user_acceptance",
"privacy_policy.policy_url",
}

View File

@ -0,0 +1,12 @@
package schema
import (
"net/url"
)
// PrivacyPolicy is the privacy policy configuration.
type PrivacyPolicy struct {
Enabled bool `koanf:"enabled"`
RequireUserAcceptance bool `koanf:"require_user_acceptance"`
PolicyURL *url.URL `koanf:"policy_url"`
}

View File

@ -10,26 +10,45 @@ type ServerConfiguration struct {
Port int `koanf:"port"`
Path string `koanf:"path"`
AssetPath string `koanf:"asset_path"`
EnablePprof bool `koanf:"enable_pprof"`
EnableExpvars bool `koanf:"enable_expvars"`
DisableHealthcheck bool `koanf:"disable_healthcheck"`
TLS ServerTLSConfiguration `koanf:"tls"`
Headers ServerHeadersConfiguration `koanf:"headers"`
TLS ServerTLS `koanf:"tls"`
Headers ServerHeaders `koanf:"headers"`
Endpoints ServerEndpoints `koanf:"endpoints"`
Buffers ServerBuffers `koanf:"buffers"`
Timeouts ServerTimeouts `koanf:"timeouts"`
}
// ServerTLSConfiguration represents the configuration of the http servers TLS options.
type ServerTLSConfiguration struct {
// ServerEndpoints is the endpoints configuration for the HTTP server.
type ServerEndpoints struct {
EnablePprof bool `koanf:"enable_pprof"`
EnableExpvars bool `koanf:"enable_expvars"`
Authz map[string]ServerAuthzEndpoint `koanf:"authz"`
}
// ServerAuthzEndpoint is the Authz endpoints configuration for the HTTP server.
type ServerAuthzEndpoint struct {
Implementation string `koanf:"implementation"`
AuthnStrategies []ServerAuthzEndpointAuthnStrategy `koanf:"authn_strategies"`
}
// ServerAuthzEndpointAuthnStrategy is the Authz endpoints configuration for the HTTP server.
type ServerAuthzEndpointAuthnStrategy struct {
Name string `koanf:"name"`
}
// ServerTLS represents the configuration of the http servers TLS options.
type ServerTLS struct {
Certificate string `koanf:"certificate"`
Key string `koanf:"key"`
ClientCertificates []string `koanf:"client_certificates"`
}
// ServerHeadersConfiguration represents the customization of the http server headers.
type ServerHeadersConfiguration struct {
// ServerHeaders represents the customization of the http server headers.
type ServerHeaders struct {
CSPTemplate string `koanf:"csp_template"`
}
@ -46,4 +65,44 @@ var DefaultServerConfiguration = ServerConfiguration{
Write: time.Second * 6,
Idle: time.Second * 30,
},
Endpoints: ServerEndpoints{
Authz: map[string]ServerAuthzEndpoint{
"legacy": {
Implementation: "Legacy",
},
"auth-request": {
Implementation: "AuthRequest",
AuthnStrategies: []ServerAuthzEndpointAuthnStrategy{
{
Name: "HeaderAuthRequestProxyAuthorization",
},
{
Name: "CookieSession",
},
},
},
"forward-auth": {
Implementation: "ForwardAuth",
AuthnStrategies: []ServerAuthzEndpointAuthnStrategy{
{
Name: "HeaderProxyAuthorization",
},
{
Name: "CookieSession",
},
},
},
"ext-authz": {
Implementation: "ExtAuthz",
AuthnStrategies: []ServerAuthzEndpointAuthnStrategy{
{
Name: "HeaderProxyAuthorization",
},
{
Name: "CookieSession",
},
},
},
},
},
}

View File

@ -4,6 +4,25 @@ default_redirection_url: https://home.example.com:8080/
server:
host: 127.0.0.1
port: 9091
endpoints:
authz:
forward-auth:
implementation: ForwardAuth
authn_strategies:
- name: HeaderProxyAuthorization
- name: CookieSession
ext-authz:
implementation: ExtAuthz
authn_strategies:
- name: HeaderProxyAuthorization
- name: CookieSession
auth-request:
implementation: AuthRequest
authn_strategies:
- name: HeaderAuthRequestProxyAuthorization
- name: CookieSession
legacy:
implementation: Legacy
log:
level: debug

View File

@ -68,6 +68,8 @@ func ValidateConfiguration(config *schema.Configuration, validator *schema.Struc
ValidateNTP(config, validator)
ValidatePasswordPolicy(&config.PasswordPolicy, validator)
ValidatePrivacyPolicy(&config.PrivacyPolicy, validator)
}
func validateDefault2FAMethod(config *schema.Configuration, validator *schema.StructValidator) {

View File

@ -286,6 +286,14 @@ const (
errFmtServerPathNoForwardSlashes = "server: option 'path' must not contain any forward slashes"
errFmtServerPathAlphaNum = "server: option 'path' must only contain alpha numeric characters"
errFmtServerEndpointsAuthzImplementation = "server: endpoints: authz: %s: option 'implementation' must be one of '%s' but is configured as '%s'"
errFmtServerEndpointsAuthzStrategy = "server: endpoints: authz: %s: authn_strategies: option 'name' must be one of '%s' but is configured as '%s'"
errFmtServerEndpointsAuthzStrategyDuplicate = "server: endpoints: authz: %s: authn_strategies: duplicate strategy name detected with name '%s'"
errFmtServerEndpointsAuthzPrefixDuplicate = "server: endpoints: authz: %s: endpoint starts with the same prefix as the '%s' endpoint with the '%s' implementation which accepts prefixes as part of its implementation"
errFmtServerEndpointsAuthzInvalidName = "server: endpoints: authz: %s: contains invalid characters"
errFmtServerEndpointsAuthzLegacyInvalidImplementation = "server: endpoints: authz: %s: option 'implementation' is invalid: the endpoint with the name 'legacy' must use the 'Legacy' implementation"
)
const (
@ -294,22 +302,17 @@ const (
errFmtPasswordPolicyZXCVBNMinScoreInvalid = "password_policy: zxcvbn: option 'min_score' is invalid: must be between 1 and 4 but it's configured as %d"
)
const (
errPrivacyPolicyEnabledWithoutURL = "privacy_policy: option 'policy_url' must be provided when the option 'enabled' is true"
errFmtPrivacyPolicyURLNotHTTPS = "privacy_policy: option 'policy_url' must have the 'https' scheme but it's configured as '%s'"
)
const (
errFmtDuoMissingOption = "duo_api: option '%s' is required when duo is enabled but it is missing"
)
// Error constants.
const (
/*
errFmtDeprecatedConfigurationKey = "the %s configuration option is deprecated and will be " +
"removed in %s, please use %s instead"
Uncomment for use when deprecating keys.
TODO: Create a method from within Koanf to automatically remap deprecated keys and produce warnings.
TODO (cont): The main consideration is making sure we do not overwrite the destination key name if it already exists.
*/
errFmtInvalidDefault2FAMethod = "option 'default_2fa_method' is configured as '%s' but must be one of " +
"the following values: '%s'"
errFmtInvalidDefault2FAMethodDisabled = "option 'default_2fa_method' is configured as '%s' " +
@ -337,6 +340,17 @@ var (
validLDAPImplementations = []string{schema.LDAPImplementationCustom, schema.LDAPImplementationActiveDirectory, schema.LDAPImplementationFreeIPA, schema.LDAPImplementationLLDAP}
)
const (
legacy = "legacy"
authzImplementationLegacy = "Legacy"
authzImplementationExtAuthz = "ExtAuthz"
)
var (
validAuthzImplementations = []string{"AuthRequest", "ForwardAuth", authzImplementationExtAuthz, authzImplementationLegacy}
validAuthzAuthnStrategies = []string{"CookieSession", "HeaderAuthorization", "HeaderProxyAuthorization", "HeaderAuthRequestProxyAuthorization", "HeaderLegacy"}
)
var (
validArgon2Variants = []string{"argon2id", "id", "argon2i", "i", "argon2d", "d"}
validSHA2CryptVariants = []string{digestSHA256, digestSHA512}
@ -374,8 +388,9 @@ var (
)
var (
reKeyReplacer = regexp.MustCompile(`\[\d+]`)
reDomainCharacters = regexp.MustCompile(`^[a-z0-9-]+(\.[a-z0-9-]+)+[a-z0-9]$`)
reKeyReplacer = regexp.MustCompile(`\[\d+]`)
reDomainCharacters = regexp.MustCompile(`^[a-z0-9-]+(\.[a-z0-9-]+)+[a-z0-9]$`)
reAuthzEndpointName = regexp.MustCompile(`^[a-zA-Z](([a-zA-Z0-9/\._-]*)([a-zA-Z]))?$`)
)
var replacedKeys = map[string]string{

View File

@ -3,6 +3,7 @@ package validator
import (
"errors"
"fmt"
"regexp"
"strings"
"github.com/authelia/authelia/v4/internal/configuration/schema"
@ -13,6 +14,20 @@ import (
func ValidateKeys(keys []string, prefix string, validator *schema.StructValidator) {
var errStrings []string
var patterns []*regexp.Regexp
for _, key := range schema.Keys {
pattern, _ := NewKeyPattern(key)
switch {
case pattern == nil:
continue
default:
patterns = append(patterns, pattern)
}
}
KEYS:
for _, key := range keys {
expectedKey := reKeyReplacer.ReplaceAllString(key, "[]")
@ -25,6 +40,12 @@ func ValidateKeys(keys []string, prefix string, validator *schema.StructValidato
continue
}
for _, p := range patterns {
if p.MatchString(expectedKey) {
continue KEYS
}
}
if err, ok := specificErrorKeys[expectedKey]; ok {
if !utils.IsStringInSlice(err, errStrings) {
errStrings = append(errStrings, err)
@ -42,3 +63,48 @@ func ValidateKeys(keys []string, prefix string, validator *schema.StructValidato
validator.Push(errors.New(err))
}
}
// NewKeyPattern returns patterns which are required to match key patterns.
func NewKeyPattern(key string) (pattern *regexp.Regexp, err error) {
switch {
case strings.Contains(key, ".*."):
return NewKeyMapPattern(key)
default:
return nil, nil
}
}
// NewKeyMapPattern returns a pattern required to match map keys.
func NewKeyMapPattern(key string) (pattern *regexp.Regexp, err error) {
parts := strings.Split(key, ".*.")
buf := &strings.Builder{}
buf.WriteString("^")
n := len(parts) - 1
for i, part := range parts {
if i != 0 {
buf.WriteString("\\.")
}
for _, r := range part {
switch r {
case '[', ']', '.', '{', '}':
buf.WriteRune('\\')
fallthrough
default:
buf.WriteRune(r)
}
}
if i < n {
buf.WriteString("\\.[a-z0-9]([a-z0-9-_]+)?[a-z0-9]")
}
}
buf.WriteString("$")
return regexp.Compile(buf.String())
}

View File

@ -7,7 +7,7 @@ import (
"github.com/authelia/authelia/v4/internal/utils"
)
// ValidatePasswordPolicy validates and update Password Policy configuration.
// ValidatePasswordPolicy validates and updates the Password Policy configuration.
func ValidatePasswordPolicy(config *schema.PasswordPolicyConfiguration, validator *schema.StructValidator) {
if !utils.IsBoolCountLessThanN(1, true, config.Standard.Enabled, config.ZXCVBN.Enabled) {
validator.Push(fmt.Errorf(errPasswordPolicyMultipleDefined))

View File

@ -0,0 +1,23 @@
package validator
import (
"fmt"
"github.com/authelia/authelia/v4/internal/configuration/schema"
)
// ValidatePrivacyPolicy validates and updates the Privacy Policy configuration.
func ValidatePrivacyPolicy(config *schema.PrivacyPolicy, validator *schema.StructValidator) {
if !config.Enabled {
return
}
switch config.PolicyURL {
case nil:
validator.Push(fmt.Errorf(errPrivacyPolicyEnabledWithoutURL))
default:
if config.PolicyURL.Scheme != schemeHTTPS {
validator.Push(fmt.Errorf(errFmtPrivacyPolicyURLNotHTTPS, config.PolicyURL.Scheme))
}
}
}

View File

@ -0,0 +1,41 @@
package validator
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/authelia/authelia/v4/internal/configuration/schema"
)
func TestValidatePrivacyPolicy(t *testing.T) {
testCases := []struct {
name string
have schema.PrivacyPolicy
expected string
}{
{"ShouldValidateDefaultConfig", schema.PrivacyPolicy{}, ""},
{"ShouldValidateValidEnabledPolicy", schema.PrivacyPolicy{Enabled: true, PolicyURL: MustParseURL("https://example.com/privacy")}, ""},
{"ShouldValidateValidEnabledPolicyWithUserAcceptance", schema.PrivacyPolicy{Enabled: true, RequireUserAcceptance: true, PolicyURL: MustParseURL("https://example.com/privacy")}, ""},
{"ShouldNotValidateOnInvalidScheme", schema.PrivacyPolicy{Enabled: true, PolicyURL: MustParseURL("http://example.com/privacy")}, "privacy_policy: option 'policy_url' must have the 'https' scheme but it's configured as 'http'"},
{"ShouldNotValidateOnMissingURL", schema.PrivacyPolicy{Enabled: true}, "privacy_policy: option 'policy_url' must be provided when the option 'enabled' is true"},
}
validator := schema.NewStructValidator()
for _, tc := range testCases {
validator.Clear()
t.Run(tc.name, func(t *testing.T) {
ValidatePrivacyPolicy(&tc.have, validator)
assert.Len(t, validator.Warnings(), 0)
if tc.expected == "" {
assert.Len(t, validator.Errors(), 0)
} else {
assert.EqualError(t, validator.Errors()[0], tc.expected)
}
})
}
}

View File

@ -3,6 +3,7 @@ package validator
import (
"fmt"
"path"
"sort"
"strings"
"github.com/authelia/authelia/v4/internal/configuration/schema"
@ -89,4 +90,97 @@ func ValidateServer(config *schema.Configuration, validator *schema.StructValida
if config.Server.Timeouts.Idle <= 0 {
config.Server.Timeouts.Idle = schema.DefaultServerConfiguration.Timeouts.Idle
}
ValidateServerEndpoints(config, validator)
}
// ValidateServerEndpoints configures the default endpoints and checks the configuration of custom endpoints.
func ValidateServerEndpoints(config *schema.Configuration, validator *schema.StructValidator) {
if config.Server.Endpoints.EnableExpvars {
validator.PushWarning(fmt.Errorf("server: endpoints: option 'enable_expvars' should not be enabled in production"))
}
if config.Server.Endpoints.EnablePprof {
validator.PushWarning(fmt.Errorf("server: endpoints: option 'enable_pprof' should not be enabled in production"))
}
if len(config.Server.Endpoints.Authz) == 0 {
config.Server.Endpoints.Authz = schema.DefaultServerConfiguration.Endpoints.Authz
return
}
authzs := make([]string, 0, len(config.Server.Endpoints.Authz))
for name := range config.Server.Endpoints.Authz {
authzs = append(authzs, name)
}
sort.Strings(authzs)
for _, name := range authzs {
endpoint := config.Server.Endpoints.Authz[name]
validateServerEndpointsAuthzEndpoint(config, name, endpoint, validator)
for _, oName := range authzs {
oEndpoint := config.Server.Endpoints.Authz[oName]
if oName == name || oName == legacy {
continue
}
switch oEndpoint.Implementation {
case authzImplementationLegacy, authzImplementationExtAuthz:
if strings.HasPrefix(name, oName+"/") {
validator.Push(fmt.Errorf(errFmtServerEndpointsAuthzPrefixDuplicate, name, oName, oEndpoint.Implementation))
}
default:
continue
}
}
validateServerEndpointsAuthzStrategies(name, endpoint.AuthnStrategies, validator)
}
}
func validateServerEndpointsAuthzEndpoint(config *schema.Configuration, name string, endpoint schema.ServerAuthzEndpoint, validator *schema.StructValidator) {
if name == legacy {
switch endpoint.Implementation {
case authzImplementationLegacy:
break
case "":
endpoint.Implementation = authzImplementationLegacy
config.Server.Endpoints.Authz[name] = endpoint
default:
if !utils.IsStringInSlice(endpoint.Implementation, validAuthzImplementations) {
validator.Push(fmt.Errorf(errFmtServerEndpointsAuthzImplementation, name, strings.Join(validAuthzImplementations, "', '"), endpoint.Implementation))
} else {
validator.Push(fmt.Errorf(errFmtServerEndpointsAuthzLegacyInvalidImplementation, name))
}
}
} else if !utils.IsStringInSlice(endpoint.Implementation, validAuthzImplementations) {
validator.Push(fmt.Errorf(errFmtServerEndpointsAuthzImplementation, name, strings.Join(validAuthzImplementations, "', '"), endpoint.Implementation))
}
if !reAuthzEndpointName.MatchString(name) {
validator.Push(fmt.Errorf(errFmtServerEndpointsAuthzInvalidName, name))
}
}
func validateServerEndpointsAuthzStrategies(name string, strategies []schema.ServerAuthzEndpointAuthnStrategy, validator *schema.StructValidator) {
names := make([]string, len(strategies))
for _, strategy := range strategies {
if utils.IsStringInSlice(strategy.Name, names) {
validator.Push(fmt.Errorf(errFmtServerEndpointsAuthzStrategyDuplicate, name, strategy.Name))
}
names = append(names, strategy.Name)
if !utils.IsStringInSlice(strategy.Name, validAuthzAuthnStrategies) {
validator.Push(fmt.Errorf(errFmtServerEndpointsAuthzStrategy, name, strings.Join(validAuthzAuthnStrategies, "', '"), strategy.Name))
}
}
}

View File

@ -29,8 +29,9 @@ func TestShouldSetDefaultServerValues(t *testing.T) {
assert.Equal(t, schema.DefaultServerConfiguration.TLS.Key, config.Server.TLS.Key)
assert.Equal(t, schema.DefaultServerConfiguration.TLS.Certificate, config.Server.TLS.Certificate)
assert.Equal(t, schema.DefaultServerConfiguration.Path, config.Server.Path)
assert.Equal(t, schema.DefaultServerConfiguration.EnableExpvars, config.Server.EnableExpvars)
assert.Equal(t, schema.DefaultServerConfiguration.EnablePprof, config.Server.EnablePprof)
assert.Equal(t, schema.DefaultServerConfiguration.Endpoints.EnableExpvars, config.Server.Endpoints.EnableExpvars)
assert.Equal(t, schema.DefaultServerConfiguration.Endpoints.EnablePprof, config.Server.Endpoints.EnablePprof)
assert.Equal(t, schema.DefaultServerConfiguration.Endpoints.Authz, config.Server.Endpoints.Authz)
}
func TestShouldSetDefaultConfig(t *testing.T) {
@ -278,3 +279,165 @@ func TestShouldValidateAndUpdatePort(t *testing.T) {
require.Len(t, validator.Errors(), 0)
assert.Equal(t, 9091, config.Server.Port)
}
func TestServerEndpointsDevelShouldWarn(t *testing.T) {
config := &schema.Configuration{
Server: schema.ServerConfiguration{
Endpoints: schema.ServerEndpoints{
EnablePprof: true,
EnableExpvars: true,
},
},
}
validator := schema.NewStructValidator()
ValidateServer(config, validator)
require.Len(t, validator.Warnings(), 2)
assert.Len(t, validator.Errors(), 0)
assert.EqualError(t, validator.Warnings()[0], "server: endpoints: option 'enable_expvars' should not be enabled in production")
assert.EqualError(t, validator.Warnings()[1], "server: endpoints: option 'enable_pprof' should not be enabled in production")
}
func TestServerAuthzEndpointErrors(t *testing.T) {
testCases := []struct {
name string
have map[string]schema.ServerAuthzEndpoint
errs []string
}{
{"ShouldAllowDefaultEndpoints", schema.DefaultServerConfiguration.Endpoints.Authz, nil},
{"ShouldAllowSetDefaultEndpoints", nil, nil},
{
"ShouldErrorOnInvalidEndpointImplementations",
map[string]schema.ServerAuthzEndpoint{
"example": {Implementation: "zero"},
},
[]string{"server: endpoints: authz: example: option 'implementation' must be one of 'AuthRequest', 'ForwardAuth', 'ExtAuthz', 'Legacy' but is configured as 'zero'"},
},
{
"ShouldErrorOnInvalidEndpointImplementationLegacy",
map[string]schema.ServerAuthzEndpoint{
"legacy": {Implementation: "zero"},
},
[]string{"server: endpoints: authz: legacy: option 'implementation' must be one of 'AuthRequest', 'ForwardAuth', 'ExtAuthz', 'Legacy' but is configured as 'zero'"},
},
{
"ShouldErrorOnInvalidEndpointLegacyImplementation",
map[string]schema.ServerAuthzEndpoint{
"legacy": {Implementation: "ExtAuthz"},
},
[]string{"server: endpoints: authz: legacy: option 'implementation' is invalid: the endpoint with the name 'legacy' must use the 'Legacy' implementation"},
},
{
"ShouldErrorOnInvalidAuthnStrategies",
map[string]schema.ServerAuthzEndpoint{
"example": {Implementation: "ExtAuthz", AuthnStrategies: []schema.ServerAuthzEndpointAuthnStrategy{{Name: "bad-name"}}},
},
[]string{"server: endpoints: authz: example: authn_strategies: option 'name' must be one of 'CookieSession', 'HeaderAuthorization', 'HeaderProxyAuthorization', 'HeaderAuthRequestProxyAuthorization', 'HeaderLegacy' but is configured as 'bad-name'"},
},
{
"ShouldErrorOnDuplicateName",
map[string]schema.ServerAuthzEndpoint{
"example": {Implementation: "ExtAuthz", AuthnStrategies: []schema.ServerAuthzEndpointAuthnStrategy{{Name: "CookieSession"}, {Name: "CookieSession"}}},
},
[]string{"server: endpoints: authz: example: authn_strategies: duplicate strategy name detected with name 'CookieSession'"},
},
{
"ShouldErrorOnInvalidChars",
map[string]schema.ServerAuthzEndpoint{
"/abc": {Implementation: "ForwardAuth"},
"/abc/": {Implementation: "ForwardAuth"},
"abc/": {Implementation: "ForwardAuth"},
"1abc": {Implementation: "ForwardAuth"},
"1abc1": {Implementation: "ForwardAuth"},
"abc1": {Implementation: "ForwardAuth"},
"-abc": {Implementation: "ForwardAuth"},
"-abc-": {Implementation: "ForwardAuth"},
"abc-": {Implementation: "ForwardAuth"},
},
[]string{
"server: endpoints: authz: -abc: contains invalid characters",
"server: endpoints: authz: -abc-: contains invalid characters",
"server: endpoints: authz: /abc: contains invalid characters",
"server: endpoints: authz: /abc/: contains invalid characters",
"server: endpoints: authz: 1abc: contains invalid characters",
"server: endpoints: authz: 1abc1: contains invalid characters",
"server: endpoints: authz: abc-: contains invalid characters",
"server: endpoints: authz: abc/: contains invalid characters",
"server: endpoints: authz: abc1: contains invalid characters",
},
},
{
"ShouldErrorOnEndpointsWithDuplicatePrefix",
map[string]schema.ServerAuthzEndpoint{
"apple": {Implementation: "ForwardAuth"},
"apple/abc": {Implementation: "ForwardAuth"},
"pear/abc": {Implementation: "ExtAuthz"},
"pear": {Implementation: "ExtAuthz"},
"another": {Implementation: "ExtAuthz"},
"another/test": {Implementation: "ForwardAuth"},
"anotherb/test": {Implementation: "ForwardAuth"},
"anothe": {Implementation: "ExtAuthz"},
"anotherc/test": {Implementation: "ForwardAuth"},
"anotherc": {Implementation: "ExtAuthz"},
"anotherd/test": {Implementation: "ForwardAuth"},
"anotherd": {Implementation: "Legacy"},
"anothere/test": {Implementation: "ExtAuthz"},
"anothere": {Implementation: "ExtAuthz"},
},
[]string{
"server: endpoints: authz: another/test: endpoint starts with the same prefix as the 'another' endpoint with the 'ExtAuthz' implementation which accepts prefixes as part of its implementation",
"server: endpoints: authz: anotherc/test: endpoint starts with the same prefix as the 'anotherc' endpoint with the 'ExtAuthz' implementation which accepts prefixes as part of its implementation",
"server: endpoints: authz: anotherd/test: endpoint starts with the same prefix as the 'anotherd' endpoint with the 'Legacy' implementation which accepts prefixes as part of its implementation",
"server: endpoints: authz: anothere/test: endpoint starts with the same prefix as the 'anothere' endpoint with the 'ExtAuthz' implementation which accepts prefixes as part of its implementation",
"server: endpoints: authz: pear/abc: endpoint starts with the same prefix as the 'pear' endpoint with the 'ExtAuthz' implementation which accepts prefixes as part of its implementation",
},
},
}
validator := schema.NewStructValidator()
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
validator.Clear()
config := newDefaultConfig()
config.Server.Endpoints.Authz = tc.have
ValidateServerEndpoints(&config, validator)
if tc.errs == nil {
assert.Len(t, validator.Warnings(), 0)
assert.Len(t, validator.Errors(), 0)
} else {
require.Len(t, validator.Errors(), len(tc.errs))
for i, expected := range tc.errs {
assert.EqualError(t, validator.Errors()[i], expected)
}
}
})
}
}
func TestServerAuthzEndpointLegacyAsImplementationLegacyWhenBlank(t *testing.T) {
have := map[string]schema.ServerAuthzEndpoint{
"legacy": {},
}
config := newDefaultConfig()
config.Server.Endpoints.Authz = have
validator := schema.NewStructValidator()
ValidateServerEndpoints(&config, validator)
assert.Len(t, validator.Warnings(), 0)
assert.Len(t, validator.Errors(), 0)
assert.Equal(t, authzImplementationLegacy, config.Server.Endpoints.Authz[legacy].Implementation)
}

View File

@ -7,6 +7,7 @@ import (
duoapi "github.com/duosecurity/duo_api_golang"
"github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/session"
)
// NewDuoAPI create duo API instance.
@ -16,8 +17,8 @@ func NewDuoAPI(duoAPI *duoapi.DuoApi) *APIImpl {
}
}
// Call call to the DuoAPI.
func (d *APIImpl) Call(ctx *middlewares.AutheliaCtx, values url.Values, method string, path string) (*Response, error) {
// Call performs a request to the DuoAPI.
func (d *APIImpl) Call(ctx *middlewares.AutheliaCtx, userSession *session.UserSession, values url.Values, method string, path string) (*Response, error) {
var response Response
_, responseBytes, err := d.DuoApi.SignedCall(method, path, values)
@ -25,7 +26,7 @@ func (d *APIImpl) Call(ctx *middlewares.AutheliaCtx, values url.Values, method s
return nil, err
}
ctx.Logger.Tracef("Duo endpoint: %s response raw data for %s from IP %s: %s", path, ctx.GetSession().Username, ctx.RemoteIP().String(), string(responseBytes))
ctx.Logger.Tracef("Duo endpoint: %s response raw data for %s from IP %s: %s", path, userSession.Username, ctx.RemoteIP().String(), string(responseBytes))
err = json.Unmarshal(responseBytes, &response)
if err != nil {
@ -35,18 +36,18 @@ func (d *APIImpl) Call(ctx *middlewares.AutheliaCtx, values url.Values, method s
if response.Stat == "FAIL" {
ctx.Logger.Warnf(
"Duo Push Auth failed to process the auth request for %s from %s: %s (%s), error code %d.",
ctx.GetSession().Username, ctx.RemoteIP().String(),
userSession.Username, ctx.RemoteIP().String(),
response.Message, response.MessageDetail, response.Code)
}
return &response, nil
}
// PreAuthCall call to the DuoAPI.
func (d *APIImpl) PreAuthCall(ctx *middlewares.AutheliaCtx, values url.Values) (*PreAuthResponse, error) {
// PreAuthCall performs a preauth request to the DuoAPI.
func (d *APIImpl) PreAuthCall(ctx *middlewares.AutheliaCtx, userSession *session.UserSession, values url.Values) (*PreAuthResponse, error) {
var preAuthResponse PreAuthResponse
response, err := d.Call(ctx, values, "POST", "/auth/v2/preauth")
response, err := d.Call(ctx, userSession, values, "POST", "/auth/v2/preauth")
if err != nil {
return nil, err
}
@ -59,11 +60,11 @@ func (d *APIImpl) PreAuthCall(ctx *middlewares.AutheliaCtx, values url.Values) (
return &preAuthResponse, nil
}
// AuthCall call to the DuoAPI.
func (d *APIImpl) AuthCall(ctx *middlewares.AutheliaCtx, values url.Values) (*AuthResponse, error) {
// AuthCall performs an auth request to the DuoAPI.
func (d *APIImpl) AuthCall(ctx *middlewares.AutheliaCtx, userSession *session.UserSession, values url.Values) (*AuthResponse, error) {
var authResponse AuthResponse
response, err := d.Call(ctx, values, "POST", "/auth/v2/auth")
response, err := d.Call(ctx, userSession, values, "POST", "/auth/v2/auth")
if err != nil {
return nil, err
}

View File

@ -7,13 +7,14 @@ import (
duoapi "github.com/duosecurity/duo_api_golang"
"github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/session"
)
// API interface wrapping duo api library for testing purpose.
type API interface {
Call(ctx *middlewares.AutheliaCtx, values url.Values, method string, path string) (*Response, error)
PreAuthCall(ctx *middlewares.AutheliaCtx, values url.Values) (*PreAuthResponse, error)
AuthCall(ctx *middlewares.AutheliaCtx, values url.Values) (*AuthResponse, error)
Call(ctx *middlewares.AutheliaCtx, userSession *session.UserSession, values url.Values, method string, path string) (response *Response, err error)
PreAuthCall(ctx *middlewares.AutheliaCtx, userSession *session.UserSession, values url.Values) (response *PreAuthResponse, err error)
AuthCall(ctx *middlewares.AutheliaCtx, userSession *session.UserSession, values url.Values) (response *AuthResponse, err error)
}
// APIImpl implementation of DuoAPI interface.

View File

@ -1,8 +1,6 @@
package handlers
import (
"time"
"github.com/valyala/fasthttp"
)
@ -18,9 +16,22 @@ const (
)
var (
headerAuthorization = []byte(fasthttp.HeaderAuthorization)
headerProxyAuthorization = []byte(fasthttp.HeaderProxyAuthorization)
headerAuthorization = []byte(fasthttp.HeaderAuthorization)
headerWWWAuthenticate = []byte(fasthttp.HeaderWWWAuthenticate)
headerProxyAuthorization = []byte(fasthttp.HeaderProxyAuthorization)
headerProxyAuthenticate = []byte(fasthttp.HeaderProxyAuthenticate)
)
const (
headerAuthorizationSchemeBasic = "basic"
)
var (
headerValueAuthenticateBasic = []byte(`Basic realm="Authorization Required"`)
)
var (
headerSessionUsername = []byte("Session-Username")
headerRemoteUser = []byte("Remote-User")
headerRemoteGroups = []byte("Remote-Groups")
@ -30,7 +41,9 @@ var (
const (
queryArgRD = "rd"
queryArgRM = "rm"
queryArgID = "id"
queryArgAuth = "auth"
queryArgConsentID = "consent_id"
queryArgWorkflow = "workflow"
queryArgWorkflowID = "workflow_id"
@ -38,16 +51,14 @@ const (
var (
qryArgID = []byte(queryArgID)
qryArgRD = []byte(queryArgRD)
qryArgAuth = []byte(queryArgAuth)
qryArgConsentID = []byte(queryArgConsentID)
)
const (
// Forbidden means the user is forbidden the access to a resource.
Forbidden authorizationMatching = iota
// NotAuthorized means the user can access the resource with more permissions.
NotAuthorized authorizationMatching = iota
// Authorized means the user is authorized given her current permissions.
Authorized authorizationMatching = iota
var (
qryValueBasic = []byte("basic")
qryValueEmpty = []byte("")
)
const (
@ -109,13 +120,6 @@ const (
logFmtErrConsentGenerate = logFmtConsentPrefix + "could not be processed: error occurred generating consent: %+v"
)
const (
testInactivity = time.Second * 10
testRedirectionURL = "http://redirection.local"
testUsername = "john"
exampleDotCom = "example.com"
)
// Duo constants.
const (
allow = "allow"
@ -124,8 +128,6 @@ const (
auth = "auth"
)
const authPrefix = "Basic "
const ldapPasswordComplexityCode = "0000052D."
var ldapPasswordComplexityCodes = []string{

View File

@ -0,0 +1,35 @@
package handlers
import (
"time"
"github.com/valyala/fasthttp"
)
var (
testRequestMethods = []string{
fasthttp.MethodOptions, fasthttp.MethodHead, fasthttp.MethodGet,
fasthttp.MethodDelete, fasthttp.MethodPatch, fasthttp.MethodPost,
fasthttp.MethodPut, fasthttp.MethodConnect, fasthttp.MethodTrace,
}
testXHR = map[string]bool{
testWithoutAccept: false,
testWithXHRHeader: true,
}
)
const (
testXOriginalMethod = "X-Original-Method"
testXOriginalUrl = "X-Original-Url"
testBypass = "bypass"
testWithoutAccept = "WithoutAccept"
testWithXHRHeader = "WithXHRHeader"
)
const (
testInactivity = time.Second * 10
testRedirectionURL = "http://redirection.local"
testUsername = "john"
exampleDotCom = "example.com"
)

View File

@ -5,16 +5,16 @@ import (
"github.com/authelia/authelia/v4/internal/duo"
"github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/session"
"github.com/authelia/authelia/v4/internal/utils"
)
// DuoPreAuth helper function for retrieving supported devices and capabilities from duo api.
func DuoPreAuth(ctx *middlewares.AutheliaCtx, duoAPI duo.API) (string, string, []DuoDevice, string, error) {
userSession := ctx.GetSession()
func DuoPreAuth(ctx *middlewares.AutheliaCtx, userSession *session.UserSession, duoAPI duo.API) (result, message string, devices []DuoDevice, enrollURL string, err error) {
values := url.Values{}
values.Set("username", userSession.Username)
preAuthResponse, err := duoAPI.PreAuthCall(ctx, values)
preAuthResponse, err := duoAPI.PreAuthCall(ctx, userSession, values)
if err != nil {
return "", "", nil, "", err
}

View File

@ -0,0 +1,170 @@
package handlers
import (
"fmt"
"net/url"
"github.com/authelia/authelia/v4/internal/authentication"
"github.com/authelia/authelia/v4/internal/authorization"
"github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/session"
"github.com/authelia/authelia/v4/internal/utils"
)
// Handler is the middlewares.RequestHandler for Authz.
func (authz *Authz) Handler(ctx *middlewares.AutheliaCtx) {
var (
object authorization.Object
autheliaURL *url.URL
provider *session.Session
err error
)
if object, err = authz.handleGetObject(ctx); err != nil {
ctx.Logger.Errorf("Error getting original request object: %v", err)
ctx.ReplyUnauthorized()
return
}
if !utils.IsURISecure(object.URL) {
ctx.Logger.Errorf("Target URL '%s' has an insecure scheme '%s', only the 'https' and 'wss' schemes are supported so session cookies can be transmitted securely", object.URL.String(), object.URL.Scheme)
ctx.ReplyUnauthorized()
return
}
if provider, err = ctx.GetSessionProviderByTargetURL(object.URL); err != nil {
ctx.Logger.WithError(err).Errorf("Target URL '%s' does not appear to be configured as a session domain", object.URL.String())
ctx.ReplyUnauthorized()
return
}
if autheliaURL, err = authz.getAutheliaURL(ctx, provider); err != nil {
ctx.Logger.WithError(err).Error("Error occurred trying to determine the URL of the portal")
ctx.ReplyUnauthorized()
return
}
var (
authn Authn
strategy AuthnStrategy
)
if authn, strategy, err = authz.authn(ctx, provider); err != nil {
authn.Object = object
ctx.Logger.WithError(err).Error("Error occurred while attempting to authenticate a request")
switch strategy {
case nil:
ctx.ReplyUnauthorized()
default:
strategy.HandleUnauthorized(ctx, &authn, authz.getRedirectionURL(&object, autheliaURL))
}
return
}
authn.Object = object
authn.Method = friendlyMethod(authn.Object.Method)
ruleHasSubject, required := ctx.Providers.Authorizer.GetRequiredLevel(
authorization.Subject{
Username: authn.Details.Username,
Groups: authn.Details.Groups,
IP: ctx.RemoteIP(),
},
object,
)
switch isAuthzResult(authn.Level, required, ruleHasSubject) {
case AuthzResultForbidden:
ctx.Logger.Infof("Access to '%s' is forbidden to user '%s'", object.URL.String(), authn.Username)
ctx.ReplyForbidden()
case AuthzResultUnauthorized:
var handler HandlerAuthzUnauthorized
if strategy != nil {
handler = strategy.HandleUnauthorized
} else {
handler = authz.handleUnauthorized
}
handler(ctx, &authn, authz.getRedirectionURL(&object, autheliaURL))
case AuthzResultAuthorized:
authz.handleAuthorized(ctx, &authn)
}
}
func (authz *Authz) getAutheliaURL(ctx *middlewares.AutheliaCtx, provider *session.Session) (autheliaURL *url.URL, err error) {
if authz.handleGetAutheliaURL == nil {
return nil, nil
}
if autheliaURL, err = authz.handleGetAutheliaURL(ctx); err != nil {
return nil, err
}
if autheliaURL != nil {
return autheliaURL, nil
}
if provider.Config.AutheliaURL != nil {
if authz.legacy {
return nil, nil
}
return provider.Config.AutheliaURL, nil
}
return nil, fmt.Errorf("authelia url lookup failed")
}
func (authz *Authz) getRedirectionURL(object *authorization.Object, autheliaURL *url.URL) (redirectionURL *url.URL) {
if autheliaURL == nil {
return nil
}
redirectionURL, _ = url.ParseRequestURI(autheliaURL.String())
qry := redirectionURL.Query()
qry.Set(queryArgRD, object.URL.String())
if object.Method != "" {
qry.Set(queryArgRM, object.Method)
}
redirectionURL.RawQuery = qry.Encode()
return redirectionURL
}
func (authz *Authz) authn(ctx *middlewares.AutheliaCtx, provider *session.Session) (authn Authn, strategy AuthnStrategy, err error) {
for _, strategy = range authz.strategies {
if authn, err = strategy.Get(ctx, provider); err != nil {
if strategy.CanHandleUnauthorized() {
return Authn{Type: authn.Type, Level: authentication.NotAuthenticated}, strategy, err
}
return Authn{Type: authn.Type, Level: authentication.NotAuthenticated}, nil, err
}
if authn.Level != authentication.NotAuthenticated {
break
}
}
if strategy.CanHandleUnauthorized() {
return authn, strategy, err
}
return authn, nil, nil
}

View File

@ -0,0 +1,448 @@
package handlers
import (
"bytes"
"encoding/base64"
"errors"
"fmt"
"net/url"
"strings"
"time"
"github.com/sirupsen/logrus"
"github.com/valyala/fasthttp"
"github.com/authelia/authelia/v4/internal/authentication"
"github.com/authelia/authelia/v4/internal/configuration/schema"
"github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/session"
"github.com/authelia/authelia/v4/internal/utils"
)
// NewCookieSessionAuthnStrategy creates a new CookieSessionAuthnStrategy.
func NewCookieSessionAuthnStrategy(refreshInterval time.Duration) *CookieSessionAuthnStrategy {
if refreshInterval < time.Second*0 {
return &CookieSessionAuthnStrategy{}
}
return &CookieSessionAuthnStrategy{
refreshEnabled: true,
refreshInterval: refreshInterval,
}
}
// NewHeaderAuthorizationAuthnStrategy creates a new HeaderAuthnStrategy using the Authorization and WWW-Authenticate
// headers, and the 407 Proxy Auth Required response.
func NewHeaderAuthorizationAuthnStrategy() *HeaderAuthnStrategy {
return &HeaderAuthnStrategy{
authn: AuthnTypeAuthorization,
headerAuthorize: headerAuthorization,
headerAuthenticate: headerWWWAuthenticate,
handleAuthenticate: true,
statusAuthenticate: fasthttp.StatusUnauthorized,
}
}
// NewHeaderProxyAuthorizationAuthnStrategy creates a new HeaderAuthnStrategy using the Proxy-Authorization and
// Proxy-Authenticate headers, and the 407 Proxy Auth Required response.
func NewHeaderProxyAuthorizationAuthnStrategy() *HeaderAuthnStrategy {
return &HeaderAuthnStrategy{
authn: AuthnTypeProxyAuthorization,
headerAuthorize: headerProxyAuthorization,
headerAuthenticate: headerProxyAuthenticate,
handleAuthenticate: true,
statusAuthenticate: fasthttp.StatusProxyAuthRequired,
}
}
// NewHeaderProxyAuthorizationAuthRequestAuthnStrategy creates a new HeaderAuthnStrategy using the Proxy-Authorization
// and WWW-Authenticate headers, and the 401 Proxy Auth Required response. This is a special AuthnStrategy for the
// AuthRequest implementation.
func NewHeaderProxyAuthorizationAuthRequestAuthnStrategy() *HeaderAuthnStrategy {
return &HeaderAuthnStrategy{
authn: AuthnTypeProxyAuthorization,
headerAuthorize: headerProxyAuthorization,
headerAuthenticate: headerWWWAuthenticate,
handleAuthenticate: true,
statusAuthenticate: fasthttp.StatusUnauthorized,
}
}
// NewHeaderLegacyAuthnStrategy creates a new HeaderLegacyAuthnStrategy.
func NewHeaderLegacyAuthnStrategy() *HeaderLegacyAuthnStrategy {
return &HeaderLegacyAuthnStrategy{}
}
// CookieSessionAuthnStrategy is a session cookie AuthnStrategy.
type CookieSessionAuthnStrategy struct {
refreshEnabled bool
refreshInterval time.Duration
}
// Get returns the Authn information for this AuthnStrategy.
func (s *CookieSessionAuthnStrategy) Get(ctx *middlewares.AutheliaCtx, provider *session.Session) (authn Authn, err error) {
authn = Authn{
Type: AuthnTypeCookie,
Level: authentication.NotAuthenticated,
}
var userSession session.UserSession
if userSession, err = provider.GetSession(ctx.RequestCtx); err != nil {
return authn, fmt.Errorf("failed to retrieve user session: %w", err)
}
if userSession.CookieDomain != provider.Config.Domain {
ctx.Logger.Warnf("Destroying session cookie as the cookie domain '%s' does not match the requests detected cookie domain '%s' which may be a sign a user tried to move this cookie from one domain to another", userSession.CookieDomain, provider.Config.Domain)
if err = provider.DestroySession(ctx.RequestCtx); err != nil {
ctx.Logger.WithError(err).Error("Error occurred trying to destroy the session cookie")
}
userSession = provider.NewDefaultUserSession()
if err = provider.SaveSession(ctx.RequestCtx, userSession); err != nil {
ctx.Logger.WithError(err).Error("Error occurred trying to save the new session cookie")
}
}
if invalid := handleVerifyGETAuthnCookieValidate(ctx, provider, &userSession, s.refreshEnabled, s.refreshInterval); invalid {
if err = ctx.DestroySession(); err != nil {
ctx.Logger.Errorf("Unable to destroy user session: %+v", err)
}
userSession = provider.NewDefaultUserSession()
userSession.LastActivity = ctx.Clock.Now().Unix()
if err = provider.SaveSession(ctx.RequestCtx, userSession); err != nil {
ctx.Logger.Errorf("Unable to save updated user session: %+v", err)
}
return authn, nil
}
if err = provider.SaveSession(ctx.RequestCtx, userSession); err != nil {
ctx.Logger.Errorf("Unable to save updated user session: %+v", err)
}
return Authn{
Username: friendlyUsername(userSession.Username),
Details: authentication.UserDetails{
Username: userSession.Username,
DisplayName: userSession.DisplayName,
Emails: userSession.Emails,
Groups: userSession.Groups,
},
Level: userSession.AuthenticationLevel,
Type: AuthnTypeCookie,
}, nil
}
// CanHandleUnauthorized returns true if this AuthnStrategy should handle Unauthorized requests.
func (s *CookieSessionAuthnStrategy) CanHandleUnauthorized() (handle bool) {
return false
}
// HandleUnauthorized is the Unauthorized handler for the cookie AuthnStrategy.
func (s *CookieSessionAuthnStrategy) HandleUnauthorized(_ *middlewares.AutheliaCtx, _ *Authn, _ *url.URL) {
}
// HeaderAuthnStrategy is a header AuthnStrategy.
type HeaderAuthnStrategy struct {
authn AuthnType
headerAuthorize []byte
headerAuthenticate []byte
handleAuthenticate bool
statusAuthenticate int
}
// Get returns the Authn information for this AuthnStrategy.
func (s *HeaderAuthnStrategy) Get(ctx *middlewares.AutheliaCtx, _ *session.Session) (authn Authn, err error) {
var (
username, password string
value []byte
)
authn = Authn{
Type: s.authn,
Level: authentication.NotAuthenticated,
}
if value = ctx.Request.Header.PeekBytes(s.headerAuthorize); value == nil {
return authn, nil
}
if username, password, err = headerAuthorizationParse(value); err != nil {
return authn, fmt.Errorf("failed to parse content of %s header: %w", s.headerAuthorize, err)
}
if username == "" || password == "" {
return authn, fmt.Errorf("failed to validate parsed credentials of %s header for user '%s': %w", s.headerAuthorize, username, err)
}
var (
valid bool
details *authentication.UserDetails
)
if valid, err = ctx.Providers.UserProvider.CheckUserPassword(username, password); err != nil {
return authn, fmt.Errorf("failed to validate parsed credentials of %s header for user '%s': %w", s.headerAuthorize, username, err)
}
if !valid {
return authn, fmt.Errorf("validated parsed credentials of %s header but they are not valid for user '%s': %w", s.headerAuthorize, username, err)
}
if details, err = ctx.Providers.UserProvider.GetDetails(username); err != nil {
if errors.Is(err, authentication.ErrUserNotFound) {
ctx.Logger.Errorf("Error occurred while attempting to get user details for user '%s': the user was not found indicating they were deleted, disabled, or otherwise no longer authorized to login", username)
return authn, err
}
return authn, fmt.Errorf("unable to retrieve details for user '%s': %w", username, err)
}
authn.Username = friendlyUsername(details.Username)
authn.Details = *details
authn.Level = authentication.OneFactor
return authn, nil
}
// CanHandleUnauthorized returns true if this AuthnStrategy should handle Unauthorized requests.
func (s *HeaderAuthnStrategy) CanHandleUnauthorized() (handle bool) {
return s.handleAuthenticate
}
// HandleUnauthorized is the Unauthorized handler for the header AuthnStrategy.
func (s *HeaderAuthnStrategy) HandleUnauthorized(ctx *middlewares.AutheliaCtx, _ *Authn, _ *url.URL) {
ctx.Logger.Debugf("Responding %d %s", s.statusAuthenticate, s.headerAuthenticate)
ctx.ReplyStatusCode(s.statusAuthenticate)
if s.headerAuthenticate != nil {
ctx.Response.Header.SetBytesKV(s.headerAuthenticate, headerValueAuthenticateBasic)
}
}
// HeaderLegacyAuthnStrategy is a legacy header AuthnStrategy which can be switched based on the query parameters.
type HeaderLegacyAuthnStrategy struct{}
// Get returns the Authn information for this AuthnStrategy.
func (s *HeaderLegacyAuthnStrategy) Get(ctx *middlewares.AutheliaCtx, _ *session.Session) (authn Authn, err error) {
var (
username, password string
value, header []byte
)
authn = Authn{
Level: authentication.NotAuthenticated,
}
if qryValueAuth := ctx.QueryArgs().PeekBytes(qryArgAuth); bytes.Equal(qryValueAuth, qryValueBasic) {
authn.Type = AuthnTypeAuthorization
header = headerAuthorization
} else {
authn.Type = AuthnTypeProxyAuthorization
header = headerProxyAuthorization
}
value = ctx.Request.Header.PeekBytes(header)
switch {
case value == nil && authn.Type == AuthnTypeAuthorization:
return authn, fmt.Errorf("header %s expected", headerAuthorization)
case value == nil:
return authn, nil
}
if username, password, err = headerAuthorizationParse(value); err != nil {
return authn, fmt.Errorf("failed to parse content of %s header: %w", header, err)
}
if username == "" || password == "" {
return authn, fmt.Errorf("failed to validate parsed credentials of %s header for user '%s': %w", header, username, err)
}
var (
valid bool
details *authentication.UserDetails
)
if valid, err = ctx.Providers.UserProvider.CheckUserPassword(username, password); err != nil {
return authn, fmt.Errorf("failed to validate parsed credentials of %s header for user '%s': %w", header, username, err)
}
if !valid {
return authn, fmt.Errorf("validated parsed credentials of %s header but they are not valid for user '%s': %w", header, username, err)
}
if details, err = ctx.Providers.UserProvider.GetDetails(username); err != nil {
if errors.Is(err, authentication.ErrUserNotFound) {
ctx.Logger.Errorf("Error occurred while attempting to get user details for user '%s': the user was not found indicating they were deleted, disabled, or otherwise no longer authorized to login", username)
return authn, err
}
return authn, fmt.Errorf("unable to retrieve details for user '%s': %w", username, err)
}
authn.Username = friendlyUsername(details.Username)
authn.Details = *details
authn.Level = authentication.OneFactor
return authn, nil
}
// CanHandleUnauthorized returns true if this AuthnStrategy should handle Unauthorized requests.
func (s *HeaderLegacyAuthnStrategy) CanHandleUnauthorized() (handle bool) {
return true
}
// HandleUnauthorized is the Unauthorized handler for the Legacy header AuthnStrategy.
func (s *HeaderLegacyAuthnStrategy) HandleUnauthorized(ctx *middlewares.AutheliaCtx, authn *Authn, _ *url.URL) {
handleAuthzUnauthorizedAuthorizationBasic(ctx, authn)
}
func handleVerifyGETAuthnCookieValidate(ctx *middlewares.AutheliaCtx, provider *session.Session, userSession *session.UserSession, profileRefreshEnabled bool, profileRefreshInterval time.Duration) (invalid bool) {
isAnonymous := userSession.Username == ""
if isAnonymous && userSession.AuthenticationLevel != authentication.NotAuthenticated {
ctx.Logger.Errorf("Session for anonymous user has an authentication level of '%s': this may be a sign of a compromise", userSession.AuthenticationLevel)
return true
}
if invalid = handleVerifyGETAuthnCookieValidateInactivity(ctx, provider, userSession, isAnonymous); invalid {
ctx.Logger.Infof("Session for user '%s' not marked as remembereded has exceeded configured session inactivity", userSession.Username)
return true
}
if invalid = handleVerifyGETAuthnCookieValidateUpdate(ctx, userSession, isAnonymous, profileRefreshEnabled, profileRefreshInterval); invalid {
return true
}
if username := ctx.Request.Header.PeekBytes(headerSessionUsername); username != nil && !strings.EqualFold(string(username), userSession.Username) {
ctx.Logger.Warnf("Session for user '%s' does not match the Session-Username header with value '%s' which could be a sign of a cookie hijack", userSession.Username, username)
return true
}
if !userSession.KeepMeLoggedIn {
userSession.LastActivity = ctx.Clock.Now().Unix()
}
return false
}
func handleVerifyGETAuthnCookieValidateInactivity(ctx *middlewares.AutheliaCtx, provider *session.Session, userSession *session.UserSession, isAnonymous bool) (invalid bool) {
if isAnonymous || userSession.KeepMeLoggedIn || int64(provider.Config.Inactivity.Seconds()) == 0 {
return false
}
ctx.Logger.Tracef("Inactivity report for user '%s'. Current Time: %d, Last Activity: %d, Maximum Inactivity: %d.", userSession.Username, ctx.Clock.Now().Unix(), userSession.LastActivity, int(provider.Config.Inactivity.Seconds()))
return time.Unix(userSession.LastActivity, 0).Add(provider.Config.Inactivity).Before(ctx.Clock.Now())
}
func handleVerifyGETAuthnCookieValidateUpdate(ctx *middlewares.AutheliaCtx, userSession *session.UserSession, isAnonymous, enabled bool, interval time.Duration) (invalid bool) {
if !enabled || isAnonymous {
return false
}
ctx.Logger.Tracef("Checking if we need check the authentication backend for an updated profile for user '%s'", userSession.Username)
if interval != schema.RefreshIntervalAlways && userSession.RefreshTTL.After(ctx.Clock.Now()) {
return false
}
ctx.Logger.Debugf("Checking the authentication backend for an updated profile for user '%s'", userSession.Username)
var (
details *authentication.UserDetails
err error
)
if details, err = ctx.Providers.UserProvider.GetDetails(userSession.Username); err != nil {
if errors.Is(err, authentication.ErrUserNotFound) {
ctx.Logger.Errorf("Error occurred while attempting to update user details for user '%s': the user was not found indicating they were deleted, disabled, or otherwise no longer authorized to login", userSession.Username)
return true
}
ctx.Logger.Errorf("Error occurred while attempting to update user details for user '%s': %v", userSession.Username, err)
return false
}
var (
diffEmails, diffGroups, diffDisplayName bool
)
diffEmails, diffGroups = utils.IsStringSlicesDifferent(userSession.Emails, details.Emails), utils.IsStringSlicesDifferent(userSession.Groups, details.Groups)
diffDisplayName = userSession.DisplayName != details.DisplayName
if interval != schema.RefreshIntervalAlways {
userSession.RefreshTTL = ctx.Clock.Now().Add(interval)
}
if !diffEmails && !diffGroups && !diffDisplayName {
ctx.Logger.Tracef("Updated profile not detected for user '%s'", userSession.Username)
return false
}
ctx.Logger.Debugf("Updated profile detected for user '%s'", userSession.Username)
if ctx.Logger.Level >= logrus.TraceLevel {
generateVerifySessionHasUpToDateProfileTraceLogs(ctx, userSession, details)
}
userSession.Emails, userSession.Groups, userSession.DisplayName = details.Emails, details.Groups, details.DisplayName
return false
}
func headerAuthorizationParse(value []byte) (username, password string, err error) {
if bytes.Equal(value, qryValueEmpty) {
return "", "", fmt.Errorf("header is malformed: empty value")
}
parts := strings.SplitN(string(value), " ", 2)
if len(parts) != 2 {
return "", "", fmt.Errorf("header is malformed: does not appear to have a scheme")
}
scheme := strings.ToLower(parts[0])
switch scheme {
case headerAuthorizationSchemeBasic:
if username, password, err = headerAuthorizationParseBasic(parts[1]); err != nil {
return username, password, fmt.Errorf("header is malformed: %w", err)
}
return username, password, nil
default:
return "", "", fmt.Errorf("header is malformed: unsupported scheme '%s': supported schemes '%s'", parts[0], strings.ToTitle(headerAuthorizationSchemeBasic))
}
}
func headerAuthorizationParseBasic(value string) (username, password string, err error) {
var content []byte
if content, err = base64.StdEncoding.DecodeString(value); err != nil {
return "", "", fmt.Errorf("could not decode credentials: %w", err)
}
strContent := string(content)
s := strings.IndexByte(strContent, ':')
if s < 1 {
return "", "", fmt.Errorf("format of header must be <user>:<password> but either doesn't have a colon or username")
}
return strContent[:s], strContent[s+1:], nil
}

View File

@ -0,0 +1,190 @@
package handlers
import (
"fmt"
"time"
"github.com/authelia/authelia/v4/internal/configuration/schema"
"github.com/authelia/authelia/v4/internal/utils"
)
// NewAuthzBuilder creates a new AuthzBuilder.
func NewAuthzBuilder() *AuthzBuilder {
return &AuthzBuilder{
config: AuthzConfig{RefreshInterval: time.Second * -1},
}
}
// WithStrategies replaces all strategies in this builder with the provided value.
func (b *AuthzBuilder) WithStrategies(strategies ...AuthnStrategy) *AuthzBuilder {
b.strategies = strategies
return b
}
// WithStrategyCookie adds the Cookie header strategy to the strategies in this builder.
func (b *AuthzBuilder) WithStrategyCookie(refreshInterval time.Duration) *AuthzBuilder {
b.strategies = append(b.strategies, NewCookieSessionAuthnStrategy(refreshInterval))
return b
}
// WithStrategyAuthorization adds the Authorization header strategy to the strategies in this builder.
func (b *AuthzBuilder) WithStrategyAuthorization() *AuthzBuilder {
b.strategies = append(b.strategies, NewHeaderAuthorizationAuthnStrategy())
return b
}
// WithStrategyProxyAuthorization adds the Proxy-Authorization header strategy to the strategies in this builder.
func (b *AuthzBuilder) WithStrategyProxyAuthorization() *AuthzBuilder {
b.strategies = append(b.strategies, NewHeaderProxyAuthorizationAuthnStrategy())
return b
}
// WithImplementationLegacy configures this builder to output an Authz which is used with the Legacy
// implementation which is a mix of the other implementations and usually works with most proxies.
func (b *AuthzBuilder) WithImplementationLegacy() *AuthzBuilder {
b.impl = AuthzImplLegacy
return b
}
// WithImplementationForwardAuth configures this builder to output an Authz which is used with the ForwardAuth
// implementation traditionally used by Traefik, Caddy, and Skipper.
func (b *AuthzBuilder) WithImplementationForwardAuth() *AuthzBuilder {
b.impl = AuthzImplForwardAuth
return b
}
// WithImplementationAuthRequest configures this builder to output an Authz which is used with the AuthRequest
// implementation traditionally used by NGINX.
func (b *AuthzBuilder) WithImplementationAuthRequest() *AuthzBuilder {
b.impl = AuthzImplAuthRequest
return b
}
// WithImplementationExtAuthz configures this builder to output an Authz which is used with the ExtAuthz
// implementation traditionally used by Envoy.
func (b *AuthzBuilder) WithImplementationExtAuthz() *AuthzBuilder {
b.impl = AuthzImplExtAuthz
return b
}
// WithConfig allows configuring the Authz config by providing a *schema.Configuration. This function converts it to
// an AuthzConfig and assigns it to the builder.
func (b *AuthzBuilder) WithConfig(config *schema.Configuration) *AuthzBuilder {
if config == nil {
return b
}
var refreshInterval time.Duration
switch config.AuthenticationBackend.RefreshInterval {
case schema.ProfileRefreshDisabled:
refreshInterval = time.Second * -1
case schema.ProfileRefreshAlways:
refreshInterval = time.Second * 0
default:
refreshInterval, _ = utils.ParseDurationString(config.AuthenticationBackend.RefreshInterval)
}
b.config = AuthzConfig{
RefreshInterval: refreshInterval,
Domains: []AuthzDomain{
{
Name: fmt.Sprintf(".%s", config.Session.Domain),
PortalURL: nil,
},
},
}
return b
}
// WithEndpointConfig configures the AuthzBuilder with a *schema.ServerAuthzEndpointConfig. Should be called AFTER
// WithConfig or WithAuthzConfig.
func (b *AuthzBuilder) WithEndpointConfig(config schema.ServerAuthzEndpoint) *AuthzBuilder {
switch config.Implementation {
case AuthzImplForwardAuth.String():
b.WithImplementationForwardAuth()
case AuthzImplAuthRequest.String():
b.WithImplementationAuthRequest()
case AuthzImplExtAuthz.String():
b.WithImplementationExtAuthz()
default:
b.WithImplementationLegacy()
}
b.WithStrategies()
for _, strategy := range config.AuthnStrategies {
switch strategy.Name {
case AuthnStrategyCookieSession:
b.strategies = append(b.strategies, NewCookieSessionAuthnStrategy(b.config.RefreshInterval))
case AuthnStrategyHeaderAuthorization:
b.strategies = append(b.strategies, NewHeaderAuthorizationAuthnStrategy())
case AuthnStrategyHeaderProxyAuthorization:
b.strategies = append(b.strategies, NewHeaderProxyAuthorizationAuthnStrategy())
case AuthnStrategyHeaderAuthRequestProxyAuthorization:
b.strategies = append(b.strategies, NewHeaderProxyAuthorizationAuthRequestAuthnStrategy())
case AuthnStrategyHeaderLegacy:
b.strategies = append(b.strategies, NewHeaderLegacyAuthnStrategy())
}
}
return b
}
// WithAuthzConfig allows configuring the Authz config by providing a AuthzConfig directly. Recommended this is only
// used in testing and WithConfig is used instead.
func (b *AuthzBuilder) WithAuthzConfig(config AuthzConfig) *AuthzBuilder {
b.config = config
return b
}
// Build returns a new Authz from the currently configured options in this builder.
func (b *AuthzBuilder) Build() (authz *Authz) {
authz = &Authz{
config: b.config,
strategies: b.strategies,
handleAuthorized: handleAuthzAuthorizedStandard,
}
if len(authz.strategies) == 0 {
switch b.impl {
case AuthzImplLegacy:
authz.strategies = []AuthnStrategy{NewHeaderLegacyAuthnStrategy(), NewCookieSessionAuthnStrategy(b.config.RefreshInterval)}
case AuthzImplAuthRequest:
authz.strategies = []AuthnStrategy{NewHeaderProxyAuthorizationAuthRequestAuthnStrategy(), NewCookieSessionAuthnStrategy(b.config.RefreshInterval)}
default:
authz.strategies = []AuthnStrategy{NewHeaderProxyAuthorizationAuthnStrategy(), NewCookieSessionAuthnStrategy(b.config.RefreshInterval)}
}
}
switch b.impl {
case AuthzImplLegacy:
authz.legacy = true
authz.handleGetObject = handleAuthzGetObjectLegacy
authz.handleUnauthorized = handleAuthzUnauthorizedLegacy
authz.handleGetAutheliaURL = handleAuthzPortalURLLegacy
case AuthzImplForwardAuth:
authz.handleGetObject = handleAuthzGetObjectForwardAuth
authz.handleUnauthorized = handleAuthzUnauthorizedForwardAuth
authz.handleGetAutheliaURL = handleAuthzPortalURLFromQuery
case AuthzImplAuthRequest:
authz.handleGetObject = handleAuthzGetObjectAuthRequest
authz.handleUnauthorized = handleAuthzUnauthorizedAuthRequest
case AuthzImplExtAuthz:
authz.handleGetObject = handleAuthzGetObjectExtAuthz
authz.handleUnauthorized = handleAuthzUnauthorizedExtAuthz
authz.handleGetAutheliaURL = handleAuthzPortalURLFromHeader
}
return authz
}

View File

@ -0,0 +1,114 @@
package handlers
import (
"fmt"
"net/url"
"strings"
"github.com/valyala/fasthttp"
"github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/utils"
)
func handleAuthzPortalURLLegacy(ctx *middlewares.AutheliaCtx) (portalURL *url.URL, err error) {
if portalURL, err = handleAuthzPortalURLFromQueryLegacy(ctx); err != nil || portalURL != nil {
return portalURL, err
}
return handleAuthzPortalURLFromHeader(ctx)
}
func handleAuthzPortalURLFromHeader(ctx *middlewares.AutheliaCtx) (portalURL *url.URL, err error) {
rawURL := ctx.XAutheliaURL()
if rawURL == nil {
return nil, nil
}
if portalURL, err = url.ParseRequestURI(string(rawURL)); err != nil {
return nil, err
}
return portalURL, nil
}
func handleAuthzPortalURLFromQuery(ctx *middlewares.AutheliaCtx) (portalURL *url.URL, err error) {
rawURL := ctx.QueryArgAutheliaURL()
if rawURL == nil {
return nil, nil
}
if portalURL, err = url.ParseRequestURI(string(rawURL)); err != nil {
return nil, err
}
return portalURL, nil
}
func handleAuthzPortalURLFromQueryLegacy(ctx *middlewares.AutheliaCtx) (portalURL *url.URL, err error) {
rawURL := ctx.QueryArgs().PeekBytes(qryArgRD)
if rawURL == nil {
return nil, nil
}
if portalURL, err = url.ParseRequestURI(string(rawURL)); err != nil {
return nil, err
}
return portalURL, nil
}
func handleAuthzAuthorizedStandard(ctx *middlewares.AutheliaCtx, authn *Authn) {
ctx.ReplyStatusCode(fasthttp.StatusOK)
if authn.Details.Username != "" {
ctx.Response.Header.SetBytesK(headerRemoteUser, authn.Details.Username)
ctx.Response.Header.SetBytesK(headerRemoteGroups, strings.Join(authn.Details.Groups, ","))
ctx.Response.Header.SetBytesK(headerRemoteName, authn.Details.DisplayName)
switch len(authn.Details.Emails) {
case 0:
ctx.Response.Header.SetBytesK(headerRemoteEmail, "")
default:
ctx.Response.Header.SetBytesK(headerRemoteEmail, authn.Details.Emails[0])
}
}
}
func handleAuthzUnauthorizedAuthorizationBasic(ctx *middlewares.AutheliaCtx, authn *Authn) {
ctx.Logger.Infof("Access to '%s' is not authorized to user '%s', sending 401 response with WWW-Authenticate header requesting Basic scheme", authn.Object.URL.String(), authn.Username)
ctx.ReplyUnauthorized()
ctx.Response.Header.SetBytesKV(headerWWWAuthenticate, headerValueAuthenticateBasic)
}
var protoHostSeparator = []byte("://")
func getRequestURIFromForwardedHeaders(protocol, host, uri []byte) (requestURI *url.URL, err error) {
if len(protocol) == 0 {
return nil, fmt.Errorf("missing protocol value")
}
if len(host) == 0 {
return nil, fmt.Errorf("missing host value")
}
value := utils.BytesJoin(protocol, protoHostSeparator, host, uri)
if requestURI, err = url.ParseRequestURI(string(value)); err != nil {
return nil, fmt.Errorf("failed to parse forwarded headers: %w", err)
}
return requestURI, nil
}
func hasInvalidMethodCharacters(v []byte) bool {
for _, c := range v {
if c < 0x41 || c > 0x5A {
return true
}
}
return false
}

View File

@ -0,0 +1,42 @@
package handlers
import (
"fmt"
"net/url"
"github.com/valyala/fasthttp"
"github.com/authelia/authelia/v4/internal/authorization"
"github.com/authelia/authelia/v4/internal/middlewares"
)
func handleAuthzGetObjectAuthRequest(ctx *middlewares.AutheliaCtx) (object authorization.Object, err error) {
var (
targetURL *url.URL
rawURL, method []byte
)
if rawURL = ctx.XOriginalURL(); len(rawURL) == 0 {
return object, middlewares.ErrMissingXOriginalURL
}
if targetURL, err = url.ParseRequestURI(string(rawURL)); err != nil {
return object, fmt.Errorf("failed to parse X-Original-URL header: %w", err)
}
if method = ctx.XOriginalMethod(); len(method) == 0 {
return object, fmt.Errorf("header 'X-Original-Method' is empty")
}
if hasInvalidMethodCharacters(method) {
return object, fmt.Errorf("header 'X-Original-Method' with value '%s' has invalid characters", method)
}
return authorization.NewObjectRaw(targetURL, method), nil
}
func handleAuthzUnauthorizedAuthRequest(ctx *middlewares.AutheliaCtx, authn *Authn, _ *url.URL) {
ctx.Logger.Infof("Access to %s (method %s) is not authorized to user %s, responding with status code %d", authn.Object.URL.String(), authn.Method, authn.Username, fasthttp.StatusUnauthorized)
ctx.ReplyUnauthorized()
}

View File

@ -0,0 +1,456 @@
package handlers
import (
"fmt"
"net/url"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
"github.com/valyala/fasthttp"
"github.com/authelia/authelia/v4/internal/authorization"
"github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/mocks"
"github.com/authelia/authelia/v4/internal/session"
)
func TestRunAuthRequestAuthzSuite(t *testing.T) {
suite.Run(t, NewAuthRequestAuthzSuite())
}
func NewAuthRequestAuthzSuite() *AuthRequestAuthzSuite {
return &AuthRequestAuthzSuite{
AuthzSuite: &AuthzSuite{
implementation: AuthzImplAuthRequest,
setRequest: setRequestAuthRequest,
},
}
}
type AuthRequestAuthzSuite struct {
*AuthzSuite
}
func (s *AuthRequestAuthzSuite) TestShouldHandleAllMethodsDeny() {
for _, method := range testRequestMethods {
s.T().Run(fmt.Sprintf("OriginalMethod%s", method), func(t *testing.T) {
for _, targetURI := range []*url.URL{
s.RequireParseRequestURI("https://one-factor.example.com"),
s.RequireParseRequestURI("https://one-factor.example.com/subpath"),
s.RequireParseRequestURI("https://one-factor.example2.com"),
s.RequireParseRequestURI("https://one-factor.example2.com/subpath"),
} {
t.Run(targetURI.String(), func(t *testing.T) {
authz := s.Builder().Build()
mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close()
s.setRequest(mock.Ctx, method, targetURI, true, false)
authz.Handler(mock.Ctx)
assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode())
assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation))
})
}
})
}
}
func (s *AuthRequestAuthzSuite) TestShouldHandleInvalidMethodCharsDeny() {
for _, method := range testRequestMethods {
method += "z"
s.T().Run(fmt.Sprintf("OriginalMethod%s", method), func(t *testing.T) {
for _, targetURI := range []*url.URL{
s.RequireParseRequestURI("https://bypass.example.com"),
s.RequireParseRequestURI("https://bypass.example.com/subpath"),
s.RequireParseRequestURI("https://bypass.example2.com"),
s.RequireParseRequestURI("https://bypass.example2.com/subpath"),
} {
t.Run(targetURI.String(), func(t *testing.T) {
authz := s.Builder().Build()
mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close()
s.setRequest(mock.Ctx, method, targetURI, true, false)
authz.Handler(mock.Ctx)
assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode())
assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation))
})
}
})
}
}
func (s *AuthRequestAuthzSuite) TestShouldHandleMissingXOriginalMethodDeny() {
for _, targetURI := range []*url.URL{
s.RequireParseRequestURI("https://bypass.example.com"),
s.RequireParseRequestURI("https://bypass.example.com/subpath"),
s.RequireParseRequestURI("https://bypass.example2.com"),
s.RequireParseRequestURI("https://bypass.example2.com/subpath"),
} {
s.T().Run(targetURI.String(), func(t *testing.T) {
authz := s.Builder().Build()
mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close()
s.setRequest(mock.Ctx, "", targetURI, true, false)
authz.Handler(mock.Ctx)
assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode())
assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation))
})
}
}
func (s *AuthRequestAuthzSuite) TestShouldHandleMissingXOriginalURLDeny() {
for _, method := range testRequestMethods {
s.T().Run(fmt.Sprintf("OriginalMethod%s", method), func(t *testing.T) {
authz := s.Builder().Build()
mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close()
s.setRequest(mock.Ctx, method, nil, true, false)
authz.Handler(mock.Ctx)
assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode())
assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation))
})
}
}
func (s *AuthRequestAuthzSuite) TestShouldHandleAllMethodsAllow() {
for _, method := range testRequestMethods {
s.T().Run(fmt.Sprintf("OriginalMethod%s", method), func(t *testing.T) {
for _, targetURI := range []*url.URL{
s.RequireParseRequestURI("https://bypass.example.com"),
s.RequireParseRequestURI("https://bypass.example.com/subpath"),
s.RequireParseRequestURI("https://bypass.example2.com"),
s.RequireParseRequestURI("https://bypass.example2.com/subpath"),
} {
t.Run(targetURI.String(), func(t *testing.T) {
authz := s.Builder().Build()
mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close()
s.setRequest(mock.Ctx, method, targetURI, true, false)
authz.Handler(mock.Ctx)
assert.Equal(t, fasthttp.StatusOK, mock.Ctx.Response.StatusCode())
assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation))
})
}
})
}
}
func (s *AuthRequestAuthzSuite) TestShouldHandleAllMethodsWithMethodsACL() {
for _, method := range testRequestMethods {
s.T().Run(fmt.Sprintf("Method%s", method), func(t *testing.T) {
for _, methodACL := range testRequestMethods {
targetURI := s.RequireParseRequestURI(fmt.Sprintf("https://bypass-%s.example.com", strings.ToLower(methodACL)))
t.Run(targetURI.String(), func(t *testing.T) {
authz := s.Builder().Build()
mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close()
for i, cookie := range mock.Ctx.Configuration.Session.Cookies {
mock.Ctx.Configuration.Session.Cookies[i].AutheliaURL = s.RequireParseRequestURI(fmt.Sprintf("https://auth.%s", cookie.Domain))
}
mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil)
s.setRequest(mock.Ctx, method, targetURI, true, false)
authz.Handler(mock.Ctx)
if method == methodACL {
assert.Equal(t, fasthttp.StatusOK, mock.Ctx.Response.StatusCode())
assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation))
} else {
assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode())
assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation))
}
})
}
})
}
}
func (s *AuthRequestAuthzSuite) TestShouldHandleInvalidURLForCVE202132637() {
testCases := []struct {
name string
uri []byte
expected int
}{
{"Should401UnauthorizedWithNullByte",
[]byte{104, 116, 116, 112, 115, 58, 47, 47, 0, 110, 111, 116, 45, 111, 110, 101, 45, 102, 97, 99, 116, 111, 114, 46, 101, 120, 97, 109, 112, 108, 101, 46, 99, 111, 109},
fasthttp.StatusUnauthorized,
},
{"Should200OkWithoutNullByte",
[]byte{104, 116, 116, 112, 115, 58, 47, 47, 110, 111, 116, 45, 111, 110, 101, 45, 102, 97, 99, 116, 111, 114, 46, 101, 120, 97, 109, 112, 108, 101, 46, 99, 111, 109},
fasthttp.StatusOK,
},
}
for _, tc := range testCases {
s.T().Run(tc.name, func(t *testing.T) {
for _, method := range testRequestMethods {
t.Run(fmt.Sprintf("OriginalMethod%s", method), func(t *testing.T) {
authz := s.Builder().Build()
mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close()
mock.Ctx.Configuration.AccessControl.DefaultPolicy = testBypass
mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(&mock.Ctx.Configuration)
mock.Ctx.Request.Header.Set(testXOriginalMethod, method)
mock.Ctx.Request.Header.SetBytesKV([]byte(testXOriginalUrl), tc.uri)
authz.Handler(mock.Ctx)
assert.Equal(t, tc.expected, mock.Ctx.Response.StatusCode())
assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation))
})
}
})
}
}
func (s *AuthRequestAuthzSuite) TestShouldNotHandleExtAuthzAllMethodsAllow() {
for _, method := range testRequestMethods {
s.T().Run(fmt.Sprintf("Method%s", method), func(t *testing.T) {
for _, targetURI := range []*url.URL{
s.RequireParseRequestURI("https://bypass.example.com"),
s.RequireParseRequestURI("https://bypass.example.com/subpath"),
s.RequireParseRequestURI("https://bypass.example2.com"),
s.RequireParseRequestURI("https://bypass.example2.com/subpath"),
} {
t.Run(targetURI.String(), func(t *testing.T) {
authz := s.Builder().Build()
mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close()
for i, cookie := range mock.Ctx.Configuration.Session.Cookies {
mock.Ctx.Configuration.Session.Cookies[i].AutheliaURL = s.RequireParseRequestURI(fmt.Sprintf("https://auth.%s", cookie.Domain))
}
mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil)
setRequestExtAuthz(mock.Ctx, method, targetURI, true, false)
authz.Handler(mock.Ctx)
assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode())
assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation))
})
}
})
}
}
func (s *AuthRequestAuthzSuite) TestShouldNotHandleExtAuthzAllMethodsAllowXHR() {
for _, method := range testRequestMethods {
s.T().Run(fmt.Sprintf("Method%s", method), func(t *testing.T) {
for xname, x := range testXHR {
t.Run(xname, func(t *testing.T) {
for _, targetURI := range []*url.URL{
s.RequireParseRequestURI("https://bypass.example.com"),
s.RequireParseRequestURI("https://bypass.example.com/subpath"),
s.RequireParseRequestURI("https://bypass.example2.com"),
s.RequireParseRequestURI("https://bypass.example2.com/subpath"),
} {
t.Run(targetURI.String(), func(t *testing.T) {
authz := s.Builder().Build()
mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close()
for i, cookie := range mock.Ctx.Configuration.Session.Cookies {
mock.Ctx.Configuration.Session.Cookies[i].AutheliaURL = s.RequireParseRequestURI(fmt.Sprintf("https://auth.%s", cookie.Domain))
}
mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil)
setRequestExtAuthz(mock.Ctx, method, targetURI, x, x)
authz.Handler(mock.Ctx)
assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode())
assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation))
})
}
})
}
})
}
}
func (s *AuthRequestAuthzSuite) TestShouldNotHandleExtAuthzAllMethodsWithMethodsACL() {
for _, method := range testRequestMethods {
s.T().Run(fmt.Sprintf("Method%s", method), func(t *testing.T) {
for _, methodACL := range testRequestMethods {
targetURI := s.RequireParseRequestURI(fmt.Sprintf("https://bypass-%s.example.com", strings.ToLower(methodACL)))
t.Run(targetURI.String(), func(t *testing.T) {
authz := s.Builder().Build()
mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close()
for i, cookie := range mock.Ctx.Configuration.Session.Cookies {
mock.Ctx.Configuration.Session.Cookies[i].AutheliaURL = s.RequireParseRequestURI(fmt.Sprintf("https://auth.%s", cookie.Domain))
}
mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil)
setRequestExtAuthz(mock.Ctx, method, targetURI, true, false)
authz.Handler(mock.Ctx)
assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode())
assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation))
})
}
})
}
}
func (s *AuthRequestAuthzSuite) TestShouldNotHandleForwardAuthAllMethodsAllow() {
for _, method := range testRequestMethods {
s.T().Run(fmt.Sprintf("Method%s", method), func(t *testing.T) {
for _, targetURI := range []*url.URL{
s.RequireParseRequestURI("https://bypass.example.com"),
s.RequireParseRequestURI("https://bypass.example.com/subpath"),
s.RequireParseRequestURI("https://bypass.example2.com"),
s.RequireParseRequestURI("https://bypass.example2.com/subpath"),
} {
t.Run(targetURI.String(), func(t *testing.T) {
authz := s.Builder().Build()
mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close()
for i, cookie := range mock.Ctx.Configuration.Session.Cookies {
mock.Ctx.Configuration.Session.Cookies[i].AutheliaURL = s.RequireParseRequestURI(fmt.Sprintf("https://auth.%s", cookie.Domain))
}
mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil)
setRequestForwardAuth(mock.Ctx, method, targetURI, true, false)
authz.Handler(mock.Ctx)
assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode())
assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation))
})
}
})
}
}
func (s *AuthRequestAuthzSuite) TestShouldNotHandleForwardAuthAllMethodsAllowXHR() {
for _, method := range testRequestMethods {
s.T().Run(fmt.Sprintf("Method%s", method), func(t *testing.T) {
for xname, x := range testXHR {
t.Run(xname, func(t *testing.T) {
for _, targetURI := range []*url.URL{
s.RequireParseRequestURI("https://bypass.example.com"),
s.RequireParseRequestURI("https://bypass.example.com/subpath"),
s.RequireParseRequestURI("https://bypass.example2.com"),
s.RequireParseRequestURI("https://bypass.example2.com/subpath"),
} {
t.Run(targetURI.String(), func(t *testing.T) {
authz := s.Builder().Build()
mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close()
for i, cookie := range mock.Ctx.Configuration.Session.Cookies {
mock.Ctx.Configuration.Session.Cookies[i].AutheliaURL = s.RequireParseRequestURI(fmt.Sprintf("https://auth.%s", cookie.Domain))
}
mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil)
setRequestForwardAuth(mock.Ctx, method, targetURI, x, x)
authz.Handler(mock.Ctx)
assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode())
assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation))
})
}
})
}
})
}
}
func (s *AuthRequestAuthzSuite) TestShouldNotHandleForwardAuthAllMethodsWithMethodsACL() {
for _, method := range testRequestMethods {
s.T().Run(fmt.Sprintf("Method%s", method), func(t *testing.T) {
for _, methodACL := range testRequestMethods {
targetURI := s.RequireParseRequestURI(fmt.Sprintf("https://bypass-%s.example.com", strings.ToLower(methodACL)))
t.Run(targetURI.String(), func(t *testing.T) {
authz := s.Builder().Build()
mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close()
for i, cookie := range mock.Ctx.Configuration.Session.Cookies {
mock.Ctx.Configuration.Session.Cookies[i].AutheliaURL = s.RequireParseRequestURI(fmt.Sprintf("https://auth.%s", cookie.Domain))
}
mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil)
setRequestForwardAuth(mock.Ctx, method, targetURI, true, false)
authz.Handler(mock.Ctx)
assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode())
assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation))
})
}
})
}
}
func setRequestAuthRequest(ctx *middlewares.AutheliaCtx, method string, targetURI *url.URL, accept, xhr bool) {
if method != "" {
ctx.Request.Header.Set(testXOriginalMethod, method)
}
if targetURI != nil {
ctx.Request.Header.Set(testXOriginalUrl, targetURI.String())
}
setRequestXHRValues(ctx, accept, xhr)
}

View File

@ -0,0 +1,55 @@
package handlers
import (
"fmt"
"net/url"
"github.com/valyala/fasthttp"
"github.com/authelia/authelia/v4/internal/authorization"
"github.com/authelia/authelia/v4/internal/middlewares"
)
func handleAuthzGetObjectExtAuthz(ctx *middlewares.AutheliaCtx) (object authorization.Object, err error) {
protocol, host, uri := ctx.XForwardedProto(), ctx.RequestCtx.Host(), ctx.AuthzPath()
var (
targetURL *url.URL
method []byte
)
if targetURL, err = getRequestURIFromForwardedHeaders(protocol, host, uri); err != nil {
return object, fmt.Errorf("failed to get target URL: %w", err)
}
if method = ctx.Method(); len(method) == 0 {
return object, fmt.Errorf("start line value 'Method' is empty")
}
if hasInvalidMethodCharacters(method) {
return object, fmt.Errorf("start line value 'Method' with value '%s' has invalid characters", method)
}
return authorization.NewObjectRaw(targetURL, method), nil
}
func handleAuthzUnauthorizedExtAuthz(ctx *middlewares.AutheliaCtx, authn *Authn, redirectionURL *url.URL) {
var (
statusCode int
)
switch {
case ctx.IsXHR() || !ctx.AcceptsMIME("text/html"):
statusCode = fasthttp.StatusUnauthorized
default:
switch authn.Object.Method {
case fasthttp.MethodGet, fasthttp.MethodOptions, fasthttp.MethodHead:
statusCode = fasthttp.StatusFound
default:
statusCode = fasthttp.StatusSeeOther
}
}
ctx.Logger.Infof("Access to %s (method %s) is not authorized to user %s, responding with status code %d with location redirect to %s", authn.Object.String(), authn.Method, authn.Username, statusCode, redirectionURL)
ctx.SpecialRedirect(redirectionURL.String(), statusCode)
}

View File

@ -0,0 +1,615 @@
package handlers
import (
"fmt"
"net/url"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
"github.com/valyala/fasthttp"
"github.com/authelia/authelia/v4/internal/authorization"
"github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/mocks"
"github.com/authelia/authelia/v4/internal/session"
)
func TestRunExtAuthzAuthzSuite(t *testing.T) {
suite.Run(t, NewExtAuthzAuthzSuite())
}
func NewExtAuthzAuthzSuite() *ExtAuthzAuthzSuite {
return &ExtAuthzAuthzSuite{
AuthzSuite: &AuthzSuite{
implementation: AuthzImplExtAuthz,
setRequest: setRequestExtAuthz,
},
}
}
type ExtAuthzAuthzSuite struct {
*AuthzSuite
}
func (s *ExtAuthzAuthzSuite) TestShouldHandleAllMethodsDeny() {
for _, method := range testRequestMethods {
s.T().Run(fmt.Sprintf("Method%s", method), func(t *testing.T) {
for _, pairURI := range []urlpair{
{s.RequireParseRequestURI("https://one-factor.example.com"), s.RequireParseRequestURI("https://auth.example.com/")},
{s.RequireParseRequestURI("https://one-factor.example.com/subpath"), s.RequireParseRequestURI("https://auth.example.com/")},
{s.RequireParseRequestURI("https://one-factor.example2.com"), s.RequireParseRequestURI("https://auth.example2.com/")},
{s.RequireParseRequestURI("https://one-factor.example2.com/subpath"), s.RequireParseRequestURI("https://auth.example2.com/")},
} {
t.Run(pairURI.TargetURI.String(), func(t *testing.T) {
expected := s.RequireParseRequestURI(pairURI.AutheliaURI.String())
authz := s.Builder().Build()
mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close()
for i, cookie := range mock.Ctx.Configuration.Session.Cookies {
mock.Ctx.Configuration.Session.Cookies[i].AutheliaURL = s.RequireParseRequestURI(fmt.Sprintf("https://auth.%s", cookie.Domain))
}
mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil)
s.setRequest(mock.Ctx, method, pairURI.TargetURI, true, false)
authz.Handler(mock.Ctx)
switch method {
case fasthttp.MethodGet, fasthttp.MethodOptions, fasthttp.MethodHead:
assert.Equal(t, fasthttp.StatusFound, mock.Ctx.Response.StatusCode())
default:
assert.Equal(t, fasthttp.StatusSeeOther, mock.Ctx.Response.StatusCode())
}
query := expected.Query()
query.Set(queryArgRD, pairURI.TargetURI.String())
query.Set(queryArgRM, method)
expected.RawQuery = query.Encode()
assert.Equal(t, expected.String(), string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation)))
})
}
})
}
}
func (s *ExtAuthzAuthzSuite) TestShouldHandleAllMethodsOverrideAutheliaURLDeny() {
for _, method := range testRequestMethods {
s.T().Run(fmt.Sprintf("Method%s", method), func(t *testing.T) {
for _, pairURI := range []urlpair{
{s.RequireParseRequestURI("https://one-factor.example.com"), s.RequireParseRequestURI("https://auth-from-override.example.com/")},
{s.RequireParseRequestURI("https://one-factor.example.com/subpath"), s.RequireParseRequestURI("https://auth-from-override.example.com/")},
{s.RequireParseRequestURI("https://one-factor.example2.com"), s.RequireParseRequestURI("https://auth-from-override.example2.com/")},
{s.RequireParseRequestURI("https://one-factor.example2.com/subpath"), s.RequireParseRequestURI("https://auth-from-override.example2.com/")},
} {
t.Run(pairURI.TargetURI.String(), func(t *testing.T) {
expected := s.RequireParseRequestURI(pairURI.AutheliaURI.String())
authz := s.Builder().Build()
mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close()
for i, cookie := range mock.Ctx.Configuration.Session.Cookies {
mock.Ctx.Configuration.Session.Cookies[i].AutheliaURL = s.RequireParseRequestURI(fmt.Sprintf("https://auth.%s", cookie.Domain))
}
mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil)
mock.Ctx.Request.Header.Set("X-Authelia-Url", pairURI.AutheliaURI.String())
s.setRequest(mock.Ctx, method, pairURI.TargetURI, true, false)
authz.Handler(mock.Ctx)
switch method {
case fasthttp.MethodGet, fasthttp.MethodOptions, fasthttp.MethodHead:
assert.Equal(t, fasthttp.StatusFound, mock.Ctx.Response.StatusCode())
default:
assert.Equal(t, fasthttp.StatusSeeOther, mock.Ctx.Response.StatusCode())
}
query := expected.Query()
query.Set(queryArgRD, pairURI.TargetURI.String())
query.Set(queryArgRM, method)
expected.RawQuery = query.Encode()
assert.Equal(t, expected.String(), string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation)))
})
}
})
}
}
func (s *ExtAuthzAuthzSuite) TestShouldHandleAllMethodsMissingAutheliaURLDeny() {
for _, method := range testRequestMethods {
s.T().Run(fmt.Sprintf("Method%s", method), func(t *testing.T) {
for _, targetURI := range []*url.URL{
s.RequireParseRequestURI("https://bypass.example.com"),
s.RequireParseRequestURI("https://bypass.example.com/subpath"),
s.RequireParseRequestURI("https://bypass.example2.com"),
s.RequireParseRequestURI("https://bypass.example2.com/subpath"),
} {
t.Run(targetURI.String(), func(t *testing.T) {
authz := s.Builder().Build()
mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close()
s.setRequest(mock.Ctx, method, targetURI, true, false)
authz.Handler(mock.Ctx)
assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode())
assert.Equal(t, "", string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation)))
})
}
})
}
}
func (s *ExtAuthzAuthzSuite) TestShouldHandleAllMethodsXHRDeny() {
for _, method := range testRequestMethods {
s.T().Run(fmt.Sprintf("Method%s", method), func(t *testing.T) {
for xname, x := range testXHR {
t.Run(xname, func(t *testing.T) {
for _, pairURI := range []urlpair{
{s.RequireParseRequestURI("https://one-factor.example.com"), s.RequireParseRequestURI("https://auth.example.com/")},
{s.RequireParseRequestURI("https://one-factor.example.com/subpath"), s.RequireParseRequestURI("https://auth.example.com/")},
{s.RequireParseRequestURI("https://one-factor.example2.com"), s.RequireParseRequestURI("https://auth.example2.com/")},
{s.RequireParseRequestURI("https://one-factor.example2.com/subpath"), s.RequireParseRequestURI("https://auth.example2.com/")},
} {
t.Run(pairURI.TargetURI.String(), func(t *testing.T) {
expected := s.RequireParseRequestURI(pairURI.AutheliaURI.String())
authz := s.Builder().Build()
mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close()
for i, cookie := range mock.Ctx.Configuration.Session.Cookies {
mock.Ctx.Configuration.Session.Cookies[i].AutheliaURL = s.RequireParseRequestURI(fmt.Sprintf("https://auth.%s", cookie.Domain))
}
mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil)
s.setRequest(mock.Ctx, method, pairURI.TargetURI, x, x)
mock.Ctx.SetUserValue("authz_path", pairURI.TargetURI.Path)
authz.Handler(mock.Ctx)
assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode())
query := expected.Query()
query.Set(queryArgRD, pairURI.TargetURI.String())
query.Set(queryArgRM, method)
expected.RawQuery = query.Encode()
assert.Equal(t, expected.String(), string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation)))
})
}
})
}
})
}
}
func (s *ExtAuthzAuthzSuite) TestShouldHandleInvalidMethodCharsDeny() {
for _, method := range testRequestMethods {
method += "z"
s.T().Run(fmt.Sprintf("Method%s", method), func(t *testing.T) {
for _, targetURI := range []*url.URL{
s.RequireParseRequestURI("https://bypass.example.com"),
s.RequireParseRequestURI("https://bypass.example.com/subpath"),
s.RequireParseRequestURI("https://bypass.example2.com"),
s.RequireParseRequestURI("https://bypass.example2.com/subpath"),
} {
t.Run(targetURI.String(), func(t *testing.T) {
authz := s.Builder().Build()
mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close()
for i, cookie := range mock.Ctx.Configuration.Session.Cookies {
mock.Ctx.Configuration.Session.Cookies[i].AutheliaURL = s.RequireParseRequestURI(fmt.Sprintf("https://auth.%s", cookie.Domain))
}
mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil)
s.setRequest(mock.Ctx, method, targetURI, true, false)
authz.Handler(mock.Ctx)
assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode())
assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation))
})
}
})
}
}
func (s *ExtAuthzAuthzSuite) TestShouldHandleMissingHostDeny() {
for _, method := range testRequestMethods {
s.T().Run(fmt.Sprintf("Method%s", method), func(t *testing.T) {
authz := s.Builder().Build()
mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close()
for i, cookie := range mock.Ctx.Configuration.Session.Cookies {
mock.Ctx.Configuration.Session.Cookies[i].AutheliaURL = s.RequireParseRequestURI(fmt.Sprintf("https://auth.%s", cookie.Domain))
}
mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil)
s.setRequest(mock.Ctx, method, nil, true, false)
authz.Handler(mock.Ctx)
assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode())
assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation))
})
}
}
func (s *ExtAuthzAuthzSuite) TestShouldHandleAllMethodsAllow() {
for _, method := range testRequestMethods {
s.T().Run(fmt.Sprintf("Method%s", method), func(t *testing.T) {
for _, targetURI := range []*url.URL{
s.RequireParseRequestURI("https://bypass.example.com"),
s.RequireParseRequestURI("https://bypass.example.com/subpath"),
s.RequireParseRequestURI("https://bypass.example2.com"),
s.RequireParseRequestURI("https://bypass.example2.com/subpath"),
} {
t.Run(targetURI.String(), func(t *testing.T) {
authz := s.Builder().Build()
mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close()
for i, cookie := range mock.Ctx.Configuration.Session.Cookies {
mock.Ctx.Configuration.Session.Cookies[i].AutheliaURL = s.RequireParseRequestURI(fmt.Sprintf("https://auth.%s", cookie.Domain))
}
mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil)
s.setRequest(mock.Ctx, method, targetURI, true, false)
authz.Handler(mock.Ctx)
assert.Equal(t, fasthttp.StatusOK, mock.Ctx.Response.StatusCode())
assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation))
})
}
})
}
}
func (s *ExtAuthzAuthzSuite) TestShouldHandleAllMethodsAllowXHR() {
for _, method := range testRequestMethods {
s.T().Run(fmt.Sprintf("Method%s", method), func(t *testing.T) {
for xname, x := range testXHR {
t.Run(xname, func(t *testing.T) {
for _, targetURI := range []*url.URL{
s.RequireParseRequestURI("https://bypass.example.com"),
s.RequireParseRequestURI("https://bypass.example.com/subpath"),
s.RequireParseRequestURI("https://bypass.example2.com"),
s.RequireParseRequestURI("https://bypass.example2.com/subpath"),
} {
t.Run(targetURI.String(), func(t *testing.T) {
authz := s.Builder().Build()
mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close()
for i, cookie := range mock.Ctx.Configuration.Session.Cookies {
mock.Ctx.Configuration.Session.Cookies[i].AutheliaURL = s.RequireParseRequestURI(fmt.Sprintf("https://auth.%s", cookie.Domain))
}
mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil)
s.setRequest(mock.Ctx, method, targetURI, x, x)
authz.Handler(mock.Ctx)
assert.Equal(t, fasthttp.StatusOK, mock.Ctx.Response.StatusCode())
assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation))
})
}
})
}
})
}
}
func (s *ExtAuthzAuthzSuite) TestShouldHandleAllMethodsWithMethodsACL() {
for _, method := range testRequestMethods {
s.T().Run(fmt.Sprintf("Method%s", method), func(t *testing.T) {
for _, methodACL := range testRequestMethods {
targetURI := s.RequireParseRequestURI(fmt.Sprintf("https://bypass-%s.example.com", strings.ToLower(methodACL)))
t.Run(targetURI.String(), func(t *testing.T) {
authz := s.Builder().Build()
mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close()
for i, cookie := range mock.Ctx.Configuration.Session.Cookies {
mock.Ctx.Configuration.Session.Cookies[i].AutheliaURL = s.RequireParseRequestURI(fmt.Sprintf("https://auth.%s", cookie.Domain))
}
mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil)
s.setRequest(mock.Ctx, method, targetURI, true, false)
authz.Handler(mock.Ctx)
if method == methodACL {
assert.Equal(t, fasthttp.StatusOK, mock.Ctx.Response.StatusCode())
assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation))
} else {
expected := s.RequireParseRequestURI("https://auth.example.com/")
switch method {
case fasthttp.MethodGet, fasthttp.MethodOptions, fasthttp.MethodHead:
assert.Equal(t, fasthttp.StatusFound, mock.Ctx.Response.StatusCode())
default:
assert.Equal(t, fasthttp.StatusSeeOther, mock.Ctx.Response.StatusCode())
}
query := expected.Query()
query.Set(queryArgRD, targetURI.String())
query.Set(queryArgRM, method)
expected.RawQuery = query.Encode()
assert.Equal(t, expected.String(), string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation)))
}
})
}
})
}
}
func (s *ExtAuthzAuthzSuite) TestShouldHandleInvalidURLForCVE202132637() {
testCases := []struct {
name string
scheme, host []byte
path string
expected int
}{
{"Should401UnauthorizedWithNullByte",
[]byte("https"), []byte{0, 110, 111, 116, 45, 111, 110, 101, 45, 102, 97, 99, 116, 111, 114, 46, 101, 120, 97, 109, 112, 108, 101, 46, 99, 111, 109}, "/path-example",
fasthttp.StatusUnauthorized,
},
{"Should200OkWithoutNullByte",
[]byte("https"), []byte{110, 111, 116, 45, 111, 110, 101, 45, 102, 97, 99, 116, 111, 114, 46, 101, 120, 97, 109, 112, 108, 101, 46, 99, 111, 109}, "/path-example",
fasthttp.StatusOK,
},
}
for _, tc := range testCases {
s.T().Run(tc.name, func(t *testing.T) {
for _, method := range testRequestMethods {
t.Run(fmt.Sprintf("Method%s", method), func(t *testing.T) {
authz := s.Builder().Build()
mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close()
mock.Ctx.Configuration.AccessControl.DefaultPolicy = testBypass
mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(&mock.Ctx.Configuration)
for i, cookie := range mock.Ctx.Configuration.Session.Cookies {
mock.Ctx.Configuration.Session.Cookies[i].AutheliaURL = s.RequireParseRequestURI(fmt.Sprintf("https://auth.%s", cookie.Domain))
}
mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil)
mock.Ctx.Request.SetHostBytes(tc.host)
mock.Ctx.Request.Header.SetMethodBytes([]byte(method))
mock.Ctx.Request.Header.SetBytesKV([]byte(fasthttp.HeaderXForwardedProto), tc.scheme)
mock.Ctx.Request.Header.Del(fasthttp.HeaderXForwardedHost)
mock.Ctx.Request.Header.Set(fasthttp.HeaderAccept, "text/html; charset=utf-8")
mock.Ctx.SetUserValue("authz_path", tc.path)
authz.Handler(mock.Ctx)
assert.Equal(t, tc.expected, mock.Ctx.Response.StatusCode())
assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation))
})
}
})
}
}
func (s *ExtAuthzAuthzSuite) TestShouldNotHandleAuthRequestAllMethodsAllow() {
for _, method := range testRequestMethods {
s.T().Run(fmt.Sprintf("OriginalMethod%s", method), func(t *testing.T) {
for _, targetURI := range []*url.URL{
s.RequireParseRequestURI("https://bypass.example.com"),
s.RequireParseRequestURI("https://bypass.example.com/subpath"),
s.RequireParseRequestURI("https://bypass.example2.com"),
s.RequireParseRequestURI("https://bypass.example2.com/subpath"),
} {
t.Run(targetURI.String(), func(t *testing.T) {
authz := s.Builder().Build()
mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close()
setRequestAuthRequest(mock.Ctx, method, targetURI, true, false)
authz.Handler(mock.Ctx)
assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode())
assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation))
})
}
})
}
}
func (s *ExtAuthzAuthzSuite) TestShouldNotHandleAuthRequestAllMethodsWithMethodsACL() {
for _, method := range testRequestMethods {
s.T().Run(fmt.Sprintf("Method%s", method), func(t *testing.T) {
for _, methodACL := range testRequestMethods {
targetURI := s.RequireParseRequestURI(fmt.Sprintf("https://bypass-%s.example.com", strings.ToLower(methodACL)))
t.Run(targetURI.String(), func(t *testing.T) {
authz := s.Builder().Build()
mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close()
for i, cookie := range mock.Ctx.Configuration.Session.Cookies {
mock.Ctx.Configuration.Session.Cookies[i].AutheliaURL = s.RequireParseRequestURI(fmt.Sprintf("https://auth.%s", cookie.Domain))
}
mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil)
setRequestAuthRequest(mock.Ctx, method, targetURI, true, false)
authz.Handler(mock.Ctx)
assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode())
assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation))
})
}
})
}
}
func (s *ExtAuthzAuthzSuite) TestShouldNotHandleForwardAuthAllMethodsAllow() {
for _, method := range testRequestMethods {
s.T().Run(fmt.Sprintf("Method%s", method), func(t *testing.T) {
for _, targetURI := range []*url.URL{
s.RequireParseRequestURI("https://bypass.example.com"),
s.RequireParseRequestURI("https://bypass.example.com/subpath"),
s.RequireParseRequestURI("https://bypass.example2.com"),
s.RequireParseRequestURI("https://bypass.example2.com/subpath"),
} {
t.Run(targetURI.String(), func(t *testing.T) {
authz := s.Builder().Build()
mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close()
for i, cookie := range mock.Ctx.Configuration.Session.Cookies {
mock.Ctx.Configuration.Session.Cookies[i].AutheliaURL = s.RequireParseRequestURI(fmt.Sprintf("https://auth.%s", cookie.Domain))
}
mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil)
setRequestForwardAuth(mock.Ctx, method, targetURI, true, false)
authz.Handler(mock.Ctx)
assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode())
assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation))
})
}
})
}
}
func (s *ExtAuthzAuthzSuite) TestShouldNotHandleForwardAuthAllMethodsAllowXHR() {
for _, method := range testRequestMethods {
s.T().Run(fmt.Sprintf("Method%s", method), func(t *testing.T) {
for xname, x := range testXHR {
t.Run(xname, func(t *testing.T) {
for _, targetURI := range []*url.URL{
s.RequireParseRequestURI("https://bypass.example.com"),
s.RequireParseRequestURI("https://bypass.example.com/subpath"),
s.RequireParseRequestURI("https://bypass.example2.com"),
s.RequireParseRequestURI("https://bypass.example2.com/subpath"),
} {
t.Run(targetURI.String(), func(t *testing.T) {
authz := s.Builder().Build()
mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close()
for i, cookie := range mock.Ctx.Configuration.Session.Cookies {
mock.Ctx.Configuration.Session.Cookies[i].AutheliaURL = s.RequireParseRequestURI(fmt.Sprintf("https://auth.%s", cookie.Domain))
}
mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil)
setRequestForwardAuth(mock.Ctx, method, targetURI, x, x)
authz.Handler(mock.Ctx)
assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode())
assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation))
})
}
})
}
})
}
}
func (s *ExtAuthzAuthzSuite) TestShouldNotHandleForwardAuthAllMethodsWithMethodsACL() {
for _, method := range testRequestMethods {
s.T().Run(fmt.Sprintf("Method%s", method), func(t *testing.T) {
for _, methodACL := range testRequestMethods {
targetURI := s.RequireParseRequestURI(fmt.Sprintf("https://bypass-%s.example.com", strings.ToLower(methodACL)))
t.Run(targetURI.String(), func(t *testing.T) {
authz := s.Builder().Build()
mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close()
for i, cookie := range mock.Ctx.Configuration.Session.Cookies {
mock.Ctx.Configuration.Session.Cookies[i].AutheliaURL = s.RequireParseRequestURI(fmt.Sprintf("https://auth.%s", cookie.Domain))
}
mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil)
setRequestForwardAuth(mock.Ctx, method, targetURI, true, false)
authz.Handler(mock.Ctx)
assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode())
assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation))
})
}
})
}
}
func setRequestExtAuthz(ctx *middlewares.AutheliaCtx, method string, targetURI *url.URL, accept, xhr bool) {
ctx.Request.Header.Del(fasthttp.HeaderXForwardedHost)
if method != "" {
ctx.Request.Header.SetMethodBytes([]byte(method))
}
if targetURI != nil {
ctx.Request.SetHost(targetURI.Host)
ctx.Request.Header.Set(fasthttp.HeaderXForwardedProto, targetURI.Scheme)
ctx.SetUserValue("authz_path", targetURI.Path)
}
setRequestXHRValues(ctx, accept, xhr)
}

View File

@ -0,0 +1,55 @@
package handlers
import (
"fmt"
"net/url"
"github.com/valyala/fasthttp"
"github.com/authelia/authelia/v4/internal/authorization"
"github.com/authelia/authelia/v4/internal/middlewares"
)
func handleAuthzGetObjectForwardAuth(ctx *middlewares.AutheliaCtx) (object authorization.Object, err error) {
protocol, host, uri := ctx.XForwardedProto(), ctx.XForwardedHost(), ctx.XForwardedURI()
var (
targetURL *url.URL
method []byte
)
if targetURL, err = getRequestURIFromForwardedHeaders(protocol, host, uri); err != nil {
return object, fmt.Errorf("failed to get target URL: %w", err)
}
if method = ctx.XForwardedMethod(); len(method) == 0 {
return object, fmt.Errorf("header 'X-Forwarded-Method' is empty")
}
if hasInvalidMethodCharacters(method) {
return object, fmt.Errorf("header 'X-Forwarded-Method' with value '%s' has invalid characters", method)
}
return authorization.NewObjectRaw(targetURL, method), nil
}
func handleAuthzUnauthorizedForwardAuth(ctx *middlewares.AutheliaCtx, authn *Authn, redirectionURL *url.URL) {
var (
statusCode int
)
switch {
case ctx.IsXHR() || !ctx.AcceptsMIME("text/html"):
statusCode = fasthttp.StatusUnauthorized
default:
switch authn.Object.Method {
case fasthttp.MethodGet, fasthttp.MethodOptions, fasthttp.MethodHead:
statusCode = fasthttp.StatusFound
default:
statusCode = fasthttp.StatusSeeOther
}
}
ctx.Logger.Infof("Access to %s (method %s) is not authorized to user %s, responding with status code %d with location redirect to %s", authn.Object.String(), authn.Method, authn.Username, statusCode, redirectionURL)
ctx.SpecialRedirect(redirectionURL.String(), statusCode)
}

View File

@ -0,0 +1,610 @@
package handlers
import (
"fmt"
"net/url"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
"github.com/valyala/fasthttp"
"github.com/authelia/authelia/v4/internal/authorization"
"github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/mocks"
"github.com/authelia/authelia/v4/internal/session"
)
func TestRunForwardAuthAuthzSuite(t *testing.T) {
suite.Run(t, NewForwardAuthAuthzSuite())
}
func NewForwardAuthAuthzSuite() *ForwardAuthAuthzSuite {
return &ForwardAuthAuthzSuite{
AuthzSuite: &AuthzSuite{
implementation: AuthzImplForwardAuth,
setRequest: setRequestForwardAuth,
},
}
}
type ForwardAuthAuthzSuite struct {
*AuthzSuite
}
func (s *ForwardAuthAuthzSuite) TestShouldHandleAllMethodsDeny() {
for _, method := range testRequestMethods {
s.T().Run(fmt.Sprintf("Method%s", method), func(t *testing.T) {
for _, pairURI := range []urlpair{
{s.RequireParseRequestURI("https://one-factor.example.com"), s.RequireParseRequestURI("https://auth.example.com/")},
{s.RequireParseRequestURI("https://one-factor.example.com/subpath"), s.RequireParseRequestURI("https://auth.example.com/")},
{s.RequireParseRequestURI("https://one-factor.example2.com"), s.RequireParseRequestURI("https://auth.example2.com/")},
{s.RequireParseRequestURI("https://one-factor.example2.com/subpath"), s.RequireParseRequestURI("https://auth.example2.com/")},
} {
t.Run(pairURI.TargetURI.String(), func(t *testing.T) {
expected := s.RequireParseRequestURI(pairURI.AutheliaURI.String())
authz := s.Builder().Build()
mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close()
for i, cookie := range mock.Ctx.Configuration.Session.Cookies {
mock.Ctx.Configuration.Session.Cookies[i].AutheliaURL = s.RequireParseRequestURI(fmt.Sprintf("https://auth.%s", cookie.Domain))
}
mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil)
s.setRequest(mock.Ctx, method, pairURI.TargetURI, true, false)
authz.Handler(mock.Ctx)
switch method {
case fasthttp.MethodGet, fasthttp.MethodOptions, fasthttp.MethodHead:
assert.Equal(t, fasthttp.StatusFound, mock.Ctx.Response.StatusCode())
default:
assert.Equal(t, fasthttp.StatusSeeOther, mock.Ctx.Response.StatusCode())
}
query := expected.Query()
query.Set(queryArgRD, pairURI.TargetURI.String())
query.Set(queryArgRM, method)
expected.RawQuery = query.Encode()
assert.Equal(t, expected.String(), string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation)))
})
}
})
}
}
func (s *ForwardAuthAuthzSuite) TestShouldHandleAllMethodsOverrideAutheliaURLDeny() {
for _, method := range testRequestMethods {
s.T().Run(fmt.Sprintf("Method%s", method), func(t *testing.T) {
for _, pairURI := range []urlpair{
{s.RequireParseRequestURI("https://one-factor.example.com"), s.RequireParseRequestURI("https://auth-from-override.example.com/")},
{s.RequireParseRequestURI("https://one-factor.example.com/subpath"), s.RequireParseRequestURI("https://auth-from-override.example.com/")},
{s.RequireParseRequestURI("https://one-factor.example2.com"), s.RequireParseRequestURI("https://auth-from-override.example2.com/")},
{s.RequireParseRequestURI("https://one-factor.example2.com/subpath"), s.RequireParseRequestURI("https://auth-from-override.example2.com/")},
} {
t.Run(pairURI.TargetURI.String(), func(t *testing.T) {
expected := s.RequireParseRequestURI(pairURI.AutheliaURI.String())
authz := s.Builder().Build()
mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close()
for i, cookie := range mock.Ctx.Configuration.Session.Cookies {
mock.Ctx.Configuration.Session.Cookies[i].AutheliaURL = s.RequireParseRequestURI(fmt.Sprintf("https://auth.%s", cookie.Domain))
}
mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil)
mock.Ctx.RequestCtx.QueryArgs().Set("authelia_url", pairURI.AutheliaURI.String())
s.setRequest(mock.Ctx, method, pairURI.TargetURI, true, false)
authz.Handler(mock.Ctx)
switch method {
case fasthttp.MethodGet, fasthttp.MethodOptions, fasthttp.MethodHead:
assert.Equal(t, fasthttp.StatusFound, mock.Ctx.Response.StatusCode())
default:
assert.Equal(t, fasthttp.StatusSeeOther, mock.Ctx.Response.StatusCode())
}
query := expected.Query()
query.Set(queryArgRD, pairURI.TargetURI.String())
query.Set(queryArgRM, method)
expected.RawQuery = query.Encode()
assert.Equal(t, expected.String(), string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation)))
})
}
})
}
}
func (s *ForwardAuthAuthzSuite) TestShouldHandleAllMethodsMissingAutheliaURLDeny() {
for _, method := range testRequestMethods {
s.T().Run(fmt.Sprintf("Method%s", method), func(t *testing.T) {
for _, targetURI := range []*url.URL{
s.RequireParseRequestURI("https://bypass.example.com"),
s.RequireParseRequestURI("https://bypass.example.com/subpath"),
s.RequireParseRequestURI("https://bypass.example2.com"),
s.RequireParseRequestURI("https://bypass.example2.com/subpath"),
} {
t.Run(targetURI.String(), func(t *testing.T) {
authz := s.Builder().Build()
mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close()
s.setRequest(mock.Ctx, method, targetURI, true, false)
authz.Handler(mock.Ctx)
assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode())
assert.Equal(t, "", string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation)))
})
}
})
}
}
func (s *ForwardAuthAuthzSuite) TestShouldHandleAllMethodsXHRDeny() {
for _, method := range testRequestMethods {
s.T().Run(fmt.Sprintf("Method%s", method), func(t *testing.T) {
for xname, x := range testXHR {
t.Run(xname, func(t *testing.T) {
for _, pairURI := range []urlpair{
{s.RequireParseRequestURI("https://one-factor.example.com"), s.RequireParseRequestURI("https://auth.example.com/")},
{s.RequireParseRequestURI("https://one-factor.example.com/subpath"), s.RequireParseRequestURI("https://auth.example.com/")},
{s.RequireParseRequestURI("https://one-factor.example2.com"), s.RequireParseRequestURI("https://auth.example2.com/")},
{s.RequireParseRequestURI("https://one-factor.example2.com/subpath"), s.RequireParseRequestURI("https://auth.example2.com/")},
} {
t.Run(pairURI.TargetURI.String(), func(t *testing.T) {
expected := s.RequireParseRequestURI(pairURI.AutheliaURI.String())
authz := s.Builder().Build()
mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close()
for i, cookie := range mock.Ctx.Configuration.Session.Cookies {
mock.Ctx.Configuration.Session.Cookies[i].AutheliaURL = s.RequireParseRequestURI(fmt.Sprintf("https://auth.%s", cookie.Domain))
}
mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil)
s.setRequest(mock.Ctx, method, pairURI.TargetURI, x, x)
authz.Handler(mock.Ctx)
assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode())
query := expected.Query()
query.Set(queryArgRD, pairURI.TargetURI.String())
query.Set(queryArgRM, method)
expected.RawQuery = query.Encode()
assert.Equal(t, expected.String(), string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation)))
})
}
})
}
})
}
}
func (s *ForwardAuthAuthzSuite) TestShouldHandleInvalidMethodCharsDeny() {
for _, method := range testRequestMethods {
method += "z"
s.T().Run(fmt.Sprintf("Method%s", method), func(t *testing.T) {
for _, targetURI := range []*url.URL{
s.RequireParseRequestURI("https://bypass.example.com"),
s.RequireParseRequestURI("https://bypass.example.com/subpath"),
s.RequireParseRequestURI("https://bypass.example2.com"),
s.RequireParseRequestURI("https://bypass.example2.com/subpath"),
} {
t.Run(targetURI.String(), func(t *testing.T) {
authz := s.Builder().Build()
mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close()
for i, cookie := range mock.Ctx.Configuration.Session.Cookies {
mock.Ctx.Configuration.Session.Cookies[i].AutheliaURL = s.RequireParseRequestURI(fmt.Sprintf("https://auth.%s", cookie.Domain))
}
mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil)
s.setRequest(mock.Ctx, method, targetURI, true, false)
authz.Handler(mock.Ctx)
assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode())
assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation))
})
}
})
}
}
func (s *ForwardAuthAuthzSuite) TestShouldHandleMissingHostDeny() {
for _, method := range testRequestMethods {
s.T().Run(fmt.Sprintf("Method%s", method), func(t *testing.T) {
authz := s.Builder().Build()
mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close()
for i, cookie := range mock.Ctx.Configuration.Session.Cookies {
mock.Ctx.Configuration.Session.Cookies[i].AutheliaURL = s.RequireParseRequestURI(fmt.Sprintf("https://auth.%s", cookie.Domain))
}
mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil)
mock.Ctx.Request.Header.Set("X-Forwarded-Method", method)
mock.Ctx.Request.Header.Set(fasthttp.HeaderXForwardedProto, "https")
mock.Ctx.Request.Header.Del(fasthttp.HeaderXForwardedHost)
mock.Ctx.Request.Header.Set("X-Forwarded-Uri", "/")
mock.Ctx.Request.Header.Set(fasthttp.HeaderAccept, "text/html; charset=utf-8")
authz.Handler(mock.Ctx)
assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode())
assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation))
})
}
}
func (s *ForwardAuthAuthzSuite) TestShouldHandleAllMethodsAllow() {
for _, method := range testRequestMethods {
s.T().Run(fmt.Sprintf("Method%s", method), func(t *testing.T) {
for _, targetURI := range []*url.URL{
s.RequireParseRequestURI("https://bypass.example.com"),
s.RequireParseRequestURI("https://bypass.example.com/subpath"),
s.RequireParseRequestURI("https://bypass.example2.com"),
s.RequireParseRequestURI("https://bypass.example2.com/subpath"),
} {
t.Run(targetURI.String(), func(t *testing.T) {
authz := s.Builder().Build()
mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close()
for i, cookie := range mock.Ctx.Configuration.Session.Cookies {
mock.Ctx.Configuration.Session.Cookies[i].AutheliaURL = s.RequireParseRequestURI(fmt.Sprintf("https://auth.%s", cookie.Domain))
}
mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil)
s.setRequest(mock.Ctx, method, targetURI, true, false)
authz.Handler(mock.Ctx)
assert.Equal(t, fasthttp.StatusOK, mock.Ctx.Response.StatusCode())
assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation))
})
}
})
}
}
func (s *ForwardAuthAuthzSuite) TestShouldHandleAllMethodsWithMethodsACL() {
for _, method := range testRequestMethods {
s.T().Run(fmt.Sprintf("Method%s", method), func(t *testing.T) {
for _, methodACL := range testRequestMethods {
targetURI := s.RequireParseRequestURI(fmt.Sprintf("https://bypass-%s.example.com", strings.ToLower(methodACL)))
t.Run(targetURI.String(), func(t *testing.T) {
authz := s.Builder().Build()
mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close()
for i, cookie := range mock.Ctx.Configuration.Session.Cookies {
mock.Ctx.Configuration.Session.Cookies[i].AutheliaURL = s.RequireParseRequestURI(fmt.Sprintf("https://auth.%s", cookie.Domain))
}
mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil)
s.setRequest(mock.Ctx, method, targetURI, true, false)
authz.Handler(mock.Ctx)
if method == methodACL {
assert.Equal(t, fasthttp.StatusOK, mock.Ctx.Response.StatusCode())
assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation))
} else {
expected := s.RequireParseRequestURI("https://auth.example.com/")
switch method {
case fasthttp.MethodGet, fasthttp.MethodOptions, fasthttp.MethodHead:
assert.Equal(t, fasthttp.StatusFound, mock.Ctx.Response.StatusCode())
default:
assert.Equal(t, fasthttp.StatusSeeOther, mock.Ctx.Response.StatusCode())
}
query := expected.Query()
query.Set(queryArgRD, targetURI.String())
query.Set(queryArgRM, method)
expected.RawQuery = query.Encode()
assert.Equal(t, expected.String(), string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation)))
}
})
}
})
}
}
func (s *ForwardAuthAuthzSuite) TestShouldHandleAllMethodsAllowXHR() {
for _, method := range testRequestMethods {
s.T().Run(fmt.Sprintf("Method%s", method), func(t *testing.T) {
for _, targetURI := range []*url.URL{
s.RequireParseRequestURI("https://bypass.example.com"),
s.RequireParseRequestURI("https://bypass.example.com/subpath"),
s.RequireParseRequestURI("https://bypass.example2.com"),
s.RequireParseRequestURI("https://bypass.example2.com/subpath"),
} {
t.Run(targetURI.String(), func(t *testing.T) {
authz := s.Builder().Build()
mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close()
for i, cookie := range mock.Ctx.Configuration.Session.Cookies {
mock.Ctx.Configuration.Session.Cookies[i].AutheliaURL = s.RequireParseRequestURI(fmt.Sprintf("https://auth.%s", cookie.Domain))
}
mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil)
s.setRequest(mock.Ctx, method, targetURI, true, true)
authz.Handler(mock.Ctx)
assert.Equal(t, fasthttp.StatusOK, mock.Ctx.Response.StatusCode())
assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation))
})
}
})
}
}
func (s *ForwardAuthAuthzSuite) TestShouldHandleInvalidURLForCVE202132637() {
testCases := []struct {
name string
scheme, host []byte
path string
expected int
}{
{"Should401UnauthorizedWithNullByte",
[]byte("https"), []byte{0, 110, 111, 116, 45, 111, 110, 101, 45, 102, 97, 99, 116, 111, 114, 46, 101, 120, 97, 109, 112, 108, 101, 46, 99, 111, 109}, "/path-example",
fasthttp.StatusUnauthorized,
},
{"Should200OkWithoutNullByte",
[]byte("https"), []byte{110, 111, 116, 45, 111, 110, 101, 45, 102, 97, 99, 116, 111, 114, 46, 101, 120, 97, 109, 112, 108, 101, 46, 99, 111, 109}, "/path-example",
fasthttp.StatusOK,
},
}
for _, tc := range testCases {
s.T().Run(tc.name, func(t *testing.T) {
for _, method := range testRequestMethods {
t.Run(fmt.Sprintf("Method%s", method), func(t *testing.T) {
authz := s.Builder().Build()
mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close()
mock.Ctx.Configuration.AccessControl.DefaultPolicy = testBypass
mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(&mock.Ctx.Configuration)
for i, cookie := range mock.Ctx.Configuration.Session.Cookies {
mock.Ctx.Configuration.Session.Cookies[i].AutheliaURL = s.RequireParseRequestURI(fmt.Sprintf("https://auth.%s", cookie.Domain))
}
mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil)
mock.Ctx.Request.Header.Set("X-Forwarded-Method", method)
mock.Ctx.Request.Header.SetBytesKV([]byte(fasthttp.HeaderXForwardedProto), tc.scheme)
mock.Ctx.Request.Header.SetBytesKV([]byte(fasthttp.HeaderXForwardedHost), tc.host)
mock.Ctx.Request.Header.Set("X-Forwarded-Uri", tc.path)
mock.Ctx.Request.Header.Set(fasthttp.HeaderAccept, "text/html; charset=utf-8")
authz.Handler(mock.Ctx)
assert.Equal(t, tc.expected, mock.Ctx.Response.StatusCode())
assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation))
})
}
})
}
}
func (s *ForwardAuthAuthzSuite) TestShouldNotHandleAuthRequestAllMethodsAllow() {
for _, method := range testRequestMethods {
s.T().Run(fmt.Sprintf("OriginalMethod%s", method), func(t *testing.T) {
for _, targetURI := range []*url.URL{
s.RequireParseRequestURI("https://bypass.example.com"),
s.RequireParseRequestURI("https://bypass.example.com/subpath"),
s.RequireParseRequestURI("https://bypass.example2.com"),
s.RequireParseRequestURI("https://bypass.example2.com/subpath"),
} {
t.Run(targetURI.String(), func(t *testing.T) {
authz := s.Builder().Build()
mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close()
setRequestAuthRequest(mock.Ctx, method, targetURI, true, false)
authz.Handler(mock.Ctx)
assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode())
assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation))
})
}
})
}
}
func (s *ForwardAuthAuthzSuite) TestShouldNotHandleAuthRequestAllMethodsWithMethodsACL() {
for _, method := range testRequestMethods {
s.T().Run(fmt.Sprintf("Method%s", method), func(t *testing.T) {
for _, methodACL := range testRequestMethods {
targetURI := s.RequireParseRequestURI(fmt.Sprintf("https://bypass-%s.example.com", strings.ToLower(methodACL)))
t.Run(targetURI.String(), func(t *testing.T) {
authz := s.Builder().Build()
mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close()
for i, cookie := range mock.Ctx.Configuration.Session.Cookies {
mock.Ctx.Configuration.Session.Cookies[i].AutheliaURL = s.RequireParseRequestURI(fmt.Sprintf("https://auth.%s", cookie.Domain))
}
mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil)
setRequestAuthRequest(mock.Ctx, method, targetURI, true, false)
authz.Handler(mock.Ctx)
assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode())
assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation))
})
}
})
}
}
func (s *ForwardAuthAuthzSuite) TestShouldNotHandleExtAuthzAllMethodsAllow() {
for _, method := range testRequestMethods {
s.T().Run(fmt.Sprintf("Method%s", method), func(t *testing.T) {
for _, targetURI := range []*url.URL{
s.RequireParseRequestURI("https://bypass.example.com"),
s.RequireParseRequestURI("https://bypass.example.com/subpath"),
s.RequireParseRequestURI("https://bypass.example2.com"),
s.RequireParseRequestURI("https://bypass.example2.com/subpath"),
} {
t.Run(targetURI.String(), func(t *testing.T) {
authz := s.Builder().Build()
mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close()
for i, cookie := range mock.Ctx.Configuration.Session.Cookies {
mock.Ctx.Configuration.Session.Cookies[i].AutheliaURL = s.RequireParseRequestURI(fmt.Sprintf("https://auth.%s", cookie.Domain))
}
mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil)
setRequestExtAuthz(mock.Ctx, method, targetURI, true, false)
authz.Handler(mock.Ctx)
assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode())
assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation))
})
}
})
}
}
func (s *ForwardAuthAuthzSuite) TestShouldNotHandleExtAuthzAllMethodsAllowXHR() {
for _, method := range testRequestMethods {
s.T().Run(fmt.Sprintf("Method%s", method), func(t *testing.T) {
for xname, x := range testXHR {
t.Run(xname, func(t *testing.T) {
for _, targetURI := range []*url.URL{
s.RequireParseRequestURI("https://bypass.example.com"),
s.RequireParseRequestURI("https://bypass.example.com/subpath"),
s.RequireParseRequestURI("https://bypass.example2.com"),
s.RequireParseRequestURI("https://bypass.example2.com/subpath"),
} {
t.Run(targetURI.String(), func(t *testing.T) {
authz := s.Builder().Build()
mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close()
for i, cookie := range mock.Ctx.Configuration.Session.Cookies {
mock.Ctx.Configuration.Session.Cookies[i].AutheliaURL = s.RequireParseRequestURI(fmt.Sprintf("https://auth.%s", cookie.Domain))
}
mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil)
setRequestExtAuthz(mock.Ctx, method, targetURI, x, x)
authz.Handler(mock.Ctx)
assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode())
assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation))
})
}
})
}
})
}
}
func (s *ForwardAuthAuthzSuite) TestShouldNotHandleExtAuthzAllMethodsWithMethodsACL() {
for _, method := range testRequestMethods {
s.T().Run(fmt.Sprintf("Method%s", method), func(t *testing.T) {
for _, methodACL := range testRequestMethods {
targetURI := s.RequireParseRequestURI(fmt.Sprintf("https://bypass-%s.example.com", strings.ToLower(methodACL)))
t.Run(targetURI.String(), func(t *testing.T) {
authz := s.Builder().Build()
mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close()
for i, cookie := range mock.Ctx.Configuration.Session.Cookies {
mock.Ctx.Configuration.Session.Cookies[i].AutheliaURL = s.RequireParseRequestURI(fmt.Sprintf("https://auth.%s", cookie.Domain))
}
mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil)
setRequestExtAuthz(mock.Ctx, method, targetURI, true, false)
authz.Handler(mock.Ctx)
assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode())
assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation))
})
}
})
}
}
func setRequestForwardAuth(ctx *middlewares.AutheliaCtx, method string, targetURI *url.URL, accept, xhr bool) {
if method != "" {
ctx.Request.Header.Set("X-Forwarded-Method", method)
}
if targetURI != nil {
ctx.Request.Header.Set(fasthttp.HeaderXForwardedProto, targetURI.Scheme)
ctx.Request.Header.Set(fasthttp.HeaderXForwardedHost, targetURI.Host)
ctx.Request.Header.Set("X-Forwarded-Uri", targetURI.Path)
}
setRequestXHRValues(ctx, accept, xhr)
}

View File

@ -0,0 +1,64 @@
package handlers
import (
"fmt"
"net/url"
"github.com/valyala/fasthttp"
"github.com/authelia/authelia/v4/internal/authorization"
"github.com/authelia/authelia/v4/internal/middlewares"
)
func handleAuthzGetObjectLegacy(ctx *middlewares.AutheliaCtx) (object authorization.Object, err error) {
var (
targetURL *url.URL
method []byte
)
if targetURL, err = ctx.GetXOriginalURLOrXForwardedURL(); err != nil {
return object, fmt.Errorf("failed to get target URL: %w", err)
}
if method = ctx.XForwardedMethod(); len(method) == 0 {
method = ctx.Method()
}
if hasInvalidMethodCharacters(method) {
return object, fmt.Errorf("header 'X-Forwarded-Method' with value '%s' has invalid characters", method)
}
return authorization.NewObjectRaw(targetURL, method), nil
}
func handleAuthzUnauthorizedLegacy(ctx *middlewares.AutheliaCtx, authn *Authn, redirectionURL *url.URL) {
var (
statusCode int
)
if authn.Type == AuthnTypeAuthorization {
handleAuthzUnauthorizedAuthorizationBasic(ctx, authn)
return
}
switch {
case ctx.IsXHR() || !ctx.AcceptsMIME("text/html") || redirectionURL == nil:
statusCode = fasthttp.StatusUnauthorized
default:
switch authn.Object.Method {
case fasthttp.MethodGet, fasthttp.MethodOptions, "":
statusCode = fasthttp.StatusFound
default:
statusCode = fasthttp.StatusSeeOther
}
}
if redirectionURL != nil {
ctx.Logger.Infof("Access to %s (method %s) is not authorized to user %s, responding with status code %d with location redirect to %s", authn.Object.URL.String(), authn.Method, authn.Username, statusCode, redirectionURL.String())
ctx.SpecialRedirect(redirectionURL.String(), statusCode)
} else {
ctx.Logger.Infof("Access to %s (method %s) is not authorized to user %s, responding with status code %d", authn.Object.URL.String(), authn.Method, authn.Username, statusCode)
ctx.ReplyUnauthorized()
}
}

View File

@ -0,0 +1,555 @@
package handlers
import (
"fmt"
"net/url"
"regexp"
"testing"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
"github.com/valyala/fasthttp"
"github.com/authelia/authelia/v4/internal/authentication"
"github.com/authelia/authelia/v4/internal/authorization"
"github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/mocks"
"github.com/authelia/authelia/v4/internal/session"
)
func TestRunLegacyAuthzSuite(t *testing.T) {
suite.Run(t, NewLegacyAuthzSuite())
}
func NewLegacyAuthzSuite() *LegacyAuthzSuite {
return &LegacyAuthzSuite{
AuthzSuite: &AuthzSuite{
implementation: AuthzImplLegacy,
setRequest: setRequestLegacy,
},
}
}
type LegacyAuthzSuite struct {
*AuthzSuite
}
func (s *LegacyAuthzSuite) TestShouldHandleAllMethodsDeny() {
for _, method := range testRequestMethods {
s.T().Run(fmt.Sprintf("Method%s", method), func(t *testing.T) {
for _, pairURI := range []urlpair{
{s.RequireParseRequestURI("https://one-factor.example.com/"), s.RequireParseRequestURI("https://auth.example.com/")},
{s.RequireParseRequestURI("https://one-factor.example.com/subpath"), s.RequireParseRequestURI("https://auth.example.com/")},
{s.RequireParseRequestURI("https://one-factor.example2.com/"), s.RequireParseRequestURI("https://auth.example2.com/")},
{s.RequireParseRequestURI("https://one-factor.example2.com/subpath"), s.RequireParseRequestURI("https://auth.example2.com/")},
} {
t.Run(pairURI.TargetURI.String(), func(t *testing.T) {
expected := s.RequireParseRequestURI(pairURI.AutheliaURI.String())
authz := s.Builder().Build()
mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close()
for i, cookie := range mock.Ctx.Configuration.Session.Cookies {
mock.Ctx.Configuration.Session.Cookies[i].AutheliaURL = s.RequireParseRequestURI(fmt.Sprintf("https://auth.%s", cookie.Domain))
}
mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil)
mock.Ctx.RequestCtx.QueryArgs().Set(queryArgRD, pairURI.AutheliaURI.String())
mock.Ctx.Request.Header.Set("X-Forwarded-Method", method)
mock.Ctx.Request.Header.Set(fasthttp.HeaderXForwardedProto, pairURI.TargetURI.Scheme)
mock.Ctx.Request.Header.Set(fasthttp.HeaderXForwardedHost, pairURI.TargetURI.Host)
mock.Ctx.Request.Header.Set("X-Forwarded-Uri", pairURI.TargetURI.Path)
mock.Ctx.Request.Header.Set(fasthttp.HeaderAccept, "text/html; charset=utf-8")
authz.Handler(mock.Ctx)
switch method {
case fasthttp.MethodGet, fasthttp.MethodOptions:
assert.Equal(t, fasthttp.StatusFound, mock.Ctx.Response.StatusCode())
default:
assert.Equal(t, fasthttp.StatusSeeOther, mock.Ctx.Response.StatusCode())
}
query := expected.Query()
query.Set(queryArgRD, pairURI.TargetURI.String())
query.Set(queryArgRM, method)
expected.RawQuery = query.Encode()
assert.Equal(t, expected.String(), string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation)))
})
}
})
}
}
func (s *LegacyAuthzSuite) TestShouldHandleAllMethodsOverrideAutheliaURLDeny() {
for _, method := range testRequestMethods {
s.T().Run(fmt.Sprintf("Method%s", method), func(t *testing.T) {
for _, pairURI := range []urlpair{
{s.RequireParseRequestURI("https://one-factor.example.com/"), s.RequireParseRequestURI("https://auth-from-override.example.com/")},
{s.RequireParseRequestURI("https://one-factor.example.com/subpath"), s.RequireParseRequestURI("https://auth-from-override.example.com/")},
{s.RequireParseRequestURI("https://one-factor.example2.com/"), s.RequireParseRequestURI("https://auth-from-override.example2.com/")},
{s.RequireParseRequestURI("https://one-factor.example2.com/subpath"), s.RequireParseRequestURI("https://auth-from-override.example2.com/")},
} {
t.Run(pairURI.TargetURI.String(), func(t *testing.T) {
expected := s.RequireParseRequestURI(pairURI.AutheliaURI.String())
authz := s.Builder().Build()
mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close()
for i, cookie := range mock.Ctx.Configuration.Session.Cookies {
mock.Ctx.Configuration.Session.Cookies[i].AutheliaURL = s.RequireParseRequestURI(fmt.Sprintf("https://auth.%s", cookie.Domain))
}
mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil)
mock.Ctx.RequestCtx.QueryArgs().Set(queryArgRD, pairURI.AutheliaURI.String())
mock.Ctx.Request.Header.Set("X-Forwarded-Method", method)
mock.Ctx.Request.Header.Set(fasthttp.HeaderXForwardedProto, pairURI.TargetURI.Scheme)
mock.Ctx.Request.Header.Set(fasthttp.HeaderXForwardedHost, pairURI.TargetURI.Host)
mock.Ctx.Request.Header.Set("X-Forwarded-Uri", pairURI.TargetURI.Path)
mock.Ctx.Request.Header.Set(fasthttp.HeaderAccept, "text/html; charset=utf-8")
authz.Handler(mock.Ctx)
switch method {
case fasthttp.MethodGet, fasthttp.MethodOptions:
assert.Equal(t, fasthttp.StatusFound, mock.Ctx.Response.StatusCode())
default:
assert.Equal(t, fasthttp.StatusSeeOther, mock.Ctx.Response.StatusCode())
}
query := expected.Query()
query.Set(queryArgRD, pairURI.TargetURI.String())
query.Set(queryArgRM, method)
expected.RawQuery = query.Encode()
assert.Equal(t, expected.String(), string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation)))
})
}
})
}
}
func (s *LegacyAuthzSuite) TestShouldHandleAllMethodsMissingAutheliaURLDeny() {
for _, method := range testRequestMethods {
s.T().Run(fmt.Sprintf("Method%s", method), func(t *testing.T) {
for _, targetURI := range []*url.URL{
s.RequireParseRequestURI("https://bypass.example.com"),
s.RequireParseRequestURI("https://bypass.example.com/subpath"),
s.RequireParseRequestURI("https://bypass.example2.com"),
s.RequireParseRequestURI("https://bypass.example2.com/subpath"),
} {
t.Run(targetURI.String(), func(t *testing.T) {
authz := s.Builder().Build()
mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close()
mock.Ctx.Request.Header.Set("X-Forwarded-Method", method)
mock.Ctx.Request.Header.Set(fasthttp.HeaderXForwardedProto, targetURI.Scheme)
mock.Ctx.Request.Header.Set(fasthttp.HeaderXForwardedHost, targetURI.Host)
mock.Ctx.Request.Header.Set("X-Forwarded-Uri", targetURI.Path)
mock.Ctx.Request.Header.Set(fasthttp.HeaderAccept, "text/html; charset=utf-8")
authz.Handler(mock.Ctx)
assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode())
assert.Equal(t, "", string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation)))
})
}
})
}
}
func (s *LegacyAuthzSuite) TestShouldHandleAllMethodsXHRDeny() {
for _, method := range testRequestMethods {
s.T().Run(fmt.Sprintf("Method%s", method), func(t *testing.T) {
for xname, x := range testXHR {
t.Run(xname, func(t *testing.T) {
for _, pairURI := range []urlpair{
{s.RequireParseRequestURI("https://one-factor.example.com/"), s.RequireParseRequestURI("https://auth.example.com/")},
{s.RequireParseRequestURI("https://one-factor.example.com/subpath"), s.RequireParseRequestURI("https://auth.example.com/")},
{s.RequireParseRequestURI("https://one-factor.example2.com/"), s.RequireParseRequestURI("https://auth.example2.com/")},
{s.RequireParseRequestURI("https://one-factor.example2.com/subpath"), s.RequireParseRequestURI("https://auth.example2.com/")},
} {
t.Run(pairURI.TargetURI.String(), func(t *testing.T) {
expected := s.RequireParseRequestURI(pairURI.AutheliaURI.String())
authz := s.Builder().Build()
mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close()
for i, cookie := range mock.Ctx.Configuration.Session.Cookies {
mock.Ctx.Configuration.Session.Cookies[i].AutheliaURL = s.RequireParseRequestURI(fmt.Sprintf("https://auth.%s", cookie.Domain))
}
mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil)
mock.Ctx.RequestCtx.QueryArgs().Set(queryArgRD, pairURI.AutheliaURI.String())
mock.Ctx.Request.Header.Set("X-Forwarded-Method", method)
mock.Ctx.Request.Header.Set(fasthttp.HeaderXForwardedProto, pairURI.TargetURI.Scheme)
mock.Ctx.Request.Header.Set(fasthttp.HeaderXForwardedHost, pairURI.TargetURI.Host)
mock.Ctx.Request.Header.Set("X-Forwarded-Uri", pairURI.TargetURI.Path)
if x {
mock.Ctx.Request.Header.Set(fasthttp.HeaderAccept, "text/html; charset=utf-8")
mock.Ctx.Request.Header.Set(fasthttp.HeaderXRequestedWith, "XMLHttpRequest")
}
authz.Handler(mock.Ctx)
assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode())
query := expected.Query()
query.Set(queryArgRD, pairURI.TargetURI.String())
query.Set(queryArgRM, method)
expected.RawQuery = query.Encode()
assert.Equal(t, expected.String(), string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation)))
})
}
})
}
})
}
}
func (s *LegacyAuthzSuite) TestShouldHandleInvalidMethodCharsDeny() {
for _, method := range testRequestMethods {
method += "z"
s.T().Run(fmt.Sprintf("Method%s", method), func(t *testing.T) {
for _, targetURI := range []*url.URL{
s.RequireParseRequestURI("https://bypass.example.com"),
s.RequireParseRequestURI("https://bypass.example.com/subpath"),
s.RequireParseRequestURI("https://bypass.example2.com"),
s.RequireParseRequestURI("https://bypass.example2.com/subpath"),
} {
t.Run(targetURI.String(), func(t *testing.T) {
authz := s.Builder().Build()
mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close()
for i, cookie := range mock.Ctx.Configuration.Session.Cookies {
mock.Ctx.Configuration.Session.Cookies[i].AutheliaURL = s.RequireParseRequestURI(fmt.Sprintf("https://auth.%s", cookie.Domain))
}
mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil)
mock.Ctx.Request.Header.Set("X-Forwarded-Method", method)
mock.Ctx.Request.Header.Set(fasthttp.HeaderXForwardedProto, targetURI.Scheme)
mock.Ctx.Request.Header.Set(fasthttp.HeaderXForwardedHost, targetURI.Host)
mock.Ctx.Request.Header.Set("X-Forwarded-Uri", targetURI.Path)
mock.Ctx.Request.Header.Set(fasthttp.HeaderAccept, "text/html; charset=utf-8")
authz.Handler(mock.Ctx)
assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode())
assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation))
})
}
})
}
}
func (s *LegacyAuthzSuite) TestShouldHandleMissingHostDeny() {
for _, method := range testRequestMethods {
s.T().Run(fmt.Sprintf("Method%s", method), func(t *testing.T) {
authz := s.Builder().Build()
mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close()
for i, cookie := range mock.Ctx.Configuration.Session.Cookies {
mock.Ctx.Configuration.Session.Cookies[i].AutheliaURL = s.RequireParseRequestURI(fmt.Sprintf("https://auth.%s", cookie.Domain))
}
mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil)
mock.Ctx.Request.Header.Set("X-Forwarded-Method", method)
mock.Ctx.Request.Header.Set(fasthttp.HeaderXForwardedProto, "https")
mock.Ctx.Request.Header.Del(fasthttp.HeaderXForwardedHost)
mock.Ctx.Request.Header.Set("X-Forwarded-Uri", "/")
mock.Ctx.Request.Header.Set(fasthttp.HeaderAccept, "text/html; charset=utf-8")
authz.Handler(mock.Ctx)
assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode())
assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation))
})
}
}
func (s *LegacyAuthzSuite) TestShouldHandleAllMethodsAllow() {
for _, method := range testRequestMethods {
s.T().Run(fmt.Sprintf("Method%s", method), func(t *testing.T) {
for _, targetURI := range []*url.URL{
s.RequireParseRequestURI("https://bypass.example.com"),
s.RequireParseRequestURI("https://bypass.example.com/subpath"),
s.RequireParseRequestURI("https://bypass.example2.com"),
s.RequireParseRequestURI("https://bypass.example2.com/subpath"),
} {
t.Run(targetURI.String(), func(t *testing.T) {
authz := s.Builder().Build()
mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close()
for i, cookie := range mock.Ctx.Configuration.Session.Cookies {
mock.Ctx.Configuration.Session.Cookies[i].AutheliaURL = s.RequireParseRequestURI(fmt.Sprintf("https://auth.%s", cookie.Domain))
}
mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil)
mock.Ctx.Request.Header.Set("X-Forwarded-Method", method)
mock.Ctx.Request.Header.Set(fasthttp.HeaderXForwardedProto, targetURI.Scheme)
mock.Ctx.Request.Header.Set(fasthttp.HeaderXForwardedHost, targetURI.Host)
mock.Ctx.Request.Header.Set("X-Forwarded-Uri", targetURI.Path)
mock.Ctx.Request.Header.Set(fasthttp.HeaderAccept, "text/html; charset=utf-8")
authz.Handler(mock.Ctx)
assert.Equal(t, fasthttp.StatusOK, mock.Ctx.Response.StatusCode())
assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation))
})
}
})
}
}
func (s *LegacyAuthzSuite) TestShouldHandleAllMethodsAllowXHR() {
for _, method := range testRequestMethods {
s.T().Run(fmt.Sprintf("Method%s", method), func(t *testing.T) {
for _, targetURI := range []*url.URL{
s.RequireParseRequestURI("https://bypass.example.com"),
s.RequireParseRequestURI("https://bypass.example.com/subpath"),
s.RequireParseRequestURI("https://bypass.example2.com"),
s.RequireParseRequestURI("https://bypass.example2.com/subpath"),
} {
t.Run(targetURI.String(), func(t *testing.T) {
authz := s.Builder().Build()
mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close()
for i, cookie := range mock.Ctx.Configuration.Session.Cookies {
mock.Ctx.Configuration.Session.Cookies[i].AutheliaURL = s.RequireParseRequestURI(fmt.Sprintf("https://auth.%s", cookie.Domain))
}
mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil)
mock.Ctx.Request.Header.Set("X-Forwarded-Method", method)
mock.Ctx.Request.Header.Set(fasthttp.HeaderXForwardedProto, targetURI.Scheme)
mock.Ctx.Request.Header.Set(fasthttp.HeaderXForwardedHost, targetURI.Host)
mock.Ctx.Request.Header.Set("X-Forwarded-Uri", targetURI.Path)
mock.Ctx.Request.Header.Set(fasthttp.HeaderAccept, "text/html; charset=utf-8")
authz.Handler(mock.Ctx)
assert.Equal(t, fasthttp.StatusOK, mock.Ctx.Response.StatusCode())
assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation))
})
}
})
}
}
func (s *LegacyAuthzSuite) TestShouldHandleLegacyBasicAuth() { // TestShouldVerifyAuthBasicArgOk.
authz := s.Builder().Build()
mock := mocks.NewMockAutheliaCtx(s.T())
defer mock.Close()
for i, cookie := range mock.Ctx.Configuration.Session.Cookies {
mock.Ctx.Configuration.Session.Cookies[i].AutheliaURL = s.RequireParseRequestURI(fmt.Sprintf("https://auth.%s", cookie.Domain))
}
mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil)
mock.Ctx.QueryArgs().Add("auth", "basic")
mock.Ctx.Request.Header.Set("Authorization", "Basic am9objpwYXNzd29yZA==")
mock.Ctx.Request.Header.Set("X-Original-URL", "https://one-factor.example.com")
gomock.InOrder(
mock.UserProviderMock.EXPECT().
CheckUserPassword(gomock.Eq("john"), gomock.Eq("password")).
Return(true, nil),
mock.UserProviderMock.EXPECT().
GetDetails(gomock.Eq("john")).
Return(&authentication.UserDetails{
Emails: []string{"john@example.com"},
Groups: []string{"dev", "admins"},
}, nil),
)
authz.Handler(mock.Ctx)
s.Equal(fasthttp.StatusOK, mock.Ctx.Response.StatusCode())
}
func (s *LegacyAuthzSuite) TestShouldHandleLegacyBasicAuthFailures() {
testCases := []struct {
name string
setup func(mock *mocks.MockAutheliaCtx)
}{
{
"HeaderAbsent", // TestShouldVerifyAuthBasicArgFailingNoHeader.
nil,
},
{
"HeaderEmpty", // TestShouldVerifyAuthBasicArgFailingEmptyHeader.
func(mock *mocks.MockAutheliaCtx) {
mock.Ctx.Request.Header.Set("Authorization", "")
},
},
{
"HeaderIncorrect", // TestShouldVerifyAuthBasicArgFailingWrongHeader.
func(mock *mocks.MockAutheliaCtx) {
mock.Ctx.Request.Header.Set("Proxy-Authorization", "Basic am9objpwYXNzd29yZA==")
},
},
{
"IncorrectPassword", // TestShouldVerifyAuthBasicArgFailingWrongPassword.
func(mock *mocks.MockAutheliaCtx) {
mock.Ctx.Request.Header.Set("Authorization", "Basic am9objpwYXNzd29yZA==")
mock.UserProviderMock.EXPECT().
CheckUserPassword(gomock.Eq("john"), gomock.Eq("password")).
Return(false, fmt.Errorf("generic error"))
},
},
{
"NoAccess", // TestShouldVerifyAuthBasicArgFailingWrongPassword.
func(mock *mocks.MockAutheliaCtx) {
mock.Ctx.Request.Header.Set("Authorization", "Basic am9objpwYXNzd29yZA==")
mock.Ctx.Request.Header.Set("X-Original-URL", "https://admin.example.com/")
gomock.InOrder(
mock.UserProviderMock.EXPECT().
CheckUserPassword(gomock.Eq("john"), gomock.Eq("password")).
Return(true, nil),
mock.UserProviderMock.EXPECT().
GetDetails(gomock.Eq("john")).
Return(&authentication.UserDetails{
Emails: []string{"john@example.com"},
Groups: []string{"dev", "admin"},
}, nil),
)
},
},
}
authz := s.Builder().Build()
for _, tc := range testCases {
s.T().Run(tc.name, func(t *testing.T) {
mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close()
for i, cookie := range mock.Ctx.Configuration.Session.Cookies {
mock.Ctx.Configuration.Session.Cookies[i].AutheliaURL = s.RequireParseRequestURI(fmt.Sprintf("https://auth.%s", cookie.Domain))
}
mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil)
mock.Ctx.QueryArgs().Add("auth", "basic")
mock.Ctx.Request.Header.Set("X-Original-URL", "https://one-factor.example.com")
if tc.setup != nil {
tc.setup(mock)
}
authz.Handler(mock.Ctx)
assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode())
assert.Equal(t, "401 Unauthorized", string(mock.Ctx.Response.Body()))
assert.Regexp(t, regexp.MustCompile("^Basic realm="), string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderWWWAuthenticate)))
})
}
}
func (s *LegacyAuthzSuite) TestShouldHandleInvalidURLForCVE202132637() {
testCases := []struct {
name string
scheme, host []byte
path string
expected int
}{
// The first byte in the host sequence is the null byte. This should never respond with 200 OK.
{"Should401UnauthorizedWithNullByte",
[]byte("https"), []byte{0, 110, 111, 116, 45, 111, 110, 101, 45, 102, 97, 99, 116, 111, 114, 46, 101, 120, 97, 109, 112, 108, 101, 46, 99, 111, 109}, "/path-example",
fasthttp.StatusUnauthorized,
},
{"Should200OkWithoutNullByte",
[]byte("https"), []byte{110, 111, 116, 45, 111, 110, 101, 45, 102, 97, 99, 116, 111, 114, 46, 101, 120, 97, 109, 112, 108, 101, 46, 99, 111, 109}, "/path-example",
fasthttp.StatusOK,
},
}
for _, tc := range testCases {
s.T().Run(tc.name, func(t *testing.T) {
for _, method := range testRequestMethods {
t.Run(fmt.Sprintf("Method%s", method), func(t *testing.T) {
authz := s.Builder().Build()
mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close()
mock.Ctx.Configuration.AccessControl.DefaultPolicy = testBypass
mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(&mock.Ctx.Configuration)
for i, cookie := range mock.Ctx.Configuration.Session.Cookies {
mock.Ctx.Configuration.Session.Cookies[i].AutheliaURL = s.RequireParseRequestURI(fmt.Sprintf("https://auth.%s", cookie.Domain))
}
mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil)
mock.Ctx.Request.Header.Set("X-Forwarded-Method", method)
mock.Ctx.Request.Header.SetBytesKV([]byte(fasthttp.HeaderXForwardedProto), tc.scheme)
mock.Ctx.Request.Header.SetBytesKV([]byte(fasthttp.HeaderXForwardedHost), tc.host)
mock.Ctx.Request.Header.Set("X-Forwarded-Uri", tc.path)
mock.Ctx.Request.Header.Set(fasthttp.HeaderAccept, "text/html; charset=utf-8")
authz.Handler(mock.Ctx)
assert.Equal(t, tc.expected, mock.Ctx.Response.StatusCode())
assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation))
})
}
})
}
}
func setRequestLegacy(ctx *middlewares.AutheliaCtx, method string, targetURI *url.URL, accept, xhr bool) {
if method != "" {
ctx.Request.Header.Set("X-Forwarded-Method", method)
}
if targetURI != nil {
ctx.Request.Header.Set(testXOriginalUrl, targetURI.String())
}
setRequestXHRValues(ctx, accept, xhr)
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,156 @@
package handlers
import (
"net/url"
"time"
"github.com/authelia/authelia/v4/internal/authentication"
"github.com/authelia/authelia/v4/internal/authorization"
"github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/session"
)
// Authz is a type which is a effectively is a middlewares.RequestHandler for authorization requests.
type Authz struct {
config AuthzConfig
strategies []AuthnStrategy
handleGetObject HandlerAuthzGetObject
handleGetAutheliaURL HandlerAuthzGetAutheliaURL
handleAuthorized HandlerAuthzAuthorized
handleUnauthorized HandlerAuthzUnauthorized
legacy bool
}
// HandlerAuthzUnauthorized is a Authz handler func that handles unauthorized responses.
type HandlerAuthzUnauthorized func(ctx *middlewares.AutheliaCtx, authn *Authn, redirectionURL *url.URL)
// HandlerAuthzAuthorized is a Authz handler func that handles authorized responses.
type HandlerAuthzAuthorized func(ctx *middlewares.AutheliaCtx, authn *Authn)
// HandlerAuthzGetAutheliaURL is a Authz handler func that handles retrieval of the Portal URL.
type HandlerAuthzGetAutheliaURL func(ctx *middlewares.AutheliaCtx) (portalURL *url.URL, err error)
// HandlerAuthzGetRedirectionURL is a Authz handler func that handles retrieval of the Redirection URL.
type HandlerAuthzGetRedirectionURL func(ctx *middlewares.AutheliaCtx, object *authorization.Object) (redirectionURL *url.URL, err error)
// HandlerAuthzGetObject is a Authz handler func that handles retrieval of the authorization.Object to authorize.
type HandlerAuthzGetObject func(ctx *middlewares.AutheliaCtx) (object authorization.Object, err error)
// HandlerAuthzVerifyObject is a Authz handler func that handles authorization of the authorization.Object.
type HandlerAuthzVerifyObject func(ctx *middlewares.AutheliaCtx, object authorization.Object) (err error)
// AuthnType is an auth type.
type AuthnType int
const (
// AuthnTypeNone is a nil Authentication AuthnType.
AuthnTypeNone AuthnType = iota
// AuthnTypeCookie is an Authentication AuthnType based on the Cookie header.
AuthnTypeCookie
// AuthnTypeProxyAuthorization is an Authentication AuthnType based on the Proxy-Authorization header.
AuthnTypeProxyAuthorization
// AuthnTypeAuthorization is an Authentication AuthnType based on the Authorization header.
AuthnTypeAuthorization
)
// Authn is authentication.
type Authn struct {
Username string
Method string
Details authentication.UserDetails
Level authentication.Level
Object authorization.Object
Type AuthnType
}
// AuthzConfig represents the configuration elements of the Authz type.
type AuthzConfig struct {
RefreshInterval time.Duration
Domains []AuthzDomain
}
// AuthzDomain represents a domain for the AuthzConfig.
type AuthzDomain struct {
Name string
PortalURL *url.URL
}
// AuthzBuilder is a builder pattern for the Authz type.
type AuthzBuilder struct {
config AuthzConfig
impl AuthzImplementation
strategies []AuthnStrategy
}
// AuthnStrategy is a strategy used for Authz authentication.
type AuthnStrategy interface {
Get(ctx *middlewares.AutheliaCtx, provider *session.Session) (authn Authn, err error)
CanHandleUnauthorized() (handle bool)
HandleUnauthorized(ctx *middlewares.AutheliaCtx, authn *Authn, redirectionURL *url.URL)
}
// AuthzResult is a result for Authz response handling determination.
type AuthzResult int
const (
// AuthzResultForbidden means the user is forbidden the access to a resource.
AuthzResultForbidden AuthzResult = iota
// AuthzResultUnauthorized means the user can access the resource with more permissions.
AuthzResultUnauthorized
// AuthzResultAuthorized means the user is authorized given her current permissions.
AuthzResultAuthorized
)
// AuthzImplementation represents an Authz implementation.
type AuthzImplementation int
// AuthnStrategy names.
const (
AuthnStrategyCookieSession = "CookieSession"
AuthnStrategyHeaderAuthorization = "HeaderAuthorization"
AuthnStrategyHeaderProxyAuthorization = "HeaderProxyAuthorization"
AuthnStrategyHeaderAuthRequestProxyAuthorization = "HeaderAuthRequestProxyAuthorization"
AuthnStrategyHeaderLegacy = "HeaderLegacy"
)
const (
// AuthzImplLegacy is the legacy Authz implementation (VerifyGET).
AuthzImplLegacy AuthzImplementation = iota
// AuthzImplForwardAuth is the modern Forward Auth Authz implementation which is used by Caddy and Traefik.
AuthzImplForwardAuth
// AuthzImplAuthRequest is the modern Auth Request Authz implementation which is used by NGINX and modelled after
// the ingress-nginx k8s ingress.
AuthzImplAuthRequest
// AuthzImplExtAuthz is the modern ExtAuthz Authz implementation which is used by Envoy.
AuthzImplExtAuthz
)
// String returns the text representation of this AuthzImplementation.
func (i AuthzImplementation) String() string {
switch i {
case AuthzImplLegacy:
return "Legacy"
case AuthzImplForwardAuth:
return "ForwardAuth"
case AuthzImplAuthRequest:
return "AuthRequest"
case AuthzImplExtAuthz:
return "ExtAuthz"
default:
return ""
}
}

View File

@ -0,0 +1,92 @@
package handlers
import (
"fmt"
"strings"
"github.com/authelia/authelia/v4/internal/authentication"
"github.com/authelia/authelia/v4/internal/authorization"
"github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/session"
"github.com/authelia/authelia/v4/internal/utils"
)
func friendlyMethod(m string) (fm string) {
switch m {
case "":
return "unknown"
default:
return m
}
}
func friendlyUsername(username string) (fusername string) {
switch username {
case "":
return "<anonymous>"
default:
return username
}
}
func isAuthzResult(level authentication.Level, required authorization.Level, ruleHasSubject bool) AuthzResult {
switch {
case required == authorization.Bypass:
return AuthzResultAuthorized
case required == authorization.Denied && (level != authentication.NotAuthenticated || !ruleHasSubject):
// If the user is not anonymous, it means that we went through all the rules related to that user identity and
// can safely conclude their access is actually forbidden. If a user is anonymous however this is not actually
// possible without some more advanced logic.
return AuthzResultForbidden
case required == authorization.OneFactor && level >= authentication.OneFactor,
required == authorization.TwoFactor && level >= authentication.TwoFactor:
return AuthzResultAuthorized
default:
return AuthzResultUnauthorized
}
}
// generateVerifySessionHasUpToDateProfileTraceLogs is used to generate trace logs only when trace logging is enabled.
// The information calculated in this function is completely useless other than trace for now.
func generateVerifySessionHasUpToDateProfileTraceLogs(ctx *middlewares.AutheliaCtx, userSession *session.UserSession,
details *authentication.UserDetails) {
groupsAdded, groupsRemoved := utils.StringSlicesDelta(userSession.Groups, details.Groups)
emailsAdded, emailsRemoved := utils.StringSlicesDelta(userSession.Emails, details.Emails)
nameDelta := userSession.DisplayName != details.DisplayName
var groupsDelta []string
if len(groupsAdded) != 0 {
groupsDelta = append(groupsDelta, fmt.Sprintf("added: %s.", strings.Join(groupsAdded, ", ")))
}
if len(groupsRemoved) != 0 {
groupsDelta = append(groupsDelta, fmt.Sprintf("removed: %s.", strings.Join(groupsRemoved, ", ")))
}
if len(groupsDelta) != 0 {
ctx.Logger.Tracef("Updated groups detected for %s. %s", userSession.Username, strings.Join(groupsDelta, " "))
} else {
ctx.Logger.Tracef("No updated groups detected for %s", userSession.Username)
}
var emailsDelta []string
if len(emailsAdded) != 0 {
emailsDelta = append(emailsDelta, fmt.Sprintf("added: %s.", strings.Join(emailsAdded, ", ")))
}
if len(emailsRemoved) != 0 {
emailsDelta = append(emailsDelta, fmt.Sprintf("removed: %s.", strings.Join(emailsRemoved, ", ")))
}
if len(emailsDelta) != 0 {
ctx.Logger.Tracef("Updated emails detected for %s. %s", userSession.Username, strings.Join(emailsDelta, " "))
} else {
ctx.Logger.Tracef("No updated emails detected for %s", userSession.Username)
}
if nameDelta {
ctx.Logger.Tracef("Updated display name detected for %s. Added: %s. Removed: %s.", userSession.Username, details.DisplayName, userSession.DisplayName)
} else {
ctx.Logger.Tracef("No updated display name detected for %s", userSession.Username)
}
}

View File

@ -5,13 +5,22 @@ import (
"net/url"
"github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/session"
)
// CheckSafeRedirectionPOST handler checking whether the redirection to a given URL provided in body is safe.
func CheckSafeRedirectionPOST(ctx *middlewares.AutheliaCtx) {
userSession := ctx.GetSession()
var (
s session.UserSession
err error
)
if userSession.IsAnonymous() {
if s, err = ctx.GetSession(); err != nil {
ctx.ReplyUnauthorized()
return
}
if s.IsAnonymous() {
ctx.ReplyUnauthorized()
return
}
@ -19,7 +28,6 @@ func CheckSafeRedirectionPOST(ctx *middlewares.AutheliaCtx) {
var (
bodyJSON checkURIWithinDomainRequestBody
targetURI *url.URL
err error
)
if err = ctx.ParseBody(&bodyJSON); err != nil {

View File

@ -21,35 +21,35 @@ func TestCheckSafeRedirection(t *testing.T) {
}{
{
"ShouldReturnUnauthorized",
session.UserSession{AuthenticationLevel: authentication.NotAuthenticated},
session.UserSession{CookieDomain: "example.com", AuthenticationLevel: authentication.NotAuthenticated},
"http://myapp.example.com",
fasthttp.StatusUnauthorized,
false,
},
{
"ShouldReturnTrueOnGoodDomain",
session.UserSession{Username: "john", AuthenticationLevel: authentication.OneFactor},
session.UserSession{CookieDomain: "example.com", Username: "john", AuthenticationLevel: authentication.OneFactor},
"https://myapp.example.com",
fasthttp.StatusOK,
true,
},
{
"ShouldReturnFalseOnGoodDomainWithBadScheme",
session.UserSession{Username: "john", AuthenticationLevel: authentication.OneFactor},
session.UserSession{CookieDomain: "example.com", Username: "john", AuthenticationLevel: authentication.OneFactor},
"http://myapp.example.com",
fasthttp.StatusOK,
false,
},
{
"ShouldReturnFalseOnBadDomainWithGoodScheme",
session.UserSession{Username: "john", AuthenticationLevel: authentication.OneFactor},
session.UserSession{CookieDomain: "example.com", Username: "john", AuthenticationLevel: authentication.OneFactor},
"https://myapp.notgood.com",
fasthttp.StatusOK,
false,
},
{
"ShouldReturnFalseOnBadDomainWithBadScheme",
session.UserSession{Username: "john", AuthenticationLevel: authentication.OneFactor},
session.UserSession{CookieDomain: "example.com", Username: "john", AuthenticationLevel: authentication.OneFactor},
"http://myapp.notgood.com",
fasthttp.StatusOK,
false,
@ -80,9 +80,11 @@ func TestCheckSafeRedirection(t *testing.T) {
func TestShouldFailOnInvalidBody(t *testing.T) {
mock := mocks.NewMockAutheliaCtxWithUserSession(t, session.UserSession{
CookieDomain: exampleDotCom,
Username: "john",
AuthenticationLevel: authentication.OneFactor,
})
defer mock.Close()
mock.Ctx.Configuration.Session.Domain = exampleDotCom
@ -94,6 +96,7 @@ func TestShouldFailOnInvalidBody(t *testing.T) {
func TestShouldFailOnInvalidURL(t *testing.T) {
mock := mocks.NewMockAutheliaCtxWithUserSession(t, session.UserSession{
CookieDomain: exampleDotCom,
Username: "john",
AuthenticationLevel: authentication.OneFactor,
})

View File

@ -4,9 +4,10 @@ import (
"errors"
"time"
"github.com/authelia/authelia/v4/internal/configuration/schema"
"github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/regulation"
"github.com/authelia/authelia/v4/internal/session"
"github.com/authelia/authelia/v4/internal/utils"
)
// FirstFactorPOST is the handler performing the first factory.
@ -90,7 +91,7 @@ func FirstFactorPOST(delayFunc middlewares.TimingAttackDelayFunc) middlewares.Re
return
}
newSession := session.NewDefaultUserSession()
newSession := provider.NewDefaultUserSession()
// Reset all values from previous session except OIDC workflow before regenerating the cookie.
if err = ctx.SaveSession(newSession); err != nil {
@ -159,3 +160,22 @@ func FirstFactorPOST(delayFunc middlewares.TimingAttackDelayFunc) middlewares.Re
}
}
}
func getProfileRefreshSettings(cfg schema.AuthenticationBackend) (refresh bool, refreshInterval time.Duration) {
if cfg.LDAP != nil {
if cfg.RefreshInterval == schema.ProfileRefreshDisabled {
refresh = false
refreshInterval = 0
} else {
refresh = true
if cfg.RefreshInterval != schema.ProfileRefreshAlways {
refreshInterval, _ = utils.ParseDurationString(cfg.RefreshInterval)
} else {
refreshInterval = schema.RefreshIntervalAlways
}
}
}
return refresh, refreshInterval
}

View File

@ -209,13 +209,14 @@ func (s *FirstFactorSuite) TestShouldAuthenticateUserWithRememberMeChecked() {
assert.Equal(s.T(), 200, s.mock.Ctx.Response.StatusCode())
assert.Equal(s.T(), []byte("{\"status\":\"OK\"}"), s.mock.Ctx.Response.Body())
// And store authentication in session.
session := s.mock.Ctx.GetSession()
assert.Equal(s.T(), "test", session.Username)
assert.Equal(s.T(), true, session.KeepMeLoggedIn)
assert.Equal(s.T(), authentication.OneFactor, session.AuthenticationLevel)
assert.Equal(s.T(), []string{"test@example.com"}, session.Emails)
assert.Equal(s.T(), []string{"dev", "admins"}, session.Groups)
userSession, err := s.mock.Ctx.GetSession()
s.Assert().NoError(err)
assert.Equal(s.T(), "test", userSession.Username)
assert.Equal(s.T(), true, userSession.KeepMeLoggedIn)
assert.Equal(s.T(), authentication.OneFactor, userSession.AuthenticationLevel)
assert.Equal(s.T(), []string{"test@example.com"}, userSession.Emails)
assert.Equal(s.T(), []string{"dev", "admins"}, userSession.Groups)
}
func (s *FirstFactorSuite) TestShouldAuthenticateUserWithRememberMeUnchecked() {
@ -250,13 +251,14 @@ func (s *FirstFactorSuite) TestShouldAuthenticateUserWithRememberMeUnchecked() {
assert.Equal(s.T(), 200, s.mock.Ctx.Response.StatusCode())
assert.Equal(s.T(), []byte("{\"status\":\"OK\"}"), s.mock.Ctx.Response.Body())
// And store authentication in session.
session := s.mock.Ctx.GetSession()
assert.Equal(s.T(), "test", session.Username)
assert.Equal(s.T(), false, session.KeepMeLoggedIn)
assert.Equal(s.T(), authentication.OneFactor, session.AuthenticationLevel)
assert.Equal(s.T(), []string{"test@example.com"}, session.Emails)
assert.Equal(s.T(), []string{"dev", "admins"}, session.Groups)
userSession, err := s.mock.Ctx.GetSession()
s.Assert().NoError(err)
assert.Equal(s.T(), "test", userSession.Username)
assert.Equal(s.T(), false, userSession.KeepMeLoggedIn)
assert.Equal(s.T(), authentication.OneFactor, userSession.AuthenticationLevel)
assert.Equal(s.T(), []string{"test@example.com"}, userSession.Emails)
assert.Equal(s.T(), []string{"dev", "admins"}, userSession.Groups)
}
func (s *FirstFactorSuite) TestShouldSaveUsernameFromAuthenticationBackendInSession() {
@ -294,13 +296,14 @@ func (s *FirstFactorSuite) TestShouldSaveUsernameFromAuthenticationBackendInSess
assert.Equal(s.T(), 200, s.mock.Ctx.Response.StatusCode())
assert.Equal(s.T(), []byte("{\"status\":\"OK\"}"), s.mock.Ctx.Response.Body())
// And store authentication in session.
session := s.mock.Ctx.GetSession()
assert.Equal(s.T(), "Test", session.Username)
assert.Equal(s.T(), true, session.KeepMeLoggedIn)
assert.Equal(s.T(), authentication.OneFactor, session.AuthenticationLevel)
assert.Equal(s.T(), []string{"test@example.com"}, session.Emails)
assert.Equal(s.T(), []string{"dev", "admins"}, session.Groups)
userSession, err := s.mock.Ctx.GetSession()
s.Assert().NoError(err)
assert.Equal(s.T(), "Test", userSession.Username)
assert.Equal(s.T(), true, userSession.KeepMeLoggedIn)
assert.Equal(s.T(), authentication.OneFactor, userSession.AuthenticationLevel)
assert.Equal(s.T(), []string{"test@example.com"}, userSession.Emails)
assert.Equal(s.T(), []string{"dev", "admins"}, userSession.Groups)
}
type FirstFactorRedirectionSuite struct {
@ -312,7 +315,7 @@ type FirstFactorRedirectionSuite struct {
func (s *FirstFactorRedirectionSuite) SetupTest() {
s.mock = mocks.NewMockAutheliaCtx(s.T())
s.mock.Ctx.Configuration.DefaultRedirectionURL = "https://default.local"
s.mock.Ctx.Configuration.AccessControl.DefaultPolicy = "bypass"
s.mock.Ctx.Configuration.AccessControl.DefaultPolicy = testBypass
s.mock.Ctx.Configuration.AccessControl.Rules = []schema.ACLRule{
{
Domains: []string{"default.local"},

View File

@ -5,7 +5,6 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/authelia/authelia/v4/internal/mocks"
@ -19,10 +18,14 @@ type LogoutSuite struct {
func (s *LogoutSuite) SetupTest() {
s.mock = mocks.NewMockAutheliaCtx(s.T())
userSession := s.mock.Ctx.GetSession()
provider, err := s.mock.Ctx.GetSessionProvider()
s.Assert().NoError(err)
userSession, err := provider.GetSession(s.mock.Ctx.RequestCtx)
s.Assert().NoError(err)
userSession.Username = testUsername
err := s.mock.Ctx.SaveSession(userSession)
require.NoError(s.T(), err)
s.Assert().NoError(provider.SaveSession(s.mock.Ctx.RequestCtx, userSession))
}
func (s *LogoutSuite) TearDownTest() {

View File

@ -11,6 +11,7 @@ import (
"github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/model"
"github.com/authelia/authelia/v4/internal/oidc"
"github.com/authelia/authelia/v4/internal/session"
)
// OpenIDConnectAuthorization handles GET/POST requests to the OpenID Connect 1.0 Authorization endpoint.
@ -64,13 +65,20 @@ func OpenIDConnectAuthorization(ctx *middlewares.AutheliaCtx, rw http.ResponseWr
issuer = ctx.RootURL()
userSession := ctx.GetSession()
var (
consent *model.OAuth2ConsentSession
handled bool
userSession session.UserSession
consent *model.OAuth2ConsentSession
handled bool
)
if userSession, err = ctx.GetSession(); err != nil {
ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' could not be processed: error occurred obtaining session information: %+v", requester.GetID(), client.GetID(), err)
ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, fosite.ErrServerError.WithHint("Could not obtain the user session."))
return
}
if consent, handled = handleOIDCAuthorizationConsent(ctx, issuer, client, userSession, rw, r, requester); handled {
return
}

View File

@ -156,7 +156,12 @@ func oidcConsentGetSessionsAndClient(ctx *middlewares.AutheliaCtx, consentID uui
err error
)
userSession = ctx.GetSession()
if userSession, err = ctx.GetSession(); err != nil {
ctx.Logger.Errorf("Unable to load user session for challenge id '%s': %v", consentID, err)
ctx.ReplyForbidden()
return userSession, nil, nil, true
}
if consent, err = ctx.Providers.StorageProvider.LoadOAuth2ConsentSessionByChallengeID(ctx, consentID); err != nil {
ctx.Logger.Errorf("Unable to load consent session with challenge id '%s': %v", consentID, err)

View File

@ -8,21 +8,31 @@ import (
"github.com/authelia/authelia/v4/internal/duo"
"github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/model"
"github.com/authelia/authelia/v4/internal/session"
"github.com/authelia/authelia/v4/internal/utils"
)
// DuoDevicesGET handler for retrieving available devices and capabilities from duo api.
func DuoDevicesGET(duoAPI duo.API) middlewares.RequestHandler {
return func(ctx *middlewares.AutheliaCtx) {
userSession := ctx.GetSession()
var (
userSession session.UserSession
err error
)
if userSession, err = ctx.GetSession(); err != nil {
ctx.Error(fmt.Errorf("failed to get session data: %w", err), messageMFAValidationFailed)
return
}
values := url.Values{}
values.Set("username", userSession.Username)
ctx.Logger.Debugf("Starting Duo PreAuth for %s", userSession.Username)
result, message, devices, enrollURL, err := DuoPreAuth(ctx, duoAPI)
result, message, devices, enrollURL, err := DuoPreAuth(ctx, &userSession, duoAPI)
if err != nil {
ctx.Error(fmt.Errorf("duo PreAuth API errored: %s", err), messageMFAValidationFailed)
ctx.Error(fmt.Errorf("duo PreAuth API errored: %w", err), messageMFAValidationFailed)
return
}
@ -80,39 +90,55 @@ func DuoDevicesGET(duoAPI duo.API) middlewares.RequestHandler {
// DuoDevicePOST update the user preferences regarding Duo device and method.
func DuoDevicePOST(ctx *middlewares.AutheliaCtx) {
device := DuoDeviceBody{}
bodyJSON := DuoDeviceBody{}
err := ctx.ParseBody(&device)
if err != nil {
var (
userSession session.UserSession
err error
)
if err = ctx.ParseBody(&bodyJSON); err != nil {
ctx.Error(err, messageMFAValidationFailed)
return
}
if !utils.IsStringInSlice(device.Method, duo.PossibleMethods) {
ctx.Error(fmt.Errorf("unknown method '%s', it should be one of %s", device.Method, strings.Join(duo.PossibleMethods, ", ")), messageMFAValidationFailed)
if !utils.IsStringInSlice(bodyJSON.Method, duo.PossibleMethods) {
ctx.Error(fmt.Errorf("unknown method '%s', it should be one of %s", bodyJSON.Method, strings.Join(duo.PossibleMethods, ", ")), messageMFAValidationFailed)
return
}
userSession := ctx.GetSession()
ctx.Logger.Debugf("Save new preferred Duo device and method of user %s to %s using %s", userSession.Username, device.Device, device.Method)
err = ctx.Providers.StorageProvider.SavePreferredDuoDevice(ctx, model.DuoDevice{Username: userSession.Username, Device: device.Device, Method: device.Method})
if userSession, err = ctx.GetSession(); err != nil {
ctx.Error(err, messageMFAValidationFailed)
return
}
ctx.Logger.Debugf("Save new preferred Duo device and method of user %s to %s using %s", userSession.Username, bodyJSON.Device, bodyJSON.Method)
err = ctx.Providers.StorageProvider.SavePreferredDuoDevice(ctx, model.DuoDevice{Username: userSession.Username, Device: bodyJSON.Device, Method: bodyJSON.Method})
if err != nil {
ctx.Error(fmt.Errorf("unable to save new preferred Duo device and method: %s", err), messageMFAValidationFailed)
ctx.Error(fmt.Errorf("unable to save new preferred Duo device and method: %w", err), messageMFAValidationFailed)
return
}
ctx.ReplyOK()
}
// SecondFactorDuoDeviceDelete deletes the useres preferred Duo device and method.
func SecondFactorDuoDeviceDelete(ctx *middlewares.AutheliaCtx) {
userSession := ctx.GetSession()
// DuoDeviceDELETE deletes the useres preferred Duo device and method.
func DuoDeviceDELETE(ctx *middlewares.AutheliaCtx) {
var (
userSession session.UserSession
err error
)
if userSession, err = ctx.GetSession(); err != nil {
ctx.Error(fmt.Errorf("unable to get session to delete preferred Duo device and method: %w", err), messageMFAValidationFailed)
return
}
ctx.Logger.Debugf("Deleting preferred Duo device and method of user %s", userSession.Username)
err := ctx.Providers.StorageProvider.DeletePreferredDuoDevice(ctx, userSession.Username)
if err != nil {
ctx.Error(fmt.Errorf("unable to delete preferred Duo device and method: %s", err), messageMFAValidationFailed)
if err = ctx.Providers.StorageProvider.DeletePreferredDuoDevice(ctx, userSession.Username); err != nil {
ctx.Error(fmt.Errorf("unable to delete preferred Duo device and method: %w", err), messageMFAValidationFailed)
return
}

View File

@ -13,6 +13,7 @@ import (
"github.com/authelia/authelia/v4/internal/duo"
"github.com/authelia/authelia/v4/internal/mocks"
"github.com/authelia/authelia/v4/internal/model"
"github.com/authelia/authelia/v4/internal/session"
)
type RegisterDuoDeviceSuite struct {
@ -22,10 +23,11 @@ type RegisterDuoDeviceSuite struct {
func (s *RegisterDuoDeviceSuite) SetupTest() {
s.mock = mocks.NewMockAutheliaCtx(s.T())
userSession := s.mock.Ctx.GetSession()
userSession.Username = testUsername
err := s.mock.Ctx.SaveSession(userSession)
userSession, err := s.mock.Ctx.GetSession()
s.Assert().NoError(err)
userSession.Username = testUsername
s.NoError(s.mock.Ctx.SaveSession(userSession))
}
func (s *RegisterDuoDeviceSuite) TearDownTest() {
@ -38,7 +40,7 @@ func (s *RegisterDuoDeviceSuite) TestShouldCallDuoAPIAndFail() {
values := url.Values{}
values.Set("username", "john")
duoMock.EXPECT().PreAuthCall(s.mock.Ctx, gomock.Eq(values)).Return(nil, fmt.Errorf("Connnection error"))
duoMock.EXPECT().PreAuthCall(s.mock.Ctx, &session.UserSession{CookieDomain: "example.com", Username: "john"}, gomock.Eq(values)).Return(nil, fmt.Errorf("Connnection error"))
DuoDevicesGET(duoMock)(s.mock.Ctx)
@ -68,7 +70,7 @@ func (s *RegisterDuoDeviceSuite) TestShouldRespondWithSelection() {
response.Result = auth
response.Devices = duoDevices
duoMock.EXPECT().PreAuthCall(s.mock.Ctx, gomock.Eq(values)).Return(&response, nil)
duoMock.EXPECT().PreAuthCall(s.mock.Ctx, &session.UserSession{CookieDomain: "example.com", Username: "john"}, gomock.Eq(values)).Return(&response, nil)
DuoDevicesGET(duoMock)(s.mock.Ctx)
@ -84,7 +86,7 @@ func (s *RegisterDuoDeviceSuite) TestShouldRespondWithAllowOnBypass() {
response := duo.PreAuthResponse{}
response.Result = allow
duoMock.EXPECT().PreAuthCall(s.mock.Ctx, gomock.Eq(values)).Return(&response, nil)
duoMock.EXPECT().PreAuthCall(s.mock.Ctx, &session.UserSession{CookieDomain: "example.com", Username: "john"}, gomock.Eq(values)).Return(&response, nil)
DuoDevicesGET(duoMock)(s.mock.Ctx)
@ -103,7 +105,7 @@ func (s *RegisterDuoDeviceSuite) TestShouldRespondWithEnroll() {
response.Result = enroll
response.EnrollPortalURL = enrollURL
duoMock.EXPECT().PreAuthCall(s.mock.Ctx, gomock.Eq(values)).Return(&response, nil)
duoMock.EXPECT().PreAuthCall(s.mock.Ctx, &session.UserSession{CookieDomain: "example.com", Username: "john"}, gomock.Eq(values)).Return(&response, nil)
DuoDevicesGET(duoMock)(s.mock.Ctx)
@ -119,7 +121,7 @@ func (s *RegisterDuoDeviceSuite) TestShouldRespondWithDeny() {
response := duo.PreAuthResponse{}
response.Result = deny
duoMock.EXPECT().PreAuthCall(s.mock.Ctx, gomock.Eq(values)).Return(&response, nil)
duoMock.EXPECT().PreAuthCall(s.mock.Ctx, &session.UserSession{CookieDomain: "example.com", Username: "john"}, gomock.Eq(values)).Return(&response, nil)
DuoDevicesGET(duoMock)(s.mock.Ctx)

View File

@ -9,8 +9,12 @@ import (
)
// identityRetrieverFromSession retriever computing the identity from the cookie session.
func identityRetrieverFromSession(ctx *middlewares.AutheliaCtx) (*session.Identity, error) {
userSession := ctx.GetSession()
func identityRetrieverFromSession(ctx *middlewares.AutheliaCtx) (identity *session.Identity, err error) {
var userSession session.UserSession
if userSession, err = ctx.GetSession(); err != nil {
return nil, fmt.Errorf("error retrieving user session for request: %w", err)
}
if len(userSession.Emails) == 0 {
return nil, fmt.Errorf("user %s does not have any email address", userSession.Username)
@ -24,7 +28,9 @@ func identityRetrieverFromSession(ctx *middlewares.AutheliaCtx) (*session.Identi
}
func isTokenUserValidFor2FARegistration(ctx *middlewares.AutheliaCtx, username string) bool {
return ctx.GetSession().Username == username
userSession, err := ctx.GetSession()
return err == nil && userSession.Username == username
}
// TOTPIdentityStart the handler for initiating the identity validation.

View File

@ -11,6 +11,7 @@ import (
"github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/model"
"github.com/authelia/authelia/v4/internal/regulation"
"github.com/authelia/authelia/v4/internal/session"
"github.com/authelia/authelia/v4/internal/storage"
)
@ -34,12 +35,19 @@ var WebauthnIdentityFinish = middlewares.IdentityVerificationFinish(
// SecondFactorWebauthnAttestationGET returns the attestation challenge from the server.
func SecondFactorWebauthnAttestationGET(ctx *middlewares.AutheliaCtx, _ string) {
var (
w *webauthn.WebAuthn
user *model.WebauthnUser
err error
w *webauthn.WebAuthn
user *model.WebauthnUser
userSession session.UserSession
err error
)
userSession := ctx.GetSession()
if userSession, err = ctx.GetSession(); err != nil {
ctx.Logger.WithError(err).Errorf("Error occurred retrieving session for %s attestation challenge", regulation.AuthTypeWebauthn)
respondUnauthorized(ctx, messageUnableToRegisterSecurityKey)
return
}
if w, err = newWebauthn(ctx); err != nil {
ctx.Logger.Errorf("Unable to create %s attestation challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
@ -96,12 +104,20 @@ func WebauthnAttestationPOST(ctx *middlewares.AutheliaCtx) {
w *webauthn.WebAuthn
user *model.WebauthnUser
userSession session.UserSession
attestationResponse *protocol.ParsedCredentialCreationData
credential *webauthn.Credential
postData *requestPostData
)
userSession := ctx.GetSession()
if userSession, err = ctx.GetSession(); err != nil {
ctx.Logger.WithError(err).Errorf("Error occurred retrieving session for %s attestation response", regulation.AuthTypeWebauthn)
respondUnauthorized(ctx, messageUnableToRegisterSecurityKey)
return
}
if userSession.Webauthn == nil {
ctx.Logger.Errorf("Webauthn session data is not present in order to handle attestation for user '%s'. This could indicate a user trying to POST to the wrong endpoint, or the session data is not present for the browser they used.", userSession.Username)

View File

@ -45,12 +45,19 @@ var ResetPasswordIdentityStart = middlewares.IdentityVerificationStart(middlewar
}, middlewares.TimingAttackDelay(10, 250, 85, time.Millisecond*500, false))
func resetPasswordIdentityFinish(ctx *middlewares.AutheliaCtx, username string) {
userSession := ctx.GetSession()
var (
userSession session.UserSession
err error
)
if userSession, err = ctx.GetSession(); err != nil {
ctx.Logger.Errorf("Unable to get session to clear password reset flag in session for user %s: %s", userSession.Username, err)
}
// TODO(c.michaud): use JWT tokens to expire the request in only few seconds for better security.
userSession.PasswordResetUsername = &username
err := ctx.SaveSession(userSession)
if err != nil {
if err = ctx.SaveSession(userSession); err != nil {
ctx.Logger.Errorf("Unable to clear password reset flag in session for user %s: %s", userSession.Username, err)
}

View File

@ -4,13 +4,22 @@ import (
"fmt"
"github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/session"
"github.com/authelia/authelia/v4/internal/templates"
"github.com/authelia/authelia/v4/internal/utils"
)
// ResetPasswordPOST handler for resetting passwords.
func ResetPasswordPOST(ctx *middlewares.AutheliaCtx) {
userSession := ctx.GetSession()
var (
userSession session.UserSession
err error
)
if userSession, err = ctx.GetSession(); err != nil {
ctx.Error(fmt.Errorf("error occurred retrieving session for user: %w", err), messageUnableToResetPassword)
return
}
// Those checks unsure that the identity verification process has been initiated and completed successfully
// otherwise PasswordReset would not be set to true. We can improve the security of this check by making the
@ -23,9 +32,8 @@ func ResetPasswordPOST(ctx *middlewares.AutheliaCtx) {
username := *userSession.PasswordResetUsername
var requestBody resetPasswordStep2RequestBody
err := ctx.ParseBody(&requestBody)
if err != nil {
if err = ctx.ParseBody(&requestBody); err != nil {
ctx.Error(err, messageUnableToResetPassword)
return
}

View File

@ -18,9 +18,12 @@ func DuoPOST(duoAPI duo.API) middlewares.RequestHandler {
var (
bodyJSON = &bodySignDuoRequest{}
device, method string
userSession session.UserSession
err error
)
if err := ctx.ParseBody(bodyJSON); err != nil {
if err = ctx.ParseBody(bodyJSON); err != nil {
ctx.Logger.Errorf(logFmtErrParseRequestBody, regulation.AuthTypeDuo, err)
respondUnauthorized(ctx, messageMFAValidationFailed)
@ -28,7 +31,11 @@ func DuoPOST(duoAPI duo.API) middlewares.RequestHandler {
return
}
userSession := ctx.GetSession()
if userSession, err = ctx.GetSession(); err != nil {
ctx.Error(fmt.Errorf("error occurred retrieving user session: %w", err), messageMFAValidationFailed)
return
}
remoteIP := ctx.RemoteIP().String()
duoDevice, err := ctx.Providers.StorageProvider.LoadPreferredDuoDevice(ctx, userSession.Username)
@ -61,7 +68,7 @@ func DuoPOST(duoAPI duo.API) middlewares.RequestHandler {
return
}
authResponse, err := duoAPI.AuthCall(ctx, values)
authResponse, err := duoAPI.AuthCall(ctx, &userSession, values)
if err != nil {
ctx.Logger.Errorf("Failed to perform Duo Auth Call for user '%s': %+v", userSession.Username, err)
@ -85,13 +92,13 @@ func DuoPOST(duoAPI duo.API) middlewares.RequestHandler {
return
}
HandleAllow(ctx, bodyJSON)
HandleAllow(ctx, &userSession, bodyJSON)
}
}
// HandleInitialDeviceSelection handler for retrieving all available devices.
func HandleInitialDeviceSelection(ctx *middlewares.AutheliaCtx, userSession *session.UserSession, duoAPI duo.API, bodyJSON *bodySignDuoRequest) (device string, method string, err error) {
result, message, devices, enrollURL, err := DuoPreAuth(ctx, duoAPI)
result, message, devices, enrollURL, err := DuoPreAuth(ctx, userSession, duoAPI)
if err != nil {
ctx.Logger.Errorf("Failed to perform Duo PreAuth for user '%s': %+v", userSession.Username, err)
@ -119,7 +126,7 @@ func HandleInitialDeviceSelection(ctx *middlewares.AutheliaCtx, userSession *ses
return "", "", nil
case allow:
ctx.Logger.Debugf("Duo authentication was bypassed for user: %s", userSession.Username)
HandleAllow(ctx, bodyJSON)
HandleAllow(ctx, userSession, bodyJSON)
return "", "", nil
case auth:
@ -136,7 +143,7 @@ func HandleInitialDeviceSelection(ctx *middlewares.AutheliaCtx, userSession *ses
// HandlePreferredDeviceCheck handler to check if the saved device and method is still valid.
func HandlePreferredDeviceCheck(ctx *middlewares.AutheliaCtx, userSession *session.UserSession, duoAPI duo.API, device string, method string, bodyJSON *bodySignDuoRequest) (string, string, error) {
result, message, devices, enrollURL, err := DuoPreAuth(ctx, duoAPI)
result, message, devices, enrollURL, err := DuoPreAuth(ctx, userSession, duoAPI)
if err != nil {
ctx.Logger.Errorf("Failed to perform Duo PreAuth for user '%s': %+v", userSession.Username, err)
@ -165,7 +172,7 @@ func HandlePreferredDeviceCheck(ctx *middlewares.AutheliaCtx, userSession *sessi
return "", "", nil
case allow:
ctx.Logger.Debugf("Duo authentication was bypassed for user: %s", userSession.Username)
HandleAllow(ctx, bodyJSON)
HandleAllow(ctx, userSession, bodyJSON)
return "", "", nil
case auth:
@ -243,11 +250,12 @@ func HandleAutoSelection(ctx *middlewares.AutheliaCtx, devices []DuoDevice, user
}
// HandleAllow handler for successful logins.
func HandleAllow(ctx *middlewares.AutheliaCtx, bodyJSON *bodySignDuoRequest) {
userSession := ctx.GetSession()
func HandleAllow(ctx *middlewares.AutheliaCtx, userSession *session.UserSession, bodyJSON *bodySignDuoRequest) {
var (
err error
)
err := ctx.RegenerateSession()
if err != nil {
if err = ctx.RegenerateSession(); err != nil {
ctx.Logger.Errorf(logFmtErrSessionRegenerate, regulation.AuthTypeDuo, userSession.Username, err)
respondUnauthorized(ctx, messageMFAValidationFailed)
@ -257,8 +265,7 @@ func HandleAllow(ctx *middlewares.AutheliaCtx, bodyJSON *bodySignDuoRequest) {
userSession.SetTwoFactorDuo(ctx.Clock.Now())
err = ctx.SaveSession(userSession)
if err != nil {
if err = ctx.SaveSession(*userSession); err != nil {
ctx.Logger.Errorf(logFmtErrSessionSave, "authentication time", regulation.AuthTypeTOTP, userSession.Username, err)
respondUnauthorized(ctx, messageMFAValidationFailed)

View File

@ -10,7 +10,6 @@ import (
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/authelia/authelia/v4/internal/configuration/schema"
@ -18,6 +17,7 @@ import (
"github.com/authelia/authelia/v4/internal/mocks"
"github.com/authelia/authelia/v4/internal/model"
"github.com/authelia/authelia/v4/internal/regulation"
"github.com/authelia/authelia/v4/internal/session"
)
type SecondFactorDuoPostSuite struct {
@ -27,10 +27,13 @@ type SecondFactorDuoPostSuite struct {
func (s *SecondFactorDuoPostSuite) SetupTest() {
s.mock = mocks.NewMockAutheliaCtx(s.T())
userSession := s.mock.Ctx.GetSession()
userSession, err := s.mock.Ctx.GetSession()
s.Assert().NoError(err)
userSession.Username = testUsername
err := s.mock.Ctx.SaveSession(userSession)
require.NoError(s.T(), err)
s.Assert().NoError(s.mock.Ctx.SaveSession(userSession))
}
func (s *SecondFactorDuoPostSuite) TearDownTest() {
@ -53,7 +56,7 @@ func (s *SecondFactorDuoPostSuite) TestShouldEnroll() {
preAuthResponse.Result = enroll
preAuthResponse.EnrollPortalURL = enrollURL
duoMock.EXPECT().PreAuthCall(s.mock.Ctx, gomock.Eq(values)).Return(&preAuthResponse, nil)
duoMock.EXPECT().PreAuthCall(s.mock.Ctx, &session.UserSession{CookieDomain: "example.com", Username: "john"}, gomock.Eq(values)).Return(&preAuthResponse, nil)
bodyBytes, err := json.Marshal(bodySignDuoRequest{})
s.Require().NoError(err)
@ -84,7 +87,7 @@ func (s *SecondFactorDuoPostSuite) TestShouldAutoSelect() {
preAuthResponse.Result = auth
preAuthResponse.Devices = duoDevices
duoMock.EXPECT().PreAuthCall(s.mock.Ctx, gomock.Eq(values)).Return(&preAuthResponse, nil)
duoMock.EXPECT().PreAuthCall(s.mock.Ctx, &session.UserSession{CookieDomain: "example.com", Username: "john"}, gomock.Eq(values)).Return(&preAuthResponse, nil)
s.mock.StorageMock.EXPECT().
SavePreferredDuoDevice(s.mock.Ctx, model.DuoDevice{Username: "john", Device: "12345ABCDEFGHIJ67890", Method: "push"}).
@ -112,7 +115,7 @@ func (s *SecondFactorDuoPostSuite) TestShouldAutoSelect() {
authResponse := duo.AuthResponse{}
authResponse.Result = allow
duoMock.EXPECT().AuthCall(s.mock.Ctx, gomock.Eq(values)).Return(&authResponse, nil)
duoMock.EXPECT().AuthCall(s.mock.Ctx, &session.UserSession{CookieDomain: "example.com", Username: "john"}, gomock.Eq(values)).Return(&authResponse, nil)
bodyBytes, err := json.Marshal(bodySignDuoRequest{TargetURL: "https://target.example.com"})
s.Require().NoError(err)
@ -135,7 +138,7 @@ func (s *SecondFactorDuoPostSuite) TestShouldDenyAutoSelect() {
preAuthResponse := duo.PreAuthResponse{}
preAuthResponse.Result = deny
duoMock.EXPECT().PreAuthCall(s.mock.Ctx, gomock.Eq(values)).Return(&preAuthResponse, nil)
duoMock.EXPECT().PreAuthCall(s.mock.Ctx, &session.UserSession{CookieDomain: "example.com", Username: "john"}, gomock.Eq(values)).Return(&preAuthResponse, nil)
values = url.Values{}
values.Set("username", "john")
@ -161,7 +164,7 @@ func (s *SecondFactorDuoPostSuite) TestShouldFailAutoSelect() {
LoadPreferredDuoDevice(s.mock.Ctx, "john").
Return(nil, errors.New("no Duo device and method saved"))
duoMock.EXPECT().PreAuthCall(s.mock.Ctx, gomock.Any()).Return(nil, fmt.Errorf("Connnection error"))
duoMock.EXPECT().PreAuthCall(s.mock.Ctx, &session.UserSession{CookieDomain: "example.com", Username: "john"}, gomock.Any()).Return(nil, fmt.Errorf("Connnection error"))
bodyBytes, err := json.Marshal(bodySignDuoRequest{TargetURL: "https://target.example.com"})
s.Require().NoError(err)
@ -188,7 +191,7 @@ func (s *SecondFactorDuoPostSuite) TestShouldDeleteOldDeviceAndEnroll() {
preAuthResponse.Result = enroll
preAuthResponse.EnrollPortalURL = enrollURL
duoMock.EXPECT().PreAuthCall(s.mock.Ctx, gomock.Eq(values)).Return(&preAuthResponse, nil)
duoMock.EXPECT().PreAuthCall(s.mock.Ctx, &session.UserSession{CookieDomain: "example.com", Username: "john"}, gomock.Eq(values)).Return(&preAuthResponse, nil)
s.mock.StorageMock.EXPECT().DeletePreferredDuoDevice(s.mock.Ctx, "john").Return(nil)
@ -222,7 +225,7 @@ func (s *SecondFactorDuoPostSuite) TestShouldDeleteOldDeviceAndCallPreauthAPIWit
preAuthResponse.Result = auth
preAuthResponse.Devices = duoDevices
duoMock.EXPECT().PreAuthCall(s.mock.Ctx, gomock.Eq(values)).Return(&preAuthResponse, nil)
duoMock.EXPECT().PreAuthCall(s.mock.Ctx, &session.UserSession{CookieDomain: "example.com", Username: "john"}, gomock.Eq(values)).Return(&preAuthResponse, nil)
s.mock.StorageMock.EXPECT().DeletePreferredDuoDevice(s.mock.Ctx, "john").Return(nil)
@ -262,7 +265,7 @@ func (s *SecondFactorDuoPostSuite) TestShouldUseOldDeviceAndSelect() {
preAuthResponse.Result = auth
preAuthResponse.Devices = duoDevices
duoMock.EXPECT().PreAuthCall(s.mock.Ctx, gomock.Eq(values)).Return(&preAuthResponse, nil)
duoMock.EXPECT().PreAuthCall(s.mock.Ctx, &session.UserSession{CookieDomain: "example.com", Username: "john"}, gomock.Eq(values)).Return(&preAuthResponse, nil)
bodyBytes, err := json.Marshal(bodySignDuoRequest{})
s.Require().NoError(err)
@ -302,7 +305,7 @@ func (s *SecondFactorDuoPostSuite) TestShouldUseInvalidMethodAndAutoSelect() {
preAuthResponse.Result = auth
preAuthResponse.Devices = duoDevices
duoMock.EXPECT().PreAuthCall(s.mock.Ctx, gomock.Eq(values)).Return(&preAuthResponse, nil)
duoMock.EXPECT().PreAuthCall(s.mock.Ctx, &session.UserSession{CookieDomain: "example.com", Username: "john"}, gomock.Eq(values)).Return(&preAuthResponse, nil)
s.mock.StorageMock.EXPECT().
SavePreferredDuoDevice(s.mock.Ctx, model.DuoDevice{Username: "john", Device: "12345ABCDEFGHIJ67890", Method: "push"}).
@ -318,7 +321,7 @@ func (s *SecondFactorDuoPostSuite) TestShouldUseInvalidMethodAndAutoSelect() {
authResponse := duo.AuthResponse{}
authResponse.Result = allow
duoMock.EXPECT().AuthCall(s.mock.Ctx, gomock.Eq(values)).Return(&authResponse, nil)
duoMock.EXPECT().AuthCall(s.mock.Ctx, &session.UserSession{CookieDomain: "example.com", Username: "john"}, gomock.Eq(values)).Return(&authResponse, nil)
bodyBytes, err := json.Marshal(bodySignDuoRequest{TargetURL: "https://target.example.com"})
s.Require().NoError(err)
@ -341,7 +344,7 @@ func (s *SecondFactorDuoPostSuite) TestShouldCallDuoPreauthAPIAndAllowAccess() {
preAuthResponse := duo.PreAuthResponse{}
preAuthResponse.Result = allow
duoMock.EXPECT().PreAuthCall(s.mock.Ctx, gomock.Eq(values)).Return(&preAuthResponse, nil)
duoMock.EXPECT().PreAuthCall(s.mock.Ctx, &session.UserSession{CookieDomain: "example.com", Username: "john"}, gomock.Eq(values)).Return(&preAuthResponse, nil)
bodyBytes, err := json.Marshal(bodySignDuoRequest{TargetURL: "https://target.example.com"})
s.Require().NoError(err)
@ -365,7 +368,7 @@ func (s *SecondFactorDuoPostSuite) TestShouldCallDuoPreauthAPIAndDenyAccess() {
preAuthResponse := duo.PreAuthResponse{}
preAuthResponse.Result = deny
duoMock.EXPECT().PreAuthCall(s.mock.Ctx, gomock.Eq(values)).Return(&preAuthResponse, nil)
duoMock.EXPECT().PreAuthCall(s.mock.Ctx, &session.UserSession{CookieDomain: "example.com", Username: "john"}, gomock.Eq(values)).Return(&preAuthResponse, nil)
values = url.Values{}
values.Set("username", "john")
@ -389,7 +392,7 @@ func (s *SecondFactorDuoPostSuite) TestShouldCallDuoPreauthAPIAndFail() {
LoadPreferredDuoDevice(s.mock.Ctx, "john").
Return(&model.DuoDevice{ID: 1, Username: "john", Device: "12345ABCDEFGHIJ67890", Method: "push"}, nil)
duoMock.EXPECT().PreAuthCall(s.mock.Ctx, gomock.Any()).Return(nil, fmt.Errorf("Connnection error"))
duoMock.EXPECT().PreAuthCall(s.mock.Ctx, &session.UserSession{CookieDomain: "example.com", Username: "john"}, gomock.Any()).Return(nil, fmt.Errorf("Connnection error"))
bodyBytes, err := json.Marshal(bodySignDuoRequest{})
s.Require().NoError(err)
@ -430,7 +433,7 @@ func (s *SecondFactorDuoPostSuite) TestShouldCallDuoAPIAndDenyAccess() {
preAuthResponse.Result = auth
preAuthResponse.Devices = duoDevices
duoMock.EXPECT().PreAuthCall(s.mock.Ctx, gomock.Eq(values)).Return(&preAuthResponse, nil)
duoMock.EXPECT().PreAuthCall(s.mock.Ctx, &session.UserSession{CookieDomain: "example.com", Username: "john"}, gomock.Eq(values)).Return(&preAuthResponse, nil)
values = url.Values{}
values.Set("username", "john")
@ -441,7 +444,7 @@ func (s *SecondFactorDuoPostSuite) TestShouldCallDuoAPIAndDenyAccess() {
response := duo.AuthResponse{}
response.Result = deny
duoMock.EXPECT().AuthCall(s.mock.Ctx, gomock.Eq(values)).Return(&response, nil)
duoMock.EXPECT().AuthCall(s.mock.Ctx, &session.UserSession{CookieDomain: "example.com", Username: "john"}, gomock.Eq(values)).Return(&response, nil)
bodyBytes, err := json.Marshal(bodySignDuoRequest{})
s.Require().NoError(err)
@ -470,9 +473,9 @@ func (s *SecondFactorDuoPostSuite) TestShouldCallDuoAPIAndFail() {
preAuthResponse.Result = auth
preAuthResponse.Devices = duoDevices
duoMock.EXPECT().PreAuthCall(s.mock.Ctx, gomock.Eq(values)).Return(&preAuthResponse, nil)
duoMock.EXPECT().PreAuthCall(s.mock.Ctx, &session.UserSession{CookieDomain: "example.com", Username: "john"}, gomock.Eq(values)).Return(&preAuthResponse, nil)
duoMock.EXPECT().AuthCall(s.mock.Ctx, gomock.Any()).Return(nil, fmt.Errorf("Connnection error"))
duoMock.EXPECT().AuthCall(s.mock.Ctx, &session.UserSession{CookieDomain: "example.com", Username: "john"}, gomock.Any()).Return(nil, fmt.Errorf("Connnection error"))
bodyBytes, err := json.Marshal(bodySignDuoRequest{})
s.Require().NoError(err)
@ -513,12 +516,12 @@ func (s *SecondFactorDuoPostSuite) TestShouldRedirectUserToDefaultURL() {
preAuthResponse.Result = auth
preAuthResponse.Devices = duoDevices
duoMock.EXPECT().PreAuthCall(s.mock.Ctx, gomock.Eq(values)).Return(&preAuthResponse, nil)
duoMock.EXPECT().PreAuthCall(s.mock.Ctx, &session.UserSession{CookieDomain: "example.com", Username: "john"}, gomock.Eq(values)).Return(&preAuthResponse, nil)
response := duo.AuthResponse{}
response.Result = allow
duoMock.EXPECT().AuthCall(s.mock.Ctx, gomock.Any()).Return(&response, nil)
duoMock.EXPECT().AuthCall(s.mock.Ctx, &session.UserSession{CookieDomain: "example.com", Username: "john"}, gomock.Any()).Return(&response, nil)
s.mock.Ctx.Configuration.DefaultRedirectionURL = testRedirectionURL
@ -562,12 +565,12 @@ func (s *SecondFactorDuoPostSuite) TestShouldNotReturnRedirectURL() {
preAuthResponse.Result = auth
preAuthResponse.Devices = duoDevices
duoMock.EXPECT().PreAuthCall(s.mock.Ctx, gomock.Eq(values)).Return(&preAuthResponse, nil)
duoMock.EXPECT().PreAuthCall(s.mock.Ctx, &session.UserSession{CookieDomain: "example.com", Username: "john"}, gomock.Eq(values)).Return(&preAuthResponse, nil)
response := duo.AuthResponse{}
response.Result = allow
duoMock.EXPECT().AuthCall(s.mock.Ctx, gomock.Any()).Return(&response, nil)
duoMock.EXPECT().AuthCall(s.mock.Ctx, &session.UserSession{CookieDomain: "example.com", Username: "john"}, gomock.Any()).Return(&response, nil)
bodyBytes, err := json.Marshal(bodySignDuoRequest{})
s.Require().NoError(err)
@ -619,12 +622,12 @@ func (s *SecondFactorDuoPostSuite) TestShouldRedirectUserToSafeTargetURL() {
preAuthResponse.Result = auth
preAuthResponse.Devices = duoDevices
duoMock.EXPECT().PreAuthCall(s.mock.Ctx, gomock.Eq(values)).Return(&preAuthResponse, nil)
duoMock.EXPECT().PreAuthCall(s.mock.Ctx, &session.UserSession{CookieDomain: "example.com", Username: "john"}, gomock.Eq(values)).Return(&preAuthResponse, nil)
response := duo.AuthResponse{}
response.Result = allow
duoMock.EXPECT().AuthCall(s.mock.Ctx, gomock.Any()).Return(&response, nil)
duoMock.EXPECT().AuthCall(s.mock.Ctx, &session.UserSession{CookieDomain: "example.com", Username: "john"}, gomock.Any()).Return(&response, nil)
bodyBytes, err := json.Marshal(bodySignDuoRequest{
TargetURL: "https://example.com",
@ -668,12 +671,12 @@ func (s *SecondFactorDuoPostSuite) TestShouldNotRedirectToUnsafeURL() {
preAuthResponse.Result = auth
preAuthResponse.Devices = duoDevices
duoMock.EXPECT().PreAuthCall(s.mock.Ctx, gomock.Eq(values)).Return(&preAuthResponse, nil)
duoMock.EXPECT().PreAuthCall(s.mock.Ctx, &session.UserSession{CookieDomain: "example.com", Username: "john"}, gomock.Eq(values)).Return(&preAuthResponse, nil)
response := duo.AuthResponse{}
response.Result = allow
duoMock.EXPECT().AuthCall(s.mock.Ctx, gomock.Any()).Return(&response, nil)
duoMock.EXPECT().AuthCall(s.mock.Ctx, &session.UserSession{CookieDomain: "example.com", Username: "john"}, gomock.Any()).Return(&response, nil)
bodyBytes, err := json.Marshal(bodySignDuoRequest{
TargetURL: "http://example.com",
@ -715,12 +718,12 @@ func (s *SecondFactorDuoPostSuite) TestShouldRegenerateSessionForPreventingSessi
preAuthResponse.Result = auth
preAuthResponse.Devices = duoDevices
duoMock.EXPECT().PreAuthCall(s.mock.Ctx, gomock.Eq(values)).Return(&preAuthResponse, nil)
duoMock.EXPECT().PreAuthCall(s.mock.Ctx, &session.UserSession{CookieDomain: "example.com", Username: "john"}, gomock.Eq(values)).Return(&preAuthResponse, nil)
response := duo.AuthResponse{}
response.Result = allow
duoMock.EXPECT().AuthCall(s.mock.Ctx, gomock.Any()).Return(&response, nil)
duoMock.EXPECT().AuthCall(s.mock.Ctx, &session.UserSession{CookieDomain: "example.com", Username: "john"}, gomock.Any()).Return(&response, nil)
bodyBytes, err := json.Marshal(bodySignDuoRequest{
TargetURL: "http://example.com",
@ -734,7 +737,7 @@ func (s *SecondFactorDuoPostSuite) TestShouldRegenerateSessionForPreventingSessi
DuoPOST(duoMock)(s.mock.Ctx)
s.mock.Assert200OK(s.T(), nil)
s.Assert().NotEqual(
s.NotEqual(
res[0][1],
string(s.mock.Ctx.Request.Header.Cookie("authelia_session")))
}

View File

@ -3,13 +3,19 @@ package handlers
import (
"github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/regulation"
"github.com/authelia/authelia/v4/internal/session"
)
// TimeBasedOneTimePasswordPOST validate the TOTP passcode provided by the user.
func TimeBasedOneTimePasswordPOST(ctx *middlewares.AutheliaCtx) {
bodyJSON := bodySignTOTPRequest{}
if err := ctx.ParseBody(&bodyJSON); err != nil {
var (
userSession session.UserSession
err error
)
if err = ctx.ParseBody(&bodyJSON); err != nil {
ctx.Logger.Errorf(logFmtErrParseRequestBody, regulation.AuthTypeTOTP, err)
respondUnauthorized(ctx, messageMFAValidationFailed)
@ -17,7 +23,13 @@ func TimeBasedOneTimePasswordPOST(ctx *middlewares.AutheliaCtx) {
return
}
userSession := ctx.GetSession()
if userSession, err = ctx.GetSession(); err != nil {
ctx.Logger.WithError(err).Error("Error occurred retrieving user session")
respondUnauthorized(ctx, messageMFAValidationFailed)
return
}
config, err := ctx.Providers.StorageProvider.LoadTOTPConfiguration(ctx, userSession.Username)
if err != nil {

View File

@ -7,7 +7,6 @@ import (
"testing"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/authelia/authelia/v4/internal/configuration/schema"
@ -24,10 +23,11 @@ type HandlerSignTOTPSuite struct {
func (s *HandlerSignTOTPSuite) SetupTest() {
s.mock = mocks.NewMockAutheliaCtx(s.T())
userSession := s.mock.Ctx.GetSession()
userSession, err := s.mock.Ctx.GetSession()
s.Assert().NoError(err)
userSession.Username = testUsername
err := s.mock.Ctx.SaveSession(userSession)
require.NoError(s.T(), err)
s.Assert().NoError(s.mock.Ctx.SaveSession(userSession))
}
func (s *HandlerSignTOTPSuite) TearDownTest() {
@ -266,7 +266,7 @@ func (s *HandlerSignTOTPSuite) TestShouldRegenerateSessionForPreventingSessionFi
TimeBasedOneTimePasswordPOST(s.mock.Ctx)
s.mock.Assert200OK(s.T(), nil)
s.Assert().NotEqual(
s.NotEqual(
res[0][1],
string(s.mock.Ctx.Request.Header.Cookie("authelia_session")))
}

View File

@ -9,17 +9,25 @@ import (
"github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/model"
"github.com/authelia/authelia/v4/internal/regulation"
"github.com/authelia/authelia/v4/internal/session"
)
// WebauthnAssertionGET handler starts the assertion ceremony.
func WebauthnAssertionGET(ctx *middlewares.AutheliaCtx) {
var (
w *webauthn.WebAuthn
user *model.WebauthnUser
err error
w *webauthn.WebAuthn
user *model.WebauthnUser
userSession session.UserSession
err error
)
userSession := ctx.GetSession()
if userSession, err = ctx.GetSession(); err != nil {
ctx.Logger.WithError(err).Error("Error occurred retrieving user session")
respondUnauthorized(ctx, messageMFAValidationFailed)
return
}
if w, err = newWebauthn(ctx); err != nil {
ctx.Logger.Errorf("Unable to configure %s during assertion challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
@ -79,8 +87,12 @@ func WebauthnAssertionGET(ctx *middlewares.AutheliaCtx) {
}
// WebauthnAssertionPOST handler completes the assertion ceremony after verifying the challenge.
//
//nolint:gocyclo
func WebauthnAssertionPOST(ctx *middlewares.AutheliaCtx) {
var (
userSession session.UserSession
err error
w *webauthn.WebAuthn
@ -95,7 +107,13 @@ func WebauthnAssertionPOST(ctx *middlewares.AutheliaCtx) {
return
}
userSession := ctx.GetSession()
if userSession, err = ctx.GetSession(); err != nil {
ctx.Logger.WithError(err).Error("Error occurred retrieving user session")
respondUnauthorized(ctx, messageMFAValidationFailed)
return
}
if userSession.Webauthn == nil {
ctx.Logger.Errorf("Webauthn session data is not present in order to handle assertion for user '%s'. This could indicate a user trying to POST to the wrong endpoint, or the session data is not present for the browser they used.", userSession.Username)

View File

@ -2,19 +2,31 @@ package handlers
import (
"github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/session"
)
// StateGET is the handler serving the user state.
func StateGET(ctx *middlewares.AutheliaCtx) {
userSession := ctx.GetSession()
var (
userSession session.UserSession
err error
)
if userSession, err = ctx.GetSession(); err != nil {
ctx.Logger.WithError(err).Error("Error occurred retrieving user session")
ctx.ReplyForbidden()
return
}
stateResponse := StateResponse{
Username: userSession.Username,
AuthenticationLevel: userSession.AuthenticationLevel,
DefaultRedirectionURL: ctx.Configuration.DefaultRedirectionURL,
}
err := ctx.SetJSONBody(stateResponse)
if err != nil {
if err = ctx.SetJSONBody(stateResponse); err != nil {
ctx.Logger.Errorf("Unable to set state response in body: %s", err)
}
}

View File

@ -27,10 +27,11 @@ func (s *StateGetSuite) TearDownTest() {
}
func (s *StateGetSuite) TestShouldReturnUsernameFromSession() {
userSession := s.mock.Ctx.GetSession()
userSession, err := s.mock.Ctx.GetSession()
s.Assert().NoError(err)
userSession.Username = "username"
err := s.mock.Ctx.SaveSession(userSession)
require.NoError(s.T(), err)
s.Assert().NoError(s.mock.Ctx.SaveSession(userSession))
StateGET(s.mock.Ctx)
@ -57,9 +58,11 @@ func (s *StateGetSuite) TestShouldReturnUsernameFromSession() {
}
func (s *StateGetSuite) TestShouldReturnAuthenticationLevelFromSession() {
userSession := s.mock.Ctx.GetSession()
userSession, err := s.mock.Ctx.GetSession()
s.Assert().NoError(err)
userSession.AuthenticationLevel = authentication.OneFactor
err := s.mock.Ctx.SaveSession(userSession)
s.Assert().NoError(s.mock.Ctx.SaveSession(userSession))
require.NoError(s.T(), err)
StateGET(s.mock.Ctx)

View File

@ -8,18 +8,26 @@ import (
"github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/model"
"github.com/authelia/authelia/v4/internal/session"
"github.com/authelia/authelia/v4/internal/utils"
)
// UserInfoPOST handles setting up info for users if necessary when they login.
func UserInfoPOST(ctx *middlewares.AutheliaCtx) {
userSession := ctx.GetSession()
var (
userInfo model.UserInfo
err error
userSession session.UserSession
userInfo model.UserInfo
err error
)
if userSession, err = ctx.GetSession(); err != nil {
ctx.Logger.WithError(err).Error("Error occurred retrieving user session")
ctx.ReplyForbidden()
return
}
if _, err = ctx.Providers.StorageProvider.LoadPreferred2FAMethod(ctx, userSession.Username); err != nil {
if errors.Is(err, sql.ErrNoRows) {
if err = ctx.Providers.StorageProvider.SavePreferred2FAMethod(ctx, userSession.Username, ""); err != nil {
@ -56,7 +64,18 @@ func UserInfoPOST(ctx *middlewares.AutheliaCtx) {
// UserInfoGET get the info related to the user identified by the session.
func UserInfoGET(ctx *middlewares.AutheliaCtx) {
userSession := ctx.GetSession()
var (
userSession session.UserSession
err error
)
if userSession, err = ctx.GetSession(); err != nil {
ctx.Logger.WithError(err).Error("Error occurred retrieving user session")
ctx.ReplyForbidden()
return
}
userInfo, err := ctx.Providers.StorageProvider.LoadUserInfo(ctx, userSession.Username)
if err != nil {
@ -74,10 +93,22 @@ func UserInfoGET(ctx *middlewares.AutheliaCtx) {
// MethodPreferencePOST update the user preferences regarding 2FA method.
func MethodPreferencePOST(ctx *middlewares.AutheliaCtx) {
bodyJSON := bodyPreferred2FAMethod{}
var (
bodyJSON bodyPreferred2FAMethod
err := ctx.ParseBody(&bodyJSON)
if err != nil {
userSession session.UserSession
err error
)
if userSession, err = ctx.GetSession(); err != nil {
ctx.Logger.WithError(err).Error("Error occurred retrieving user session")
ctx.Error(err, messageOperationFailed)
return
}
if err = ctx.ParseBody(&bodyJSON); err != nil {
ctx.Error(err, messageOperationFailed)
return
}
@ -87,7 +118,6 @@ func MethodPreferencePOST(ctx *middlewares.AutheliaCtx) {
return
}
userSession := ctx.GetSession()
ctx.Logger.Debugf("Save new preferred 2FA method of user %s to %s", userSession.Username, bodyJSON.Method)
err = ctx.Providers.StorageProvider.SavePreferred2FAMethod(ctx, userSession.Username, bodyJSON.Method)

View File

@ -9,7 +9,6 @@ import (
"github.com/golang/mock/gomock"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/authelia/authelia/v4/internal/configuration/schema"
@ -24,12 +23,12 @@ type FetchSuite struct {
func (s *FetchSuite) SetupTest() {
s.mock = mocks.NewMockAutheliaCtx(s.T())
// Set the initial user session.
userSession := s.mock.Ctx.GetSession()
userSession, err := s.mock.Ctx.GetSession()
s.Assert().NoError(err)
userSession.Username = testUsername
userSession.AuthenticationLevel = 1
err := s.mock.Ctx.SaveSession(userSession)
require.NoError(s.T(), err)
s.Assert().NoError(s.mock.Ctx.SaveSession(userSession))
}
func (s *FetchSuite) TearDownTest() {
@ -101,12 +100,12 @@ func TestUserInfoEndpoint_SetCorrectMethod(t *testing.T) {
mock := mocks.NewMockAutheliaCtx(t)
// Set the initial user session.
userSession := mock.Ctx.GetSession()
userSession, err := mock.Ctx.GetSession()
assert.NoError(t, err)
userSession.Username = testUsername
userSession.AuthenticationLevel = 1
err := mock.Ctx.SaveSession(userSession)
require.NoError(t, err)
assert.NoError(t, mock.Ctx.SaveSession(userSession))
mock.StorageMock.
EXPECT().
@ -266,12 +265,12 @@ func TestUserInfoEndpoint_SetDefaultMethod(t *testing.T) {
mock.Ctx.Configuration.Session = sessionConfig
}
// Set the initial user session.
userSession := mock.Ctx.GetSession()
userSession, err := mock.Ctx.GetSession()
assert.NoError(t, err)
userSession.Username = testUsername
userSession.AuthenticationLevel = 1
err := mock.Ctx.SaveSession(userSession)
require.NoError(t, err)
assert.NoError(t, mock.Ctx.SaveSession(userSession))
if resp.db.Method == "" {
gomock.InOrder(
@ -372,12 +371,12 @@ type SaveSuite struct {
func (s *SaveSuite) SetupTest() {
s.mock = mocks.NewMockAutheliaCtx(s.T())
// Set the initial user session.
userSession := s.mock.Ctx.GetSession()
userSession, err := s.mock.Ctx.GetSession()
s.Assert().NoError(err)
userSession.Username = testUsername
userSession.AuthenticationLevel = 1
err := s.mock.Ctx.SaveSession(userSession)
require.NoError(s.T(), err)
s.Assert().NoError(s.mock.Ctx.SaveSession(userSession))
}
func (s *SaveSuite) TearDownTest() {

View File

@ -6,15 +6,29 @@ import (
"github.com/valyala/fasthttp"
"github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/model"
"github.com/authelia/authelia/v4/internal/session"
"github.com/authelia/authelia/v4/internal/storage"
)
// UserTOTPInfoGET returns the users TOTP configuration.
func UserTOTPInfoGET(ctx *middlewares.AutheliaCtx) {
userSession := ctx.GetSession()
var (
userSession session.UserSession
err error
)
config, err := ctx.Providers.StorageProvider.LoadTOTPConfiguration(ctx, userSession.Username)
if err != nil {
if userSession, err = ctx.GetSession(); err != nil {
ctx.Logger.WithError(err).Error("Error occurred retrieving user session")
ctx.ReplyForbidden()
return
}
var config *model.TOTPConfiguration
if config, err = ctx.Providers.StorageProvider.LoadTOTPConfiguration(ctx, userSession.Username); err != nil {
if errors.Is(err, storage.ErrNoTOTPConfiguration) {
ctx.SetStatusCode(fasthttp.StatusNotFound)
ctx.SetJSONError("Could not find TOTP Configuration for user.")

View File

@ -1,515 +0,0 @@
package handlers
import (
"bytes"
"encoding/base64"
"fmt"
"net"
"net/url"
"strings"
"time"
"github.com/valyala/fasthttp"
"github.com/authelia/authelia/v4/internal/authentication"
"github.com/authelia/authelia/v4/internal/authorization"
"github.com/authelia/authelia/v4/internal/configuration/schema"
"github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/session"
"github.com/authelia/authelia/v4/internal/utils"
)
func isSchemeWSS(url *url.URL) bool {
return url.Scheme == "wss"
}
// parseBasicAuth parses an HTTP Basic Authentication string.
// "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==" returns ("Aladdin", "open sesame", true).
func parseBasicAuth(header []byte, auth string) (username, password string, err error) {
if !strings.HasPrefix(auth, authPrefix) {
return "", "", fmt.Errorf("%s prefix not found in %s header", strings.Trim(authPrefix, " "), header)
}
c, err := base64.StdEncoding.DecodeString(auth[len(authPrefix):])
if err != nil {
return "", "", err
}
cs := string(c)
s := strings.IndexByte(cs, ':')
if s < 0 {
return "", "", fmt.Errorf("format of %s header must be user:password", header)
}
return cs[:s], cs[s+1:], nil
}
// isTargetURLAuthorized check whether the given user is authorized to access the resource.
func isTargetURLAuthorized(authorizer *authorization.Authorizer, targetURL *url.URL,
username string, userGroups []string, clientIP net.IP, method []byte, authLevel authentication.Level) authorizationMatching {
hasSubject, level := authorizer.GetRequiredLevel(
authorization.Subject{
Username: username,
Groups: userGroups,
IP: clientIP,
},
authorization.NewObjectRaw(targetURL, method))
switch {
case level == authorization.Bypass:
return Authorized
case level == authorization.Denied && (username != "" || !hasSubject):
// If the user is not anonymous, it means that we went through
// all the rules related to that user and knowing who he is we can
// deduce the access is forbidden
// For anonymous users though, we check that the matched rule has no subject
// if matched rule has not subject then this rule applies to all users including anonymous.
return Forbidden
case level == authorization.OneFactor && authLevel >= authentication.OneFactor,
level == authorization.TwoFactor && authLevel >= authentication.TwoFactor:
return Authorized
}
return NotAuthorized
}
// verifyBasicAuth verify that the provided username and password are correct and
// that the user is authorized to target the resource.
func verifyBasicAuth(ctx *middlewares.AutheliaCtx, header, auth []byte) (username, name string, groups, emails []string, authLevel authentication.Level, err error) {
username, password, err := parseBasicAuth(header, string(auth))
if err != nil {
return "", "", nil, nil, authentication.NotAuthenticated, fmt.Errorf("unable to parse content of %s header: %s", header, err)
}
authenticated, err := ctx.Providers.UserProvider.CheckUserPassword(username, password)
if err != nil {
return "", "", nil, nil, authentication.NotAuthenticated, fmt.Errorf("unable to check credentials extracted from %s header: %w", header, err)
}
// If the user is not correctly authenticated, send a 401.
if !authenticated {
// Request Basic Authentication otherwise.
return "", "", nil, nil, authentication.NotAuthenticated, fmt.Errorf("user %s is not authenticated", username)
}
details, err := ctx.Providers.UserProvider.GetDetails(username)
if err != nil {
return "", "", nil, nil, authentication.NotAuthenticated, fmt.Errorf("unable to retrieve details of user %s: %s", username, err)
}
return username, details.DisplayName, details.Groups, details.Emails, authentication.OneFactor, nil
}
// setForwardedHeaders set the forwarded User, Groups, Name and Email headers.
func setForwardedHeaders(headers *fasthttp.ResponseHeader, username, name string, groups, emails []string) {
if username != "" {
headers.SetBytesK(headerRemoteUser, username)
headers.SetBytesK(headerRemoteGroups, strings.Join(groups, ","))
headers.SetBytesK(headerRemoteName, name)
if emails != nil {
headers.SetBytesK(headerRemoteEmail, emails[0])
} else {
headers.SetBytesK(headerRemoteEmail, "")
}
}
}
func isSessionInactiveTooLong(ctx *middlewares.AutheliaCtx, userSession *session.UserSession, isUserAnonymous bool) (isInactiveTooLong bool) {
domainSession, err := ctx.GetSessionProvider()
if err != nil {
return false
}
if userSession.KeepMeLoggedIn || isUserAnonymous || int64(domainSession.Config.Inactivity.Seconds()) == 0 {
return false
}
isInactiveTooLong = time.Unix(userSession.LastActivity, 0).Add(domainSession.Config.Inactivity).Before(ctx.Clock.Now())
ctx.Logger.Tracef("Inactivity report for user '%s'. Current Time: %d, Last Activity: %d, Maximum Inactivity: %d.", userSession.Username, ctx.Clock.Now().Unix(), userSession.LastActivity, int(domainSession.Config.Inactivity.Seconds()))
return isInactiveTooLong
}
// verifySessionCookie verifies if a user is identified by a cookie.
func verifySessionCookie(ctx *middlewares.AutheliaCtx, targetURL *url.URL, userSession *session.UserSession, refreshProfile bool,
refreshProfileInterval time.Duration) (username, name string, groups, emails []string, authLevel authentication.Level, err error) {
// No username in the session means the user is anonymous.
isUserAnonymous := userSession.IsAnonymous()
if isUserAnonymous && userSession.AuthenticationLevel != authentication.NotAuthenticated {
return "", "", nil, nil, authentication.NotAuthenticated, fmt.Errorf("an anonymous user cannot be authenticated (this might be the sign of a security compromise)")
}
if isSessionInactiveTooLong(ctx, userSession, isUserAnonymous) {
// Destroy the session a new one will be regenerated on next request.
if err = ctx.DestroySession(); err != nil {
return "", "", nil, nil, authentication.NotAuthenticated, fmt.Errorf("unable to destroy session for user '%s' after the session has been inactive too long: %w", userSession.Username, err)
}
ctx.Logger.Warnf("Session destroyed for user '%s' after exceeding configured session inactivity and not being marked as remembered", userSession.Username)
return "", "", nil, nil, authentication.NotAuthenticated, nil
}
if err = verifySessionHasUpToDateProfile(ctx, targetURL, userSession, refreshProfile, refreshProfileInterval); err != nil {
if err == authentication.ErrUserNotFound {
if err = ctx.DestroySession(); err != nil {
ctx.Logger.Errorf("Unable to destroy user session after provider refresh didn't find the user: %v", err)
}
return userSession.Username, userSession.DisplayName, userSession.Groups, userSession.Emails, authentication.NotAuthenticated, err
}
ctx.Logger.Errorf("Error occurred while attempting to update user details from LDAP: %v", err)
return "", "", nil, nil, authentication.NotAuthenticated, err
}
return userSession.Username, userSession.DisplayName, userSession.Groups, userSession.Emails, userSession.AuthenticationLevel, nil
}
func handleUnauthorized(ctx *middlewares.AutheliaCtx, targetURL fmt.Stringer, cookieDomain string, isBasicAuth bool, username string, method []byte) {
var (
statusCode int
friendlyUsername string
friendlyRequestMethod string
)
switch username {
case "":
friendlyUsername = "<anonymous>"
default:
friendlyUsername = username
}
if isBasicAuth {
ctx.Logger.Infof("Access to %s is not authorized to user %s, sending 401 response with basic auth header", targetURL.String(), friendlyUsername)
ctx.ReplyUnauthorized()
ctx.Response.Header.Add("WWW-Authenticate", "Basic realm=\"Authentication required\"")
return
}
rm := string(method)
switch rm {
case "":
friendlyRequestMethod = "unknown"
default:
friendlyRequestMethod = rm
}
redirectionURL := ctxGetPortalURL(ctx)
if redirectionURL != nil {
if !utils.IsURISafeRedirection(redirectionURL, cookieDomain) {
ctx.Logger.Errorf("Configured Portal URL '%s' does not appear to be able to write cookies for the '%s' domain", redirectionURL, cookieDomain)
ctx.ReplyUnauthorized()
return
}
qry := redirectionURL.Query()
qry.Set(queryArgRD, targetURL.String())
if rm != "" {
qry.Set("rm", rm)
}
redirectionURL.RawQuery = qry.Encode()
}
switch {
case ctx.IsXHR() || !ctx.AcceptsMIME("text/html") || redirectionURL == nil:
statusCode = fasthttp.StatusUnauthorized
default:
switch rm {
case fasthttp.MethodGet, fasthttp.MethodOptions, "":
statusCode = fasthttp.StatusFound
default:
statusCode = fasthttp.StatusSeeOther
}
}
if redirectionURL != nil {
ctx.Logger.Infof("Access to %s (method %s) is not authorized to user %s, responding with status code %d with location redirect to %s", targetURL.String(), friendlyRequestMethod, friendlyUsername, statusCode, redirectionURL)
ctx.SpecialRedirect(redirectionURL.String(), statusCode)
} else {
ctx.Logger.Infof("Access to %s (method %s) is not authorized to user %s, responding with status code %d", targetURL.String(), friendlyRequestMethod, friendlyUsername, statusCode)
ctx.ReplyUnauthorized()
}
}
func updateActivityTimestamp(ctx *middlewares.AutheliaCtx, isBasicAuth bool) error {
if isBasicAuth {
return nil
}
userSession := ctx.GetSession()
// We don't need to update the activity timestamp when user checked keep me logged in.
if userSession.KeepMeLoggedIn {
return nil
}
// Mark current activity.
userSession.LastActivity = ctx.Clock.Now().Unix()
return ctx.SaveSession(userSession)
}
// generateVerifySessionHasUpToDateProfileTraceLogs is used to generate trace logs only when trace logging is enabled.
// The information calculated in this function is completely useless other than trace for now.
func generateVerifySessionHasUpToDateProfileTraceLogs(ctx *middlewares.AutheliaCtx, userSession *session.UserSession,
details *authentication.UserDetails) {
groupsAdded, groupsRemoved := utils.StringSlicesDelta(userSession.Groups, details.Groups)
emailsAdded, emailsRemoved := utils.StringSlicesDelta(userSession.Emails, details.Emails)
nameDelta := userSession.DisplayName != details.DisplayName
// Check Groups.
var groupsDelta []string
if len(groupsAdded) != 0 {
groupsDelta = append(groupsDelta, fmt.Sprintf("added: %s.", strings.Join(groupsAdded, ", ")))
}
if len(groupsRemoved) != 0 {
groupsDelta = append(groupsDelta, fmt.Sprintf("removed: %s.", strings.Join(groupsRemoved, ", ")))
}
if len(groupsDelta) != 0 {
ctx.Logger.Tracef("Updated groups detected for %s. %s", userSession.Username, strings.Join(groupsDelta, " "))
} else {
ctx.Logger.Tracef("No updated groups detected for %s", userSession.Username)
}
// Check Emails.
var emailsDelta []string
if len(emailsAdded) != 0 {
emailsDelta = append(emailsDelta, fmt.Sprintf("added: %s.", strings.Join(emailsAdded, ", ")))
}
if len(emailsRemoved) != 0 {
emailsDelta = append(emailsDelta, fmt.Sprintf("removed: %s.", strings.Join(emailsRemoved, ", ")))
}
if len(emailsDelta) != 0 {
ctx.Logger.Tracef("Updated emails detected for %s. %s", userSession.Username, strings.Join(emailsDelta, " "))
} else {
ctx.Logger.Tracef("No updated emails detected for %s", userSession.Username)
}
// Check Name.
if nameDelta {
ctx.Logger.Tracef("Updated display name detected for %s. Added: %s. Removed: %s.", userSession.Username, details.DisplayName, userSession.DisplayName)
} else {
ctx.Logger.Tracef("No updated display name detected for %s", userSession.Username)
}
}
func verifySessionHasUpToDateProfile(ctx *middlewares.AutheliaCtx, targetURL *url.URL, userSession *session.UserSession,
refreshProfile bool, refreshProfileInterval time.Duration) error {
// TODO: Add a check for LDAP password changes based on a time format attribute.
// See https://www.authelia.com/o/threatmodel#potential-future-guarantees
ctx.Logger.Tracef("Checking if we need check the authentication backend for an updated profile for %s.", userSession.Username)
if !refreshProfile || userSession.IsAnonymous() || targetURL == nil {
return nil
}
if refreshProfileInterval != schema.RefreshIntervalAlways && userSession.RefreshTTL.After(ctx.Clock.Now()) {
return nil
}
ctx.Logger.Debugf("Checking the authentication backend for an updated profile for user %s", userSession.Username)
details, err := ctx.Providers.UserProvider.GetDetails(userSession.Username)
// Only update the session if we could get the new details.
if err != nil {
return err
}
emailsDiff := utils.IsStringSlicesDifferent(userSession.Emails, details.Emails)
groupsDiff := utils.IsStringSlicesDifferent(userSession.Groups, details.Groups)
nameDiff := userSession.DisplayName != details.DisplayName
if !groupsDiff && !emailsDiff && !nameDiff {
ctx.Logger.Tracef("Updated profile not detected for %s.", userSession.Username)
// Only update TTL if the user has an interval set.
// We get to this check when there were no changes.
// Also make sure to update the session even if no difference was found.
// This is so that we don't check every subsequent request after this one.
if refreshProfileInterval != schema.RefreshIntervalAlways {
// Update RefreshTTL and save session if refresh is not set to always.
userSession.RefreshTTL = ctx.Clock.Now().Add(refreshProfileInterval)
return ctx.SaveSession(*userSession)
}
} else {
ctx.Logger.Debugf("Updated profile detected for %s.", userSession.Username)
if ctx.Configuration.Log.Level == "trace" {
generateVerifySessionHasUpToDateProfileTraceLogs(ctx, userSession, details)
}
userSession.Emails = details.Emails
userSession.Groups = details.Groups
userSession.DisplayName = details.DisplayName
// Only update TTL if the user has a interval set.
if refreshProfileInterval != schema.RefreshIntervalAlways {
userSession.RefreshTTL = ctx.Clock.Now().Add(refreshProfileInterval)
}
// Return the result of save session if there were changes.
return ctx.SaveSession(*userSession)
}
// Return nil if disabled or if no changes and refresh interval set to always.
return nil
}
func getProfileRefreshSettings(cfg schema.AuthenticationBackend) (refresh bool, refreshInterval time.Duration) {
if cfg.LDAP != nil {
if cfg.RefreshInterval == schema.ProfileRefreshDisabled {
refresh = false
refreshInterval = 0
} else {
refresh = true
if cfg.RefreshInterval != schema.ProfileRefreshAlways {
// Skip Error Check since validator checks it.
refreshInterval, _ = utils.ParseDurationString(cfg.RefreshInterval)
} else {
refreshInterval = schema.RefreshIntervalAlways
}
}
}
return refresh, refreshInterval
}
func verifyAuth(ctx *middlewares.AutheliaCtx, targetURL *url.URL, refreshProfile bool, refreshProfileInterval time.Duration) (isBasicAuth bool, username, name string, groups, emails []string, authLevel authentication.Level, err error) {
authHeader := headerProxyAuthorization
if bytes.Equal(ctx.QueryArgs().Peek("auth"), []byte("basic")) {
authHeader = headerAuthorization
isBasicAuth = true
}
authValue := ctx.Request.Header.PeekBytes(authHeader)
if authValue != nil {
isBasicAuth = true
} else if isBasicAuth {
return isBasicAuth, username, name, groups, emails, authLevel, fmt.Errorf("basic auth requested via query arg, but no value provided via %s header", authHeader)
}
if isBasicAuth {
username, name, groups, emails, authLevel, err = verifyBasicAuth(ctx, authHeader, authValue)
return isBasicAuth, username, name, groups, emails, authLevel, err
}
userSession := ctx.GetSession()
if username, name, groups, emails, authLevel, err = verifySessionCookie(ctx, targetURL, &userSession, refreshProfile, refreshProfileInterval); err != nil {
return isBasicAuth, username, name, groups, emails, authLevel, err
}
sessionUsername := ctx.Request.Header.PeekBytes(headerSessionUsername)
if sessionUsername != nil && !strings.EqualFold(string(sessionUsername), username) {
ctx.Logger.Warnf("Possible cookie hijack or attempt to bypass security detected destroying the session and sending 401 response")
if err = ctx.DestroySession(); err != nil {
ctx.Logger.Errorf("Unable to destroy user session after handler could not match them to their %s header: %s", headerSessionUsername, err)
}
return isBasicAuth, username, name, groups, emails, authLevel, fmt.Errorf("could not match user %s to their %s header with a value of %s when visiting %s", username, headerSessionUsername, sessionUsername, targetURL.String())
}
return isBasicAuth, username, name, groups, emails, authLevel, err
}
// VerifyGET returns the handler verifying if a request is allowed to go through.
func VerifyGET(cfg schema.AuthenticationBackend) middlewares.RequestHandler {
refreshProfile, refreshProfileInterval := getProfileRefreshSettings(cfg)
return func(ctx *middlewares.AutheliaCtx) {
ctx.Logger.Tracef("Headers=%s", ctx.Request.Header.String())
targetURL, err := ctx.GetOriginalURL()
if err != nil {
ctx.Logger.Errorf("Unable to parse target URL: %s", err)
ctx.ReplyUnauthorized()
return
}
if !utils.IsURISecure(targetURL) {
ctx.Logger.Errorf("Scheme of target URL %s must be secure since cookies are "+
"only transported over a secure connection for security reasons", targetURL.String())
ctx.ReplyUnauthorized()
return
}
cookieDomain := ctx.GetTargetURICookieDomain(targetURL)
if cookieDomain == "" {
l := len(ctx.Configuration.Session.Cookies)
if l == 1 {
ctx.Logger.Errorf("Target URL '%s' was not detected as a match to the '%s' session cookie domain",
targetURL.String(), ctx.Configuration.Session.Cookies[0].Domain)
} else {
domains := make([]string, 0, len(ctx.Configuration.Session.Cookies))
for i, domain := range ctx.Configuration.Session.Cookies {
domains[i] = domain.Domain
}
ctx.Logger.Errorf("Target URL '%s' was not detected as a match to any of the '%s' session cookie domains",
targetURL.String(), strings.Join(domains, "', '"))
}
ctx.ReplyUnauthorized()
return
}
ctx.Logger.Debugf("Target URL '%s' was detected as a match to the '%s' session cookie domain", targetURL.String(), cookieDomain)
method := ctx.XForwardedMethod()
isBasicAuth, username, name, groups, emails, authLevel, err := verifyAuth(ctx, targetURL, refreshProfile, refreshProfileInterval)
if err != nil {
ctx.Logger.Errorf("Error caught when verifying user authorization: %s", err)
if err = updateActivityTimestamp(ctx, isBasicAuth); err != nil {
ctx.Error(fmt.Errorf("unable to update last activity: %s", err), messageOperationFailed)
return
}
handleUnauthorized(ctx, targetURL, cookieDomain, isBasicAuth, username, method)
return
}
authorized := isTargetURLAuthorized(ctx.Providers.Authorizer, targetURL, username,
groups, ctx.RemoteIP(), method, authLevel)
switch authorized {
case Forbidden:
ctx.Logger.Infof("Access to %s is forbidden to user %s", targetURL.String(), username)
ctx.ReplyForbidden()
case NotAuthorized:
handleUnauthorized(ctx, targetURL, cookieDomain, isBasicAuth, username, method)
case Authorized:
setForwardedHeaders(&ctx.Response.Header, username, name, groups, emails)
}
if err = updateActivityTimestamp(ctx, isBasicAuth); err != nil {
ctx.Error(fmt.Errorf("unable to update last activity: %s", err), messageOperationFailed)
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -11,6 +11,7 @@ import (
"github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/model"
"github.com/authelia/authelia/v4/internal/regulation"
"github.com/authelia/authelia/v4/internal/session"
"github.com/authelia/authelia/v4/internal/storage"
)
@ -34,9 +35,20 @@ func getWebauthnDeviceIDFromContext(ctx *middlewares.AutheliaCtx) (int, error) {
// WebauthnDevicesGET returns all devices registered for the current user.
func WebauthnDevicesGET(ctx *middlewares.AutheliaCtx) {
s := ctx.GetSession()
var (
userSession session.UserSession
err error
)
devices, err := ctx.Providers.StorageProvider.LoadWebauthnDevicesByUsername(ctx, s.Username)
if userSession, err = ctx.GetSession(); err != nil {
ctx.Logger.WithError(err).Error("Error occurred retrieving user session")
ctx.ReplyForbidden()
return
}
devices, err := ctx.Providers.StorageProvider.LoadWebauthnDevicesByUsername(ctx, userSession.Username)
if err != nil && err != storage.ErrNoWebauthnDevice {
ctx.Error(err, messageOperationFailed)
@ -54,15 +66,23 @@ func WebauthnDevicePUT(ctx *middlewares.AutheliaCtx) {
var (
bodyJSON bodyEditWebauthnDeviceRequest
id int
device *model.WebauthnDevice
err error
id int
device *model.WebauthnDevice
userSession session.UserSession
err error
)
s := ctx.GetSession()
if userSession, err = ctx.GetSession(); err != nil {
ctx.Logger.WithError(err).Error("Error occurred retrieving user session")
ctx.ReplyForbidden()
return
}
if err = json.Unmarshal(ctx.PostBody(), &bodyJSON); err != nil {
ctx.Logger.Errorf("Unable to parse %s update request data for user '%s': %+v", regulation.AuthTypeWebauthn, s.Username, err)
ctx.Logger.Errorf("Unable to parse %s update request data for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
ctx.SetStatusCode(fasthttp.StatusBadRequest)
ctx.Error(err, messageOperationFailed)
@ -79,12 +99,12 @@ func WebauthnDevicePUT(ctx *middlewares.AutheliaCtx) {
return
}
if device.Username != s.Username {
ctx.Error(fmt.Errorf("user '%s' tried to delete device with id '%d' which belongs to '%s", s.Username, device.ID, device.Username), messageOperationFailed)
if device.Username != userSession.Username {
ctx.Error(fmt.Errorf("user '%s' tried to delete device with id '%d' which belongs to '%s", userSession.Username, device.ID, device.Username), messageOperationFailed)
return
}
if err = ctx.Providers.StorageProvider.UpdateWebauthnDeviceDescription(ctx, s.Username, id, bodyJSON.Description); err != nil {
if err = ctx.Providers.StorageProvider.UpdateWebauthnDeviceDescription(ctx, userSession.Username, id, bodyJSON.Description); err != nil {
ctx.Error(err, messageOperationFailed)
return
}
@ -93,9 +113,10 @@ func WebauthnDevicePUT(ctx *middlewares.AutheliaCtx) {
// WebauthnDeviceDELETE deletes a specific device for the current user.
func WebauthnDeviceDELETE(ctx *middlewares.AutheliaCtx) {
var (
id int
device *model.WebauthnDevice
err error
id int
device *model.WebauthnDevice
userSession session.UserSession
err error
)
if id, err = getWebauthnDeviceIDFromContext(ctx); err != nil {
@ -107,10 +128,16 @@ func WebauthnDeviceDELETE(ctx *middlewares.AutheliaCtx) {
return
}
s := ctx.GetSession()
if userSession, err = ctx.GetSession(); err != nil {
ctx.Logger.WithError(err).Error("Error occurred retrieving user session")
if device.Username != s.Username {
ctx.Error(fmt.Errorf("user '%s' tried to delete device with id '%d' which belongs to '%s", s.Username, device.ID, device.Username), messageOperationFailed)
ctx.ReplyForbidden()
return
}
if device.Username != userSession.Username {
ctx.Error(fmt.Errorf("user '%s' tried to delete device with id '%d' which belongs to '%s", userSession.Username, device.ID, device.Username), messageOperationFailed)
return
}

View File

@ -13,6 +13,7 @@ import (
"github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/model"
"github.com/authelia/authelia/v4/internal/oidc"
"github.com/authelia/authelia/v4/internal/session"
)
// Handle1FAResponse handle the redirection upon 1FA authentication.
@ -155,7 +156,13 @@ func handleOIDCWorkflowResponseWithTargetURL(ctx *middlewares.AutheliaCtx, targe
return
}
userSession := ctx.GetSession()
var userSession session.UserSession
if userSession, err = ctx.GetSession(); err != nil {
ctx.Error(fmt.Errorf("unable to redirect to '%s': failed to lookup session: %w", targetURL, err), messageAuthenticationFailed)
return
}
if userSession.IsAnonymous() {
ctx.Error(fmt.Errorf("unable to redirect to '%s': user is anonymous", targetURL), messageAuthenticationFailed)
@ -200,7 +207,13 @@ func handleOIDCWorkflowResponseWithID(ctx *middlewares.AutheliaCtx, id string) {
return
}
userSession := ctx.GetSession()
var userSession session.UserSession
if userSession, err = ctx.GetSession(); err != nil {
ctx.Error(fmt.Errorf("unable to redirect for authorization/consent for client with id '%s' with consent challenge id '%s': failed to lookup session: %w", client.ID, consent.ChallengeID, err), messageAuthenticationFailed)
return
}
if userSession.IsAnonymous() {
ctx.Error(fmt.Errorf("unable to redirect for authorization/consent for client with id '%s' with consent challenge id '%s': user is anonymous", client.ID, consent.ChallengeID), messageAuthenticationFailed)
@ -251,7 +264,7 @@ func markAuthenticationAttempt(ctx *middlewares.AutheliaCtx, successful bool, ba
refererURL, err := url.ParseRequestURI(string(referer))
if err == nil {
requestURI = refererURL.Query().Get(queryArgRD)
requestMethod = refererURL.Query().Get("rm")
requestMethod = refererURL.Query().Get(queryArgRM)
}
}

View File

@ -17,8 +17,6 @@ import (
// MethodList is the list of available methods.
type MethodList = []string
type authorizationMatching int
// configurationBody the content returned by the configuration endpoint.
type configurationBody struct {
AvailableMethods MethodList `json:"available_methods"`

View File

@ -1,33 +1,13 @@
package handlers
import (
"bytes"
"fmt"
"net/url"
"github.com/authelia/authelia/v4/internal/authentication"
"github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/templates"
)
var bytesEmpty = []byte("")
func ctxGetPortalURL(ctx *middlewares.AutheliaCtx) (portalURL *url.URL) {
var rawURL []byte
if rawURL = ctx.QueryArgRedirect(); rawURL != nil && !bytes.Equal(rawURL, bytesEmpty) {
portalURL, _ = url.ParseRequestURI(string(rawURL))
return portalURL
} else if rawURL = ctx.XAutheliaURL(); rawURL != nil && !bytes.Equal(rawURL, bytesEmpty) {
portalURL, _ = url.ParseRequestURI(string(rawURL))
return portalURL
}
return nil
}
func ctxLogEvent(ctx *middlewares.AutheliaCtx, username, description string, eventDetails map[string]any) {
var (
details *authentication.UserDetails

View File

@ -35,7 +35,7 @@ func newWebauthn(ctx *middlewares.AutheliaCtx) (w *webauthn.WebAuthn, err error)
u *url.URL
)
if u, err = ctx.GetOriginalURL(); err != nil {
if u, err = ctx.GetXOriginalURLOrXForwardedURL(); err != nil {
return nil, err
}

View File

@ -151,7 +151,7 @@ func TestWebauthnNewWebauthnShouldReturnErrWhenHeadersNotAvailable(t *testing.T)
w, err := newWebauthn(ctx.Ctx)
assert.Nil(t, w)
assert.EqualError(t, err, "Missing header X-Forwarded-Host")
assert.EqualError(t, err, "missing required X-Forwarded-Host header")
}
func TestWebauthnNewWebauthnShouldReturnErrWhenWebauthnNotConfigured(t *testing.T) {

View File

@ -15,6 +15,6 @@ type Provider interface {
// Recorder of metrics.
type Recorder interface {
RecordRequest(statusCode, requestMethod string, elapsed time.Duration)
RecordVerifyRequest(statusCode string)
RecordAuthz(statusCode string)
RecordAuthenticationDuration(success bool, elapsed time.Duration)
}

Some files were not shown because too many files have changed in this diff Show More