Merge origin/master into feat-settings-ui
commit
7d17c39c52
262
api/openapi.yml
262
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:
|
||||
|
@ -1181,6 +1353,32 @@ components:
|
|||
type: integer
|
||||
required: true
|
||||
description: Numeric Webauthn Device ID
|
||||
originalMethodParam:
|
||||
name: X-Original-Method
|
||||
in: header
|
||||
description: Request Method
|
||||
required: true
|
||||
style: simple
|
||||
explode: true
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
- "GET"
|
||||
- "HEAD"
|
||||
- "POST"
|
||||
- "PUT"
|
||||
- "PATCH"
|
||||
- "DELETE"
|
||||
- "TRACE"
|
||||
- "CONNECT"
|
||||
- "OPTIONS"
|
||||
- "COPY"
|
||||
- "LOCK"
|
||||
- "MKCOL"
|
||||
- "MOVE"
|
||||
- "PROPFIND"
|
||||
- "PROPPATCH"
|
||||
- "UNLOCK"
|
||||
originalURLParam:
|
||||
name: X-Original-URL
|
||||
in: header
|
||||
|
@ -1216,6 +1414,56 @@ components:
|
|||
- "PROPFIND"
|
||||
- "PROPPATCH"
|
||||
- "UNLOCK"
|
||||
forwardedProtoParam:
|
||||
name: X-Forwarded-Proto
|
||||
in: header
|
||||
description: Redirection URL (Scheme / Protocol)
|
||||
required: true
|
||||
style: simple
|
||||
explode: true
|
||||
example: "https"
|
||||
schema:
|
||||
type: string
|
||||
forwardedHostParam:
|
||||
name: X-Forwarded-Host
|
||||
in: header
|
||||
description: Redirection URL (Host)
|
||||
required: true
|
||||
style: simple
|
||||
explode: true
|
||||
example: "example.com"
|
||||
schema:
|
||||
type: string
|
||||
forwardedURIParam:
|
||||
name: X-Forwarded-Uri
|
||||
in: header
|
||||
description: Redirection URL (URI)
|
||||
required: true
|
||||
style: simple
|
||||
explode: true
|
||||
example: "/path/example"
|
||||
schema:
|
||||
type: string
|
||||
forwardedForParam:
|
||||
name: X-Forwarded-For
|
||||
in: header
|
||||
description: Clients IP address or IP address chain
|
||||
required: false
|
||||
style: simple
|
||||
explode: true
|
||||
example: "192.168.0.55,192.168.0.20"
|
||||
schema:
|
||||
type: string
|
||||
autheliaURLParam:
|
||||
name: X-Authelia-URL
|
||||
in: header
|
||||
description: Authelia Portal URL
|
||||
required: false
|
||||
style: simple
|
||||
explode: true
|
||||
example: "https://auth.example.com"
|
||||
schema:
|
||||
type: string
|
||||
authParam:
|
||||
name: auth
|
||||
in: query
|
||||
|
|
|
@ -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])
|
||||
}
|
||||
|
|
|
@ -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])
|
||||
}
|
||||
}
|
||||
|
|
|
@ -348,6 +348,8 @@ func runSuiteTests(suiteName string, withEnv bool) error {
|
|||
cmd.Env = append(cmd.Env, "HEADLESS=y")
|
||||
}
|
||||
|
||||
cmd.Env = append(cmd.Env, "SUITES_LOG_LEVEL="+log.GetLevel().String())
|
||||
|
||||
testErr := cmd.Run()
|
||||
|
||||
// If the tests failed, run the error hook.
|
||||
|
|
|
@ -140,9 +140,7 @@ func setupSuite(cmd *cobra.Command, args []string) {
|
|||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = s.SetUp(suiteTmpDirectory)
|
||||
|
||||
if err != nil {
|
||||
if err = s.SetUp(suiteTmpDirectory); err != nil {
|
||||
log.Error("Failure during environment deployment.")
|
||||
teardownSuite(nil, args)
|
||||
log.Fatal(err)
|
||||
|
|
|
@ -51,12 +51,6 @@ server:
|
|||
## Useful to allow overriding of specific static assets.
|
||||
# asset_path: /config/assets/
|
||||
|
||||
## Enables the pprof endpoint.
|
||||
enable_pprof: false
|
||||
|
||||
## Enables the expvars endpoint.
|
||||
enable_expvars: false
|
||||
|
||||
## Disables writing the health check vars to /app/.healthcheck.env which makes healthcheck.sh return exit code 0.
|
||||
## This is disabled by default if either /app/.healthcheck.env or /app/healthcheck.sh do not exist.
|
||||
disable_healthcheck: false
|
||||
|
@ -104,6 +98,30 @@ server:
|
|||
## Idle timeout.
|
||||
# idle: 30s
|
||||
|
||||
## Server Endpoints configuration.
|
||||
## This section is considered advanced and it SHOULD NOT be configured unless you've read the relevant documentation.
|
||||
# endpoints:
|
||||
## Enables the pprof endpoint.
|
||||
# enable_pprof: false
|
||||
|
||||
## Enables the expvars endpoint.
|
||||
# enable_expvars: false
|
||||
|
||||
## Configure the authz endpoints.
|
||||
# authz:
|
||||
# forward-auth:
|
||||
# implementation: ForwardAuth
|
||||
# authn_strategies: []
|
||||
# ext-authz:
|
||||
# implementation: ExtAuthz
|
||||
# authn_strategies: []
|
||||
# auth-request:
|
||||
# implementation: AuthRequest
|
||||
# authn_strategies: []
|
||||
# legacy:
|
||||
# implementation: Legacy
|
||||
# authn_strategies: []
|
||||
|
||||
##
|
||||
## Log Configuration
|
||||
##
|
||||
|
@ -505,7 +523,6 @@ authentication_backend:
|
|||
# variant: standard
|
||||
# cost: 12
|
||||
|
||||
|
||||
##
|
||||
## Password Policy Configuration.
|
||||
##
|
||||
|
@ -540,6 +557,23 @@ password_policy:
|
|||
## Configures the minimum score allowed.
|
||||
min_score: 3
|
||||
|
||||
##
|
||||
## Privacy Policy Configuration
|
||||
##
|
||||
## Parameters used for displaying the privacy policy link and drawer.
|
||||
privacy_policy:
|
||||
|
||||
## Enables the display of the privacy policy using the policy_url.
|
||||
enabled: false
|
||||
|
||||
## Enables the display of the privacy policy drawer which requires users accept the privacy policy
|
||||
## on a per-browser basis.
|
||||
require_user_acceptance: false
|
||||
|
||||
## The URL of the privacy policy document. Must be an absolute URL and must have the 'https://' scheme.
|
||||
## If the privacy policy enabled option is true, this MUST be provided.
|
||||
policy_url: ''
|
||||
|
||||
##
|
||||
## Access Control Configuration
|
||||
##
|
||||
|
|
|
@ -0,0 +1,209 @@
|
|||
---
|
||||
title: "4.38: Pre-Release Notes"
|
||||
description: "Authelia 4.38 is just around the corner. This version has several additional features and improvements to existing features. In this blog post we'll discuss the new features and roughly what it means for users."
|
||||
lead: "Pre-Release Notes for 4.38"
|
||||
excerpt: "Authelia 4.38 is just around the corner. This version has several additional features and improvements to existing features. In this blog post we'll discuss the new features and roughly what it means for users."
|
||||
date: 2023-01-18T19:47:09+10:00
|
||||
draft: false
|
||||
images: []
|
||||
categories: ["News", "Release Notes"]
|
||||
tags: ["releases", "pre-release-notes"]
|
||||
contributors: ["James Elliott"]
|
||||
pinned: false
|
||||
homepage: false
|
||||
---
|
||||
|
||||
Authelia [4.38](https://github.com/authelia/authelia/milestone/17) is just around the corner. This version has several
|
||||
additional features and improvements to existing features. In this blog post we'll discuss the new features and roughly
|
||||
what it means for users.
|
||||
|
||||
Overall this release adds several major roadmap items. It's quite a big release. We expect a few bugs here and there but
|
||||
nothing major. It's one of our biggest releases to date, so while it's taken a longer time than usual it's for good
|
||||
reason we think.
|
||||
|
||||
We understand it's taking a bit longer than usual and people are getting anxious for their particular feature of
|
||||
interest. We're trying to ensure that we sufficiently add automated tests to all of the new features in both the backend
|
||||
and in the frontend via automated browser-based testing in Chromium to ensure a high quality user experience.
|
||||
|
||||
As this is a larger release we're probably going to ask users to help with some experimentation. If you're comfortable
|
||||
backing up your database then please keep your eyes peeled in the [chat](../../information/contact.md#chat).
|
||||
|
||||
_**Note:** These features discussed in this blog post are still subject to change however they represent the most likely
|
||||
outcome._
|
||||
|
||||
_**Important Note:** There are some changes in this release which deprecate older configurations. The changes should be
|
||||
backwards compatible, however mistakes happen. In addition we advise making the adjustments to your configuration as
|
||||
necessary as several new features will not be available or even possible without making the necessary adjustments. We
|
||||
will be publishing some guides on making these adjustments on the blog in the near future, including an FAQ catered to
|
||||
specific scenarios._
|
||||
|
||||
## OpenID Connect 1.0
|
||||
|
||||
As part of our ongoing effort for comprehensive support for [OpenID Connect 1.0] we'll be introducing several important
|
||||
features. Please see the [roadmap](../../roadmap/active/openid-connect.md) for more information.
|
||||
|
||||
##### OAuth 2.0 Pushed Authorization Requests
|
||||
|
||||
Support for [RFC9126] known as [Pushed Authorization Requests] is one of the main features being added to our
|
||||
[OpenID Connect 1.0] implementation in this release.
|
||||
|
||||
[Pushed Authorization Requests] allows for relying parties / clients to send the Authorization Request parameters over a
|
||||
back-channel and receive an opaque URI to be used as the `redirect_uri` on the standard Authorization endpoint in place
|
||||
of the standard Authorization Request parameters.
|
||||
|
||||
The endpoint used by this mechanism requires the relying party provides the Token Endpoint authentication parameters.
|
||||
|
||||
This means the actual Authorization Request parameters are never sent in the clear over the front-channel. This helps
|
||||
mitigate a few things:
|
||||
|
||||
1. Enhanced privacy. This is the primary focus of this specification.
|
||||
2. Part of conforming to the [OpenID Connect 1.0] specification [Financial-grade API Security Profile 1.0 (Advanced)].
|
||||
3. Reduces the attack surface by preventing an attacker from adjusting request parameters prior to the Authorization
|
||||
Server receiving them.
|
||||
4. Reduces the attack surface marginally as less information is available over the front-channel which is the most
|
||||
likely location where an attacker would have access to information. While reducing access to information is not
|
||||
a reasonable primary security method, when combined with other mechanisms present in [OpenID Connect 1.0] it is
|
||||
meaningful.
|
||||
|
||||
Even if an attacker gets the [Authorization Code], they are unlikely to have the `client_id` for example, and this is
|
||||
required to exchange the [Authorization Code] for an [Access Token] and ID Token.
|
||||
|
||||
This option can be enforced globally for users who only use relying parties which support
|
||||
[Pushed Authorization Requests], or can be individually enforced for each relying party which has support.
|
||||
|
||||
##### Proof Key for Code Exchange by OAuth Public Clients
|
||||
|
||||
While we already support [RFC7636] commonly known as [Proof Key for Code Exchange], and support enforcement at a global
|
||||
level for either public clients or all clients, we're adding a feature where administrators will be able to enforce
|
||||
[Proof Key for Code Exchange] on individual clients.
|
||||
|
||||
It should also be noted that [Proof Key for Code Exchange] can be used at the same time as
|
||||
[OAuth 2.0 Pushed Authorization Requests](#oauth-20-pushed-authorization-requests).
|
||||
|
||||
These features combined with our requirement for the HTTPS scheme are very powerful security measures.
|
||||
|
||||
[RFC7636]: https://datatracker.ietf.org/doc/html/rfc7636
|
||||
[RFC9126]: https://datatracker.ietf.org/doc/html/rfc9126
|
||||
|
||||
[Proof Key for Code Exchange]: https://oauth.net/2/pkce/
|
||||
[Access Token]: https://oauth.net/2/access-tokens/
|
||||
[Authorization Code]: https://oauth.net/2/grant-types/authorization-code/
|
||||
[Financial-grade API Security Profile 1.0 (Advanced)]: https://openid.net/specs/openid-financial-api-part-2-1_0.html
|
||||
[OpenID Connect 1.0]: https://openid.net/
|
||||
[OpenID Connect 1.0]: https://openid.net/
|
||||
[Pushed Authorization Requests]: https://oauth.net/2/pushed-authorization-requests/
|
||||
|
||||
## Multi-Domain Protection
|
||||
|
||||
In this release we are releasing the main implementation of the Multi-Domain Protection roadmap item.
|
||||
Please see the [roadmap](../../roadmap/active/openid-connect.md) for more information.
|
||||
|
||||
##### Initial Implementation
|
||||
|
||||
_**Important Note:** This feature at the time of this writing, will not work well with Webauthn. Steps are being taken
|
||||
to address this however it will not specifically delay the release of this feature._
|
||||
|
||||
This release see's the initial implementation of multi-domain protection. Users will be able to configure more than a
|
||||
single root domain for cookies provided none of them are a subdomain of another domain configured. In addition each
|
||||
domain can have individual settings.
|
||||
|
||||
This does not allow single sign-on between these distinct domains. When surveyed users had very low interest in this
|
||||
feature and technically speaking it's not trivial to implement such a feature as a lot of critical security
|
||||
considerations need to be addressed.
|
||||
|
||||
In addition this feature will allow configuration based detection of the Authelia Portal URI on proxies other than
|
||||
NGINX/NGINX Proxy Manager/SWAG/HAProxy with the use of the new
|
||||
[Customizable Authorization Endpoints](#customizable-authorization-endpoints). This is important as it means you only
|
||||
need to configure a single middleware or helper to perform automatic redirection.
|
||||
|
||||
## Webauthn
|
||||
|
||||
As part of our ongoing effort for comprehensive support for Webauthn we'll be introducing several important
|
||||
features. Please see the [roadmap](../../roadmap/active/webauthn.md) for more information.
|
||||
|
||||
##### Multiple Webauthn Credentials Per-User
|
||||
|
||||
In this release we see full support for multiple Webauthn credentials. This is a fairly basic feature but getting the
|
||||
frontend experience right is important to us. This is going to be supported via the
|
||||
[User Control Panel](#user-dashboard--control-panel).
|
||||
|
||||
## Customizable Authorization Endpoints
|
||||
|
||||
For the longest time we've managed to have the `/api/verify` endpoint perform all authorization verification. This has
|
||||
served us well however we've been growing out of it. This endpoint is being deprecated in favor of new customizable
|
||||
per-implementation endpoints. Each existing proxy we support uses one of these distinct implementations.
|
||||
|
||||
The old endpoint will still work, in fact you can technically configure an additional endpoint using the methodology of
|
||||
it via the `Legacy` implementation. However this is strongly discouraged and will not intentionally have new features or
|
||||
fixes (excluding security fixes) going forward.
|
||||
|
||||
In addition to being able to customize them you can create your own, and completely disable support for all other
|
||||
implementations in the process. Use of these new endpoints will require reconfiguration of your proxy, we plan to
|
||||
release a guide for each proxy.
|
||||
|
||||
## User Dashboard / Control Panel
|
||||
|
||||
As part of our ongoing effort for comprehensive support for a User Dashboard / Control Panel we'll be introducing
|
||||
several important features. Please see the [roadmap](../../roadmap/active/dashboard-control-panel.md) for more
|
||||
information.
|
||||
|
||||
##### Device Registration OTP
|
||||
|
||||
Instead of the current link, in this release users will instead be sent a One Time Password, cryptographically randomly
|
||||
generated by Authelia. This One Time Password will grant users a duration to perform security sensitive tasks.
|
||||
|
||||
The motivation for this is that it works in more situations, and is slightly less prone to phishing.
|
||||
|
||||
##### TOTP Registration
|
||||
|
||||
Instead of just assuming that users have successfully registered their TOTP application, we will require users to enter
|
||||
the TOTP code prior to it being saved to the database.
|
||||
|
||||
## Configuration
|
||||
|
||||
Several enhancements are landing for the configuration.
|
||||
|
||||
##### Directories
|
||||
|
||||
Users will now be able to configure a directory where all `.yml` and `.yaml` files will be loaded in lexical order.
|
||||
This will not allow combining lists of items, but it will allow you to split portions of the configuration easily.
|
||||
|
||||
##### Discovery
|
||||
|
||||
Environment variables are being added to assist with configuration discovery, and this will be the default method for
|
||||
our containers. The advantage is that since the variable will be available when execing into the container, even if
|
||||
the configuration paths have changed or you've defined additional paths, the `authelia` command will know where the
|
||||
files are if you properly use this variables.
|
||||
|
||||
##### Templating
|
||||
|
||||
The file based configuration will have access to several experimental templating filters which will assist in creating
|
||||
configuration templates. The initial one will just expand *most* environment variables into the configuration. The
|
||||
second will use the go template engine in a very similar way to how Helm operates.
|
||||
|
||||
As these features are experimental they may break, be removed, or otherwise not operate as expected. However most of our
|
||||
testing indicates they're incredibly solid.
|
||||
|
||||
##### LDAP Implementation
|
||||
|
||||
Several new LDAP implementations which provide defaults are being introduced in this version to assist users in
|
||||
integrating their LDAP server with Authelia.
|
||||
|
||||
## Miscellaneous
|
||||
|
||||
Some miscellaneous notes about this release.
|
||||
|
||||
##### Email Notifications
|
||||
|
||||
Events triggered by users will generate new notifications sent to their inbox, for example adding a new 2FA device.
|
||||
|
||||
##### Storage Import/Export
|
||||
|
||||
Utility functions to assist in exporting and subsequently importing the important values in Authelia are being added and
|
||||
unified in this release.
|
||||
|
||||
##### Privacy Policy
|
||||
|
||||
We'll be introducing a feature which allows administrators to more easily comply with the GDPR which optionally shows a
|
||||
link to their individual privacy policy on the frontend, and optionally requires users to accept it before using
|
||||
Authelia.
|
|
@ -0,0 +1,72 @@
|
|||
---
|
||||
title: "Privacy Policy"
|
||||
description: "Privacy Policy Configuration."
|
||||
lead: "This describes a section of the configuration for enabling a Privacy Policy link display."
|
||||
date: 2020-02-29T01:43:59+01:00
|
||||
draft: false
|
||||
images: []
|
||||
menu:
|
||||
configuration:
|
||||
parent: "miscellaneous"
|
||||
weight: 199100
|
||||
toc: true
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
```yaml
|
||||
privacy_policy:
|
||||
enabled: false
|
||||
require_user_acceptance: false
|
||||
policy_url: ''
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
### enabled
|
||||
|
||||
{{< confkey type="boolean" default="false" required="no" >}}
|
||||
|
||||
Enables the display of the Privacy Policy link.
|
||||
|
||||
### require_user_acceptance
|
||||
|
||||
{{< confkey type="boolean" default="false" required="no" >}}
|
||||
|
||||
Requires users accept per-browser the Privacy Policy via a Dialog Drawer at the bottom of the page. The fact they have
|
||||
accepted is recorded and checked in the browser
|
||||
[localStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage).
|
||||
|
||||
If the user has not accepted the policy they should not be able to interact with the Authelia UI via normal means.
|
||||
|
||||
Administrators who are required to abide by the [GDPR] or other privacy laws should be advised that
|
||||
[OpenID Connect 1.0](../identity-providers/open-id-connect.md) clients configured with the `implicit` consent mode are
|
||||
unlikely to trigger the display of the Authelia UI if the user is already authenticated.
|
||||
|
||||
We wont be adding checks like this to the `implicit` consent mode when that mode in particular is unlikely to be
|
||||
compliant with those laws, and that mode is not strictly compliant with the OpenID Connect 1.0 specifications. It is
|
||||
therefore recommended if `require_user_acceptance` is enabled then administrators should avoid using the `implicit`
|
||||
consent mode or do so at their own risk.
|
||||
|
||||
### policy_url
|
||||
|
||||
{{< confkey type="string" required="situational" >}}
|
||||
|
||||
The privacy policy URL is a URL which optionally is displayed in the frontend linking users to the administrators
|
||||
privacy policy. This is useful for users who wish to abide by laws such as the [GDPR].
|
||||
Administrators can view the particulars of what _Authelia_ collects out of the box with our
|
||||
[Privacy Policy](https://www.authelia.com/privacy/#application).
|
||||
|
||||
This value must be an absolute URL, and must have the `https://` scheme.
|
||||
|
||||
This option is required if the [enabled](#enabled) option is true.
|
||||
|
||||
[GDPR]: https://gdpr-info.eu/
|
||||
|
||||
_**Example:**_
|
||||
|
||||
```yaml
|
||||
privacy_policy:
|
||||
enabled: true
|
||||
policy_url: 'https://www.example.com/privacy-policy'
|
||||
```
|
|
@ -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).
|
|
@ -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
|
||||
|
|
|
@ -204,4 +204,4 @@ Configures the server write timeout.
|
|||
*__Note:__ This setting uses the [duration notation format](#duration-notation-format). Please see the
|
||||
[common options](#duration-notation-format) documentation for information on this format.*
|
||||
|
||||
Configures the server write timeout.
|
||||
Configures the server idle timeout.
|
||||
|
|
|
@ -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/
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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/
|
||||
|
|
|
@ -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 >}}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
|
@ -43,7 +43,7 @@ has many drawbacks that just are not satisfactory in order to easily facilitate
|
|||
|
||||
### Initial Implementation
|
||||
|
||||
{{< roadmap-status stage="waiting" >}}
|
||||
{{< roadmap-status stage="in-progress" version="v4.38.0" >}}
|
||||
|
||||
This stage is waiting on the choice to handle sessions. Initial implementation will involve just a basic cookie
|
||||
implementation where users will be required to sign in to each root domain and no SSO functionality will exist.
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -51,12 +51,6 @@ server:
|
|||
## Useful to allow overriding of specific static assets.
|
||||
# asset_path: /config/assets/
|
||||
|
||||
## Enables the pprof endpoint.
|
||||
enable_pprof: false
|
||||
|
||||
## Enables the expvars endpoint.
|
||||
enable_expvars: false
|
||||
|
||||
## Disables writing the health check vars to /app/.healthcheck.env which makes healthcheck.sh return exit code 0.
|
||||
## This is disabled by default if either /app/.healthcheck.env or /app/healthcheck.sh do not exist.
|
||||
disable_healthcheck: false
|
||||
|
@ -104,6 +98,30 @@ server:
|
|||
## Idle timeout.
|
||||
# idle: 30s
|
||||
|
||||
## Server Endpoints configuration.
|
||||
## This section is considered advanced and it SHOULD NOT be configured unless you've read the relevant documentation.
|
||||
# endpoints:
|
||||
## Enables the pprof endpoint.
|
||||
# enable_pprof: false
|
||||
|
||||
## Enables the expvars endpoint.
|
||||
# enable_expvars: false
|
||||
|
||||
## Configure the authz endpoints.
|
||||
# authz:
|
||||
# forward-auth:
|
||||
# implementation: ForwardAuth
|
||||
# authn_strategies: []
|
||||
# ext-authz:
|
||||
# implementation: ExtAuthz
|
||||
# authn_strategies: []
|
||||
# auth-request:
|
||||
# implementation: AuthRequest
|
||||
# authn_strategies: []
|
||||
# legacy:
|
||||
# implementation: Legacy
|
||||
# authn_strategies: []
|
||||
|
||||
##
|
||||
## Log Configuration
|
||||
##
|
||||
|
@ -505,7 +523,6 @@ authentication_backend:
|
|||
# variant: standard
|
||||
# cost: 12
|
||||
|
||||
|
||||
##
|
||||
## Password Policy Configuration.
|
||||
##
|
||||
|
@ -540,6 +557,23 @@ password_policy:
|
|||
## Configures the minimum score allowed.
|
||||
min_score: 3
|
||||
|
||||
##
|
||||
## Privacy Policy Configuration
|
||||
##
|
||||
## Parameters used for displaying the privacy policy link and drawer.
|
||||
privacy_policy:
|
||||
|
||||
## Enables the display of the privacy policy using the policy_url.
|
||||
enabled: false
|
||||
|
||||
## Enables the display of the privacy policy drawer which requires users accept the privacy policy
|
||||
## on a per-browser basis.
|
||||
require_user_acceptance: false
|
||||
|
||||
## The URL of the privacy policy document. Must be an absolute URL and must have the 'https://' scheme.
|
||||
## If the privacy policy enabled option is true, this MUST be provided.
|
||||
policy_url: ''
|
||||
|
||||
##
|
||||
## Access Control Configuration
|
||||
##
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
|
|
@ -23,4 +23,5 @@ type Configuration struct {
|
|||
Telemetry TelemetryConfig `koanf:"telemetry"`
|
||||
Webauthn WebauthnConfiguration `koanf:"webauthn"`
|
||||
PasswordPolicy PasswordPolicyConfiguration `koanf:"password_policy"`
|
||||
PrivacyPolicy PrivacyPolicy `koanf:"privacy_policy"`
|
||||
}
|
||||
|
|
|
@ -235,13 +235,17 @@ var Keys = []string{
|
|||
"server.port",
|
||||
"server.path",
|
||||
"server.asset_path",
|
||||
"server.enable_pprof",
|
||||
"server.enable_expvars",
|
||||
"server.disable_healthcheck",
|
||||
"server.tls.certificate",
|
||||
"server.tls.key",
|
||||
"server.tls.client_certificates",
|
||||
"server.headers.csp_template",
|
||||
"server.endpoints.enable_pprof",
|
||||
"server.endpoints.enable_expvars",
|
||||
"server.endpoints.authz",
|
||||
"server.endpoints.authz.*.implementation",
|
||||
"server.endpoints.authz.*.authn_strategies",
|
||||
"server.endpoints.authz.*.authn_strategies[].name",
|
||||
"server.buffers.read",
|
||||
"server.buffers.write",
|
||||
"server.timeouts.read",
|
||||
|
@ -268,4 +272,7 @@ var Keys = []string{
|
|||
"password_policy.standard.require_special",
|
||||
"password_policy.zxcvbn.enabled",
|
||||
"password_policy.zxcvbn.min_score",
|
||||
"privacy_policy.enabled",
|
||||
"privacy_policy.require_user_acceptance",
|
||||
"privacy_policy.policy_url",
|
||||
}
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
package schema
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// PrivacyPolicy is the privacy policy configuration.
|
||||
type PrivacyPolicy struct {
|
||||
Enabled bool `koanf:"enabled"`
|
||||
RequireUserAcceptance bool `koanf:"require_user_acceptance"`
|
||||
PolicyURL *url.URL `koanf:"policy_url"`
|
||||
}
|
|
@ -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",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -68,6 +68,8 @@ func ValidateConfiguration(config *schema.Configuration, validator *schema.Struc
|
|||
ValidateNTP(config, validator)
|
||||
|
||||
ValidatePasswordPolicy(&config.PasswordPolicy, validator)
|
||||
|
||||
ValidatePrivacyPolicy(&config.PrivacyPolicy, validator)
|
||||
}
|
||||
|
||||
func validateDefault2FAMethod(config *schema.Configuration, validator *schema.StructValidator) {
|
||||
|
|
|
@ -286,6 +286,14 @@ const (
|
|||
|
||||
errFmtServerPathNoForwardSlashes = "server: option 'path' must not contain any forward slashes"
|
||||
errFmtServerPathAlphaNum = "server: option 'path' must only contain alpha numeric characters"
|
||||
|
||||
errFmtServerEndpointsAuthzImplementation = "server: endpoints: authz: %s: option 'implementation' must be one of '%s' but is configured as '%s'"
|
||||
errFmtServerEndpointsAuthzStrategy = "server: endpoints: authz: %s: authn_strategies: option 'name' must be one of '%s' but is configured as '%s'"
|
||||
errFmtServerEndpointsAuthzStrategyDuplicate = "server: endpoints: authz: %s: authn_strategies: duplicate strategy name detected with name '%s'"
|
||||
errFmtServerEndpointsAuthzPrefixDuplicate = "server: endpoints: authz: %s: endpoint starts with the same prefix as the '%s' endpoint with the '%s' implementation which accepts prefixes as part of its implementation"
|
||||
errFmtServerEndpointsAuthzInvalidName = "server: endpoints: authz: %s: contains invalid characters"
|
||||
|
||||
errFmtServerEndpointsAuthzLegacyInvalidImplementation = "server: endpoints: authz: %s: option 'implementation' is invalid: the endpoint with the name 'legacy' must use the 'Legacy' implementation"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -294,22 +302,17 @@ const (
|
|||
errFmtPasswordPolicyZXCVBNMinScoreInvalid = "password_policy: zxcvbn: option 'min_score' is invalid: must be between 1 and 4 but it's configured as %d"
|
||||
)
|
||||
|
||||
const (
|
||||
errPrivacyPolicyEnabledWithoutURL = "privacy_policy: option 'policy_url' must be provided when the option 'enabled' is true"
|
||||
errFmtPrivacyPolicyURLNotHTTPS = "privacy_policy: option 'policy_url' must have the 'https' scheme but it's configured as '%s'"
|
||||
)
|
||||
|
||||
const (
|
||||
errFmtDuoMissingOption = "duo_api: option '%s' is required when duo is enabled but it is missing"
|
||||
)
|
||||
|
||||
// Error constants.
|
||||
const (
|
||||
/*
|
||||
errFmtDeprecatedConfigurationKey = "the %s configuration option is deprecated and will be " +
|
||||
"removed in %s, please use %s instead"
|
||||
|
||||
Uncomment for use when deprecating keys.
|
||||
|
||||
TODO: Create a method from within Koanf to automatically remap deprecated keys and produce warnings.
|
||||
TODO (cont): The main consideration is making sure we do not overwrite the destination key name if it already exists.
|
||||
*/
|
||||
|
||||
errFmtInvalidDefault2FAMethod = "option 'default_2fa_method' is configured as '%s' but must be one of " +
|
||||
"the following values: '%s'"
|
||||
errFmtInvalidDefault2FAMethodDisabled = "option 'default_2fa_method' is configured as '%s' " +
|
||||
|
@ -337,6 +340,17 @@ var (
|
|||
validLDAPImplementations = []string{schema.LDAPImplementationCustom, schema.LDAPImplementationActiveDirectory, schema.LDAPImplementationFreeIPA, schema.LDAPImplementationLLDAP}
|
||||
)
|
||||
|
||||
const (
|
||||
legacy = "legacy"
|
||||
authzImplementationLegacy = "Legacy"
|
||||
authzImplementationExtAuthz = "ExtAuthz"
|
||||
)
|
||||
|
||||
var (
|
||||
validAuthzImplementations = []string{"AuthRequest", "ForwardAuth", authzImplementationExtAuthz, authzImplementationLegacy}
|
||||
validAuthzAuthnStrategies = []string{"CookieSession", "HeaderAuthorization", "HeaderProxyAuthorization", "HeaderAuthRequestProxyAuthorization", "HeaderLegacy"}
|
||||
)
|
||||
|
||||
var (
|
||||
validArgon2Variants = []string{"argon2id", "id", "argon2i", "i", "argon2d", "d"}
|
||||
validSHA2CryptVariants = []string{digestSHA256, digestSHA512}
|
||||
|
@ -374,8 +388,9 @@ var (
|
|||
)
|
||||
|
||||
var (
|
||||
reKeyReplacer = regexp.MustCompile(`\[\d+]`)
|
||||
reDomainCharacters = regexp.MustCompile(`^[a-z0-9-]+(\.[a-z0-9-]+)+[a-z0-9]$`)
|
||||
reKeyReplacer = regexp.MustCompile(`\[\d+]`)
|
||||
reDomainCharacters = regexp.MustCompile(`^[a-z0-9-]+(\.[a-z0-9-]+)+[a-z0-9]$`)
|
||||
reAuthzEndpointName = regexp.MustCompile(`^[a-zA-Z](([a-zA-Z0-9/\._-]*)([a-zA-Z]))?$`)
|
||||
)
|
||||
|
||||
var replacedKeys = map[string]string{
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ import (
|
|||
"github.com/authelia/authelia/v4/internal/utils"
|
||||
)
|
||||
|
||||
// ValidatePasswordPolicy validates and update Password Policy configuration.
|
||||
// ValidatePasswordPolicy validates and updates the Password Policy configuration.
|
||||
func ValidatePasswordPolicy(config *schema.PasswordPolicyConfiguration, validator *schema.StructValidator) {
|
||||
if !utils.IsBoolCountLessThanN(1, true, config.Standard.Enabled, config.ZXCVBN.Enabled) {
|
||||
validator.Push(fmt.Errorf(errPasswordPolicyMultipleDefined))
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
package validator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
||||
)
|
||||
|
||||
// ValidatePrivacyPolicy validates and updates the Privacy Policy configuration.
|
||||
func ValidatePrivacyPolicy(config *schema.PrivacyPolicy, validator *schema.StructValidator) {
|
||||
if !config.Enabled {
|
||||
return
|
||||
}
|
||||
|
||||
switch config.PolicyURL {
|
||||
case nil:
|
||||
validator.Push(fmt.Errorf(errPrivacyPolicyEnabledWithoutURL))
|
||||
default:
|
||||
if config.PolicyURL.Scheme != schemeHTTPS {
|
||||
validator.Push(fmt.Errorf(errFmtPrivacyPolicyURLNotHTTPS, config.PolicyURL.Scheme))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package validator
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
||||
)
|
||||
|
||||
func TestValidatePrivacyPolicy(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
have schema.PrivacyPolicy
|
||||
expected string
|
||||
}{
|
||||
{"ShouldValidateDefaultConfig", schema.PrivacyPolicy{}, ""},
|
||||
{"ShouldValidateValidEnabledPolicy", schema.PrivacyPolicy{Enabled: true, PolicyURL: MustParseURL("https://example.com/privacy")}, ""},
|
||||
{"ShouldValidateValidEnabledPolicyWithUserAcceptance", schema.PrivacyPolicy{Enabled: true, RequireUserAcceptance: true, PolicyURL: MustParseURL("https://example.com/privacy")}, ""},
|
||||
{"ShouldNotValidateOnInvalidScheme", schema.PrivacyPolicy{Enabled: true, PolicyURL: MustParseURL("http://example.com/privacy")}, "privacy_policy: option 'policy_url' must have the 'https' scheme but it's configured as 'http'"},
|
||||
{"ShouldNotValidateOnMissingURL", schema.PrivacyPolicy{Enabled: true}, "privacy_policy: option 'policy_url' must be provided when the option 'enabled' is true"},
|
||||
}
|
||||
|
||||
validator := schema.NewStructValidator()
|
||||
|
||||
for _, tc := range testCases {
|
||||
validator.Clear()
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ValidatePrivacyPolicy(&tc.have, validator)
|
||||
|
||||
assert.Len(t, validator.Warnings(), 0)
|
||||
|
||||
if tc.expected == "" {
|
||||
assert.Len(t, validator.Errors(), 0)
|
||||
} else {
|
||||
assert.EqualError(t, validator.Errors()[0], tc.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
|
@ -18,9 +16,22 @@ const (
|
|||
)
|
||||
|
||||
var (
|
||||
headerAuthorization = []byte(fasthttp.HeaderAuthorization)
|
||||
headerProxyAuthorization = []byte(fasthttp.HeaderProxyAuthorization)
|
||||
headerAuthorization = []byte(fasthttp.HeaderAuthorization)
|
||||
headerWWWAuthenticate = []byte(fasthttp.HeaderWWWAuthenticate)
|
||||
|
||||
headerProxyAuthorization = []byte(fasthttp.HeaderProxyAuthorization)
|
||||
headerProxyAuthenticate = []byte(fasthttp.HeaderProxyAuthenticate)
|
||||
)
|
||||
|
||||
const (
|
||||
headerAuthorizationSchemeBasic = "basic"
|
||||
)
|
||||
|
||||
var (
|
||||
headerValueAuthenticateBasic = []byte(`Basic realm="Authorization Required"`)
|
||||
)
|
||||
|
||||
var (
|
||||
headerSessionUsername = []byte("Session-Username")
|
||||
headerRemoteUser = []byte("Remote-User")
|
||||
headerRemoteGroups = []byte("Remote-Groups")
|
||||
|
@ -30,7 +41,9 @@ var (
|
|||
|
||||
const (
|
||||
queryArgRD = "rd"
|
||||
queryArgRM = "rm"
|
||||
queryArgID = "id"
|
||||
queryArgAuth = "auth"
|
||||
queryArgConsentID = "consent_id"
|
||||
queryArgWorkflow = "workflow"
|
||||
queryArgWorkflowID = "workflow_id"
|
||||
|
@ -38,16 +51,14 @@ const (
|
|||
|
||||
var (
|
||||
qryArgID = []byte(queryArgID)
|
||||
qryArgRD = []byte(queryArgRD)
|
||||
qryArgAuth = []byte(queryArgAuth)
|
||||
qryArgConsentID = []byte(queryArgConsentID)
|
||||
)
|
||||
|
||||
const (
|
||||
// Forbidden means the user is forbidden the access to a resource.
|
||||
Forbidden authorizationMatching = iota
|
||||
// NotAuthorized means the user can access the resource with more permissions.
|
||||
NotAuthorized authorizationMatching = iota
|
||||
// Authorized means the user is authorized given her current permissions.
|
||||
Authorized authorizationMatching = iota
|
||||
var (
|
||||
qryValueBasic = []byte("basic")
|
||||
qryValueEmpty = []byte("")
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -109,13 +120,6 @@ const (
|
|||
logFmtErrConsentGenerate = logFmtConsentPrefix + "could not be processed: error occurred generating consent: %+v"
|
||||
)
|
||||
|
||||
const (
|
||||
testInactivity = time.Second * 10
|
||||
testRedirectionURL = "http://redirection.local"
|
||||
testUsername = "john"
|
||||
exampleDotCom = "example.com"
|
||||
)
|
||||
|
||||
// Duo constants.
|
||||
const (
|
||||
allow = "allow"
|
||||
|
@ -124,8 +128,6 @@ const (
|
|||
auth = "auth"
|
||||
)
|
||||
|
||||
const authPrefix = "Basic "
|
||||
|
||||
const ldapPasswordComplexityCode = "0000052D."
|
||||
|
||||
var ldapPasswordComplexityCodes = []string{
|
||||
|
|
|
@ -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"
|
||||
)
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,555 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/valyala/fasthttp"
|
||||
|
||||
"github.com/authelia/authelia/v4/internal/authentication"
|
||||
"github.com/authelia/authelia/v4/internal/authorization"
|
||||
"github.com/authelia/authelia/v4/internal/middlewares"
|
||||
"github.com/authelia/authelia/v4/internal/mocks"
|
||||
"github.com/authelia/authelia/v4/internal/session"
|
||||
)
|
||||
|
||||
func TestRunLegacyAuthzSuite(t *testing.T) {
|
||||
suite.Run(t, NewLegacyAuthzSuite())
|
||||
}
|
||||
|
||||
func NewLegacyAuthzSuite() *LegacyAuthzSuite {
|
||||
return &LegacyAuthzSuite{
|
||||
AuthzSuite: &AuthzSuite{
|
||||
implementation: AuthzImplLegacy,
|
||||
setRequest: setRequestLegacy,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type LegacyAuthzSuite struct {
|
||||
*AuthzSuite
|
||||
}
|
||||
|
||||
func (s *LegacyAuthzSuite) TestShouldHandleAllMethodsDeny() {
|
||||
for _, method := range testRequestMethods {
|
||||
s.T().Run(fmt.Sprintf("Method%s", method), func(t *testing.T) {
|
||||
for _, pairURI := range []urlpair{
|
||||
{s.RequireParseRequestURI("https://one-factor.example.com/"), s.RequireParseRequestURI("https://auth.example.com/")},
|
||||
{s.RequireParseRequestURI("https://one-factor.example.com/subpath"), s.RequireParseRequestURI("https://auth.example.com/")},
|
||||
{s.RequireParseRequestURI("https://one-factor.example2.com/"), s.RequireParseRequestURI("https://auth.example2.com/")},
|
||||
{s.RequireParseRequestURI("https://one-factor.example2.com/subpath"), s.RequireParseRequestURI("https://auth.example2.com/")},
|
||||
} {
|
||||
t.Run(pairURI.TargetURI.String(), func(t *testing.T) {
|
||||
expected := s.RequireParseRequestURI(pairURI.AutheliaURI.String())
|
||||
|
||||
authz := s.Builder().Build()
|
||||
|
||||
mock := mocks.NewMockAutheliaCtx(t)
|
||||
|
||||
defer mock.Close()
|
||||
|
||||
for i, cookie := range mock.Ctx.Configuration.Session.Cookies {
|
||||
mock.Ctx.Configuration.Session.Cookies[i].AutheliaURL = s.RequireParseRequestURI(fmt.Sprintf("https://auth.%s", cookie.Domain))
|
||||
}
|
||||
|
||||
mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil)
|
||||
|
||||
mock.Ctx.RequestCtx.QueryArgs().Set(queryArgRD, pairURI.AutheliaURI.String())
|
||||
mock.Ctx.Request.Header.Set("X-Forwarded-Method", method)
|
||||
mock.Ctx.Request.Header.Set(fasthttp.HeaderXForwardedProto, pairURI.TargetURI.Scheme)
|
||||
mock.Ctx.Request.Header.Set(fasthttp.HeaderXForwardedHost, pairURI.TargetURI.Host)
|
||||
mock.Ctx.Request.Header.Set("X-Forwarded-Uri", pairURI.TargetURI.Path)
|
||||
mock.Ctx.Request.Header.Set(fasthttp.HeaderAccept, "text/html; charset=utf-8")
|
||||
|
||||
authz.Handler(mock.Ctx)
|
||||
|
||||
switch method {
|
||||
case fasthttp.MethodGet, fasthttp.MethodOptions:
|
||||
assert.Equal(t, fasthttp.StatusFound, mock.Ctx.Response.StatusCode())
|
||||
default:
|
||||
assert.Equal(t, fasthttp.StatusSeeOther, mock.Ctx.Response.StatusCode())
|
||||
}
|
||||
|
||||
query := expected.Query()
|
||||
query.Set(queryArgRD, pairURI.TargetURI.String())
|
||||
query.Set(queryArgRM, method)
|
||||
expected.RawQuery = query.Encode()
|
||||
|
||||
assert.Equal(t, expected.String(), string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation)))
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s *LegacyAuthzSuite) TestShouldHandleAllMethodsOverrideAutheliaURLDeny() {
|
||||
for _, method := range testRequestMethods {
|
||||
s.T().Run(fmt.Sprintf("Method%s", method), func(t *testing.T) {
|
||||
for _, pairURI := range []urlpair{
|
||||
{s.RequireParseRequestURI("https://one-factor.example.com/"), s.RequireParseRequestURI("https://auth-from-override.example.com/")},
|
||||
{s.RequireParseRequestURI("https://one-factor.example.com/subpath"), s.RequireParseRequestURI("https://auth-from-override.example.com/")},
|
||||
{s.RequireParseRequestURI("https://one-factor.example2.com/"), s.RequireParseRequestURI("https://auth-from-override.example2.com/")},
|
||||
{s.RequireParseRequestURI("https://one-factor.example2.com/subpath"), s.RequireParseRequestURI("https://auth-from-override.example2.com/")},
|
||||
} {
|
||||
t.Run(pairURI.TargetURI.String(), func(t *testing.T) {
|
||||
expected := s.RequireParseRequestURI(pairURI.AutheliaURI.String())
|
||||
|
||||
authz := s.Builder().Build()
|
||||
|
||||
mock := mocks.NewMockAutheliaCtx(t)
|
||||
|
||||
defer mock.Close()
|
||||
|
||||
for i, cookie := range mock.Ctx.Configuration.Session.Cookies {
|
||||
mock.Ctx.Configuration.Session.Cookies[i].AutheliaURL = s.RequireParseRequestURI(fmt.Sprintf("https://auth.%s", cookie.Domain))
|
||||
}
|
||||
|
||||
mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil)
|
||||
|
||||
mock.Ctx.RequestCtx.QueryArgs().Set(queryArgRD, pairURI.AutheliaURI.String())
|
||||
mock.Ctx.Request.Header.Set("X-Forwarded-Method", method)
|
||||
mock.Ctx.Request.Header.Set(fasthttp.HeaderXForwardedProto, pairURI.TargetURI.Scheme)
|
||||
mock.Ctx.Request.Header.Set(fasthttp.HeaderXForwardedHost, pairURI.TargetURI.Host)
|
||||
mock.Ctx.Request.Header.Set("X-Forwarded-Uri", pairURI.TargetURI.Path)
|
||||
mock.Ctx.Request.Header.Set(fasthttp.HeaderAccept, "text/html; charset=utf-8")
|
||||
|
||||
authz.Handler(mock.Ctx)
|
||||
|
||||
switch method {
|
||||
case fasthttp.MethodGet, fasthttp.MethodOptions:
|
||||
assert.Equal(t, fasthttp.StatusFound, mock.Ctx.Response.StatusCode())
|
||||
default:
|
||||
assert.Equal(t, fasthttp.StatusSeeOther, mock.Ctx.Response.StatusCode())
|
||||
}
|
||||
|
||||
query := expected.Query()
|
||||
query.Set(queryArgRD, pairURI.TargetURI.String())
|
||||
query.Set(queryArgRM, method)
|
||||
expected.RawQuery = query.Encode()
|
||||
|
||||
assert.Equal(t, expected.String(), string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation)))
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s *LegacyAuthzSuite) TestShouldHandleAllMethodsMissingAutheliaURLDeny() {
|
||||
for _, method := range testRequestMethods {
|
||||
s.T().Run(fmt.Sprintf("Method%s", method), func(t *testing.T) {
|
||||
for _, targetURI := range []*url.URL{
|
||||
s.RequireParseRequestURI("https://bypass.example.com"),
|
||||
s.RequireParseRequestURI("https://bypass.example.com/subpath"),
|
||||
s.RequireParseRequestURI("https://bypass.example2.com"),
|
||||
s.RequireParseRequestURI("https://bypass.example2.com/subpath"),
|
||||
} {
|
||||
t.Run(targetURI.String(), func(t *testing.T) {
|
||||
authz := s.Builder().Build()
|
||||
|
||||
mock := mocks.NewMockAutheliaCtx(t)
|
||||
|
||||
defer mock.Close()
|
||||
|
||||
mock.Ctx.Request.Header.Set("X-Forwarded-Method", method)
|
||||
mock.Ctx.Request.Header.Set(fasthttp.HeaderXForwardedProto, targetURI.Scheme)
|
||||
mock.Ctx.Request.Header.Set(fasthttp.HeaderXForwardedHost, targetURI.Host)
|
||||
mock.Ctx.Request.Header.Set("X-Forwarded-Uri", targetURI.Path)
|
||||
mock.Ctx.Request.Header.Set(fasthttp.HeaderAccept, "text/html; charset=utf-8")
|
||||
|
||||
authz.Handler(mock.Ctx)
|
||||
|
||||
assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode())
|
||||
assert.Equal(t, "", string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation)))
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s *LegacyAuthzSuite) TestShouldHandleAllMethodsXHRDeny() {
|
||||
for _, method := range testRequestMethods {
|
||||
s.T().Run(fmt.Sprintf("Method%s", method), func(t *testing.T) {
|
||||
for xname, x := range testXHR {
|
||||
t.Run(xname, func(t *testing.T) {
|
||||
for _, pairURI := range []urlpair{
|
||||
{s.RequireParseRequestURI("https://one-factor.example.com/"), s.RequireParseRequestURI("https://auth.example.com/")},
|
||||
{s.RequireParseRequestURI("https://one-factor.example.com/subpath"), s.RequireParseRequestURI("https://auth.example.com/")},
|
||||
{s.RequireParseRequestURI("https://one-factor.example2.com/"), s.RequireParseRequestURI("https://auth.example2.com/")},
|
||||
{s.RequireParseRequestURI("https://one-factor.example2.com/subpath"), s.RequireParseRequestURI("https://auth.example2.com/")},
|
||||
} {
|
||||
t.Run(pairURI.TargetURI.String(), func(t *testing.T) {
|
||||
expected := s.RequireParseRequestURI(pairURI.AutheliaURI.String())
|
||||
|
||||
authz := s.Builder().Build()
|
||||
|
||||
mock := mocks.NewMockAutheliaCtx(t)
|
||||
|
||||
defer mock.Close()
|
||||
|
||||
for i, cookie := range mock.Ctx.Configuration.Session.Cookies {
|
||||
mock.Ctx.Configuration.Session.Cookies[i].AutheliaURL = s.RequireParseRequestURI(fmt.Sprintf("https://auth.%s", cookie.Domain))
|
||||
}
|
||||
|
||||
mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil)
|
||||
|
||||
mock.Ctx.RequestCtx.QueryArgs().Set(queryArgRD, pairURI.AutheliaURI.String())
|
||||
mock.Ctx.Request.Header.Set("X-Forwarded-Method", method)
|
||||
mock.Ctx.Request.Header.Set(fasthttp.HeaderXForwardedProto, pairURI.TargetURI.Scheme)
|
||||
mock.Ctx.Request.Header.Set(fasthttp.HeaderXForwardedHost, pairURI.TargetURI.Host)
|
||||
mock.Ctx.Request.Header.Set("X-Forwarded-Uri", pairURI.TargetURI.Path)
|
||||
|
||||
if x {
|
||||
mock.Ctx.Request.Header.Set(fasthttp.HeaderAccept, "text/html; charset=utf-8")
|
||||
mock.Ctx.Request.Header.Set(fasthttp.HeaderXRequestedWith, "XMLHttpRequest")
|
||||
}
|
||||
|
||||
authz.Handler(mock.Ctx)
|
||||
|
||||
assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode())
|
||||
|
||||
query := expected.Query()
|
||||
query.Set(queryArgRD, pairURI.TargetURI.String())
|
||||
query.Set(queryArgRM, method)
|
||||
expected.RawQuery = query.Encode()
|
||||
|
||||
assert.Equal(t, expected.String(), string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation)))
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s *LegacyAuthzSuite) TestShouldHandleInvalidMethodCharsDeny() {
|
||||
for _, method := range testRequestMethods {
|
||||
method += "z"
|
||||
|
||||
s.T().Run(fmt.Sprintf("Method%s", method), func(t *testing.T) {
|
||||
for _, targetURI := range []*url.URL{
|
||||
s.RequireParseRequestURI("https://bypass.example.com"),
|
||||
s.RequireParseRequestURI("https://bypass.example.com/subpath"),
|
||||
s.RequireParseRequestURI("https://bypass.example2.com"),
|
||||
s.RequireParseRequestURI("https://bypass.example2.com/subpath"),
|
||||
} {
|
||||
t.Run(targetURI.String(), func(t *testing.T) {
|
||||
authz := s.Builder().Build()
|
||||
|
||||
mock := mocks.NewMockAutheliaCtx(t)
|
||||
|
||||
defer mock.Close()
|
||||
|
||||
for i, cookie := range mock.Ctx.Configuration.Session.Cookies {
|
||||
mock.Ctx.Configuration.Session.Cookies[i].AutheliaURL = s.RequireParseRequestURI(fmt.Sprintf("https://auth.%s", cookie.Domain))
|
||||
}
|
||||
|
||||
mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil)
|
||||
|
||||
mock.Ctx.Request.Header.Set("X-Forwarded-Method", method)
|
||||
mock.Ctx.Request.Header.Set(fasthttp.HeaderXForwardedProto, targetURI.Scheme)
|
||||
mock.Ctx.Request.Header.Set(fasthttp.HeaderXForwardedHost, targetURI.Host)
|
||||
mock.Ctx.Request.Header.Set("X-Forwarded-Uri", targetURI.Path)
|
||||
mock.Ctx.Request.Header.Set(fasthttp.HeaderAccept, "text/html; charset=utf-8")
|
||||
|
||||
authz.Handler(mock.Ctx)
|
||||
|
||||
assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode())
|
||||
assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation))
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s *LegacyAuthzSuite) TestShouldHandleMissingHostDeny() {
|
||||
for _, method := range testRequestMethods {
|
||||
s.T().Run(fmt.Sprintf("Method%s", method), func(t *testing.T) {
|
||||
authz := s.Builder().Build()
|
||||
|
||||
mock := mocks.NewMockAutheliaCtx(t)
|
||||
|
||||
defer mock.Close()
|
||||
|
||||
for i, cookie := range mock.Ctx.Configuration.Session.Cookies {
|
||||
mock.Ctx.Configuration.Session.Cookies[i].AutheliaURL = s.RequireParseRequestURI(fmt.Sprintf("https://auth.%s", cookie.Domain))
|
||||
}
|
||||
|
||||
mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil)
|
||||
|
||||
mock.Ctx.Request.Header.Set("X-Forwarded-Method", method)
|
||||
mock.Ctx.Request.Header.Set(fasthttp.HeaderXForwardedProto, "https")
|
||||
mock.Ctx.Request.Header.Del(fasthttp.HeaderXForwardedHost)
|
||||
mock.Ctx.Request.Header.Set("X-Forwarded-Uri", "/")
|
||||
mock.Ctx.Request.Header.Set(fasthttp.HeaderAccept, "text/html; charset=utf-8")
|
||||
|
||||
authz.Handler(mock.Ctx)
|
||||
|
||||
assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode())
|
||||
assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s *LegacyAuthzSuite) TestShouldHandleAllMethodsAllow() {
|
||||
for _, method := range testRequestMethods {
|
||||
s.T().Run(fmt.Sprintf("Method%s", method), func(t *testing.T) {
|
||||
for _, targetURI := range []*url.URL{
|
||||
s.RequireParseRequestURI("https://bypass.example.com"),
|
||||
s.RequireParseRequestURI("https://bypass.example.com/subpath"),
|
||||
s.RequireParseRequestURI("https://bypass.example2.com"),
|
||||
s.RequireParseRequestURI("https://bypass.example2.com/subpath"),
|
||||
} {
|
||||
t.Run(targetURI.String(), func(t *testing.T) {
|
||||
authz := s.Builder().Build()
|
||||
|
||||
mock := mocks.NewMockAutheliaCtx(t)
|
||||
|
||||
defer mock.Close()
|
||||
|
||||
for i, cookie := range mock.Ctx.Configuration.Session.Cookies {
|
||||
mock.Ctx.Configuration.Session.Cookies[i].AutheliaURL = s.RequireParseRequestURI(fmt.Sprintf("https://auth.%s", cookie.Domain))
|
||||
}
|
||||
|
||||
mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil)
|
||||
|
||||
mock.Ctx.Request.Header.Set("X-Forwarded-Method", method)
|
||||
mock.Ctx.Request.Header.Set(fasthttp.HeaderXForwardedProto, targetURI.Scheme)
|
||||
mock.Ctx.Request.Header.Set(fasthttp.HeaderXForwardedHost, targetURI.Host)
|
||||
mock.Ctx.Request.Header.Set("X-Forwarded-Uri", targetURI.Path)
|
||||
mock.Ctx.Request.Header.Set(fasthttp.HeaderAccept, "text/html; charset=utf-8")
|
||||
|
||||
authz.Handler(mock.Ctx)
|
||||
|
||||
assert.Equal(t, fasthttp.StatusOK, mock.Ctx.Response.StatusCode())
|
||||
assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation))
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s *LegacyAuthzSuite) TestShouldHandleAllMethodsAllowXHR() {
|
||||
for _, method := range testRequestMethods {
|
||||
s.T().Run(fmt.Sprintf("Method%s", method), func(t *testing.T) {
|
||||
for _, targetURI := range []*url.URL{
|
||||
s.RequireParseRequestURI("https://bypass.example.com"),
|
||||
s.RequireParseRequestURI("https://bypass.example.com/subpath"),
|
||||
s.RequireParseRequestURI("https://bypass.example2.com"),
|
||||
s.RequireParseRequestURI("https://bypass.example2.com/subpath"),
|
||||
} {
|
||||
t.Run(targetURI.String(), func(t *testing.T) {
|
||||
authz := s.Builder().Build()
|
||||
|
||||
mock := mocks.NewMockAutheliaCtx(t)
|
||||
|
||||
defer mock.Close()
|
||||
|
||||
for i, cookie := range mock.Ctx.Configuration.Session.Cookies {
|
||||
mock.Ctx.Configuration.Session.Cookies[i].AutheliaURL = s.RequireParseRequestURI(fmt.Sprintf("https://auth.%s", cookie.Domain))
|
||||
}
|
||||
|
||||
mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil)
|
||||
|
||||
mock.Ctx.Request.Header.Set("X-Forwarded-Method", method)
|
||||
mock.Ctx.Request.Header.Set(fasthttp.HeaderXForwardedProto, targetURI.Scheme)
|
||||
mock.Ctx.Request.Header.Set(fasthttp.HeaderXForwardedHost, targetURI.Host)
|
||||
mock.Ctx.Request.Header.Set("X-Forwarded-Uri", targetURI.Path)
|
||||
mock.Ctx.Request.Header.Set(fasthttp.HeaderAccept, "text/html; charset=utf-8")
|
||||
|
||||
authz.Handler(mock.Ctx)
|
||||
|
||||
assert.Equal(t, fasthttp.StatusOK, mock.Ctx.Response.StatusCode())
|
||||
assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation))
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s *LegacyAuthzSuite) TestShouldHandleLegacyBasicAuth() { // TestShouldVerifyAuthBasicArgOk.
|
||||
authz := s.Builder().Build()
|
||||
|
||||
mock := mocks.NewMockAutheliaCtx(s.T())
|
||||
|
||||
defer mock.Close()
|
||||
|
||||
for i, cookie := range mock.Ctx.Configuration.Session.Cookies {
|
||||
mock.Ctx.Configuration.Session.Cookies[i].AutheliaURL = s.RequireParseRequestURI(fmt.Sprintf("https://auth.%s", cookie.Domain))
|
||||
}
|
||||
|
||||
mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil)
|
||||
|
||||
mock.Ctx.QueryArgs().Add("auth", "basic")
|
||||
mock.Ctx.Request.Header.Set("Authorization", "Basic am9objpwYXNzd29yZA==")
|
||||
mock.Ctx.Request.Header.Set("X-Original-URL", "https://one-factor.example.com")
|
||||
|
||||
gomock.InOrder(
|
||||
mock.UserProviderMock.EXPECT().
|
||||
CheckUserPassword(gomock.Eq("john"), gomock.Eq("password")).
|
||||
Return(true, nil),
|
||||
|
||||
mock.UserProviderMock.EXPECT().
|
||||
GetDetails(gomock.Eq("john")).
|
||||
Return(&authentication.UserDetails{
|
||||
Emails: []string{"john@example.com"},
|
||||
Groups: []string{"dev", "admins"},
|
||||
}, nil),
|
||||
)
|
||||
|
||||
authz.Handler(mock.Ctx)
|
||||
|
||||
s.Equal(fasthttp.StatusOK, mock.Ctx.Response.StatusCode())
|
||||
}
|
||||
|
||||
func (s *LegacyAuthzSuite) TestShouldHandleLegacyBasicAuthFailures() {
|
||||
testCases := []struct {
|
||||
name string
|
||||
setup func(mock *mocks.MockAutheliaCtx)
|
||||
}{
|
||||
{
|
||||
"HeaderAbsent", // TestShouldVerifyAuthBasicArgFailingNoHeader.
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"HeaderEmpty", // TestShouldVerifyAuthBasicArgFailingEmptyHeader.
|
||||
func(mock *mocks.MockAutheliaCtx) {
|
||||
mock.Ctx.Request.Header.Set("Authorization", "")
|
||||
},
|
||||
},
|
||||
{
|
||||
"HeaderIncorrect", // TestShouldVerifyAuthBasicArgFailingWrongHeader.
|
||||
func(mock *mocks.MockAutheliaCtx) {
|
||||
mock.Ctx.Request.Header.Set("Proxy-Authorization", "Basic am9objpwYXNzd29yZA==")
|
||||
},
|
||||
},
|
||||
{
|
||||
"IncorrectPassword", // TestShouldVerifyAuthBasicArgFailingWrongPassword.
|
||||
func(mock *mocks.MockAutheliaCtx) {
|
||||
mock.Ctx.Request.Header.Set("Authorization", "Basic am9objpwYXNzd29yZA==")
|
||||
|
||||
mock.UserProviderMock.EXPECT().
|
||||
CheckUserPassword(gomock.Eq("john"), gomock.Eq("password")).
|
||||
Return(false, fmt.Errorf("generic error"))
|
||||
},
|
||||
},
|
||||
{
|
||||
"NoAccess", // TestShouldVerifyAuthBasicArgFailingWrongPassword.
|
||||
func(mock *mocks.MockAutheliaCtx) {
|
||||
mock.Ctx.Request.Header.Set("Authorization", "Basic am9objpwYXNzd29yZA==")
|
||||
mock.Ctx.Request.Header.Set("X-Original-URL", "https://admin.example.com/")
|
||||
|
||||
gomock.InOrder(
|
||||
mock.UserProviderMock.EXPECT().
|
||||
CheckUserPassword(gomock.Eq("john"), gomock.Eq("password")).
|
||||
Return(true, nil),
|
||||
|
||||
mock.UserProviderMock.EXPECT().
|
||||
GetDetails(gomock.Eq("john")).
|
||||
Return(&authentication.UserDetails{
|
||||
Emails: []string{"john@example.com"},
|
||||
Groups: []string{"dev", "admin"},
|
||||
}, nil),
|
||||
)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
authz := s.Builder().Build()
|
||||
|
||||
for _, tc := range testCases {
|
||||
s.T().Run(tc.name, func(t *testing.T) {
|
||||
mock := mocks.NewMockAutheliaCtx(t)
|
||||
|
||||
defer mock.Close()
|
||||
|
||||
for i, cookie := range mock.Ctx.Configuration.Session.Cookies {
|
||||
mock.Ctx.Configuration.Session.Cookies[i].AutheliaURL = s.RequireParseRequestURI(fmt.Sprintf("https://auth.%s", cookie.Domain))
|
||||
}
|
||||
|
||||
mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil)
|
||||
|
||||
mock.Ctx.QueryArgs().Add("auth", "basic")
|
||||
mock.Ctx.Request.Header.Set("X-Original-URL", "https://one-factor.example.com")
|
||||
|
||||
if tc.setup != nil {
|
||||
tc.setup(mock)
|
||||
}
|
||||
|
||||
authz.Handler(mock.Ctx)
|
||||
|
||||
assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode())
|
||||
assert.Equal(t, "401 Unauthorized", string(mock.Ctx.Response.Body()))
|
||||
assert.Regexp(t, regexp.MustCompile("^Basic realm="), string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderWWWAuthenticate)))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s *LegacyAuthzSuite) TestShouldHandleInvalidURLForCVE202132637() {
|
||||
testCases := []struct {
|
||||
name string
|
||||
scheme, host []byte
|
||||
path string
|
||||
expected int
|
||||
}{
|
||||
// The first byte in the host sequence is the null byte. This should never respond with 200 OK.
|
||||
{"Should401UnauthorizedWithNullByte",
|
||||
[]byte("https"), []byte{0, 110, 111, 116, 45, 111, 110, 101, 45, 102, 97, 99, 116, 111, 114, 46, 101, 120, 97, 109, 112, 108, 101, 46, 99, 111, 109}, "/path-example",
|
||||
fasthttp.StatusUnauthorized,
|
||||
},
|
||||
{"Should200OkWithoutNullByte",
|
||||
[]byte("https"), []byte{110, 111, 116, 45, 111, 110, 101, 45, 102, 97, 99, 116, 111, 114, 46, 101, 120, 97, 109, 112, 108, 101, 46, 99, 111, 109}, "/path-example",
|
||||
fasthttp.StatusOK,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
s.T().Run(tc.name, func(t *testing.T) {
|
||||
for _, method := range testRequestMethods {
|
||||
t.Run(fmt.Sprintf("Method%s", method), func(t *testing.T) {
|
||||
authz := s.Builder().Build()
|
||||
|
||||
mock := mocks.NewMockAutheliaCtx(t)
|
||||
|
||||
defer mock.Close()
|
||||
|
||||
mock.Ctx.Configuration.AccessControl.DefaultPolicy = testBypass
|
||||
mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(&mock.Ctx.Configuration)
|
||||
|
||||
for i, cookie := range mock.Ctx.Configuration.Session.Cookies {
|
||||
mock.Ctx.Configuration.Session.Cookies[i].AutheliaURL = s.RequireParseRequestURI(fmt.Sprintf("https://auth.%s", cookie.Domain))
|
||||
}
|
||||
|
||||
mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil)
|
||||
|
||||
mock.Ctx.Request.Header.Set("X-Forwarded-Method", method)
|
||||
mock.Ctx.Request.Header.SetBytesKV([]byte(fasthttp.HeaderXForwardedProto), tc.scheme)
|
||||
mock.Ctx.Request.Header.SetBytesKV([]byte(fasthttp.HeaderXForwardedHost), tc.host)
|
||||
mock.Ctx.Request.Header.Set("X-Forwarded-Uri", tc.path)
|
||||
mock.Ctx.Request.Header.Set(fasthttp.HeaderAccept, "text/html; charset=utf-8")
|
||||
|
||||
authz.Handler(mock.Ctx)
|
||||
|
||||
assert.Equal(t, tc.expected, mock.Ctx.Response.StatusCode())
|
||||
assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation))
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func setRequestLegacy(ctx *middlewares.AutheliaCtx, method string, targetURI *url.URL, accept, xhr bool) {
|
||||
if method != "" {
|
||||
ctx.Request.Header.Set("X-Forwarded-Method", method)
|
||||
}
|
||||
|
||||
if targetURI != nil {
|
||||
ctx.Request.Header.Set(testXOriginalUrl, targetURI.String())
|
||||
}
|
||||
|
||||
setRequestXHRValues(ctx, accept, xhr)
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -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 ""
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -5,13 +5,22 @@ import (
|
|||
"net/url"
|
||||
|
||||
"github.com/authelia/authelia/v4/internal/middlewares"
|
||||
"github.com/authelia/authelia/v4/internal/session"
|
||||
)
|
||||
|
||||
// CheckSafeRedirectionPOST handler checking whether the redirection to a given URL provided in body is safe.
|
||||
func CheckSafeRedirectionPOST(ctx *middlewares.AutheliaCtx) {
|
||||
userSession := ctx.GetSession()
|
||||
var (
|
||||
s session.UserSession
|
||||
err error
|
||||
)
|
||||
|
||||
if userSession.IsAnonymous() {
|
||||
if s, err = ctx.GetSession(); err != nil {
|
||||
ctx.ReplyUnauthorized()
|
||||
return
|
||||
}
|
||||
|
||||
if s.IsAnonymous() {
|
||||
ctx.ReplyUnauthorized()
|
||||
return
|
||||
}
|
||||
|
@ -19,7 +28,6 @@ func CheckSafeRedirectionPOST(ctx *middlewares.AutheliaCtx) {
|
|||
var (
|
||||
bodyJSON checkURIWithinDomainRequestBody
|
||||
targetURI *url.URL
|
||||
err error
|
||||
)
|
||||
|
||||
if err = ctx.ParseBody(&bodyJSON); err != nil {
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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"},
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
"github.com/authelia/authelia/v4/internal/middlewares"
|
||||
"github.com/authelia/authelia/v4/internal/model"
|
||||
"github.com/authelia/authelia/v4/internal/regulation"
|
||||
"github.com/authelia/authelia/v4/internal/session"
|
||||
"github.com/authelia/authelia/v4/internal/storage"
|
||||
)
|
||||
|
||||
|
@ -34,12 +35,19 @@ var WebauthnIdentityFinish = middlewares.IdentityVerificationFinish(
|
|||
// SecondFactorWebauthnAttestationGET returns the attestation challenge from the server.
|
||||
func SecondFactorWebauthnAttestationGET(ctx *middlewares.AutheliaCtx, _ string) {
|
||||
var (
|
||||
w *webauthn.WebAuthn
|
||||
user *model.WebauthnUser
|
||||
err error
|
||||
w *webauthn.WebAuthn
|
||||
user *model.WebauthnUser
|
||||
userSession session.UserSession
|
||||
err error
|
||||
)
|
||||
|
||||
userSession := ctx.GetSession()
|
||||
if userSession, err = ctx.GetSession(); err != nil {
|
||||
ctx.Logger.WithError(err).Errorf("Error occurred retrieving session for %s attestation challenge", regulation.AuthTypeWebauthn)
|
||||
|
||||
respondUnauthorized(ctx, messageUnableToRegisterSecurityKey)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if w, err = newWebauthn(ctx); err != nil {
|
||||
ctx.Logger.Errorf("Unable to create %s attestation challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
|
||||
|
@ -96,12 +104,20 @@ func WebauthnAttestationPOST(ctx *middlewares.AutheliaCtx) {
|
|||
w *webauthn.WebAuthn
|
||||
user *model.WebauthnUser
|
||||
|
||||
userSession session.UserSession
|
||||
|
||||
attestationResponse *protocol.ParsedCredentialCreationData
|
||||
credential *webauthn.Credential
|
||||
postData *requestPostData
|
||||
)
|
||||
|
||||
userSession := ctx.GetSession()
|
||||
if userSession, err = ctx.GetSession(); err != nil {
|
||||
ctx.Logger.WithError(err).Errorf("Error occurred retrieving session for %s attestation response", regulation.AuthTypeWebauthn)
|
||||
|
||||
respondUnauthorized(ctx, messageUnableToRegisterSecurityKey)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if userSession.Webauthn == nil {
|
||||
ctx.Logger.Errorf("Webauthn session data is not present in order to handle attestation for user '%s'. This could indicate a user trying to POST to the wrong endpoint, or the session data is not present for the browser they used.", userSession.Username)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")))
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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")))
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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.")
|
||||
|
|
|
@ -1,515 +0,0 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/valyala/fasthttp"
|
||||
|
||||
"github.com/authelia/authelia/v4/internal/authentication"
|
||||
"github.com/authelia/authelia/v4/internal/authorization"
|
||||
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
||||
"github.com/authelia/authelia/v4/internal/middlewares"
|
||||
"github.com/authelia/authelia/v4/internal/session"
|
||||
"github.com/authelia/authelia/v4/internal/utils"
|
||||
)
|
||||
|
||||
func isSchemeWSS(url *url.URL) bool {
|
||||
return url.Scheme == "wss"
|
||||
}
|
||||
|
||||
// parseBasicAuth parses an HTTP Basic Authentication string.
|
||||
// "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==" returns ("Aladdin", "open sesame", true).
|
||||
func parseBasicAuth(header []byte, auth string) (username, password string, err error) {
|
||||
if !strings.HasPrefix(auth, authPrefix) {
|
||||
return "", "", fmt.Errorf("%s prefix not found in %s header", strings.Trim(authPrefix, " "), header)
|
||||
}
|
||||
|
||||
c, err := base64.StdEncoding.DecodeString(auth[len(authPrefix):])
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
cs := string(c)
|
||||
s := strings.IndexByte(cs, ':')
|
||||
|
||||
if s < 0 {
|
||||
return "", "", fmt.Errorf("format of %s header must be user:password", header)
|
||||
}
|
||||
|
||||
return cs[:s], cs[s+1:], nil
|
||||
}
|
||||
|
||||
// isTargetURLAuthorized check whether the given user is authorized to access the resource.
|
||||
func isTargetURLAuthorized(authorizer *authorization.Authorizer, targetURL *url.URL,
|
||||
username string, userGroups []string, clientIP net.IP, method []byte, authLevel authentication.Level) authorizationMatching {
|
||||
hasSubject, level := authorizer.GetRequiredLevel(
|
||||
authorization.Subject{
|
||||
Username: username,
|
||||
Groups: userGroups,
|
||||
IP: clientIP,
|
||||
},
|
||||
authorization.NewObjectRaw(targetURL, method))
|
||||
|
||||
switch {
|
||||
case level == authorization.Bypass:
|
||||
return Authorized
|
||||
case level == authorization.Denied && (username != "" || !hasSubject):
|
||||
// If the user is not anonymous, it means that we went through
|
||||
// all the rules related to that user and knowing who he is we can
|
||||
// deduce the access is forbidden
|
||||
// For anonymous users though, we check that the matched rule has no subject
|
||||
// if matched rule has not subject then this rule applies to all users including anonymous.
|
||||
return Forbidden
|
||||
case level == authorization.OneFactor && authLevel >= authentication.OneFactor,
|
||||
level == authorization.TwoFactor && authLevel >= authentication.TwoFactor:
|
||||
return Authorized
|
||||
}
|
||||
|
||||
return NotAuthorized
|
||||
}
|
||||
|
||||
// verifyBasicAuth verify that the provided username and password are correct and
|
||||
// that the user is authorized to target the resource.
|
||||
func verifyBasicAuth(ctx *middlewares.AutheliaCtx, header, auth []byte) (username, name string, groups, emails []string, authLevel authentication.Level, err error) {
|
||||
username, password, err := parseBasicAuth(header, string(auth))
|
||||
|
||||
if err != nil {
|
||||
return "", "", nil, nil, authentication.NotAuthenticated, fmt.Errorf("unable to parse content of %s header: %s", header, err)
|
||||
}
|
||||
|
||||
authenticated, err := ctx.Providers.UserProvider.CheckUserPassword(username, password)
|
||||
|
||||
if err != nil {
|
||||
return "", "", nil, nil, authentication.NotAuthenticated, fmt.Errorf("unable to check credentials extracted from %s header: %w", header, err)
|
||||
}
|
||||
|
||||
// If the user is not correctly authenticated, send a 401.
|
||||
if !authenticated {
|
||||
// Request Basic Authentication otherwise.
|
||||
return "", "", nil, nil, authentication.NotAuthenticated, fmt.Errorf("user %s is not authenticated", username)
|
||||
}
|
||||
|
||||
details, err := ctx.Providers.UserProvider.GetDetails(username)
|
||||
|
||||
if err != nil {
|
||||
return "", "", nil, nil, authentication.NotAuthenticated, fmt.Errorf("unable to retrieve details of user %s: %s", username, err)
|
||||
}
|
||||
|
||||
return username, details.DisplayName, details.Groups, details.Emails, authentication.OneFactor, nil
|
||||
}
|
||||
|
||||
// setForwardedHeaders set the forwarded User, Groups, Name and Email headers.
|
||||
func setForwardedHeaders(headers *fasthttp.ResponseHeader, username, name string, groups, emails []string) {
|
||||
if username != "" {
|
||||
headers.SetBytesK(headerRemoteUser, username)
|
||||
headers.SetBytesK(headerRemoteGroups, strings.Join(groups, ","))
|
||||
headers.SetBytesK(headerRemoteName, name)
|
||||
|
||||
if emails != nil {
|
||||
headers.SetBytesK(headerRemoteEmail, emails[0])
|
||||
} else {
|
||||
headers.SetBytesK(headerRemoteEmail, "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isSessionInactiveTooLong(ctx *middlewares.AutheliaCtx, userSession *session.UserSession, isUserAnonymous bool) (isInactiveTooLong bool) {
|
||||
domainSession, err := ctx.GetSessionProvider()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if userSession.KeepMeLoggedIn || isUserAnonymous || int64(domainSession.Config.Inactivity.Seconds()) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
isInactiveTooLong = time.Unix(userSession.LastActivity, 0).Add(domainSession.Config.Inactivity).Before(ctx.Clock.Now())
|
||||
|
||||
ctx.Logger.Tracef("Inactivity report for user '%s'. Current Time: %d, Last Activity: %d, Maximum Inactivity: %d.", userSession.Username, ctx.Clock.Now().Unix(), userSession.LastActivity, int(domainSession.Config.Inactivity.Seconds()))
|
||||
|
||||
return isInactiveTooLong
|
||||
}
|
||||
|
||||
// verifySessionCookie verifies if a user is identified by a cookie.
|
||||
func verifySessionCookie(ctx *middlewares.AutheliaCtx, targetURL *url.URL, userSession *session.UserSession, refreshProfile bool,
|
||||
refreshProfileInterval time.Duration) (username, name string, groups, emails []string, authLevel authentication.Level, err error) {
|
||||
// No username in the session means the user is anonymous.
|
||||
isUserAnonymous := userSession.IsAnonymous()
|
||||
|
||||
if isUserAnonymous && userSession.AuthenticationLevel != authentication.NotAuthenticated {
|
||||
return "", "", nil, nil, authentication.NotAuthenticated, fmt.Errorf("an anonymous user cannot be authenticated (this might be the sign of a security compromise)")
|
||||
}
|
||||
|
||||
if isSessionInactiveTooLong(ctx, userSession, isUserAnonymous) {
|
||||
// Destroy the session a new one will be regenerated on next request.
|
||||
if err = ctx.DestroySession(); err != nil {
|
||||
return "", "", nil, nil, authentication.NotAuthenticated, fmt.Errorf("unable to destroy session for user '%s' after the session has been inactive too long: %w", userSession.Username, err)
|
||||
}
|
||||
|
||||
ctx.Logger.Warnf("Session destroyed for user '%s' after exceeding configured session inactivity and not being marked as remembered", userSession.Username)
|
||||
|
||||
return "", "", nil, nil, authentication.NotAuthenticated, nil
|
||||
}
|
||||
|
||||
if err = verifySessionHasUpToDateProfile(ctx, targetURL, userSession, refreshProfile, refreshProfileInterval); err != nil {
|
||||
if err == authentication.ErrUserNotFound {
|
||||
if err = ctx.DestroySession(); err != nil {
|
||||
ctx.Logger.Errorf("Unable to destroy user session after provider refresh didn't find the user: %v", err)
|
||||
}
|
||||
|
||||
return userSession.Username, userSession.DisplayName, userSession.Groups, userSession.Emails, authentication.NotAuthenticated, err
|
||||
}
|
||||
|
||||
ctx.Logger.Errorf("Error occurred while attempting to update user details from LDAP: %v", err)
|
||||
|
||||
return "", "", nil, nil, authentication.NotAuthenticated, err
|
||||
}
|
||||
|
||||
return userSession.Username, userSession.DisplayName, userSession.Groups, userSession.Emails, userSession.AuthenticationLevel, nil
|
||||
}
|
||||
|
||||
func handleUnauthorized(ctx *middlewares.AutheliaCtx, targetURL fmt.Stringer, cookieDomain string, isBasicAuth bool, username string, method []byte) {
|
||||
var (
|
||||
statusCode int
|
||||
friendlyUsername string
|
||||
friendlyRequestMethod string
|
||||
)
|
||||
|
||||
switch username {
|
||||
case "":
|
||||
friendlyUsername = "<anonymous>"
|
||||
default:
|
||||
friendlyUsername = username
|
||||
}
|
||||
|
||||
if isBasicAuth {
|
||||
ctx.Logger.Infof("Access to %s is not authorized to user %s, sending 401 response with basic auth header", targetURL.String(), friendlyUsername)
|
||||
ctx.ReplyUnauthorized()
|
||||
ctx.Response.Header.Add("WWW-Authenticate", "Basic realm=\"Authentication required\"")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
rm := string(method)
|
||||
|
||||
switch rm {
|
||||
case "":
|
||||
friendlyRequestMethod = "unknown"
|
||||
default:
|
||||
friendlyRequestMethod = rm
|
||||
}
|
||||
|
||||
redirectionURL := ctxGetPortalURL(ctx)
|
||||
|
||||
if redirectionURL != nil {
|
||||
if !utils.IsURISafeRedirection(redirectionURL, cookieDomain) {
|
||||
ctx.Logger.Errorf("Configured Portal URL '%s' does not appear to be able to write cookies for the '%s' domain", redirectionURL, cookieDomain)
|
||||
|
||||
ctx.ReplyUnauthorized()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
qry := redirectionURL.Query()
|
||||
|
||||
qry.Set(queryArgRD, targetURL.String())
|
||||
|
||||
if rm != "" {
|
||||
qry.Set("rm", rm)
|
||||
}
|
||||
|
||||
redirectionURL.RawQuery = qry.Encode()
|
||||
}
|
||||
|
||||
switch {
|
||||
case ctx.IsXHR() || !ctx.AcceptsMIME("text/html") || redirectionURL == nil:
|
||||
statusCode = fasthttp.StatusUnauthorized
|
||||
default:
|
||||
switch rm {
|
||||
case fasthttp.MethodGet, fasthttp.MethodOptions, "":
|
||||
statusCode = fasthttp.StatusFound
|
||||
default:
|
||||
statusCode = fasthttp.StatusSeeOther
|
||||
}
|
||||
}
|
||||
|
||||
if redirectionURL != nil {
|
||||
ctx.Logger.Infof("Access to %s (method %s) is not authorized to user %s, responding with status code %d with location redirect to %s", targetURL.String(), friendlyRequestMethod, friendlyUsername, statusCode, redirectionURL)
|
||||
ctx.SpecialRedirect(redirectionURL.String(), statusCode)
|
||||
} else {
|
||||
ctx.Logger.Infof("Access to %s (method %s) is not authorized to user %s, responding with status code %d", targetURL.String(), friendlyRequestMethod, friendlyUsername, statusCode)
|
||||
ctx.ReplyUnauthorized()
|
||||
}
|
||||
}
|
||||
|
||||
func updateActivityTimestamp(ctx *middlewares.AutheliaCtx, isBasicAuth bool) error {
|
||||
if isBasicAuth {
|
||||
return nil
|
||||
}
|
||||
|
||||
userSession := ctx.GetSession()
|
||||
// We don't need to update the activity timestamp when user checked keep me logged in.
|
||||
if userSession.KeepMeLoggedIn {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Mark current activity.
|
||||
userSession.LastActivity = ctx.Clock.Now().Unix()
|
||||
|
||||
return ctx.SaveSession(userSession)
|
||||
}
|
||||
|
||||
// generateVerifySessionHasUpToDateProfileTraceLogs is used to generate trace logs only when trace logging is enabled.
|
||||
// The information calculated in this function is completely useless other than trace for now.
|
||||
func generateVerifySessionHasUpToDateProfileTraceLogs(ctx *middlewares.AutheliaCtx, userSession *session.UserSession,
|
||||
details *authentication.UserDetails) {
|
||||
groupsAdded, groupsRemoved := utils.StringSlicesDelta(userSession.Groups, details.Groups)
|
||||
emailsAdded, emailsRemoved := utils.StringSlicesDelta(userSession.Emails, details.Emails)
|
||||
nameDelta := userSession.DisplayName != details.DisplayName
|
||||
|
||||
// Check Groups.
|
||||
var groupsDelta []string
|
||||
if len(groupsAdded) != 0 {
|
||||
groupsDelta = append(groupsDelta, fmt.Sprintf("added: %s.", strings.Join(groupsAdded, ", ")))
|
||||
}
|
||||
|
||||
if len(groupsRemoved) != 0 {
|
||||
groupsDelta = append(groupsDelta, fmt.Sprintf("removed: %s.", strings.Join(groupsRemoved, ", ")))
|
||||
}
|
||||
|
||||
if len(groupsDelta) != 0 {
|
||||
ctx.Logger.Tracef("Updated groups detected for %s. %s", userSession.Username, strings.Join(groupsDelta, " "))
|
||||
} else {
|
||||
ctx.Logger.Tracef("No updated groups detected for %s", userSession.Username)
|
||||
}
|
||||
|
||||
// Check Emails.
|
||||
var emailsDelta []string
|
||||
if len(emailsAdded) != 0 {
|
||||
emailsDelta = append(emailsDelta, fmt.Sprintf("added: %s.", strings.Join(emailsAdded, ", ")))
|
||||
}
|
||||
|
||||
if len(emailsRemoved) != 0 {
|
||||
emailsDelta = append(emailsDelta, fmt.Sprintf("removed: %s.", strings.Join(emailsRemoved, ", ")))
|
||||
}
|
||||
|
||||
if len(emailsDelta) != 0 {
|
||||
ctx.Logger.Tracef("Updated emails detected for %s. %s", userSession.Username, strings.Join(emailsDelta, " "))
|
||||
} else {
|
||||
ctx.Logger.Tracef("No updated emails detected for %s", userSession.Username)
|
||||
}
|
||||
|
||||
// Check Name.
|
||||
if nameDelta {
|
||||
ctx.Logger.Tracef("Updated display name detected for %s. Added: %s. Removed: %s.", userSession.Username, details.DisplayName, userSession.DisplayName)
|
||||
} else {
|
||||
ctx.Logger.Tracef("No updated display name detected for %s", userSession.Username)
|
||||
}
|
||||
}
|
||||
|
||||
func verifySessionHasUpToDateProfile(ctx *middlewares.AutheliaCtx, targetURL *url.URL, userSession *session.UserSession,
|
||||
refreshProfile bool, refreshProfileInterval time.Duration) error {
|
||||
// TODO: Add a check for LDAP password changes based on a time format attribute.
|
||||
// See https://www.authelia.com/o/threatmodel#potential-future-guarantees
|
||||
ctx.Logger.Tracef("Checking if we need check the authentication backend for an updated profile for %s.", userSession.Username)
|
||||
|
||||
if !refreshProfile || userSession.IsAnonymous() || targetURL == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if refreshProfileInterval != schema.RefreshIntervalAlways && userSession.RefreshTTL.After(ctx.Clock.Now()) {
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx.Logger.Debugf("Checking the authentication backend for an updated profile for user %s", userSession.Username)
|
||||
details, err := ctx.Providers.UserProvider.GetDetails(userSession.Username)
|
||||
// Only update the session if we could get the new details.
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
emailsDiff := utils.IsStringSlicesDifferent(userSession.Emails, details.Emails)
|
||||
groupsDiff := utils.IsStringSlicesDifferent(userSession.Groups, details.Groups)
|
||||
nameDiff := userSession.DisplayName != details.DisplayName
|
||||
|
||||
if !groupsDiff && !emailsDiff && !nameDiff {
|
||||
ctx.Logger.Tracef("Updated profile not detected for %s.", userSession.Username)
|
||||
// Only update TTL if the user has an interval set.
|
||||
// We get to this check when there were no changes.
|
||||
// Also make sure to update the session even if no difference was found.
|
||||
// This is so that we don't check every subsequent request after this one.
|
||||
if refreshProfileInterval != schema.RefreshIntervalAlways {
|
||||
// Update RefreshTTL and save session if refresh is not set to always.
|
||||
userSession.RefreshTTL = ctx.Clock.Now().Add(refreshProfileInterval)
|
||||
return ctx.SaveSession(*userSession)
|
||||
}
|
||||
} else {
|
||||
ctx.Logger.Debugf("Updated profile detected for %s.", userSession.Username)
|
||||
if ctx.Configuration.Log.Level == "trace" {
|
||||
generateVerifySessionHasUpToDateProfileTraceLogs(ctx, userSession, details)
|
||||
}
|
||||
userSession.Emails = details.Emails
|
||||
userSession.Groups = details.Groups
|
||||
userSession.DisplayName = details.DisplayName
|
||||
|
||||
// Only update TTL if the user has a interval set.
|
||||
if refreshProfileInterval != schema.RefreshIntervalAlways {
|
||||
userSession.RefreshTTL = ctx.Clock.Now().Add(refreshProfileInterval)
|
||||
}
|
||||
// Return the result of save session if there were changes.
|
||||
return ctx.SaveSession(*userSession)
|
||||
}
|
||||
|
||||
// Return nil if disabled or if no changes and refresh interval set to always.
|
||||
return nil
|
||||
}
|
||||
|
||||
func getProfileRefreshSettings(cfg schema.AuthenticationBackend) (refresh bool, refreshInterval time.Duration) {
|
||||
if cfg.LDAP != nil {
|
||||
if cfg.RefreshInterval == schema.ProfileRefreshDisabled {
|
||||
refresh = false
|
||||
refreshInterval = 0
|
||||
} else {
|
||||
refresh = true
|
||||
|
||||
if cfg.RefreshInterval != schema.ProfileRefreshAlways {
|
||||
// Skip Error Check since validator checks it.
|
||||
refreshInterval, _ = utils.ParseDurationString(cfg.RefreshInterval)
|
||||
} else {
|
||||
refreshInterval = schema.RefreshIntervalAlways
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return refresh, refreshInterval
|
||||
}
|
||||
|
||||
func verifyAuth(ctx *middlewares.AutheliaCtx, targetURL *url.URL, refreshProfile bool, refreshProfileInterval time.Duration) (isBasicAuth bool, username, name string, groups, emails []string, authLevel authentication.Level, err error) {
|
||||
authHeader := headerProxyAuthorization
|
||||
if bytes.Equal(ctx.QueryArgs().Peek("auth"), []byte("basic")) {
|
||||
authHeader = headerAuthorization
|
||||
isBasicAuth = true
|
||||
}
|
||||
|
||||
authValue := ctx.Request.Header.PeekBytes(authHeader)
|
||||
if authValue != nil {
|
||||
isBasicAuth = true
|
||||
} else if isBasicAuth {
|
||||
return isBasicAuth, username, name, groups, emails, authLevel, fmt.Errorf("basic auth requested via query arg, but no value provided via %s header", authHeader)
|
||||
}
|
||||
|
||||
if isBasicAuth {
|
||||
username, name, groups, emails, authLevel, err = verifyBasicAuth(ctx, authHeader, authValue)
|
||||
|
||||
return isBasicAuth, username, name, groups, emails, authLevel, err
|
||||
}
|
||||
|
||||
userSession := ctx.GetSession()
|
||||
|
||||
if username, name, groups, emails, authLevel, err = verifySessionCookie(ctx, targetURL, &userSession, refreshProfile, refreshProfileInterval); err != nil {
|
||||
return isBasicAuth, username, name, groups, emails, authLevel, err
|
||||
}
|
||||
|
||||
sessionUsername := ctx.Request.Header.PeekBytes(headerSessionUsername)
|
||||
if sessionUsername != nil && !strings.EqualFold(string(sessionUsername), username) {
|
||||
ctx.Logger.Warnf("Possible cookie hijack or attempt to bypass security detected destroying the session and sending 401 response")
|
||||
|
||||
if err = ctx.DestroySession(); err != nil {
|
||||
ctx.Logger.Errorf("Unable to destroy user session after handler could not match them to their %s header: %s", headerSessionUsername, err)
|
||||
}
|
||||
|
||||
return isBasicAuth, username, name, groups, emails, authLevel, fmt.Errorf("could not match user %s to their %s header with a value of %s when visiting %s", username, headerSessionUsername, sessionUsername, targetURL.String())
|
||||
}
|
||||
|
||||
return isBasicAuth, username, name, groups, emails, authLevel, err
|
||||
}
|
||||
|
||||
// VerifyGET returns the handler verifying if a request is allowed to go through.
|
||||
func VerifyGET(cfg schema.AuthenticationBackend) middlewares.RequestHandler {
|
||||
refreshProfile, refreshProfileInterval := getProfileRefreshSettings(cfg)
|
||||
|
||||
return func(ctx *middlewares.AutheliaCtx) {
|
||||
ctx.Logger.Tracef("Headers=%s", ctx.Request.Header.String())
|
||||
targetURL, err := ctx.GetOriginalURL()
|
||||
|
||||
if err != nil {
|
||||
ctx.Logger.Errorf("Unable to parse target URL: %s", err)
|
||||
ctx.ReplyUnauthorized()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if !utils.IsURISecure(targetURL) {
|
||||
ctx.Logger.Errorf("Scheme of target URL %s must be secure since cookies are "+
|
||||
"only transported over a secure connection for security reasons", targetURL.String())
|
||||
ctx.ReplyUnauthorized()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
cookieDomain := ctx.GetTargetURICookieDomain(targetURL)
|
||||
|
||||
if cookieDomain == "" {
|
||||
l := len(ctx.Configuration.Session.Cookies)
|
||||
|
||||
if l == 1 {
|
||||
ctx.Logger.Errorf("Target URL '%s' was not detected as a match to the '%s' session cookie domain",
|
||||
targetURL.String(), ctx.Configuration.Session.Cookies[0].Domain)
|
||||
} else {
|
||||
domains := make([]string, 0, len(ctx.Configuration.Session.Cookies))
|
||||
|
||||
for i, domain := range ctx.Configuration.Session.Cookies {
|
||||
domains[i] = domain.Domain
|
||||
}
|
||||
|
||||
ctx.Logger.Errorf("Target URL '%s' was not detected as a match to any of the '%s' session cookie domains",
|
||||
targetURL.String(), strings.Join(domains, "', '"))
|
||||
}
|
||||
|
||||
ctx.ReplyUnauthorized()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Logger.Debugf("Target URL '%s' was detected as a match to the '%s' session cookie domain", targetURL.String(), cookieDomain)
|
||||
|
||||
method := ctx.XForwardedMethod()
|
||||
isBasicAuth, username, name, groups, emails, authLevel, err := verifyAuth(ctx, targetURL, refreshProfile, refreshProfileInterval)
|
||||
|
||||
if err != nil {
|
||||
ctx.Logger.Errorf("Error caught when verifying user authorization: %s", err)
|
||||
|
||||
if err = updateActivityTimestamp(ctx, isBasicAuth); err != nil {
|
||||
ctx.Error(fmt.Errorf("unable to update last activity: %s", err), messageOperationFailed)
|
||||
return
|
||||
}
|
||||
|
||||
handleUnauthorized(ctx, targetURL, cookieDomain, isBasicAuth, username, method)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
authorized := isTargetURLAuthorized(ctx.Providers.Authorizer, targetURL, username,
|
||||
groups, ctx.RemoteIP(), method, authLevel)
|
||||
|
||||
switch authorized {
|
||||
case Forbidden:
|
||||
ctx.Logger.Infof("Access to %s is forbidden to user %s", targetURL.String(), username)
|
||||
ctx.ReplyForbidden()
|
||||
case NotAuthorized:
|
||||
handleUnauthorized(ctx, targetURL, cookieDomain, isBasicAuth, username, method)
|
||||
case Authorized:
|
||||
setForwardedHeaders(&ctx.Response.Header, username, name, groups, emails)
|
||||
}
|
||||
|
||||
if err = updateActivityTimestamp(ctx, isBasicAuth); err != nil {
|
||||
ctx.Error(fmt.Errorf("unable to update last activity: %s", err), messageOperationFailed)
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -11,6 +11,7 @@ import (
|
|||
"github.com/authelia/authelia/v4/internal/middlewares"
|
||||
"github.com/authelia/authelia/v4/internal/model"
|
||||
"github.com/authelia/authelia/v4/internal/regulation"
|
||||
"github.com/authelia/authelia/v4/internal/session"
|
||||
"github.com/authelia/authelia/v4/internal/storage"
|
||||
)
|
||||
|
||||
|
@ -34,9 +35,20 @@ func getWebauthnDeviceIDFromContext(ctx *middlewares.AutheliaCtx) (int, error) {
|
|||
|
||||
// WebauthnDevicesGET returns all devices registered for the current user.
|
||||
func WebauthnDevicesGET(ctx *middlewares.AutheliaCtx) {
|
||||
s := ctx.GetSession()
|
||||
var (
|
||||
userSession session.UserSession
|
||||
err error
|
||||
)
|
||||
|
||||
devices, err := ctx.Providers.StorageProvider.LoadWebauthnDevicesByUsername(ctx, s.Username)
|
||||
if userSession, err = ctx.GetSession(); err != nil {
|
||||
ctx.Logger.WithError(err).Error("Error occurred retrieving user session")
|
||||
|
||||
ctx.ReplyForbidden()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
devices, err := ctx.Providers.StorageProvider.LoadWebauthnDevicesByUsername(ctx, userSession.Username)
|
||||
|
||||
if err != nil && err != storage.ErrNoWebauthnDevice {
|
||||
ctx.Error(err, messageOperationFailed)
|
||||
|
@ -54,15 +66,23 @@ func WebauthnDevicePUT(ctx *middlewares.AutheliaCtx) {
|
|||
var (
|
||||
bodyJSON bodyEditWebauthnDeviceRequest
|
||||
|
||||
id int
|
||||
device *model.WebauthnDevice
|
||||
err error
|
||||
id int
|
||||
device *model.WebauthnDevice
|
||||
userSession session.UserSession
|
||||
|
||||
err error
|
||||
)
|
||||
|
||||
s := ctx.GetSession()
|
||||
if userSession, err = ctx.GetSession(); err != nil {
|
||||
ctx.Logger.WithError(err).Error("Error occurred retrieving user session")
|
||||
|
||||
ctx.ReplyForbidden()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if err = json.Unmarshal(ctx.PostBody(), &bodyJSON); err != nil {
|
||||
ctx.Logger.Errorf("Unable to parse %s update request data for user '%s': %+v", regulation.AuthTypeWebauthn, s.Username, err)
|
||||
ctx.Logger.Errorf("Unable to parse %s update request data for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
|
||||
|
||||
ctx.SetStatusCode(fasthttp.StatusBadRequest)
|
||||
ctx.Error(err, messageOperationFailed)
|
||||
|
@ -79,12 +99,12 @@ func WebauthnDevicePUT(ctx *middlewares.AutheliaCtx) {
|
|||
return
|
||||
}
|
||||
|
||||
if device.Username != s.Username {
|
||||
ctx.Error(fmt.Errorf("user '%s' tried to delete device with id '%d' which belongs to '%s", s.Username, device.ID, device.Username), messageOperationFailed)
|
||||
if device.Username != userSession.Username {
|
||||
ctx.Error(fmt.Errorf("user '%s' tried to delete device with id '%d' which belongs to '%s", userSession.Username, device.ID, device.Username), messageOperationFailed)
|
||||
return
|
||||
}
|
||||
|
||||
if err = ctx.Providers.StorageProvider.UpdateWebauthnDeviceDescription(ctx, s.Username, id, bodyJSON.Description); err != nil {
|
||||
if err = ctx.Providers.StorageProvider.UpdateWebauthnDeviceDescription(ctx, userSession.Username, id, bodyJSON.Description); err != nil {
|
||||
ctx.Error(err, messageOperationFailed)
|
||||
return
|
||||
}
|
||||
|
@ -93,9 +113,10 @@ func WebauthnDevicePUT(ctx *middlewares.AutheliaCtx) {
|
|||
// WebauthnDeviceDELETE deletes a specific device for the current user.
|
||||
func WebauthnDeviceDELETE(ctx *middlewares.AutheliaCtx) {
|
||||
var (
|
||||
id int
|
||||
device *model.WebauthnDevice
|
||||
err error
|
||||
id int
|
||||
device *model.WebauthnDevice
|
||||
userSession session.UserSession
|
||||
err error
|
||||
)
|
||||
|
||||
if id, err = getWebauthnDeviceIDFromContext(ctx); err != nil {
|
||||
|
@ -107,10 +128,16 @@ func WebauthnDeviceDELETE(ctx *middlewares.AutheliaCtx) {
|
|||
return
|
||||
}
|
||||
|
||||
s := ctx.GetSession()
|
||||
if userSession, err = ctx.GetSession(); err != nil {
|
||||
ctx.Logger.WithError(err).Error("Error occurred retrieving user session")
|
||||
|
||||
if device.Username != s.Username {
|
||||
ctx.Error(fmt.Errorf("user '%s' tried to delete device with id '%d' which belongs to '%s", s.Username, device.ID, device.Username), messageOperationFailed)
|
||||
ctx.ReplyForbidden()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if device.Username != userSession.Username {
|
||||
ctx.Error(fmt.Errorf("user '%s' tried to delete device with id '%d' which belongs to '%s", userSession.Username, device.ID, device.Username), messageOperationFailed)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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"`
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -15,6 +15,6 @@ type Provider interface {
|
|||
// Recorder of metrics.
|
||||
type Recorder interface {
|
||||
RecordRequest(statusCode, requestMethod string, elapsed time.Duration)
|
||||
RecordVerifyRequest(statusCode string)
|
||||
RecordAuthz(statusCode string)
|
||||
RecordAuthenticationDuration(success bool, elapsed time.Duration)
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue