diff --git a/api/openapi.yml b/api/openapi.yml index 1468e1bdd..ad7115be9 100644 --- a/api/openapi.yml +++ b/api/openapi.yml @@ -111,6 +111,12 @@ paths: application/json: schema: $ref: '#/components/schemas/handlers.StateResponse' + {{- $redir := "https://auth.example.com/?rd=https%3A%2F%2Fexample.com&rm=GET" }} + {{- if .Domain }} + {{- $redir = printf "%s?rd=%s&rm=GET" .BaseURL (urlquery (printf "https://%s" .Domain)) }} + {{- else if .BaseURL }} + {{- $redir = printf "%s?rd=%s&rm=GET" .BaseURL (urlquery .BaseURL) }} + {{- end }} {{- range $name, $config := .EndpointsAuthz }} {{- $uri := printf "/api/authz/%s" $name }} {{- if (eq $name "legacy") }}{{ $uri = "/api/verify" }}{{ end }} @@ -188,8 +194,37 @@ paths: schema: type: string example: admin,devs + set-cookie: + description: Sets a new cookie value + schema: + type: string + "302": + description: Found + headers: + location: + description: Redirect Location for user authorization + example: {{ $redir }} + set-cookie: + description: Sets a new cookie value + schema: + type: string + "303": + description: See Other + headers: + location: + description: Redirect Location for user authorization + example: {{ $redir }} + set-cookie: + description: Sets a new cookie value + schema: + type: string "401": description: Unauthorized + headers: + set-cookie: + description: Sets a new cookie value + schema: + type: string security: - authelia_auth: [] {{- end }} @@ -232,6 +267,32 @@ paths: schema: type: string example: admin,devs + set-cookie: + description: Sets a new cookie value + schema: + type: string + "302": + description: Found + headers: + location: + description: Redirect Location for user authorization + example: {{ $redir }} + set-cookie: + description: Sets a new cookie value + schema: + type: string + "303": + description: See Other + headers: + location: + description: Redirect Location for user authorization + example: {{ $redir }} + set-cookie: + description: Sets a new cookie value + schema: + type: string + "400": + description: Bad Request "401": description: Unauthorized security: @@ -275,6 +336,32 @@ paths: schema: type: string example: admin,devs + set-cookie: + description: Sets a new cookie value + schema: + type: string + "302": + description: Found + headers: + location: + description: Redirect Location for user authorization + example: {{ $redir }} + set-cookie: + description: Sets a new cookie value + schema: + type: string + "303": + description: See Other + headers: + location: + description: Redirect Location for user authorization + example: {{ $redir }} + set-cookie: + description: Sets a new cookie value + schema: + type: string + "400": + description: Bad Request "401": description: Unauthorized security: @@ -316,8 +403,22 @@ paths: schema: type: string example: admin,devs + set-cookie: + description: Sets a new cookie value + schema: + type: string + "400": + description: Bad Request "401": description: Unauthorized + headers: + location: + description: Redirect Location for user authorization + example: {{ $redir }} + set-cookie: + description: Sets a new cookie value + schema: + type: string security: - authelia_auth: [] {{- end }} @@ -1904,7 +2005,7 @@ components: properties: appidExclude: type: string - example: https://auth.example.com + example: {{ .BaseURL }} webauthn.PublicKeyCredentialRequestOptions: type: object properties: @@ -1939,7 +2040,7 @@ components: properties: appid: type: string - example: https://auth.example.com + example: {{ .BaseURL }} webauthn.Transports: type: object properties: diff --git a/docs/content/en/integration/proxies/caddy.md b/docs/content/en/integration/proxies/caddy.md index e9b13b422..51b00303c 100644 --- a/docs/content/en/integration/proxies/caddy.md +++ b/docs/content/en/integration/proxies/caddy.md @@ -87,6 +87,23 @@ following are the assumptions we make: * This domain and the subdomains will have to be adapted in all examples to match your specific domains unless you're just testing or you want ot use that specific domain +## Implementation + +[Caddy] utilizes the [ForwardAuth](../../reference/guides/proxy-authorization.md#forwardauth) Authz implementation. The +associated [Metadata](../../reference/guides/proxy-authorization.md#forwardauth-metadata) should be considered required. + +The examples below assume you are using the default +[Authz Endpoints Configuration](../../configuration/miscellaneous/server-endpoints-authz.md) or one similar to the +following minimal configuration: + +```yaml +server: + endpoints: + authz: + forward-auth: + implementation: ForwardAuth +``` + ## Configuration Below you will find commented examples of the following configuration: diff --git a/docs/content/en/integration/proxies/envoy.md b/docs/content/en/integration/proxies/envoy.md index 2e4245930..0d45b7cea 100644 --- a/docs/content/en/integration/proxies/envoy.md +++ b/docs/content/en/integration/proxies/envoy.md @@ -61,6 +61,23 @@ following are the assumptions we make: * This domain and the subdomains will have to be adapted in all examples to match your specific domains unless you're just testing or you want ot use that specific domain +## Implementation + +[Envoy] utilizes the [ExtAuthz](../../reference/guides/proxy-authorization.md#extauthz) Authz implementation. The +associated [Metadata](../../reference/guides/proxy-authorization.md#extauthz-metadata) should be considered required. + +The examples below assume you are using the default +[Authz Endpoints Configuration](../../configuration/miscellaneous/server-endpoints-authz.md) or one similar to the +following minimal configuration: + +```yaml +server: + endpoints: + authz: + ext-authz: + implementation: ExtAuthz +``` + ## Configuration Below you will find commented examples of the following configuration: diff --git a/docs/content/en/integration/proxies/haproxy.md b/docs/content/en/integration/proxies/haproxy.md index a8ffee72c..12b9f98a1 100644 --- a/docs/content/en/integration/proxies/haproxy.md +++ b/docs/content/en/integration/proxies/haproxy.md @@ -90,22 +90,37 @@ following are the assumptions we make: * This domain and the subdomains will have to be adapted in all examples to match your specific domains unless you're just testing or you want ot use that specific domain +## Implementation + +[HAProxy] utilizes the [ForwardAuth](../../reference/guides/proxy-authorization.md#forwardauth) Authz implementation. The +associated [Metadata](../../reference/guides/proxy-authorization.md#forwardauth-metadata) should be considered required. + +The examples below assume you are using the default +[Authz Endpoints Configuration](../../configuration/miscellaneous/server-endpoints-authz.md) or one similar to the +following minimal configuration: + +```yaml +server: + endpoints: + authz: + forward-auth: + implementation: ForwardAuth +``` + ## Configuration Below you will find commented examples of the following configuration: * Authelia Portal -* Protected Endpoint (Nextcloud) -* Protected Endpoint with `Authorization` header for basic authentication (Heimdall) +* Protected Endpoints (Nextcloud) With this configuration you can protect your virtual hosts with Authelia, by following the steps below: -1. Add host(s) to the `protected-frontends` or `protected-frontends-basic` ACLs to support protection with Authelia. -You can separate each subdomain with a `|` in the regex, for example: +1. Add host(s) to the `protected-frontends` ACLs to support protection with Authelia. You can separate each subdomain + with a `|` in the regex, for example: ```text acl protected-frontends hdr(host) -m reg -i ^(?i)(jenkins|nextcloud|phpmyadmin)\.example\.com - acl protected-frontends-basic hdr(host) -m reg -i ^(?i)(heimdall)\.example\.com ``` 2. Add host ACL(s) in the form of `host-service`, this will be utilised to route to the correct @@ -190,46 +205,24 @@ frontend fe_http option forwardfor # Host ACLs - acl protected-frontends hdr(host) -m reg -i ^(?i)(nextcloud)\.example\.com - acl protected-frontends-basic hdr(host) -m reg -i ^(?i)(heimdall)\.example\.com - acl host-authelia hdr(host) -i auth.example.com - acl host-nextcloud hdr(host) -i nextcloud.example.com - acl host-heimdall hdr(host) -i heimdall.example.com - - # This is required if utilising basic auth with /api/verify?auth=basic - http-request set-var(txn.host) hdr(Host) + acl protected-frontends hdr(Host) -m reg -i ^(?i)(nextcloud|heimdall)\.example\.com + acl host-authelia hdr(Host) -i auth.example.com + acl host-nextcloud hdr(Host) -i nextcloud.example.com + acl host-heimdall hdr(Host) -i heimdall.example.com http-request set-var(req.scheme) str(https) if { ssl_fc } http-request set-var(req.scheme) str(http) if !{ ssl_fc } http-request set-var(req.questionmark) str(?) if { query -m found } - # These are optional if you wish to use the Methods rule in the access_control section. - #http-request set-var(req.method) str(CONNECT) if { method CONNECT } - #http-request set-var(req.method) str(GET) if { method GET } - #http-request set-var(req.method) str(HEAD) if { method HEAD } - #http-request set-var(req.method) str(OPTIONS) if { method OPTIONS } - #http-request set-var(req.method) str(POST) if { method POST } - #http-request set-var(req.method) str(TRACE) if { method TRACE } - #http-request set-var(req.method) str(PUT) if { method PUT } - #http-request set-var(req.method) str(PATCH) if { method PATCH } - #http-request set-var(req.method) str(DELETE) if { method DELETE } - #http-request set-header X-Forwarded-Method %[var(req.method)] - - # Required headers - http-request set-header X-Real-IP %[src] - 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] + # Required Headers + http-request set-header X-Forwarded-Method %[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] # Protect endpoints with haproxy-auth-request and Authelia - 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 - - # Redirect protected-frontends to Authelia if not authenticated - http-request redirect location https://auth.example.com/?rd=%[var(req.scheme)]://%[base]%[var(req.questionmark)]%[query] if protected-frontends !{ var(txn.auth_response_successful) -m bool } - # Send 401 and pass `WWW-Authenticate` header on protected-frontend-basic if not pre-authenticated - http-request set-var(txn.auth) var(req.auth_response_header.www_authenticate) if protected-frontends-basic !{ var(txn.auth_response_successful) -m bool } - http-response deny deny_status 401 hdr WWW-Authenticate %[var(txn.auth)] if { var(txn.host) -m reg -i ^(?i)(heimdall)\.example\.com } !{ var(txn.auth_response_successful) -m bool } + http-request lua.auth-intercept be_authelia /api/authz/forward-auth HEAD * authorization,proxy-authorization,remote_user,remote-user,remote-groups,remote-name,remote-email - if protected-frontends + http-request redirect location %[var(txn.auth_response_location)] if protected-frontends !{ var(txn.auth_response_successful) -m bool } # Authelia backend route use_backend be_authelia if host-authelia @@ -242,24 +235,6 @@ backend be_authelia server authelia authelia:9091 backend be_nextcloud - ## Pass the special authorization response headers to the protected application. - acl authorization_exist var(req.auth_response_header.authorization) -m found - acl proxy_authorization_exist var(req.auth_response_header.proxy_authorization) -m found - - http-request set-header Authorization %[var(req.auth_response_header.authorization)] if authorization_exist - http-request set-header Proxy-Authorization %[var(req.auth_response_header.proxy_authorization)] if proxy_authorization_exist - - ## Pass the special metadata response headers to the protected application. - acl remote_user_exist var(req.auth_response_header.remote_user) -m found - acl remote_groups_exist var(req.auth_response_header.remote_groups) -m found - acl remote_name_exist var(req.auth_response_header.remote_name) -m found - acl remote_email_exist var(req.auth_response_header.remote_email) -m found - - http-request set-header Remote-User %[var(req.auth_response_header.remote_user)] if remote_user_exist - http-request set-header Remote-Groups %[var(req.auth_response_header.remote_groups)] if remote_groups_exist - http-request set-header Remote-Name %[var(req.auth_response_header.remote_name)] if remote_name_exist - http-request set-header Remote-Email %[var(req.auth_response_header.remote_email)] if remote_email_exist - ## Pass the Set-Cookie response headers to the user. acl set_cookie_exist var(req.auth_response_header.set_cookie) -m found http-response set-header Set-Cookie %[var(req.auth_response_header.set_cookie)] if set_cookie_exist @@ -267,24 +242,6 @@ backend be_nextcloud server nextcloud nextcloud:443 ssl verify none backend be_heimdall - ## Pass the special authorization response headers to the protected application. - acl authorization_exist var(req.auth_response_header.authorization) -m found - acl proxy_authorization_exist var(req.auth_response_header.proxy_authorization) -m found - - http-request set-header Authorization %[var(req.auth_response_header.authorization)] if authorization_exist - http-request set-header Proxy-Authorization %[var(req.auth_response_header.proxy_authorization)] if proxy_authorization_exist - - ## Pass the special metadata response headers to the protected application. - acl remote_user_exist var(req.auth_response_header.remote_user) -m found - acl remote_groups_exist var(req.auth_response_header.remote_groups) -m found - acl remote_name_exist var(req.auth_response_header.remote_name) -m found - acl remote_email_exist var(req.auth_response_header.remote_email) -m found - - http-request set-header Remote-User %[var(req.auth_response_header.remote_user)] if remote_user_exist - http-request set-header Remote-Groups %[var(req.auth_response_header.remote_groups)] if remote_groups_exist - http-request set-header Remote-Name %[var(req.auth_response_header.remote_name)] if remote_name_exist - http-request set-header Remote-Email %[var(req.auth_response_header.remote_email)] if remote_email_exist - ## Pass the Set-Cookie response headers to the user. acl set_cookie_exist var(req.auth_response_header.set_cookie) -m found http-response set-header Set-Cookie %[var(req.auth_response_header.set_cookie)] if set_cookie_exist @@ -311,47 +268,37 @@ defaults frontend fe_http bind *:443 ssl crt /usr/local/etc/haproxy/haproxy.pem - # Host ACLs - acl protected-frontends hdr(host) -m reg -i ^(?i)(nextcloud)\.example\.com - acl protected-frontends-basic hdr(host) -m reg -i ^(?i)(heimdall)\.example\.com - acl host-authelia hdr(host) -i auth.example.com - acl host-nextcloud hdr(host) -i nextcloud.example.com - acl host-heimdall hdr(host) -i heimdall.example.com + ## Trusted Proxies. + http-request del-header X-Forwarded-For - # This is required if utilising basic auth with /api/verify?auth=basic - http-request set-var(txn.host) hdr(Host) + ## Comment the above directive and the two directives below to enable the trusted proxies ACL. + # acl src-trusted_proxies src -f trusted_proxies.src.acl + # http-request del-header X-Forwarded-For if !src-trusted_proxies + + ## Ensure X-Forwarded-For is set for the auth request. + acl hdr-xff_exists req.hdr(X-Forwarded-For) -m found + http-request set-header X-Forwarded-For %[src] if !hdr-xff_exists + option forwardfor + + # Host ACLs + acl protected-frontends hdr(Host) -m reg -i ^(?i)(nextcloud|heimdall)\.example\.com + acl host-authelia hdr(Host) -i auth.example.com + acl host-nextcloud hdr(Host) -i nextcloud.example.com + acl host-heimdall hdr(Host) -i heimdall.example.com http-request set-var(req.scheme) str(https) if { ssl_fc } http-request set-var(req.scheme) str(http) if !{ ssl_fc } http-request set-var(req.questionmark) str(?) if { query -m found } - # These are optional if you wish to use the Methods rule in the access_control section. - #http-request set-var(req.method) str(CONNECT) if { method CONNECT } - #http-request set-var(req.method) str(GET) if { method GET } - #http-request set-var(req.method) str(HEAD) if { method HEAD } - #http-request set-var(req.method) str(OPTIONS) if { method OPTIONS } - #http-request set-var(req.method) str(POST) if { method POST } - #http-request set-var(req.method) str(TRACE) if { method TRACE } - #http-request set-var(req.method) str(PUT) if { method PUT } - #http-request set-var(req.method) str(PATCH) if { method PATCH } - #http-request set-var(req.method) str(DELETE) if { method DELETE } - #http-request set-header X-Forwarded-Method %[var(req.method)] - - # Required headers - http-request set-header X-Real-IP %[src] - 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] + # Required Headers + http-request set-header X-Forwarded-Method %[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] # Protect endpoints with haproxy-auth-request and Authelia - 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 - - # Redirect protected-frontends to Authelia if not authenticated - http-request redirect location https://auth.example.com/?rd=%[var(req.scheme)]://%[base]%[var(req.questionmark)]%[query] if protected-frontends !{ var(txn.auth_response_successful) -m bool } - # Send 401 and pass `WWW-Authenticate` header on protected-frontend-basic if not pre-authenticated - http-request set-var(txn.auth) var(req.auth_response_header.www_authenticate) if protected-frontends-basic !{ var(txn.auth_response_successful) -m bool } - http-response deny deny_status 401 hdr WWW-Authenticate %[var(txn.auth)] if { var(txn.host) -m reg -i ^(?i)(heimdall)\.example\.com } !{ var(txn.auth_response_successful) -m bool } + http-request lua.auth-intercept be_authelia_proxy /api/authz/forward-auth HEAD * authorization,proxy-authorization,remote_user,remote-user,remote-groups,remote-name,remote-email - if protected-frontends + http-request redirect location %[var(txn.auth_response_location)] if protected-frontends !{ var(txn.auth_response_successful) -m bool } # Authelia backend route use_backend be_authelia if host-authelia @@ -373,24 +320,6 @@ listen authelia_proxy server authelia authelia:9091 ssl verify none backend be_nextcloud - ## Pass the special authorization response headers to the protected application. - acl authorization_exist var(req.auth_response_header.authorization) -m found - acl proxy_authorization_exist var(req.auth_response_header.proxy_authorization) -m found - - http-request set-header Authorization %[var(req.auth_response_header.authorization)] if authorization_exist - http-request set-header Proxy-Authorization %[var(req.auth_response_header.proxy_authorization)] if proxy_authorization_exist - - ## Pass the special metadata response headers to the protected application. - acl remote_user_exist var(req.auth_response_header.remote_user) -m found - acl remote_groups_exist var(req.auth_response_header.remote_groups) -m found - acl remote_name_exist var(req.auth_response_header.remote_name) -m found - acl remote_email_exist var(req.auth_response_header.remote_email) -m found - - http-request set-header Remote-User %[var(req.auth_response_header.remote_user)] if remote_user_exist - http-request set-header Remote-Groups %[var(req.auth_response_header.remote_groups)] if remote_groups_exist - http-request set-header Remote-Name %[var(req.auth_response_header.remote_name)] if remote_name_exist - http-request set-header Remote-Email %[var(req.auth_response_header.remote_email)] if remote_email_exist - ## Pass the Set-Cookie response headers to the user. acl set_cookie_exist var(req.auth_response_header.set_cookie) -m found http-response set-header Set-Cookie %[var(req.auth_response_header.set_cookie)] if set_cookie_exist @@ -398,24 +327,6 @@ backend be_nextcloud server nextcloud nextcloud:443 ssl verify none backend be_heimdall - ## Pass the special authorization response headers to the protected application. - acl authorization_exist var(req.auth_response_header.authorization) -m found - acl proxy_authorization_exist var(req.auth_response_header.proxy_authorization) -m found - - http-request set-header Authorization %[var(req.auth_response_header.authorization)] if authorization_exist - http-request set-header Proxy-Authorization %[var(req.auth_response_header.proxy_authorization)] if proxy_authorization_exist - - ## Pass the special metadata response headers to the protected application. - acl remote_user_exist var(req.auth_response_header.remote_user) -m found - acl remote_groups_exist var(req.auth_response_header.remote_groups) -m found - acl remote_name_exist var(req.auth_response_header.remote_name) -m found - acl remote_email_exist var(req.auth_response_header.remote_email) -m found - - http-request set-header Remote-User %[var(req.auth_response_header.remote_user)] if remote_user_exist - http-request set-header Remote-Groups %[var(req.auth_response_header.remote_groups)] if remote_groups_exist - http-request set-header Remote-Name %[var(req.auth_response_header.remote_name)] if remote_name_exist - http-request set-header Remote-Email %[var(req.auth_response_header.remote_email)] if remote_email_exist - ## Pass the Set-Cookie response headers to the user. acl set_cookie_exist var(req.auth_response_header.set_cookie) -m found http-response set-header Set-Cookie %[var(req.auth_response_header.set_cookie)] if set_cookie_exist diff --git a/docs/content/en/integration/proxies/nginx.md b/docs/content/en/integration/proxies/nginx.md index 345aa2430..4c9e9f95a 100644 --- a/docs/content/en/integration/proxies/nginx.md +++ b/docs/content/en/integration/proxies/nginx.md @@ -34,8 +34,8 @@ You need the following to run __Authelia__ with [NGINX]: * [NGINX] must be built with the `http_auth_request` module which is relatively common * [NGINX] must be built with the `http_realip` module which is relatively common -* [NGINX] must be built with the `http_set_misc` module or the `nginx-mod-http-set-misc` package if you want to preserve - more than one query parameter when redirected to the portal due to a limitation in [NGINX] +* [NGINX] must be built with the `http_set_misc` module or the `nginx-mod-http-set-misc` package if you want to use the + legacy method and preserve more than one query parameter when redirected to the portal due to a limitation in [NGINX] ## Trusted Proxies @@ -76,6 +76,23 @@ following are the assumptions we make: * This domain and the subdomains will have to be adapted in all examples to match your specific domains unless you're just testing or you want ot use that specific domain +## Implementation + +[NGINX] utilizes the [AuthRequest](../../reference/guides/proxy-authorization.md#authrequest) Authz implementation. The +associated [Metadata](../../reference/guides/proxy-authorization.md#authrequest-metadata) should be considered required. + +The examples below assume you are using the default +[Authz Endpoints Configuration](../../configuration/miscellaneous/server-endpoints-authz.md) or one similar to the +following minimal configuration: + +```yaml +server: + endpoints: + authz: + auth-request: + implementation: AuthRequest +``` + ## Docker Compose The following docker compose example has various applications suitable for setting up an example environment. @@ -449,14 +466,6 @@ and is paired with [authelia-location.conf](#authelia-locationconf).* ## Send a subrequest to Authelia to verify if the user is authenticated and has permission to access the resource. auth_request /internal/authelia/authz; -## Set the $target_url variable based on the original request. - -## Comment this line if you're using nginx without the http_set_misc module. -set_escape_uri $target_url $scheme://$http_host$request_uri; - -## Uncomment this line if you're using NGINX without the http_set_misc module. -# set $target_url $scheme://$http_host$request_uri; - ## Save the upstream authorization response headers from Authelia to variables. auth_request_set $authorization $upstream_http_authorization; auth_request_set $proxy_authorization $upstream_http_proxy_authorization; @@ -481,11 +490,23 @@ proxy_set_header Remote-Name $name; auth_request_set $cookie $upstream_http_set_cookie; add_header Set-Cookie $cookie; -## IMPORTANT: The below URL `https://auth.example.com/` MUST be replaced with the externally accessible URL of the -## Authelia Portal/Site. -## -## If the subreqest returns 200 pass to the backend, if the subrequest returns 401 redirect to the portal. -error_page 401 =302 https://auth.example.com/?rd=$target_url; +## Configure the redirection when the authz failure occurs. Lines starting with 'Modern Method' and 'Legacy Method' +## should be commented / uncommented as pairs. The modern method uses the session cookies configuration's authelia_url +## value to determine the redirection URL here. It's much simpler and compatible with the mutli-cookie domain easily. + +## Modern Method: Set the $redirection_url to the Location header of the response to the Authz endpoint. +auth_request_set $redirection_url $upstream_http_location; + +## Modern Method: When there is a 401 response code from the authz endpoint redirect to the $redirection_url. +error_page 401 =302 $redirection_url; + +## Legacy Method: Set $target_url to the original requested URL. +## This requires http_set_misc module, replace 'set_escape_uri' with 'set' if you don't have this module. +# set_escape_uri $target_url $scheme://$http_host$request_uri; + +## Legacy Method: When there is a 401 response code from the authz endpoint redirect to the portal with the 'rd' +## URL parameter set to $target_url. This requires users update 'auth.example.com/' with their external authelia URL. +# error_page 401 =302 https://auth.example.com/?rd=$target_url; ``` {{< /details >}} @@ -555,12 +576,6 @@ endpoint. It's recommended to use [authelia-authrequest.conf](#authelia-authrequ ## Send a subrequest to Authelia to verify if the user is authenticated and has permission to access the resource. auth_request /internal/authelia/authz/basic; -## Comment this line if you're using nginx without the http_set_misc module. -set_escape_uri $target_url $scheme://$http_host$request_uri; - -## Uncomment this line if you're using NGINX without the http_set_misc module. -# set $target_url $scheme://$http_host$request_uri; - ## Save the upstream response headers from Authelia to variables. auth_request_set $user $upstream_http_remote_user; auth_request_set $groups $upstream_http_remote_groups; diff --git a/docs/content/en/integration/proxies/traefik.md b/docs/content/en/integration/proxies/traefik.md index f9d9b9c1c..d5553e95d 100644 --- a/docs/content/en/integration/proxies/traefik.md +++ b/docs/content/en/integration/proxies/traefik.md @@ -61,6 +61,23 @@ networks to the trusted proxy list in [Traefik]: See the [Entry Points](https://doc.traefik.io/traefik/routing/entrypoints) documentation for more information. +## Implementation + +[Traefik] utilizes the [ForwardAuth](../../reference/guides/proxy-authorization.md#forwardauth) Authz implementation. The +associated [Metadata](../../reference/guides/proxy-authorization.md#forwardauth-metadata) should be considered required. + +The examples below assume you are using the default +[Authz Endpoints Configuration](../../configuration/miscellaneous/server-endpoints-authz.md) or one similar to the +following minimal configuration: + +```yaml +server: + endpoints: + authz: + forward-auth: + implementation: ForwardAuth +``` + ## Configuration Below you will find commented examples of the following docker deployment: diff --git a/docs/content/en/integration/proxies/traefikv1.md b/docs/content/en/integration/proxies/traefikv1.md index 38abc37e1..0519782ca 100644 --- a/docs/content/en/integration/proxies/traefikv1.md +++ b/docs/content/en/integration/proxies/traefikv1.md @@ -74,6 +74,23 @@ following are the assumptions we make: * This domain and the subdomains will have to be adapted in all examples to match your specific domains unless you're just testing or you want ot use that specific domain +## Implementation + +[Traefik] utilizes the [ForwardAuth](../../reference/guides/proxy-authorization.md#forwardauth) Authz implementation. The +associated [Metadata](../../reference/guides/proxy-authorization.md#forwardauth-metadata) should be considered required. + +The examples below assume you are using the default +[Authz Endpoints Configuration](../../configuration/miscellaneous/server-endpoints-authz.md) or one similar to the +following minimal configuration: + +```yaml +server: + endpoints: + authz: + forward-auth: + implementation: ForwardAuth +``` + ## Configuration Below you will find commented examples of the following docker deployment: diff --git a/docs/content/en/reference/guides/proxy-authorization.md b/docs/content/en/reference/guides/proxy-authorization.md index 175655eb4..8a7a72f4c 100644 --- a/docs/content/en/reference/guides/proxy-authorization.md +++ b/docs/content/en/reference/guides/proxy-authorization.md @@ -64,7 +64,7 @@ completely unset. ### 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]. +[forward_auth directive], [HAProxy] via the [auth-request lua plugin], and [Skipper] via the [webhook auth filter]. #### ForwardAuth Metadata @@ -87,7 +87,7 @@ This is the implementation which supports [Traefik] via the [ForwardAuth Middlew ### ExtAuthz -This is the implementation which supports [Envoy] via the [ExtAuthz Extension Filter]. +This is the implementation which supports [Envoy] via the [HTTP ExtAuthz Filter]. #### ExtAuthz Metadata @@ -110,26 +110,31 @@ This is the implementation which supports [Envoy] via the [ExtAuthz Extension Fi ### AuthRequest -This is the implementation which supports [NGINX] via the [auth_request HTTP module] and [HAProxy] via the -[auth-request lua plugin]. +This is the implementation which supports [NGINX] via the [auth_request HTTP module], and can technically support +[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_ | +#### AuthRequest Metadata -_**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._ +| 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 | Session Cookie Configuration | `authelia_url` | + +_**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. However we return the appropriate URL to redirect users to with the `Location` header which +simplifies this process especially for multi-cookie domain deployments._ #### AuthRequest Metadata Alternatives -| Metadata | Alternative Type | Source | Key | -|:--------:|:----------------:|:----------:|:---------:| -| IP | Fallback | TCP Packet | Source IP | +| Metadata | Alternative Type | Source | Key | +|:------------:|:----------------:|:--------------:|:--------------:| +| IP | Fallback | TCP Packet | Source IP | +| Authelia URL | Override | Query Argument | `authelia_url` | ### Legacy @@ -213,7 +218,7 @@ or the header is malformed it will respond with the [WWW-Authenticate] header. [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 +[HTTP ExtAuthz 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/ diff --git a/docs/content/en/reference/guides/templating.md b/docs/content/en/reference/guides/templating.md index e0d3b7d44..31ab3a96c 100644 --- a/docs/content/en/reference/guides/templating.md +++ b/docs/content/en/reference/guides/templating.md @@ -81,6 +81,8 @@ The following functions which mimic the behaviour of helm exist in most templati - indent - nindent - uuidv4 +- urlquery +- urlunquery (opposite of urlquery) See the [Helm Documentation](https://helm.sh/docs/chart_template_guide/function_list/) for more information. Please note that only the functions listed above are supported and the functions don't necessarily behave exactly the same. diff --git a/internal/handlers/const.go b/internal/handlers/const.go index 932d1ab5a..953b0f748 100644 --- a/internal/handlers/const.go +++ b/internal/handlers/const.go @@ -15,12 +15,22 @@ const ( ActionResetPassword = "ResetPassword" ) +const ( + anonymous = "" +) + var ( headerAuthorization = []byte(fasthttp.HeaderAuthorization) headerWWWAuthenticate = []byte(fasthttp.HeaderWWWAuthenticate) headerProxyAuthorization = []byte(fasthttp.HeaderProxyAuthorization) headerProxyAuthenticate = []byte(fasthttp.HeaderProxyAuthenticate) + + headerSessionUsername = []byte("Session-Username") + headerRemoteUser = []byte("Remote-User") + headerRemoteGroups = []byte("Remote-Groups") + headerRemoteName = []byte("Remote-Name") + headerRemoteEmail = []byte("Remote-Email") ) const ( @@ -31,14 +41,6 @@ var ( headerValueAuthenticateBasic = []byte(`Basic realm="Authorization Required"`) ) -var ( - headerSessionUsername = []byte("Session-Username") - headerRemoteUser = []byte("Remote-User") - headerRemoteGroups = []byte("Remote-Groups") - headerRemoteName = []byte("Remote-Name") - headerRemoteEmail = []byte("Remote-Email") -) - const ( queryArgRD = "rd" queryArgRM = "rm" @@ -87,6 +89,8 @@ const ( ) const ( + logFmtAuthzRedirect = "Access to %s (method %s) is not authorized to user %s, responding with status code %d with location redirect to %s" + logFmtAuthorizationPrefix = "Authorization Request with id '%s' on client with id '%s' " logFmtErrConsentCantDetermineConsentMode = logFmtAuthorizationPrefix + "could not be processed: error occurred generating consent: client consent mode could not be reliably determined" diff --git a/internal/handlers/handler_authz.go b/internal/handlers/handler_authz.go index 88596552c..f8adb6bba 100644 --- a/internal/handlers/handler_authz.go +++ b/internal/handlers/handler_authz.go @@ -21,9 +21,9 @@ func (authz *Authz) Handler(ctx *middlewares.AutheliaCtx) { ) if object, err = authz.handleGetObject(ctx); err != nil { - ctx.Logger.Errorf("Error getting original request object: %v", err) + ctx.Logger.WithError(err).Error("Error getting Target URL and Request Method") - ctx.ReplyUnauthorized() + ctx.ReplyStatusCode(authz.config.StatusCodeBadRequest) return } @@ -31,23 +31,23 @@ func (authz *Authz) Handler(ctx *middlewares.AutheliaCtx) { 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() + ctx.ReplyStatusCode(authz.config.StatusCodeBadRequest) 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.Logger.WithError(err).WithField("target_url", object.URL.String()).Error("Target URL does not appear to have a relevant session cookies configuration") - ctx.ReplyUnauthorized() + ctx.ReplyStatusCode(authz.config.StatusCodeBadRequest) 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.Logger.WithError(err).WithField("target_url", object.URL.String()).Error("Error occurred trying to determine the external Authelia URL for Target URL") - ctx.ReplyUnauthorized() + ctx.ReplyStatusCode(authz.config.StatusCodeBadRequest) return } @@ -104,23 +104,23 @@ func (authz *Authz) Handler(ctx *middlewares.AutheliaCtx) { } 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 || authz.legacy { + switch { + case authz.implementation == AuthzImplLegacy: return autheliaURL, nil + case autheliaURL != nil: + switch { + case utils.HasURIDomainSuffix(autheliaURL, provider.Config.Domain): + return autheliaURL, nil + default: + return nil, fmt.Errorf("authelia url '%s' is not valid for detected domain '%s' as the url does not have the domain as a suffix", autheliaURL.String(), provider.Config.Domain) + } } if provider.Config.AutheliaURL != nil { - if authz.legacy { - return nil, nil - } - return provider.Config.AutheliaURL, nil } @@ -134,6 +134,10 @@ func (authz *Authz) getRedirectionURL(object *authorization.Object, autheliaURL redirectionURL, _ = url.ParseRequestURI(autheliaURL.String()) + if redirectionURL.Path == "" { + redirectionURL.Path = "/" + } + qry := redirectionURL.Query() qry.Set(queryArgRD, object.URL.String()) @@ -151,10 +155,10 @@ func (authz *Authz) authn(ctx *middlewares.AutheliaCtx, provider *session.Sessio 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, Username: anonymous}, strategy, err } - return Authn{Type: authn.Type, Level: authentication.NotAuthenticated}, nil, err + return Authn{Type: authn.Type, Level: authentication.NotAuthenticated, Username: anonymous}, nil, err } if authn.Level != authentication.NotAuthenticated { diff --git a/internal/handlers/handler_authz_authn.go b/internal/handlers/handler_authz_authn.go index 10188e19e..ef13c9bd6 100644 --- a/internal/handlers/handler_authz_authn.go +++ b/internal/handlers/handler_authz_authn.go @@ -81,13 +81,14 @@ type CookieSessionAuthnStrategy struct { // 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 + authn = Authn{ + Type: AuthnTypeCookie, + Level: authentication.NotAuthenticated, + Username: anonymous, + } + if userSession, err = provider.GetSession(ctx.RequestCtx); err != nil { return authn, fmt.Errorf("failed to retrieve user session: %w", err) } @@ -108,21 +109,21 @@ func (s *CookieSessionAuthnStrategy) Get(ctx *middlewares.AutheliaCtx, provider 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) + ctx.Logger.WithError(err).Errorf("Unable to destroy user session") } 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) + ctx.Logger.WithError(err).Error("Unable to save updated user session") } return authn, nil } if err = provider.SaveSession(ctx.RequestCtx, userSession); err != nil { - ctx.Logger.Errorf("Unable to save updated user session: %+v", err) + ctx.Logger.WithError(err).Error("Unable to save updated user session") } return Authn{ @@ -164,8 +165,9 @@ func (s *HeaderAuthnStrategy) Get(ctx *middlewares.AutheliaCtx, _ *session.Sessi ) authn = Authn{ - Type: s.authn, - Level: authentication.NotAuthenticated, + Type: s.authn, + Level: authentication.NotAuthenticated, + Username: anonymous, } if value = ctx.Request.Header.PeekBytes(s.headerAuthorize); value == nil { @@ -195,7 +197,7 @@ func (s *HeaderAuthnStrategy) Get(ctx *middlewares.AutheliaCtx, _ *session.Sessi 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) + ctx.Logger.WithField("username", username).Error("Error occurred while attempting to get user details for user: the user was not found indicating they were deleted, disabled, or otherwise no longer authorized to login") return authn, err } @@ -237,7 +239,8 @@ func (s *HeaderLegacyAuthnStrategy) Get(ctx *middlewares.AutheliaCtx, _ *session ) authn = Authn{ - Level: authentication.NotAuthenticated, + Level: authentication.NotAuthenticated, + Username: anonymous, } if qryValueAuth := ctx.QueryArgs().PeekBytes(qryArgAuth); bytes.Equal(qryValueAuth, qryValueBasic) { @@ -280,7 +283,7 @@ func (s *HeaderLegacyAuthnStrategy) Get(ctx *middlewares.AutheliaCtx, _ *session 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) + ctx.Logger.WithField("username", username).Error("Error occurred while attempting to get user details for user: the user was not found indicating they were deleted, disabled, or otherwise no longer authorized to login") return authn, err } @@ -309,13 +312,13 @@ func handleVerifyGETAuthnCookieValidate(ctx *middlewares.AutheliaCtx, provider * 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) + ctx.Logger.WithFields(map[string]any{"username": anonymous, "level": userSession.AuthenticationLevel.String()}).Errorf("Session for user has an invalid authentication level: this may be a sign of a compromise") 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) + ctx.Logger.WithField("username", userSession.Username).Info("Session for user not marked as remembered has exceeded configured session inactivity") return true } @@ -325,7 +328,7 @@ func handleVerifyGETAuthnCookieValidate(ctx *middlewares.AutheliaCtx, provider * } 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) + ctx.Logger.WithField("username", userSession.Username).Warnf("Session for user does not match the Session-Username header with value '%s' which could be a sign of a cookie hijack", username) return true } @@ -342,7 +345,7 @@ func handleVerifyGETAuthnCookieValidateInactivity(ctx *middlewares.AutheliaCtx, 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())) + ctx.Logger.WithField("username", userSession.Username).Tracef("Inactivity report for user. Current Time: %d, Last Activity: %d, Maximum Inactivity: %d.", 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()) } @@ -352,13 +355,13 @@ func handleVerifyGETAuthnCookieValidateUpdate(ctx *middlewares.AutheliaCtx, user return false } - ctx.Logger.Tracef("Checking if we need check the authentication backend for an updated profile for user '%s'", userSession.Username) + ctx.Logger.WithField("username", userSession.Username).Trace("Checking if we need check the authentication backend for an updated profile for user") 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) + ctx.Logger.WithField("username", userSession.Username).Debug("Checking the authentication backend for an updated profile for user") var ( details *authentication.UserDetails @@ -367,12 +370,12 @@ func handleVerifyGETAuthnCookieValidateUpdate(ctx *middlewares.AutheliaCtx, user 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) + ctx.Logger.WithField("username", userSession.Username).Error("Error occurred while attempting to update user details for user: the user was not found indicating they were deleted, disabled, or otherwise no longer authorized to login") return true } - ctx.Logger.Errorf("Error occurred while attempting to update user details for user '%s': %v", userSession.Username, err) + ctx.Logger.WithError(err).WithField("username", userSession.Username).Error("Error occurred while attempting to update user details for user") return false } @@ -389,12 +392,12 @@ func handleVerifyGETAuthnCookieValidateUpdate(ctx *middlewares.AutheliaCtx, user } if !diffEmails && !diffGroups && !diffDisplayName { - ctx.Logger.Tracef("Updated profile not detected for user '%s'", userSession.Username) + ctx.Logger.WithField("username", userSession.Username).Trace("Updated profile not detected for user") return false } - ctx.Logger.Debugf("Updated profile detected for user '%s'", userSession.Username) + ctx.Logger.WithField("username", userSession.Username).Debug("Updated profile detected for user") if ctx.Logger.Level >= logrus.TraceLevel { generateVerifySessionHasUpToDateProfileTraceLogs(ctx, userSession, details) diff --git a/internal/handlers/handler_authz_builder.go b/internal/handlers/handler_authz_builder.go index 98aa39215..dffa91b34 100644 --- a/internal/handlers/handler_authz_builder.go +++ b/internal/handlers/handler_authz_builder.go @@ -1,9 +1,10 @@ package handlers import ( - "fmt" "time" + "github.com/valyala/fasthttp" + "github.com/authelia/authelia/v4/internal/configuration/schema" "github.com/authelia/authelia/v4/internal/utils" ) @@ -22,31 +23,10 @@ func (b *AuthzBuilder) WithStrategies(strategies ...AuthnStrategy) *AuthzBuilder 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 + b.implementation = AuthzImplLegacy return b } @@ -54,7 +34,7 @@ func (b *AuthzBuilder) WithImplementationLegacy() *AuthzBuilder { // 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 + b.implementation = AuthzImplForwardAuth return b } @@ -62,7 +42,7 @@ func (b *AuthzBuilder) WithImplementationForwardAuth() *AuthzBuilder { // 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 + b.implementation = AuthzImplAuthRequest return b } @@ -70,7 +50,7 @@ func (b *AuthzBuilder) WithImplementationAuthRequest() *AuthzBuilder { // 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 + b.implementation = AuthzImplExtAuthz return b } @@ -95,12 +75,6 @@ func (b *AuthzBuilder) WithConfig(config *schema.Configuration) *AuthzBuilder { b.config = AuthzConfig{ RefreshInterval: refreshInterval, - Domains: []AuthzDomain{ - { - Name: fmt.Sprintf(".%s", config.Session.Domain), - PortalURL: nil, - }, - }, } return b @@ -140,24 +114,19 @@ func (b *AuthzBuilder) WithEndpointConfig(config schema.ServerAuthzEndpoint) *Au 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, + implementation: b.implementation, } + authz.config.StatusCodeBadRequest = fasthttp.StatusBadRequest + if len(authz.strategies) == 0 { - switch b.impl { + switch b.implementation { case AuthzImplLegacy: authz.strategies = []AuthnStrategy{NewHeaderLegacyAuthnStrategy(), NewCookieSessionAuthnStrategy(b.config.RefreshInterval)} case AuthzImplAuthRequest: @@ -167,9 +136,9 @@ func (b *AuthzBuilder) Build() (authz *Authz) { } } - switch b.impl { + switch b.implementation { case AuthzImplLegacy: - authz.legacy = true + authz.config.StatusCodeBadRequest = fasthttp.StatusUnauthorized authz.handleGetObject = handleAuthzGetObjectLegacy authz.handleUnauthorized = handleAuthzUnauthorizedLegacy authz.handleGetAutheliaURL = handleAuthzPortalURLLegacy @@ -180,6 +149,7 @@ func (b *AuthzBuilder) Build() (authz *Authz) { case AuthzImplAuthRequest: authz.handleGetObject = handleAuthzGetObjectAuthRequest authz.handleUnauthorized = handleAuthzUnauthorizedAuthRequest + authz.handleGetAutheliaURL = handleAuthzPortalURLFromQuery case AuthzImplExtAuthz: authz.handleGetObject = handleAuthzGetObjectExtAuthz authz.handleUnauthorized = handleAuthzUnauthorizedExtAuthz diff --git a/internal/handlers/handler_authz_impl_authrequest.go b/internal/handlers/handler_authz_impl_authrequest.go index 19292201f..11b3e5371 100644 --- a/internal/handlers/handler_authz_impl_authrequest.go +++ b/internal/handlers/handler_authz_impl_authrequest.go @@ -36,7 +36,13 @@ func handleAuthzGetObjectAuthRequest(ctx *middlewares.AutheliaCtx) (object autho 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() +func handleAuthzUnauthorizedAuthRequest(ctx *middlewares.AutheliaCtx, authn *Authn, redirectionURL *url.URL) { + ctx.Logger.Infof(logFmtAuthzRedirect, authn.Object.URL.String(), authn.Method, authn.Username, fasthttp.StatusUnauthorized, redirectionURL) + + switch authn.Object.Method { + case fasthttp.MethodHead: + ctx.SpecialRedirectNoBody(redirectionURL.String(), fasthttp.StatusUnauthorized) + default: + ctx.SpecialRedirect(redirectionURL.String(), fasthttp.StatusUnauthorized) + } } diff --git a/internal/handlers/handler_authz_impl_authrequest_test.go b/internal/handlers/handler_authz_impl_authrequest_test.go index 6f4a1d8b4..f360bf1dc 100644 --- a/internal/handlers/handler_authz_impl_authrequest_test.go +++ b/internal/handlers/handler_authz_impl_authrequest_test.go @@ -13,7 +13,7 @@ import ( "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" + "github.com/authelia/authelia/v4/internal/utils" ) func TestRunAuthRequestAuthzSuite(t *testing.T) { @@ -35,26 +35,36 @@ type AuthRequestAuthzSuite struct { 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"), + 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(targetURI.String(), func(t *testing.T) { + 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() - s.setRequest(mock.Ctx, method, targetURI, true, false) + s.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) + + s.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) + + s.setRequest(mock.Ctx, method, pairURI.TargetURI, true, false) authz.Handler(mock.Ctx) + query := expected.Query() + query.Set(queryArgRD, pairURI.TargetURI.String()) + query.Set(queryArgRM, method) + expected.RawQuery = query.Encode() assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode()) - assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation)) + assert.Equal(t, expected.String(), string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation))) }) } }) @@ -83,7 +93,7 @@ func (s *AuthRequestAuthzSuite) TestShouldHandleInvalidMethodCharsDeny() { authz.Handler(mock.Ctx) - assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode()) + assert.Equal(t, fasthttp.StatusBadRequest, mock.Ctx.Response.StatusCode()) assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation)) }) } @@ -109,7 +119,7 @@ func (s *AuthRequestAuthzSuite) TestShouldHandleMissingXOriginalMethodDeny() { authz.Handler(mock.Ctx) - assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode()) + assert.Equal(t, fasthttp.StatusBadRequest, mock.Ctx.Response.StatusCode()) assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation)) }) } @@ -128,7 +138,7 @@ func (s *AuthRequestAuthzSuite) TestShouldHandleMissingXOriginalURLDeny() { authz.Handler(mock.Ctx) - assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode()) + assert.Equal(t, fasthttp.StatusBadRequest, mock.Ctx.Response.StatusCode()) assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation)) }) } @@ -150,6 +160,8 @@ func (s *AuthRequestAuthzSuite) TestShouldHandleAllMethodsAllow() { defer mock.Close() + s.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) + s.setRequest(mock.Ctx, method, targetURI, true, false) authz.Handler(mock.Ctx) @@ -174,11 +186,7 @@ func (s *AuthRequestAuthzSuite) TestShouldHandleAllMethodsWithMethodsACL() { 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.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) s.setRequest(mock.Ctx, method, targetURI, true, false) @@ -188,8 +196,21 @@ func (s *AuthRequestAuthzSuite) TestShouldHandleAllMethodsWithMethodsACL() { 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)) + expected := s.RequireParseRequestURI("https://auth.example.com/") + + query := expected.Query() + query.Set(queryArgRD, targetURI.String()) + query.Set(queryArgRM, method) + expected.RawQuery = query.Encode() + + switch method { + case fasthttp.MethodHead: + assert.Nil(t, mock.Ctx.Response.Body()) + default: + assert.Equal(t, fmt.Sprintf(`%d %s`, utils.StringHTMLEscape(expected.String()), fasthttp.StatusUnauthorized, fasthttp.StatusMessage(fasthttp.StatusUnauthorized)), string(mock.Ctx.Response.Body())) + } + + assert.Equal(t, expected.String(), string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation))) } }) } @@ -205,7 +226,7 @@ func (s *AuthRequestAuthzSuite) TestShouldHandleInvalidURLForCVE202132637() { }{ {"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, + fasthttp.StatusBadRequest, }, {"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}, @@ -226,6 +247,8 @@ func (s *AuthRequestAuthzSuite) TestShouldHandleInvalidURLForCVE202132637() { mock.Ctx.Configuration.AccessControl.DefaultPolicy = testBypass mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(&mock.Ctx.Configuration) + s.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) + mock.Ctx.Request.Header.Set(testXOriginalMethod, method) mock.Ctx.Request.Header.SetBytesKV([]byte(testXOriginalUrl), tc.uri) @@ -255,17 +278,13 @@ func (s *AuthRequestAuthzSuite) TestShouldNotHandleExtAuthzAllMethodsAllow() { 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.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) setRequestExtAuthz(mock.Ctx, method, targetURI, true, false) authz.Handler(mock.Ctx) - assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode()) + assert.Equal(t, fasthttp.StatusBadRequest, mock.Ctx.Response.StatusCode()) assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation)) }) } @@ -291,17 +310,13 @@ func (s *AuthRequestAuthzSuite) TestShouldNotHandleExtAuthzAllMethodsAllowXHR() 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.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) setRequestExtAuthz(mock.Ctx, method, targetURI, x, x) authz.Handler(mock.Ctx) - assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode()) + assert.Equal(t, fasthttp.StatusBadRequest, mock.Ctx.Response.StatusCode()) assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation)) }) } @@ -323,17 +338,13 @@ func (s *AuthRequestAuthzSuite) TestShouldNotHandleExtAuthzAllMethodsWithMethods 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.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) setRequestExtAuthz(mock.Ctx, method, targetURI, true, false) authz.Handler(mock.Ctx) - assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode()) + assert.Equal(t, fasthttp.StatusBadRequest, mock.Ctx.Response.StatusCode()) assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation)) }) } @@ -357,17 +368,13 @@ func (s *AuthRequestAuthzSuite) TestShouldNotHandleForwardAuthAllMethodsAllow() 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.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) setRequestForwardAuth(mock.Ctx, method, targetURI, true, false) authz.Handler(mock.Ctx) - assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode()) + assert.Equal(t, fasthttp.StatusBadRequest, mock.Ctx.Response.StatusCode()) assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation)) }) } @@ -393,17 +400,13 @@ func (s *AuthRequestAuthzSuite) TestShouldNotHandleForwardAuthAllMethodsAllowXHR 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.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) setRequestForwardAuth(mock.Ctx, method, targetURI, x, x) authz.Handler(mock.Ctx) - assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode()) + assert.Equal(t, fasthttp.StatusBadRequest, mock.Ctx.Response.StatusCode()) assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation)) }) } @@ -425,17 +428,13 @@ func (s *AuthRequestAuthzSuite) TestShouldNotHandleForwardAuthAllMethodsWithMeth 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.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) setRequestForwardAuth(mock.Ctx, method, targetURI, true, false) authz.Handler(mock.Ctx) - assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode()) + assert.Equal(t, fasthttp.StatusBadRequest, mock.Ctx.Response.StatusCode()) assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation)) }) } diff --git a/internal/handlers/handler_authz_impl_extauthz.go b/internal/handlers/handler_authz_impl_extauthz.go index ccae832d9..67786a33d 100644 --- a/internal/handlers/handler_authz_impl_extauthz.go +++ b/internal/handlers/handler_authz_impl_extauthz.go @@ -50,6 +50,12 @@ func handleAuthzUnauthorizedExtAuthz(ctx *middlewares.AutheliaCtx, authn *Authn, } } - 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) + ctx.Logger.Infof(logFmtAuthzRedirect, authn.Object.String(), authn.Method, authn.Username, statusCode, redirectionURL) + + switch authn.Object.Method { + case fasthttp.MethodHead: + ctx.SpecialRedirectNoBody(redirectionURL.String(), statusCode) + default: + ctx.SpecialRedirect(redirectionURL.String(), statusCode) + } } diff --git a/internal/handlers/handler_authz_impl_extauthz_test.go b/internal/handlers/handler_authz_impl_extauthz_test.go index 820cb66d1..642df3ffc 100644 --- a/internal/handlers/handler_authz_impl_extauthz_test.go +++ b/internal/handlers/handler_authz_impl_extauthz_test.go @@ -13,7 +13,7 @@ import ( "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" + "github.com/authelia/authelia/v4/internal/utils" ) func TestRunExtAuthzAuthzSuite(t *testing.T) { @@ -51,11 +51,7 @@ func (s *ExtAuthzAuthzSuite) TestShouldHandleAllMethodsDeny() { 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.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) s.setRequest(mock.Ctx, method, pairURI.TargetURI, true, false) @@ -98,11 +94,7 @@ func (s *ExtAuthzAuthzSuite) TestShouldHandleAllMethodsOverrideAutheliaURLDeny() 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.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) mock.Ctx.Request.Header.Set("X-Authelia-Url", pairURI.AutheliaURI.String()) s.setRequest(mock.Ctx, method, pairURI.TargetURI, true, false) @@ -148,8 +140,10 @@ func (s *ExtAuthzAuthzSuite) TestShouldHandleAllMethodsMissingAutheliaURLDeny() authz.Handler(mock.Ctx) - assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode()) + assert.Equal(t, fasthttp.StatusBadRequest, mock.Ctx.Response.StatusCode()) + assert.Equal(t, fmt.Sprintf("%d %s", fasthttp.StatusBadRequest, fasthttp.StatusMessage(fasthttp.StatusBadRequest)), string(mock.Ctx.Response.Body())) assert.Equal(t, "", string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation))) + assert.Equal(t, "text/plain; charset=utf-8", string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderContentType))) }) } }) @@ -176,11 +170,7 @@ func (s *ExtAuthzAuthzSuite) TestShouldHandleAllMethodsXHRDeny() { 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.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) s.setRequest(mock.Ctx, method, pairURI.TargetURI, x, x) @@ -222,17 +212,13 @@ func (s *ExtAuthzAuthzSuite) TestShouldHandleInvalidMethodCharsDeny() { 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.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) s.setRequest(mock.Ctx, method, targetURI, true, false) authz.Handler(mock.Ctx) - assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode()) + assert.Equal(t, fasthttp.StatusBadRequest, mock.Ctx.Response.StatusCode()) assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation)) }) } @@ -249,17 +235,13 @@ func (s *ExtAuthzAuthzSuite) TestShouldHandleMissingHostDeny() { 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.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) s.setRequest(mock.Ctx, method, nil, true, false) authz.Handler(mock.Ctx) - assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode()) + assert.Equal(t, fasthttp.StatusBadRequest, mock.Ctx.Response.StatusCode()) assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation)) }) } @@ -281,11 +263,7 @@ func (s *ExtAuthzAuthzSuite) TestShouldHandleAllMethodsAllow() { 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.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) s.setRequest(mock.Ctx, method, targetURI, true, false) @@ -317,11 +295,7 @@ func (s *ExtAuthzAuthzSuite) TestShouldHandleAllMethodsAllowXHR() { 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.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) s.setRequest(mock.Ctx, method, targetURI, x, x) @@ -349,11 +323,7 @@ func (s *ExtAuthzAuthzSuite) TestShouldHandleAllMethodsWithMethodsACL() { 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.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) s.setRequest(mock.Ctx, method, targetURI, true, false) @@ -365,18 +335,23 @@ func (s *ExtAuthzAuthzSuite) TestShouldHandleAllMethodsWithMethodsACL() { } 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() + switch method { + case fasthttp.MethodHead: + assert.Equal(t, fasthttp.StatusFound, mock.Ctx.Response.StatusCode()) + assert.Nil(t, mock.Ctx.Response.Body()) + case fasthttp.MethodGet, fasthttp.MethodOptions: + assert.Equal(t, fasthttp.StatusFound, mock.Ctx.Response.StatusCode()) + assert.Equal(t, fmt.Sprintf(`%d %s`, utils.StringHTMLEscape(expected.String()), fasthttp.StatusFound, fasthttp.StatusMessage(fasthttp.StatusFound)), string(mock.Ctx.Response.Body())) + default: + assert.Equal(t, fasthttp.StatusSeeOther, mock.Ctx.Response.StatusCode()) + assert.Equal(t, fmt.Sprintf(`%d %s`, utils.StringHTMLEscape(expected.String()), fasthttp.StatusSeeOther, fasthttp.StatusMessage(fasthttp.StatusSeeOther)), string(mock.Ctx.Response.Body())) + } + assert.Equal(t, expected.String(), string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation))) } }) @@ -394,7 +369,7 @@ func (s *ExtAuthzAuthzSuite) TestShouldHandleInvalidURLForCVE202132637() { }{ {"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, + fasthttp.StatusBadRequest, }, {"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", @@ -415,11 +390,7 @@ func (s *ExtAuthzAuthzSuite) TestShouldHandleInvalidURLForCVE202132637() { 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) + s.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) mock.Ctx.Request.SetHostBytes(tc.host) mock.Ctx.Request.Header.SetMethodBytes([]byte(method)) @@ -458,7 +429,7 @@ func (s *ExtAuthzAuthzSuite) TestShouldNotHandleAuthRequestAllMethodsAllow() { authz.Handler(mock.Ctx) - assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode()) + assert.Equal(t, fasthttp.StatusBadRequest, mock.Ctx.Response.StatusCode()) assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation)) }) } @@ -478,17 +449,13 @@ func (s *ExtAuthzAuthzSuite) TestShouldNotHandleAuthRequestAllMethodsWithMethods 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.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) setRequestAuthRequest(mock.Ctx, method, targetURI, true, false) authz.Handler(mock.Ctx) - assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode()) + assert.Equal(t, fasthttp.StatusBadRequest, mock.Ctx.Response.StatusCode()) assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation)) }) } @@ -512,17 +479,13 @@ func (s *ExtAuthzAuthzSuite) TestShouldNotHandleForwardAuthAllMethodsAllow() { 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.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) setRequestForwardAuth(mock.Ctx, method, targetURI, true, false) authz.Handler(mock.Ctx) - assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode()) + assert.Equal(t, fasthttp.StatusBadRequest, mock.Ctx.Response.StatusCode()) assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation)) }) } @@ -548,17 +511,13 @@ func (s *ExtAuthzAuthzSuite) TestShouldNotHandleForwardAuthAllMethodsAllowXHR() 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.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) setRequestForwardAuth(mock.Ctx, method, targetURI, x, x) authz.Handler(mock.Ctx) - assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode()) + assert.Equal(t, fasthttp.StatusBadRequest, mock.Ctx.Response.StatusCode()) assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation)) }) } @@ -580,17 +539,13 @@ func (s *ExtAuthzAuthzSuite) TestShouldNotHandleForwardAuthAllMethodsWithMethods 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.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) setRequestForwardAuth(mock.Ctx, method, targetURI, true, false) authz.Handler(mock.Ctx) - assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode()) + assert.Equal(t, fasthttp.StatusBadRequest, mock.Ctx.Response.StatusCode()) assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation)) }) } diff --git a/internal/handlers/handler_authz_impl_forwardauth.go b/internal/handlers/handler_authz_impl_forwardauth.go index a042c13bb..d385cf971 100644 --- a/internal/handlers/handler_authz_impl_forwardauth.go +++ b/internal/handlers/handler_authz_impl_forwardauth.go @@ -50,6 +50,12 @@ func handleAuthzUnauthorizedForwardAuth(ctx *middlewares.AutheliaCtx, authn *Aut } } - 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) + ctx.Logger.Infof(logFmtAuthzRedirect, authn.Object.String(), authn.Method, authn.Username, statusCode, redirectionURL) + + switch authn.Object.Method { + case fasthttp.MethodHead: + ctx.SpecialRedirectNoBody(redirectionURL.String(), statusCode) + default: + ctx.SpecialRedirect(redirectionURL.String(), statusCode) + } } diff --git a/internal/handlers/handler_authz_impl_forwardauth_test.go b/internal/handlers/handler_authz_impl_forwardauth_test.go index d7ea3baab..de8be0ba5 100644 --- a/internal/handlers/handler_authz_impl_forwardauth_test.go +++ b/internal/handlers/handler_authz_impl_forwardauth_test.go @@ -13,7 +13,7 @@ import ( "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" + "github.com/authelia/authelia/v4/internal/utils" ) func TestRunForwardAuthAuthzSuite(t *testing.T) { @@ -51,11 +51,9 @@ func (s *ForwardAuthAuthzSuite) TestShouldHandleAllMethodsDeny() { 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)) - } + s.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) - mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil) + s.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) s.setRequest(mock.Ctx, method, pairURI.TargetURI, true, false) @@ -98,11 +96,7 @@ func (s *ForwardAuthAuthzSuite) TestShouldHandleAllMethodsOverrideAutheliaURLDen 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.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) mock.Ctx.RequestCtx.QueryArgs().Set("authelia_url", pairURI.AutheliaURI.String()) s.setRequest(mock.Ctx, method, pairURI.TargetURI, true, false) @@ -148,8 +142,10 @@ func (s *ForwardAuthAuthzSuite) TestShouldHandleAllMethodsMissingAutheliaURLDeny authz.Handler(mock.Ctx) - assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode()) + assert.Equal(t, fasthttp.StatusBadRequest, mock.Ctx.Response.StatusCode()) + assert.Equal(t, fmt.Sprintf("%d %s", fasthttp.StatusBadRequest, fasthttp.StatusMessage(fasthttp.StatusBadRequest)), string(mock.Ctx.Response.Body())) assert.Equal(t, "", string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation))) + assert.Equal(t, "text/plain; charset=utf-8", string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderContentType))) }) } }) @@ -176,11 +172,7 @@ func (s *ForwardAuthAuthzSuite) TestShouldHandleAllMethodsXHRDeny() { 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.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) s.setRequest(mock.Ctx, method, pairURI.TargetURI, x, x) @@ -220,17 +212,13 @@ func (s *ForwardAuthAuthzSuite) TestShouldHandleInvalidMethodCharsDeny() { 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.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) s.setRequest(mock.Ctx, method, targetURI, true, false) authz.Handler(mock.Ctx) - assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode()) + assert.Equal(t, fasthttp.StatusBadRequest, mock.Ctx.Response.StatusCode()) assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation)) }) } @@ -247,11 +235,7 @@ func (s *ForwardAuthAuthzSuite) TestShouldHandleMissingHostDeny() { 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.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) mock.Ctx.Request.Header.Set("X-Forwarded-Method", method) mock.Ctx.Request.Header.Set(fasthttp.HeaderXForwardedProto, "https") @@ -261,7 +245,7 @@ func (s *ForwardAuthAuthzSuite) TestShouldHandleMissingHostDeny() { authz.Handler(mock.Ctx) - assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode()) + assert.Equal(t, fasthttp.StatusBadRequest, mock.Ctx.Response.StatusCode()) assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation)) }) } @@ -283,11 +267,7 @@ func (s *ForwardAuthAuthzSuite) TestShouldHandleAllMethodsAllow() { 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.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) s.setRequest(mock.Ctx, method, targetURI, true, false) @@ -313,11 +293,7 @@ func (s *ForwardAuthAuthzSuite) TestShouldHandleAllMethodsWithMethodsACL() { 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.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) s.setRequest(mock.Ctx, method, targetURI, true, false) @@ -329,18 +305,23 @@ func (s *ForwardAuthAuthzSuite) TestShouldHandleAllMethodsWithMethodsACL() { } 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() + switch method { + case fasthttp.MethodHead: + assert.Equal(t, fasthttp.StatusFound, mock.Ctx.Response.StatusCode()) + assert.Nil(t, mock.Ctx.Response.Body()) + case fasthttp.MethodGet, fasthttp.MethodOptions: + assert.Equal(t, fasthttp.StatusFound, mock.Ctx.Response.StatusCode()) + assert.Equal(t, fmt.Sprintf(`%d %s`, utils.StringHTMLEscape(expected.String()), fasthttp.StatusFound, fasthttp.StatusMessage(fasthttp.StatusFound)), string(mock.Ctx.Response.Body())) + default: + assert.Equal(t, fasthttp.StatusSeeOther, mock.Ctx.Response.StatusCode()) + assert.Equal(t, fmt.Sprintf(`%d %s`, utils.StringHTMLEscape(expected.String()), fasthttp.StatusSeeOther, fasthttp.StatusMessage(fasthttp.StatusSeeOther)), string(mock.Ctx.Response.Body())) + } + assert.Equal(t, expected.String(), string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation))) } }) @@ -365,11 +346,7 @@ func (s *ForwardAuthAuthzSuite) TestShouldHandleAllMethodsAllowXHR() { 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.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) s.setRequest(mock.Ctx, method, targetURI, true, true) @@ -392,7 +369,7 @@ func (s *ForwardAuthAuthzSuite) TestShouldHandleInvalidURLForCVE202132637() { }{ {"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, + fasthttp.StatusBadRequest, }, {"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", @@ -413,11 +390,7 @@ func (s *ForwardAuthAuthzSuite) TestShouldHandleInvalidURLForCVE202132637() { 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) + s.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) mock.Ctx.Request.Header.Set("X-Forwarded-Method", method) mock.Ctx.Request.Header.SetBytesKV([]byte(fasthttp.HeaderXForwardedProto), tc.scheme) @@ -455,7 +428,7 @@ func (s *ForwardAuthAuthzSuite) TestShouldNotHandleAuthRequestAllMethodsAllow() authz.Handler(mock.Ctx) - assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode()) + assert.Equal(t, fasthttp.StatusBadRequest, mock.Ctx.Response.StatusCode()) assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation)) }) } @@ -475,17 +448,13 @@ func (s *ForwardAuthAuthzSuite) TestShouldNotHandleAuthRequestAllMethodsWithMeth 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.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) setRequestAuthRequest(mock.Ctx, method, targetURI, true, false) authz.Handler(mock.Ctx) - assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode()) + assert.Equal(t, fasthttp.StatusBadRequest, mock.Ctx.Response.StatusCode()) assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation)) }) } @@ -509,17 +478,13 @@ func (s *ForwardAuthAuthzSuite) TestShouldNotHandleExtAuthzAllMethodsAllow() { 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.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) setRequestExtAuthz(mock.Ctx, method, targetURI, true, false) authz.Handler(mock.Ctx) - assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode()) + assert.Equal(t, fasthttp.StatusBadRequest, mock.Ctx.Response.StatusCode()) assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation)) }) } @@ -545,17 +510,13 @@ func (s *ForwardAuthAuthzSuite) TestShouldNotHandleExtAuthzAllMethodsAllowXHR() 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.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) setRequestExtAuthz(mock.Ctx, method, targetURI, x, x) authz.Handler(mock.Ctx) - assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode()) + assert.Equal(t, fasthttp.StatusBadRequest, mock.Ctx.Response.StatusCode()) assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation)) }) } @@ -577,17 +538,13 @@ func (s *ForwardAuthAuthzSuite) TestShouldNotHandleExtAuthzAllMethodsWithMethods 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.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) setRequestExtAuthz(mock.Ctx, method, targetURI, true, false) authz.Handler(mock.Ctx) - assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode()) + assert.Equal(t, fasthttp.StatusBadRequest, mock.Ctx.Response.StatusCode()) assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation)) }) } diff --git a/internal/handlers/handler_authz_impl_legacy.go b/internal/handlers/handler_authz_impl_legacy.go index 33af89616..25a851152 100644 --- a/internal/handlers/handler_authz_impl_legacy.go +++ b/internal/handlers/handler_authz_impl_legacy.go @@ -47,7 +47,7 @@ func handleAuthzUnauthorizedLegacy(ctx *middlewares.AutheliaCtx, authn *Authn, r statusCode = fasthttp.StatusUnauthorized default: switch authn.Object.Method { - case fasthttp.MethodGet, fasthttp.MethodOptions, "": + case fasthttp.MethodGet, fasthttp.MethodOptions, fasthttp.MethodHead, "": statusCode = fasthttp.StatusFound default: statusCode = fasthttp.StatusSeeOther @@ -55,8 +55,14 @@ func handleAuthzUnauthorizedLegacy(ctx *middlewares.AutheliaCtx, authn *Authn, r } 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) + ctx.Logger.Infof(logFmtAuthzRedirect, authn.Object.URL.String(), authn.Method, authn.Username, statusCode, redirectionURL) + + switch authn.Object.Method { + case fasthttp.MethodHead: + ctx.SpecialRedirectNoBody(redirectionURL.String(), statusCode) + default: + ctx.SpecialRedirect(redirectionURL.String(), statusCode) + } } else { ctx.Logger.Infof("Access to %s (method %s) is not authorized to user %s, responding with status code %d", authn.Object.URL.String(), authn.Method, authn.Username, statusCode) ctx.ReplyUnauthorized() diff --git a/internal/handlers/handler_authz_impl_legacy_test.go b/internal/handlers/handler_authz_impl_legacy_test.go index 541920b6d..82d798d84 100644 --- a/internal/handlers/handler_authz_impl_legacy_test.go +++ b/internal/handlers/handler_authz_impl_legacy_test.go @@ -4,6 +4,7 @@ import ( "fmt" "net/url" "regexp" + "strings" "testing" "github.com/golang/mock/gomock" @@ -15,7 +16,7 @@ import ( "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" + "github.com/authelia/authelia/v4/internal/utils" ) func TestRunLegacyAuthzSuite(t *testing.T) { @@ -53,11 +54,7 @@ func (s *LegacyAuthzSuite) TestShouldHandleAllMethodsDeny() { 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.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) mock.Ctx.RequestCtx.QueryArgs().Set(queryArgRD, pairURI.AutheliaURI.String()) mock.Ctx.Request.Header.Set("X-Forwarded-Method", method) @@ -69,7 +66,7 @@ func (s *LegacyAuthzSuite) TestShouldHandleAllMethodsDeny() { authz.Handler(mock.Ctx) switch method { - case fasthttp.MethodGet, fasthttp.MethodOptions: + 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()) @@ -105,11 +102,7 @@ func (s *LegacyAuthzSuite) TestShouldHandleAllMethodsOverrideAutheliaURLDeny() { 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.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) mock.Ctx.RequestCtx.QueryArgs().Set(queryArgRD, pairURI.AutheliaURI.String()) mock.Ctx.Request.Header.Set("X-Forwarded-Method", method) @@ -121,7 +114,7 @@ func (s *LegacyAuthzSuite) TestShouldHandleAllMethodsOverrideAutheliaURLDeny() { authz.Handler(mock.Ctx) switch method { - case fasthttp.MethodGet, fasthttp.MethodOptions: + 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()) @@ -227,7 +220,7 @@ func (s *LegacyAuthzSuite) TestShouldHandleAllMethodsRDAutheliaURLOneFactorStatu authz.Handler(mock.Ctx) switch method { - case fasthttp.MethodGet, fasthttp.MethodOptions: + 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()) @@ -264,11 +257,7 @@ func (s *LegacyAuthzSuite) TestShouldHandleAllMethodsXHRDeny() { 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.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) mock.Ctx.RequestCtx.QueryArgs().Set(queryArgRD, pairURI.AutheliaURI.String()) mock.Ctx.Request.Header.Set("X-Forwarded-Method", method) @@ -317,11 +306,7 @@ func (s *LegacyAuthzSuite) TestShouldHandleInvalidMethodCharsDeny() { 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.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) mock.Ctx.Request.Header.Set("X-Forwarded-Method", method) mock.Ctx.Request.Header.Set(fasthttp.HeaderXForwardedProto, targetURI.Scheme) @@ -348,11 +333,7 @@ func (s *LegacyAuthzSuite) TestShouldHandleMissingHostDeny() { 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.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) mock.Ctx.Request.Header.Set("X-Forwarded-Method", method) mock.Ctx.Request.Header.Set(fasthttp.HeaderXForwardedProto, "https") @@ -384,11 +365,7 @@ func (s *LegacyAuthzSuite) TestShouldHandleAllMethodsAllow() { 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.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) mock.Ctx.Request.Header.Set("X-Forwarded-Method", method) mock.Ctx.Request.Header.Set(fasthttp.HeaderXForwardedProto, targetURI.Scheme) @@ -406,6 +383,56 @@ func (s *LegacyAuthzSuite) TestShouldHandleAllMethodsAllow() { } } +func (s *LegacyAuthzSuite) 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() + + s.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) + + s.setRequest(mock.Ctx, method, targetURI, true, false) + mock.Ctx.RequestCtx.QueryArgs().Set(queryArgRD, "https://auth.example.com") + + 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/") + + query := expected.Query() + query.Set(queryArgRD, targetURI.String()) + query.Set(queryArgRM, method) + expected.RawQuery = query.Encode() + + switch method { + case fasthttp.MethodHead: + assert.Equal(t, fasthttp.StatusFound, mock.Ctx.Response.StatusCode()) + assert.Nil(t, mock.Ctx.Response.Body()) + case fasthttp.MethodGet, fasthttp.MethodOptions: + assert.Equal(t, fasthttp.StatusFound, mock.Ctx.Response.StatusCode()) + assert.Equal(t, fmt.Sprintf(`%d %s`, utils.StringHTMLEscape(expected.String()), fasthttp.StatusFound, fasthttp.StatusMessage(fasthttp.StatusFound)), string(mock.Ctx.Response.Body())) + default: + assert.Equal(t, fasthttp.StatusSeeOther, mock.Ctx.Response.StatusCode()) + assert.Equal(t, fmt.Sprintf(`%d %s`, utils.StringHTMLEscape(expected.String()), fasthttp.StatusSeeOther, fasthttp.StatusMessage(fasthttp.StatusSeeOther)), string(mock.Ctx.Response.Body())) + } + + assert.Equal(t, expected.String(), string(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) { @@ -422,11 +449,7 @@ func (s *LegacyAuthzSuite) TestShouldHandleAllMethodsAllowXHR() { 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.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) mock.Ctx.Request.Header.Set("X-Forwarded-Method", method) mock.Ctx.Request.Header.Set(fasthttp.HeaderXForwardedProto, targetURI.Scheme) @@ -451,11 +474,7 @@ func (s *LegacyAuthzSuite) TestShouldHandleLegacyBasicAuth() { // TestShouldVeri 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.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) mock.Ctx.QueryArgs().Add("auth", "basic") mock.Ctx.Request.Header.Set("Authorization", "Basic am9objpwYXNzd29yZA==") @@ -540,11 +559,7 @@ func (s *LegacyAuthzSuite) TestShouldHandleLegacyBasicAuthFailures() { 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.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) mock.Ctx.QueryArgs().Add("auth", "basic") mock.Ctx.Request.Header.Set("X-Original-URL", "https://one-factor.example.com") @@ -593,11 +608,7 @@ func (s *LegacyAuthzSuite) TestShouldHandleInvalidURLForCVE202132637() { 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) + s.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) mock.Ctx.Request.Header.Set("X-Forwarded-Method", method) mock.Ctx.Request.Header.SetBytesKV([]byte(fasthttp.HeaderXForwardedProto), tc.scheme) diff --git a/internal/handlers/handler_authz_test.go b/internal/handlers/handler_authz_test.go index 432ee7bbd..bd32a0228 100644 --- a/internal/handlers/handler_authz_test.go +++ b/internal/handlers/handler_authz_test.go @@ -50,9 +50,12 @@ func (s *AuthzSuite) RequireParseRequestURI(rawURL string) *url.URL { return u } -type urlpair struct { - TargetURI *url.URL - AutheliaURI *url.URL +func (s *AuthzSuite) ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock *mocks.MockAutheliaCtx) { + 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) } func (s *AuthzSuite) Builder() (builder *AuthzBuilder) { @@ -87,11 +90,7 @@ func (s *AuthzSuite) TestShouldNotBeAbleToParseBasicAuth() { 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.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) targetURI := s.RequireParseRequestURI("https://test.example.com") @@ -124,11 +123,7 @@ func (s *AuthzSuite) TestShouldApplyDefaultPolicy() { 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.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) targetURI := s.RequireParseRequestURI("https://test.example.com") @@ -181,11 +176,7 @@ func (s *AuthzSuite) TestShouldDenyObject() { 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.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) targetURI := s.RequireParseRequestURI(tc.value) @@ -193,7 +184,12 @@ func (s *AuthzSuite) TestShouldDenyObject() { authz.Handler(mock.Ctx) - assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode()) + switch s.implementation { + case AuthzImplLegacy: + assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode()) + default: + assert.Equal(t, fasthttp.StatusBadRequest, mock.Ctx.Response.StatusCode()) + } }) } } @@ -209,11 +205,7 @@ func (s *AuthzSuite) TestShouldApplyPolicyOfBypassDomain() { 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.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) targetURI := s.RequireParseRequestURI("https://bypass.example.com") @@ -250,11 +242,7 @@ func (s *AuthzSuite) TestShouldVerifyFailureToGetDetailsUsingBasicScheme() { 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.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) targetURI := s.RequireParseRequestURI("https://bypass.example.com") @@ -299,11 +287,7 @@ func (s *AuthzSuite) TestShouldNotFailOnMissingEmail() { mock.Clock.Set(time.Now()) - for i, cookie := range mock.Ctx.Configuration.Session.Cookies { - mock.Ctx.Configuration.Session.Cookies[i].AutheliaURL = s.RequireParseRequestURI(fmt.Sprintf("https://auth.%s", cookie.Domain)) - } - - mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil) + s.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) targetURI := s.RequireParseRequestURI("https://bypass.example.com") @@ -340,11 +324,7 @@ func (s *AuthzSuite) TestShouldApplyPolicyOfOneFactorDomain() { 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.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) targetURI := s.RequireParseRequestURI("https://one-factor.example.com") @@ -392,11 +372,7 @@ func (s *AuthzSuite) TestShouldHandleAnyCaseSchemeParameter() { 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.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) targetURI := s.RequireParseRequestURI("https://one-factor.example.com") @@ -435,11 +411,7 @@ func (s *AuthzSuite) TestShouldApplyPolicyOfTwoFactorDomain() { 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.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) targetURI := s.RequireParseRequestURI("https://two-factor.example.com") @@ -483,11 +455,7 @@ func (s *AuthzSuite) TestShouldApplyPolicyOfDenyDomain() { 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.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) targetURI := s.RequireParseRequestURI("https://deny.example.com") @@ -534,11 +502,7 @@ func (s *AuthzSuite) TestShouldApplyPolicyOfOneFactorDomainWithAuthorizationHead 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.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) targetURI := s.RequireParseRequestURI("https://one-factor.example.com") @@ -584,11 +548,7 @@ func (s *AuthzSuite) TestShouldHandleAuthzWithoutHeaderNoCookie() { 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.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) targetURI := s.RequireParseRequestURI("https://one-factor.example.com") @@ -621,11 +581,7 @@ func (s *AuthzSuite) TestShouldHandleAuthzWithEmptyAuthorizationHeader() { 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.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) targetURI := s.RequireParseRequestURI("https://one-factor.example.com") @@ -660,11 +616,7 @@ func (s *AuthzSuite) TestShouldHandleAuthzWithAuthorizationHeaderInvalidPassword 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.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) targetURI := s.RequireParseRequestURI("https://one-factor.example.com") @@ -700,11 +652,7 @@ func (s *AuthzSuite) TestShouldHandleAuthzWithIncorrectAuthHeader() { // TestSho 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.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) targetURI := s.RequireParseRequestURI("https://one-factor.example.com") @@ -744,11 +692,7 @@ func (s *AuthzSuite) TestShouldDestroySessionWhenInactiveForTooLong() { mock.Ctx.Configuration.Session.Cookies[0].Inactivity = testInactivity - for i, cookie := range mock.Ctx.Configuration.Session.Cookies { - mock.Ctx.Configuration.Session.Cookies[i].AutheliaURL = s.RequireParseRequestURI(fmt.Sprintf("https://auth.%s", cookie.Domain)) - } - - mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil) + s.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) targetURI := s.RequireParseRequestURI("https://two-factor.example.com") @@ -796,11 +740,7 @@ func (s *AuthzSuite) TestShouldNotDestroySessionWhenInactiveForTooLongRememberMe mock.Ctx.Configuration.Session.Cookies[0].Inactivity = testInactivity - for i, cookie := range mock.Ctx.Configuration.Session.Cookies { - mock.Ctx.Configuration.Session.Cookies[i].AutheliaURL = s.RequireParseRequestURI(fmt.Sprintf("https://auth.%s", cookie.Domain)) - } - - mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil) + s.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) targetURI := s.RequireParseRequestURI("https://two-factor.example.com") @@ -850,11 +790,7 @@ func (s *AuthzSuite) TestShouldNotDestroySessionWhenNotInactiveForTooLong() { mock.Ctx.Configuration.Session.Cookies[0].Inactivity = testInactivity - for i, cookie := range mock.Ctx.Configuration.Session.Cookies { - mock.Ctx.Configuration.Session.Cookies[i].AutheliaURL = s.RequireParseRequestURI(fmt.Sprintf("https://auth.%s", cookie.Domain)) - } - - mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil) + s.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) targetURI := s.RequireParseRequestURI("https://two-factor.example.com") @@ -905,11 +841,7 @@ func (s *AuthzSuite) TestShouldUpdateInactivityTimestampEvenWhenHittingForbidden mock.Ctx.Configuration.Session.Cookies[0].Inactivity = testInactivity - for i, cookie := range mock.Ctx.Configuration.Session.Cookies { - mock.Ctx.Configuration.Session.Cookies[i].AutheliaURL = s.RequireParseRequestURI(fmt.Sprintf("https://auth.%s", cookie.Domain)) - } - - mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil) + s.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) targetURI := s.RequireParseRequestURI("https://deny.example.com") @@ -971,11 +903,7 @@ func (s *AuthzSuite) TestShouldNotRefreshUserDetailsFromBackendWhenRefreshDisabl mock.Ctx.Configuration.AuthenticationBackend.RefreshInterval = schema.ProfileRefreshDisabled mock.Ctx.Configuration.Session.Cookies[0].Inactivity = testInactivity - for i, cookie := range mock.Ctx.Configuration.Session.Cookies { - mock.Ctx.Configuration.Session.Cookies[i].AutheliaURL = s.RequireParseRequestURI(fmt.Sprintf("https://auth.%s", cookie.Domain)) - } - - mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil) + s.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) targetURI := s.RequireParseRequestURI("https://two-factor.example.com") @@ -1057,11 +985,7 @@ func (s *AuthzSuite) TestShouldDestroySessionWhenUserDoesNotExist() { mock.Ctx.Configuration.Session.Cookies[0].Inactivity = testInactivity - for i, cookie := range mock.Ctx.Configuration.Session.Cookies { - mock.Ctx.Configuration.Session.Cookies[i].AutheliaURL = s.RequireParseRequestURI(fmt.Sprintf("https://auth.%s", cookie.Domain)) - } - - mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil) + s.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) targetURI := s.RequireParseRequestURI("https://two-factor.example.com") @@ -1149,11 +1073,7 @@ func (s *AuthzSuite) TestShouldUpdateRemovedUserGroupsFromBackendAndDeny() { mock.Ctx.Configuration.Session.Cookies[0].Inactivity = testInactivity - for i, cookie := range mock.Ctx.Configuration.Session.Cookies { - mock.Ctx.Configuration.Session.Cookies[i].AutheliaURL = s.RequireParseRequestURI(fmt.Sprintf("https://auth.%s", cookie.Domain)) - } - - mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil) + s.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) targetURI := s.RequireParseRequestURI("https://admin.example.com") @@ -1239,11 +1159,7 @@ func (s *AuthzSuite) TestShouldUpdateAddedUserGroupsFromBackendAndDeny() { mock.Ctx.Configuration.Session.Cookies[0].Inactivity = testInactivity - for i, cookie := range mock.Ctx.Configuration.Session.Cookies { - mock.Ctx.Configuration.Session.Cookies[i].AutheliaURL = s.RequireParseRequestURI(fmt.Sprintf("https://auth.%s", cookie.Domain)) - } - - mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil) + s.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) targetURI := s.RequireParseRequestURI("https://admin.example.com") @@ -1328,11 +1244,7 @@ func (s *AuthzSuite) TestShouldCheckValidSessionUsernameHeaderAndReturn200() { mock.Ctx.Configuration.Session.Cookies[0].Inactivity = testInactivity - for i, cookie := range mock.Ctx.Configuration.Session.Cookies { - mock.Ctx.Configuration.Session.Cookies[i].AutheliaURL = s.RequireParseRequestURI(fmt.Sprintf("https://auth.%s", cookie.Domain)) - } - - mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil) + s.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) targetURI := s.RequireParseRequestURI("https://one-factor.example.com") @@ -1385,11 +1297,7 @@ func (s *AuthzSuite) TestShouldCheckInvalidSessionUsernameHeaderAndReturn401AndD mock.Ctx.Configuration.Session.Cookies[0].Inactivity = testInactivity - for i, cookie := range mock.Ctx.Configuration.Session.Cookies { - mock.Ctx.Configuration.Session.Cookies[i].AutheliaURL = s.RequireParseRequestURI(fmt.Sprintf("https://auth.%s", cookie.Domain)) - } - - mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil) + s.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) targetURI := s.RequireParseRequestURI("https://one-factor.example.com") @@ -1462,11 +1370,7 @@ func (s *AuthzSuite) TestShouldNotRedirectRequestsForBypassACLWhenInactiveForToo mock.Ctx.Configuration.Session.Cookies[0].Inactivity = testInactivity - for i, cookie := range mock.Ctx.Configuration.Session.Cookies { - mock.Ctx.Configuration.Session.Cookies[i].AutheliaURL = s.RequireParseRequestURI(fmt.Sprintf("https://auth.%s", cookie.Domain)) - } - - mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil) + s.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) targetURI := s.RequireParseRequestURI("https://bypass.example.com") @@ -1520,7 +1424,7 @@ func (s *AuthzSuite) TestShouldNotRedirectRequestsForBypassACLWhenInactiveForToo } func (s *AuthzSuite) TestShouldFailToParsePortalURL() { - if s.setRequest == nil || s.implementation == AuthzImplAuthRequest { + if s.setRequest == nil { s.T().Skip() } @@ -1538,20 +1442,20 @@ func (s *AuthzSuite) TestShouldFailToParsePortalURL() { mock.Ctx.Configuration.Session.Cookies[0].Inactivity = testInactivity - for i, cookie := range mock.Ctx.Configuration.Session.Cookies { - mock.Ctx.Configuration.Session.Cookies[i].AutheliaURL = s.RequireParseRequestURI(fmt.Sprintf("https://auth.%s", cookie.Domain)) - } - - mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil) + s.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) targetURI := s.RequireParseRequestURI("https://bypass.example.com") s.setRequest(mock.Ctx, fasthttp.MethodGet, targetURI, true, false) + expected := fasthttp.StatusBadRequest + switch s.implementation { case AuthzImplLegacy: + expected = fasthttp.StatusUnauthorized + mock.Ctx.RequestCtx.QueryArgs().Set(queryArgRD, "JKL$#N%KJ#@$N") - case AuthzImplForwardAuth: + case AuthzImplForwardAuth, AuthzImplAuthRequest: mock.Ctx.RequestCtx.QueryArgs().Set("authelia_url", "JKL$#N%KJ#@$N") case AuthzImplExtAuthz: mock.Ctx.Request.Header.Set("X-Authelia-URL", "JKL$#N%KJ#@$N") @@ -1559,7 +1463,10 @@ func (s *AuthzSuite) TestShouldFailToParsePortalURL() { authz.Handler(mock.Ctx) - s.Equal(fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode()) + s.Equal(expected, mock.Ctx.Response.StatusCode()) + s.Equal(fmt.Sprintf("%d %s", expected, fasthttp.StatusMessage(expected)), string(mock.Ctx.Response.Body())) + s.Equal("", string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderLocation))) + s.Equal("text/plain; charset=utf-8", string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderContentType))) } func setRequestXHRValues(ctx *middlewares.AutheliaCtx, accept, xhr bool) { @@ -1571,3 +1478,8 @@ func setRequestXHRValues(ctx *middlewares.AutheliaCtx, accept, xhr bool) { ctx.Request.Header.Set(fasthttp.HeaderXRequestedWith, "XMLHttpRequest") } } + +type urlpair struct { + TargetURI *url.URL + AutheliaURI *url.URL +} diff --git a/internal/handlers/handler_authz_types.go b/internal/handlers/handler_authz_types.go index 549e7505f..ca2bfa7a1 100644 --- a/internal/handlers/handler_authz_types.go +++ b/internal/handlers/handler_authz_types.go @@ -10,7 +10,8 @@ import ( "github.com/authelia/authelia/v4/internal/session" ) -// Authz is a type which is a effectively is a middlewares.RequestHandler for authorization requests. +// Authz is a type which is a effectively is a middlewares.RequestHandler for authorization requests. This should NOT be +// manually used and developers should instead use NewAuthzBuilder. type Authz struct { config AuthzConfig @@ -23,7 +24,7 @@ type Authz struct { handleAuthorized HandlerAuthzAuthorized handleUnauthorized HandlerAuthzUnauthorized - legacy bool + implementation AuthzImplementation } // HandlerAuthzUnauthorized is a Authz handler func that handles unauthorized responses. @@ -75,20 +76,17 @@ type Authn struct { // 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 + // StatusCodeBadRequest is sent for configuration issues prior to performing authorization checks. It's set by the + // builder. + StatusCodeBadRequest int } // AuthzBuilder is a builder pattern for the Authz type. type AuthzBuilder struct { - config AuthzConfig - impl AuthzImplementation - strategies []AuthnStrategy + config AuthzConfig + implementation AuthzImplementation + strategies []AuthnStrategy } // AuthnStrategy is a strategy used for Authz authentication. diff --git a/internal/handlers/handler_authz_util.go b/internal/handlers/handler_authz_util.go index 5fadcd953..44b034ced 100644 --- a/internal/handlers/handler_authz_util.go +++ b/internal/handlers/handler_authz_util.go @@ -1,9 +1,6 @@ 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" @@ -23,7 +20,7 @@ func friendlyMethod(m string) (fm string) { func friendlyUsername(username string) (fusername string) { switch username { case "": - return "" + return anonymous default: return username } @@ -54,39 +51,55 @@ func generateVerifySessionHasUpToDateProfileTraceLogs(ctx *middlewares.AutheliaC 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, ", "))) + fields := map[string]any{"username": userSession.Username} + msg := "User session groups are current" + + if len(groupsAdded) != 0 || len(groupsRemoved) != 0 { + if len(groupsAdded) != 0 { + fields["added"] = groupsAdded + } + + if len(groupsRemoved) != 0 { + fields["removed"] = groupsRemoved + } + + msg = "User session groups were updated" } - if len(groupsRemoved) != 0 { - groupsDelta = append(groupsDelta, fmt.Sprintf("removed: %s.", strings.Join(groupsRemoved, ", "))) - } + ctx.Logger.WithFields(fields).Trace(msg) - if len(groupsDelta) != 0 { - ctx.Logger.Tracef("Updated groups detected for %s. %s", userSession.Username, strings.Join(groupsDelta, " ")) + if len(emailsAdded) != 0 || len(emailsRemoved) != 0 { + if len(emailsAdded) != 0 { + fields["added"] = emailsAdded + } else { + delete(fields, "added") + } + + if len(emailsRemoved) != 0 { + fields["removed"] = emailsRemoved + } else { + delete(fields, "removed") + } + + msg = "User session emails were updated" } else { - ctx.Logger.Tracef("No updated groups detected for %s", userSession.Username) + msg = "User session emails are current" + + delete(fields, "added") + delete(fields, "removed") } - 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) - } + ctx.Logger.WithFields(fields).Trace(msg) if nameDelta { - ctx.Logger.Tracef("Updated display name detected for %s. Added: %s. Removed: %s.", userSession.Username, details.DisplayName, userSession.DisplayName) + ctx.Logger. + WithFields(map[string]any{ + "username": userSession.Username, + "before": userSession.DisplayName, + "after": details.DisplayName, + }). + Trace("User session display name updated") } else { - ctx.Logger.Tracef("No updated display name detected for %s", userSession.Username) + ctx.Logger.Trace("User session display name is current") } } diff --git a/internal/handlers/handler_oidc_userinfo.go b/internal/handlers/handler_oidc_userinfo.go index ddb61cb35..3e1314d72 100644 --- a/internal/handlers/handler_oidc_userinfo.go +++ b/internal/handlers/handler_oidc_userinfo.go @@ -33,7 +33,7 @@ func OpenIDConnectUserinfo(ctx *middlewares.AutheliaCtx, rw http.ResponseWriter, req.Context(), fosite.AccessTokenFromRequest(req), fosite.AccessToken, oidcSession); err != nil { rfc := fosite.ErrorToRFC6749Error(err) - ctx.Logger.Errorf("UserInfo Request failed with error: %+v", rfc) + ctx.Logger.Errorf("UserInfo Request failed with error: %s", rfc.WithExposeDebug(true).GetDescription()) if rfc.StatusCode() == http.StatusUnauthorized { rw.Header().Set(fasthttp.HeaderWWWAuthenticate, fmt.Sprintf(`Bearer error="%s",error_description="%s"`, rfc.ErrorField, rfc.GetDescription())) @@ -47,7 +47,7 @@ func OpenIDConnectUserinfo(ctx *middlewares.AutheliaCtx, rw http.ResponseWriter, clientID := requester.GetClient().GetID() if tokenType != fosite.AccessToken { - ctx.Logger.Errorf("UserInfo Request with id '%s' on client with id '%s' failed with error: bearer authorization failed as the token is not an access_token", requester.GetID(), client.GetID()) + ctx.Logger.Errorf("UserInfo Request with id '%s' on client with id '%s' failed with error: bearer authorization failed as the token is not an access token", requester.GetID(), client.GetID()) errStr := "Only access tokens are allowed in the authorization header." rw.Header().Set(fasthttp.HeaderWWWAuthenticate, fmt.Sprintf(`Bearer error="invalid_token",error_description="%s"`, errStr)) @@ -57,7 +57,11 @@ func OpenIDConnectUserinfo(ctx *middlewares.AutheliaCtx, rw http.ResponseWriter, } if client, err = ctx.Providers.OpenIDConnect.GetFullClient(clientID); err != nil { - ctx.Providers.OpenIDConnect.WriteError(rw, req, errors.WithStack(fosite.ErrServerError.WithHint("Unable to assert type of client"))) + rfc := fosite.ErrorToRFC6749Error(err) + + ctx.Logger.Errorf("UserInfo Request with id '%s' on client with id '%s' failed to retrieve client configuration with error: %s", requester.GetID(), client.GetID(), rfc.WithExposeDebug(true).GetDescription()) + + ctx.Providers.OpenIDConnect.WriteError(rw, req, errors.WithStack(rfc)) return } @@ -100,7 +104,7 @@ func OpenIDConnectUserinfo(ctx *middlewares.AutheliaCtx, rw http.ResponseWriter, var jti uuid.UUID if jti, err = uuid.NewRandom(); err != nil { - ctx.Providers.OpenIDConnect.WriteError(rw, req, fosite.ErrServerError.WithHintf("Could not generate JTI.")) + ctx.Providers.OpenIDConnect.WriteError(rw, req, fosite.ErrServerError.WithHint("Could not generate JTI.")) return } @@ -120,9 +124,9 @@ func OpenIDConnectUserinfo(ctx *middlewares.AutheliaCtx, rw http.ResponseWriter, return } - rw.Header().Set("Content-Type", "application/jwt") + rw.Header().Set(fasthttp.HeaderContentType, "application/jwt") _, _ = rw.Write([]byte(token)) - case "none", "": + case oidc.SigningAlgorithmNone, "": ctx.Providers.OpenIDConnect.Write(rw, req, claims) default: ctx.Providers.OpenIDConnect.WriteError(rw, req, errors.WithStack(fosite.ErrServerError.WithHintf("Unsupported UserInfo signing algorithm '%s'.", client.UserinfoSigningAlgorithm))) diff --git a/internal/middlewares/authelia_context.go b/internal/middlewares/authelia_context.go index 8d1f06174..bb7840c33 100644 --- a/internal/middlewares/authelia_context.go +++ b/internal/middlewares/authelia_context.go @@ -563,13 +563,27 @@ func (ctx *AutheliaCtx) AcceptsMIME(mime string) (acceptsMime bool) { } // SpecialRedirect performs a redirect similar to fasthttp.RequestCtx except it allows statusCode 401 and includes body -// content in the form of a link to the location. +// content in the form of a link to the location if the request method was not head. func (ctx *AutheliaCtx) SpecialRedirect(uri string, statusCode int) { + var u []byte + + u, statusCode = ctx.setSpecialRedirect(uri, statusCode) + + ctx.SetContentTypeTextHTML() + ctx.SetBodyString(fmt.Sprintf("%d %s", utils.StringHTMLEscape(string(u)), statusCode, fasthttp.StatusMessage(statusCode))) +} + +// SpecialRedirectNoBody performs a redirect similar to fasthttp.RequestCtx except it allows statusCode 401 and includes +// no body. +func (ctx *AutheliaCtx) SpecialRedirectNoBody(uri string, statusCode int) { + _, _ = ctx.setSpecialRedirect(uri, statusCode) +} + +func (ctx *AutheliaCtx) setSpecialRedirect(uri string, statusCode int) ([]byte, int) { if statusCode < fasthttp.StatusMovedPermanently || (statusCode > fasthttp.StatusSeeOther && statusCode != fasthttp.StatusTemporaryRedirect && statusCode != fasthttp.StatusPermanentRedirect && statusCode != fasthttp.StatusUnauthorized) { statusCode = fasthttp.StatusFound } - ctx.SetContentTypeTextHTML() ctx.SetStatusCode(statusCode) u := fasthttp.AcquireURI() @@ -577,11 +591,13 @@ func (ctx *AutheliaCtx) SpecialRedirect(uri string, statusCode int) { ctx.URI().CopyTo(u) u.Update(uri) - ctx.Response.Header.SetBytesKV(headerLocation, u.FullURI()) + raw := u.FullURI() - ctx.SetBodyString(fmt.Sprintf("%d %s", utils.StringHTMLEscape(string(u.FullURI())), statusCode, fasthttp.StatusMessage(statusCode))) + ctx.Response.Header.SetBytesKV(headerLocation, raw) fasthttp.ReleaseURI(u) + + return raw, statusCode } // RecordAuthn records authentication metrics. diff --git a/internal/oidc/store.go b/internal/oidc/store.go index 9089137e1..a5a181c2b 100644 --- a/internal/oidc/store.go +++ b/internal/oidc/store.go @@ -79,7 +79,7 @@ func (s *Store) GetClientPolicy(id string) (level authorization.Level) { func (s *Store) GetFullClient(id string) (client *Client, err error) { client, ok := s.clients[id] if !ok { - return nil, fosite.ErrNotFound + return nil, fosite.ErrInvalidClient } return client, nil diff --git a/internal/oidc/store_test.go b/internal/oidc/store_test.go index c5bff5b0c..580e864e4 100644 --- a/internal/oidc/store_test.go +++ b/internal/oidc/store_test.go @@ -59,7 +59,7 @@ func TestOpenIDConnectStore_GetInternalClient(t *testing.T) { }, nil) client, err := s.GetClient(context.Background(), "myinvalidclient") - assert.EqualError(t, err, "not_found") + assert.EqualError(t, err, "invalid_client") assert.Nil(t, client) client, err = s.GetClient(context.Background(), "myclient") @@ -113,7 +113,7 @@ func TestOpenIDConnectStore_GetInternalClient_InvalidClient(t *testing.T) { client, err := s.GetFullClient("another-client") assert.Nil(t, client) - assert.EqualError(t, err, "not_found") + assert.EqualError(t, err, "invalid_client") } func TestOpenIDConnectStore_IsValidClientID(t *testing.T) { diff --git a/internal/server/handlers.go b/internal/server/handlers.go index e2d07d1b7..f2ddbea66 100644 --- a/internal/server/handlers.go +++ b/internal/server/handlers.go @@ -196,8 +196,8 @@ func handleRouter(config *schema.Configuration, providers middlewares.Providers) case "legacy": log. WithField("path_prefix", pathAuthzLegacy). - WithField("impl", endpoint.Implementation). - WithField("methods", []string{"*"}). + WithField("implementation", endpoint.Implementation). + WithField("methods", "*"). Trace("Registering Authz Endpoint") r.ANY(pathAuthzLegacy, handler) @@ -207,8 +207,8 @@ func handleRouter(config *schema.Configuration, providers middlewares.Providers) case handlers.AuthzImplLegacy.String(), handlers.AuthzImplExtAuthz.String(): log. WithField("path_prefix", uri). - WithField("impl", endpoint.Implementation). - WithField("methods", []string{"*"}). + WithField("implementation", endpoint.Implementation). + WithField("methods", "*"). Trace("Registering Authz Endpoint") r.ANY(uri, handler) @@ -216,7 +216,7 @@ func handleRouter(config *schema.Configuration, providers middlewares.Providers) default: log. WithField("path", uri). - WithField("impl", endpoint.Implementation). + WithField("implementation", endpoint.Implementation). WithField("methods", []string{fasthttp.MethodGet, fasthttp.MethodHead}). Trace("Registering Authz Endpoint") diff --git a/internal/server/template.go b/internal/server/template.go index be71d9874..57a120c01 100644 --- a/internal/server/template.go +++ b/internal/server/template.go @@ -61,16 +61,27 @@ func ServeTemplatedFile(t templates.Template, opts *TemplatedFileOptions) middle var ( rememberMe string + baseURL string + domain string provider *session.Session ) if provider, err = ctx.GetSessionProvider(); err == nil { + if provider.Config.AutheliaURL != nil { + baseURL = provider.Config.AutheliaURL.String() + } else { + baseURL = ctx.RootURLSlash().String() + } + + domain = provider.Config.Domain rememberMe = strconv.FormatBool(!provider.Config.DisableRememberMe) + } else { + baseURL = ctx.RootURLSlash().String() } data := &bytes.Buffer{} - if err = t.Execute(data, opts.CommonData(ctx.BasePath(), ctx.RootURLSlash().String(), nonce, logoOverride, rememberMe)); err != nil { + if err = t.Execute(data, opts.CommonData(ctx.BasePath(), baseURL, domain, nonce, logoOverride, rememberMe)); err != nil { ctx.RequestCtx.Error("an error occurred", fasthttp.StatusServiceUnavailable) ctx.Logger.WithError(err).Errorf("Error occcurred rendering template") @@ -118,11 +129,28 @@ func ServeTemplatedOpenAPI(t templates.Template, opts *TemplatedFileOptions) mid ctx.SetContentTypeTextPlain() } - var err error + var ( + baseURL string + domain string + provider *session.Session + err error + ) + + if provider, err = ctx.GetSessionProvider(); err == nil { + if provider.Config.AutheliaURL != nil { + baseURL = provider.Config.AutheliaURL.String() + } else { + baseURL = ctx.RootURLSlash().String() + } + + domain = provider.Config.Domain + } else { + baseURL = ctx.RootURLSlash().String() + } data := &bytes.Buffer{} - if err = t.Execute(data, opts.OpenAPIData(ctx.BasePath(), ctx.RootURLSlash().String(), nonce)); err != nil { + if err = t.Execute(data, opts.OpenAPIData(ctx.BasePath(), baseURL, domain, nonce)); err != nil { ctx.RequestCtx.Error("an error occurred", fasthttp.StatusServiceUnavailable) ctx.Logger.WithError(err).Errorf("Error occcurred rendering template") @@ -285,14 +313,15 @@ type TemplatedFileOptions struct { } // CommonData returns a TemplatedFileCommonData with the dynamic options. -func (options *TemplatedFileOptions) CommonData(base, baseURL, nonce, logoOverride, rememberMe string) TemplatedFileCommonData { +func (options *TemplatedFileOptions) CommonData(base, baseURL, domain, nonce, logoOverride, rememberMe string) TemplatedFileCommonData { if rememberMe != "" { - return options.commonDataWithRememberMe(base, baseURL, nonce, logoOverride, rememberMe) + return options.commonDataWithRememberMe(base, baseURL, domain, nonce, logoOverride, rememberMe) } return TemplatedFileCommonData{ Base: base, BaseURL: baseURL, + Domain: domain, CSPNonce: nonce, LogoOverride: logoOverride, DuoSelfEnrollment: options.DuoSelfEnrollment, @@ -307,10 +336,11 @@ func (options *TemplatedFileOptions) CommonData(base, baseURL, nonce, logoOverri } // CommonDataWithRememberMe returns a TemplatedFileCommonData with the dynamic options. -func (options *TemplatedFileOptions) commonDataWithRememberMe(base, baseURL, nonce, logoOverride, rememberMe string) TemplatedFileCommonData { +func (options *TemplatedFileOptions) commonDataWithRememberMe(base, baseURL, domain, nonce, logoOverride, rememberMe string) TemplatedFileCommonData { return TemplatedFileCommonData{ Base: base, BaseURL: baseURL, + Domain: domain, CSPNonce: nonce, LogoOverride: logoOverride, DuoSelfEnrollment: options.DuoSelfEnrollment, @@ -323,10 +353,11 @@ func (options *TemplatedFileOptions) commonDataWithRememberMe(base, baseURL, non } // OpenAPIData returns a TemplatedFileOpenAPIData with the dynamic options. -func (options *TemplatedFileOptions) OpenAPIData(base, baseURL, nonce string) TemplatedFileOpenAPIData { +func (options *TemplatedFileOptions) OpenAPIData(base, baseURL, domain, nonce string) TemplatedFileOpenAPIData { return TemplatedFileOpenAPIData{ Base: base, BaseURL: baseURL, + Domain: domain, CSPNonce: nonce, Session: options.Session, @@ -343,6 +374,7 @@ func (options *TemplatedFileOptions) OpenAPIData(base, baseURL, nonce string) Te type TemplatedFileCommonData struct { Base string BaseURL string + Domain string CSPNonce string LogoOverride string DuoSelfEnrollment string @@ -359,6 +391,7 @@ type TemplatedFileCommonData struct { type TemplatedFileOpenAPIData struct { Base string BaseURL string + Domain string CSPNonce string Session string PasswordReset bool diff --git a/internal/server/template_test.go b/internal/server/template_test.go new file mode 100644 index 000000000..e85730c51 --- /dev/null +++ b/internal/server/template_test.go @@ -0,0 +1,82 @@ +package server + +import ( + "io/fs" + "net/url" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/valyala/fasthttp" + + "github.com/authelia/authelia/v4/internal/configuration/schema" + "github.com/authelia/authelia/v4/internal/mocks" + "github.com/authelia/authelia/v4/internal/session" + "github.com/authelia/authelia/v4/internal/templates" +) + +const ( + assetsOpenAPIPath = "public_html/api/openapi.yml" + localOpenAPIPath = "../../api/openapi.yml" +) + +type ReadFileOpenAPI struct{} + +func (lfs *ReadFileOpenAPI) Open(name string) (fs.File, error) { + switch name { + case assetsOpenAPIPath: + return os.Open(localOpenAPIPath) + default: + return assets.Open(name) + } +} + +func (lfs *ReadFileOpenAPI) ReadFile(name string) ([]byte, error) { + switch name { + case assetsOpenAPIPath: + return os.ReadFile(localOpenAPIPath) + default: + return assets.ReadFile(name) + } +} + +func TestShouldTemplateOpenAPI(t *testing.T) { + provider, err := templates.New(templates.Config{}) + require.NoError(t, err) + + fs := &ReadFileOpenAPI{} + + require.NoError(t, provider.LoadTemplatedAssets(fs)) + + mock := mocks.NewMockAutheliaCtx(t) + + mock.Ctx.Configuration.Server = schema.DefaultServerConfiguration + mock.Ctx.Configuration.Session = schema.SessionConfiguration{ + Cookies: []schema.SessionCookieConfiguration{ + { + SessionCookieCommonConfiguration: schema.SessionCookieCommonConfiguration{ + Domain: "example.com", + }, + AutheliaURL: &url.URL{Scheme: "https", Host: "auth.example.com", Path: "/"}, + }, + }, + } + + mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil) + + opts := NewTemplatedFileOptions(&mock.Ctx.Configuration) + + handler := ServeTemplatedOpenAPI(provider.GetAssetOpenAPISpecTemplate(), opts) + + mock.Ctx.Request.Header.Set(fasthttp.HeaderXForwardedProto, "https") + mock.Ctx.Request.Header.Set(fasthttp.HeaderXForwardedHost, "example.com") + mock.Ctx.Request.Header.Set("X-Forwarded-Uri", "/api/openapi.yml") + + handler(mock.Ctx) + + assert.Equal(t, fasthttp.StatusOK, mock.Ctx.Response.StatusCode()) + assert.NotEqual(t, "", string(mock.Ctx.Response.Body())) + + assert.Contains(t, string(mock.Ctx.Response.Body()), "example: https://auth.example.com/?rd=https%3A%2F%2Fexample.com&rm=GET") +} diff --git a/internal/suites/ActiveDirectory/configuration.yml b/internal/suites/ActiveDirectory/configuration.yml index 26044c7e5..88c297b54 100644 --- a/internal/suites/ActiveDirectory/configuration.yml +++ b/internal/suites/ActiveDirectory/configuration.yml @@ -31,10 +31,12 @@ authentication_backend: session: secret: unsecure_session_secret - domain: example.com expiration: 3600 # 1 hour inactivity: 300 # 5 minutes remember_me: 1y + cookies: + - domain: 'example.com' + authelia_url: 'https://login.example.com:8080' storage: encryption_key: a_not_so_secure_encryption_key diff --git a/internal/suites/HighAvailability/configuration.yml b/internal/suites/HighAvailability/configuration.yml index c1d3002e5..8794cbd35 100644 --- a/internal/suites/HighAvailability/configuration.yml +++ b/internal/suites/HighAvailability/configuration.yml @@ -87,7 +87,9 @@ session: secret: unsecure_session_secret expiration: 3600 # 1 hour inactivity: 300 # 5 minutes - domain: example.com + cookies: + - domain: 'example.com' + authelia_url: 'https://login.example.com:8080' redis: username: authelia password: redis-user-password diff --git a/internal/suites/MultiCookieDomain/configuration.yml b/internal/suites/MultiCookieDomain/configuration.yml index 8102b658e..d19f591f9 100644 --- a/internal/suites/MultiCookieDomain/configuration.yml +++ b/internal/suites/MultiCookieDomain/configuration.yml @@ -32,13 +32,14 @@ session: cookies: - name: 'authelia_session' domain: 'example.com' + authelia_url: 'https://login.example.com:8080' - name: 'example2_session' domain: 'example2.com' - authelia_url: 'https://login.example2.com' + authelia_url: 'https://login.example2.com:8080' remember_me: -1 - name: 'authelia_session' domain: 'example3.com' - authelia_url: 'https://login.example3.com' + authelia_url: 'https://login.example3.com:8080' storage: encryption_key: a_not_so_secure_encryption_key diff --git a/internal/suites/example/compose/haproxy/auth-request.lua b/internal/suites/example/compose/haproxy/auth-request.lua index 37b75f160..1504948d4 100644 --- a/internal/suites/example/compose/haproxy/auth-request.lua +++ b/internal/suites/example/compose/haproxy/auth-request.lua @@ -19,9 +19,42 @@ -- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -- SOFTWARE. +-- +-- SPDX-License-Identifier: MIT local http = require("haproxy-lua-http") +core.register_action("auth-request", { "http-req" }, function(txn, be, path) + auth_request(txn, be, path, "HEAD", ".*", "-", "-") +end, 2) + +core.register_action("auth-intercept", { "http-req" }, function(txn, be, path, method, hdr_req, hdr_succeed, hdr_fail) + hdr_req = globToLuaPattern(hdr_req) + hdr_succeed = globToLuaPattern(hdr_succeed) + hdr_fail = globToLuaPattern(hdr_fail) + auth_request(txn, be, path, method, hdr_req, hdr_succeed, hdr_fail) +end, 6) + +function globToLuaPattern(glob) + if glob == "-" then + return "-" + end + -- magic chars: '^', '$', '(', ')', '%', '.', '[', ']', '*', '+', '-', '?' + -- https://www.lua.org/manual/5.4/manual.html#6.4.1 + -- + -- this chain is: + -- 1. escaping all the magic chars, adding a `%` in front of all of them, + -- except the chars being processed later in the chain; + -- 1.1. all the chars inside the [set] are magic chars and have special + -- meaning inside a set, so we're also escaping all of them to avoid + -- misbehavior; + -- 2. converting "match all" `*` and "match one" `?` to their Lua pattern + -- counterparts; + -- 3. adding start and finish boundaries outside the whole string and, + -- being a comma-separated list, between every single item as well. + return "^" .. glob:gsub("[%^%$%(%)%%%.%[%]%+%-]", "%%%1"):gsub("*", ".*"):gsub("?", "."):gsub(",", "$,^") .. "$" +end + function set_var_pre_2_2(txn, var, value) return txn:set_var(var, value) end @@ -44,8 +77,49 @@ function sanitize_header_for_variable(header) return header:gsub("[^a-zA-Z0-9]", "_") end +-- header_match checks whether the provided header matches the pattern. +-- pattern is a comma-separated list of Lua Patterns. +function header_match(header, pattern) + if header == "content-length" or header == "host" or pattern == "-" then + return false + end + for p in pattern:gmatch("[^,]*") do + if header:match(p) then + return true + end + end + return false +end -core.register_action("auth-request", { "http-req" }, function(txn, be, path) +-- Terminates the transaction and sends the provided response to the client. +-- hdr_fail filters header names that should be provided using Lua Patterns. +function send_response(txn, response, hdr_fail) + local reply = txn:reply() + if response then + reply:set_status(response.status_code) + for header, value in response:get_headers(false) do + if header_match(header, hdr_fail) then + reply:add_header(header, value) + end + end + if response.content then + reply:set_body(response.content) + end + else + reply:set_status(500) + end + txn:done(reply) +end + +-- auth_request makes the request to the external authentication service +-- and waits for the response. hdr_* params receive a comma-separated +-- list of Lua Patterns used to identify the headers that should be +-- copied between the requests and responses. A dash `-` in these params +-- mean that the headers shouldn't be copied at all. +-- Special values and behavior: +-- * method == "*": call the auth service using the same method used by the client. +-- * hdr_fail == "-": make the Lua script to not terminate the request. +function auth_request(txn, be, path, method, hdr_req, hdr_succeed, hdr_fail) set_var(txn, "txn.auth_response_successful", false) -- Check whether the given backend exists. @@ -75,7 +149,7 @@ core.register_action("auth-request", { "http-req" }, function(txn, be, path) -- socket.http's format. local headers = {} for header, values in pairs(txn.http:req_get_headers()) do - if header ~= 'content-length' then + if header_match(header, hdr_req) then for i, v in pairs(values) do if headers[header] == nil then headers[header] = v @@ -87,28 +161,46 @@ core.register_action("auth-request", { "http-req" }, function(txn, be, path) end -- Make request to backend. - local response, err = http.head { + if method == "*" then + method = txn.sf:method() + end + local response, err = http.send(method:upper(), { url = "http://" .. addr .. path, headers = headers, - } + }) + + -- `terminate_on_failure == true` means that the Lua script should send the response + -- and terminate the transaction in the case of a failure. This will happen when + -- hdr_fail content isn't a dash `-`. + local terminate_on_failure = hdr_fail ~= "-" -- Check whether we received a valid HTTP response. if response == nil then txn:Warning("Failure in auth-request backend '" .. be .. "': " .. err) set_var(txn, "txn.auth_response_code", 500) + if terminate_on_failure then + send_response(txn) + end return end set_var(txn, "txn.auth_response_code", response.status_code) + local response_ok = 200 <= response.status_code and response.status_code < 300 for header, value in response:get_headers(true) do set_var(txn, "req.auth_response_header." .. sanitize_header_for_variable(header), value) + if response_ok and hdr_succeed ~= "-" and header_match(header, hdr_succeed) then + txn.http:req_set_header(header, value) + end end - -- 2xx: Allow request. - if 200 <= response.status_code and response.status_code < 300 then + -- response_ok means 2xx: allow request. + if response_ok then set_var(txn, "txn.auth_response_successful", true) - -- Don't allow other codes. + -- Don't allow codes < 200 or >= 300. + -- Forward the response to the client if required. + elseif terminate_on_failure then + send_response(txn, response, hdr_fail) -- Codes with Location: Passthrough location at redirect. elseif response.status_code == 301 or response.status_code == 302 or response.status_code == 303 or response.status_code == 307 or response.status_code == 308 then set_var(txn, "txn.auth_response_location", response:get_header("location", "last")) @@ -116,4 +208,4 @@ core.register_action("auth-request", { "http-req" }, function(txn, be, path) elseif response.status_code ~= 401 and response.status_code ~= 403 then txn:Warning("Invalid status code in auth-request backend '" .. be .. "': " .. response.status_code) end -end, 2) +end diff --git a/internal/suites/example/compose/haproxy/haproxy.cfg b/internal/suites/example/compose/haproxy/haproxy.cfg index ae9529d2c..63c471284 100644 --- a/internal/suites/example/compose/haproxy/haproxy.cfg +++ b/internal/suites/example/compose/haproxy/haproxy.cfg @@ -42,25 +42,17 @@ frontend fe_http http-request set-var(req.scheme) str(https) if { ssl_fc } http-request set-var(req.scheme) str(http) if !{ ssl_fc } http-request set-var(req.questionmark) str(?) if { query -m found } - http-request set-var(req.method) str(CONNECT) if { method CONNECT } - http-request set-var(req.method) str(GET) if { method GET } - http-request set-var(req.method) str(HEAD) if { method HEAD } - http-request set-var(req.method) str(OPTIONS) if { method OPTIONS } - http-request set-var(req.method) str(POST) if { method POST } - http-request set-var(req.method) str(TRACE) if { method TRACE } - http-request set-var(req.method) str(PUT) if { method PUT } - http-request set-var(req.method) str(PATCH) if { method PATCH } - http-request set-var(req.method) str(DELETE) if { method DELETE } http-request set-header X-Real-IP %[src] - 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] + http-request set-header X-Forwarded-Method %[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] # be_auth_request is used to make HAProxy do the TLS termination since the Lua script # does not know how to handle it (see https://github.com/TimWolla/haproxy-auth-request/issues/12). - http-request lua.auth-request be_auth_request /api/authz/auth-request if protected-frontends - - http-request redirect location https://login.example.com:8080/?rd=%[var(req.scheme)]://%[base]%[var(req.questionmark)]%[query]&rm=%[var(req.method)] if protected-frontends !{ var(txn.auth_response_successful) -m bool } + http-request lua.auth-intercept be_auth_request /api/authz/forward-auth HEAD * authorization,proxy-authorization,remote_user,remote-user,remote-groups,remote-name,remote-email - if protected-frontends + http-request redirect location %[var(txn.auth_response_location)] if protected-frontends !{ var(txn.auth_response_successful) -m bool } use_backend be_authelia if host-authelia-portal api-path || devworkflow-path || jwks-path || locales-path || wellknown-path use_backend fe_authelia if host-authelia-portal !api-path @@ -86,24 +78,6 @@ backend fe_authelia server authelia-backend authelia-backend:9091 check backup resolvers docker ssl verify none backend be_httpbin - ## Pass the special authorization response headers to the protected application. - acl authorization_exist var(req.auth_response_header.authorization) -m found - acl proxy_authorization_exist var(req.auth_response_header.proxy_authorization) -m found - - http-request set-header Authorization %[var(req.auth_response_header.authorization)] if authorization_exist - http-request set-header Proxy-Authorization %[var(req.auth_response_header.proxy_authorization)] if proxy_authorization_exist - - ## Pass the special metadata response headers to the protected application. - acl remote_user_exist var(req.auth_response_header.remote_user) -m found - acl remote_groups_exist var(req.auth_response_header.remote_groups) -m found - acl remote_name_exist var(req.auth_response_header.remote_name) -m found - acl remote_email_exist var(req.auth_response_header.remote_email) -m found - - http-request set-header Remote-User %[var(req.auth_response_header.remote_user)] if remote_user_exist - http-request set-header Remote-Groups %[var(req.auth_response_header.remote_groups)] if remote_groups_exist - http-request set-header Remote-Name %[var(req.auth_response_header.remote_name)] if remote_name_exist - http-request set-header Remote-Email %[var(req.auth_response_header.remote_email)] if remote_email_exist - ## Pass the Set-Cookie response headers to the user. acl set_cookie_exist var(req.auth_response_header.set_cookie) -m found http-response set-header Set-Cookie %[var(req.auth_response_header.set_cookie)] if set_cookie_exist @@ -114,4 +88,8 @@ backend be_mail server smtp-backend smtp:1080 resolvers docker backend be_protected + ## Pass the Set-Cookie response headers to the user. + acl set_cookie_exist var(req.auth_response_header.set_cookie) -m found + http-response set-header Set-Cookie %[var(req.auth_response_header.set_cookie)] if set_cookie_exist + server nginx-backend nginx-backend:80 resolvers docker diff --git a/internal/suites/example/compose/nginx/portal/nginx.conf b/internal/suites/example/compose/nginx/portal/nginx.conf index d7260426c..a667ba80d 100644 --- a/internal/suites/example/compose/nginx/portal/nginx.conf +++ b/internal/suites/example/compose/nginx/portal/nginx.conf @@ -166,9 +166,6 @@ http { ## Send a subrequest to Authelia to verify if the user is authenticated and has permission to access the resource. auth_request /internal/authelia/authz; - ## Set the $target_url variable based on the original request. - set $target_url $scheme://$http_host$request_uri; - ## Save the upstream authorization response headers from Authelia to variables. auth_request_set $authorization $upstream_http_authorization; auth_request_set $proxy_authorization $upstream_http_proxy_authorization; @@ -193,8 +190,23 @@ http { auth_request_set $cookie $upstream_http_set_cookie; add_header Set-Cookie $cookie; - ## If the subreqest returns 200 pass to the backend, if the subrequest returns 401 redirect to the portal. - error_page 401 =302 https://login.$basedomain:8080/?rd=$target_url; + ## Configure the redirection when the Authz failure occurs. Lines starting with 'Modern Method' and 'Legacy Method' + ## should be commented / uncommented as pairs. The modern method uses the session cookies configuration's authelia_url + ## value to determine the redirection URL here. It's much simpler and compatible with the mutli-cookie domain easily. + + ## Modern Method: Set the $redirection_url to the Location header of the response to the Authz endpoint. + auth_request_set $redirection_url $upstream_http_location; + + ## Modern Method: When there is a 401 response code from the Authz endpoint redirect to the $redirection_url. + error_page 401 =302 $redirection_url; + + ## Legacy Method: Set $target_url to the original requested URL. + ## This requires http_set_misc module, replace 'set_escape_uri' with 'set' if you don't have this module. + # set $target_url $scheme://$http_host$request_uri; + + ## Legacy Method: When there is a 401 response code from the Authz endpoint redirect to the portal with the 'rd' + ## URL parameter set to $target_url. This requires users update 'auth.example.com/' with their external authelia URL. + # error_page 401 =302 https://login.$basedomain:8080/?rd=$target_url; # Authelia relies on Proxy-Authorization header to authenticate in basic auth. # but for the sake of simplicity (because Authorization in supported in most @@ -252,9 +264,6 @@ http { ## Send a subrequest to Authelia to verify if the user is authenticated and has permission to access the resource. auth_request /internal/authelia/authz; - ## Set the $target_url variable based on the original request. - set $target_url $scheme://$http_host$request_uri; - ## Save the upstream authorization response headers from Authelia to variables. auth_request_set $authorization $upstream_http_authorization; auth_request_set $proxy_authorization $upstream_http_proxy_authorization; @@ -279,8 +288,23 @@ http { auth_request_set $cookie $upstream_http_set_cookie; add_header Set-Cookie $cookie; - ## If the subreqest returns 200 pass to the backend, if the subrequest returns 401 redirect to the portal. - error_page 401 =302 https://login.$basedomain:8080/?rd=$target_url; + ## Configure the redirection when the Authz failure occurs. Lines starting with 'Modern Method' and 'Legacy Method' + ## should be commented / uncommented as pairs. The modern method uses the session cookies configuration's authelia_url + ## value to determine the redirection URL here. It's much simpler and compatible with the mutli-cookie domain easily. + + ## Modern Method: Set the $redirection_url to the Location header of the response to the Authz endpoint. + auth_request_set $redirection_url $upstream_http_location; + + ## Modern Method: When there is a 401 response code from the Authz endpoint redirect to the $redirection_url. + error_page 401 =302 $redirection_url; + + ## Legacy Method: Set $target_url to the original requested URL. + ## This requires http_set_misc module, replace 'set_escape_uri' with 'set' if you don't have this module. + # set $target_url $scheme://$http_host$request_uri; + + ## Legacy Method: When there is a 401 response code from the Authz endpoint redirect to the portal with the 'rd' + ## URL parameter set to $target_url. This requires users update 'auth.example.com/' with their external authelia URL. + # error_page 401 =302 https://login.$basedomain:8080/?rd=$target_url; proxy_pass $upstream_headers; } @@ -309,9 +333,6 @@ http { ## Send a subrequest to Authelia to verify if the user is authenticated and has permission to access the resource. auth_request /internal/authelia/authz; - ## Set the $target_url variable based on the original request. - set $target_url $scheme://$http_host$request_uri; - ## Save the upstream authorization response headers from Authelia to variables. auth_request_set $authorization $upstream_http_authorization; auth_request_set $proxy_authorization $upstream_http_proxy_authorization; @@ -336,8 +357,23 @@ http { auth_request_set $cookie $upstream_http_set_cookie; add_header Set-Cookie $cookie; - ## If the subreqest returns 200 pass to the backend, if the subrequest returns 401 redirect to the portal. - error_page 401 =302 https://login.$basedomain:8080/?rd=$target_url; + ## Configure the redirection when the Authz failure occurs. Lines starting with 'Modern Method' and 'Legacy Method' + ## should be commented / uncommented as pairs. The modern method uses the session cookies configuration's authelia_url + ## value to determine the redirection URL here. It's much simpler and compatible with the mutli-cookie domain easily. + + ## Modern Method: Set the $redirection_url to the Location header of the response to the Authz endpoint. + auth_request_set $redirection_url $upstream_http_location; + + ## Modern Method: When there is a 401 response code from the Authz endpoint redirect to the $redirection_url. + error_page 401 =302 $redirection_url; + + ## Legacy Method: Set $target_url to the original requested URL. + ## This requires http_set_misc module, replace 'set_escape_uri' with 'set' if you don't have this module. + # set $target_url $scheme://$http_host$request_uri; + + ## Legacy Method: When there is a 401 response code from the Authz endpoint redirect to the portal with the 'rd' + ## URL parameter set to $target_url. This requires users update 'auth.example.com/' with their external authelia URL. + # error_page 401 =302 https://login.$basedomain:8080/?rd=$target_url; # Route the request to the correct virtual host in the backend. proxy_set_header Host $http_host; diff --git a/internal/templates/funcs.go b/internal/templates/funcs.go index f8be41e3d..d8e6e3538 100644 --- a/internal/templates/funcs.go +++ b/internal/templates/funcs.go @@ -9,6 +9,7 @@ import ( "encoding/hex" "fmt" "hash" + "net/url" "os" "path" "path/filepath" @@ -79,6 +80,8 @@ func FuncMap() map[string]any { "indent": FuncIndent, "nindent": FuncNewlineIndent, "uuidv4": FuncUUIDv4, + "urlquery": url.QueryEscape, + "urlunquery": url.QueryUnescape, } } diff --git a/internal/templates/provider.go b/internal/templates/provider.go index 10235dca6..40e737725 100644 --- a/internal/templates/provider.go +++ b/internal/templates/provider.go @@ -1,9 +1,9 @@ package templates import ( - "embed" "fmt" th "html/template" + "io/fs" "path" tt "text/template" ) @@ -28,7 +28,7 @@ type Provider struct { } // LoadTemplatedAssets takes an embed.FS and loads each templated asset document into a Template. -func (p *Provider) LoadTemplatedAssets(fs embed.FS) (err error) { +func (p *Provider) LoadTemplatedAssets(fs fs.ReadFileFS) (err error) { var ( data []byte )