summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--api/openapi.yml262
-rw-r--r--cmd/authelia-gen/cmd_code.go118
-rw-r--r--cmd/authelia-gen/helpers.go131
-rw-r--r--config.template.yml30
-rw-r--r--docs/content/en/configuration/miscellaneous/server-endpoints-authz.md72
-rw-r--r--docs/content/en/configuration/miscellaneous/server.md56
-rw-r--r--docs/content/en/contributing/development/environment.md22
-rw-r--r--docs/content/en/integration/kubernetes/istio.md18
-rw-r--r--docs/content/en/integration/kubernetes/nginx-ingress.md6
-rw-r--r--docs/content/en/integration/kubernetes/traefik-ingress.md15
-rw-r--r--docs/content/en/integration/proxies/caddy.md9
-rw-r--r--docs/content/en/integration/proxies/envoy.md16
-rw-r--r--docs/content/en/integration/proxies/haproxy.md15
-rw-r--r--docs/content/en/integration/proxies/introduction.md11
-rw-r--r--docs/content/en/integration/proxies/nginx.md18
-rw-r--r--docs/content/en/integration/proxies/support.md31
-rw-r--r--docs/content/en/integration/proxies/traefik.md41
-rw-r--r--docs/content/en/integration/proxies/traefikv1.md17
-rw-r--r--docs/content/en/reference/guides/metrics.md25
-rw-r--r--docs/content/en/reference/guides/proxy-authorization.md247
-rw-r--r--docs/data/configkeys.json2
-rw-r--r--examples/compose/lite/docker-compose.yml6
-rw-r--r--examples/compose/local/docker-compose.yml6
-rw-r--r--internal/authentication/const.go2
-rw-r--r--internal/authorization/authorizer.go6
-rw-r--r--internal/authorization/const.go3
-rw-r--r--internal/commands/root.go8
-rw-r--r--internal/configuration/config.template.yml30
-rw-r--r--internal/configuration/deprecation.go14
-rw-r--r--internal/configuration/schema/keys.go8
-rw-r--r--internal/configuration/schema/server.go75
-rw-r--r--internal/configuration/test_resources/config.yml19
-rw-r--r--internal/configuration/validator/const.go24
-rw-r--r--internal/configuration/validator/keys.go66
-rw-r--r--internal/configuration/validator/server.go94
-rw-r--r--internal/configuration/validator/server_test.go167
-rw-r--r--internal/duo/duo.go21
-rw-r--r--internal/duo/types.go7
-rw-r--r--internal/handlers/const.go40
-rw-r--r--internal/handlers/const_test.go35
-rw-r--r--internal/handlers/duo.go6
-rw-r--r--internal/handlers/handler_authz.go170
-rw-r--r--internal/handlers/handler_authz_authn.go448
-rw-r--r--internal/handlers/handler_authz_builder.go190
-rw-r--r--internal/handlers/handler_authz_common.go114
-rw-r--r--internal/handlers/handler_authz_impl_authrequest.go42
-rw-r--r--internal/handlers/handler_authz_impl_authrequest_test.go456
-rw-r--r--internal/handlers/handler_authz_impl_extauthz.go55
-rw-r--r--internal/handlers/handler_authz_impl_extauthz_test.go615
-rw-r--r--internal/handlers/handler_authz_impl_forwardauth.go55
-rw-r--r--internal/handlers/handler_authz_impl_forwardauth_test.go610
-rw-r--r--internal/handlers/handler_authz_impl_legacy.go64
-rw-r--r--internal/handlers/handler_authz_impl_legacy_test.go555
-rw-r--r--internal/handlers/handler_authz_test.go1573
-rw-r--r--internal/handlers/handler_authz_types.go156
-rw-r--r--internal/handlers/handler_authz_util.go92
-rw-r--r--internal/handlers/handler_checks_safe_redirection.go14
-rw-r--r--internal/handlers/handler_checks_safe_redirection_test.go13
-rw-r--r--internal/handlers/handler_firstfactor.go24
-rw-r--r--internal/handlers/handler_firstfactor_test.go47
-rw-r--r--internal/handlers/handler_logout_test.go11
-rw-r--r--internal/handlers/handler_oidc_authorization.go16
-rw-r--r--internal/handlers/handler_oidc_consent.go7
-rw-r--r--internal/handlers/handler_register_duo_device.go62
-rw-r--r--internal/handlers/handler_register_duo_device_test.go18
-rw-r--r--internal/handlers/handler_register_totp.go12
-rw-r--r--internal/handlers/handler_register_webauthn.go26
-rw-r--r--internal/handlers/handler_reset_password_step1.go13
-rw-r--r--internal/handlers/handler_reset_password_step2.go14
-rw-r--r--internal/handlers/handler_sign_duo.go35
-rw-r--r--internal/handlers/handler_sign_duo_test.go67
-rw-r--r--internal/handlers/handler_sign_totp.go16
-rw-r--r--internal/handlers/handler_sign_totp_test.go10
-rw-r--r--internal/handlers/handler_sign_webauthn.go28
-rw-r--r--internal/handlers/handler_state.go18
-rw-r--r--internal/handlers/handler_state_test.go13
-rw-r--r--internal/handlers/handler_user_info.go48
-rw-r--r--internal/handlers/handler_user_info_test.go33
-rw-r--r--internal/handlers/handler_user_totp.go20
-rw-r--r--internal/handlers/handler_verify.go515
-rw-r--r--internal/handlers/handler_verify_test.go1503
-rw-r--r--internal/handlers/response.go19
-rw-r--r--internal/handlers/types.go2
-rw-r--r--internal/handlers/util.go20
-rw-r--r--internal/handlers/webauthn.go2
-rw-r--r--internal/handlers/webauthn_test.go2
-rw-r--r--internal/metrics/metrics.go2
-rw-r--r--internal/metrics/prometheus.go46
-rw-r--r--internal/middlewares/authelia_context.go306
-rw-r--r--internal/middlewares/authelia_context_test.go16
-rw-r--r--internal/middlewares/const.go12
-rw-r--r--internal/middlewares/errors.go15
-rw-r--r--internal/middlewares/metrics.go6
-rw-r--r--internal/middlewares/require_first_factor.go2
-rw-r--r--internal/middlewares/strip_path.go2
-rw-r--r--internal/middlewares/types.go2
-rw-r--r--internal/mocks/authelia_ctx.go132
-rw-r--r--internal/mocks/duo_api.go28
-rw-r--r--internal/regulation/regulator.go2
-rw-r--r--internal/regulation/types.go2
-rw-r--r--internal/server/const.go6
-rw-r--r--internal/server/handlers.go64
-rw-r--r--internal/server/server_test.go17
-rw-r--r--internal/server/template.go18
-rw-r--r--internal/session/provider_test.go6
-rw-r--r--internal/session/session.go57
-rw-r--r--internal/session/types.go2
-rw-r--r--internal/suites/BypassAll/configuration.yml4
-rw-r--r--internal/suites/CLI/configuration.yml2
-rw-r--r--internal/suites/Caddy/configuration.yml9
-rw-r--r--internal/suites/Docker/configuration.yml4
-rw-r--r--internal/suites/DuoPush/configuration.yml4
-rw-r--r--internal/suites/Envoy/configuration.yml6
-rw-r--r--internal/suites/HAProxy/configuration.yml4
-rw-r--r--internal/suites/LDAP/configuration.yml4
-rw-r--r--internal/suites/MariaDB/configuration.yml4
-rw-r--r--internal/suites/MySQL/configuration.yml4
-rw-r--r--internal/suites/NetworkACL/configuration.yml4
-rw-r--r--internal/suites/OIDC/configuration.yml3
-rw-r--r--internal/suites/OIDCTraefik/configuration.yml5
-rw-r--r--internal/suites/OneFactorOnly/configuration.yml4
-rw-r--r--internal/suites/PathPrefix/configuration.yml4
-rw-r--r--internal/suites/Postgres/configuration.yml4
-rw-r--r--internal/suites/ShortTimeouts/configuration.yml5
-rw-r--r--internal/suites/Standalone/configuration.yml4
-rw-r--r--internal/suites/Traefik/configuration.yml4
-rw-r--r--internal/suites/Traefik2/configuration.yml10
-rw-r--r--internal/suites/const.go6
-rw-r--r--internal/suites/example/compose/authelia/docker-compose.backend.dev.yml4
-rw-r--r--internal/suites/example/compose/caddy/Caddyfile4
-rw-r--r--internal/suites/example/compose/envoy/envoy.yaml12
-rw-r--r--internal/suites/example/compose/haproxy/haproxy.cfg22
-rw-r--r--internal/suites/example/compose/httpbin/docker-compose.yml4
-rw-r--r--internal/suites/example/compose/nginx/backend/docker-compose.yml2
-rw-r--r--internal/suites/example/compose/nginx/portal/nginx.conf14
-rw-r--r--internal/suites/example/compose/traefik2/docker-compose.yml2
-rw-r--r--internal/suites/example/kube/authelia/authelia.yml13
-rw-r--r--internal/suites/example/kube/authelia/configs/configuration.yml5
-rw-r--r--internal/suites/scenario_multiple_cookie_domain_test.go29
-rw-r--r--internal/suites/scenario_one_factor_test.go32
-rw-r--r--internal/suites/scenario_two_factor_test.go32
-rw-r--r--internal/suites/suite_multi_cookie_domain_test.go6
-rw-r--r--internal/suites/suite_pathprefix_test.go8
-rw-r--r--internal/suites/suite_standalone_test.go4
-rw-r--r--internal/suites/utils.go24
-rw-r--r--internal/suites/verify_url_is.go7
-rw-r--r--internal/utils/bytes.go28
147 files changed, 7861 insertions, 2854 deletions
diff --git a/api/openapi.yml b/api/openapi.yml
index b51f5d329..7979d6f94 100644
--- a/api/openapi.yml
+++ b/api/openapi.yml
@@ -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:
@@ -1135,6 +1307,32 @@ paths:
{{- end }}
components:
parameters:
+ 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
@@ -1170,6 +1368,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
diff --git a/cmd/authelia-gen/cmd_code.go b/cmd/authelia-gen/cmd_code.go
index 1359298c5..49dd6ca4a 100644
--- a/cmd/authelia-gen/cmd_code.go
+++ b/cmd/authelia-gen/cmd_code.go
@@ -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])
-}
diff --git a/cmd/authelia-gen/helpers.go b/cmd/authelia-gen/helpers.go
index 09ac84e8f..cb8606537 100644
--- a/cmd/authelia-gen/helpers.go
+++ b/cmd/authelia-gen/helpers.go
@@ -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])
+ }
+}
diff --git a/config.template.yml b/config.template.yml
index 4743bf515..7da204922 100644
--- a/config.template.yml
+++ b/config.template.yml
@@ -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
##
diff --git a/docs/content/en/configuration/miscellaneous/server-endpoints-authz.md b/docs/content/en/configuration/miscellaneous/server-endpoints-authz.md
new file mode 100644
index 000000000..49ca3c3e0
--- /dev/null
+++ b/docs/content/en/configuration/miscellaneous/server-endpoints-authz.md
@@ -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).
diff --git a/docs/content/en/configuration/miscellaneous/server.md b/docs/content/en/configuration/miscellaneous/server.md
index 39340d5bf..2eaa4d1db 100644
--- a/docs/content/en/configuration/miscellaneous/server.md
+++ b/docs/content/en/configuration/miscellaneous/server.md
@@ -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
diff --git a/docs/content/en/contributing/development/environment.md b/docs/content/en/contributing/development/environment.md
index 864e5d479..1a81643d6 100644
--- a/docs/content/en/contributing/development/environment.md
+++ b/docs/content/en/contributing/development/environment.md
@@ -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/
diff --git a/docs/content/en/integration/kubernetes/istio.md b/docs/content/en/integration/kubernetes/istio.md
index 9a0d162f7..c1c4927e3 100644
--- a/docs/content/en/integration/kubernetes/istio.md
+++ b/docs/content/en/integration/kubernetes/istio.md
@@ -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
diff --git a/docs/content/en/integration/kubernetes/nginx-ingress.md b/docs/content/en/integration/kubernetes/nginx-ingress.md
index 614626e60..d538aa860 100644
--- a/docs/content/en/integration/kubernetes/nginx-ingress.md
+++ b/docs/content/en/integration/kubernetes/nginx-ingress.md
@@ -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/
diff --git a/docs/content/en/integration/kubernetes/traefik-ingress.md b/docs/content/en/integration/kubernetes/traefik-ingress.md
index 3ada01b8e..3a50d8e9e 100644
--- a/docs/content/en/integration/kubernetes/traefik-ingress.md
+++ b/docs/content/en/integration/kubernetes/traefik-ingress.md
@@ -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 >}}
diff --git a/docs/content/en/integration/proxies/caddy.md b/docs/content/en/integration/proxies/caddy.md
index 904fb17df..f552e7502 100644
--- a/docs/content/en/integration/proxies/caddy.md
+++ b/docs/content/en/integration/proxies/caddy.md
@@ -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}
diff --git a/docs/content/en/integration/proxies/envoy.md b/docs/content/en/integration/proxies/envoy.md
index b03b09a7e..25cece96d 100644
--- a/docs/content/en/integration/proxies/envoy.md
+++ b/docs/content/en/integration/proxies/envoy.md
@@ -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:
diff --git a/docs/content/en/integration/proxies/haproxy.md b/docs/content/en/integration/proxies/haproxy.md
index f5a8dd178..88c6d25f7 100644
--- a/docs/content/en/integration/proxies/haproxy.md
+++ b/docs/content/en/integration/proxies/haproxy.md
@@ -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
diff --git a/docs/content/en/integration/proxies/introduction.md b/docs/content/en/integration/proxies/introduction.md
index a747a1fa5..4670ee6d1 100644
--- a/docs/content/en/integration/proxies/introduction.md
+++ b/docs/content/en/integration/proxies/introduction.md
@@ -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
diff --git a/docs/content/en/integration/proxies/nginx.md b/docs/content/en/integration/proxies/nginx.md
index 683c2cf2a..7f4907b16 100644
--- a/docs/content/en/integration/proxies/nginx.md
+++ b/docs/content/en/integration/proxies/nginx.md
@@ -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;
diff --git a/docs/content/en/integration/proxies/support.md b/docs/content/en/integration/proxies/support.md
index d74252e4e..01711ce27 100644
--- a/docs/content/en/integration/proxies/support.md
+++ b/docs/content/en/integration/proxies/support.md
@@ -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:
diff --git a/docs/content/en/integration/proxies/traefik.md b/docs/content/en/integration/proxies/traefik.md
index cc9317e97..0eed9e4d4 100644
--- a/docs/content/en/integration/proxies/traefik.md
+++ b/docs/content/en/integration/proxies/traefik.md
@@ -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
diff --git a/docs/content/en/integration/proxies/traefikv1.md b/docs/content/en/integration/proxies/traefikv1.md
index 1867bf3b1..7a2273ae3 100644
--- a/docs/content/en/integration/proxies/traefikv1.md
+++ b/docs/content/en/integration/proxies/traefikv1.md
@@ -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
diff --git a/docs/content/en/reference/guides/metrics.md b/docs/content/en/reference/guides/metrics.md
index a6020a4dd..fd7371392 100644
--- a/docs/content/en/reference/guides/metrics.md
+++ b/docs/content/en/reference/guides/metrics.md
@@ -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
diff --git a/docs/content/en/reference/guides/proxy-authorization.md b/docs/content/en/reference/guides/proxy-authorization.md
new file mode 100644
index 000000000..22420a13d
--- /dev/null
+++ b/docs/content/en/reference/guides/proxy-authorization.md
@@ -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
diff --git a/docs/data/configkeys.json b/docs/data/configkeys.json
index b5bed61d4..8dcc550cb 100644
--- a/docs/data/configkeys.json
+++ b/docs/data/configkeys.json
@@ -1 +1 @@
-[{"path":"theme","secret":false,"env":"AUTHELIA_THEME"},{"path":"certificates_directory","secret":false,"env":"AUTHELIA_CERTIFICATES_DIRECTORY"},{"path":"jwt_secret","secret":true,"env":"AUTHELIA_JWT_SECRET_FILE"},{"path":"default_redirection_url","secret":false,"env":"AUTHELIA_DEFAULT_REDIRECTION_URL"},{"path":"default_2fa_method","secret":false,"env":"AUTHELIA_DEFAULT_2FA_METHOD"},{"path":"log.level","secret":false,"env":"AUTHELIA_LOG_LEVEL"},{"path":"log.format","secret":false,"env":"AUTHELIA_LOG_FORMAT"},{"path":"log.file_path","secret":false,"env":"AUTHELIA_LOG_FILE_PATH"},{"path":"log.keep_stdout","secret":false,"env":"AUTHELIA_LOG_KEEP_STDOUT"},{"path":"identity_providers.oidc.hmac_secret","secret":true,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_HMAC_SECRET_FILE"},{"path":"identity_providers.oidc.issuer_certificate_chain","secret":true,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ISSUER_CERTIFICATE_CHAIN_FILE"},{"path":"identity_providers.oidc.issuer_private_key","secret":true,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ISSUER_PRIVATE_KEY_FILE"},{"path":"identity_providers.oidc.access_token_lifespan","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ACCESS_TOKEN_LIFESPAN"},{"path":"identity_providers.oidc.authorize_code_lifespan","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_AUTHORIZE_CODE_LIFESPAN"},{"path":"identity_providers.oidc.id_token_lifespan","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ID_TOKEN_LIFESPAN"},{"path":"identity_providers.oidc.refresh_token_lifespan","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_REFRESH_TOKEN_LIFESPAN"},{"path":"identity_providers.oidc.enable_client_debug_messages","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ENABLE_CLIENT_DEBUG_MESSAGES"},{"path":"identity_providers.oidc.minimum_parameter_entropy","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_MINIMUM_PARAMETER_ENTROPY"},{"path":"identity_providers.oidc.enforce_pkce","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ENFORCE_PKCE"},{"path":"identity_providers.oidc.enable_pkce_plain_challenge","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ENABLE_PKCE_PLAIN_CHALLENGE"},{"path":"identity_providers.oidc.cors.endpoints","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_CORS_ENDPOINTS"},{"path":"identity_providers.oidc.cors.allowed_origins","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_CORS_ALLOWED_ORIGINS"},{"path":"identity_providers.oidc.cors.allowed_origins_from_client_redirect_uris","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_CORS_ALLOWED_ORIGINS_FROM_CLIENT_REDIRECT_URIS"},{"path":"identity_providers.oidc.clients","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_CLIENTS"},{"path":"authentication_backend.password_reset.disable","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_PASSWORD_RESET_DISABLE"},{"path":"authentication_backend.password_reset.custom_url","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_PASSWORD_RESET_CUSTOM_URL"},{"path":"authentication_backend.refresh_interval","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_REFRESH_INTERVAL"},{"path":"authentication_backend.file.path","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PATH"},{"path":"authentication_backend.file.watch","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_WATCH"},{"path":"authentication_backend.file.password.algorithm","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ALGORITHM"},{"path":"authentication_backend.file.password.argon2.variant","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ARGON2_VARIANT"},{"path":"authentication_backend.file.password.argon2.iterations","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ARGON2_ITERATIONS"},{"path":"authentication_backend.file.password.argon2.memory","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ARGON2_MEMORY"},{"path":"authentication_backend.file.password.argon2.parallelism","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ARGON2_PARALLELISM"},{"path":"authentication_backend.file.password.argon2.key_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ARGON2_KEY_LENGTH"},{"path":"authentication_backend.file.password.argon2.salt_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ARGON2_SALT_LENGTH"},{"path":"authentication_backend.file.password.sha2crypt.variant","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SHA2CRYPT_VARIANT"},{"path":"authentication_backend.file.password.sha2crypt.iterations","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SHA2CRYPT_ITERATIONS"},{"path":"authentication_backend.file.password.sha2crypt.salt_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SHA2CRYPT_SALT_LENGTH"},{"path":"authentication_backend.file.password.pbkdf2.variant","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_PBKDF2_VARIANT"},{"path":"authentication_backend.file.password.pbkdf2.iterations","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_PBKDF2_ITERATIONS"},{"path":"authentication_backend.file.password.pbkdf2.salt_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_PBKDF2_SALT_LENGTH"},{"path":"authentication_backend.file.password.bcrypt.variant","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_BCRYPT_VARIANT"},{"path":"authentication_backend.file.password.bcrypt.cost","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_BCRYPT_COST"},{"path":"authentication_backend.file.password.scrypt.iterations","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SCRYPT_ITERATIONS"},{"path":"authentication_backend.file.password.scrypt.block_size","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SCRYPT_BLOCK_SIZE"},{"path":"authentication_backend.file.password.scrypt.parallelism","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SCRYPT_PARALLELISM"},{"path":"authentication_backend.file.password.scrypt.key_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SCRYPT_KEY_LENGTH"},{"path":"authentication_backend.file.password.scrypt.salt_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SCRYPT_SALT_LENGTH"},{"path":"authentication_backend.file.password.iterations","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ITERATIONS"},{"path":"authentication_backend.file.password.memory","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_MEMORY"},{"path":"authentication_backend.file.password.parallelism","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_PARALLELISM"},{"path":"authentication_backend.file.password.key_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_KEY_LENGTH"},{"path":"authentication_backend.file.password.salt_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SALT_LENGTH"},{"path":"authentication_backend.file.search.email","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_SEARCH_EMAIL"},{"path":"authentication_backend.file.search.case_insensitive","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_SEARCH_CASE_INSENSITIVE"},{"path":"authentication_backend.ldap.implementation","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_IMPLEMENTATION"},{"path":"authentication_backend.ldap.url","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_URL"},{"path":"authentication_backend.ldap.timeout","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_TIMEOUT"},{"path":"authentication_backend.ldap.start_tls","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_START_TLS"},{"path":"authentication_backend.ldap.tls.minimum_version","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_TLS_MINIMUM_VERSION"},{"path":"authentication_backend.ldap.tls.maximum_version","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_TLS_MAXIMUM_VERSION"},{"path":"authentication_backend.ldap.tls.skip_verify","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_TLS_SKIP_VERIFY"},{"path":"authentication_backend.ldap.tls.server_name","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_TLS_SERVER_NAME"},{"path":"authentication_backend.ldap.tls.private_key","secret":true,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_TLS_PRIVATE_KEY_FILE"},{"path":"authentication_backend.ldap.tls.certificate_chain","secret":true,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_TLS_CERTIFICATE_CHAIN_FILE"},{"path":"authentication_backend.ldap.base_dn","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_BASE_DN"},{"path":"authentication_backend.ldap.additional_users_dn","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_ADDITIONAL_USERS_DN"},{"path":"authentication_backend.ldap.users_filter","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_USERS_FILTER"},{"path":"authentication_backend.ldap.additional_groups_dn","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_ADDITIONAL_GROUPS_DN"},{"path":"authentication_backend.ldap.groups_filter","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_GROUPS_FILTER"},{"path":"authentication_backend.ldap.group_name_attribute","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_GROUP_NAME_ATTRIBUTE"},{"path":"authentication_backend.ldap.username_attribute","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_USERNAME_ATTRIBUTE"},{"path":"authentication_backend.ldap.mail_attribute","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_MAIL_ATTRIBUTE"},{"path":"authentication_backend.ldap.display_name_attribute","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_DISPLAY_NAME_ATTRIBUTE"},{"path":"authentication_backend.ldap.permit_referrals","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PERMIT_REFERRALS"},{"path":"authentication_backend.ldap.permit_unauthenticated_bind","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PERMIT_UNAUTHENTICATED_BIND"},{"path":"authentication_backend.ldap.permit_feature_detection_failure","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PERMIT_FEATURE_DETECTION_FAILURE"},{"path":"authentication_backend.ldap.user","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_USER"},{"path":"authentication_backend.ldap.password","secret":true,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE"},{"path":"session.secret","secret":true,"env":"AUTHELIA_SESSION_SECRET_FILE"},{"path":"session.name","secret":false,"env":"AUTHELIA_SESSION_NAME"},{"path":"session.domain","secret":false,"env":"AUTHELIA_SESSION_DOMAIN"},{"path":"session.same_site","secret":false,"env":"AUTHELIA_SESSION_SAME_SITE"},{"path":"session.expiration","secret":false,"env":"AUTHELIA_SESSION_EXPIRATION"},{"path":"session.inactivity","secret":false,"env":"AUTHELIA_SESSION_INACTIVITY"},{"path":"session.remember_me","secret":false,"env":"AUTHELIA_SESSION_REMEMBER_ME"},{"path":"session","secret":false,"env":"AUTHELIA_SESSION"},{"path":"session.cookies","secret":false,"env":"AUTHELIA_SESSION_COOKIES"},{"path":"session.redis.host","secret":false,"env":"AUTHELIA_SESSION_REDIS_HOST"},{"path":"session.redis.port","secret":false,"env":"AUTHELIA_SESSION_REDIS_PORT"},{"path":"session.redis.username","secret":false,"env":"AUTHELIA_SESSION_REDIS_USERNAME"},{"path":"session.redis.password","secret":true,"env":"AUTHELIA_SESSION_REDIS_PASSWORD_FILE"},{"path":"session.redis.database_index","secret":false,"env":"AUTHELIA_SESSION_REDIS_DATABASE_INDEX"},{"path":"session.redis.maximum_active_connections","secret":false,"env":"AUTHELIA_SESSION_REDIS_MAXIMUM_ACTIVE_CONNECTIONS"},{"path":"session.redis.minimum_idle_connections","secret":false,"env":"AUTHELIA_SESSION_REDIS_MINIMUM_IDLE_CONNECTIONS"},{"path":"session.redis.tls.minimum_version","secret":false,"env":"AUTHELIA_SESSION_REDIS_TLS_MINIMUM_VERSION"},{"path":"session.redis.tls.maximum_version","secret":false,"env":"AUTHELIA_SESSION_REDIS_TLS_MAXIMUM_VERSION"},{"path":"session.redis.tls.skip_verify","secret":false,"env":"AUTHELIA_SESSION_REDIS_TLS_SKIP_VERIFY"},{"path":"session.redis.tls.server_name","secret":false,"env":"AUTHELIA_SESSION_REDIS_TLS_SERVER_NAME"},{"path":"session.redis.tls.private_key","secret":true,"env":"AUTHELIA_SESSION_REDIS_TLS_PRIVATE_KEY_FILE"},{"path":"session.redis.tls.certificate_chain","secret":true,"env":"AUTHELIA_SESSION_REDIS_TLS_CERTIFICATE_CHAIN_FILE"},{"path":"session.redis.high_availability.sentinel_name","secret":false,"env":"AUTHELIA_SESSION_REDIS_HIGH_AVAILABILITY_SENTINEL_NAME"},{"path":"session.redis.high_availability.sentinel_username","secret":false,"env":"AUTHELIA_SESSION_REDIS_HIGH_AVAILABILITY_SENTINEL_USERNAME"},{"path":"session.redis.high_availability.sentinel_password","secret":true,"env":"AUTHELIA_SESSION_REDIS_HIGH_AVAILABILITY_SENTINEL_PASSWORD_FILE"},{"path":"session.redis.high_availability.nodes","secret":false,"env":"AUTHELIA_SESSION_REDIS_HIGH_AVAILABILITY_NODES"},{"path":"session.redis.high_availability.route_by_latency","secret":false,"env":"AUTHELIA_SESSION_REDIS_HIGH_AVAILABILITY_ROUTE_BY_LATENCY"},{"path":"session.redis.high_availability.route_randomly","secret":false,"env":"AUTHELIA_SESSION_REDIS_HIGH_AVAILABILITY_ROUTE_RANDOMLY"},{"path":"totp.disable","secret":false,"env":"AUTHELIA_TOTP_DISABLE"},{"path":"totp.issuer","secret":false,"env":"AUTHELIA_TOTP_ISSUER"},{"path":"totp.algorithm","secret":false,"env":"AUTHELIA_TOTP_ALGORITHM"},{"path":"totp.digits","secret":false,"env":"AUTHELIA_TOTP_DIGITS"},{"path":"totp.period","secret":false,"env":"AUTHELIA_TOTP_PERIOD"},{"path":"totp.skew","secret":false,"env":"AUTHELIA_TOTP_SKEW"},{"path":"totp.secret_size","secret":false,"env":"AUTHELIA_TOTP_SECRET_SIZE"},{"path":"duo_api.disable","secret":false,"env":"AUTHELIA_DUO_API_DISABLE"},{"path":"duo_api.hostname","secret":false,"env":"AUTHELIA_DUO_API_HOSTNAME"},{"path":"duo_api.integration_key","secret":true,"env":"AUTHELIA_DUO_API_INTEGRATION_KEY_FILE"},{"path":"duo_api.secret_key","secret":true,"env":"AUTHELIA_DUO_API_SECRET_KEY_FILE"},{"path":"duo_api.enable_self_enrollment","secret":false,"env":"AUTHELIA_DUO_API_ENABLE_SELF_ENROLLMENT"},{"path":"access_control.default_policy","secret":false,"env":"AUTHELIA_ACCESS_CONTROL_DEFAULT_POLICY"},{"path":"access_control.networks","secret":false,"env":"AUTHELIA_ACCESS_CONTROL_NETWORKS"},{"path":"access_control.rules","secret":false,"env":"AUTHELIA_ACCESS_CONTROL_RULES"},{"path":"ntp.address","secret":false,"env":"AUTHELIA_NTP_ADDRESS"},{"path":"ntp.version","secret":false,"env":"AUTHELIA_NTP_VERSION"},{"path":"ntp.max_desync","secret":false,"env":"AUTHELIA_NTP_MAX_DESYNC"},{"path":"ntp.disable_startup_check","secret":false,"env":"AUTHELIA_NTP_DISABLE_STARTUP_CHECK"},{"path":"ntp.disable_failure","secret":false,"env":"AUTHELIA_NTP_DISABLE_FAILURE"},{"path":"regulation.max_retries","secret":false,"env":"AUTHELIA_REGULATION_MAX_RETRIES"},{"path":"regulation.find_time","secret":false,"env":"AUTHELIA_REGULATION_FIND_TIME"},{"path":"regulation.ban_time","secret":false,"env":"AUTHELIA_REGULATION_BAN_TIME"},{"path":"storage.local.path","secret":false,"env":"AUTHELIA_STORAGE_LOCAL_PATH"},{"path":"storage.mysql.host","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_HOST"},{"path":"storage.mysql.port","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_PORT"},{"path":"storage.mysql.database","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_DATABASE"},{"path":"storage.mysql.username","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_USERNAME"},{"path":"storage.mysql.password","secret":true,"env":"AUTHELIA_STORAGE_MYSQL_PASSWORD_FILE"},{"path":"storage.mysql.timeout","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_TIMEOUT"},{"path":"storage.mysql.tls.minimum_version","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_TLS_MINIMUM_VERSION"},{"path":"storage.mysql.tls.maximum_version","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_TLS_MAXIMUM_VERSION"},{"path":"storage.mysql.tls.skip_verify","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_TLS_SKIP_VERIFY"},{"path":"storage.mysql.tls.server_name","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_TLS_SERVER_NAME"},{"path":"storage.mysql.tls.private_key","secret":true,"env":"AUTHELIA_STORAGE_MYSQL_TLS_PRIVATE_KEY_FILE"},{"path":"storage.mysql.tls.certificate_chain","secret":true,"env":"AUTHELIA_STORAGE_MYSQL_TLS_CERTIFICATE_CHAIN_FILE"},{"path":"storage.postgres.host","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_HOST"},{"path":"storage.postgres.port","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_PORT"},{"path":"storage.postgres.database","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_DATABASE"},{"path":"storage.postgres.username","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_USERNAME"},{"path":"storage.postgres.password","secret":true,"env":"AUTHELIA_STORAGE_POSTGRES_PASSWORD_FILE"},{"path":"storage.postgres.timeout","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_TIMEOUT"},{"path":"storage.postgres.schema","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_SCHEMA"},{"path":"storage.postgres.tls.minimum_version","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_TLS_MINIMUM_VERSION"},{"path":"storage.postgres.tls.maximum_version","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_TLS_MAXIMUM_VERSION"},{"path":"storage.postgres.tls.skip_verify","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_TLS_SKIP_VERIFY"},{"path":"storage.postgres.tls.server_name","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_TLS_SERVER_NAME"},{"path":"storage.postgres.tls.private_key","secret":true,"env":"AUTHELIA_STORAGE_POSTGRES_TLS_PRIVATE_KEY_FILE"},{"path":"storage.postgres.tls.certificate_chain","secret":true,"env":"AUTHELIA_STORAGE_POSTGRES_TLS_CERTIFICATE_CHAIN_FILE"},{"path":"storage.postgres.ssl.mode","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_SSL_MODE"},{"path":"storage.postgres.ssl.root_certificate","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_SSL_ROOT_CERTIFICATE"},{"path":"storage.postgres.ssl.certificate","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_SSL_CERTIFICATE"},{"path":"storage.postgres.ssl.key","secret":true,"env":"AUTHELIA_STORAGE_POSTGRES_SSL_KEY_FILE"},{"path":"storage.encryption_key","secret":true,"env":"AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE"},{"path":"notifier.disable_startup_check","secret":false,"env":"AUTHELIA_NOTIFIER_DISABLE_STARTUP_CHECK"},{"path":"notifier.filesystem.filename","secret":false,"env":"AUTHELIA_NOTIFIER_FILESYSTEM_FILENAME"},{"path":"notifier.smtp.host","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_HOST"},{"path":"notifier.smtp.port","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_PORT"},{"path":"notifier.smtp.timeout","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_TIMEOUT"},{"path":"notifier.smtp.username","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_USERNAME"},{"path":"notifier.smtp.password","secret":true,"env":"AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE"},{"path":"notifier.smtp.identifier","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_IDENTIFIER"},{"path":"notifier.smtp.sender","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_SENDER"},{"path":"notifier.smtp.subject","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_SUBJECT"},{"path":"notifier.smtp.startup_check_address","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_STARTUP_CHECK_ADDRESS"},{"path":"notifier.smtp.disable_require_tls","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_DISABLE_REQUIRE_TLS"},{"path":"notifier.smtp.disable_html_emails","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_DISABLE_HTML_EMAILS"},{"path":"notifier.smtp.disable_starttls","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_DISABLE_STARTTLS"},{"path":"notifier.smtp.tls.minimum_version","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_TLS_MINIMUM_VERSION"},{"path":"notifier.smtp.tls.maximum_version","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_TLS_MAXIMUM_VERSION"},{"path":"notifier.smtp.tls.skip_verify","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_TLS_SKIP_VERIFY"},{"path":"notifier.smtp.tls.server_name","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_TLS_SERVER_NAME"},{"path":"notifier.smtp.tls.private_key","secret":true,"env":"AUTHELIA_NOTIFIER_SMTP_TLS_PRIVATE_KEY_FILE"},{"path":"notifier.smtp.tls.certificate_chain","secret":true,"env":"AUTHELIA_NOTIFIER_SMTP_TLS_CERTIFICATE_CHAIN_FILE"},{"path":"notifier.template_path","secret":false,"env":"AUTHELIA_NOTIFIER_TEMPLATE_PATH"},{"path":"server.host","secret":false,"env":"AUTHELIA_SERVER_HOST"},{"path":"server.port","secret":false,"env":"AUTHELIA_SERVER_PORT"},{"path":"server.path","secret":false,"env":"AUTHELIA_SERVER_PATH"},{"path":"server.asset_path","secret":false,"env":"AUTHELIA_SERVER_ASSET_PATH"},{"path":"server.enable_pprof","secret":false,"env":"AUTHELIA_SERVER_ENABLE_PPROF"},{"path":"server.enable_expvars","secret":false,"env":"AUTHELIA_SERVER_ENABLE_EXPVARS"},{"path":"server.disable_healthcheck","secret":false,"env":"AUTHELIA_SERVER_DISABLE_HEALTHCHECK"},{"path":"server.tls.certificate","secret":false,"env":"AUTHELIA_SERVER_TLS_CERTIFICATE"},{"path":"server.tls.key","secret":true,"env":"AUTHELIA_SERVER_TLS_KEY_FILE"},{"path":"server.tls.client_certificates","secret":false,"env":"AUTHELIA_SERVER_TLS_CLIENT_CERTIFICATES"},{"path":"server.headers.csp_template","secret":false,"env":"AUTHELIA_SERVER_HEADERS_CSP_TEMPLATE"},{"path":"server.buffers.read","secret":false,"env":"AUTHELIA_SERVER_BUFFERS_READ"},{"path":"server.buffers.write","secret":false,"env":"AUTHELIA_SERVER_BUFFERS_WRITE"},{"path":"server.timeouts.read","secret":false,"env":"AUTHELIA_SERVER_TIMEOUTS_READ"},{"path":"server.timeouts.write","secret":false,"env":"AUTHELIA_SERVER_TIMEOUTS_WRITE"},{"path":"server.timeouts.idle","secret":false,"env":"AUTHELIA_SERVER_TIMEOUTS_IDLE"},{"path":"telemetry.metrics.enabled","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_ENABLED"},{"path":"telemetry.metrics.address","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_ADDRESS"},{"path":"telemetry.metrics.buffers.read","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_BUFFERS_READ"},{"path":"telemetry.metrics.buffers.write","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_BUFFERS_WRITE"},{"path":"telemetry.metrics.timeouts.read","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_TIMEOUTS_READ"},{"path":"telemetry.metrics.timeouts.write","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_TIMEOUTS_WRITE"},{"path":"telemetry.metrics.timeouts.idle","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_TIMEOUTS_IDLE"},{"path":"webauthn.disable","secret":false,"env":"AUTHELIA_WEBAUTHN_DISABLE"},{"path":"webauthn.display_name","secret":false,"env":"AUTHELIA_WEBAUTHN_DISPLAY_NAME"},{"path":"webauthn.attestation_conveyance_preference","secret":false,"env":"AUTHELIA_WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE"},{"path":"webauthn.user_verification","secret":false,"env":"AUTHELIA_WEBAUTHN_USER_VERIFICATION"},{"path":"webauthn.timeout","secret":false,"env":"AUTHELIA_WEBAUTHN_TIMEOUT"},{"path":"password_policy.standard.enabled","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_ENABLED"},{"path":"password_policy.standard.min_length","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_MIN_LENGTH"},{"path":"password_policy.standard.max_length","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_MAX_LENGTH"},{"path":"password_policy.standard.require_uppercase","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_REQUIRE_UPPERCASE"},{"path":"password_policy.standard.require_lowercase","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_REQUIRE_LOWERCASE"},{"path":"password_policy.standard.require_number","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_REQUIRE_NUMBER"},{"path":"password_policy.standard.require_special","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_REQUIRE_SPECIAL"},{"path":"password_policy.zxcvbn.enabled","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_ZXCVBN_ENABLED"},{"path":"password_policy.zxcvbn.min_score","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_ZXCVBN_MIN_SCORE"},{"path":"privacy_policy.enabled","secret":false,"env":"AUTHELIA_PRIVACY_POLICY_ENABLED"},{"path":"privacy_policy.require_user_acceptance","secret":false,"env":"AUTHELIA_PRIVACY_POLICY_REQUIRE_USER_ACCEPTANCE"},{"path":"privacy_policy.policy_url","secret":false,"env":"AUTHELIA_PRIVACY_POLICY_POLICY_URL"}] \ No newline at end of file
+[{"path":"theme","secret":false,"env":"AUTHELIA_THEME"},{"path":"certificates_directory","secret":false,"env":"AUTHELIA_CERTIFICATES_DIRECTORY"},{"path":"jwt_secret","secret":true,"env":"AUTHELIA_JWT_SECRET_FILE"},{"path":"default_redirection_url","secret":false,"env":"AUTHELIA_DEFAULT_REDIRECTION_URL"},{"path":"default_2fa_method","secret":false,"env":"AUTHELIA_DEFAULT_2FA_METHOD"},{"path":"log.level","secret":false,"env":"AUTHELIA_LOG_LEVEL"},{"path":"log.format","secret":false,"env":"AUTHELIA_LOG_FORMAT"},{"path":"log.file_path","secret":false,"env":"AUTHELIA_LOG_FILE_PATH"},{"path":"log.keep_stdout","secret":false,"env":"AUTHELIA_LOG_KEEP_STDOUT"},{"path":"identity_providers.oidc.hmac_secret","secret":true,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_HMAC_SECRET_FILE"},{"path":"identity_providers.oidc.issuer_certificate_chain","secret":true,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ISSUER_CERTIFICATE_CHAIN_FILE"},{"path":"identity_providers.oidc.issuer_private_key","secret":true,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ISSUER_PRIVATE_KEY_FILE"},{"path":"identity_providers.oidc.access_token_lifespan","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ACCESS_TOKEN_LIFESPAN"},{"path":"identity_providers.oidc.authorize_code_lifespan","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_AUTHORIZE_CODE_LIFESPAN"},{"path":"identity_providers.oidc.id_token_lifespan","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ID_TOKEN_LIFESPAN"},{"path":"identity_providers.oidc.refresh_token_lifespan","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_REFRESH_TOKEN_LIFESPAN"},{"path":"identity_providers.oidc.enable_client_debug_messages","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ENABLE_CLIENT_DEBUG_MESSAGES"},{"path":"identity_providers.oidc.minimum_parameter_entropy","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_MINIMUM_PARAMETER_ENTROPY"},{"path":"identity_providers.oidc.enforce_pkce","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ENFORCE_PKCE"},{"path":"identity_providers.oidc.enable_pkce_plain_challenge","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ENABLE_PKCE_PLAIN_CHALLENGE"},{"path":"identity_providers.oidc.cors.endpoints","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_CORS_ENDPOINTS"},{"path":"identity_providers.oidc.cors.allowed_origins","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_CORS_ALLOWED_ORIGINS"},{"path":"identity_providers.oidc.cors.allowed_origins_from_client_redirect_uris","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_CORS_ALLOWED_ORIGINS_FROM_CLIENT_REDIRECT_URIS"},{"path":"identity_providers.oidc.clients","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_CLIENTS"},{"path":"authentication_backend.password_reset.disable","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_PASSWORD_RESET_DISABLE"},{"path":"authentication_backend.password_reset.custom_url","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_PASSWORD_RESET_CUSTOM_URL"},{"path":"authentication_backend.refresh_interval","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_REFRESH_INTERVAL"},{"path":"authentication_backend.file.path","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PATH"},{"path":"authentication_backend.file.watch","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_WATCH"},{"path":"authentication_backend.file.password.algorithm","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ALGORITHM"},{"path":"authentication_backend.file.password.argon2.variant","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ARGON2_VARIANT"},{"path":"authentication_backend.file.password.argon2.iterations","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ARGON2_ITERATIONS"},{"path":"authentication_backend.file.password.argon2.memory","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ARGON2_MEMORY"},{"path":"authentication_backend.file.password.argon2.parallelism","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ARGON2_PARALLELISM"},{"path":"authentication_backend.file.password.argon2.key_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ARGON2_KEY_LENGTH"},{"path":"authentication_backend.file.password.argon2.salt_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ARGON2_SALT_LENGTH"},{"path":"authentication_backend.file.password.sha2crypt.variant","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SHA2CRYPT_VARIANT"},{"path":"authentication_backend.file.password.sha2crypt.iterations","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SHA2CRYPT_ITERATIONS"},{"path":"authentication_backend.file.password.sha2crypt.salt_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SHA2CRYPT_SALT_LENGTH"},{"path":"authentication_backend.file.password.pbkdf2.variant","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_PBKDF2_VARIANT"},{"path":"authentication_backend.file.password.pbkdf2.iterations","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_PBKDF2_ITERATIONS"},{"path":"authentication_backend.file.password.pbkdf2.salt_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_PBKDF2_SALT_LENGTH"},{"path":"authentication_backend.file.password.bcrypt.variant","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_BCRYPT_VARIANT"},{"path":"authentication_backend.file.password.bcrypt.cost","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_BCRYPT_COST"},{"path":"authentication_backend.file.password.scrypt.iterations","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SCRYPT_ITERATIONS"},{"path":"authentication_backend.file.password.scrypt.block_size","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SCRYPT_BLOCK_SIZE"},{"path":"authentication_backend.file.password.scrypt.parallelism","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SCRYPT_PARALLELISM"},{"path":"authentication_backend.file.password.scrypt.key_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SCRYPT_KEY_LENGTH"},{"path":"authentication_backend.file.password.scrypt.salt_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SCRYPT_SALT_LENGTH"},{"path":"authentication_backend.file.password.iterations","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ITERATIONS"},{"path":"authentication_backend.file.password.memory","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_MEMORY"},{"path":"authentication_backend.file.password.parallelism","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_PARALLELISM"},{"path":"authentication_backend.file.password.key_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_KEY_LENGTH"},{"path":"authentication_backend.file.password.salt_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SALT_LENGTH"},{"path":"authentication_backend.file.search.email","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_SEARCH_EMAIL"},{"path":"authentication_backend.file.search.case_insensitive","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_SEARCH_CASE_INSENSITIVE"},{"path":"authentication_backend.ldap.implementation","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_IMPLEMENTATION"},{"path":"authentication_backend.ldap.url","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_URL"},{"path":"authentication_backend.ldap.timeout","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_TIMEOUT"},{"path":"authentication_backend.ldap.start_tls","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_START_TLS"},{"path":"authentication_backend.ldap.tls.minimum_version","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_TLS_MINIMUM_VERSION"},{"path":"authentication_backend.ldap.tls.maximum_version","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_TLS_MAXIMUM_VERSION"},{"path":"authentication_backend.ldap.tls.skip_verify","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_TLS_SKIP_VERIFY"},{"path":"authentication_backend.ldap.tls.server_name","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_TLS_SERVER_NAME"},{"path":"authentication_backend.ldap.tls.private_key","secret":true,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_TLS_PRIVATE_KEY_FILE"},{"path":"authentication_backend.ldap.tls.certificate_chain","secret":true,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_TLS_CERTIFICATE_CHAIN_FILE"},{"path":"authentication_backend.ldap.base_dn","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_BASE_DN"},{"path":"authentication_backend.ldap.additional_users_dn","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_ADDITIONAL_USERS_DN"},{"path":"authentication_backend.ldap.users_filter","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_USERS_FILTER"},{"path":"authentication_backend.ldap.additional_groups_dn","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_ADDITIONAL_GROUPS_DN"},{"path":"authentication_backend.ldap.groups_filter","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_GROUPS_FILTER"},{"path":"authentication_backend.ldap.group_name_attribute","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_GROUP_NAME_ATTRIBUTE"},{"path":"authentication_backend.ldap.username_attribute","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_USERNAME_ATTRIBUTE"},{"path":"authentication_backend.ldap.mail_attribute","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_MAIL_ATTRIBUTE"},{"path":"authentication_backend.ldap.display_name_attribute","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_DISPLAY_NAME_ATTRIBUTE"},{"path":"authentication_backend.ldap.permit_referrals","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PERMIT_REFERRALS"},{"path":"authentication_backend.ldap.permit_unauthenticated_bind","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PERMIT_UNAUTHENTICATED_BIND"},{"path":"authentication_backend.ldap.permit_feature_detection_failure","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PERMIT_FEATURE_DETECTION_FAILURE"},{"path":"authentication_backend.ldap.user","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_USER"},{"path":"authentication_backend.ldap.password","secret":true,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE"},{"path":"session.secret","secret":true,"env":"AUTHELIA_SESSION_SECRET_FILE"},{"path":"session.name","secret":false,"env":"AUTHELIA_SESSION_NAME"},{"path":"session.domain","secret":false,"env":"AUTHELIA_SESSION_DOMAIN"},{"path":"session.same_site","secret":false,"env":"AUTHELIA_SESSION_SAME_SITE"},{"path":"session.expiration","secret":false,"env":"AUTHELIA_SESSION_EXPIRATION"},{"path":"session.inactivity","secret":false,"env":"AUTHELIA_SESSION_INACTIVITY"},{"path":"session.remember_me","secret":false,"env":"AUTHELIA_SESSION_REMEMBER_ME"},{"path":"session","secret":false,"env":"AUTHELIA_SESSION"},{"path":"session.cookies","secret":false,"env":"AUTHELIA_SESSION_COOKIES"},{"path":"session.redis.host","secret":false,"env":"AUTHELIA_SESSION_REDIS_HOST"},{"path":"session.redis.port","secret":false,"env":"AUTHELIA_SESSION_REDIS_PORT"},{"path":"session.redis.username","secret":false,"env":"AUTHELIA_SESSION_REDIS_USERNAME"},{"path":"session.redis.password","secret":true,"env":"AUTHELIA_SESSION_REDIS_PASSWORD_FILE"},{"path":"session.redis.database_index","secret":false,"env":"AUTHELIA_SESSION_REDIS_DATABASE_INDEX"},{"path":"session.redis.maximum_active_connections","secret":false,"env":"AUTHELIA_SESSION_REDIS_MAXIMUM_ACTIVE_CONNECTIONS"},{"path":"session.redis.minimum_idle_connections","secret":false,"env":"AUTHELIA_SESSION_REDIS_MINIMUM_IDLE_CONNECTIONS"},{"path":"session.redis.tls.minimum_version","secret":false,"env":"AUTHELIA_SESSION_REDIS_TLS_MINIMUM_VERSION"},{"path":"session.redis.tls.maximum_version","secret":false,"env":"AUTHELIA_SESSION_REDIS_TLS_MAXIMUM_VERSION"},{"path":"session.redis.tls.skip_verify","secret":false,"env":"AUTHELIA_SESSION_REDIS_TLS_SKIP_VERIFY"},{"path":"session.redis.tls.server_name","secret":false,"env":"AUTHELIA_SESSION_REDIS_TLS_SERVER_NAME"},{"path":"session.redis.tls.private_key","secret":true,"env":"AUTHELIA_SESSION_REDIS_TLS_PRIVATE_KEY_FILE"},{"path":"session.redis.tls.certificate_chain","secret":true,"env":"AUTHELIA_SESSION_REDIS_TLS_CERTIFICATE_CHAIN_FILE"},{"path":"session.redis.high_availability.sentinel_name","secret":false,"env":"AUTHELIA_SESSION_REDIS_HIGH_AVAILABILITY_SENTINEL_NAME"},{"path":"session.redis.high_availability.sentinel_username","secret":false,"env":"AUTHELIA_SESSION_REDIS_HIGH_AVAILABILITY_SENTINEL_USERNAME"},{"path":"session.redis.high_availability.sentinel_password","secret":true,"env":"AUTHELIA_SESSION_REDIS_HIGH_AVAILABILITY_SENTINEL_PASSWORD_FILE"},{"path":"session.redis.high_availability.nodes","secret":false,"env":"AUTHELIA_SESSION_REDIS_HIGH_AVAILABILITY_NODES"},{"path":"session.redis.high_availability.route_by_latency","secret":false,"env":"AUTHELIA_SESSION_REDIS_HIGH_AVAILABILITY_ROUTE_BY_LATENCY"},{"path":"session.redis.high_availability.route_randomly","secret":false,"env":"AUTHELIA_SESSION_REDIS_HIGH_AVAILABILITY_ROUTE_RANDOMLY"},{"path":"totp.disable","secret":false,"env":"AUTHELIA_TOTP_DISABLE"},{"path":"totp.issuer","secret":false,"env":"AUTHELIA_TOTP_ISSUER"},{"path":"totp.algorithm","secret":false,"env":"AUTHELIA_TOTP_ALGORITHM"},{"path":"totp.digits","secret":false,"env":"AUTHELIA_TOTP_DIGITS"},{"path":"totp.period","secret":false,"env":"AUTHELIA_TOTP_PERIOD"},{"path":"totp.skew","secret":false,"env":"AUTHELIA_TOTP_SKEW"},{"path":"totp.secret_size","secret":false,"env":"AUTHELIA_TOTP_SECRET_SIZE"},{"path":"duo_api.disable","secret":false,"env":"AUTHELIA_DUO_API_DISABLE"},{"path":"duo_api.hostname","secret":false,"env":"AUTHELIA_DUO_API_HOSTNAME"},{"path":"duo_api.integration_key","secret":true,"env":"AUTHELIA_DUO_API_INTEGRATION_KEY_FILE"},{"path":"duo_api.secret_key","secret":true,"env":"AUTHELIA_DUO_API_SECRET_KEY_FILE"},{"path":"duo_api.enable_self_enrollment","secret":false,"env":"AUTHELIA_DUO_API_ENABLE_SELF_ENROLLMENT"},{"path":"access_control.default_policy","secret":false,"env":"AUTHELIA_ACCESS_CONTROL_DEFAULT_POLICY"},{"path":"access_control.networks","secret":false,"env":"AUTHELIA_ACCESS_CONTROL_NETWORKS"},{"path":"access_control.rules","secret":false,"env":"AUTHELIA_ACCESS_CONTROL_RULES"},{"path":"ntp.address","secret":false,"env":"AUTHELIA_NTP_ADDRESS"},{"path":"ntp.version","secret":false,"env":"AUTHELIA_NTP_VERSION"},{"path":"ntp.max_desync","secret":false,"env":"AUTHELIA_NTP_MAX_DESYNC"},{"path":"ntp.disable_startup_check","secret":false,"env":"AUTHELIA_NTP_DISABLE_STARTUP_CHECK"},{"path":"ntp.disable_failure","secret":false,"env":"AUTHELIA_NTP_DISABLE_FAILURE"},{"path":"regulation.max_retries","secret":false,"env":"AUTHELIA_REGULATION_MAX_RETRIES"},{"path":"regulation.find_time","secret":false,"env":"AUTHELIA_REGULATION_FIND_TIME"},{"path":"regulation.ban_time","secret":false,"env":"AUTHELIA_REGULATION_BAN_TIME"},{"path":"storage.local.path","secret":false,"env":"AUTHELIA_STORAGE_LOCAL_PATH"},{"path":"storage.mysql.host","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_HOST"},{"path":"storage.mysql.port","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_PORT"},{"path":"storage.mysql.database","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_DATABASE"},{"path":"storage.mysql.username","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_USERNAME"},{"path":"storage.mysql.password","secret":true,"env":"AUTHELIA_STORAGE_MYSQL_PASSWORD_FILE"},{"path":"storage.mysql.timeout","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_TIMEOUT"},{"path":"storage.mysql.tls.minimum_version","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_TLS_MINIMUM_VERSION"},{"path":"storage.mysql.tls.maximum_version","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_TLS_MAXIMUM_VERSION"},{"path":"storage.mysql.tls.skip_verify","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_TLS_SKIP_VERIFY"},{"path":"storage.mysql.tls.server_name","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_TLS_SERVER_NAME"},{"path":"storage.mysql.tls.private_key","secret":true,"env":"AUTHELIA_STORAGE_MYSQL_TLS_PRIVATE_KEY_FILE"},{"path":"storage.mysql.tls.certificate_chain","secret":true,"env":"AUTHELIA_STORAGE_MYSQL_TLS_CERTIFICATE_CHAIN_FILE"},{"path":"storage.postgres.host","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_HOST"},{"path":"storage.postgres.port","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_PORT"},{"path":"storage.postgres.database","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_DATABASE"},{"path":"storage.postgres.username","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_USERNAME"},{"path":"storage.postgres.password","secret":true,"env":"AUTHELIA_STORAGE_POSTGRES_PASSWORD_FILE"},{"path":"storage.postgres.timeout","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_TIMEOUT"},{"path":"storage.postgres.schema","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_SCHEMA"},{"path":"storage.postgres.tls.minimum_version","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_TLS_MINIMUM_VERSION"},{"path":"storage.postgres.tls.maximum_version","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_TLS_MAXIMUM_VERSION"},{"path":"storage.postgres.tls.skip_verify","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_TLS_SKIP_VERIFY"},{"path":"storage.postgres.tls.server_name","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_TLS_SERVER_NAME"},{"path":"storage.postgres.tls.private_key","secret":true,"env":"AUTHELIA_STORAGE_POSTGRES_TLS_PRIVATE_KEY_FILE"},{"path":"storage.postgres.tls.certificate_chain","secret":true,"env":"AUTHELIA_STORAGE_POSTGRES_TLS_CERTIFICATE_CHAIN_FILE"},{"path":"storage.postgres.ssl.mode","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_SSL_MODE"},{"path":"storage.postgres.ssl.root_certificate","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_SSL_ROOT_CERTIFICATE"},{"path":"storage.postgres.ssl.certificate","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_SSL_CERTIFICATE"},{"path":"storage.postgres.ssl.key","secret":true,"env":"AUTHELIA_STORAGE_POSTGRES_SSL_KEY_FILE"},{"path":"storage.encryption_key","secret":true,"env":"AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE"},{"path":"notifier.disable_startup_check","secret":false,"env":"AUTHELIA_NOTIFIER_DISABLE_STARTUP_CHECK"},{"path":"notifier.filesystem.filename","secret":false,"env":"AUTHELIA_NOTIFIER_FILESYSTEM_FILENAME"},{"path":"notifier.smtp.host","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_HOST"},{"path":"notifier.smtp.port","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_PORT"},{"path":"notifier.smtp.timeout","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_TIMEOUT"},{"path":"notifier.smtp.username","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_USERNAME"},{"path":"notifier.smtp.password","secret":true,"env":"AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE"},{"path":"notifier.smtp.identifier","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_IDENTIFIER"},{"path":"notifier.smtp.sender","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_SENDER"},{"path":"notifier.smtp.subject","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_SUBJECT"},{"path":"notifier.smtp.startup_check_address","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_STARTUP_CHECK_ADDRESS"},{"path":"notifier.smtp.disable_require_tls","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_DISABLE_REQUIRE_TLS"},{"path":"notifier.smtp.disable_html_emails","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_DISABLE_HTML_EMAILS"},{"path":"notifier.smtp.disable_starttls","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_DISABLE_STARTTLS"},{"path":"notifier.smtp.tls.minimum_version","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_TLS_MINIMUM_VERSION"},{"path":"notifier.smtp.tls.maximum_version","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_TLS_MAXIMUM_VERSION"},{"path":"notifier.smtp.tls.skip_verify","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_TLS_SKIP_VERIFY"},{"path":"notifier.smtp.tls.server_name","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_TLS_SERVER_NAME"},{"path":"notifier.smtp.tls.private_key","secret":true,"env":"AUTHELIA_NOTIFIER_SMTP_TLS_PRIVATE_KEY_FILE"},{"path":"notifier.smtp.tls.certificate_chain","secret":true,"env":"AUTHELIA_NOTIFIER_SMTP_TLS_CERTIFICATE_CHAIN_FILE"},{"path":"notifier.template_path","secret":false,"env":"AUTHELIA_NOTIFIER_TEMPLATE_PATH"},{"path":"server.host","secret":false,"env":"AUTHELIA_SERVER_HOST"},{"path":"server.port","secret":false,"env":"AUTHELIA_SERVER_PORT"},{"path":"server.path","secret":false,"env":"AUTHELIA_SERVER_PATH"},{"path":"server.asset_path","secret":false,"env":"AUTHELIA_SERVER_ASSET_PATH"},{"path":"server.disable_healthcheck","secret":false,"env":"AUTHELIA_SERVER_DISABLE_HEALTHCHECK"},{"path":"server.tls.certificate","secret":false,"env":"AUTHELIA_SERVER_TLS_CERTIFICATE"},{"path":"server.tls.key","secret":true,"env":"AUTHELIA_SERVER_TLS_KEY_FILE"},{"path":"server.tls.client_certificates","secret":false,"env":"AUTHELIA_SERVER_TLS_CLIENT_CERTIFICATES"},{"path":"server.headers.csp_template","secret":false,"env":"AUTHELIA_SERVER_HEADERS_CSP_TEMPLATE"},{"path":"server.endpoints.enable_pprof","secret":false,"env":"AUTHELIA_SERVER_ENDPOINTS_ENABLE_PPROF"},{"path":"server.endpoints.enable_expvars","secret":false,"env":"AUTHELIA_SERVER_ENDPOINTS_ENABLE_EXPVARS"},{"path":"server.endpoints.authz","secret":false,"env":"AUTHELIA_SERVER_ENDPOINTS_AUTHZ"},{"path":"server.endpoints.authz.*.implementation","secret":false,"env":"AUTHELIA_SERVER_ENDPOINTS_AUTHZ_*_IMPLEMENTATION"},{"path":"server.endpoints.authz.*.authn_strategies","secret":false,"env":"AUTHELIA_SERVER_ENDPOINTS_AUTHZ_*_AUTHN_STRATEGIES"},{"path":"server.buffers.read","secret":false,"env":"AUTHELIA_SERVER_BUFFERS_READ"},{"path":"server.buffers.write","secret":false,"env":"AUTHELIA_SERVER_BUFFERS_WRITE"},{"path":"server.timeouts.read","secret":false,"env":"AUTHELIA_SERVER_TIMEOUTS_READ"},{"path":"server.timeouts.write","secret":false,"env":"AUTHELIA_SERVER_TIMEOUTS_WRITE"},{"path":"server.timeouts.idle","secret":false,"env":"AUTHELIA_SERVER_TIMEOUTS_IDLE"},{"path":"telemetry.metrics.enabled","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_ENABLED"},{"path":"telemetry.metrics.address","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_ADDRESS"},{"path":"telemetry.metrics.buffers.read","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_BUFFERS_READ"},{"path":"telemetry.metrics.buffers.write","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_BUFFERS_WRITE"},{"path":"telemetry.metrics.timeouts.read","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_TIMEOUTS_READ"},{"path":"telemetry.metrics.timeouts.write","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_TIMEOUTS_WRITE"},{"path":"telemetry.metrics.timeouts.idle","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_TIMEOUTS_IDLE"},{"path":"webauthn.disable","secret":false,"env":"AUTHELIA_WEBAUTHN_DISABLE"},{"path":"webauthn.display_name","secret":false,"env":"AUTHELIA_WEBAUTHN_DISPLAY_NAME"},{"path":"webauthn.attestation_conveyance_preference","secret":false,"env":"AUTHELIA_WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE"},{"path":"webauthn.user_verification","secret":false,"env":"AUTHELIA_WEBAUTHN_USER_VERIFICATION"},{"path":"webauthn.timeout","secret":false,"env":"AUTHELIA_WEBAUTHN_TIMEOUT"},{"path":"password_policy.standard.enabled","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_ENABLED"},{"path":"password_policy.standard.min_length","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_MIN_LENGTH"},{"path":"password_policy.standard.max_length","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_MAX_LENGTH"},{"path":"password_policy.standard.require_uppercase","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_REQUIRE_UPPERCASE"},{"path":"password_policy.standard.require_lowercase","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_REQUIRE_LOWERCASE"},{"path":"password_policy.standard.require_number","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_REQUIRE_NUMBER"},{"path":"password_policy.standard.require_special","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_REQUIRE_SPECIAL"},{"path":"password_policy.zxcvbn.enabled","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_ZXCVBN_ENABLED"},{"path":"password_policy.zxcvbn.min_score","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_ZXCVBN_MIN_SCORE"},{"path":"privacy_policy.enabled","secret":false,"env":"AUTHELIA_PRIVACY_POLICY_ENABLED"},{"path":"privacy_policy.require_user_acceptance","secret":false,"env":"AUTHELIA_PRIVACY_POLICY_REQUIRE_USER_ACCEPTANCE"},{"path":"privacy_policy.policy_url","secret":false,"env":"AUTHELIA_PRIVACY_POLICY_POLICY_URL"}] \ No newline at end of file
diff --git a/examples/compose/lite/docker-compose.yml b/examples/compose/lite/docker-compose.yml
index 5d0cc450d..4ab9849b8 100644
--- a/examples/compose/lite/docker-compose.yml
+++ b/examples/compose/lite/docker-compose.yml
@@ -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'
diff --git a/examples/compose/local/docker-compose.yml b/examples/compose/local/docker-compose.yml
index 59619d41e..8d3015f52 100644
--- a/examples/compose/local/docker-compose.yml
+++ b/examples/compose/local/docker-compose.yml
@@ -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'
diff --git a/internal/authentication/const.go b/internal/authentication/const.go
index 4d626d290..f93d0ea5b 100644
--- a/internal/authentication/const.go
+++ b/internal/authentication/const.go
@@ -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
)
diff --git a/internal/authorization/authorizer.go b/internal/authorization/authorizer.go
index 9a27f48db..7d625864a 100644
--- a/internal/authorization/authorizer.go
+++ b/internal/authorization/authorizer.go
@@ -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))
diff --git a/internal/authorization/const.go b/internal/authorization/const.go
index afda76888..d20fa20f3 100644
--- a/internal/authorization/const.go
+++ b/internal/authorization/const.go
@@ -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
)
diff --git a/internal/commands/root.go b/internal/commands/root.go
index 29fdad27a..0ac6d8843 100644
--- a/internal/commands/root.go
+++ b/internal/commands/root.go
@@ -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)
}
diff --git a/internal/configuration/config.template.yml b/internal/configuration/config.template.yml
index 4743bf515..7da204922 100644
--- a/internal/configuration/config.template.yml
+++ b/internal/configuration/config.template.yml
@@ -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
##
diff --git a/internal/configuration/deprecation.go b/internal/configuration/deprecation.go
index e37750996..b49711898 100644
--- a/internal/configuration/deprecation.go
+++ b/internal/configuration/deprecation.go
@@ -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,
+ },
}
diff --git a/internal/configuration/schema/keys.go b/internal/configuration/schema/keys.go
index 171f36201..a5122ee82 100644
--- a/internal/configuration/schema/keys.go
+++ b/internal/configuration/schema/keys.go
@@ -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",
diff --git a/internal/configuration/schema/server.go b/internal/configuration/schema/server.go
index c6ad3e5d5..c786567b0 100644
--- a/internal/configuration/schema/server.go
+++ b/internal/configuration/schema/server.go
@@ -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",
+ },
+ },
+ },
+ },
+ },
}
diff --git a/internal/configuration/test_resources/config.yml b/internal/configuration/test_resources/config.yml
index ea48a847f..3d6437343 100644
--- a/internal/configuration/test_resources/config.yml
+++ b/internal/configuration/test_resources/config.yml
@@ -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
diff --git a/internal/configuration/validator/const.go b/internal/configuration/validator/const.go
index bbc59a87c..7573a02cc 100644
--- a/internal/configuration/validator/const.go
+++ b/internal/configuration/validator/const.go
@@ -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 (
@@ -332,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}
@@ -369,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{
diff --git a/internal/configuration/validator/keys.go b/internal/configuration/validator/keys.go
index e3e5fdcf7..67b9d964d 100644
--- a/internal/configuration/validator/keys.go
+++ b/internal/configuration/validator/keys.go
@@ -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())
+}
diff --git a/internal/configuration/validator/server.go b/internal/configuration/validator/server.go
index b0529ab0e..66a12d150 100644
--- a/internal/configuration/validator/server.go
+++ b/internal/configuration/validator/server.go
@@ -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))
+ }
+ }
}
diff --git a/internal/configuration/validator/server_test.go b/internal/configuration/validator/server_test.go
index bbbdb4010..c70e7124e 100644
--- a/internal/configuration/validator/server_test.go
+++ b/internal/configuration/validator/server_test.go
@@ -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)
+}
diff --git a/internal/duo/duo.go b/internal/duo/duo.go
index 345190904..b34290c7b 100644
--- a/internal/duo/duo.go
+++ b/internal/duo/duo.go
@@ -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
}
diff --git a/internal/duo/types.go b/internal/duo/types.go
index 54599cd02..c865bc49e 100644
--- a/internal/duo/types.go
+++ b/internal/duo/types.go
@@ -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.
diff --git a/internal/handlers/const.go b/internal/handlers/const.go
index 2403bf779..932d1ab5a 100644
--- a/internal/handlers/const.go
+++ b/internal/handlers/const.go
@@ -1,8 +1,6 @@
package handlers
import (
- "time"
-
"github.com/valyala/fasthttp"
)
@@ -18,9 +16,22 @@ const (
)
var (
- headerAuthorization = []byte(fasthttp.HeaderAuthorization)
+ 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 (
@@ -108,13 +119,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"
@@ -123,8 +127,6 @@ const (
auth = "auth"
)
-const authPrefix = "Basic "
-
const ldapPasswordComplexityCode = "0000052D."
var ldapPasswordComplexityCodes = []string{
diff --git a/internal/handlers/const_test.go b/internal/handlers/const_test.go
new file mode 100644
index 000000000..4ff49b9bb
--- /dev/null
+++ b/internal/handlers/const_test.go
@@ -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"
+)
diff --git a/internal/handlers/duo.go b/internal/handlers/duo.go
index 098907bda..ceb4b5240 100644
--- a/internal/handlers/duo.go
+++ b/internal/handlers/duo.go
@@ -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
}
diff --git a/internal/handlers/handler_authz.go b/internal/handlers/handler_authz.go
new file mode 100644
index 000000000..c143bd3bc
--- /dev/null
+++ b/internal/handlers/handler_authz.go
@@ -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
+}
diff --git a/internal/handlers/handler_authz_authn.go b/internal/handlers/handler_authz_authn.go
new file mode 100644
index 000000000..10188e19e
--- /dev/null
+++ b/internal/handlers/handler_authz_authn.go
@@ -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
+}
diff --git a/internal/handlers/handler_authz_builder.go b/internal/handlers/handler_authz_builder.go
new file mode 100644
index 000000000..98aa39215
--- /dev/null
+++ b/internal/handlers/handler_authz_builder.go
@@ -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
+}
diff --git a/internal/handlers/handler_authz_common.go b/internal/handlers/handler_authz_common.go
new file mode 100644
index 000000000..e25bea544
--- /dev/null
+++ b/internal/handlers/handler_authz_common.go
@@ -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
+}
diff --git a/internal/handlers/handler_authz_impl_authrequest.go b/internal/handlers/handler_authz_impl_authrequest.go
new file mode 100644
index 000000000..19292201f
--- /dev/null
+++ b/internal/handlers/handler_authz_impl_authrequest.go
@@ -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()
+}
diff --git a/internal/handlers/handler_authz_impl_authrequest_test.go b/internal/handlers/handler_authz_impl_authrequest_test.go
new file mode 100644
index 000000000..6f4a1d8b4
--- /dev/null
+++ b/internal/handlers/handler_authz_impl_authrequest_test.go
@@ -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)
+}
diff --git a/internal/handlers/handler_authz_impl_extauthz.go b/internal/handlers/handler_authz_impl_extauthz.go
new file mode 100644
index 000000000..ccae832d9
--- /dev/null
+++ b/internal/handlers/handler_authz_impl_extauthz.go
@@ -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)
+}
diff --git a/internal/handlers/handler_authz_impl_extauthz_test.go b/internal/handlers/handler_authz_impl_extauthz_test.go
new file mode 100644
index 000000000..820cb66d1
--- /dev/null
+++ b/internal/handlers/handler_authz_impl_extauthz_test.go
@@ -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)
+}
diff --git a/internal/handlers/handler_authz_impl_forwardauth.go b/internal/handlers/handler_authz_impl_forwardauth.go
new file mode 100644
index 000000000..a042c13bb
--- /dev/null
+++ b/internal/handlers/handler_authz_impl_forwardauth.go
@@ -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)
+}
diff --git a/internal/handlers/handler_authz_impl_forwardauth_test.go b/internal/handlers/handler_authz_impl_forwardauth_test.go
new file mode 100644
index 000000000..d7ea3baab
--- /dev/null
+++ b/internal/handlers/handler_authz_impl_forwardauth_test.go
@@ -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)
+}
diff --git a/internal/handlers/handler_authz_impl_legacy.go b/internal/handlers/handler_authz_impl_legacy.go
new file mode 100644
index 000000000..33af89616
--- /dev/null
+++ b/internal/handlers/handler_authz_impl_legacy.go
@@ -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()
+ }
+}
diff --git a/internal/handlers/handler_authz_impl_legacy_test.go b/internal/handlers/handler_authz_impl_legacy_test.go
new file mode 100644
index 000000000..a76936529
--- /dev/null
+++ b/internal/handlers/handler_authz_impl_legacy_test.go
@@ -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)
+}
diff --git a/internal/handlers/handler_authz_test.go b/internal/handlers/handler_authz_test.go
new file mode 100644
index 000000000..432ee7bbd
--- /dev/null
+++ b/internal/handlers/handler_authz_test.go
@@ -0,0 +1,1573 @@
+package handlers
+
+import (
+ "fmt"
+ "net/url"
+ "testing"
+ "time"
+
+ "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/configuration/schema"
+ "github.com/authelia/authelia/v4/internal/middlewares"
+ "github.com/authelia/authelia/v4/internal/mocks"
+ "github.com/authelia/authelia/v4/internal/session"
+ "github.com/authelia/authelia/v4/internal/utils"
+)
+
+type AuthzSuite struct {
+ suite.Suite
+
+ implementation AuthzImplementation
+ builder *AuthzBuilder
+ setRequest func(ctx *middlewares.AutheliaCtx, method string, targetURI *url.URL, accept bool, xhr bool)
+}
+
+func (s *AuthzSuite) GetMock(config *schema.Configuration, targetURI *url.URL, session *session.UserSession) *mocks.MockAutheliaCtx {
+ mock := mocks.NewMockAutheliaCtx(s.T())
+
+ if session != nil {
+ domain := mock.Ctx.GetTargetURICookieDomain(targetURI)
+
+ provider, err := mock.Ctx.GetCookieDomainSessionProvider(domain)
+ s.Require().NoError(err)
+
+ s.Require().NoError(provider.SaveSession(mock.Ctx.RequestCtx, *session))
+ }
+
+ return mock
+}
+
+func (s *AuthzSuite) RequireParseRequestURI(rawURL string) *url.URL {
+ u, err := url.ParseRequestURI(rawURL)
+
+ s.Require().NoError(err)
+
+ return u
+}
+
+type urlpair struct {
+ TargetURI *url.URL
+ AutheliaURI *url.URL
+}
+
+func (s *AuthzSuite) Builder() (builder *AuthzBuilder) {
+ if s.builder != nil {
+ return s.builder
+ }
+
+ switch s.implementation {
+ case AuthzImplExtAuthz:
+ return NewAuthzBuilder().WithImplementationExtAuthz()
+ case AuthzImplForwardAuth:
+ return NewAuthzBuilder().WithImplementationForwardAuth()
+ case AuthzImplAuthRequest:
+ return NewAuthzBuilder().WithImplementationAuthRequest()
+ case AuthzImplLegacy:
+ return NewAuthzBuilder().WithImplementationLegacy()
+ }
+
+ s.T().FailNow()
+
+ return
+}
+
+func (s *AuthzSuite) TestShouldNotBeAbleToParseBasicAuth() {
+ if s.setRequest == nil {
+ s.T().Skip()
+ }
+
+ 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)
+
+ targetURI := s.RequireParseRequestURI("https://test.example.com")
+
+ s.setRequest(mock.Ctx, fasthttp.MethodGet, targetURI, true, false)
+
+ mock.Ctx.Request.Header.Set(fasthttp.HeaderProxyAuthorization, "Basic am9objpaaaaaaaaaaaaaaaa")
+
+ authz.Handler(mock.Ctx)
+
+ switch s.implementation {
+ case AuthzImplAuthRequest, AuthzImplLegacy:
+ s.Equal(fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode())
+ s.Equal(`Basic realm="Authorization Required"`, string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderWWWAuthenticate)))
+ s.Equal([]byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderProxyAuthenticate))
+ default:
+ s.Equal(fasthttp.StatusProxyAuthRequired, mock.Ctx.Response.StatusCode())
+ s.Equal([]byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderWWWAuthenticate))
+ s.Equal(`Basic realm="Authorization Required"`, string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderProxyAuthenticate)))
+ }
+}
+
+func (s *AuthzSuite) TestShouldApplyDefaultPolicy() {
+ if s.setRequest == nil {
+ s.T().Skip()
+ }
+
+ 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)
+
+ targetURI := s.RequireParseRequestURI("https://test.example.com")
+
+ s.setRequest(mock.Ctx, fasthttp.MethodGet, targetURI, true, false)
+
+ mock.Ctx.Request.Header.Set(fasthttp.HeaderProxyAuthorization, "Basic am9objpwYXNzd29yZA==")
+
+ 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.StatusForbidden, mock.Ctx.Response.StatusCode())
+ s.Equal([]byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderWWWAuthenticate))
+ s.Equal([]byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderProxyAuthenticate))
+}
+
+func (s *AuthzSuite) TestShouldDenyObject() {
+ if s.setRequest == nil {
+ s.T().Skip()
+ }
+
+ testCases := []struct {
+ name string
+ value string
+ }{
+ {
+ "NotProtected",
+ "https://test.not-a-protected-domain.com",
+ },
+ {
+ "Insecure",
+ "http://test.example.com",
+ },
+ }
+
+ 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)
+
+ targetURI := s.RequireParseRequestURI(tc.value)
+
+ s.setRequest(mock.Ctx, fasthttp.MethodGet, targetURI, true, false)
+
+ authz.Handler(mock.Ctx)
+
+ assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode())
+ })
+ }
+}
+
+func (s *AuthzSuite) TestShouldApplyPolicyOfBypassDomain() {
+ if s.setRequest == nil {
+ s.T().Skip()
+ }
+
+ 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)
+
+ targetURI := s.RequireParseRequestURI("https://bypass.example.com")
+
+ s.setRequest(mock.Ctx, fasthttp.MethodGet, targetURI, true, false)
+
+ mock.Ctx.Request.Header.Set(fasthttp.HeaderProxyAuthorization, "Basic am9objpwYXNzd29yZA==")
+
+ 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())
+ s.Equal([]byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderWWWAuthenticate))
+ s.Equal([]byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderProxyAuthenticate))
+}
+
+func (s *AuthzSuite) TestShouldVerifyFailureToGetDetailsUsingBasicScheme() {
+ if s.setRequest == nil {
+ s.T().Skip()
+ }
+
+ 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)
+
+ targetURI := s.RequireParseRequestURI("https://bypass.example.com")
+
+ s.setRequest(mock.Ctx, fasthttp.MethodGet, targetURI, true, false)
+
+ mock.Ctx.Request.Header.Set(fasthttp.HeaderProxyAuthorization, "Basic am9objpwYXNzd29yZA==")
+
+ gomock.InOrder(
+ mock.UserProviderMock.EXPECT().
+ CheckUserPassword(gomock.Eq("john"), gomock.Eq("password")).
+ Return(true, nil),
+
+ mock.UserProviderMock.EXPECT().
+ GetDetails(gomock.Eq("john")).
+ Return(nil, fmt.Errorf("generic failure")),
+ )
+
+ authz.Handler(mock.Ctx)
+
+ switch s.implementation {
+ case AuthzImplAuthRequest, AuthzImplLegacy:
+ s.Equal(fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode())
+ s.Equal(`Basic realm="Authorization Required"`, string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderWWWAuthenticate)))
+ s.Equal([]byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderProxyAuthenticate))
+ default:
+ s.Equal(fasthttp.StatusProxyAuthRequired, mock.Ctx.Response.StatusCode())
+ s.Equal([]byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderWWWAuthenticate))
+ s.Equal(`Basic realm="Authorization Required"`, string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderProxyAuthenticate)))
+ }
+}
+
+func (s *AuthzSuite) TestShouldNotFailOnMissingEmail() {
+ if s.setRequest == nil {
+ s.T().Skip()
+ }
+
+ authz := s.Builder().Build()
+
+ mock := mocks.NewMockAutheliaCtx(s.T())
+
+ defer mock.Close()
+
+ mock.Clock.Set(time.Now())
+
+ 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)
+
+ targetURI := s.RequireParseRequestURI("https://bypass.example.com")
+
+ s.setRequest(mock.Ctx, fasthttp.MethodGet, targetURI, true, false)
+
+ userSession, err := mock.Ctx.GetSession()
+ s.Require().NoError(err)
+
+ userSession.Username = testUsername
+ userSession.DisplayName = "John Smith"
+ userSession.Groups = []string{"abc,123"}
+ userSession.Emails = nil
+ userSession.AuthenticationLevel = authentication.OneFactor
+ userSession.RefreshTTL = mock.Clock.Now().Add(5 * time.Minute)
+
+ s.Require().NoError(mock.Ctx.SaveSession(userSession))
+
+ authz.Handler(mock.Ctx)
+
+ s.Equal(fasthttp.StatusOK, mock.Ctx.Response.StatusCode())
+ s.Equal(testUsername, string(mock.Ctx.Response.Header.PeekBytes(headerRemoteUser)))
+ s.Equal("John Smith", string(mock.Ctx.Response.Header.PeekBytes(headerRemoteName)))
+ s.Equal("abc,123", string(mock.Ctx.Response.Header.PeekBytes(headerRemoteGroups)))
+}
+
+func (s *AuthzSuite) TestShouldApplyPolicyOfOneFactorDomain() {
+ if s.setRequest == nil {
+ s.T().Skip()
+ }
+
+ 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)
+
+ targetURI := s.RequireParseRequestURI("https://one-factor.example.com")
+
+ s.setRequest(mock.Ctx, fasthttp.MethodGet, targetURI, true, false)
+
+ mock.Ctx.Request.Header.Set(fasthttp.HeaderProxyAuthorization, "Basic am9objpwYXNzd29yZA==")
+
+ 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())
+ s.Equal([]byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderWWWAuthenticate))
+ s.Equal([]byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderProxyAuthenticate))
+}
+
+func (s *AuthzSuite) TestShouldHandleAnyCaseSchemeParameter() {
+ if s.setRequest == nil {
+ s.T().Skip()
+ }
+
+ testCases := []struct {
+ name, scheme string
+ }{
+ {"Standard", "Basic"},
+ {"LowerCase", "basic"},
+ {"UpperCase", "BASIC"},
+ {"MixedCase", "BaSIc"},
+ }
+
+ authz := s.Builder().Build()
+
+ for _, tc := range testCases {
+ s.T().Run(tc.name, func(t *testing.T) {
+ 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)
+
+ targetURI := s.RequireParseRequestURI("https://one-factor.example.com")
+
+ s.setRequest(mock.Ctx, fasthttp.MethodGet, targetURI, true, false)
+
+ mock.Ctx.Request.Header.Set(fasthttp.HeaderProxyAuthorization, fmt.Sprintf("%s am9objpwYXNzd29yZA==", tc.scheme))
+
+ 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())
+ s.Equal([]byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderWWWAuthenticate))
+ s.Equal([]byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderProxyAuthenticate))
+ })
+ }
+}
+
+func (s *AuthzSuite) TestShouldApplyPolicyOfTwoFactorDomain() {
+ if s.setRequest == nil {
+ s.T().Skip()
+ }
+
+ 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)
+
+ targetURI := s.RequireParseRequestURI("https://two-factor.example.com")
+
+ s.setRequest(mock.Ctx, fasthttp.MethodGet, targetURI, true, false)
+
+ mock.Ctx.Request.Header.Set(fasthttp.HeaderProxyAuthorization, "Basic am9objpwYXNzd29yZA==")
+
+ 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)
+
+ switch s.implementation {
+ case AuthzImplAuthRequest, AuthzImplLegacy:
+ s.Equal(fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode())
+ s.Equal(`Basic realm="Authorization Required"`, string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderWWWAuthenticate)))
+ s.Equal([]byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderProxyAuthenticate))
+ default:
+ s.Equal(fasthttp.StatusProxyAuthRequired, mock.Ctx.Response.StatusCode())
+ s.Equal([]byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderWWWAuthenticate))
+ s.Equal(`Basic realm="Authorization Required"`, string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderProxyAuthenticate)))
+ }
+}
+
+func (s *AuthzSuite) TestShouldApplyPolicyOfDenyDomain() {
+ if s.setRequest == nil {
+ s.T().Skip()
+ }
+
+ 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)
+
+ targetURI := s.RequireParseRequestURI("https://deny.example.com")
+
+ s.setRequest(mock.Ctx, fasthttp.MethodGet, targetURI, true, false)
+
+ mock.Ctx.Request.Header.Set(fasthttp.HeaderProxyAuthorization, "Basic am9objpwYXNzd29yZA==")
+
+ 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.StatusForbidden, mock.Ctx.Response.StatusCode())
+ s.Equal([]byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderWWWAuthenticate))
+ s.Equal([]byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderProxyAuthenticate))
+}
+
+func (s *AuthzSuite) TestShouldApplyPolicyOfOneFactorDomainWithAuthorizationHeader() {
+ if s.setRequest == nil {
+ s.T().Skip()
+ }
+
+ // Equivalent of TestShouldVerifyAuthBasicArgOk.
+
+ builder := NewAuthzBuilder().WithImplementationLegacy()
+
+ builder = builder.WithStrategies(
+ NewHeaderAuthorizationAuthnStrategy(),
+ NewHeaderProxyAuthorizationAuthRequestAuthnStrategy(),
+ NewCookieSessionAuthnStrategy(builder.config.RefreshInterval),
+ )
+
+ authz := 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)
+
+ targetURI := s.RequireParseRequestURI("https://one-factor.example.com")
+
+ s.setRequest(mock.Ctx, fasthttp.MethodGet, targetURI, true, false)
+
+ mock.Ctx.Request.Header.Set(fasthttp.HeaderAuthorization, "Basic am9objpwYXNzd29yZA==")
+
+ 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())
+ s.Equal([]byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderWWWAuthenticate))
+ s.Equal([]byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderProxyAuthenticate))
+}
+
+func (s *AuthzSuite) TestShouldHandleAuthzWithoutHeaderNoCookie() {
+ if s.setRequest == nil {
+ s.T().Skip()
+ }
+
+ // Equivalent of TestShouldVerifyAuthBasicArgFailingNoHeader.
+
+ builder := NewAuthzBuilder().WithImplementationLegacy()
+
+ builder = builder.WithStrategies(
+ NewHeaderAuthorizationAuthnStrategy(),
+ NewHeaderProxyAuthorizationAuthRequestAuthnStrategy(),
+ )
+
+ authz := 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)
+
+ targetURI := s.RequireParseRequestURI("https://one-factor.example.com")
+
+ s.setRequest(mock.Ctx, fasthttp.MethodGet, targetURI, true, false)
+
+ authz.Handler(mock.Ctx)
+
+ s.Equal(fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode())
+ s.Equal(`Basic realm="Authorization Required"`, string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderWWWAuthenticate)))
+ s.Equal([]byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderProxyAuthenticate))
+}
+
+func (s *AuthzSuite) TestShouldHandleAuthzWithEmptyAuthorizationHeader() {
+ if s.setRequest == nil {
+ s.T().Skip()
+ }
+
+ // Equivalent of TestShouldVerifyAuthBasicArgFailingEmptyHeader.
+
+ builder := NewAuthzBuilder().WithImplementationLegacy()
+
+ builder = builder.WithStrategies(
+ NewHeaderAuthorizationAuthnStrategy(),
+ NewHeaderProxyAuthorizationAuthRequestAuthnStrategy(),
+ )
+
+ authz := 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)
+
+ targetURI := s.RequireParseRequestURI("https://one-factor.example.com")
+
+ s.setRequest(mock.Ctx, fasthttp.MethodGet, targetURI, true, false)
+
+ mock.Ctx.Request.Header.Set(fasthttp.HeaderAuthorization, "")
+
+ authz.Handler(mock.Ctx)
+
+ s.Equal(fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode())
+ s.Equal(`Basic realm="Authorization Required"`, string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderWWWAuthenticate)))
+ s.Equal([]byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderProxyAuthenticate))
+}
+
+func (s *AuthzSuite) TestShouldHandleAuthzWithAuthorizationHeaderInvalidPassword() {
+ if s.setRequest == nil {
+ s.T().Skip()
+ }
+
+ // Equivalent of TestShouldVerifyAuthBasicArgFailingWrongPassword.
+
+ builder := NewAuthzBuilder().WithImplementationLegacy()
+
+ builder = builder.WithStrategies(
+ NewHeaderAuthorizationAuthnStrategy(),
+ NewHeaderProxyAuthorizationAuthRequestAuthnStrategy(),
+ )
+
+ authz := 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)
+
+ targetURI := s.RequireParseRequestURI("https://one-factor.example.com")
+
+ s.setRequest(mock.Ctx, fasthttp.MethodGet, targetURI, true, false)
+
+ mock.Ctx.Request.Header.Set(fasthttp.HeaderAuthorization, "Basic am9objpwYXNzd29yZA==")
+
+ mock.UserProviderMock.EXPECT().
+ CheckUserPassword(gomock.Eq("john"), gomock.Eq("password")).
+ Return(false, nil)
+
+ authz.Handler(mock.Ctx)
+
+ s.Equal(fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode())
+ s.Equal(`Basic realm="Authorization Required"`, string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderWWWAuthenticate)))
+ s.Equal([]byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderProxyAuthenticate))
+}
+
+func (s *AuthzSuite) TestShouldHandleAuthzWithIncorrectAuthHeader() { // TestShouldVerifyAuthBasicArgFailingWrongHeader.
+ if s.setRequest == nil {
+ s.T().Skip()
+ }
+
+ builder := s.Builder()
+
+ builder = builder.WithStrategies(
+ NewHeaderAuthorizationAuthnStrategy(),
+ )
+
+ authz := 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)
+
+ targetURI := s.RequireParseRequestURI("https://one-factor.example.com")
+
+ s.setRequest(mock.Ctx, fasthttp.MethodGet, targetURI, true, false)
+
+ mock.Ctx.Request.Header.Set(fasthttp.HeaderProxyAuthorization, "Basic am9objpwYXNzd29yZA==")
+
+ authz.Handler(mock.Ctx)
+
+ s.Equal(fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode())
+ s.Equal(`Basic realm="Authorization Required"`, string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderWWWAuthenticate)))
+ s.Equal([]byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderProxyAuthenticate))
+}
+
+func (s *AuthzSuite) TestShouldDestroySessionWhenInactiveForTooLong() {
+ if s.setRequest == nil {
+ s.T().Skip()
+ }
+
+ builder := s.Builder()
+
+ builder = builder.WithStrategies(
+ NewCookieSessionAuthnStrategy(testInactivity),
+ )
+
+ authz := builder.Build()
+
+ mock := mocks.NewMockAutheliaCtx(s.T())
+
+ defer mock.Close()
+
+ mock.Ctx.Clock = &mock.Clock
+
+ mock.Clock.Set(time.Now())
+
+ past := mock.Clock.Now().Add(-1 * time.Hour)
+
+ mock.Ctx.Configuration.Session.Cookies[0].Inactivity = testInactivity
+
+ 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)
+
+ targetURI := s.RequireParseRequestURI("https://two-factor.example.com")
+
+ s.setRequest(mock.Ctx, fasthttp.MethodGet, targetURI, true, false)
+
+ userSession, err := mock.Ctx.GetSession()
+ s.Require().NoError(err)
+
+ userSession.Username = testUsername
+ userSession.AuthenticationLevel = authentication.TwoFactor
+ userSession.LastActivity = past.Unix()
+
+ s.Require().NoError(mock.Ctx.SaveSession(userSession))
+
+ authz.Handler(mock.Ctx)
+
+ userSession, err = mock.Ctx.GetSession()
+ s.Require().NoError(err)
+
+ s.Equal("", userSession.Username)
+ s.Equal(authentication.NotAuthenticated, userSession.AuthenticationLevel)
+ s.Equal(mock.Clock.Now().Unix(), userSession.LastActivity)
+}
+
+func (s *AuthzSuite) TestShouldNotDestroySessionWhenInactiveForTooLongRememberMe() {
+ if s.setRequest == nil {
+ s.T().Skip()
+ }
+
+ builder := s.Builder()
+
+ builder = builder.WithStrategies(
+ NewCookieSessionAuthnStrategy(testInactivity),
+ )
+
+ authz := builder.Build()
+
+ mock := mocks.NewMockAutheliaCtx(s.T())
+
+ defer mock.Close()
+
+ mock.Ctx.Clock = &mock.Clock
+
+ mock.Clock.Set(time.Now())
+
+ mock.Ctx.Configuration.Session.Cookies[0].Inactivity = testInactivity
+
+ 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)
+
+ targetURI := s.RequireParseRequestURI("https://two-factor.example.com")
+
+ s.setRequest(mock.Ctx, fasthttp.MethodGet, targetURI, true, false)
+
+ userSession, err := mock.Ctx.GetSession()
+ s.Require().NoError(err)
+
+ userSession.Username = testUsername
+ userSession.AuthenticationLevel = authentication.TwoFactor
+ userSession.LastActivity = 0
+ userSession.KeepMeLoggedIn = true
+ userSession.RefreshTTL = mock.Clock.Now().Add(5 * time.Minute)
+
+ s.Require().NoError(mock.Ctx.SaveSession(userSession))
+
+ authz.Handler(mock.Ctx)
+
+ userSession, err = mock.Ctx.GetSession()
+ s.Require().NoError(err)
+
+ s.Equal(testUsername, userSession.Username)
+ s.Equal(authentication.TwoFactor, userSession.AuthenticationLevel)
+ s.Equal(int64(0), userSession.LastActivity)
+}
+
+func (s *AuthzSuite) TestShouldNotDestroySessionWhenNotInactiveForTooLong() {
+ if s.setRequest == nil {
+ s.T().Skip()
+ }
+
+ builder := s.Builder()
+
+ builder = builder.WithStrategies(
+ NewCookieSessionAuthnStrategy(testInactivity),
+ )
+
+ authz := builder.Build()
+
+ mock := mocks.NewMockAutheliaCtx(s.T())
+
+ defer mock.Close()
+
+ mock.Ctx.Clock = &mock.Clock
+
+ mock.Clock.Set(time.Now())
+
+ mock.Ctx.Configuration.Session.Cookies[0].Inactivity = testInactivity
+
+ 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)
+
+ targetURI := s.RequireParseRequestURI("https://two-factor.example.com")
+
+ s.setRequest(mock.Ctx, fasthttp.MethodGet, targetURI, true, false)
+
+ last := mock.Clock.Now().Add(-1 * time.Second)
+
+ userSession, err := mock.Ctx.GetSession()
+ s.Require().NoError(err)
+
+ userSession.Username = testUsername
+ userSession.AuthenticationLevel = authentication.TwoFactor
+ userSession.LastActivity = last.Unix()
+ userSession.RefreshTTL = mock.Clock.Now().Add(5 * time.Minute)
+
+ s.Require().NoError(mock.Ctx.SaveSession(userSession))
+
+ authz.Handler(mock.Ctx)
+
+ userSession, err = mock.Ctx.GetSession()
+ s.Require().NoError(err)
+
+ s.Equal(testUsername, userSession.Username)
+ s.Equal(authentication.TwoFactor, userSession.AuthenticationLevel)
+ s.Equal(mock.Clock.Now().Unix(), userSession.LastActivity)
+}
+
+func (s *AuthzSuite) TestShouldUpdateInactivityTimestampEvenWhenHittingForbiddenResources() {
+ if s.setRequest == nil {
+ s.T().Skip()
+ }
+
+ builder := s.Builder()
+
+ builder = builder.WithStrategies(
+ NewCookieSessionAuthnStrategy(testInactivity),
+ )
+
+ authz := builder.Build()
+
+ mock := mocks.NewMockAutheliaCtx(s.T())
+
+ defer mock.Close()
+
+ mock.Ctx.Clock = &mock.Clock
+
+ mock.Clock.Set(time.Now())
+
+ mock.Ctx.Configuration.Session.Cookies[0].Inactivity = testInactivity
+
+ 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)
+
+ targetURI := s.RequireParseRequestURI("https://deny.example.com")
+
+ s.setRequest(mock.Ctx, fasthttp.MethodGet, targetURI, true, false)
+
+ last := mock.Clock.Now().Add(-3 * time.Second)
+
+ userSession, err := mock.Ctx.GetSession()
+ s.Require().NoError(err)
+
+ userSession.Username = testUsername
+ userSession.AuthenticationLevel = authentication.TwoFactor
+ userSession.LastActivity = last.Unix()
+ userSession.RefreshTTL = mock.Clock.Now().Add(5 * time.Minute)
+
+ s.Require().NoError(mock.Ctx.SaveSession(userSession))
+
+ authz.Handler(mock.Ctx)
+
+ userSession, err = mock.Ctx.GetSession()
+ s.Require().NoError(err)
+
+ s.Equal(testUsername, userSession.Username)
+ s.Equal(authentication.TwoFactor, userSession.AuthenticationLevel)
+ s.Equal(mock.Clock.Now().Unix(), userSession.LastActivity)
+}
+
+func (s *AuthzSuite) TestShouldNotRefreshUserDetailsFromBackendWhenRefreshDisabled() {
+ if s.setRequest == nil {
+ s.T().Skip()
+ }
+
+ builder := s.Builder()
+
+ builder = builder.WithStrategies(
+ NewCookieSessionAuthnStrategy(-1 * time.Second),
+ )
+
+ authz := builder.Build()
+
+ user := &authentication.UserDetails{
+ Username: "john",
+ Groups: []string{
+ "admin",
+ "users",
+ },
+ Emails: []string{
+ "john@example.com",
+ },
+ }
+
+ mock := mocks.NewMockAutheliaCtx(s.T())
+
+ defer mock.Close()
+
+ mock.Clock.Set(time.Now())
+
+ mock.Ctx.Clock = &mock.Clock
+ mock.Ctx.Configuration.AuthenticationBackend.RefreshInterval = schema.ProfileRefreshDisabled
+ mock.Ctx.Configuration.Session.Cookies[0].Inactivity = testInactivity
+
+ 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)
+
+ targetURI := s.RequireParseRequestURI("https://two-factor.example.com")
+
+ s.setRequest(mock.Ctx, fasthttp.MethodGet, targetURI, true, false)
+
+ userSession, err := mock.Ctx.GetSession()
+ s.Require().NoError(err)
+
+ userSession.Username = user.Username
+ userSession.Groups = user.Groups
+ userSession.Emails = user.Emails
+ userSession.KeepMeLoggedIn = true
+ userSession.AuthenticationLevel = authentication.TwoFactor
+ userSession.LastActivity = mock.Clock.Now().Unix()
+
+ s.Require().NoError(mock.Ctx.SaveSession(userSession))
+
+ mock.UserProviderMock.EXPECT().GetDetails("john").Times(0)
+
+ authz.Handler(mock.Ctx)
+
+ s.Equal(fasthttp.StatusOK, mock.Ctx.Response.StatusCode())
+
+ targetURI = s.RequireParseRequestURI("https://admin.example.com")
+
+ s.setRequest(mock.Ctx, fasthttp.MethodGet, targetURI, true, false)
+
+ authz.Handler(mock.Ctx)
+
+ s.Equal(fasthttp.StatusOK, mock.Ctx.Response.StatusCode())
+
+ userSession, err = mock.Ctx.GetSession()
+ s.Require().NoError(err)
+
+ s.Equal(user.Username, userSession.Username)
+ s.Equal(authentication.TwoFactor, userSession.AuthenticationLevel)
+ s.Equal(mock.Clock.Now().Unix(), userSession.LastActivity)
+ s.Require().Len(userSession.Groups, 2)
+ s.Equal("admin", userSession.Groups[0])
+ s.Equal("users", userSession.Groups[1])
+ s.Equal(utils.RFC3339Zero, userSession.RefreshTTL.Unix())
+
+ authz.Handler(mock.Ctx)
+
+ s.Equal(fasthttp.StatusOK, mock.Ctx.Response.StatusCode())
+
+ userSession, err = mock.Ctx.GetSession()
+ s.Require().NoError(err)
+
+ s.Equal(user.Username, userSession.Username)
+ s.Equal(authentication.TwoFactor, userSession.AuthenticationLevel)
+ s.Equal(mock.Clock.Now().Unix(), userSession.LastActivity)
+ s.Require().Len(userSession.Groups, 2)
+ s.Equal("admin", userSession.Groups[0])
+ s.Equal("users", userSession.Groups[1])
+ s.Equal(utils.RFC3339Zero, userSession.RefreshTTL.Unix())
+}
+
+func (s *AuthzSuite) TestShouldDestroySessionWhenUserDoesNotExist() {
+ if s.setRequest == nil {
+ s.T().Skip()
+ }
+
+ builder := s.Builder()
+
+ builder = builder.WithStrategies(
+ NewCookieSessionAuthnStrategy(5 * time.Minute),
+ )
+
+ authz := builder.Build()
+
+ mock := mocks.NewMockAutheliaCtx(s.T())
+
+ defer mock.Close()
+
+ mock.Ctx.Clock = &mock.Clock
+
+ mock.Clock.Set(time.Now())
+
+ mock.Ctx.Configuration.Session.Cookies[0].Inactivity = testInactivity
+
+ 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)
+
+ targetURI := s.RequireParseRequestURI("https://two-factor.example.com")
+
+ s.setRequest(mock.Ctx, fasthttp.MethodGet, targetURI, true, false)
+
+ user := &authentication.UserDetails{
+ Username: "john",
+ Groups: []string{
+ "admin",
+ "users",
+ },
+ Emails: []string{
+ "john@example.com",
+ },
+ }
+
+ userSession, err := mock.Ctx.GetSession()
+ s.Require().NoError(err)
+
+ userSession.Username = user.Username
+ userSession.AuthenticationLevel = authentication.TwoFactor
+ userSession.LastActivity = mock.Clock.Now().Unix()
+ userSession.RefreshTTL = mock.Clock.Now().Add(-1 * time.Minute)
+ userSession.Groups = user.Groups
+ userSession.Emails = user.Emails
+ userSession.KeepMeLoggedIn = true
+
+ s.Require().NoError(mock.Ctx.SaveSession(userSession))
+
+ gomock.InOrder(
+ mock.UserProviderMock.EXPECT().GetDetails("john").Return(user, nil).Times(1),
+ mock.UserProviderMock.EXPECT().GetDetails("john").Return(nil, authentication.ErrUserNotFound).Times(1),
+ )
+
+ authz.Handler(mock.Ctx)
+
+ s.Equal(fasthttp.StatusOK, mock.Ctx.Response.StatusCode())
+
+ userSession, err = mock.Ctx.GetSession()
+ s.Require().NoError(err)
+
+ s.Equal(mock.Clock.Now().Add(5*time.Minute).Unix(), userSession.RefreshTTL.Unix())
+
+ userSession.RefreshTTL = mock.Clock.Now().Add(-1 * time.Minute)
+
+ s.Require().NoError(mock.Ctx.SaveSession(userSession))
+
+ authz.Handler(mock.Ctx)
+
+ switch s.implementation {
+ case AuthzImplAuthRequest, AuthzImplLegacy:
+ s.Equal(fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode())
+ default:
+ s.Equal(fasthttp.StatusFound, mock.Ctx.Response.StatusCode())
+ }
+
+ userSession, err = mock.Ctx.GetSession()
+ s.Require().NoError(err)
+
+ s.Equal("", userSession.Username)
+ s.Equal(authentication.NotAuthenticated, userSession.AuthenticationLevel)
+ s.True(userSession.IsAnonymous())
+}
+
+func (s *AuthzSuite) TestShouldUpdateRemovedUserGroupsFromBackendAndDeny() {
+ if s.setRequest == nil {
+ s.T().Skip()
+ }
+
+ builder := s.Builder()
+
+ builder = builder.WithStrategies(
+ NewCookieSessionAuthnStrategy(5 * time.Minute),
+ )
+
+ authz := builder.Build()
+
+ mock := mocks.NewMockAutheliaCtx(s.T())
+
+ defer mock.Close()
+
+ mock.Ctx.Clock = &mock.Clock
+
+ mock.Clock.Set(time.Now())
+
+ mock.Ctx.Configuration.Session.Cookies[0].Inactivity = testInactivity
+
+ 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)
+
+ targetURI := s.RequireParseRequestURI("https://admin.example.com")
+
+ s.setRequest(mock.Ctx, fasthttp.MethodGet, targetURI, true, false)
+
+ user := &authentication.UserDetails{
+ Username: "john",
+ Groups: []string{
+ "admin",
+ "users",
+ },
+ Emails: []string{
+ "john@example.com",
+ },
+ }
+
+ userSession, err := mock.Ctx.GetSession()
+ s.Require().NoError(err)
+
+ userSession.Username = user.Username
+ userSession.AuthenticationLevel = authentication.TwoFactor
+ userSession.LastActivity = mock.Clock.Now().Unix()
+ userSession.RefreshTTL = mock.Clock.Now().Add(-1 * time.Minute)
+ userSession.Groups = user.Groups
+ userSession.Emails = user.Emails
+ userSession.KeepMeLoggedIn = true
+
+ s.Require().NoError(mock.Ctx.SaveSession(userSession))
+
+ gomock.InOrder(
+ mock.UserProviderMock.EXPECT().GetDetails("john").Return(user, nil).Times(1),
+ mock.UserProviderMock.EXPECT().GetDetails("john").Return(user, nil).Times(1),
+ )
+
+ authz.Handler(mock.Ctx)
+
+ s.Equal(fasthttp.StatusOK, mock.Ctx.Response.StatusCode())
+
+ userSession, err = mock.Ctx.GetSession()
+ s.Require().NoError(err)
+
+ s.Equal(mock.Clock.Now().Add(5*time.Minute).Unix(), userSession.RefreshTTL.Unix())
+ s.Require().Len(userSession.Groups, 2)
+ s.Require().Equal("admin", userSession.Groups[0])
+ s.Require().Equal("users", userSession.Groups[1])
+
+ user.Groups = []string{"users"}
+
+ mock.Clock.Set(mock.Clock.Now().Add(6 * time.Minute))
+
+ authz.Handler(mock.Ctx)
+
+ s.Equal(fasthttp.StatusForbidden, mock.Ctx.Response.StatusCode())
+
+ userSession, err = mock.Ctx.GetSession()
+ s.Require().NoError(err)
+
+ s.Equal(mock.Clock.Now().Add(5*time.Minute).Unix(), userSession.RefreshTTL.Unix())
+ s.Require().Len(userSession.Groups, 1)
+ s.Require().Equal("users", userSession.Groups[0])
+}
+
+func (s *AuthzSuite) TestShouldUpdateAddedUserGroupsFromBackendAndDeny() {
+ if s.setRequest == nil {
+ s.T().Skip()
+ }
+
+ builder := s.Builder()
+
+ builder = builder.WithStrategies(
+ NewCookieSessionAuthnStrategy(5 * time.Minute),
+ )
+
+ authz := builder.Build()
+
+ mock := mocks.NewMockAutheliaCtx(s.T())
+
+ defer mock.Close()
+
+ mock.Ctx.Clock = &mock.Clock
+
+ mock.Clock.Set(time.Now())
+
+ mock.Ctx.Configuration.Session.Cookies[0].Inactivity = testInactivity
+
+ 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)
+
+ targetURI := s.RequireParseRequestURI("https://admin.example.com")
+
+ s.setRequest(mock.Ctx, fasthttp.MethodGet, targetURI, true, false)
+
+ user := &authentication.UserDetails{
+ Username: "john",
+ Groups: []string{
+ "users",
+ },
+ Emails: []string{
+ "john@example.com",
+ },
+ }
+
+ userSession, err := mock.Ctx.GetSession()
+ s.Require().NoError(err)
+
+ userSession.Username = user.Username
+ userSession.AuthenticationLevel = authentication.TwoFactor
+ userSession.LastActivity = mock.Clock.Now().Unix()
+ userSession.RefreshTTL = mock.Clock.Now().Add(-1 * time.Minute)
+ userSession.Groups = user.Groups
+ userSession.Emails = user.Emails
+ userSession.KeepMeLoggedIn = true
+
+ s.Require().NoError(mock.Ctx.SaveSession(userSession))
+
+ gomock.InOrder(
+ mock.UserProviderMock.EXPECT().GetDetails("john").Return(user, nil).Times(1),
+ mock.UserProviderMock.EXPECT().GetDetails("john").Return(user, nil).Times(1),
+ )
+
+ authz.Handler(mock.Ctx)
+
+ s.Equal(fasthttp.StatusForbidden, mock.Ctx.Response.StatusCode())
+
+ userSession, err = mock.Ctx.GetSession()
+ s.Require().NoError(err)
+
+ s.Equal(mock.Clock.Now().Add(5*time.Minute).Unix(), userSession.RefreshTTL.Unix())
+ s.Require().Len(userSession.Groups, 1)
+ s.Require().Equal("users", userSession.Groups[0])
+
+ user.Groups = []string{"admin", "users"}
+
+ mock.Clock.Set(mock.Clock.Now().Add(6 * time.Minute))
+
+ authz.Handler(mock.Ctx)
+
+ s.Equal(fasthttp.StatusOK, mock.Ctx.Response.StatusCode())
+
+ userSession, err = mock.Ctx.GetSession()
+ s.Require().NoError(err)
+
+ s.Equal(mock.Clock.Now().Add(5*time.Minute).Unix(), userSession.RefreshTTL.Unix())
+ s.Require().Len(userSession.Groups, 2)
+ s.Require().Equal("admin", userSession.Groups[0])
+ s.Require().Equal("users", userSession.Groups[1])
+}
+
+func (s *AuthzSuite) TestShouldCheckValidSessionUsernameHeaderAndReturn200() {
+ if s.setRequest == nil {
+ s.T().Skip()
+ }
+
+ builder := s.Builder()
+
+ builder = builder.WithStrategies(
+ NewCookieSessionAuthnStrategy(testInactivity),
+ )
+
+ authz := builder.Build()
+
+ mock := mocks.NewMockAutheliaCtx(s.T())
+
+ defer mock.Close()
+
+ mock.Ctx.Clock = &mock.Clock
+
+ mock.Clock.Set(time.Now())
+
+ mock.Ctx.Configuration.Session.Cookies[0].Inactivity = testInactivity
+
+ 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)
+
+ targetURI := s.RequireParseRequestURI("https://one-factor.example.com")
+
+ s.setRequest(mock.Ctx, fasthttp.MethodGet, targetURI, true, false)
+
+ mock.Ctx.Request.Header.SetBytesK(headerSessionUsername, testUsername)
+
+ userSession, err := mock.Ctx.GetSession()
+ s.Require().NoError(err)
+
+ userSession.Username = testUsername
+ userSession.AuthenticationLevel = authentication.OneFactor
+ userSession.LastActivity = mock.Clock.Now().Unix()
+ userSession.RefreshTTL = mock.Clock.Now().Add(5 * time.Minute)
+
+ s.Require().NoError(mock.Ctx.SaveSession(userSession))
+
+ authz.Handler(mock.Ctx)
+
+ s.Equal(fasthttp.StatusOK, mock.Ctx.Response.StatusCode())
+
+ userSession, err = mock.Ctx.GetSession()
+ s.Require().NoError(err)
+
+ s.Equal(testUsername, userSession.Username)
+ s.Equal(authentication.OneFactor, userSession.AuthenticationLevel)
+ s.Equal(mock.Clock.Now().Unix(), userSession.LastActivity)
+}
+
+func (s *AuthzSuite) TestShouldCheckInvalidSessionUsernameHeaderAndReturn401AndDestroySession() {
+ if s.setRequest == nil {
+ s.T().Skip()
+ }
+
+ builder := s.Builder()
+
+ builder = builder.WithStrategies(
+ NewCookieSessionAuthnStrategy(testInactivity),
+ )
+
+ authz := builder.Build()
+
+ mock := mocks.NewMockAutheliaCtx(s.T())
+
+ defer mock.Close()
+
+ mock.Ctx.Clock = &mock.Clock
+
+ mock.Clock.Set(time.Now())
+
+ mock.Ctx.Configuration.Session.Cookies[0].Inactivity = testInactivity
+
+ 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)
+
+ targetURI := s.RequireParseRequestURI("https://one-factor.example.com")
+
+ s.setRequest(mock.Ctx, fasthttp.MethodGet, targetURI, true, false)
+
+ mock.Ctx.Request.Header.SetBytesK(headerSessionUsername, "root")
+
+ userSession, err := mock.Ctx.GetSession()
+ s.Require().NoError(err)
+
+ userSession.Username = testUsername
+ userSession.AuthenticationLevel = authentication.OneFactor
+ userSession.LastActivity = mock.Clock.Now().Unix()
+ userSession.RefreshTTL = mock.Clock.Now().Add(5 * time.Minute)
+
+ s.Require().NoError(mock.Ctx.SaveSession(userSession))
+
+ authz.Handler(mock.Ctx)
+
+ switch s.implementation {
+ case AuthzImplAuthRequest, AuthzImplLegacy:
+ s.Equal(fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode())
+ default:
+ s.Equal(fasthttp.StatusFound, mock.Ctx.Response.StatusCode())
+ location := s.RequireParseRequestURI(mock.Ctx.Configuration.Session.Cookies[0].AutheliaURL.String())
+
+ if location.Path == "" {
+ location.Path = "/"
+ }
+
+ query := location.Query()
+ query.Set(queryArgRD, targetURI.String())
+ query.Set(queryArgRM, fasthttp.MethodGet)
+
+ location.RawQuery = query.Encode()
+
+ s.Equal(location.String(), string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation)))
+ }
+
+ userSession, err = mock.Ctx.GetSession()
+ s.Require().NoError(err)
+
+ s.Equal("", userSession.Username)
+ s.Equal(authentication.NotAuthenticated, userSession.AuthenticationLevel)
+ s.Equal(mock.Clock.Now().Unix(), userSession.LastActivity)
+}
+
+func (s *AuthzSuite) TestShouldNotRedirectRequestsForBypassACLWhenInactiveForTooLong() {
+ if s.setRequest == nil {
+ s.T().Skip()
+ }
+
+ builder := s.Builder()
+
+ builder = builder.WithStrategies(
+ NewCookieSessionAuthnStrategy(testInactivity),
+ )
+
+ authz := builder.Build()
+
+ mock := mocks.NewMockAutheliaCtx(s.T())
+
+ defer mock.Close()
+
+ mock.Ctx.Clock = &mock.Clock
+
+ mock.Clock.Set(time.Now())
+
+ past := mock.Clock.Now().Add(-24 * time.Hour)
+
+ mock.Ctx.Configuration.Session.Cookies[0].Inactivity = testInactivity
+
+ 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)
+
+ targetURI := s.RequireParseRequestURI("https://bypass.example.com")
+
+ s.setRequest(mock.Ctx, fasthttp.MethodGet, targetURI, true, false)
+
+ userSession, err := mock.Ctx.GetSession()
+ s.Require().NoError(err)
+
+ userSession.Username = testUsername
+ userSession.AuthenticationLevel = authentication.TwoFactor
+ userSession.LastActivity = past.Unix()
+
+ s.Require().NoError(mock.Ctx.SaveSession(userSession))
+
+ authz.Handler(mock.Ctx)
+
+ s.Equal(fasthttp.StatusOK, mock.Ctx.Response.StatusCode())
+
+ userSession, err = mock.Ctx.GetSession()
+ s.Require().NoError(err)
+
+ s.Equal("", userSession.Username)
+ s.Equal(authentication.NotAuthenticated, userSession.AuthenticationLevel)
+ s.Equal(mock.Clock.Now().Unix(), userSession.LastActivity)
+
+ targetURI = s.RequireParseRequestURI("https://two-factor.example.com")
+
+ s.setRequest(mock.Ctx, fasthttp.MethodGet, targetURI, true, false)
+
+ authz.Handler(mock.Ctx)
+
+ switch s.implementation {
+ case AuthzImplAuthRequest, AuthzImplLegacy:
+ s.Equal(fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode())
+ default:
+ s.Equal(fasthttp.StatusFound, mock.Ctx.Response.StatusCode())
+ location := s.RequireParseRequestURI(mock.Ctx.Configuration.Session.Cookies[0].AutheliaURL.String())
+
+ if location.Path == "" {
+ location.Path = "/"
+ }
+
+ query := location.Query()
+ query.Set(queryArgRD, targetURI.String())
+ query.Set(queryArgRM, fasthttp.MethodGet)
+
+ location.RawQuery = query.Encode()
+
+ s.Equal(location.String(), string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation)))
+ }
+}
+
+func (s *AuthzSuite) TestShouldFailToParsePortalURL() {
+ if s.setRequest == nil || s.implementation == AuthzImplAuthRequest {
+ s.T().Skip()
+ }
+
+ builder := s.Builder()
+
+ builder = builder.WithStrategies(
+ NewCookieSessionAuthnStrategy(testInactivity),
+ )
+
+ authz := builder.Build()
+
+ mock := mocks.NewMockAutheliaCtx(s.T())
+
+ defer mock.Close()
+
+ mock.Ctx.Configuration.Session.Cookies[0].Inactivity = testInactivity
+
+ 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)
+
+ targetURI := s.RequireParseRequestURI("https://bypass.example.com")
+
+ s.setRequest(mock.Ctx, fasthttp.MethodGet, targetURI, true, false)
+
+ switch s.implementation {
+ case AuthzImplLegacy:
+ mock.Ctx.RequestCtx.QueryArgs().Set(queryArgRD, "JKL$#N%KJ#@$N")
+ case AuthzImplForwardAuth:
+ mock.Ctx.RequestCtx.QueryArgs().Set("authelia_url", "JKL$#N%KJ#@$N")
+ case AuthzImplExtAuthz:
+ mock.Ctx.Request.Header.Set("X-Authelia-URL", "JKL$#N%KJ#@$N")
+ }
+
+ authz.Handler(mock.Ctx)
+
+ s.Equal(fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode())
+}
+
+func setRequestXHRValues(ctx *middlewares.AutheliaCtx, accept, xhr bool) {
+ if accept {
+ ctx.Request.Header.Set(fasthttp.HeaderAccept, "text/html; charset=utf-8")
+ }
+
+ if xhr {
+ ctx.Request.Header.Set(fasthttp.HeaderXRequestedWith, "XMLHttpRequest")
+ }
+}
diff --git a/internal/handlers/handler_authz_types.go b/internal/handlers/handler_authz_types.go
new file mode 100644
index 000000000..549e7505f
--- /dev/null
+++ b/internal/handlers/handler_authz_types.go
@@ -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 ""
+ }
+}
diff --git a/internal/handlers/handler_authz_util.go b/internal/handlers/handler_authz_util.go
new file mode 100644
index 000000000..5fadcd953
--- /dev/null
+++ b/internal/handlers/handler_authz_util.go
@@ -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)
+ }
+}
diff --git a/internal/handlers/handler_checks_safe_redirection.go b/internal/handlers/handler_checks_safe_redirection.go
index be1eef04d..72b2bc215 100644
--- a/internal/handlers/handler_checks_safe_redirection.go
+++ b/internal/handlers/handler_checks_safe_redirection.go
@@ -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 s, err = ctx.GetSession(); err != nil {
+ ctx.ReplyUnauthorized()
+ return
+ }
- if userSession.IsAnonymous() {
+ 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 {
diff --git a/internal/handlers/handler_checks_safe_redirection_test.go b/internal/handlers/handler_checks_safe_redirection_test.go
index cb5ec41de..27afaacc2 100644
--- a/internal/handlers/handler_checks_safe_redirection_test.go
+++ b/internal/handlers/handler_checks_safe_redirection_test.go
@@ -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,
})
diff --git a/internal/handlers/handler_firstfactor.go b/internal/handlers/handler_firstfactor.go
index 3f896fb28..75aea5e02 100644
--- a/internal/handlers/handler_firstfactor.go
+++ b/internal/handlers/handler_firstfactor.go
@@ -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
+}
diff --git a/internal/handlers/handler_firstfactor_test.go b/internal/handlers/handler_firstfactor_test.go
index e2864279e..d6f8082ad 100644
--- a/internal/handlers/handler_firstfactor_test.go
+++ b/internal/handlers/handler_firstfactor_test.go
@@ -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"},
diff --git a/internal/handlers/handler_logout_test.go b/internal/handlers/handler_logout_test.go
index 1b14f2e1a..9ffcd2f99 100644
--- a/internal/handlers/handler_logout_test.go
+++ b/internal/handlers/handler_logout_test.go
@@ -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() {
diff --git a/internal/handlers/handler_oidc_authorization.go b/internal/handlers/handler_oidc_authorization.go
index 887f11088..57f686068 100644
--- a/internal/handlers/handler_oidc_authorization.go
+++ b/internal/handlers/handler_oidc_authorization.go
@@ -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
}
diff --git a/internal/handlers/handler_oidc_consent.go b/internal/handlers/handler_oidc_consent.go
index 98935cba0..bea6e9067 100644
--- a/internal/handlers/handler_oidc_consent.go
+++ b/internal/handlers/handler_oidc_consent.go
@@ -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)
diff --git a/internal/handlers/handler_register_duo_device.go b/internal/handlers/handler_register_duo_device.go
index 8182cbdce..ca93950e6 100644
--- a/internal/handlers/handler_register_duo_device.go
+++ b/internal/handlers/handler_register_duo_device.go
@@ -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
}
diff --git a/internal/handlers/handler_register_duo_device_test.go b/internal/handlers/handler_register_duo_device_test.go
index cd77e8a39..f16eead9d 100644
--- a/internal/handlers/handler_register_duo_device_test.go
+++ b/internal/handlers/handler_register_duo_device_test.go
@@ -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)
diff --git a/internal/handlers/handler_register_totp.go b/internal/handlers/handler_register_totp.go
index 160a32961..91fd20072 100644
--- a/internal/handlers/handler_register_totp.go
+++ b/internal/handlers/handler_register_totp.go
@@ -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.
diff --git a/internal/handlers/handler_register_webauthn.go b/internal/handlers/handler_register_webauthn.go
index 8a3a8cfc0..a29802f2e 100644
--- a/internal/handlers/handler_register_webauthn.go
+++ b/internal/handlers/handler_register_webauthn.go
@@ -10,6 +10,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"
)
// WebauthnIdentityStart the handler for initiating the identity validation.
@@ -31,12 +32,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)
@@ -88,11 +96,19 @@ func WebauthnAttestationPOST(ctx *middlewares.AutheliaCtx) {
w *webauthn.WebAuthn
user *model.WebauthnUser
+ userSession session.UserSession
+
attestationResponse *protocol.ParsedCredentialCreationData
credential *webauthn.Credential
)
- 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)
diff --git a/internal/handlers/handler_reset_password_step1.go b/internal/handlers/handler_reset_password_step1.go
index 9c8704410..dc0935ec8 100644
--- a/internal/handlers/handler_reset_password_step1.go
+++ b/internal/handlers/handler_reset_password_step1.go
@@ -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)
}
diff --git a/internal/handlers/handler_reset_password_step2.go b/internal/handlers/handler_reset_password_step2.go
index 9683bfb4f..795364a12 100644
--- a/internal/handlers/handler_reset_password_step2.go
+++ b/internal/handlers/handler_reset_password_step2.go
@@ -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
}
diff --git a/internal/handlers/handler_sign_duo.go b/internal/handlers/handler_sign_duo.go
index 609edca76..f3bb2339a 100644
--- a/internal/handlers/handler_sign_duo.go
+++ b/internal/handlers/handler_sign_duo.go
@@ -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)
diff --git a/internal/handlers/handler_sign_duo_test.go b/internal/handlers/handler_sign_duo_test.go
index 70765bf71..284dac9ef 100644
--- a/internal/handlers/handler_sign_duo_test.go
+++ b/internal/handlers/handler_sign_duo_test.go
@@ -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")))
}
diff --git a/internal/handlers/handler_sign_totp.go b/internal/handlers/handler_sign_totp.go
index 1825dcb84..9afebec7d 100644
--- a/internal/handlers/handler_sign_totp.go
+++ b/internal/handlers/handler_sign_totp.go
@@ -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 {
diff --git a/internal/handlers/handler_sign_totp_test.go b/internal/handlers/handler_sign_totp_test.go
index b4cd12086..c1c6ea3ab 100644
--- a/internal/handlers/handler_sign_totp_test.go
+++ b/internal/handlers/handler_sign_totp_test.go
@@ -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")))
}
diff --git a/internal/handlers/handler_sign_webauthn.go b/internal/handlers/handler_sign_webauthn.go
index 571f2d8cf..da7b88f82 100644
--- a/internal/handlers/handler_sign_webauthn.go
+++ b/internal/handlers/handler_sign_webauthn.go
@@ -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)
diff --git a/internal/handlers/handler_state.go b/internal/handlers/handler_state.go
index 086750fea..c537d9b65 100644
--- a/internal/handlers/handler_state.go
+++ b/internal/handlers/handler_state.go
@@ -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)
}
}
diff --git a/internal/handlers/handler_state_test.go b/internal/handlers/handler_state_test.go
index 2605be470..20baf25ce 100644
--- a/internal/handlers/handler_state_test.go
+++ b/internal/handlers/handler_state_test.go
@@ -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)
diff --git a/internal/handlers/handler_user_info.go b/internal/handlers/handler_user_info.go
index 6e4f5dc0d..54aa056db 100644
--- a/internal/handlers/handler_user_info.go
+++ b/internal/handlers/handler_user_info.go
@@ -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)
diff --git a/internal/handlers/handler_user_info_test.go b/internal/handlers/handler_user_info_test.go
index 1fb8e2a83..251f7e247 100644
--- a/internal/handlers/handler_user_info_test.go
+++ b/internal/handlers/handler_user_info_test.go
@@ -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() {
diff --git a/internal/handlers/handler_user_totp.go b/internal/handlers/handler_user_totp.go
index ee80cc7c8..be3293ae7 100644
--- a/internal/handlers/handler_user_totp.go
+++ b/internal/handlers/handler_user_totp.go
@@ -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.")
diff --git a/internal/handlers/handler_verify.go b/internal/handlers/handler_verify.go
deleted file mode 100644
index 723a05c69..000000000
--- a/internal/handlers/handler_verify.go
+++ /dev/null
@@ -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)
- }
- }
-}
diff --git a/internal/handlers/handler_verify_test.go b/internal/handlers/handler_verify_test.go
deleted file mode 100644
index 20cbf8f22..000000000
--- a/internal/handlers/handler_verify_test.go
+++ /dev/null
@@ -1,1503 +0,0 @@
-package handlers
-
-import (
- "fmt"
- "net"
- "net/url"
- "regexp"
- "testing"
- "time"
-
- "github.com/golang/mock/gomock"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
- "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/configuration/schema"
- "github.com/authelia/authelia/v4/internal/mocks"
- "github.com/authelia/authelia/v4/internal/session"
- "github.com/authelia/authelia/v4/internal/utils"
-)
-
-var verifyGetCfg = schema.AuthenticationBackend{
- RefreshInterval: schema.RefreshIntervalDefault,
- LDAP: &schema.LDAPAuthenticationBackend{},
-}
-
-func TestShouldRaiseWhenTargetUrlIsMalformed(t *testing.T) {
- mock := mocks.NewMockAutheliaCtx(t)
- defer mock.Close()
- mock.Ctx.Request.Header.Set("X-Forwarded-Proto", "https")
- mock.Ctx.Request.Header.Set("X-Forwarded-Host", "home.example.com")
- mock.Ctx.Request.Header.Set("X-Forwarded-URI", "/abc")
- originalURL, err := mock.Ctx.GetOriginalURL()
- assert.NoError(t, err)
-
- expectedURL, err := url.ParseRequestURI("https://home.example.com/abc")
- assert.NoError(t, err)
- assert.Equal(t, expectedURL, originalURL)
-}
-
-func TestShouldRaiseWhenNoHeaderProvidedToDetectTargetURL(t *testing.T) {
- mock := mocks.NewMockAutheliaCtx(t)
- mock.Ctx.Request.Header.Del("X-Forwarded-Host")
-
- defer mock.Close()
- _, err := mock.Ctx.GetOriginalURL()
- assert.Error(t, err)
- assert.Equal(t, "Missing header X-Forwarded-Host", err.Error())
-}
-
-func TestShouldRaiseWhenNoXForwardedHostHeaderProvidedToDetectTargetURL(t *testing.T) {
- mock := mocks.NewMockAutheliaCtx(t)
- defer mock.Close()
-
- mock.Ctx.Request.Header.Del("X-Forwarded-Host")
- mock.Ctx.Request.Header.Set("X-Forwarded-Proto", "https")
- _, err := mock.Ctx.GetOriginalURL()
- assert.Error(t, err)
- assert.Equal(t, "Missing header X-Forwarded-Host", err.Error())
-}
-
-func TestShouldRaiseWhenXForwardedProtoIsNotParsable(t *testing.T) {
- mock := mocks.NewMockAutheliaCtx(t)
- defer mock.Close()
-
- mock.Ctx.Request.Header.Set("X-Forwarded-Proto", "!:;;:,")
- mock.Ctx.Request.Header.Set("X-Forwarded-Host", "myhost.local")
-
- _, err := mock.Ctx.GetOriginalURL()
- assert.Error(t, err)
- assert.Equal(t, "Unable to parse URL !:;;:,://myhost.local/: parse \"!:;;:,://myhost.local/\": invalid URI for request", err.Error())
-}
-
-func TestShouldRaiseWhenXForwardedURIIsNotParsable(t *testing.T) {
- mock := mocks.NewMockAutheliaCtx(t)
- defer mock.Close()
-
- mock.Ctx.Request.Header.Set("X-Forwarded-Proto", "https")
- mock.Ctx.Request.Header.Set("X-Forwarded-Host", "myhost.local")
- mock.Ctx.Request.Header.Set("X-Forwarded-URI", "!:;;:,")
-
- _, err := mock.Ctx.GetOriginalURL()
- require.Error(t, err)
- assert.Equal(t, "Unable to parse URL https://myhost.local!:;;:,: parse \"https://myhost.local!:;;:,\": invalid port \":,\" after host", err.Error())
-}
-
-// Test parseBasicAuth.
-func TestShouldRaiseWhenHeaderDoesNotContainBasicPrefix(t *testing.T) {
- _, _, err := parseBasicAuth(headerProxyAuthorization, "alzefzlfzemjfej==")
- assert.Error(t, err)
- assert.Equal(t, "Basic prefix not found in Proxy-Authorization header", err.Error())
-}
-
-func TestShouldRaiseWhenCredentialsAreNotInBase64(t *testing.T) {
- _, _, err := parseBasicAuth(headerProxyAuthorization, "Basic alzefzlfzemjfej==")
- assert.Error(t, err)
- assert.Equal(t, "illegal base64 data at input byte 16", err.Error())
-}
-
-func TestShouldRaiseWhenCredentialsAreNotInCorrectForm(t *testing.T) {
- // The decoded format should be user:password.
- _, _, err := parseBasicAuth(headerProxyAuthorization, "Basic am9obiBwYXNzd29yZA==")
- assert.Error(t, err)
- assert.Equal(t, "format of Proxy-Authorization header must be user:password", err.Error())
-}
-
-func TestShouldUseProvidedHeaderName(t *testing.T) {
- // The decoded format should be user:password.
- _, _, err := parseBasicAuth([]byte("HeaderName"), "")
- assert.Error(t, err)
- assert.Equal(t, "Basic prefix not found in HeaderName header", err.Error())
-}
-
-func TestShouldReturnUsernameAndPassword(t *testing.T) {
- // the decoded format should be user:password.
- user, password, err := parseBasicAuth(headerProxyAuthorization, "Basic am9objpwYXNzd29yZA==")
- assert.NoError(t, err)
- assert.Equal(t, "john", user)
- assert.Equal(t, "password", password)
-}
-
-// Test isTargetURLAuthorized.
-func TestShouldCheckAuthorizationMatching(t *testing.T) {
- type Rule struct {
- Policy string
- AuthLevel authentication.Level
- ExpectedMatching authorizationMatching
- }
-
- rules := []Rule{
- {"bypass", authentication.NotAuthenticated, Authorized},
- {"bypass", authentication.OneFactor, Authorized},
- {"bypass", authentication.TwoFactor, Authorized},
-
- {"one_factor", authentication.NotAuthenticated, NotAuthorized},
- {"one_factor", authentication.OneFactor, Authorized},
- {"one_factor", authentication.TwoFactor, Authorized},
-
- {"two_factor", authentication.NotAuthenticated, NotAuthorized},
- {"two_factor", authentication.OneFactor, NotAuthorized},
- {"two_factor", authentication.TwoFactor, Authorized},
-
- {"deny", authentication.NotAuthenticated, Forbidden},
- {"deny", authentication.OneFactor, Forbidden},
- {"deny", authentication.TwoFactor, Forbidden},
- }
-
- u, _ := url.ParseRequestURI("https://test.example.com")
-
- for _, rule := range rules {
- authorizer := authorization.NewAuthorizer(&schema.Configuration{
- AccessControl: schema.AccessControlConfiguration{
- DefaultPolicy: "deny",
- Rules: []schema.ACLRule{{
- Domains: []string{"test.example.com"},
- Policy: rule.Policy,
- }},
- }})
-
- username := ""
- if rule.AuthLevel > authentication.NotAuthenticated {
- username = testUsername
- }
-
- matching := isTargetURLAuthorized(authorizer, u, username, []string{}, net.ParseIP("127.0.0.1"), []byte("GET"), rule.AuthLevel)
- assert.Equal(t, rule.ExpectedMatching, matching, "policy=%s, authLevel=%v, expected=%v, actual=%v",
- rule.Policy, rule.AuthLevel, rule.ExpectedMatching, matching)
- }
-}
-
-// Test verifyBasicAuth.
-func TestShouldVerifyWrongCredentials(t *testing.T) {
- mock := mocks.NewMockAutheliaCtx(t)
- defer mock.Close()
-
- mock.UserProviderMock.EXPECT().
- CheckUserPassword(gomock.Eq("john"), gomock.Eq("password")).
- Return(false, nil)
-
- _, _, _, _, _, err := verifyBasicAuth(mock.Ctx, headerProxyAuthorization, []byte("Basic am9objpwYXNzd29yZA=="))
-
- assert.Error(t, err)
-}
-
-type BasicAuthorizationSuite struct {
- suite.Suite
-}
-
-func NewBasicAuthorizationSuite() *BasicAuthorizationSuite {
- return &BasicAuthorizationSuite{}
-}
-
-func (s *BasicAuthorizationSuite) TestShouldNotBeAbleToParseBasicAuth() {
- mock := mocks.NewMockAutheliaCtx(s.T())
- defer mock.Close()
-
- mock.Ctx.Request.Header.Set("Proxy-Authorization", "Basic am9objpaaaaaaaaaaaaaaaa")
- mock.Ctx.Request.Header.Set("X-Original-URL", "https://test.example.com")
-
- VerifyGET(verifyGetCfg)(mock.Ctx)
-
- assert.Equal(s.T(), 401, mock.Ctx.Response.StatusCode())
-}
-
-func (s *BasicAuthorizationSuite) TestShouldApplyDefaultPolicy() {
- mock := mocks.NewMockAutheliaCtx(s.T())
- defer mock.Close()
-
- mock.Ctx.Request.Header.Set("Proxy-Authorization", "Basic am9objpwYXNzd29yZA==")
- mock.Ctx.Request.Header.Set("X-Original-URL", "https://test.example.com")
-
- 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)
-
- VerifyGET(verifyGetCfg)(mock.Ctx)
-
- assert.Equal(s.T(), 403, mock.Ctx.Response.StatusCode())
-}
-
-func (s *BasicAuthorizationSuite) TestShouldApplyPolicyOfBypassDomain() {
- mock := mocks.NewMockAutheliaCtx(s.T())
- defer mock.Close()
-
- mock.Ctx.Request.Header.Set("Proxy-Authorization", "Basic am9objpwYXNzd29yZA==")
- mock.Ctx.Request.Header.Set("X-Original-URL", "https://bypass.example.com")
-
- 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)
-
- VerifyGET(verifyGetCfg)(mock.Ctx)
-
- assert.Equal(s.T(), 200, mock.Ctx.Response.StatusCode())
-}
-
-func (s *BasicAuthorizationSuite) TestShouldApplyPolicyOfOneFactorDomain() {
- mock := mocks.NewMockAutheliaCtx(s.T())
- defer mock.Close()
-
- mock.Ctx.Request.Header.Set("Proxy-Authorization", "Basic am9objpwYXNzd29yZA==")
- mock.Ctx.Request.Header.Set("X-Original-URL", "https://one-factor.example.com")
-
- 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)
-
- VerifyGET(verifyGetCfg)(mock.Ctx)
-
- assert.Equal(s.T(), 200, mock.Ctx.Response.StatusCode())
-}
-
-func (s *BasicAuthorizationSuite) TestShouldApplyPolicyOfTwoFactorDomain() {
- mock := mocks.NewMockAutheliaCtx(s.T())
- defer mock.Close()
-
- mock.Ctx.Request.Header.Set("Proxy-Authorization", "Basic am9objpwYXNzd29yZA==")
- mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com")
-
- 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)
-
- VerifyGET(verifyGetCfg)(mock.Ctx)
-
- assert.Equal(s.T(), 401, mock.Ctx.Response.StatusCode())
-}
-
-func (s *BasicAuthorizationSuite) TestShouldApplyPolicyOfDenyDomain() {
- mock := mocks.NewMockAutheliaCtx(s.T())
- defer mock.Close()
-
- mock.Ctx.Request.Header.Set("Proxy-Authorization", "Basic am9objpwYXNzd29yZA==")
- mock.Ctx.Request.Header.Set("X-Original-URL", "https://deny.example.com")
-
- 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)
-
- VerifyGET(verifyGetCfg)(mock.Ctx)
-
- assert.Equal(s.T(), 403, mock.Ctx.Response.StatusCode())
-}
-
-func (s *BasicAuthorizationSuite) TestShouldVerifyAuthBasicArgOk() {
- mock := mocks.NewMockAutheliaCtx(s.T())
- defer mock.Close()
-
- 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")
-
- 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)
-
- VerifyGET(verifyGetCfg)(mock.Ctx)
-
- assert.Equal(s.T(), 200, mock.Ctx.Response.StatusCode())
-}
-
-func (s *BasicAuthorizationSuite) TestShouldVerifyAuthBasicArgFailingNoHeader() {
- mock := mocks.NewMockAutheliaCtx(s.T())
- defer mock.Close()
-
- mock.Ctx.QueryArgs().Add("auth", "basic")
- mock.Ctx.Request.Header.Set("X-Original-URL", "https://one-factor.example.com")
-
- VerifyGET(verifyGetCfg)(mock.Ctx)
-
- assert.Equal(s.T(), 401, mock.Ctx.Response.StatusCode())
- assert.Equal(s.T(), "401 Unauthorized", string(mock.Ctx.Response.Body()))
- assert.NotEmpty(s.T(), mock.Ctx.Response.Header.Peek("WWW-Authenticate"))
- assert.Regexp(s.T(), regexp.MustCompile("^Basic realm="), string(mock.Ctx.Response.Header.Peek("WWW-Authenticate")))
-}
-
-func (s *BasicAuthorizationSuite) TestShouldVerifyAuthBasicArgFailingEmptyHeader() {
- mock := mocks.NewMockAutheliaCtx(s.T())
- defer mock.Close()
-
- mock.Ctx.QueryArgs().Add("auth", "basic")
- mock.Ctx.Request.Header.Set("Authorization", "")
- mock.Ctx.Request.Header.Set("X-Original-URL", "https://one-factor.example.com")
-
- VerifyGET(verifyGetCfg)(mock.Ctx)
-
- assert.Equal(s.T(), 401, mock.Ctx.Response.StatusCode())
- assert.Equal(s.T(), "401 Unauthorized", string(mock.Ctx.Response.Body()))
- assert.NotEmpty(s.T(), mock.Ctx.Response.Header.Peek("WWW-Authenticate"))
- assert.Regexp(s.T(), regexp.MustCompile("^Basic realm="), string(mock.Ctx.Response.Header.Peek("WWW-Authenticate")))
-}
-
-func (s *BasicAuthorizationSuite) TestShouldVerifyAuthBasicArgFailingWrongPassword() {
- mock := mocks.NewMockAutheliaCtx(s.T())
- defer mock.Close()
-
- 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")
-
- mock.UserProviderMock.EXPECT().
- CheckUserPassword(gomock.Eq("john"), gomock.Eq("password")).
- Return(false, nil)
-
- VerifyGET(verifyGetCfg)(mock.Ctx)
-
- assert.Equal(s.T(), 401, mock.Ctx.Response.StatusCode())
- assert.Equal(s.T(), "401 Unauthorized", string(mock.Ctx.Response.Body()))
- assert.NotEmpty(s.T(), mock.Ctx.Response.Header.Peek("WWW-Authenticate"))
- assert.Regexp(s.T(), regexp.MustCompile("^Basic realm="), string(mock.Ctx.Response.Header.Peek("WWW-Authenticate")))
-}
-
-func (s *BasicAuthorizationSuite) TestShouldVerifyAuthBasicArgFailingWrongHeader() {
- mock := mocks.NewMockAutheliaCtx(s.T())
- defer mock.Close()
-
- mock.Ctx.QueryArgs().Add("auth", "basic")
- mock.Ctx.Request.Header.Set("Proxy-Authorization", "Basic am9objpwYXNzd29yZA==")
- mock.Ctx.Request.Header.Set("X-Original-URL", "https://one-factor.example.com")
-
- VerifyGET(verifyGetCfg)(mock.Ctx)
-
- assert.Equal(s.T(), 401, mock.Ctx.Response.StatusCode())
- assert.Equal(s.T(), "401 Unauthorized", string(mock.Ctx.Response.Body()))
- assert.NotEmpty(s.T(), mock.Ctx.Response.Header.Peek("WWW-Authenticate"))
- assert.Regexp(s.T(), regexp.MustCompile("^Basic realm="), string(mock.Ctx.Response.Header.Peek("WWW-Authenticate")))
-}
-
-func TestShouldVerifyAuthorizationsUsingBasicAuth(t *testing.T) {
- suite.Run(t, NewBasicAuthorizationSuite())
-}
-
-func TestShouldVerifyWrongCredentialsInBasicAuth(t *testing.T) {
- mock := mocks.NewMockAutheliaCtx(t)
- defer mock.Close()
-
- mock.UserProviderMock.EXPECT().
- CheckUserPassword(gomock.Eq("john"), gomock.Eq("wrongpass")).
- Return(false, nil)
-
- mock.Ctx.Request.Header.Set("Proxy-Authorization", "Basic am9objp3cm9uZ3Bhc3M=")
- mock.Ctx.Request.Header.Set("X-Original-URL", "https://test.example.com")
-
- VerifyGET(verifyGetCfg)(mock.Ctx)
- expStatus, actualStatus := 401, mock.Ctx.Response.StatusCode()
- assert.Equal(t, expStatus, actualStatus, "URL=%s -> StatusCode=%d != ExpectedStatusCode=%d",
- "https://test.example.com", actualStatus, expStatus)
-}
-
-func TestShouldRedirectWithGroups(t *testing.T) {
- mock := mocks.NewMockAutheliaCtx(t)
- defer mock.Close()
-
- mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(&schema.Configuration{
- AccessControl: schema.AccessControlConfiguration{
- DefaultPolicy: "deny",
- Rules: []schema.ACLRule{
- {
- Domains: []string{"app.example.com"},
- Policy: "one_factor",
- Resources: []regexp.Regexp{
- *regexp.MustCompile(`^/code-(?P<User>\w+)([/?].*)?$`),
- },
- },
- },
- },
- })
-
- mock.Ctx.Request.Header.Set("Accept", "text/html; charset=utf-8")
- mock.Ctx.Request.Header.Set(fasthttp.HeaderXForwardedProto, "https")
- mock.Ctx.Request.Header.Set(fasthttp.HeaderXForwardedHost, "app.example.com")
- mock.Ctx.Request.Header.Set("X-Forwarded-Uri", "/code-test/login")
-
- mock.Ctx.Request.SetRequestURI("/api/verify/?rd=https://auth.example.com")
-
- VerifyGET(verifyGetCfg)(mock.Ctx)
-
- assert.Equal(t, fasthttp.StatusFound, mock.Ctx.Response.StatusCode())
-}
-
-func TestShouldVerifyFailingPasswordCheckingInBasicAuth(t *testing.T) {
- mock := mocks.NewMockAutheliaCtx(t)
- defer mock.Close()
-
- mock.UserProviderMock.EXPECT().
- CheckUserPassword(gomock.Eq("john"), gomock.Eq("wrongpass")).
- Return(false, fmt.Errorf("Failed"))
-
- mock.Ctx.Request.Header.Set("Proxy-Authorization", "Basic am9objp3cm9uZ3Bhc3M=")
- mock.Ctx.Request.Header.Set("X-Original-URL", "https://test.example.com")
-
- VerifyGET(verifyGetCfg)(mock.Ctx)
- expStatus, actualStatus := 401, mock.Ctx.Response.StatusCode()
- assert.Equal(t, expStatus, actualStatus, "URL=%s -> StatusCode=%d != ExpectedStatusCode=%d",
- "https://test.example.com", actualStatus, expStatus)
-}
-
-func TestShouldVerifyFailingDetailsFetchingInBasicAuth(t *testing.T) {
- mock := mocks.NewMockAutheliaCtx(t)
- defer mock.Close()
-
- mock.UserProviderMock.EXPECT().
- CheckUserPassword(gomock.Eq("john"), gomock.Eq("password")).
- Return(true, nil)
-
- mock.UserProviderMock.EXPECT().
- GetDetails(gomock.Eq("john")).
- Return(nil, fmt.Errorf("Failed"))
-
- mock.Ctx.Request.Header.Set("Proxy-Authorization", "Basic am9objpwYXNzd29yZA==")
- mock.Ctx.Request.Header.Set("X-Original-URL", "https://test.example.com")
-
- VerifyGET(verifyGetCfg)(mock.Ctx)
- expStatus, actualStatus := 401, mock.Ctx.Response.StatusCode()
- assert.Equal(t, expStatus, actualStatus, "URL=%s -> StatusCode=%d != ExpectedStatusCode=%d",
- "https://test.example.com", actualStatus, expStatus)
-}
-
-func TestShouldNotCrashOnEmptyEmail(t *testing.T) {
- mock := mocks.NewMockAutheliaCtx(t)
- defer mock.Close()
-
- mock.Clock.Set(time.Now())
-
- userSession := mock.Ctx.GetSession()
- userSession.Username = testUsername
- userSession.Emails = nil
- userSession.AuthenticationLevel = authentication.OneFactor
- userSession.RefreshTTL = mock.Clock.Now().Add(5 * time.Minute)
-
- err := mock.Ctx.SaveSession(userSession)
- require.NoError(t, err)
-
- mock.Ctx.Request.Header.Set("X-Original-URL", "https://bypass.example.com")
-
- VerifyGET(verifyGetCfg)(mock.Ctx)
-
- expStatus, actualStatus := 200, mock.Ctx.Response.StatusCode()
- assert.Equal(t, expStatus, actualStatus, "URL=%s -> StatusCode=%d != ExpectedStatusCode=%d",
- "https://bypass.example.com", actualStatus, expStatus)
- assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek("Remote-Email"))
-}
-
-type Pair struct {
- URL string
- Username string
- Emails []string
- AuthenticationLevel authentication.Level
- ExpectedStatusCode int
-}
-
-func (p Pair) String() string {
- return fmt.Sprintf("url=%s, username=%s, auth_lvl=%d, exp_status=%d",
- p.URL, p.Username, p.AuthenticationLevel, p.ExpectedStatusCode)
-}
-
-//nolint:gocyclo // This is a test.
-func TestShouldRedirectAuthorizations(t *testing.T) {
- testCases := []struct {
- name string
-
- method, originalURL, autheliaURL string
-
- expected int
- }{
- {"ShouldReturnFoundMethodNone", "", "https://one-factor.example.com/", "https://auth.example.com/", fasthttp.StatusFound},
- {"ShouldReturnFoundMethodGET", "GET", "https://one-factor.example.com/", "https://auth.example.com/", fasthttp.StatusFound},
- {"ShouldReturnFoundMethodOPTIONS", "OPTIONS", "https://one-factor.example.com/", "https://auth.example.com/", fasthttp.StatusFound},
- {"ShouldReturnSeeOtherMethodPOST", "POST", "https://one-factor.example.com/", "https://auth.example.com/", fasthttp.StatusSeeOther},
- {"ShouldReturnSeeOtherMethodPATCH", "PATCH", "https://one-factor.example.com/", "https://auth.example.com/", fasthttp.StatusSeeOther},
- {"ShouldReturnSeeOtherMethodPUT", "PUT", "https://one-factor.example.com/", "https://auth.example.com/", fasthttp.StatusSeeOther},
- {"ShouldReturnSeeOtherMethodDELETE", "DELETE", "https://one-factor.example.com/", "https://auth.example.com/", fasthttp.StatusSeeOther},
- {"ShouldReturnUnauthorizedBadDomain", "GET", "https://one-factor.example.com/", "https://auth.notexample.com/", fasthttp.StatusUnauthorized},
- }
-
- handler := VerifyGET(verifyGetCfg)
-
- for _, tc := range testCases {
- var (
- suffix string
- xhr bool
- )
-
- for i := 0; i < 2; i++ {
- switch i {
- case 0:
- suffix += "QueryParameter"
- default:
- suffix += "RequestHeader"
- }
-
- for j := 0; j < 2; j++ {
- switch j {
- case 0:
- xhr = false
- case 1:
- xhr = true
- suffix += "XHR"
- }
-
- t.Run(tc.name+suffix, func(t *testing.T) {
- mock := mocks.NewMockAutheliaCtx(t)
- defer mock.Close()
-
- mock.Clock.Set(time.Now())
-
- autheliaURL, err := url.ParseRequestURI(tc.autheliaURL)
-
- require.NoError(t, err)
-
- originalURL, err := url.ParseRequestURI(tc.originalURL)
-
- require.NoError(t, err)
-
- if xhr {
- mock.Ctx.Request.Header.Set(fasthttp.HeaderXRequestedWith, "XMLHttpRequest")
- }
-
- var rm string
-
- if tc.method != "" {
- rm = fmt.Sprintf("&rm=%s", tc.method)
- mock.Ctx.Request.Header.Set("X-Forwarded-Method", tc.method)
- }
-
- mock.Ctx.Request.Header.Set("Accept", "text/html; charset=utf-8")
- mock.Ctx.Request.Header.Set("X-Original-URL", originalURL.String())
-
- if i == 0 {
- mock.Ctx.Request.SetRequestURI(fmt.Sprintf("/?rd=%s", url.QueryEscape(autheliaURL.String())))
- } else {
- mock.Ctx.Request.Header.Set("X-Authelia-URL", autheliaURL.String())
- }
-
- handler(mock.Ctx)
-
- if xhr && tc.expected != fasthttp.StatusUnauthorized {
- assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode())
- } else {
- assert.Equal(t, tc.expected, mock.Ctx.Response.StatusCode())
- }
-
- switch {
- case xhr && tc.expected != fasthttp.StatusUnauthorized:
- href := utils.StringHTMLEscape(fmt.Sprintf("%s?rd=%s%s", autheliaURL.String(), url.QueryEscape(originalURL.String()), rm))
- assert.Equal(t, fmt.Sprintf("<a href=\"%s\">%d %s</a>", href, fasthttp.StatusUnauthorized, fasthttp.StatusMessage(fasthttp.StatusUnauthorized)), string(mock.Ctx.Response.Body()))
- case tc.expected >= fasthttp.StatusMultipleChoices && tc.expected < fasthttp.StatusBadRequest:
- href := utils.StringHTMLEscape(fmt.Sprintf("%s?rd=%s%s", autheliaURL.String(), url.QueryEscape(originalURL.String()), rm))
- assert.Equal(t, fmt.Sprintf("<a href=\"%s\">%d %s</a>", href, tc.expected, fasthttp.StatusMessage(tc.expected)), string(mock.Ctx.Response.Body()))
- case tc.expected < fasthttp.StatusMultipleChoices:
- assert.Equal(t, utils.StringHTMLEscape(fmt.Sprintf("%d %s", tc.expected, fasthttp.StatusMessage(tc.expected))), string(mock.Ctx.Response.Body()))
- default:
- assert.Equal(t, utils.StringHTMLEscape(fmt.Sprintf("%d %s", tc.expected, fasthttp.StatusMessage(tc.expected))), string(mock.Ctx.Response.Body()))
- }
- })
- }
- }
- }
-}
-
-func TestShouldVerifyAuthorizationsUsingSessionCookie(t *testing.T) {
- testCases := []Pair{
- // should apply default policy.
- {"https://test.example.com", "", nil, authentication.NotAuthenticated, 403},
- {"https://bypass.example.com", "", nil, authentication.NotAuthenticated, 200},
- {"https://one-factor.example.com", "", nil, authentication.NotAuthenticated, 401},
- {"https://two-factor.example.com", "", nil, authentication.NotAuthenticated, 401},
- {"https://deny.example.com", "", nil, authentication.NotAuthenticated, 403},
-
- {"https://test.example.com", "john", []string{"john.doe@example.com"}, authentication.OneFactor, 403},
- {"https://bypass.example.com", "john", []string{"john.doe@example.com"}, authentication.OneFactor, 200},
- {"https://one-factor.example.com", "john", []string{"john.doe@example.com"}, authentication.OneFactor, 200},
- {"https://two-factor.example.com", "john", []string{"john.doe@example.com"}, authentication.OneFactor, 401},
- {"https://deny.example.com", "john", []string{"john.doe@example.com"}, authentication.OneFactor, 403},
-
- {"https://test.example.com", "john", []string{"john.doe@example.com"}, authentication.TwoFactor, 403},
- {"https://bypass.example.com", "john", []string{"john.doe@example.com"}, authentication.TwoFactor, 200},
- {"https://one-factor.example.com", "john", []string{"john.doe@example.com"}, authentication.TwoFactor, 200},
- {"https://two-factor.example.com", "john", []string{"john.doe@example.com"}, authentication.TwoFactor, 200},
- {"https://deny.example.com", "john", []string{"john.doe@example.com"}, authentication.TwoFactor, 403},
- }
-
- for i, tc := range testCases {
- t.Run(tc.String(), func(t *testing.T) {
- mock := mocks.NewMockAutheliaCtx(t)
- defer mock.Close()
-
- mock.Clock.Set(time.Now())
-
- mock.Ctx.Request.Header.Set("X-Original-URL", tc.URL)
-
- userSession := mock.Ctx.GetSession()
- userSession.Username = tc.Username
- userSession.Emails = tc.Emails
- userSession.AuthenticationLevel = tc.AuthenticationLevel
- userSession.RefreshTTL = mock.Clock.Now().Add(5 * time.Minute)
-
- err := mock.Ctx.SaveSession(userSession)
- require.NoError(t, err)
-
- VerifyGET(verifyGetCfg)(mock.Ctx)
- expStatus, actualStatus := tc.ExpectedStatusCode, mock.Ctx.Response.StatusCode()
- assert.Equal(t, expStatus, actualStatus, "URL=%s -> AuthLevel=%d, StatusCode=%d != ExpectedStatusCode=%d",
- tc.URL, tc.AuthenticationLevel, actualStatus, expStatus)
-
- fmt.Println(i)
- if tc.ExpectedStatusCode == 200 && tc.Username != "" {
- assert.Equal(t, tc.ExpectedStatusCode, mock.Ctx.Response.StatusCode())
- assert.Equal(t, []byte(tc.Username), mock.Ctx.Response.Header.Peek("Remote-User"))
- assert.Equal(t, []byte("john.doe@example.com"), mock.Ctx.Response.Header.Peek("Remote-Email"))
- } else {
- assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek("Remote-User"))
- assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek("Remote-Email"))
- }
- })
- }
-}
-
-func TestShouldDestroySessionWhenInactiveForTooLong(t *testing.T) {
- mock := mocks.NewMockAutheliaCtx(t)
- defer mock.Close()
-
- clock := utils.TestingClock{}
- clock.Set(time.Now())
- past := clock.Now().Add(-1 * time.Hour)
-
- mock.Ctx.Configuration.Session.Cookies[0].Inactivity = testInactivity
- // Reload the session provider since the configuration is indirect.
- mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil)
- assert.Equal(t, time.Second*10, mock.Ctx.Configuration.Session.Cookies[0].Inactivity)
-
- mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com")
-
- userSession := mock.Ctx.GetSession()
- userSession.Username = testUsername
- userSession.AuthenticationLevel = authentication.TwoFactor
- userSession.LastActivity = past.Unix()
-
- err := mock.Ctx.SaveSession(userSession)
- require.NoError(t, err)
-
- VerifyGET(verifyGetCfg)(mock.Ctx)
-
- // The session has been destroyed.
- newUserSession := mock.Ctx.GetSession()
- assert.Equal(t, "", newUserSession.Username)
- assert.Equal(t, authentication.NotAuthenticated, newUserSession.AuthenticationLevel)
-
- // Check the inactivity timestamp has been updated to current time in the new session.
- assert.Equal(t, clock.Now().Unix(), newUserSession.LastActivity)
-}
-
-func TestShouldDestroySessionWhenInactiveForTooLongUsingDurationNotation(t *testing.T) {
- mock := mocks.NewMockAutheliaCtx(t)
- defer mock.Close()
-
- clock := utils.TestingClock{}
- clock.Set(time.Now())
-
- mock.Ctx.Configuration.Session.Cookies[0].Inactivity = time.Second * 10
- // Reload the session provider since the configuration is indirect.
- mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil)
- assert.Equal(t, time.Second*10, mock.Ctx.Configuration.Session.Cookies[0].Inactivity)
-
- userSession := mock.Ctx.GetSession()
- userSession.Username = testUsername
- userSession.AuthenticationLevel = authentication.TwoFactor
- userSession.LastActivity = clock.Now().Add(-1 * time.Hour).Unix()
-
- err := mock.Ctx.SaveSession(userSession)
- require.NoError(t, err)
-
- mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com")
-
- VerifyGET(verifyGetCfg)(mock.Ctx)
-
- // The session has been destroyed.
- newUserSession := mock.Ctx.GetSession()
- assert.Equal(t, "", newUserSession.Username)
- assert.Equal(t, authentication.NotAuthenticated, newUserSession.AuthenticationLevel)
-}
-
-func TestShouldKeepSessionWhenUserCheckedRememberMeAndIsInactiveForTooLong(t *testing.T) {
- mock := mocks.NewMockAutheliaCtx(t)
- defer mock.Close()
-
- mock.Clock.Set(time.Now())
-
- mock.Ctx.Configuration.Session.Cookies[0].Inactivity = testInactivity
-
- userSession := mock.Ctx.GetSession()
- userSession.Username = testUsername
- userSession.Emails = []string{"john.doe@example.com"}
- userSession.AuthenticationLevel = authentication.TwoFactor
- userSession.LastActivity = 0
- userSession.KeepMeLoggedIn = true
- userSession.RefreshTTL = mock.Clock.Now().Add(5 * time.Minute)
-
- err := mock.Ctx.SaveSession(userSession)
- require.NoError(t, err)
-
- mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com")
-
- VerifyGET(verifyGetCfg)(mock.Ctx)
-
- // Check the session is still active.
- newUserSession := mock.Ctx.GetSession()
- assert.Equal(t, "john", newUserSession.Username)
- assert.Equal(t, authentication.TwoFactor, newUserSession.AuthenticationLevel)
-
- // Check the inactivity timestamp is set to 0 in case remember me is checked.
- assert.Equal(t, int64(0), newUserSession.LastActivity)
-}
-
-func TestShouldKeepSessionWhenInactivityTimeoutHasNotBeenExceeded(t *testing.T) {
- mock := mocks.NewMockAutheliaCtx(t)
- defer mock.Close()
-
- mock.Clock.Set(time.Now())
-
- mock.Ctx.Configuration.Session.Cookies[0].Inactivity = testInactivity
-
- past := mock.Clock.Now().Add(-1 * time.Hour)
-
- userSession := mock.Ctx.GetSession()
- userSession.Username = testUsername
- userSession.Emails = []string{"john.doe@example.com"}
- userSession.AuthenticationLevel = authentication.TwoFactor
- userSession.LastActivity = past.Unix()
- userSession.RefreshTTL = mock.Clock.Now().Add(5 * time.Minute)
-
- err := mock.Ctx.SaveSession(userSession)
- require.NoError(t, err)
-
- mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com")
-
- VerifyGET(verifyGetCfg)(mock.Ctx)
-
- // The session has been destroyed.
- newUserSession := mock.Ctx.GetSession()
- assert.Equal(t, "john", newUserSession.Username)
- assert.Equal(t, authentication.TwoFactor, newUserSession.AuthenticationLevel)
-
- // Check the inactivity timestamp has been updated to current time in the new session.
- assert.Equal(t, mock.Clock.Now().Unix(), newUserSession.LastActivity)
-}
-
-// In the case of Traefik and Nginx ingress controller in Kube, the response to an inactive
-// session is 302 instead of 401.
-func TestShouldRedirectWhenSessionInactiveForTooLongAndRDParamProvided(t *testing.T) {
- mock := mocks.NewMockAutheliaCtx(t)
- defer mock.Close()
-
- clock := utils.TestingClock{}
- clock.Set(time.Now())
-
- mock.Ctx.Configuration.Session.Cookies[0].Inactivity = testInactivity
- // Reload the session provider since the configuration is indirect.
- mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil)
- assert.Equal(t, time.Second*10, mock.Ctx.Configuration.Session.Cookies[0].Inactivity)
-
- past := clock.Now().Add(-1 * time.Hour)
-
- userSession := mock.Ctx.GetSession()
- userSession.Username = testUsername
- userSession.AuthenticationLevel = authentication.TwoFactor
- userSession.LastActivity = past.Unix()
-
- err := mock.Ctx.SaveSession(userSession)
- require.NoError(t, err)
-
- mock.Ctx.QueryArgs().Add(queryArgRD, "https://login.example.com")
- mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com")
- mock.Ctx.Request.Header.Set("X-Forwarded-Method", "GET")
- mock.Ctx.Request.Header.Set("Accept", "text/html; charset=utf-8")
- VerifyGET(verifyGetCfg)(mock.Ctx)
-
- assert.Equal(t, "<a href=\"https://login.example.com/?rd=https%3A%2F%2Ftwo-factor.example.com&amp;rm=GET\">302 Found</a>",
- string(mock.Ctx.Response.Body()))
- assert.Equal(t, 302, mock.Ctx.Response.StatusCode())
-
- // Check the inactivity timestamp has been updated to current time in the new session.
- newUserSession := mock.Ctx.GetSession()
- assert.Equal(t, clock.Now().Unix(), newUserSession.LastActivity)
-}
-
-func TestShouldRedirectWithCorrectStatusCodeBasedOnRequestMethod(t *testing.T) {
- mock := mocks.NewMockAutheliaCtx(t)
- defer mock.Close()
-
- mock.Ctx.QueryArgs().Add(queryArgRD, "https://login.example.com")
- mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com")
- mock.Ctx.Request.Header.Set("X-Forwarded-Method", "GET")
- mock.Ctx.Request.Header.Set("Accept", "text/html; charset=utf-8")
-
- VerifyGET(verifyGetCfg)(mock.Ctx)
-
- assert.Equal(t, "<a href=\"https://login.example.com/?rd=https%3A%2F%2Ftwo-factor.example.com&amp;rm=GET\">302 Found</a>",
- string(mock.Ctx.Response.Body()))
- assert.Equal(t, 302, mock.Ctx.Response.StatusCode())
-
- mock.Ctx.QueryArgs().Add(queryArgRD, "https://login.example.com")
- mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com")
- mock.Ctx.Request.Header.Set("X-Forwarded-Method", "POST")
- mock.Ctx.Request.Header.Set("Accept", "text/html; charset=utf-8")
-
- VerifyGET(verifyGetCfg)(mock.Ctx)
-
- assert.Equal(t, "<a href=\"https://login.example.com/?rd=https%3A%2F%2Ftwo-factor.example.com&amp;rm=POST\">303 See Other</a>",
- string(mock.Ctx.Response.Body()))
- assert.Equal(t, 303, mock.Ctx.Response.StatusCode())
-}
-
-func TestShouldUpdateInactivityTimestampEvenWhenHittingForbiddenResources(t *testing.T) {
- mock := mocks.NewMockAutheliaCtx(t)
- defer mock.Close()
-
- mock.Clock.Set(time.Now())
-
- mock.Ctx.Configuration.Session.Cookies[0].Inactivity = testInactivity
-
- past := mock.Clock.Now().Add(-1 * time.Hour)
-
- userSession := mock.Ctx.GetSession()
- userSession.Username = testUsername
- userSession.AuthenticationLevel = authentication.TwoFactor
- userSession.LastActivity = past.Unix()
- userSession.RefreshTTL = mock.Clock.Now().Add(5 * time.Minute)
-
- err := mock.Ctx.SaveSession(userSession)
- require.NoError(t, err)
-
- mock.Ctx.Request.Header.Set("X-Original-URL", "https://deny.example.com")
-
- VerifyGET(verifyGetCfg)(mock.Ctx)
-
- // The resource if forbidden.
- assert.Equal(t, 403, mock.Ctx.Response.StatusCode())
-
- // Check the inactivity timestamp has been updated to current time in the new session.
- newUserSession := mock.Ctx.GetSession()
- assert.Equal(t, mock.Clock.Now().Unix(), newUserSession.LastActivity)
-}
-
-func TestShouldURLEncodeRedirectionURLParameter(t *testing.T) {
- mock := mocks.NewMockAutheliaCtx(t)
- defer mock.Close()
-
- mock.Clock.Set(time.Now())
-
- userSession := mock.Ctx.GetSession()
- userSession.Username = testUsername
- userSession.AuthenticationLevel = authentication.NotAuthenticated
- userSession.RefreshTTL = mock.Clock.Now().Add(5 * time.Minute)
-
- err := mock.Ctx.SaveSession(userSession)
- require.NoError(t, err)
-
- mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com")
- mock.Ctx.Request.Header.Set("Accept", "text/html; charset=utf-8")
- mock.Ctx.Request.SetHost("mydomain.com")
- mock.Ctx.Request.SetRequestURI("/?rd=https://auth.example.com")
-
- VerifyGET(verifyGetCfg)(mock.Ctx)
-
- assert.Equal(t, "<a href=\"https://auth.example.com/?rd=https%3A%2F%2Ftwo-factor.example.com\">302 Found</a>",
- string(mock.Ctx.Response.Body()))
-}
-
-func TestShouldURLEncodeRedirectionHeader(t *testing.T) {
- mock := mocks.NewMockAutheliaCtx(t)
- defer mock.Close()
-
- mock.Clock.Set(time.Now())
-
- userSession := mock.Ctx.GetSession()
- userSession.Username = testUsername
- userSession.AuthenticationLevel = authentication.NotAuthenticated
- userSession.RefreshTTL = mock.Clock.Now().Add(5 * time.Minute)
-
- err := mock.Ctx.SaveSession(userSession)
- require.NoError(t, err)
-
- mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com")
- mock.Ctx.Request.Header.Set("X-Authelia-URL", "https://auth.example.com")
- mock.Ctx.Request.Header.Set("Accept", "text/html; charset=utf-8")
- mock.Ctx.Request.SetHost("mydomain.com")
-
- VerifyGET(verifyGetCfg)(mock.Ctx)
-
- assert.Equal(t, "<a href=\"https://auth.example.com/?rd=https%3A%2F%2Ftwo-factor.example.com\">302 Found</a>",
- string(mock.Ctx.Response.Body()))
-}
-
-func TestSchemeIsWSS(t *testing.T) {
- GetURL := func(u string) *url.URL {
- x, err := url.ParseRequestURI(u)
- require.NoError(t, err)
-
- return x
- }
-
- assert.False(t, isSchemeWSS(
- GetURL("ws://mytest.example.com/abc/?query=abc")))
- assert.False(t, isSchemeWSS(
- GetURL("http://mytest.example.com/abc/?query=abc")))
- assert.False(t, isSchemeWSS(
- GetURL("https://mytest.example.com/abc/?query=abc")))
- assert.True(t, isSchemeWSS(
- GetURL("wss://mytest.example.com/abc/?query=abc")))
-}
-
-func TestShouldNotRefreshUserGroupsFromBackend(t *testing.T) {
- mock := mocks.NewMockAutheliaCtx(t)
- defer mock.Close()
-
- // Setup pointer to john so we can adjust it during the test.
- user := &authentication.UserDetails{
- Username: "john",
- Groups: []string{
- "admin",
- "users",
- },
- Emails: []string{
- "john@example.com",
- },
- }
-
- cfg := verifyGetCfg
- cfg.RefreshInterval = "disable"
- verifyGet := VerifyGET(cfg)
-
- mock.UserProviderMock.EXPECT().GetDetails("john").Times(0)
-
- clock := utils.TestingClock{}
- clock.Set(time.Now())
-
- userSession := mock.Ctx.GetSession()
- userSession.Username = user.Username
- userSession.AuthenticationLevel = authentication.TwoFactor
- userSession.LastActivity = clock.Now().Unix()
- userSession.Groups = user.Groups
- userSession.Emails = user.Emails
- userSession.KeepMeLoggedIn = true
- err := mock.Ctx.SaveSession(userSession)
- require.NoError(t, err)
-
- mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com")
- verifyGet(mock.Ctx)
- assert.Equal(t, 200, mock.Ctx.Response.StatusCode())
-
- mock.Ctx.Request.Header.Set("X-Original-URL", "https://admin.example.com")
- verifyGet(mock.Ctx)
- assert.Equal(t, 200, mock.Ctx.Response.StatusCode())
-
- // Check Refresh TTL has not been updated.
- userSession = mock.Ctx.GetSession()
-
- // Check user groups are correct.
- require.Len(t, userSession.Groups, len(user.Groups))
- assert.Equal(t, utils.RFC3339Zero, userSession.RefreshTTL.Unix())
- assert.Equal(t, "admin", userSession.Groups[0])
- assert.Equal(t, "users", userSession.Groups[1])
-
- mock.Ctx.Request.Header.Set("X-Original-URL", "https://admin.example.com")
- verifyGet(mock.Ctx)
- assert.Equal(t, 200, mock.Ctx.Response.StatusCode())
-
- // Check admin group is not removed from the session.
- userSession = mock.Ctx.GetSession()
- assert.Equal(t, utils.RFC3339Zero, userSession.RefreshTTL.Unix())
- require.Len(t, userSession.Groups, 2)
- assert.Equal(t, "admin", userSession.Groups[0])
- assert.Equal(t, "users", userSession.Groups[1])
-}
-
-func TestShouldNotRefreshUserGroupsFromBackendWhenDisabled(t *testing.T) {
- mock := mocks.NewMockAutheliaCtx(t)
- defer mock.Close()
-
- // Setup user john.
- user := &authentication.UserDetails{
- Username: "john",
- Groups: []string{
- "admin",
- "users",
- },
- Emails: []string{
- "john@example.com",
- },
- }
-
- mock.UserProviderMock.EXPECT().GetDetails("john").Times(0)
-
- clock := utils.TestingClock{}
- clock.Set(time.Now())
-
- userSession := mock.Ctx.GetSession()
- userSession.Username = user.Username
- userSession.AuthenticationLevel = authentication.TwoFactor
- userSession.LastActivity = clock.Now().Unix()
- userSession.RefreshTTL = clock.Now().Add(-1 * time.Minute)
- userSession.Groups = user.Groups
- userSession.Emails = user.Emails
- userSession.KeepMeLoggedIn = true
- err := mock.Ctx.SaveSession(userSession)
-
- require.NoError(t, err)
-
- mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com")
-
- config := verifyGetCfg
- config.RefreshInterval = schema.ProfileRefreshDisabled
-
- VerifyGET(config)(mock.Ctx)
- assert.Equal(t, 200, mock.Ctx.Response.StatusCode())
-
- // Session time should NOT have been updated, it should still have a refresh TTL 1 minute in the past.
- userSession = mock.Ctx.GetSession()
- assert.Equal(t, clock.Now().Add(-1*time.Minute).Unix(), userSession.RefreshTTL.Unix())
-}
-
-func TestShouldDestroySessionWhenUserNotExist(t *testing.T) {
- mock := mocks.NewMockAutheliaCtx(t)
- defer mock.Close()
-
- // Setup user john.
- user := &authentication.UserDetails{
- Username: "john",
- Groups: []string{
- "admin",
- "users",
- },
- Emails: []string{
- "john@example.com",
- },
- }
-
- mock.UserProviderMock.EXPECT().GetDetails("john").Return(user, nil).Times(1)
-
- clock := utils.TestingClock{}
- clock.Set(time.Now())
-
- userSession := mock.Ctx.GetSession()
- userSession.Username = user.Username
- userSession.AuthenticationLevel = authentication.TwoFactor
- userSession.LastActivity = clock.Now().Unix()
- userSession.RefreshTTL = clock.Now().Add(-1 * time.Minute)
- userSession.Groups = user.Groups
- userSession.Emails = user.Emails
- userSession.KeepMeLoggedIn = true
- err := mock.Ctx.SaveSession(userSession)
-
- require.NoError(t, err)
-
- mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com")
-
- VerifyGET(verifyGetCfg)(mock.Ctx)
- assert.Equal(t, 200, mock.Ctx.Response.StatusCode())
-
- // Session time should NOT have been updated, it should still have a refresh TTL 1 minute in the past.
- userSession = mock.Ctx.GetSession()
- assert.Equal(t, clock.Now().Add(5*time.Minute).Unix(), userSession.RefreshTTL.Unix())
-
- // Simulate a Deleted User.
- userSession.RefreshTTL = clock.Now().Add(-1 * time.Minute)
- err = mock.Ctx.SaveSession(userSession)
-
- require.NoError(t, err)
-
- mock.UserProviderMock.EXPECT().GetDetails("john").Return(nil, authentication.ErrUserNotFound).Times(1)
-
- VerifyGET(verifyGetCfg)(mock.Ctx)
-
- assert.Equal(t, 401, mock.Ctx.Response.StatusCode())
-
- userSession = mock.Ctx.GetSession()
- assert.Equal(t, "", userSession.Username)
- assert.Equal(t, authentication.NotAuthenticated, userSession.AuthenticationLevel)
-}
-
-func TestShouldGetRemovedUserGroupsFromBackend(t *testing.T) {
- mock := mocks.NewMockAutheliaCtx(t)
- defer mock.Close()
-
- // Setup pointer to john so we can adjust it during the test.
- user := &authentication.UserDetails{
- Username: "john",
- Groups: []string{
- "admin",
- "users",
- },
- Emails: []string{
- "john@example.com",
- },
- }
-
- verifyGet := VerifyGET(verifyGetCfg)
-
- mock.UserProviderMock.EXPECT().GetDetails("john").Return(user, nil).Times(2)
-
- clock := utils.TestingClock{}
- clock.Set(time.Now())
-
- userSession := mock.Ctx.GetSession()
- userSession.Username = user.Username
- userSession.AuthenticationLevel = authentication.TwoFactor
- userSession.LastActivity = clock.Now().Unix()
- userSession.RefreshTTL = clock.Now().Add(-1 * time.Minute)
- userSession.Groups = user.Groups
- userSession.Emails = user.Emails
- userSession.KeepMeLoggedIn = true
- err := mock.Ctx.SaveSession(userSession)
- require.NoError(t, err)
-
- mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com")
- verifyGet(mock.Ctx)
- assert.Equal(t, 200, mock.Ctx.Response.StatusCode())
-
- // Request should get refresh settings and new user details.
-
- mock.Ctx.Request.Header.Set("X-Original-URL", "https://admin.example.com")
- verifyGet(mock.Ctx)
- assert.Equal(t, 200, mock.Ctx.Response.StatusCode())
-
- // Check Refresh TTL has been updated since admin.example.com has a group subject and refresh is enabled.
- userSession = mock.Ctx.GetSession()
-
- // Check user groups are correct.
- require.Len(t, userSession.Groups, len(user.Groups))
- assert.Equal(t, clock.Now().Add(5*time.Minute).Unix(), userSession.RefreshTTL.Unix())
- assert.Equal(t, "admin", userSession.Groups[0])
- assert.Equal(t, "users", userSession.Groups[1])
-
- // Remove the admin group, and force the next request to refresh.
- user.Groups = []string{"users"}
- userSession.RefreshTTL = clock.Now().Add(-1 * time.Second)
- err = mock.Ctx.SaveSession(userSession)
- require.NoError(t, err)
-
- mock.Ctx.Request.Header.Set("X-Original-URL", "https://admin.example.com")
- verifyGet(mock.Ctx)
- assert.Equal(t, 403, mock.Ctx.Response.StatusCode())
-
- // Check admin group is removed from the session.
- userSession = mock.Ctx.GetSession()
- assert.Equal(t, clock.Now().Add(5*time.Minute).Unix(), userSession.RefreshTTL.Unix())
- require.Len(t, userSession.Groups, 1)
- assert.Equal(t, "users", userSession.Groups[0])
-}
-
-func TestShouldGetAddedUserGroupsFromBackend(t *testing.T) {
- mock := mocks.NewMockAutheliaCtx(t)
-
- // Setup pointer to john so we can adjust it during the test.
- user := &authentication.UserDetails{
- Username: "john",
- Groups: []string{
- "admin",
- "users",
- },
- Emails: []string{
- "john@example.com",
- },
- }
-
- mock.UserProviderMock.EXPECT().GetDetails("john").Return(user, nil).Times(1)
-
- verifyGet := VerifyGET(verifyGetCfg)
-
- mock.Clock.Set(time.Now())
-
- userSession := mock.Ctx.GetSession()
- userSession.Username = user.Username
- userSession.AuthenticationLevel = authentication.TwoFactor
- userSession.LastActivity = mock.Clock.Now().Unix()
- userSession.RefreshTTL = mock.Clock.Now().Add(-1 * time.Minute)
- userSession.Groups = user.Groups
- userSession.Emails = user.Emails
- userSession.KeepMeLoggedIn = true
- err := mock.Ctx.SaveSession(userSession)
- require.NoError(t, err)
-
- mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com")
- verifyGet(mock.Ctx)
- assert.Equal(t, 200, mock.Ctx.Response.StatusCode())
-
- mock.Ctx.Request.Header.Set("X-Original-URL", "https://grafana.example.com")
- verifyGet(mock.Ctx)
- assert.Equal(t, 403, mock.Ctx.Response.StatusCode())
-
- // Check Refresh TTL has been updated since grafana.example.com has a group subject and refresh is enabled.
- userSession = mock.Ctx.GetSession()
-
- // Check user groups are correct.
- require.Len(t, userSession.Groups, len(user.Groups))
- assert.Equal(t, mock.Clock.Now().Add(5*time.Minute).Unix(), userSession.RefreshTTL.Unix())
- assert.Equal(t, "admin", userSession.Groups[0])
- assert.Equal(t, "users", userSession.Groups[1])
-
- // Add the grafana group, and force the next request to refresh.
- user.Groups = append(user.Groups, "grafana")
- userSession.RefreshTTL = mock.Clock.Now().Add(-1 * time.Second)
- err = mock.Ctx.SaveSession(userSession)
- require.NoError(t, err)
-
- // Reset otherwise we get the last 403 when we check the Response. Is there a better way to do this?
- mock.Close()
-
- mock = mocks.NewMockAutheliaCtx(t)
- defer mock.Close()
- err = mock.Ctx.SaveSession(userSession)
- assert.NoError(t, err)
-
- mock.Clock.Set(time.Now())
-
- gomock.InOrder(
- mock.UserProviderMock.EXPECT().GetDetails("john").Return(user, nil).Times(1),
- )
-
- mock.Ctx.Request.Header.Set("X-Original-URL", "https://grafana.example.com")
- VerifyGET(verifyGetCfg)(mock.Ctx)
- assert.Equal(t, 200, mock.Ctx.Response.StatusCode())
-
- // Check admin group is removed from the session.
- userSession = mock.Ctx.GetSession()
- assert.Equal(t, true, userSession.KeepMeLoggedIn)
- assert.Equal(t, authentication.TwoFactor, userSession.AuthenticationLevel)
- assert.Equal(t, mock.Clock.Now().Add(5*time.Minute).Unix(), userSession.RefreshTTL.Unix())
- require.Len(t, userSession.Groups, 3)
- assert.Equal(t, "admin", userSession.Groups[0])
- assert.Equal(t, "users", userSession.Groups[1])
- assert.Equal(t, "grafana", userSession.Groups[2])
-}
-
-func TestShouldCheckValidSessionUsernameHeaderAndReturn200(t *testing.T) {
- mock := mocks.NewMockAutheliaCtx(t)
- defer mock.Close()
-
- mock.Clock.Set(time.Now())
-
- expectedStatusCode := 200
-
- userSession := mock.Ctx.GetSession()
- userSession.Username = testUsername
- userSession.AuthenticationLevel = authentication.OneFactor
- userSession.RefreshTTL = mock.Clock.Now().Add(5 * time.Minute)
-
- err := mock.Ctx.SaveSession(userSession)
- require.NoError(t, err)
-
- mock.Ctx.Request.Header.Set("X-Original-URL", "https://one-factor.example.com")
- mock.Ctx.Request.Header.SetBytesK(headerSessionUsername, testUsername)
- VerifyGET(verifyGetCfg)(mock.Ctx)
-
- assert.Equal(t, expectedStatusCode, mock.Ctx.Response.StatusCode())
- assert.Equal(t, "", string(mock.Ctx.Response.Body()))
-}
-
-func TestShouldCheckInvalidSessionUsernameHeaderAndReturn401(t *testing.T) {
- mock := mocks.NewMockAutheliaCtx(t)
- defer mock.Close()
-
- mock.Clock.Set(time.Now())
-
- expectedStatusCode := 401
-
- userSession := mock.Ctx.GetSession()
- userSession.Username = testUsername
- userSession.AuthenticationLevel = authentication.OneFactor
- userSession.RefreshTTL = mock.Clock.Now().Add(5 * time.Minute)
-
- err := mock.Ctx.SaveSession(userSession)
- require.NoError(t, err)
-
- mock.Ctx.Request.Header.Set("X-Original-URL", "https://one-factor.example.com")
- mock.Ctx.Request.Header.SetBytesK(headerSessionUsername, "root")
- VerifyGET(verifyGetCfg)(mock.Ctx)
-
- assert.Equal(t, expectedStatusCode, mock.Ctx.Response.StatusCode())
- assert.Equal(t, "401 Unauthorized", string(mock.Ctx.Response.Body()))
-}
-
-func TestGetProfileRefreshSettings(t *testing.T) {
- cfg := verifyGetCfg
-
- refresh, interval := getProfileRefreshSettings(cfg)
-
- assert.Equal(t, true, refresh)
- assert.Equal(t, 5*time.Minute, interval)
-
- cfg.RefreshInterval = schema.ProfileRefreshDisabled
-
- refresh, interval = getProfileRefreshSettings(cfg)
-
- assert.Equal(t, false, refresh)
- assert.Equal(t, time.Duration(0), interval)
-
- cfg.RefreshInterval = schema.ProfileRefreshAlways
-
- refresh, interval = getProfileRefreshSettings(cfg)
-
- assert.Equal(t, true, refresh)
- assert.Equal(t, time.Duration(0), interval)
-}
-
-func TestShouldNotRedirectRequestsForBypassACLWhenInactiveForTooLong(t *testing.T) {
- mock := mocks.NewMockAutheliaCtx(t)
- defer mock.Close()
-
- clock := utils.TestingClock{}
- clock.Set(time.Now())
- past := clock.Now().Add(-1 * time.Hour)
-
- mock.Ctx.Configuration.Session.Cookies[0].Inactivity = testInactivity
- // Reload the session provider since the configuration is indirect.
- mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil)
- assert.Equal(t, time.Second*10, mock.Ctx.Configuration.Session.Cookies[0].Inactivity)
-
- userSession := mock.Ctx.GetSession()
- userSession.Username = testUsername
- userSession.AuthenticationLevel = authentication.TwoFactor
- userSession.LastActivity = past.Unix()
-
- err := mock.Ctx.SaveSession(userSession)
- require.NoError(t, err)
-
- // Should respond 200 OK.
- mock.Ctx.QueryArgs().Add(queryArgRD, "https://login.example.com")
- mock.Ctx.Request.Header.Set("X-Forwarded-Method", "GET")
- mock.Ctx.Request.Header.Set("Accept", "text/html; charset=utf-8")
- mock.Ctx.Request.Header.Set("X-Original-URL", "https://bypass.example.com")
- VerifyGET(verifyGetCfg)(mock.Ctx)
- assert.Equal(t, fasthttp.StatusOK, mock.Ctx.Response.StatusCode())
- assert.Nil(t, mock.Ctx.Response.Header.Peek("Location"))
-
- // Should respond 302 Found.
- mock.Ctx.QueryArgs().Add(queryArgRD, "https://login.example.com")
- mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com")
- mock.Ctx.Request.Header.Set("X-Forwarded-Method", "GET")
- mock.Ctx.Request.Header.Set("Accept", "text/html; charset=utf-8")
- VerifyGET(verifyGetCfg)(mock.Ctx)
- assert.Equal(t, fasthttp.StatusFound, mock.Ctx.Response.StatusCode())
- assert.Equal(t, "https://login.example.com/?rd=https%3A%2F%2Ftwo-factor.example.com&rm=GET", string(mock.Ctx.Response.Header.Peek("Location")))
-
- // Should respond 401 Unauthorized.
- mock.Ctx.QueryArgs().Del(queryArgRD)
- mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com")
- mock.Ctx.Request.Header.Set("X-Forwarded-Method", "GET")
- mock.Ctx.Request.Header.Set("Accept", "text/html; charset=utf-8")
- VerifyGET(verifyGetCfg)(mock.Ctx)
- assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode())
- assert.Nil(t, mock.Ctx.Response.Header.Peek("Location"))
-}
-
-func TestIsSessionInactiveTooLong(t *testing.T) {
- testCases := []struct {
- name string
- have *session.UserSession
- now time.Time
- inactivity time.Duration
- expected bool
- }{
- {
- name: "ShouldNotBeInactiveTooLong",
- have: &session.UserSession{Username: "john", LastActivity: 1656994960},
- now: time.Unix(1656994970, 0),
- inactivity: time.Second * 90,
- expected: false,
- },
- {
- name: "ShouldNotBeInactiveTooLongIfAnonymous",
- have: &session.UserSession{Username: "", LastActivity: 1656994960},
- now: time.Unix(1656994990, 0),
- inactivity: time.Second * 20,
- expected: false,
- },
- {
- name: "ShouldNotBeInactiveTooLongIfRemembered",
- have: &session.UserSession{Username: "john", LastActivity: 1656994960, KeepMeLoggedIn: true},
- now: time.Unix(1656994990, 0),
- inactivity: time.Second * 20,
- expected: false,
- },
- {
- name: "ShouldNotBeInactiveTooLongIfDisabled",
- have: &session.UserSession{Username: "john", LastActivity: 1656994960},
- now: time.Unix(1656994990, 0),
- inactivity: time.Second * 0,
- expected: false,
- },
- {
- name: "ShouldBeInactiveTooLong",
- have: &session.UserSession{Username: "john", LastActivity: 1656994960},
- now: time.Unix(4656994990, 0),
- inactivity: time.Second * 1,
- expected: true,
- },
- }
-
- for _, tc := range testCases {
- t.Run(tc.name, func(t *testing.T) {
- ctx := mocks.NewMockAutheliaCtx(t)
-
- defer ctx.Close()
-
- ctx.Ctx.Configuration.Session.Cookies[0].Inactivity = tc.inactivity
- ctx.Ctx.Providers.SessionProvider = session.NewProvider(ctx.Ctx.Configuration.Session, nil)
-
- ctx.Clock.Set(tc.now)
- ctx.Ctx.Clock = &ctx.Clock
-
- actual := isSessionInactiveTooLong(ctx.Ctx, tc.have, tc.have.Username == "")
-
- assert.Equal(t, tc.expected, actual)
- })
- }
-}
diff --git a/internal/handlers/response.go b/internal/handlers/response.go
index 89e964630..b3de77c3c 100644
--- a/internal/handlers/response.go
+++ b/internal/handlers/response.go
@@ -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)
}
}
diff --git a/internal/handlers/types.go b/internal/handlers/types.go
index 517d297a6..6079f1ac0 100644
--- a/internal/handlers/types.go
+++ b/internal/handlers/types.go
@@ -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"`
diff --git a/internal/handlers/util.go b/internal/handlers/util.go
index 431bbc0ce..f29111dce 100644
--- a/internal/handlers/util.go
+++ b/internal/handlers/util.go
@@ -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
diff --git a/internal/handlers/webauthn.go b/internal/handlers/webauthn.go
index 81624e92e..d9703e2df 100644
--- a/internal/handlers/webauthn.go
+++ b/internal/handlers/webauthn.go
@@ -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
}
diff --git a/internal/handlers/webauthn_test.go b/internal/handlers/webauthn_test.go
index 7695adc3b..44e5b237d 100644
--- a/internal/handlers/webauthn_test.go
+++ b/internal/handlers/webauthn_test.go
@@ -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) {
diff --git a/internal/metrics/metrics.go b/internal/metrics/metrics.go
index 82b490edf..4a4a03da5 100644
--- a/internal/metrics/metrics.go
+++ b/internal/metrics/metrics.go
@@ -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)
}
diff --git a/internal/metrics/prometheus.go b/internal/metrics/prometheus.go
index 61ae96951..8b5a375b5 100644
--- a/internal/metrics/prometheus.go
+++ b/internal/metrics/prometheus.go
@@ -19,12 +19,12 @@ func NewPrometheus() (provider *Prometheus) {
// Prometheus is a middleware for recording prometheus metrics.
type Prometheus struct {
- authDuration *prometheus.HistogramVec
- reqDuration *prometheus.HistogramVec
- reqCounter *prometheus.CounterVec
- reqVerifyCounter *prometheus.CounterVec
- auth1FACounter *prometheus.CounterVec
- auth2FACounter *prometheus.CounterVec
+ authnDuration *prometheus.HistogramVec
+ reqDuration *prometheus.HistogramVec
+ reqCounter *prometheus.CounterVec
+ authzCounter *prometheus.CounterVec
+ authnCounter *prometheus.CounterVec
+ authn2FACounter *prometheus.CounterVec
}
// RecordRequest takes the statusCode string, requestMethod string, and the elapsed time.Duration to record the request and request duration metrics.
@@ -33,31 +33,31 @@ func (r *Prometheus) RecordRequest(statusCode, requestMethod string, elapsed tim
r.reqDuration.WithLabelValues(statusCode).Observe(elapsed.Seconds())
}
-// RecordVerifyRequest takes the statusCode string to record the verify endpoint request metrics.
-func (r *Prometheus) RecordVerifyRequest(statusCode string) {
- r.reqVerifyCounter.WithLabelValues(statusCode).Inc()
+// RecordAuthz takes the statusCode string to record the verify endpoint request metrics.
+func (r *Prometheus) RecordAuthz(statusCode string) {
+ r.authzCounter.WithLabelValues(statusCode).Inc()
}
-// RecordAuthentication takes the success and regulated booleans and a method string to record the authentication metrics.
-func (r *Prometheus) RecordAuthentication(success, banned bool, authType string) {
+// RecordAuthn takes the success and regulated booleans and a method string to record the authentication metrics.
+func (r *Prometheus) RecordAuthn(success, banned bool, authType string) {
switch authType {
case "1fa", "":
- r.auth1FACounter.WithLabelValues(strconv.FormatBool(success), strconv.FormatBool(banned)).Inc()
+ r.authnCounter.WithLabelValues(strconv.FormatBool(success), strconv.FormatBool(banned)).Inc()
default:
- r.auth2FACounter.WithLabelValues(strconv.FormatBool(success), strconv.FormatBool(banned), authType).Inc()
+ r.authn2FACounter.WithLabelValues(strconv.FormatBool(success), strconv.FormatBool(banned), authType).Inc()
}
}
// RecordAuthenticationDuration takes the statusCode string, requestMethod string, and the elapsed time.Duration to record the request and request duration metrics.
func (r *Prometheus) RecordAuthenticationDuration(success bool, elapsed time.Duration) {
- r.authDuration.WithLabelValues(strconv.FormatBool(success)).Observe(elapsed.Seconds())
+ r.authnDuration.WithLabelValues(strconv.FormatBool(success)).Observe(elapsed.Seconds())
}
func (r *Prometheus) register() {
- r.authDuration = promauto.NewHistogramVec(
+ r.authnDuration = promauto.NewHistogramVec(
prometheus.HistogramOpts{
Subsystem: "authelia",
- Name: "authentication_duration",
+ Name: "authn_duration",
Help: "The time an authentication attempt takes in seconds.",
Buckets: []float64{.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},
},
@@ -83,28 +83,28 @@ func (r *Prometheus) register() {
[]string{"code", "method"},
)
- r.reqVerifyCounter = promauto.NewCounterVec(
+ r.authzCounter = promauto.NewCounterVec(
prometheus.CounterOpts{
Subsystem: "authelia",
- Name: "verify_request",
- Help: "The number of verify requests processed.",
+ Name: "authz",
+ Help: "The number of authz requests processed.",
},
[]string{"code"},
)
- r.auth1FACounter = promauto.NewCounterVec(
+ r.authnCounter = promauto.NewCounterVec(
prometheus.CounterOpts{
Subsystem: "authelia",
- Name: "authentication_first_factor",
+ Name: "authn",
Help: "The number of 1FA authentications processed.",
},
[]string{"success", "banned"},
)
- r.auth2FACounter = promauto.NewCounterVec(
+ r.authn2FACounter = promauto.NewCounterVec(
prometheus.CounterOpts{
Subsystem: "authelia",
- Name: "authentication_second_factor",
+ Name: "authn_second_factor",
Help: "The number of 2FA authentications processed.",
},
[]string{"success", "banned", "type"},
diff --git a/internal/middlewares/authelia_context.go b/internal/middlewares/authelia_context.go
index 036517964..8d1f06174 100644
--- a/internal/middlewares/authelia_context.go
+++ b/internal/middlewares/authelia_context.go
@@ -2,9 +2,11 @@ package middlewares
import (
"encoding/json"
+ "errors"
"fmt"
"net"
"net/url"
+ "path"
"strings"
"github.com/asaskevich/govalidator"
@@ -128,12 +130,17 @@ func (ctx *AutheliaCtx) ReplyBadRequest() {
ctx.ReplyStatusCode(fasthttp.StatusBadRequest)
}
-// XForwardedProto return the content of the X-Forwarded-Proto header.
+// XForwardedMethod returns the content of the X-Forwarded-Method header.
+func (ctx *AutheliaCtx) XForwardedMethod() (method []byte) {
+ return ctx.Request.Header.PeekBytes(headerXForwardedMethod)
+}
+
+// XForwardedProto returns the content of the X-Forwarded-Proto header.
func (ctx *AutheliaCtx) XForwardedProto() (proto []byte) {
- proto = ctx.RequestCtx.Request.Header.PeekBytes(headerXForwardedProto)
+ proto = ctx.Request.Header.PeekBytes(headerXForwardedProto)
if proto == nil {
- if ctx.RequestCtx.IsTLS() {
+ if ctx.IsTLS() {
return protoHTTPS
}
@@ -143,14 +150,14 @@ func (ctx *AutheliaCtx) XForwardedProto() (proto []byte) {
return proto
}
-// XForwardedMethod return the content of the X-Forwarded-Method header.
-func (ctx *AutheliaCtx) XForwardedMethod() []byte {
- return ctx.RequestCtx.Request.Header.PeekBytes(headerXForwardedMethod)
+// XForwardedHost returns the content of the X-Forwarded-Host header.
+func (ctx *AutheliaCtx) XForwardedHost() (host []byte) {
+ return ctx.Request.Header.PeekBytes(headerXForwardedHost)
}
-// XForwardedHost return the content of the X-Forwarded-Host header.
-func (ctx *AutheliaCtx) XForwardedHost() (host []byte) {
- host = ctx.RequestCtx.Request.Header.PeekBytes(headerXForwardedHost)
+// GetXForwardedHost returns the content of the X-Forwarded-Host header falling back to the Host header.
+func (ctx *AutheliaCtx) GetXForwardedHost() (host []byte) {
+ host = ctx.XForwardedHost()
if host == nil {
return ctx.RequestCtx.Host()
@@ -159,41 +166,60 @@ func (ctx *AutheliaCtx) XForwardedHost() (host []byte) {
return host
}
-// XForwardedURI return the content of the X-Forwarded-URI header.
-func (ctx *AutheliaCtx) XForwardedURI() (uri []byte) {
- uri = ctx.RequestCtx.Request.Header.PeekBytes(headerXForwardedURI)
+// XForwardedURI returns the content of the X-Forwarded-Uri header.
+func (ctx *AutheliaCtx) XForwardedURI() (host []byte) {
+ return ctx.Request.Header.PeekBytes(headerXForwardedURI)
+}
+
+// GetXForwardedURI returns the content of the X-Forwarded-URI header, falling back to the start-line request path.
+func (ctx *AutheliaCtx) GetXForwardedURI() (uri []byte) {
+ uri = ctx.XForwardedURI()
if len(uri) == 0 {
- return ctx.RequestCtx.RequestURI()
+ return ctx.RequestURI()
}
return uri
}
-// XOriginalURL returns the content of the X-Original-URL header.
-func (ctx *AutheliaCtx) XOriginalURL() []byte {
- return ctx.RequestCtx.Request.Header.PeekBytes(headerXOriginalURL)
+// XOriginalMethod returns the content of the X-Original-Method header.
+func (ctx *AutheliaCtx) XOriginalMethod() []byte {
+ return ctx.Request.Header.PeekBytes(headerXOriginalMethod)
}
-// XOriginalMethod return the content of the X-Original-Method header.
-func (ctx *AutheliaCtx) XOriginalMethod() []byte {
- return ctx.RequestCtx.Request.Header.PeekBytes(headerXOriginalMethod)
+// XOriginalURL returns the content of the X-Original-URL header.
+func (ctx *AutheliaCtx) XOriginalURL() []byte {
+ return ctx.Request.Header.PeekBytes(headerXOriginalURL)
}
-// XAutheliaURL return the content of the X-Authelia-URL header which is used to communicate the location of the
+// XAutheliaURL returns the content of the X-Authelia-URL header which is used to communicate the location of the
// portal when using proxies like Envoy.
func (ctx *AutheliaCtx) XAutheliaURL() []byte {
- return ctx.RequestCtx.Request.Header.PeekBytes(headerXAutheliaURL)
+ return ctx.Request.Header.PeekBytes(headerXAutheliaURL)
}
-// QueryArgRedirect return the content of the rd query argument.
+// QueryArgRedirect returns the content of the 'rd' query argument.
func (ctx *AutheliaCtx) QueryArgRedirect() []byte {
- return ctx.RequestCtx.QueryArgs().PeekBytes(qryArgRedirect)
+ return ctx.QueryArgs().PeekBytes(qryArgRedirect)
+}
+
+// QueryArgAutheliaURL returns the content of the 'authelia_url' query argument.
+func (ctx *AutheliaCtx) QueryArgAutheliaURL() []byte {
+ return ctx.QueryArgs().PeekBytes(qryArgAutheliaURL)
+}
+
+// AuthzPath returns the 'authz_path' value.
+func (ctx *AutheliaCtx) AuthzPath() (uri []byte) {
+ if uv := ctx.UserValueBytes(keyUserValueAuthzPath); uv != nil {
+ return []byte(uv.(string))
+ }
+
+ return nil
}
// BasePath returns the base_url as per the path visited by the client.
func (ctx *AutheliaCtx) BasePath() string {
- if baseURL := ctx.UserValueBytes(UserValueKeyBaseURL); baseURL != nil {
+ if baseURL := ctx.UserValueBytes(keyUserValueBaseURL); baseURL != nil {
return baseURL.(string)
}
@@ -202,7 +228,7 @@ func (ctx *AutheliaCtx) BasePath() string {
// BasePathSlash is the same as BasePath but returns a final slash as well.
func (ctx *AutheliaCtx) BasePathSlash() string {
- if baseURL := ctx.UserValueBytes(UserValueKeyBaseURL); baseURL != nil {
+ if baseURL := ctx.UserValueBytes(keyUserValueBaseURL); baseURL != nil {
return baseURL.(string) + strSlash
}
@@ -213,7 +239,7 @@ func (ctx *AutheliaCtx) BasePathSlash() string {
func (ctx *AutheliaCtx) RootURL() (issuerURL *url.URL) {
return &url.URL{
Scheme: string(ctx.XForwardedProto()),
- Host: string(ctx.XForwardedHost()),
+ Host: string(ctx.GetXForwardedHost()),
Path: ctx.BasePath(),
}
}
@@ -222,13 +248,17 @@ func (ctx *AutheliaCtx) RootURL() (issuerURL *url.URL) {
func (ctx *AutheliaCtx) RootURLSlash() (issuerURL *url.URL) {
return &url.URL{
Scheme: string(ctx.XForwardedProto()),
- Host: string(ctx.XForwardedHost()),
+ Host: string(ctx.GetXForwardedHost()),
Path: ctx.BasePathSlash(),
}
}
// GetTargetURICookieDomain returns the session provider for the targetURI domain.
func (ctx *AutheliaCtx) GetTargetURICookieDomain(targetURI *url.URL) string {
+ if targetURI == nil {
+ return ""
+ }
+
hostname := targetURI.Hostname()
for _, domain := range ctx.Configuration.Session.Cookies {
@@ -253,46 +283,82 @@ func (ctx *AutheliaCtx) IsSafeRedirectionTargetURI(targetURI *url.URL) bool {
func (ctx *AutheliaCtx) GetCookieDomain() (domain string, err error) {
var targetURI *url.URL
- if targetURI, err = ctx.GetOriginalURL(); err != nil {
+ if targetURI, err = ctx.GetXOriginalURLOrXForwardedURL(); err != nil {
return "", fmt.Errorf("unable to retrieve cookie domain: %s", err)
}
return ctx.GetTargetURICookieDomain(targetURI), nil
}
+// GetSessionProviderByTargetURL returns the session provider for the Request's domain.
+func (ctx *AutheliaCtx) GetSessionProviderByTargetURL(targetURL *url.URL) (provider *session.Session, err error) {
+ domain := ctx.GetTargetURICookieDomain(targetURL)
+
+ if domain == "" {
+ return nil, fmt.Errorf("unable to retrieve domain session: %w", err)
+ }
+
+ return ctx.Providers.SessionProvider.Get(domain)
+}
+
// GetSessionProvider returns the session provider for the Request's domain.
func (ctx *AutheliaCtx) GetSessionProvider() (provider *session.Session, err error) {
- var cookieDomain string
+ if ctx.session == nil {
+ var domain string
+
+ if domain, err = ctx.GetCookieDomain(); err != nil {
+ return nil, err
+ }
- if cookieDomain, err = ctx.GetCookieDomain(); err != nil {
- return nil, err
+ if ctx.session, err = ctx.GetCookieDomainSessionProvider(domain); err != nil {
+ return nil, err
+ }
}
- if cookieDomain == "" {
- return nil, fmt.Errorf("unable to retrieve domain session: %s", err)
+ return ctx.session, nil
+}
+
+// GetCookieDomainSessionProvider returns the session provider for the provided domain.
+func (ctx *AutheliaCtx) GetCookieDomainSessionProvider(domain string) (provider *session.Session, err error) {
+ if domain == "" {
+ return nil, fmt.Errorf("unable to retrieve domain session: %w", err)
}
- return ctx.Providers.SessionProvider.Get(cookieDomain)
+ return ctx.Providers.SessionProvider.Get(domain)
}
-// GetSession return the user session. Any update will be saved in cache.
-func (ctx *AutheliaCtx) GetSession() session.UserSession {
- provider, err := ctx.GetSessionProvider()
- if err != nil {
- ctx.Logger.Error("Unable to retrieve domain session")
- return session.NewDefaultUserSession()
+// GetSession returns the user session provided the cookie provider could be discovered. It is recommended to get the
+// provider itself if you also need to update or destroy sessions.
+func (ctx *AutheliaCtx) GetSession() (userSession session.UserSession, err error) {
+ var provider *session.Session
+
+ if provider, err = ctx.GetSessionProvider(); err != nil {
+ return userSession, err
}
- userSession, err := provider.GetSession(ctx.RequestCtx)
- if err != nil {
+ if userSession, err = provider.GetSession(ctx.RequestCtx); err != nil {
ctx.Logger.Error("Unable to retrieve user session")
- return session.NewDefaultUserSession()
+ return provider.NewDefaultUserSession(), nil
}
- return userSession
+ 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")
+ }
+ }
+
+ return userSession, nil
}
-// SaveSession save the content of the session.
+// SaveSession saves the content of the session.
func (ctx *AutheliaCtx) SaveSession(userSession session.UserSession) error {
provider, err := ctx.GetSessionProvider()
if err != nil {
@@ -302,7 +368,7 @@ func (ctx *AutheliaCtx) SaveSession(userSession session.UserSession) error {
return provider.SaveSession(ctx.RequestCtx, userSession)
}
-// RegenerateSession regenerates user session.
+// RegenerateSession regenerates a user session.
func (ctx *AutheliaCtx) RegenerateSession() error {
provider, err := ctx.GetSessionProvider()
if err != nil {
@@ -312,7 +378,7 @@ func (ctx *AutheliaCtx) RegenerateSession() error {
return provider.RegenerateSession(ctx.RequestCtx)
}
-// DestroySession destroy user session.
+// DestroySession destroys a user session.
func (ctx *AutheliaCtx) DestroySession() error {
provider, err := ctx.GetSessionProvider()
if err != nil {
@@ -349,6 +415,36 @@ func (ctx *AutheliaCtx) ParseBody(value any) error {
return nil
}
+// SetContentTypeApplicationJSON sets the Content-Type header to 'application/json; charset=utf-8'.
+func (ctx *AutheliaCtx) SetContentTypeApplicationJSON() {
+ ctx.SetContentTypeBytes(contentTypeApplicationJSON)
+}
+
+// SetContentTypeTextPlain efficiently sets the Content-Type header to 'text/plain; charset=utf-8'.
+func (ctx *AutheliaCtx) SetContentTypeTextPlain() {
+ ctx.SetContentTypeBytes(contentTypeTextPlain)
+}
+
+// SetContentTypeTextHTML efficiently sets the Content-Type header to 'text/html; charset=utf-8'.
+func (ctx *AutheliaCtx) SetContentTypeTextHTML() {
+ ctx.SetContentTypeBytes(contentTypeTextHTML)
+}
+
+// SetContentTypeApplicationYAML efficiently sets the Content-Type header to 'application/yaml; charset=utf-8'.
+func (ctx *AutheliaCtx) SetContentTypeApplicationYAML() {
+ ctx.SetContentTypeBytes(contentTypeApplicationYAML)
+}
+
+// SetContentSecurityPolicy sets the Content-Security-Policy header.
+func (ctx *AutheliaCtx) SetContentSecurityPolicy(value string) {
+ ctx.Response.Header.SetBytesK(headerContentSecurityPolicy, value)
+}
+
+// SetContentSecurityPolicyBytes sets the Content-Security-Policy header.
+func (ctx *AutheliaCtx) SetContentSecurityPolicyBytes(value []byte) {
+ ctx.Response.Header.SetBytesKV(headerContentSecurityPolicy, value)
+}
+
// SetJSONBody Set json body.
func (ctx *AutheliaCtx) SetJSONBody(value any) error {
return ctx.ReplyJSON(OKResponse{Status: "OK", Data: value}, 0)
@@ -368,52 +464,88 @@ func (ctx *AutheliaCtx) RemoteIP() net.IP {
return ctx.RequestCtx.RemoteIP()
}
-// GetOriginalURL extract the URL from the request headers (X-Original-URL or X-Forwarded-* headers).
-func (ctx *AutheliaCtx) GetOriginalURL() (*url.URL, error) {
- originalURL := ctx.XOriginalURL()
- if originalURL != nil {
- parsedURL, err := url.ParseRequestURI(string(originalURL))
- if err != nil {
- return nil, fmt.Errorf("Unable to parse URL extracted from X-Original-URL header: %v", err)
- }
+// GetXForwardedURL returns the parsed X-Forwarded-Proto, X-Forwarded-Host, and X-Forwarded-URI request header as a
+// *url.URL.
+func (ctx *AutheliaCtx) GetXForwardedURL() (requestURI *url.URL, err error) {
+ forwardedProto, forwardedHost, forwardedURI := ctx.XForwardedProto(), ctx.GetXForwardedHost(), ctx.GetXForwardedURI()
- ctx.Logger.Trace("Using X-Original-URL header content as targeted site URL")
+ if forwardedProto == nil {
+ return nil, ErrMissingXForwardedProto
+ }
- return parsedURL, nil
+ if forwardedHost == nil {
+ return nil, ErrMissingXForwardedHost
}
- forwardedProto, forwardedHost, forwardedURI := ctx.XForwardedProto(), ctx.XForwardedHost(), ctx.XForwardedURI()
+ value := utils.BytesJoin(forwardedProto, protoHostSeparator, forwardedHost, forwardedURI)
- if forwardedProto == nil {
- return nil, errMissingXForwardedProto
+ if requestURI, err = url.ParseRequestURI(string(value)); err != nil {
+ return nil, fmt.Errorf("failed to parse X-Forwarded Headers: %w", err)
}
- if forwardedHost == nil {
- return nil, errMissingXForwardedHost
+ return requestURI, nil
+}
+
+// GetXOriginalURL returns the parsed X-OriginalURL request header as a *url.URL.
+func (ctx *AutheliaCtx) GetXOriginalURL() (requestURI *url.URL, err error) {
+ value := ctx.XOriginalURL()
+
+ if value == nil {
+ return nil, ErrMissingXOriginalURL
}
- var requestURI string
+ if requestURI, err = url.ParseRequestURI(string(value)); err != nil {
+ return nil, fmt.Errorf("failed to parse X-Original-URL header: %w", err)
+ }
- forwardedProto = append(forwardedProto, protoHostSeparator...)
- requestURI = string(append(forwardedProto,
- append(forwardedHost, forwardedURI...)...))
+ return requestURI, nil
+}
- parsedURL, err := url.ParseRequestURI(requestURI)
- if err != nil {
- return nil, fmt.Errorf("Unable to parse URL %s: %v", requestURI, err)
+// GetXOriginalURLOrXForwardedURL returns the parsed X-Original-URL request header if it's available or the parsed
+// X-Forwarded request headers if not.
+func (ctx *AutheliaCtx) GetXOriginalURLOrXForwardedURL() (requestURI *url.URL, err error) {
+ requestURI, err = ctx.GetXOriginalURL()
+
+ switch {
+ case err == nil:
+ return requestURI, nil
+ case errors.Is(err, ErrMissingXOriginalURL):
+ return ctx.GetXForwardedURL()
+ default:
+ return requestURI, err
}
+}
- ctx.Logger.Tracef("Using X-Fowarded-Proto, X-Forwarded-Host and X-Forwarded-URI headers " +
- "to construct targeted site URL")
+// IssuerURL returns the expected Issuer.
+func (ctx *AutheliaCtx) IssuerURL() (issuerURL *url.URL, err error) {
+ issuerURL = &url.URL{
+ Scheme: strProtoHTTPS,
+ }
- return parsedURL, nil
+ if scheme := ctx.XForwardedProto(); scheme != nil {
+ issuerURL.Scheme = string(scheme)
+ }
+
+ if host := ctx.GetXForwardedHost(); len(host) != 0 {
+ issuerURL.Host = string(host)
+ } else {
+ return nil, ErrMissingXForwardedHost
+ }
+
+ if base := ctx.BasePath(); base != "" {
+ issuerURL.Path = path.Join(issuerURL.Path, base)
+ }
+
+ return issuerURL, nil
}
// IsXHR returns true if the request is a XMLHttpRequest.
func (ctx *AutheliaCtx) IsXHR() (xhr bool) {
- requestedWith := ctx.Request.Header.PeekBytes(headerXRequestedWith)
+ if requestedWith := ctx.Request.Header.PeekBytes(headerXRequestedWith); requestedWith != nil && strings.EqualFold(string(requestedWith), headerValueXRequestedWithXHR) {
+ return true
+ }
- return requestedWith != nil && strings.EqualFold(string(requestedWith), headerValueXRequestedWithXHR)
+ return false
}
// AcceptsMIME takes a mime type and returns true if the request accepts that type or the wildcard type.
@@ -452,31 +584,11 @@ func (ctx *AutheliaCtx) SpecialRedirect(uri string, statusCode int) {
fasthttp.ReleaseURI(u)
}
-// RecordAuthentication records authentication metrics.
-func (ctx *AutheliaCtx) RecordAuthentication(success, regulated bool, method string) {
+// RecordAuthn records authentication metrics.
+func (ctx *AutheliaCtx) RecordAuthn(success, regulated bool, method string) {
if ctx.Providers.Metrics == nil {
return
}
- ctx.Providers.Metrics.RecordAuthentication(success, regulated, method)
-}
-
-// SetContentTypeTextPlain efficiently sets the Content-Type header to 'text/plain; charset=utf-8'.
-func (ctx *AutheliaCtx) SetContentTypeTextPlain() {
- ctx.SetContentTypeBytes(contentTypeTextPlain)
-}
-
-// SetContentTypeTextHTML efficiently sets the Content-Type header to 'text/html; charset=utf-8'.
-func (ctx *AutheliaCtx) SetContentTypeTextHTML() {
- ctx.SetContentTypeBytes(contentTypeTextHTML)
-}
-
-// SetContentTypeApplicationJSON efficiently sets the Content-Type header to 'application/json; charset=utf-8'.
-func (ctx *AutheliaCtx) SetContentTypeApplicationJSON() {
- ctx.SetContentTypeBytes(contentTypeApplicationJSON)
-}
-
-// SetContentTypeApplicationYAML efficiently sets the Content-Type header to 'application/yaml; charset=utf-8'.
-func (ctx *AutheliaCtx) SetContentTypeApplicationYAML() {
- ctx.SetContentTypeBytes(contentTypeApplicationYAML)
+ ctx.Providers.Metrics.RecordAuthn(success, regulated, method)
}
diff --git a/internal/middlewares/authelia_context_test.go b/internal/middlewares/authelia_context_test.go
index abf63b4dd..ef4a761f1 100644
--- a/internal/middlewares/authelia_context_test.go
+++ b/internal/middlewares/authelia_context_test.go
@@ -150,7 +150,7 @@ func TestShouldGetOriginalURLFromOriginalURLHeader(t *testing.T) {
defer mock.Close()
mock.Ctx.Request.Header.Set("X-Original-URL", "https://home.example.com")
- originalURL, err := mock.Ctx.GetOriginalURL()
+ originalURL, err := mock.Ctx.GetXOriginalURLOrXForwardedURL()
assert.NoError(t, err)
expectedURL, err := url.ParseRequestURI("https://home.example.com")
@@ -163,7 +163,7 @@ func TestShouldGetOriginalURLFromForwardedHeadersWithoutURI(t *testing.T) {
defer mock.Close()
mock.Ctx.Request.Header.Set(fasthttp.HeaderXForwardedProto, "https")
mock.Ctx.Request.Header.Set(fasthttp.HeaderXForwardedHost, "home.example.com")
- originalURL, err := mock.Ctx.GetOriginalURL()
+ originalURL, err := mock.Ctx.GetXOriginalURLOrXForwardedURL()
assert.NoError(t, err)
expectedURL, err := url.ParseRequestURI("https://home.example.com/")
@@ -175,9 +175,9 @@ func TestShouldGetOriginalURLFromForwardedHeadersWithURI(t *testing.T) {
mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close()
mock.Ctx.Request.Header.Set("X-Original-URL", "htt-ps//home?-.example.com")
- _, err := mock.Ctx.GetOriginalURL()
+ _, err := mock.Ctx.GetXOriginalURLOrXForwardedURL()
assert.Error(t, err)
- assert.Equal(t, "Unable to parse URL extracted from X-Original-URL header: parse \"htt-ps//home?-.example.com\": invalid URI for request", err.Error())
+ assert.EqualError(t, err, "failed to parse X-Original-URL header: parse \"htt-ps//home?-.example.com\": invalid URI for request")
}
func TestShouldFallbackToNonXForwardedHeaders(t *testing.T) {
@@ -190,8 +190,8 @@ func TestShouldFallbackToNonXForwardedHeaders(t *testing.T) {
mock.Ctx.RequestCtx.Request.SetHost("auth.example.com:1234")
assert.Equal(t, []byte("http"), mock.Ctx.XForwardedProto())
- assert.Equal(t, []byte("auth.example.com:1234"), mock.Ctx.XForwardedHost())
- assert.Equal(t, []byte("/2fa/one-time-password"), mock.Ctx.XForwardedURI())
+ assert.Equal(t, []byte("auth.example.com:1234"), mock.Ctx.GetXForwardedHost())
+ assert.Equal(t, []byte("/2fa/one-time-password"), mock.Ctx.GetXForwardedURI())
}
func TestShouldOnlyFallbackToNonXForwardedHeadersWhenNil(t *testing.T) {
@@ -208,8 +208,8 @@ func TestShouldOnlyFallbackToNonXForwardedHeadersWhenNil(t *testing.T) {
mock.Ctx.RequestCtx.Request.Header.Set("X-Forwarded-Method", "GET")
assert.Equal(t, []byte("https"), mock.Ctx.XForwardedProto())
- assert.Equal(t, []byte("auth.example.com:1234"), mock.Ctx.XForwardedHost())
- assert.Equal(t, []byte("/base/2fa/one-time-password"), mock.Ctx.XForwardedURI())
+ assert.Equal(t, []byte("auth.example.com:1234"), mock.Ctx.GetXForwardedHost())
+ assert.Equal(t, []byte("/base/2fa/one-time-password"), mock.Ctx.GetXForwardedURI())
assert.Equal(t, []byte("GET"), mock.Ctx.XForwardedMethod())
}
diff --git a/internal/middlewares/const.go b/internal/middlewares/const.go
index dc5519f2e..5332b6317 100644
--- a/internal/middlewares/const.go
+++ b/internal/middlewares/const.go
@@ -71,18 +71,20 @@ const (
strProtoHTTP = "http"
strSlash = "/"
- queryArgRedirect = "rd"
- queryArgToken = "token"
+ queryArgRedirect = "rd"
+ queryArgAutheliaURL = "authelia_url"
+ queryArgToken = "token"
)
var (
protoHTTPS = []byte(strProtoHTTPS)
protoHTTP = []byte(strProtoHTTP)
- qryArgRedirect = []byte(queryArgRedirect)
+ qryArgRedirect = []byte(queryArgRedirect)
+ qryArgAutheliaURL = []byte(queryArgAutheliaURL)
- // UserValueKeyBaseURL is the User Value key where we store the Base URL.
- UserValueKeyBaseURL = []byte("base_url")
+ keyUserValueBaseURL = []byte("base_url")
+ keyUserValueAuthzPath = []byte("authz_path")
// UserValueKeyFormPost is the User Value key where we indicate the form_post response mode.
UserValueKeyFormPost = []byte("form_post")
diff --git a/internal/middlewares/errors.go b/internal/middlewares/errors.go
index d1d892b80..5a9e4a64b 100644
--- a/internal/middlewares/errors.go
+++ b/internal/middlewares/errors.go
@@ -2,5 +2,16 @@ package middlewares
import "errors"
-var errMissingXForwardedHost = errors.New("Missing header X-Forwarded-Host")
-var errMissingXForwardedProto = errors.New("Missing header X-Forwarded-Proto")
+var (
+ // ErrMissingXForwardedProto is returned on methods which require an X-Forwarded-Proto header.
+ ErrMissingXForwardedProto = errors.New("missing required X-Forwarded-Proto header")
+
+ // ErrMissingXForwardedHost is returned on methods which require an X-Forwarded-Host header.
+ ErrMissingXForwardedHost = errors.New("missing required X-Forwarded-Host header")
+
+ // ErrMissingHeaderHost is returned on methods which require an Host header.
+ ErrMissingHeaderHost = errors.New("missing required Host header")
+
+ // ErrMissingXOriginalURL is returned on methods which require an X-Original-URL header.
+ ErrMissingXOriginalURL = errors.New("missing required X-Original-URL header")
+)
diff --git a/internal/middlewares/metrics.go b/internal/middlewares/metrics.go
index f908cc4ec..7f58cac8b 100644
--- a/internal/middlewares/metrics.go
+++ b/internal/middlewares/metrics.go
@@ -29,8 +29,8 @@ func NewMetricsRequest(metrics metrics.Recorder) (middleware Basic) {
}
}
-// NewMetricsVerifyRequest returns a middleware if provided with a metrics.Recorder, otherwise it returns nil.
-func NewMetricsVerifyRequest(metrics metrics.Recorder) (middleware Basic) {
+// NewMetricsAuthzRequest returns a middleware if provided with a metrics.Recorder, otherwise it returns nil.
+func NewMetricsAuthzRequest(metrics metrics.Recorder) (middleware Basic) {
if metrics == nil {
return nil
}
@@ -41,7 +41,7 @@ func NewMetricsVerifyRequest(metrics metrics.Recorder) (middleware Basic) {
statusCode := strconv.Itoa(ctx.Response.StatusCode())
- metrics.RecordVerifyRequest(statusCode)
+ metrics.RecordAuthz(statusCode)
}
}
}
diff --git a/internal/middlewares/require_first_factor.go b/internal/middlewares/require_first_factor.go
index cd104583d..381eaaefd 100644
--- a/internal/middlewares/require_first_factor.go
+++ b/internal/middlewares/require_first_factor.go
@@ -7,7 +7,7 @@ import (
// Require1FA check if user has enough permissions to execute the next handler.
func Require1FA(next RequestHandler) RequestHandler {
return func(ctx *AutheliaCtx) {
- if ctx.GetSession().AuthenticationLevel < authentication.OneFactor {
+ if s, err := ctx.GetSession(); err != nil || s.AuthenticationLevel < authentication.OneFactor {
ctx.ReplyForbidden()
return
}
diff --git a/internal/middlewares/strip_path.go b/internal/middlewares/strip_path.go
index 7ebfc3549..8b53f80db 100644
--- a/internal/middlewares/strip_path.go
+++ b/internal/middlewares/strip_path.go
@@ -13,7 +13,7 @@ func StripPath(path string) (middleware Middleware) {
uri := ctx.RequestURI()
if strings.HasPrefix(string(uri), path) {
- ctx.SetUserValueBytes(UserValueKeyBaseURL, path)
+ ctx.SetUserValueBytes(keyUserValueBaseURL, path)
newURI := strings.TrimPrefix(string(uri), path)
ctx.Request.SetRequestURI(newURI)
diff --git a/internal/middlewares/types.go b/internal/middlewares/types.go
index 14893a606..0d04e738c 100644
--- a/internal/middlewares/types.go
+++ b/internal/middlewares/types.go
@@ -29,6 +29,8 @@ type AutheliaCtx struct {
Configuration schema.Configuration
Clock utils.Clock
+
+ session *session.Session
}
// Providers contain all provider provided to Authelia.
diff --git a/internal/mocks/authelia_ctx.go b/internal/mocks/authelia_ctx.go
index 17f742648..486beeac9 100644
--- a/internal/mocks/authelia_ctx.go
+++ b/internal/mocks/authelia_ctx.go
@@ -70,28 +70,116 @@ func NewMockAutheliaCtx(t *testing.T) *MockAutheliaCtx {
},
}
- config.AccessControl.DefaultPolicy = "deny"
- config.AccessControl.Rules = []schema.ACLRule{{
- Domains: []string{"bypass.example.com"},
- Policy: "bypass",
- }, {
- Domains: []string{"one-factor.example.com"},
- Policy: "one_factor",
- }, {
- Domains: []string{"two-factor.example.com"},
- Policy: "two_factor",
- }, {
- Domains: []string{"deny.example.com"},
- Policy: "deny",
- }, {
- Domains: []string{"admin.example.com"},
- Policy: "two_factor",
- Subjects: [][]string{{"group:admin"}},
- }, {
- Domains: []string{"grafana.example.com"},
- Policy: "two_factor",
- Subjects: [][]string{{"group:grafana"}},
- }}
+ config.AccessControl = schema.AccessControlConfiguration{
+ DefaultPolicy: "deny",
+ Rules: []schema.ACLRule{
+ {
+ Domains: []string{"bypass.example.com"},
+ Policy: "bypass",
+ },
+ {
+ Domains: []string{"bypass-get.example.com"},
+ Policy: "bypass",
+ Methods: []string{fasthttp.MethodGet},
+ },
+ {
+ Domains: []string{"bypass-head.example.com"},
+ Policy: "bypass",
+ Methods: []string{fasthttp.MethodHead},
+ },
+ {
+ Domains: []string{"bypass-options.example.com"},
+ Policy: "bypass",
+ Methods: []string{fasthttp.MethodOptions},
+ },
+ {
+ Domains: []string{"bypass-trace.example.com"},
+ Policy: "bypass",
+ Methods: []string{fasthttp.MethodTrace},
+ },
+ {
+ Domains: []string{"bypass-put.example.com"},
+ Policy: "bypass",
+ Methods: []string{fasthttp.MethodPut},
+ },
+ {
+ Domains: []string{"bypass-patch.example.com"},
+ Policy: "bypass",
+ Methods: []string{fasthttp.MethodPatch},
+ },
+ {
+ Domains: []string{"bypass-post.example.com"},
+ Policy: "bypass",
+ Methods: []string{fasthttp.MethodPost},
+ },
+ {
+ Domains: []string{"bypass-delete.example.com"},
+ Policy: "bypass",
+ Methods: []string{fasthttp.MethodDelete},
+ },
+ {
+ Domains: []string{"bypass-connect.example.com"},
+ Policy: "bypass",
+ Methods: []string{fasthttp.MethodConnect},
+ },
+ {
+ Domains: []string{
+ "bypass-get.example.com", "bypass-head.example.com", "bypass-options.example.com",
+ "bypass-trace.example.com", "bypass-put.example.com", "bypass-patch.example.com",
+ "bypass-post.example.com", "bypass-delete.example.com", "bypass-connect.example.com",
+ },
+ Policy: "one_factor",
+ },
+ {
+ Domains: []string{"one-factor.example.com"},
+ Policy: "one_factor",
+ },
+ {
+ Domains: []string{"two-factor.example.com"},
+ Policy: "two_factor",
+ },
+ {
+ Domains: []string{"deny.example.com"},
+ Policy: "deny",
+ },
+ {
+ Domains: []string{"admin.example.com"},
+ Policy: "two_factor",
+ Subjects: [][]string{{"group:admin"}},
+ },
+ {
+ Domains: []string{"grafana.example.com"},
+ Policy: "two_factor",
+ Subjects: [][]string{{"group:grafana"}},
+ },
+ {
+ Domains: []string{"bypass.example2.com"},
+ Policy: "bypass",
+ },
+ {
+ Domains: []string{"one-factor.example2.com"},
+ Policy: "one_factor",
+ },
+ {
+ Domains: []string{"two-factor.example2.com"},
+ Policy: "two_factor",
+ },
+ {
+ Domains: []string{"deny.example2.com"},
+ Policy: "deny",
+ },
+ {
+ Domains: []string{"admin.example2.com"},
+ Policy: "two_factor",
+ Subjects: [][]string{{"group:admin"}},
+ },
+ {
+ Domains: []string{"grafana.example2.com"},
+ Policy: "two_factor",
+ Subjects: [][]string{{"group:grafana"}},
+ },
+ },
+ }
providers := middlewares.Providers{}
diff --git a/internal/mocks/duo_api.go b/internal/mocks/duo_api.go
index d5a753305..d358f9c74 100644
--- a/internal/mocks/duo_api.go
+++ b/internal/mocks/duo_api.go
@@ -8,10 +8,10 @@ import (
url "net/url"
reflect "reflect"
- gomock "github.com/golang/mock/gomock"
-
duo "github.com/authelia/authelia/v4/internal/duo"
middlewares "github.com/authelia/authelia/v4/internal/middlewares"
+ session "github.com/authelia/authelia/v4/internal/session"
+ gomock "github.com/golang/mock/gomock"
)
// MockAPI is a mock of API interface.
@@ -38,46 +38,46 @@ func (m *MockAPI) EXPECT() *MockAPIMockRecorder {
}
// AuthCall mocks base method.
-func (m *MockAPI) AuthCall(arg0 *middlewares.AutheliaCtx, arg1 url.Values) (*duo.AuthResponse, error) {
+func (m *MockAPI) AuthCall(arg0 *middlewares.AutheliaCtx, arg1 *session.UserSession, arg2 url.Values) (*duo.AuthResponse, error) {
m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "AuthCall", arg0, arg1)
+ ret := m.ctrl.Call(m, "AuthCall", arg0, arg1, arg2)
ret0, _ := ret[0].(*duo.AuthResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// AuthCall indicates an expected call of AuthCall.
-func (mr *MockAPIMockRecorder) AuthCall(arg0, arg1 interface{}) *gomock.Call {
+func (mr *MockAPIMockRecorder) AuthCall(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthCall", reflect.TypeOf((*MockAPI)(nil).AuthCall), arg0, arg1)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthCall", reflect.TypeOf((*MockAPI)(nil).AuthCall), arg0, arg1, arg2)
}
// Call mocks base method.
-func (m *MockAPI) Call(arg0 *middlewares.AutheliaCtx, arg1 url.Values, arg2, arg3 string) (*duo.Response, error) {
+func (m *MockAPI) Call(arg0 *middlewares.AutheliaCtx, arg1 *session.UserSession, arg2 url.Values, arg3, arg4 string) (*duo.Response, error) {
m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "Call", arg0, arg1, arg2, arg3)
+ ret := m.ctrl.Call(m, "Call", arg0, arg1, arg2, arg3, arg4)
ret0, _ := ret[0].(*duo.Response)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Call indicates an expected call of Call.
-func (mr *MockAPIMockRecorder) Call(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+func (mr *MockAPIMockRecorder) Call(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Call", reflect.TypeOf((*MockAPI)(nil).Call), arg0, arg1, arg2, arg3)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Call", reflect.TypeOf((*MockAPI)(nil).Call), arg0, arg1, arg2, arg3, arg4)
}
// PreAuthCall mocks base method.
-func (m *MockAPI) PreAuthCall(arg0 *middlewares.AutheliaCtx, arg1 url.Values) (*duo.PreAuthResponse, error) {
+func (m *MockAPI) PreAuthCall(arg0 *middlewares.AutheliaCtx, arg1 *session.UserSession, arg2 url.Values) (*duo.PreAuthResponse, error) {
m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "PreAuthCall", arg0, arg1)
+ ret := m.ctrl.Call(m, "PreAuthCall", arg0, arg1, arg2)
ret0, _ := ret[0].(*duo.PreAuthResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// PreAuthCall indicates an expected call of PreAuthCall.
-func (mr *MockAPIMockRecorder) PreAuthCall(arg0, arg1 interface{}) *gomock.Call {
+func (mr *MockAPIMockRecorder) PreAuthCall(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PreAuthCall", reflect.TypeOf((*MockAPI)(nil).PreAuthCall), arg0, arg1)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PreAuthCall", reflect.TypeOf((*MockAPI)(nil).PreAuthCall), arg0, arg1, arg2)
}
diff --git a/internal/regulation/regulator.go b/internal/regulation/regulator.go
index e7e2ba37a..c57cabdcd 100644
--- a/internal/regulation/regulator.go
+++ b/internal/regulation/regulator.go
@@ -24,7 +24,7 @@ func NewRegulator(config schema.RegulationConfiguration, provider storage.Regula
// Mark an authentication attempt.
// We split Mark and Regulate in order to avoid timing attacks.
func (r *Regulator) Mark(ctx Context, successful, banned bool, username, requestURI, requestMethod, authType string) error {
- ctx.RecordAuthentication(successful, banned, strings.ToLower(authType))
+ ctx.RecordAuthn(successful, banned, strings.ToLower(authType))
return r.storageProvider.AppendAuthenticationLog(ctx, model.AuthenticationAttempt{
Time: r.clock.Now(),
diff --git a/internal/regulation/types.go b/internal/regulation/types.go
index 3e902a78d..d5ad21edd 100644
--- a/internal/regulation/types.go
+++ b/internal/regulation/types.go
@@ -31,5 +31,5 @@ type Context interface {
// MetricsRecorder represents the methods used to record regulation.
type MetricsRecorder interface {
- RecordAuthentication(success, banned bool, authType string)
+ RecordAuthn(success, banned bool, authType string)
}
diff --git a/internal/server/const.go b/internal/server/const.go
index d4842f152..069797b65 100644
--- a/internal/server/const.go
+++ b/internal/server/const.go
@@ -14,6 +14,12 @@ const (
extYML = ".yml"
)
+const (
+ pathAuthz = "/api/authz"
+ pathAuthzLegacy = "/api/verify"
+ pathParamAuthzEnvoy = "{authz_path:*}"
+)
+
var (
filesRoot = []string{"manifest.json", "robots.txt"}
filesSwagger = []string{
diff --git a/internal/server/handlers.go b/internal/server/handlers.go
index 277cb16f9..106fea7fd 100644
--- a/internal/server/handlers.go
+++ b/internal/server/handlers.go
@@ -3,6 +3,7 @@ package server
import (
"net"
"os"
+ "path"
"strings"
"time"
@@ -90,7 +91,10 @@ func handleNotFound(next fasthttp.RequestHandler) fasthttp.RequestHandler {
}
}
+//nolint:gocyclo
func handleRouter(config schema.Configuration, providers middlewares.Providers) fasthttp.RequestHandler {
+ log := logging.Logger()
+
optsTemplatedFile := NewTemplatedFileOptions(&config)
serveIndexHandler := ServeTemplatedFile(providers.Templates.GetAssetIndexTemplate(), optsTemplatedFile)
@@ -100,7 +104,7 @@ func handleRouter(config schema.Configuration, providers middlewares.Providers)
handlerPublicHTML := newPublicHTMLEmbeddedHandler()
handlerLocales := newLocalesEmbeddedHandler()
- middleware := middlewares.NewBridgeBuilder(config, providers).
+ bridge := middlewares.NewBridgeBuilder(config, providers).
WithPreMiddlewares(middlewares.SecurityHeaders).Build()
policyCORSPublicGET := middlewares.NewCORSPolicyBuilder().
@@ -111,7 +115,7 @@ func handleRouter(config schema.Configuration, providers middlewares.Providers)
r := router.New()
// Static Assets.
- r.GET("/", middleware(serveIndexHandler))
+ r.GET("/", bridge(serveIndexHandler))
for _, f := range filesRoot {
r.GET("/"+f, handlerPublicHTML)
@@ -126,11 +130,11 @@ func handleRouter(config schema.Configuration, providers middlewares.Providers)
r.GET("/locales/{language:[a-z]{1,3}}/{namespace:[a-z]+}.json", middlewares.AssetOverride(config.Server.AssetPath, 0, handlerLocales))
// Swagger.
- r.GET("/api/", middleware(serveOpenAPIHandler))
+ r.GET("/api/", bridge(serveOpenAPIHandler))
r.OPTIONS("/api/", policyCORSPublicGET.HandleOPTIONS)
- r.GET("/api/index.html", middleware(serveOpenAPIHandler))
+ r.GET("/api/index.html", bridge(serveOpenAPIHandler))
r.OPTIONS("/api/index.html", policyCORSPublicGET.HandleOPTIONS)
- r.GET("/api/openapi.yml", policyCORSPublicGET.Middleware(middleware(serveOpenAPISpecHandler)))
+ r.GET("/api/openapi.yml", policyCORSPublicGET.Middleware(bridge(serveOpenAPISpecHandler)))
r.OPTIONS("/api/openapi.yml", policyCORSPublicGET.HandleOPTIONS)
for _, file := range filesSwagger {
@@ -153,10 +157,48 @@ func handleRouter(config schema.Configuration, providers middlewares.Providers)
r.GET("/api/configuration/password-policy", middlewareAPI(handlers.PasswordPolicyConfigurationGET))
- metricsVRMW := middlewares.NewMetricsVerifyRequest(providers.Metrics)
+ metricsVRMW := middlewares.NewMetricsAuthzRequest(providers.Metrics)
+
+ for name, endpoint := range config.Server.Endpoints.Authz {
+ uri := path.Join(pathAuthz, name)
+
+ authz := handlers.NewAuthzBuilder().WithConfig(&config).WithEndpointConfig(endpoint).Build()
+
+ handler := middlewares.Wrap(metricsVRMW, bridge(authz.Handler))
+
+ switch name {
+ case "legacy":
+ log.
+ WithField("path_prefix", pathAuthzLegacy).
+ WithField("impl", endpoint.Implementation).
+ WithField("methods", []string{"*"}).
+ Trace("Registering Authz Endpoint")
- r.ANY("/api/verify", middlewares.Wrap(metricsVRMW, middleware(handlers.VerifyGET(config.AuthenticationBackend))))
- r.ANY("/api/verify/{path:*}", middlewares.Wrap(metricsVRMW, middleware(handlers.VerifyGET(config.AuthenticationBackend))))
+ r.ANY(pathAuthzLegacy, handler)
+ r.ANY(path.Join(pathAuthzLegacy, pathParamAuthzEnvoy), handler)
+ default:
+ switch endpoint.Implementation {
+ case handlers.AuthzImplLegacy.String(), handlers.AuthzImplExtAuthz.String():
+ log.
+ WithField("path_prefix", uri).
+ WithField("impl", endpoint.Implementation).
+ WithField("methods", []string{"*"}).
+ Trace("Registering Authz Endpoint")
+
+ r.ANY(uri, handler)
+ r.ANY(path.Join(uri, pathParamAuthzEnvoy), handler)
+ default:
+ log.
+ WithField("path", uri).
+ WithField("impl", endpoint.Implementation).
+ WithField("methods", []string{fasthttp.MethodGet, fasthttp.MethodHead}).
+ Trace("Registering Authz Endpoint")
+
+ r.GET(uri, handler)
+ r.HEAD(uri, handler)
+ }
+ }
+ }
r.POST("/api/checks/safe-redirection", middlewareAPI(handlers.CheckSafeRedirectionPOST))
@@ -217,11 +259,11 @@ func handleRouter(config schema.Configuration, providers middlewares.Providers)
r.POST("/api/secondfactor/duo_device", middleware1FA(handlers.DuoDevicePOST))
}
- if config.Server.EnablePprof {
+ if config.Server.Endpoints.EnablePprof {
r.GET("/debug/pprof/{name?}", pprofhandler.PprofHandler)
}
- if config.Server.EnableExpvars {
+ if config.Server.Endpoints.EnableExpvars {
r.GET("/debug/vars", expvarhandler.ExpvarHandler)
}
@@ -315,7 +357,7 @@ func handleRouter(config schema.Configuration, providers middlewares.Providers)
r.HandleMethodNotAllowed = true
r.MethodNotAllowed = handlers.Status(fasthttp.StatusMethodNotAllowed)
- r.NotFound = handleNotFound(middleware(serveIndexHandler))
+ r.NotFound = handleNotFound(bridge(serveIndexHandler))
handler := middlewares.LogRequest(r.Handler)
if config.Server.Path != "" {
diff --git a/internal/server/server_test.go b/internal/server/server_test.go
index dd87678e5..9b6dfa992 100644
--- a/internal/server/server_test.go
+++ b/internal/server/server_test.go
@@ -9,6 +9,7 @@ import (
"io"
"net/http"
"os"
+ "path"
"strconv"
"strings"
"testing"
@@ -25,6 +26,12 @@ import (
"github.com/authelia/authelia/v4/internal/utils"
)
+func Test(t *testing.T) {
+ fmt.Println(path.Join("/api/authz/", "abc"))
+ fmt.Println(path.Join("/api/authz/", "abc/123/", "{path:*}"))
+ fmt.Println(path.Join("/api/authz/", "abc/123/"))
+}
+
// TemporaryCertificate contains the FD of 2 temporary files containing the PEM format of the certificate and private key.
type TemporaryCertificate struct {
CertFile *os.File
@@ -190,7 +197,7 @@ func TestShouldRaiseErrorWhenClientDoesNotSkipVerify(t *testing.T) {
tlsServerContext, err := NewTLSServerContext(schema.Configuration{
Server: schema.ServerConfiguration{
- TLS: schema.ServerTLSConfiguration{
+ TLS: schema.ServerTLS{
Certificate: certificateContext.Certificates[0].CertFile.Name(),
Key: certificateContext.Certificates[0].KeyFile.Name(),
},
@@ -218,7 +225,7 @@ func TestShouldServeOverTLSWhenClientDoesSkipVerify(t *testing.T) {
tlsServerContext, err := NewTLSServerContext(schema.Configuration{
Server: schema.ServerConfiguration{
- TLS: schema.ServerTLSConfiguration{
+ TLS: schema.ServerTLS{
Certificate: certificateContext.Certificates[0].CertFile.Name(),
Key: certificateContext.Certificates[0].KeyFile.Name(),
},
@@ -255,7 +262,7 @@ func TestShouldServeOverTLSWhenClientHasProperRootCA(t *testing.T) {
tlsServerContext, err := NewTLSServerContext(schema.Configuration{
Server: schema.ServerConfiguration{
- TLS: schema.ServerTLSConfiguration{
+ TLS: schema.ServerTLS{
Certificate: certificateContext.Certificates[0].CertFile.Name(),
Key: certificateContext.Certificates[0].KeyFile.Name(),
},
@@ -306,7 +313,7 @@ func TestShouldRaiseWhenMutualTLSIsConfiguredAndClientIsNotAuthenticated(t *test
tlsServerContext, err := NewTLSServerContext(schema.Configuration{
Server: schema.ServerConfiguration{
- TLS: schema.ServerTLSConfiguration{
+ TLS: schema.ServerTLS{
Certificate: certificateContext.Certificates[0].CertFile.Name(),
Key: certificateContext.Certificates[0].KeyFile.Name(),
ClientCertificates: []string{clientCert.CertFile.Name()},
@@ -349,7 +356,7 @@ func TestShouldServeProperlyWhenMutualTLSIsConfiguredAndClientIsAuthenticated(t
tlsServerContext, err := NewTLSServerContext(schema.Configuration{
Server: schema.ServerConfiguration{
- TLS: schema.ServerTLSConfiguration{
+ TLS: schema.ServerTLS{
Certificate: certificateContext.Certificates[0].CertFile.Name(),
Key: certificateContext.Certificates[0].KeyFile.Name(),
ClientCertificates: []string{clientCert.CertFile.Name()},
diff --git a/internal/server/template.go b/internal/server/template.go
index 7592e67ae..3e4de786c 100644
--- a/internal/server/template.go
+++ b/internal/server/template.go
@@ -210,6 +210,7 @@ func NewTemplatedFileOptions(config *schema.Configuration) (opts *TemplatedFileO
EndpointsTOTP: !config.TOTP.Disable,
EndpointsDuo: !config.DuoAPI.Disable,
EndpointsOpenIDConnect: !(config.IdentityProviders.OIDC == nil),
+ EndpointsAuthz: config.Server.Endpoints.Authz,
}
if config.PrivacyPolicy.Enabled {
@@ -241,6 +242,8 @@ type TemplatedFileOptions struct {
EndpointsTOTP bool
EndpointsDuo bool
EndpointsOpenIDConnect bool
+
+ EndpointsAuthz map[string]schema.ServerAuthzEndpoint
}
// CommonData returns a TemplatedFileCommonData with the dynamic options.
@@ -288,12 +291,13 @@ func (options *TemplatedFileOptions) OpenAPIData(base, baseURL, nonce string) Te
BaseURL: baseURL,
CSPNonce: nonce,
- Session: options.Session,
- PasswordReset: options.EndpointsPasswordReset,
- Webauthn: options.EndpointsWebauthn,
- TOTP: options.EndpointsTOTP,
- Duo: options.EndpointsDuo,
- OpenIDConnect: options.EndpointsOpenIDConnect,
+ Session: options.Session,
+ PasswordReset: options.EndpointsPasswordReset,
+ Webauthn: options.EndpointsWebauthn,
+ TOTP: options.EndpointsTOTP,
+ Duo: options.EndpointsDuo,
+ OpenIDConnect: options.EndpointsOpenIDConnect,
+ EndpointsAuthz: options.EndpointsAuthz,
}
}
@@ -324,4 +328,6 @@ type TemplatedFileOpenAPIData struct {
TOTP bool
Duo bool
OpenIDConnect bool
+
+ EndpointsAuthz map[string]schema.ServerAuthzEndpoint
}
diff --git a/internal/session/provider_test.go b/internal/session/provider_test.go
index 81eab3cff..4073ad546 100644
--- a/internal/session/provider_test.go
+++ b/internal/session/provider_test.go
@@ -39,7 +39,7 @@ func TestShouldInitializerSession(t *testing.T) {
session, err := provider.GetSession(ctx)
assert.NoError(t, err)
- assert.Equal(t, NewDefaultUserSession(), session)
+ assert.Equal(t, provider.NewDefaultUserSession(), session)
}
func TestShouldUpdateSession(t *testing.T) {
@@ -60,6 +60,7 @@ func TestShouldUpdateSession(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, UserSession{
+ CookieDomain: testDomain,
Username: testUsername,
AuthenticationLevel: authentication.TwoFactor,
}, session)
@@ -98,6 +99,7 @@ func TestShouldSetSessionAuthenticationLevels(t *testing.T) {
assert.Equal(t, timeZeroFactor, authAt)
assert.Equal(t, UserSession{
+ CookieDomain: testDomain,
Username: testUsername,
AuthenticationLevel: authentication.OneFactor,
LastActivity: timeOneFactor.Unix(),
@@ -114,6 +116,7 @@ func TestShouldSetSessionAuthenticationLevels(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, UserSession{
+ CookieDomain: testDomain,
Username: testUsername,
AuthenticationLevel: authentication.TwoFactor,
LastActivity: timeTwoFactor.Unix(),
@@ -168,6 +171,7 @@ func TestShouldSetSessionAuthenticationLevelsAMR(t *testing.T) {
assert.Equal(t, timeZeroFactor, authAt)
assert.Equal(t, UserSession{
+ CookieDomain: testDomain,
Username: testUsername,
AuthenticationLevel: authentication.OneFactor,
LastActivity: timeOneFactor.Unix(),
diff --git a/internal/session/session.go b/internal/session/session.go
index 89c32830e..5d4e9577e 100644
--- a/internal/session/session.go
+++ b/internal/session/session.go
@@ -4,7 +4,7 @@ import (
"encoding/json"
"time"
- fasthttpsession "github.com/fasthttp/session/v2"
+ "github.com/fasthttp/session/v2"
"github.com/valyala/fasthttp"
"github.com/authelia/authelia/v4/internal/configuration/schema"
@@ -14,15 +14,24 @@ import (
type Session struct {
Config schema.SessionCookieConfiguration
- sessionHolder *fasthttpsession.Session
+ sessionHolder *session.Session
+}
+
+// NewDefaultUserSession returns a new default UserSession for this session provider.
+func (p *Session) NewDefaultUserSession() (userSession UserSession) {
+ userSession = NewDefaultUserSession()
+
+ userSession.CookieDomain = p.Config.Domain
+
+ return userSession
}
// GetSession return the user session from a request.
-func (p *Session) GetSession(ctx *fasthttp.RequestCtx) (UserSession, error) {
- store, err := p.sessionHolder.Get(ctx)
+func (p *Session) GetSession(ctx *fasthttp.RequestCtx) (userSession UserSession, err error) {
+ var store *session.Store
- if err != nil {
- return NewDefaultUserSession(), err
+ if store, err = p.sessionHolder.Get(ctx); err != nil {
+ return p.NewDefaultUserSession(), err
}
userSessionJSON, ok := store.Get(userSessionStorerKey).([]byte)
@@ -30,42 +39,38 @@ func (p *Session) GetSession(ctx *fasthttp.RequestCtx) (UserSession, error) {
// If userSession is not yet defined we create the new session with default values
// and save it in the store.
if !ok {
- userSession := NewDefaultUserSession()
+ userSession = p.NewDefaultUserSession()
store.Set(userSessionStorerKey, userSession)
return userSession, nil
}
- var userSession UserSession
- err = json.Unmarshal(userSessionJSON, &userSession)
-
- if err != nil {
- return NewDefaultUserSession(), err
+ if err = json.Unmarshal(userSessionJSON, &userSession); err != nil {
+ return p.NewDefaultUserSession(), err
}
return userSession, nil
}
// SaveSession save the user session.
-func (p *Session) SaveSession(ctx *fasthttp.RequestCtx, userSession UserSession) error {
- store, err := p.sessionHolder.Get(ctx)
+func (p *Session) SaveSession(ctx *fasthttp.RequestCtx, userSession UserSession) (err error) {
+ var (
+ store *session.Store
+ userSessionJSON []byte
+ )
- if err != nil {
+ if store, err = p.sessionHolder.Get(ctx); err != nil {
return err
}
- userSessionJSON, err := json.Marshal(userSession)
-
- if err != nil {
+ if userSessionJSON, err = json.Marshal(userSession); err != nil {
return err
}
store.Set(userSessionStorerKey, userSessionJSON)
- err = p.sessionHolder.Save(ctx, store)
-
- if err != nil {
+ if err = p.sessionHolder.Save(ctx, store); err != nil {
return err
}
@@ -74,9 +79,7 @@ func (p *Session) SaveSession(ctx *fasthttp.RequestCtx, userSession UserSession)
// RegenerateSession regenerate a session ID.
func (p *Session) RegenerateSession(ctx *fasthttp.RequestCtx) error {
- err := p.sessionHolder.Regenerate(ctx)
-
- return err
+ return p.sessionHolder.Regenerate(ctx)
}
// DestroySession destroy a session ID and delete the cookie.
@@ -85,10 +88,10 @@ func (p *Session) DestroySession(ctx *fasthttp.RequestCtx) error {
}
// UpdateExpiration update the expiration of the cookie and session.
-func (p *Session) UpdateExpiration(ctx *fasthttp.RequestCtx, expiration time.Duration) error {
- store, err := p.sessionHolder.Get(ctx)
+func (p *Session) UpdateExpiration(ctx *fasthttp.RequestCtx, expiration time.Duration) (err error) {
+ var store *session.Store
- if err != nil {
+ if store, err = p.sessionHolder.Get(ctx); err != nil {
return err
}
diff --git a/internal/session/types.go b/internal/session/types.go
index f256da2cb..9fc7bd5b4 100644
--- a/internal/session/types.go
+++ b/internal/session/types.go
@@ -18,6 +18,8 @@ type ProviderConfig struct {
// UserSession is the structure representing the session of a user.
type UserSession struct {
+ CookieDomain string
+
Username string
DisplayName string
// TODO(c.michaud): move groups out of the session.
diff --git a/internal/suites/BypassAll/configuration.yml b/internal/suites/BypassAll/configuration.yml
index e2b592a5f..8e5157fdc 100644
--- a/internal/suites/BypassAll/configuration.yml
+++ b/internal/suites/BypassAll/configuration.yml
@@ -20,10 +20,12 @@ authentication_backend:
session:
secret: unsecure_session_secret
- domain: example.com
expiration: 3600 # 1 hour
inactivity: 300 # 5 minutes
remember_me: 1y
+ cookies:
+ - domain: 'example.com'
+ authelia_url: 'https://login.example.com:8080'
storage:
encryption_key: a_not_so_secure_encryption_key
diff --git a/internal/suites/CLI/configuration.yml b/internal/suites/CLI/configuration.yml
index 99a1aee7d..a01f2a06b 100644
--- a/internal/suites/CLI/configuration.yml
+++ b/internal/suites/CLI/configuration.yml
@@ -23,7 +23,7 @@ session:
cookies:
- name: 'authelia_session'
domain: 'example.com'
- authelia_url: 'https://login.example.com'
+ authelia_url: 'https://login.example.com:8080'
expiration: 3600 # 1 hour
inactivity: 300 # 5 minutes
remember_me: 1y
diff --git a/internal/suites/Caddy/configuration.yml b/internal/suites/Caddy/configuration.yml
index 4ce6a5b4d..38031dcdc 100644
--- a/internal/suites/Caddy/configuration.yml
+++ b/internal/suites/Caddy/configuration.yml
@@ -11,6 +11,11 @@ server:
tls:
certificate: /config/ssl/cert.pem
key: /config/ssl/key.pem
+ endpoints:
+ authz:
+ caddy:
+ implementation: ForwardAuth
+ authn_strategies: []
log:
level: debug
@@ -21,10 +26,12 @@ authentication_backend:
session:
secret: unsecure_session_secret
- domain: example.com
expiration: 3600 # 1 hour
inactivity: 300 # 5 minutes
remember_me: 1y
+ cookies:
+ - domain: 'example.com'
+ authelia_url: 'https://login.example.com:8080'
storage:
encryption_key: a_not_so_secure_encryption_key
diff --git a/internal/suites/Docker/configuration.yml b/internal/suites/Docker/configuration.yml
index d37132c67..095e9fdb1 100644
--- a/internal/suites/Docker/configuration.yml
+++ b/internal/suites/Docker/configuration.yml
@@ -21,10 +21,12 @@ authentication_backend:
session:
secret: unsecure_session_secret
- domain: example.com
expiration: 3600 # 1 hour
inactivity: 300 # 5 minutes
remember_me: 1y
+ cookies:
+ - domain: 'example.com'
+ authelia_url: 'https://login.example.com:8080'
storage:
encryption_key: a_not_so_secure_encryption_key
diff --git a/internal/suites/DuoPush/configuration.yml b/internal/suites/DuoPush/configuration.yml
index cb8caaec0..79c40ec3a 100644
--- a/internal/suites/DuoPush/configuration.yml
+++ b/internal/suites/DuoPush/configuration.yml
@@ -21,10 +21,12 @@ authentication_backend:
session:
secret: unsecure_session_secret
- domain: example.com
expiration: 3600 # 1 hour
inactivity: 300 # 5 minutes
remember_me: 1y
+ cookies:
+ - domain: 'example.com'
+ authelia_url: 'https://login.example.com:8080'
# Configuration of the storage backend used to store data and secrets. i.e. totp data
storage:
diff --git a/internal/suites/Envoy/configuration.yml b/internal/suites/Envoy/configuration.yml
index e1a05f80d..6a5ff2021 100644
--- a/internal/suites/Envoy/configuration.yml
+++ b/internal/suites/Envoy/configuration.yml
@@ -11,6 +11,11 @@ server:
tls:
certificate: /config/ssl/cert.pem
key: /config/ssl/key.pem
+ endpoints:
+ authz:
+ ext-authz:
+ implementation: ExtAuthz
+ authn_strategies: []
log:
level: debug
@@ -27,6 +32,7 @@ session:
cookies:
- name: 'authelia_session'
domain: 'example.com'
+ authelia_url: 'https://login.example.com:8080/'
storage:
encryption_key: a_not_so_secure_encryption_key
diff --git a/internal/suites/HAProxy/configuration.yml b/internal/suites/HAProxy/configuration.yml
index 1de216395..ddbe9e58e 100644
--- a/internal/suites/HAProxy/configuration.yml
+++ b/internal/suites/HAProxy/configuration.yml
@@ -20,10 +20,12 @@ authentication_backend:
session:
secret: unsecure_session_secret
- domain: example.com
expiration: 3600 # 1 hour
inactivity: 300 # 5 minutes
remember_me: 1y
+ cookies:
+ - domain: 'example.com'
+ authelia_url: 'https://login.example.com:8080'
storage:
encryption_key: a_not_so_secure_encryption_key
diff --git a/internal/suites/LDAP/configuration.yml b/internal/suites/LDAP/configuration.yml
index f69a46c7c..120e89224 100644
--- a/internal/suites/LDAP/configuration.yml
+++ b/internal/suites/LDAP/configuration.yml
@@ -35,10 +35,12 @@ authentication_backend:
session:
secret: unsecure_session_secret
- domain: example.com
expiration: 3600 # 1 hour
inactivity: 300 # 5 minutes
remember_me: 1y
+ cookies:
+ - domain: 'example.com'
+ authelia_url: 'https://login.example.com:8080'
storage:
encryption_key: a_not_so_secure_encryption_key
diff --git a/internal/suites/MariaDB/configuration.yml b/internal/suites/MariaDB/configuration.yml
index 3228275b3..e575b1408 100644
--- a/internal/suites/MariaDB/configuration.yml
+++ b/internal/suites/MariaDB/configuration.yml
@@ -21,10 +21,12 @@ authentication_backend:
session:
secret: unsecure_session_secret
- domain: example.com
expiration: 3600 # 1 hour
inactivity: 300 # 5 minutes
remember_me: 1y
+ cookies:
+ - domain: 'example.com'
+ authelia_url: 'https://login.example.com:8080'
# Configuration of the storage backend used to store data and secrets. i.e. totp data
storage:
diff --git a/internal/suites/MySQL/configuration.yml b/internal/suites/MySQL/configuration.yml
index cf8742d0b..7dd065be9 100644
--- a/internal/suites/MySQL/configuration.yml
+++ b/internal/suites/MySQL/configuration.yml
@@ -22,10 +22,12 @@ authentication_backend:
session:
secret: unsecure_session_secret
- domain: example.com
expiration: 3600 # 1 hour
inactivity: 300 # 5 minutes
remember_me: 1y
+ cookies:
+ - domain: 'example.com'
+ authelia_url: 'https://login.example.com:8080'
# Configuration of the storage backend used to store data and secrets. i.e. totp data
storage:
diff --git a/internal/suites/NetworkACL/configuration.yml b/internal/suites/NetworkACL/configuration.yml
index 5bdf2fa87..c07cfe3ee 100644
--- a/internal/suites/NetworkACL/configuration.yml
+++ b/internal/suites/NetworkACL/configuration.yml
@@ -20,10 +20,12 @@ authentication_backend:
session:
secret: unsecure_session_secret
- domain: example.com
expiration: 3600 # 1 hour
inactivity: 300 # 5 minutes
remember_me: 1y
+ cookies:
+ - domain: 'example.com'
+ authelia_url: 'https://login.example.com:8080'
# Configuration of the storage backend used to store data and secrets. i.e. totp data
storage:
diff --git a/internal/suites/OIDC/configuration.yml b/internal/suites/OIDC/configuration.yml
index 543c6da38..0b1d17f2f 100644
--- a/internal/suites/OIDC/configuration.yml
+++ b/internal/suites/OIDC/configuration.yml
@@ -18,7 +18,8 @@ session:
secret: unsecure_session_secret
cookies:
- - domain: example.com
+ - domain: 'example.com'
+ authelia_url: 'https://login.example.com:8080'
expiration: 3600 # 1 hour
inactivity: 300 # 5 minutes
remember_me: 1y
diff --git a/internal/suites/OIDCTraefik/configuration.yml b/internal/suites/OIDCTraefik/configuration.yml
index 367dc4e2b..2e089fda9 100644
--- a/internal/suites/OIDCTraefik/configuration.yml
+++ b/internal/suites/OIDCTraefik/configuration.yml
@@ -16,10 +16,13 @@ authentication_backend:
session:
secret: unsecure_session_secret
- domain: example.com
expiration: 3600 # 1 hour
inactivity: 300 # 5 minutes
remember_me: 1y
+ cookies:
+ - domain: 'example.com'
+ authelia_url: 'https://login.example.com:8080'
+
# We use redis here to keep the users authenticated when Authelia restarts
# It eases development.
redis:
diff --git a/internal/suites/OneFactorOnly/configuration.yml b/internal/suites/OneFactorOnly/configuration.yml
index 203a803bd..bc3ac4485 100644
--- a/internal/suites/OneFactorOnly/configuration.yml
+++ b/internal/suites/OneFactorOnly/configuration.yml
@@ -21,10 +21,12 @@ authentication_backend:
session:
secret: unsecure_session_secret
- domain: example.com
expiration: 3600 # 1 hour
inactivity: 300 # 5 minutes
remember_me: 1y
+ cookies:
+ - domain: 'example.com'
+ authelia_url: 'https://login.example.com:8080'
storage:
encryption_key: a_not_so_secure_encryption_key
diff --git a/internal/suites/PathPrefix/configuration.yml b/internal/suites/PathPrefix/configuration.yml
index 323187c5e..decb58436 100644
--- a/internal/suites/PathPrefix/configuration.yml
+++ b/internal/suites/PathPrefix/configuration.yml
@@ -21,10 +21,12 @@ authentication_backend:
session:
secret: unsecure_session_secret
- domain: example.com
expiration: 3600 # 1 hour
inactivity: 300 # 5 minutes
remember_me: 1y
+ cookies:
+ - domain: 'example.com'
+ authelia_url: 'https://login.example.com:8080/auth/'
storage:
encryption_key: a_not_so_secure_encryption_key
diff --git a/internal/suites/Postgres/configuration.yml b/internal/suites/Postgres/configuration.yml
index 958bf763a..ebeba850a 100644
--- a/internal/suites/Postgres/configuration.yml
+++ b/internal/suites/Postgres/configuration.yml
@@ -21,10 +21,12 @@ authentication_backend:
session:
secret: unsecure_session_secret
- domain: example.com
expiration: 3600 # 1 hour
inactivity: 300 # 5 minutes
remember_me: 1y
+ cookies:
+ - domain: 'example.com'
+ authelia_url: 'https://login.example.com:8080'
# Configuration of the storage backend used to store data and secrets. i.e. totp data
storage:
diff --git a/internal/suites/ShortTimeouts/configuration.yml b/internal/suites/ShortTimeouts/configuration.yml
index e73648a65..82bcb9149 100644
--- a/internal/suites/ShortTimeouts/configuration.yml
+++ b/internal/suites/ShortTimeouts/configuration.yml
@@ -22,8 +22,9 @@ authentication_backend:
session:
secret: unsecure_session_secret
cookies:
- - name: authelia_session
- domain: example.com
+ - name: 'authelia_sessin'
+ domain: 'example.com'
+ authelia_url: 'https://login.example.com:8080'
inactivity: 5
expiration: 8
remember_me: 1y
diff --git a/internal/suites/Standalone/configuration.yml b/internal/suites/Standalone/configuration.yml
index a86b20807..0deb53017 100644
--- a/internal/suites/Standalone/configuration.yml
+++ b/internal/suites/Standalone/configuration.yml
@@ -24,10 +24,12 @@ authentication_backend:
path: /config/users.yml
session:
- domain: example.com
expiration: 3600
inactivity: 300
remember_me: 1y
+ cookies:
+ - domain: 'example.com'
+ authelia_url: 'https://login.example.com:8080'
storage:
encryption_key: a_not_so_secure_encryption_key
diff --git a/internal/suites/Traefik/configuration.yml b/internal/suites/Traefik/configuration.yml
index 4ce6a5b4d..babc67281 100644
--- a/internal/suites/Traefik/configuration.yml
+++ b/internal/suites/Traefik/configuration.yml
@@ -21,10 +21,12 @@ authentication_backend:
session:
secret: unsecure_session_secret
- domain: example.com
expiration: 3600 # 1 hour
inactivity: 300 # 5 minutes
remember_me: 1y
+ cookies:
+ - domain: 'example.com'
+ authelia_url: 'https://login.example.com:8080'
storage:
encryption_key: a_not_so_secure_encryption_key
diff --git a/internal/suites/Traefik2/configuration.yml b/internal/suites/Traefik2/configuration.yml
index 8442be3cb..93da678a0 100644
--- a/internal/suites/Traefik2/configuration.yml
+++ b/internal/suites/Traefik2/configuration.yml
@@ -11,6 +11,11 @@ server:
tls:
certificate: /config/ssl/cert.pem
key: /config/ssl/key.pem
+ endpoints:
+ authz:
+ forward-auth:
+ implementation: ForwardAuth
+ authn_strategies: []
log:
level: debug
@@ -21,10 +26,13 @@ authentication_backend:
session:
secret: unsecure_session_secret
- domain: example.com
expiration: 3600 # 1 hour
inactivity: 300 # 5 minutes
remember_me: 1y
+ cookies:
+ - domain: 'example.com'
+ authelia_url: 'https://login.example.com:8080'
+
redis:
host: redis
port: 6379
diff --git a/internal/suites/const.go b/internal/suites/const.go
index b1fec2124..5cd016775 100644
--- a/internal/suites/const.go
+++ b/internal/suites/const.go
@@ -2,6 +2,7 @@ package suites
import (
"fmt"
+ "os"
"github.com/authelia/authelia/v4/internal/configuration/schema"
)
@@ -13,6 +14,11 @@ var (
Example3DotCom = "example3.com:8080"
)
+// GetPathPrefix returns the prefix/url_base of the login portal.
+func GetPathPrefix() string {
+ return os.Getenv("PathPrefix")
+}
+
// LoginBaseURLFmt the base URL of the login portal for specified baseDomain.
func LoginBaseURLFmt(baseDomain string) string {
if baseDomain == "" {
diff --git a/internal/suites/example/compose/authelia/docker-compose.backend.dev.yml b/internal/suites/example/compose/authelia/docker-compose.backend.dev.yml
index 73ded6bcd..a685a8c4b 100644
--- a/internal/suites/example/compose/authelia/docker-compose.backend.dev.yml
+++ b/internal/suites/example/compose/authelia/docker-compose.backend.dev.yml
@@ -21,11 +21,11 @@ services:
- '${GOPATH}:/go'
labels:
# Traefik 1.x
- - 'traefik.frontend.rule=Host:login.example.com;PathPrefix:/.well-known,/api,/locales,/devworkflow,/jwks.json'
+ - 'traefik.frontend.rule=Host:login.example.com;PathPrefix:/api,/devworkflow,/locales'
- 'traefik.protocol=https'
# Traefik 2.x
- 'traefik.enable=true'
- - 'traefik.http.routers.authelia_backend.rule=Host(`login.example.com`) && PathPrefix(`/.well-known`) || Host(`login.example.com`) && PathPrefix(`${PathPrefix}/.well-known`) || Host(`login.example.com`) && PathPrefix(`/api`) || Host(`login.example.com`) && PathPrefix(`${PathPrefix}/api`) || Host(`login.example.com`) && PathPrefix(`/locales`) || Host(`login.example.com`) && PathPrefix(`${PathPrefix}/locales`) || Host(`login.example.com`) && Path(`/devworkflow`) || Host(`login.example.com`) && Path(`${PathPrefix}/devworkflow`) || Host(`login.example.com`) && Path(`/jwks.json`) || Host(`login.example.com`) && Path(`${PathPrefix}/jwks.json`) || Host(`login.example.com`) && Path(`/static/media/logo.png`) || Host(`login.example.com`) && Path(`${PathPrefix}/static/media/logo.png`)' # yamllint disable-line rule:line-length
+ - 'traefik.http.routers.authelia_backend.rule=Host(`login.example.com`) && PathPrefix(`/.well-known`) || Host(`login.example.com`) && PathPrefix(`${PathPrefix}/.well-known`) || Host(`login.example.com`) && PathPrefix(`/api`) || Host(`login.example.com`) && PathPrefix(`${PathPrefix}/api`) || Host(`login.example.com`) && PathPrefix(`/devworkflow`) || Host(`login.example.com`) && PathPrefix(`${PathPrefix}/devworkflow`) || Host(`login.example.com`) && PathPrefix(`/locales`) || Host(`login.example.com`) && PathPrefix(`${PathPrefix}/locales`) || Host(`login.example.com`) && Path(`/jwks.json`) || Host(`login.example.com`) && Path(`${PathPrefix}/jwks.json`)' # yamllint disable-line rule:line-length
- 'traefik.http.routers.authelia_backend.entrypoints=https'
- 'traefik.http.routers.authelia_backend.tls=true'
- 'traefik.http.services.authelia_backend.loadbalancer.server.scheme=https'
diff --git a/internal/suites/example/compose/caddy/Caddyfile b/internal/suites/example/compose/caddy/Caddyfile
index b9d0d89ee..7096787d6 100644
--- a/internal/suites/example/compose/caddy/Caddyfile
+++ b/internal/suites/example/compose/caddy/Caddyfile
@@ -30,7 +30,7 @@ login.example.com:8080 {
}
reverse_proxy /devworkflow authelia-backend:9091 {
- import tls-transport
+ import tls-transport
}
reverse_proxy /jwks.json authelia-backend:9091 {
@@ -59,7 +59,7 @@ mail.example.com:8080 {
tls internal
log
forward_auth authelia-backend:9091 {
- uri /api/verify?rd=https://login.example.com:8080
+ uri /api/authz/caddy
copy_headers Remote-User Remote-Groups Remote-Name Remote-Email
import tls-transport
}
diff --git a/internal/suites/example/compose/envoy/envoy.yaml b/internal/suites/example/compose/envoy/envoy.yaml
index 796558927..f43d28649 100644
--- a/internal/suites/example/compose/envoy/envoy.yaml
+++ b/internal/suites/example/compose/envoy/envoy.yaml
@@ -79,7 +79,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-backend:9091
cluster: authelia-backend
@@ -91,18 +91,8 @@ static_resources:
- exact: cookie
- exact: proxy-authorization
headers_to_add:
- - key: X-Authelia-URL
- value: 'https://login.example.com:8080/'
- - 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%'
authorization_response:
allowed_upstream_headers:
patterns:
diff --git a/internal/suites/example/compose/haproxy/haproxy.cfg b/internal/suites/example/compose/haproxy/haproxy.cfg
index e1ecdefd4..68e16148a 100644
--- a/internal/suites/example/compose/haproxy/haproxy.cfg
+++ b/internal/suites/example/compose/haproxy/haproxy.cfg
@@ -7,8 +7,10 @@ defaults
default-server init-addr none
mode http
log global
- option httplog
option forwardfor
+ option httplog
+ option httpchk
+ http-check expect rstatus ^2
resolvers docker
nameserver ip 127.0.0.11:53
@@ -25,11 +27,11 @@ frontend fe_http
bind *:8080 ssl crt /usr/local/etc/haproxy/haproxy.pem
acl api-path path_beg -i /api
- acl wellknown-path path_beg -i /.well-known
- acl locales-path path_beg -i /locales
acl devworkflow-path path -i -m end /devworkflow
- acl jwks-path path -i -m end /jwks.json
acl headers-path path -i -m end /headers
+ acl jwks-path path -i -m end /jwks.json
+ acl locales-path path_beg -i /locales
+ acl wellknown-path path_beg -i /.well-known
acl host-authelia-portal hdr(host) -i login.example.com:8080
acl protected-frontends hdr(host) -m reg -i ^(?i)(admin|home|public|secure|singlefactor)\.example\.com
@@ -45,20 +47,18 @@ frontend fe_http
http-request set-var(req.method) str(PUT) if { method PUT }
http-request set-var(req.method) str(PATCH) if { method PATCH }
http-request set-var(req.method) str(DELETE) if { method DELETE }
- http-request set-header X-Forwarded-Method %[var(req.method)]
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]
# be_auth_request is used to make HAProxy do the TLS termination since the Lua script
# does not know how to handle it (see https://github.com/TimWolla/haproxy-auth-request/issues/12).
- http-request lua.auth-request be_auth_request /api/verify if protected-frontends
+ http-request lua.auth-request be_auth_request /api/authz/auth-request if protected-frontends
- http-request redirect location https://login.example.com:8080/?rd=%[var(req.scheme)]://%[base]%[var(req.questionmark)]%[query] if protected-frontends !{ var(txn.auth_response_successful) -m bool }
+ http-request redirect location https://login.example.com:8080/?rd=%[var(req.scheme)]://%[base]%[var(req.questionmark)]%[query]&rm=%[var(req.method)] if protected-frontends !{ var(txn.auth_response_successful) -m bool }
- use_backend be_authelia if host-authelia-portal api-path || wellknown-path || locales-path || devworkflow-path || jwks-path
+ use_backend be_authelia if host-authelia-portal api-path || devworkflow-path || jwks-path || locales-path || wellknown-path
use_backend fe_authelia if host-authelia-portal !api-path
use_backend be_httpbin if protected-frontends headers-path
use_backend be_mail if { hdr(host) -i mail.example.com:8080 }
diff --git a/internal/suites/example/compose/httpbin/docker-compose.yml b/internal/suites/example/compose/httpbin/docker-compose.yml
index fae6542b7..15683d340 100644
--- a/internal/suites/example/compose/httpbin/docker-compose.yml
+++ b/internal/suites/example/compose/httpbin/docker-compose.yml
@@ -9,10 +9,10 @@ services:
# Traefik 1.x
- 'traefik.frontend.rule=Host:public.example.com;Path:/headers'
- 'traefik.frontend.priority=120'
- - 'traefik.frontend.auth.forward.address=https://authelia-backend:9091/api/verify?rd=https://login.example.com:8080/'
+ - 'traefik.frontend.auth.forward.address=https://authelia-backend:9091/api/authz/forward-auth'
- 'traefik.frontend.auth.forward.tls.insecureSkipVerify=true'
- 'traefik.frontend.auth.forward.trustForwardHeader=true'
- - 'traefik.frontend.auth.forward.authResponseHeaders=Remote-User, Remote-Groups, Remote-Name, Remote-Email'
+ - 'traefik.frontend.auth.forward.authResponseHeaders=Remote-User,Remote-Groups,Remote-Name,Remote-Email'
# Traefik 2.x
- 'traefik.enable=true'
- 'traefik.http.routers.httpbin.rule=Host(`public.example.com`) && Path(`/headers`)'
diff --git a/internal/suites/example/compose/nginx/backend/docker-compose.yml b/internal/suites/example/compose/nginx/backend/docker-compose.yml
index 89ed3adbe..e71ed8dbd 100644
--- a/internal/suites/example/compose/nginx/backend/docker-compose.yml
+++ b/internal/suites/example/compose/nginx/backend/docker-compose.yml
@@ -6,7 +6,7 @@ services:
labels:
# Traefik 1.x
- 'traefik.frontend.rule=Host:home.example.com,public.example.com,secure.example.com,admin.example.com,singlefactor.example.com' # yamllint disable-line rule:line-length
- - 'traefik.frontend.auth.forward.address=https://authelia-backend:9091/api/verify?rd=https://login.example.com:8080' # yamllint disable-line rule:line-length
+ - 'traefik.frontend.auth.forward.address=https://authelia-backend:9091/api/authz/forward-auth' # yamllint disable-line rule:line-length
- 'traefik.frontend.auth.forward.tls.insecureSkipVerify=true'
- 'traefik.frontend.auth.forward.trustForwardHeader=true'
- 'traefik.frontend.auth.forward.authResponseHeaders=Remote-User,Remote-Groups,Remote-Name,Remote-Email'
diff --git a/internal/suites/example/compose/nginx/portal/nginx.conf b/internal/suites/example/compose/nginx/portal/nginx.conf
index f44c373a2..0af06dfa4 100644
--- a/internal/suites/example/compose/nginx/portal/nginx.conf
+++ b/internal/suites/example/compose/nginx/portal/nginx.conf
@@ -148,7 +148,7 @@ http {
server_name ~^(public|admin|secure|dev|singlefactor|mx[1-2])(\.mail)?\.(?<basedomain>example([0-9])*\.com)$;
resolver 127.0.0.11 ipv6=off;
- set $upstream_verify https://authelia-backend:9091/api/verify;
+ set $upstream_verify https://authelia-backend:9091/api/authz/auth-request;
set $upstream_endpoint http://nginx-backend;
set $upstream_headers http://httpbin:8000/headers;
@@ -209,12 +209,9 @@ http {
#
# X-Forwarded-Proto is mandatory since Authelia uses the "trust proxy" option.
# See https://expressjs.com/en/guide/behind-proxies.html
+ proxy_set_header X-Original-Method $request_method;
proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
- 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-Forwarded-For $proxy_add_x_forwarded_for;
# Authelia can receive Proxy-Authorization to authenticate however most of the clients
@@ -256,7 +253,7 @@ http {
server_name ~^oidc(-public)?\.(?<basedomain>example([0-9])*\.com)$;
resolver 127.0.0.11 ipv6=off;
- set $upstream_verify https://authelia-backend:9091/api/verify;
+ set $upstream_verify https://authelia-backend:9091/api/authz/auth-request;
set $upstream_endpoint http://oidc-client:8080;
ssl_certificate /etc/ssl/server.cert;
@@ -298,12 +295,9 @@ http {
#
# X-Forwarded-Proto is mandatory since Authelia uses the "trust proxy" option.
# See https://expressjs.com/en/guide/behind-proxies.html
+ proxy_set_header X-Original-Method $request_method;
proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
- proxy_set_header X-Forwarded-Proto $scheme;
- proxy_set_header X-Forwarded-Host $http_host;
- proxy_set_header X-Forwarded-URI $request_uri;
-
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# Authelia can receive Proxy-Authorization to authenticate however most of the clients
diff --git a/internal/suites/example/compose/traefik2/docker-compose.yml b/internal/suites/example/compose/traefik2/docker-compose.yml
index 3c61ce86a..ea30cf683 100644
--- a/internal/suites/example/compose/traefik2/docker-compose.yml
+++ b/internal/suites/example/compose/traefik2/docker-compose.yml
@@ -12,7 +12,7 @@ services:
- 'traefik.http.routers.api.service=api@internal'
- 'traefik.http.routers.api.tls=true'
# Traefik 2.x
- - 'traefik.http.middlewares.authelia.forwardauth.address=https://authelia-backend:9091${PathPrefix}/api/verify?rd=https://login.example.com:8080${PathPrefix}' # yamllint disable-line rule:line-length
+ - 'traefik.http.middlewares.authelia.forwardauth.address=https://authelia-backend:9091${PathPrefix}/api/authz/forward-auth' # yamllint disable-line rule:line-length
- 'traefik.http.middlewares.authelia.forwardauth.tls.insecureSkipVerify=true'
- '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
diff --git a/internal/suites/example/kube/authelia/authelia.yml b/internal/suites/example/kube/authelia/authelia.yml
index da1366902..90e138189 100644
--- a/internal/suites/example/kube/authelia/authelia.yml
+++ b/internal/suites/example/kube/authelia/authelia.yml
@@ -142,12 +142,15 @@ metadata:
app.kubernetes.io/name: authelia
spec:
forwardAuth:
- address: https://authelia-service.authelia.svc.cluster.local/api/verify?rd=https://login.example.com:8080
+ address: 'https://authelia-service.authelia.svc.cluster.local/api/authz/forward-auth'
+ trustForwardHeader: true
authResponseHeaders:
- - Remote-User
- - Remote-Name
- - Remote-Email
- - Remote-Groups
+ - 'Authorization'
+ - 'Proxy-Authorization'
+ - 'Remote-User'
+ - 'Remote-Groups'
+ - 'Remote-Email'
+ - 'Remote-Name'
tls:
insecureSkipVerify: true
...
diff --git a/internal/suites/example/kube/authelia/configs/configuration.yml b/internal/suites/example/kube/authelia/configs/configuration.yml
index fa4e3a7e5..19eec843f 100644
--- a/internal/suites/example/kube/authelia/configs/configuration.yml
+++ b/internal/suites/example/kube/authelia/configs/configuration.yml
@@ -87,7 +87,10 @@ session:
expiration: 3600 # 1 hour
inactivity: 300 # 5 minutes
remember_me: 1y
- domain: example.com
+ cookies:
+ - domain: 'example.com'
+ authelia_url: 'https://login.example.com:8080'
+
redis:
host: redis-service
port: 6379
diff --git a/internal/suites/scenario_multiple_cookie_domain_test.go b/internal/suites/scenario_multiple_cookie_domain_test.go
index b0d0604a7..e8acb3c5e 100644
--- a/internal/suites/scenario_multiple_cookie_domain_test.go
+++ b/internal/suites/scenario_multiple_cookie_domain_test.go
@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"log"
+ "strings"
"time"
)
@@ -12,17 +13,19 @@ type MultiCookieDomainScenario struct {
*RodSuite
domain, nextDomain string
+ cookieNames []string
remember bool
}
// NewMultiCookieDomainScenario returns a new Multi Cookie Domain Test Scenario.
-func NewMultiCookieDomainScenario(domain, nextDomain string, remember bool) *MultiCookieDomainScenario {
+func NewMultiCookieDomainScenario(domain, nextDomain string, cookieNames []string, remember bool) *MultiCookieDomainScenario {
return &MultiCookieDomainScenario{
- RodSuite: NewRodSuite(""),
- domain: domain,
- nextDomain: nextDomain,
- remember: remember,
+ RodSuite: NewRodSuite(""),
+ domain: domain,
+ nextDomain: nextDomain,
+ cookieNames: cookieNames,
+ remember: remember,
}
}
@@ -56,6 +59,22 @@ func (s *MultiCookieDomainScenario) TearDownTest() {
s.MustClose()
}
+func (s *MultiCookieDomainScenario) TestCookieName() {
+ ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
+ defer func() {
+ cancel()
+ s.collectScreenshot(ctx.Err(), s.Page)
+ }()
+
+ s.doLoginOneFactor(s.T(), s.Context(ctx), "john", "password", s.remember, s.domain, "")
+
+ s.WaitElementLocatedByID(s.T(), s.Context(ctx), "logout-button")
+
+ cookieNames := s.GetCookieNames()
+
+ s.Assert().Equalf(s.cookieNames, cookieNames, "cookie names should include '%s' (only and all of) but includes '%s'", strings.Join(s.cookieNames, ","), strings.Join(cookieNames, ","))
+}
+
func (s *MultiCookieDomainScenario) TestRememberMe() {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer func() {
diff --git a/internal/suites/scenario_one_factor_test.go b/internal/suites/scenario_one_factor_test.go
index adef3c575..519fc3a58 100644
--- a/internal/suites/scenario_one_factor_test.go
+++ b/internal/suites/scenario_one_factor_test.go
@@ -4,6 +4,8 @@ import (
"context"
"fmt"
"log"
+ "net/url"
+ "regexp"
"testing"
"time"
@@ -48,6 +50,36 @@ func (s *OneFactorSuite) TearDownTest() {
s.MustClose()
}
+func (s *OneFactorSuite) TestShouldNotAuthorizeSecretBeforeOneFactor() {
+ ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
+ defer func() {
+ cancel()
+ s.collectScreenshot(ctx.Err(), s.Page)
+ }()
+
+ targetURL := fmt.Sprintf("%s/secret.html", SingleFactorBaseURL)
+
+ s.doVisit(s.T(), s.Context(ctx), targetURL)
+
+ s.verifyIsFirstFactorPage(s.T(), s.Context(ctx))
+
+ raw := GetLoginBaseURLWithFallbackPrefix(BaseDomain, "/")
+
+ expected, err := url.ParseRequestURI(raw)
+ s.Assert().NoError(err)
+ s.Require().NotNil(expected)
+
+ query := expected.Query()
+
+ query.Set("rd", targetURL)
+
+ expected.RawQuery = query.Encode()
+
+ rx := regexp.MustCompile(fmt.Sprintf(`^%s(&rm=GET)?$`, regexp.QuoteMeta(expected.String())))
+
+ s.verifyURLIsRegexp(s.T(), s.Context(ctx), rx)
+}
+
func (s *OneFactorSuite) TestShouldAuthorizeSecretAfterOneFactor() {
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer func() {
diff --git a/internal/suites/scenario_two_factor_test.go b/internal/suites/scenario_two_factor_test.go
index caf898d09..de4e077cd 100644
--- a/internal/suites/scenario_two_factor_test.go
+++ b/internal/suites/scenario_two_factor_test.go
@@ -4,6 +4,8 @@ import (
"context"
"fmt"
"log"
+ "net/url"
+ "regexp"
"testing"
"time"
@@ -62,6 +64,36 @@ func (s *TwoFactorSuite) TearDownTest() {
s.MustClose()
}
+func (s *TwoFactorSuite) TestShouldNotAuthorizeSecretBeforeTwoFactor() {
+ ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
+ defer func() {
+ cancel()
+ s.collectScreenshot(ctx.Err(), s.Page)
+ }()
+
+ targetURL := fmt.Sprintf("%s/secret.html", AdminBaseURL)
+
+ s.doVisit(s.T(), s.Context(ctx), targetURL)
+
+ s.verifyIsFirstFactorPage(s.T(), s.Context(ctx))
+
+ raw := GetLoginBaseURLWithFallbackPrefix(BaseDomain, "/")
+
+ expected, err := url.ParseRequestURI(raw)
+ s.Assert().NoError(err)
+ s.Require().NotNil(expected)
+
+ query := expected.Query()
+
+ query.Set("rd", targetURL)
+
+ expected.RawQuery = query.Encode()
+
+ rx := regexp.MustCompile(fmt.Sprintf(`^%s(&rm=GET)?$`, regexp.QuoteMeta(expected.String())))
+
+ s.verifyURLIsRegexp(s.T(), s.Context(ctx), rx)
+}
+
func (s *TwoFactorSuite) TestShouldAuthorizeSecretAfterTwoFactor() {
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer func() {
diff --git a/internal/suites/suite_multi_cookie_domain_test.go b/internal/suites/suite_multi_cookie_domain_test.go
index 4c27e9200..33861fd6a 100644
--- a/internal/suites/suite_multi_cookie_domain_test.go
+++ b/internal/suites/suite_multi_cookie_domain_test.go
@@ -19,15 +19,15 @@ type MultiCookieDomainSuite struct {
}
func (s *MultiCookieDomainSuite) TestMultiCookieDomainFirstDomainScenario() {
- suite.Run(s.T(), NewMultiCookieDomainScenario(BaseDomain, Example2DotCom, true))
+ suite.Run(s.T(), NewMultiCookieDomainScenario(BaseDomain, Example2DotCom, []string{"authelia_session"}, true))
}
func (s *MultiCookieDomainSuite) TestMultiCookieDomainSecondDomainScenario() {
- suite.Run(s.T(), NewMultiCookieDomainScenario(Example2DotCom, BaseDomain, false))
+ suite.Run(s.T(), NewMultiCookieDomainScenario(Example2DotCom, BaseDomain, []string{"example2_session"}, false))
}
func (s *MultiCookieDomainSuite) TestMultiCookieDomainThirdDomainScenario() {
- suite.Run(s.T(), NewMultiCookieDomainScenario(Example3DotCom, BaseDomain, true))
+ suite.Run(s.T(), NewMultiCookieDomainScenario(Example3DotCom, BaseDomain, []string{"authelia_session"}, true))
}
func TestMultiCookieDomainSuite(t *testing.T) {
diff --git a/internal/suites/suite_pathprefix_test.go b/internal/suites/suite_pathprefix_test.go
index 9f2cd5912..a821ca17a 100644
--- a/internal/suites/suite_pathprefix_test.go
+++ b/internal/suites/suite_pathprefix_test.go
@@ -16,6 +16,10 @@ func NewPathPrefixSuite() *PathPrefixSuite {
}
}
+func (s *PathPrefixSuite) TestCheckEnv() {
+ s.Assert().Equal("/auth", GetPathPrefix())
+}
+
func (s *PathPrefixSuite) Test1FAScenario() {
suite.Run(s.T(), New1FAScenario())
}
@@ -32,6 +36,10 @@ func (s *PathPrefixSuite) TestResetPasswordScenario() {
suite.Run(s.T(), NewResetPasswordScenario())
}
+func (s *PathPrefixSuite) SetupSuite() {
+ s.T().Setenv("PathPrefix", "/auth")
+}
+
func TestPathPrefixSuite(t *testing.T) {
if testing.Short() {
t.Skip("skipping suite test in short mode")
diff --git a/internal/suites/suite_standalone_test.go b/internal/suites/suite_standalone_test.go
index f82acbe41..79e9e3d8f 100644
--- a/internal/suites/suite_standalone_test.go
+++ b/internal/suites/suite_standalone_test.go
@@ -274,7 +274,7 @@ func (s *StandaloneSuite) TestShouldVerifyAPIVerifyRedirectFromXOriginalURL() {
s.Assert().NoError(err)
urlEncodedAdminURL := url.QueryEscape(AdminBaseURL)
- s.Assert().Equal(fmt.Sprintf("<a href=\"%s\">302 Found</a>", utils.StringHTMLEscape(fmt.Sprintf("%s/?rd=%s", GetLoginBaseURL(BaseDomain), urlEncodedAdminURL))), string(body))
+ s.Assert().Equal(fmt.Sprintf("<a href=\"%s\">302 Found</a>", utils.StringHTMLEscape(fmt.Sprintf("%s/?rd=%s&rm=GET", GetLoginBaseURL(BaseDomain), urlEncodedAdminURL))), string(body))
}
func (s *StandaloneSuite) TestShouldVerifyAPIVerifyRedirectFromXOriginalHostURI() {
@@ -293,7 +293,7 @@ func (s *StandaloneSuite) TestShouldVerifyAPIVerifyRedirectFromXOriginalHostURI(
s.Assert().NoError(err)
urlEncodedAdminURL := url.QueryEscape(SecureBaseURL + "/")
- s.Assert().Equal(fmt.Sprintf("<a href=\"%s\">302 Found</a>", utils.StringHTMLEscape(fmt.Sprintf("%s/?rd=%s", GetLoginBaseURL(BaseDomain), urlEncodedAdminURL))), string(body))
+ s.Assert().Equal(fmt.Sprintf("<a href=\"%s\">302 Found</a>", utils.StringHTMLEscape(fmt.Sprintf("%s/?rd=%s&rm=GET", GetLoginBaseURL(BaseDomain), urlEncodedAdminURL))), string(body))
}
func (s *StandaloneSuite) TestShouldRecordMetrics() {
diff --git a/internal/suites/utils.go b/internal/suites/utils.go
index f6b8e89eb..4ed46ad91 100644
--- a/internal/suites/utils.go
+++ b/internal/suites/utils.go
@@ -53,11 +53,18 @@ func GetBrowserPath() (path string, err error) {
// GetLoginBaseURL returns the URL of the login portal and the path prefix if specified.
func GetLoginBaseURL(baseDomain string) string {
- if pathPrefix := os.Getenv("PathPrefix"); pathPrefix != "" {
- return LoginBaseURLFmt(baseDomain) + pathPrefix
+ return LoginBaseURLFmt(baseDomain) + GetPathPrefix()
+}
+
+// GetLoginBaseURLWithFallbackPrefix overloads GetLoginBaseURL and includes '/' as a prefix if the prefix is empty.
+func GetLoginBaseURLWithFallbackPrefix(baseDomain, fallback string) string {
+ prefix := GetPathPrefix()
+
+ if prefix == "" {
+ prefix = fallback
}
- return LoginBaseURLFmt(baseDomain)
+ return LoginBaseURLFmt(baseDomain) + prefix
}
func (rs *RodSession) collectCoverage(page *rod.Page) {
@@ -187,6 +194,17 @@ func (rs *RodSession) collectScreenshot(err error, page *rod.Page) {
}
}
+func (s *RodSuite) GetCookieNames() (names []string) {
+ cookies, err := s.Page.Cookies(nil)
+ s.Require().NoError(err)
+
+ for _, cookie := range cookies {
+ names = append(names, cookie.Name)
+ }
+
+ return names
+}
+
func fixCoveragePath(path string, file os.FileInfo, err error) error {
if err != nil {
return err
diff --git a/internal/suites/verify_url_is.go b/internal/suites/verify_url_is.go
index 10c59c62f..1b269a188 100644
--- a/internal/suites/verify_url_is.go
+++ b/internal/suites/verify_url_is.go
@@ -1,6 +1,7 @@
package suites
import (
+ "regexp"
"testing"
"github.com/go-rod/rod"
@@ -11,3 +12,9 @@ func (rs *RodSession) verifyURLIs(t *testing.T, page *rod.Page, url string) {
currentURL := page.MustInfo().URL
require.Equal(t, url, currentURL, "they should be equal")
}
+
+func (rs *RodSession) verifyURLIsRegexp(t *testing.T, page *rod.Page, rx *regexp.Regexp) {
+ currentURL := page.MustInfo().URL
+
+ require.Regexp(t, rx, currentURL, "url should match the expression")
+}
diff --git a/internal/utils/bytes.go b/internal/utils/bytes.go
new file mode 100644
index 000000000..c8443728d
--- /dev/null
+++ b/internal/utils/bytes.go
@@ -0,0 +1,28 @@
+package utils
+
+// BytesJoin is an alternate form of bytes.Join which doesn't use a sep.
+func BytesJoin(s ...[]byte) (dst []byte) {
+ if len(s) == 0 {
+ return []byte{}
+ }
+
+ if len(s) == 1 {
+ return append([]byte(nil), s[0]...)
+ }
+
+ var (
+ n, dstp int
+ )
+
+ for _, v := range s {
+ n += len(v)
+ }
+
+ dst = make([]byte, n)
+
+ for _, v := range s {
+ dstp += copy(dst[dstp:], v)
+ }
+
+ return dst
+}