feat(handlers): authorization header switch via query param to /api/verify (#1563)
* [FEATURE] Add auth query param to /api/verify (#1353) When `/api/verify` is called with `?auth=basic`, use the standard Authorization header instead of Proxy-Authorization. * [FIX] Better basic auth error reporting * [FIX] Return 401 when using basic auth instead of redirecting * [TESTS] Add tests for auth=basic query param * [DOCS] Mention auth=basic argument and provide nginx example * docs: add/adjust basic auth query arg docs for proxies Co-authored-by: Amir Zarrinkafsh <nightah@me.com>pull/1761/head
parent
4f099b76d7
commit
ba65a3db82
|
@ -28,13 +28,15 @@ Below you will find commented examples of the following configuration:
|
|||
|
||||
* Authelia portal
|
||||
* Protected endpoint (Nextcloud)
|
||||
* Protected endpoint with `Authorization` header for basic authentication (Heimdall)
|
||||
* [haproxy-auth-request](https://github.com/TimWolla/haproxy-auth-request/blob/master/auth-request.lua)
|
||||
|
||||
With this configuration you can protect your virtual hosts with Authelia, by following the steps below:
|
||||
1. Add host(s) to the `protected-frontends` ACL to support protection with Authelia.
|
||||
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:
|
||||
```
|
||||
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
|
||||
backend upon successful authentication, for example:
|
||||
|
@ -42,12 +44,14 @@ backend upon successful authentication, for example:
|
|||
acl host-jenkins hdr(host) -i jenkins.example.com
|
||||
acl host-nextcloud hdr(host) -i nextcloud.example.com
|
||||
acl host-phpmyadmin hdr(host) -i phpmyadmin.example.com
|
||||
acl host-heimdall hdr(host) -i heimdall.example.com
|
||||
```
|
||||
3. Add backend route for your service(s), for example:
|
||||
```
|
||||
use_backend be_jenkins if host-jenkins
|
||||
use_backend be_nextcloud if host-nextcloud
|
||||
use_backend be_phpmyadmin if host-phpmyadmin
|
||||
use_backend be_heimdall if host-heimdall
|
||||
```
|
||||
4. Add backend definitions for your service(s), for example:
|
||||
```
|
||||
|
@ -57,6 +61,8 @@ backend upon successful authentication, for example:
|
|||
server nextcloud nextcloud:443 ssl verify none
|
||||
backend be_phpmyadmin
|
||||
server phpmyadmin phpmyadmin:80
|
||||
backend be_heimdall
|
||||
server heimdall heimdall:443 ssl verify none
|
||||
```
|
||||
|
||||
### Secure Authelia with TLS
|
||||
|
@ -87,8 +93,10 @@ frontend fe_http
|
|||
|
||||
# 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
|
||||
|
||||
http-request set-var(req.scheme) str(https) if { ssl_fc }
|
||||
http-request set-var(req.scheme) str(http) if !{ ssl_fc }
|
||||
|
@ -102,13 +110,16 @@ frontend fe_http
|
|||
|
||||
# Protect endpoints with haproxy-auth-request and Authelia
|
||||
http-request lua.auth-request be_authelia /api/verify 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
|
||||
|
||||
# Authelia backend route
|
||||
use_backend be_authelia if host-authelia
|
||||
# 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 }
|
||||
http-request redirect location https://auth.example.com/?rd=%[var(req.scheme)]://%[base]%[var(req.questionmark)]%[query] if (protected-frontends || protected-frontends-basic) !{ var(txn.auth_response_successful) -m bool }
|
||||
# Service backend route(s)
|
||||
use_backend be_nextcloud if host-nextcloud
|
||||
use_backend be_heimdall if host-heimdall
|
||||
|
||||
backend be_authelia
|
||||
server authelia authelia:9091
|
||||
|
@ -125,6 +136,19 @@ backend be_nextcloud
|
|||
http-request set-header Remote-Email %[var(req.auth_response_header.remote_email)] if remote_email_exist
|
||||
|
||||
server nextcloud nextcloud:443 ssl verify none
|
||||
|
||||
backend be_heimdall
|
||||
# Pass Remote-User, Remote-Name, Remote-Email and Remote-Groups headers
|
||||
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
|
||||
|
||||
server heimdall heimdall:443 ssl verify none
|
||||
```
|
||||
|
||||
##### haproxy.cfg (TLS enabled Authelia)
|
||||
|
@ -147,8 +171,10 @@ frontend fe_http
|
|||
|
||||
# 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
|
||||
|
||||
http-request set-var(req.scheme) str(https) if { ssl_fc }
|
||||
http-request set-var(req.scheme) str(http) if !{ ssl_fc }
|
||||
|
@ -162,13 +188,16 @@ frontend fe_http
|
|||
|
||||
# Protect endpoints with haproxy-auth-request and Authelia
|
||||
http-request lua.auth-request be_authelia_proxy /api/verify 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
|
||||
|
||||
# Authelia backend route
|
||||
use_backend be_authelia if host-authelia
|
||||
# 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 }
|
||||
http-request redirect location https://auth.example.com/?rd=%[var(req.scheme)]://%[base]%[var(req.questionmark)]%[query] if (protected-frontends || protected-frontends-basic) !{ var(txn.auth_response_successful) -m bool }
|
||||
# Service backend route(s)
|
||||
use_backend be_nextcloud if host-nextcloud
|
||||
use_backend be_heimdall if host-heimdall
|
||||
|
||||
backend be_authelia
|
||||
server authelia authelia:9091
|
||||
|
@ -194,6 +223,19 @@ backend be_nextcloud
|
|||
http-request set-header Remote-Email %[var(req.auth_response_header.remote_email)] if remote_email_exist
|
||||
|
||||
server nextcloud nextcloud:443 ssl verify none
|
||||
|
||||
backend be_heimdall
|
||||
# Pass Remote-User, Remote-Name, Remote-Email and Remote-Groups headers
|
||||
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
|
||||
|
||||
server heimdall heimdall:443 ssl verify none
|
||||
```
|
||||
|
||||
[HAproxy]: https://www.haproxy.org/
|
||||
|
|
|
@ -25,11 +25,13 @@ With the below configuration you can add `authelia.conf` to virtual hosts to sup
|
|||
#### Supplementary config
|
||||
|
||||
##### authelia.conf
|
||||
|
||||
```nginx
|
||||
set $upstream_authelia http://authelia:9091/api/verify;
|
||||
|
||||
# Virtual endpoint created by nginx to forward auth requests.
|
||||
location /authelia {
|
||||
internal;
|
||||
set $upstream_authelia http://authelia:9091/api/verify;
|
||||
proxy_pass_request_body off;
|
||||
proxy_pass $upstream_authelia;
|
||||
proxy_set_header Content-Length "";
|
||||
|
@ -67,6 +69,7 @@ location /authelia {
|
|||
```
|
||||
|
||||
##### auth.conf
|
||||
|
||||
```nginx
|
||||
# Basic Authelia Config
|
||||
# Send a subsequent request to Authelia to verify if the user is authenticated
|
||||
|
@ -94,6 +97,7 @@ error_page 401 =302 https://auth.example.com/?rd=$target_url;
|
|||
```
|
||||
|
||||
##### proxy.conf
|
||||
|
||||
```nginx
|
||||
client_body_buffer_size 128k;
|
||||
|
||||
|
@ -176,4 +180,174 @@ server {
|
|||
}
|
||||
```
|
||||
|
||||
### Basic Auth Example
|
||||
|
||||
Here's an example for using HTTP basic auth on a specific endpoint. It is based on the full example above.
|
||||
|
||||
##### authelia-basic.conf
|
||||
|
||||
```nginx
|
||||
# Notice we added the auth=basic query arg here
|
||||
set $upstream_authelia http://authelia:9091/api/verify?auth=basic;
|
||||
|
||||
location /authelia {
|
||||
internal;
|
||||
proxy_pass_request_body off;
|
||||
proxy_pass $upstream_authelia;
|
||||
proxy_set_header Content-Length "";
|
||||
|
||||
# Timeout if the real server is dead
|
||||
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503;
|
||||
|
||||
# [REQUIRED] Needed by Authelia to check authorizations of the resource.
|
||||
# Provide either X-Original-URL and X-Forwarded-Proto or
|
||||
# X-Forwarded-Proto, X-Forwarded-Host and X-Forwarded-Uri or both.
|
||||
# Those headers will be used by Authelia to deduce the target url of the user.
|
||||
# Basic Proxy Config
|
||||
client_body_buffer_size 128k;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $http_host;
|
||||
proxy_set_header X-Forwarded-Uri $request_uri;
|
||||
proxy_set_header X-Forwarded-Ssl on;
|
||||
proxy_redirect http:// $scheme://;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection "";
|
||||
proxy_cache_bypass $cookie_session;
|
||||
proxy_no_cache $cookie_session;
|
||||
proxy_buffers 4 32k;
|
||||
|
||||
# Advanced Proxy Config
|
||||
send_timeout 5m;
|
||||
proxy_read_timeout 240;
|
||||
proxy_send_timeout 240;
|
||||
proxy_connect_timeout 240;
|
||||
}
|
||||
```
|
||||
|
||||
##### auth-basic.conf
|
||||
|
||||
Same as `auth.conf` but without the `error_page` directive. We want nginx to proxy the 401 back to the client, not to return a 301.
|
||||
|
||||
```nginx
|
||||
# Basic Authelia Config
|
||||
# Send a subsequent request to Authelia to verify if the user is authenticated
|
||||
# and has the right permissions to access the resource.
|
||||
auth_request /authelia;
|
||||
# Set the `target_url` variable based on the request. It will be used to build the portal
|
||||
# URL with the correct redirection parameter.
|
||||
auth_request_set $target_url $scheme://$http_host$request_uri;
|
||||
# Set the X-Forwarded-User and X-Forwarded-Groups with the headers
|
||||
# returned by Authelia for the backends which can consume them.
|
||||
# This is not safe, as the backend must make sure that they come from the
|
||||
# proxy. In the future, it's gonna be safe to just use OAuth.
|
||||
auth_request_set $user $upstream_http_remote_user;
|
||||
auth_request_set $groups $upstream_http_remote_groups;
|
||||
auth_request_set $name $upstream_http_remote_name;
|
||||
auth_request_set $email $upstream_http_remote_email;
|
||||
proxy_set_header Remote-User $user;
|
||||
proxy_set_header Remote-Groups $groups;
|
||||
proxy_set_header Remote-Name $name;
|
||||
proxy_set_header Remote-Email $email;
|
||||
# If Authelia returns 401, then nginx passes it to the user.
|
||||
# If it returns 200, then the request pass through to the backend.
|
||||
```
|
||||
|
||||
#### Protected Endpoint
|
||||
|
||||
```nginx
|
||||
server {
|
||||
server_name nextcloud.example.com;
|
||||
listen 80;
|
||||
return 301 https://$server_name$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
server_name nextcloud.example.com;
|
||||
listen 443 ssl http2;
|
||||
include /config/nginx/ssl.conf;
|
||||
include /config/nginx/authelia-basic.conf; # Use the "basic" endpoint
|
||||
|
||||
location / {
|
||||
set $upstream_nextcloud https://nextcloud;
|
||||
proxy_pass $upstream_nextcloud;
|
||||
include /config/nginx/auth-basic.conf; # Activate authelia with basic auth
|
||||
include /config/nginx/proxy.conf; # this file is the exact same as above
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Basic auth for specific client
|
||||
|
||||
If you'd like to force basic auth for some requests, you can use the following template:
|
||||
|
||||
##### authelia-detect.conf
|
||||
|
||||
```nginx
|
||||
set $is_basic_auth ""; # false value
|
||||
set $upstream_authelia http://authelia:9091/api/verify;
|
||||
|
||||
# Detect the client you want to force basic auth for here
|
||||
# For the example we just match a path on the original request
|
||||
if ($request_uri = "/force-basic") {
|
||||
set $is_basic_auth "true";
|
||||
set $upstream_authelia "$upstream_authelia?auth=basic";
|
||||
}
|
||||
|
||||
location = /authelia {
|
||||
# Same as above
|
||||
}
|
||||
|
||||
# A new virtual endpoint to used if the auth_request failed
|
||||
location = /authelia-redirect {
|
||||
internal;
|
||||
|
||||
if ($is_basic_auth) {
|
||||
# This is a request where we decided to use basic auth, return a 401.
|
||||
# Nginx will also proxy back the WWW-Authenticate header from Authelia's
|
||||
# response. This is what informs the client we're expecting basic auth.
|
||||
return 401;
|
||||
}
|
||||
|
||||
# The original request didn't target /force-basic, redirect to the pretty login page
|
||||
# This is what `error_page 401 =302 https://auth.example.com/?rd=$target_url;` did.
|
||||
return 302 https://auth.example.com/$is_args$args;
|
||||
}
|
||||
```
|
||||
|
||||
##### auth.conf
|
||||
|
||||
Here we replace `error_page` directive to determine if basic auth should be utilised or not.
|
||||
|
||||
```nginx
|
||||
# Basic Authelia Config
|
||||
# Send a subsequent request to Authelia to verify if the user is authenticated
|
||||
# and has the right permissions to access the resource.
|
||||
auth_request /authelia;
|
||||
# Set the `target_url` variable based on the request. It will be used to build the portal
|
||||
# URL with the correct redirection parameter.
|
||||
auth_request_set $target_url $scheme://$http_host$request_uri;
|
||||
# Set the X-Forwarded-User and X-Forwarded-Groups with the headers
|
||||
# returned by Authelia for the backends which can consume them.
|
||||
# This is not safe, as the backend must make sure that they come from the
|
||||
# proxy. In the future, it's gonna be safe to just use OAuth.
|
||||
auth_request_set $user $upstream_http_remote_user;
|
||||
auth_request_set $groups $upstream_http_remote_groups;
|
||||
auth_request_set $name $upstream_http_remote_name;
|
||||
auth_request_set $email $upstream_http_remote_email;
|
||||
proxy_set_header Remote-User $user;
|
||||
proxy_set_header Remote-Groups $groups;
|
||||
proxy_set_header Remote-Name $name;
|
||||
proxy_set_header Remote-Email $email;
|
||||
# If Authelia returns 401, then nginx passes it to the user.
|
||||
# If it returns 200, then the request pass through to the backend.
|
||||
error_page 401 /authelia-redirect?rd=$target_url;
|
||||
```
|
||||
|
||||
This tells nginx to use the virtual endpoint we defined above in case the auth_request failed.
|
||||
|
||||
[nginx]: https://www.nginx.com/
|
||||
|
|
|
@ -17,11 +17,17 @@ Below you will find commented examples of the following configuration:
|
|||
* Traefik 1.x
|
||||
* Authelia portal
|
||||
* Protected endpoint (Nextcloud)
|
||||
* Protected endpoint with `Authorization` header for basic authentication (Heimdall)
|
||||
|
||||
The below configuration looks to provide examples of running Traefik 1.x with labels to protect your endpoint (Nextcloud in this case).
|
||||
|
||||
Please ensure that you also setup the respective [ACME configuration](https://docs.traefik.io/v1.7/configuration/acme/) for your Traefik setup as this is not covered in the example below.
|
||||
|
||||
### Basic Authentication
|
||||
|
||||
Authelia provides the means to be able to authenticate your first factor via the `Proxy-Authorization` header.
|
||||
Given that this is not compatible with Traefik 1.x you can call Authelia's `/api/verify` endpoint with the `auth=basic` query parameter to force a switch to the `Authentication` header.
|
||||
|
||||
##### docker-compose.yml
|
||||
```yml
|
||||
version: '3'
|
||||
|
@ -94,6 +100,26 @@ services:
|
|||
- PUID=1000
|
||||
- PGID=1000
|
||||
- TZ=Australia/Melbourne
|
||||
|
||||
heimdall:
|
||||
image: linuxserver/heimdall
|
||||
container_name: heimdall
|
||||
volumes:
|
||||
- /path/to/heimdall/config:/config
|
||||
networks:
|
||||
- net
|
||||
labels:
|
||||
- 'traefik.frontend.rule=Host:heimdall.example.com'
|
||||
- 'traefik.frontend.auth.forward.address=http://authelia:9091/api/verify?auth=basic
|
||||
- 'traefik.frontend.auth.forward.trustForwardHeader=true'
|
||||
- 'traefik.frontend.auth.forward.authResponseHeaders=Remote-User,Remote-Groups,Remote-Name,Remote-Email'
|
||||
expose:
|
||||
- 443
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- TZ=Australia/Melbourne
|
||||
```
|
||||
|
||||
[Traefik 1.x]: https://docs.traefik.io/v1.7/
|
||||
|
|
|
@ -17,11 +17,17 @@ Below you will find commented examples of the following configuration:
|
|||
* Traefik 2.x
|
||||
* Authelia portal
|
||||
* Protected endpoint (Nextcloud)
|
||||
* Protected endpoint with `Authorization` header for basic authentication (Heimdall)
|
||||
|
||||
The below configuration looks to provide examples of running Traefik 2.x with labels to protect your endpoint (Nextcloud in this case).
|
||||
|
||||
Please ensure that you also setup the respective [ACME configuration](https://docs.traefik.io/https/acme/) for your Traefik setup as this is not covered in the example below.
|
||||
|
||||
### Basic Authentication
|
||||
|
||||
Authelia provides the means to be able to authenticate your first factor via the `Proxy-Authorization` header, this is compatible with Traefik >= 2.4.1.
|
||||
If you are running Traefik < 2.4.1, or you have a use-case which requires the use of the `Authorization` header/basic authentication login prompt you can call Authelia's `/api/verify` endpoint with the `auth=basic` query parameter to force a switch to the `Authentication` header.
|
||||
|
||||
##### docker-compose.yml
|
||||
```yml
|
||||
version: '3'
|
||||
|
@ -77,6 +83,9 @@ services:
|
|||
- 'traefik.http.middlewares.authelia.forwardauth.address=http://authelia:9091/api/verify?rd=https://login.example.com/'
|
||||
- 'traefik.http.middlewares.authelia.forwardauth.trustForwardHeader=true'
|
||||
- 'traefik.http.middlewares.authelia.forwardauth.authResponseHeaders=Remote-User, Remote-Groups, Remote-Name, Remote-Email'
|
||||
- 'traefik.http.middlewares.authelia-basic.forwardauth.address=http://authelia:9091/api/verify?auth=basic'
|
||||
- 'traefik.http.middlewares.authelia-basic.forwardauth.trustForwardHeader=true'
|
||||
- 'traefik.http.middlewares.authelia-basic.forwardauth.authResponseHeaders=Remote-User, Remote-Groups, Remote-Name, Remote-Email'
|
||||
expose:
|
||||
- 9091
|
||||
restart: unless-stopped
|
||||
|
@ -104,6 +113,27 @@ services:
|
|||
- PUID=1000
|
||||
- PGID=1000
|
||||
- TZ=Australia/Melbourne
|
||||
|
||||
heimdall:
|
||||
image: linuxserver/heimdall
|
||||
container_name: heimdall
|
||||
volumes:
|
||||
- /path/to/heimdall/config:/config
|
||||
networks:
|
||||
- net
|
||||
labels:
|
||||
- 'traefik.enable=true'
|
||||
- 'traefik.http.routers.heimdall.rule=Host(`heimdall.example.com`)'
|
||||
- 'traefik.http.routers.heimdall.entrypoints=https'
|
||||
- 'traefik.http.routers.heimdall.tls=true'
|
||||
- 'traefik.http.routers.heimdall.middlewares=authelia-basic@docker'
|
||||
expose:
|
||||
- 443
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- TZ=Australia/Melbourne
|
||||
```
|
||||
|
||||
## FAQ
|
||||
|
|
|
@ -21,13 +21,26 @@ To know more about the configuration of the feature, please visit the
|
|||
documentation about the [configuration](../configuration/access-control.md).
|
||||
|
||||
|
||||
## Proxy-Authorization header
|
||||
## HTTP Basic Auth
|
||||
|
||||
Authelia supports two different methods for basic auth.
|
||||
|
||||
### Proxy-Authorization header
|
||||
|
||||
Authelia reads credentials from the header `Proxy-Authorization` instead of
|
||||
the usual `Authorization` header. This is because in some circumstances both Authelia
|
||||
and the application could require authentication in order to provide specific
|
||||
authorizations at the level of the application.
|
||||
|
||||
### API argument
|
||||
|
||||
If instead of the `Proxy-Authorization` header you want, or need, to use the more
|
||||
conventional `Authorization` header, you should then configure your reverse-proxy
|
||||
to use `/api/verify?auth=basic`.
|
||||
When authentication fails and `auth=basic` was set, Authelia's response will include
|
||||
the `WWW-Authenticate` header. This will cause browsers to prompt for authentication,
|
||||
and users will not land on the HTML login page.
|
||||
|
||||
|
||||
## Session-Username header
|
||||
|
||||
|
|
|
@ -11,8 +11,11 @@ const ResetPasswordAction = "ResetPassword"
|
|||
|
||||
const authPrefix = "Basic "
|
||||
|
||||
// AuthorizationHeader is the basic-auth HTTP header Authelia utilises.
|
||||
const AuthorizationHeader = "Proxy-Authorization"
|
||||
// ProxyAuthorizationHeader is the basic-auth HTTP header Authelia utilises.
|
||||
const ProxyAuthorizationHeader = "Proxy-Authorization"
|
||||
|
||||
// AuthorizationHeader is the basic-auth HTTP header Authelia utilises with "auth=basic" query param.
|
||||
const AuthorizationHeader = "Authorization"
|
||||
|
||||
// SessionUsernameHeader is used as additional protection to validate a user for things like pam_exec.
|
||||
const SessionUsernameHeader = "Session-Username"
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net"
|
||||
|
@ -75,9 +76,9 @@ func getOriginalURL(ctx *middlewares.AutheliaCtx) (*url.URL, error) {
|
|||
|
||||
// parseBasicAuth parses an HTTP Basic Authentication string.
|
||||
// "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==" returns ("Aladdin", "open sesame", true).
|
||||
func parseBasicAuth(auth string) (username, password string, err error) {
|
||||
func parseBasicAuth(header, auth string) (username, password string, err error) {
|
||||
if !strings.HasPrefix(auth, authPrefix) {
|
||||
return "", "", fmt.Errorf("%s prefix not found in %s header", strings.Trim(authPrefix, " "), AuthorizationHeader)
|
||||
return "", "", fmt.Errorf("%s prefix not found in %s header", strings.Trim(authPrefix, " "), header)
|
||||
}
|
||||
|
||||
c, err := base64.StdEncoding.DecodeString(auth[len(authPrefix):])
|
||||
|
@ -89,7 +90,7 @@ func parseBasicAuth(auth string) (username, password string, err error) {
|
|||
s := strings.IndexByte(cs, ':')
|
||||
|
||||
if s < 0 {
|
||||
return "", "", fmt.Errorf("Format of %s header must be user:password", AuthorizationHeader)
|
||||
return "", "", fmt.Errorf("Format of %s header must be user:password", header)
|
||||
}
|
||||
|
||||
return cs[:s], cs[s+1:], nil
|
||||
|
@ -125,17 +126,17 @@ func isTargetURLAuthorized(authorizer *authorization.Authorizer, targetURL url.U
|
|||
|
||||
// verifyBasicAuth verify that the provided username and password are correct and
|
||||
// that the user is authorized to target the resource.
|
||||
func verifyBasicAuth(auth []byte, targetURL url.URL, ctx *middlewares.AutheliaCtx) (username, name string, groups, emails []string, authLevel authentication.Level, err error) { //nolint:unparam
|
||||
username, password, err := parseBasicAuth(string(auth))
|
||||
func verifyBasicAuth(header string, auth []byte, targetURL url.URL, ctx *middlewares.AutheliaCtx) (username, name string, groups, emails []string, authLevel authentication.Level, err error) { //nolint:unparam
|
||||
username, password, err := parseBasicAuth(header, string(auth))
|
||||
|
||||
if err != nil {
|
||||
return "", "", nil, nil, authentication.NotAuthenticated, fmt.Errorf("Unable to parse content of %s header: %s", AuthorizationHeader, err)
|
||||
return "", "", nil, nil, authentication.NotAuthenticated, fmt.Errorf("Unable to parse content of %s header: %s", header, err)
|
||||
}
|
||||
|
||||
authenticated, err := ctx.Providers.UserProvider.CheckUserPassword(username, password)
|
||||
|
||||
if err != nil {
|
||||
return "", "", nil, nil, authentication.NotAuthenticated, fmt.Errorf("Unable to check credentials extracted from %s header: %s", AuthorizationHeader, err)
|
||||
return "", "", nil, nil, authentication.NotAuthenticated, fmt.Errorf("Unable to check credentials extracted from %s header: %s", header, err)
|
||||
}
|
||||
|
||||
// If the user is not correctly authenticated, send a 401.
|
||||
|
@ -232,7 +233,15 @@ func verifySessionCookie(ctx *middlewares.AutheliaCtx, targetURL *url.URL, userS
|
|||
return userSession.Username, userSession.DisplayName, userSession.Groups, userSession.Emails, userSession.AuthenticationLevel, nil
|
||||
}
|
||||
|
||||
func handleUnauthorized(ctx *middlewares.AutheliaCtx, targetURL fmt.Stringer, username string) {
|
||||
func handleUnauthorized(ctx *middlewares.AutheliaCtx, targetURL fmt.Stringer, isBasicAuth bool, username string) {
|
||||
if isBasicAuth {
|
||||
ctx.Logger.Infof("Access to %s is not authorized to user %s, sending 401 response with basic auth header", targetURL.String(), username)
|
||||
ctx.ReplyUnauthorized()
|
||||
ctx.Response.Header.Add("WWW-Authenticate", "Basic realm=\"Authentication required\"")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Kubernetes ingress controller and Traefik use the rd parameter of the verify
|
||||
// endpoint to provide the URL of the login portal. The target URL of the user
|
||||
// is computed from X-Forwarded-* headers or X-Original-URL.
|
||||
|
@ -394,6 +403,47 @@ func getProfileRefreshSettings(cfg schema.AuthenticationBackendConfiguration) (r
|
|||
return refresh, refreshInterval
|
||||
}
|
||||
|
||||
func verifyAuth(ctx *middlewares.AutheliaCtx, targetURL *url.URL, refreshProfile bool, refreshProfileInterval time.Duration) (isBasicAuth bool, username, name string, groups, emails []string, authLevel authentication.Level, err error) {
|
||||
authHeader := ProxyAuthorizationHeader
|
||||
if bytes.Equal(ctx.QueryArgs().Peek("auth"), []byte("basic")) {
|
||||
authHeader = AuthorizationHeader
|
||||
isBasicAuth = true
|
||||
}
|
||||
|
||||
authValue := ctx.Request.Header.Peek(authHeader)
|
||||
if authValue != nil {
|
||||
isBasicAuth = true
|
||||
} else if isBasicAuth {
|
||||
err = fmt.Errorf("Basic auth requested via query arg, but no value provided via %s header", authHeader)
|
||||
return
|
||||
}
|
||||
|
||||
if isBasicAuth {
|
||||
username, name, groups, emails, authLevel, err = verifyBasicAuth(authHeader, authValue, *targetURL, ctx)
|
||||
return
|
||||
}
|
||||
|
||||
userSession := ctx.GetSession()
|
||||
username, name, groups, emails, authLevel, err = verifySessionCookie(ctx, targetURL, &userSession, refreshProfile, refreshProfileInterval)
|
||||
|
||||
sessionUsername := ctx.Request.Header.Peek(SessionUsernameHeader)
|
||||
if sessionUsername != nil && !strings.EqualFold(string(sessionUsername), username) {
|
||||
ctx.Logger.Warnf("Possible cookie hijack or attempt to bypass security detected destroying the session and sending 401 response")
|
||||
|
||||
err = ctx.Providers.SessionProvider.DestroySession(ctx.RequestCtx)
|
||||
if err != nil {
|
||||
ctx.Logger.Error(
|
||||
fmt.Errorf(
|
||||
"Unable to destroy user session after handler could not match them to their %s header: %s",
|
||||
SessionUsernameHeader, err))
|
||||
}
|
||||
|
||||
err = fmt.Errorf("Could not match user %s to their %s header with a value of %s when visiting %s", username, SessionUsernameHeader, sessionUsername, targetURL.String())
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// VerifyGet returns the handler verifying if a request is allowed to go through.
|
||||
func VerifyGet(cfg schema.AuthenticationBackendConfiguration) middlewares.RequestHandler {
|
||||
refreshProfile, refreshProfileInterval := getProfileRefreshSettings(cfg)
|
||||
|
@ -423,40 +473,7 @@ func VerifyGet(cfg schema.AuthenticationBackendConfiguration) middlewares.Reques
|
|||
return
|
||||
}
|
||||
|
||||
var username, name string
|
||||
|
||||
var groups, emails []string
|
||||
|
||||
var authLevel authentication.Level
|
||||
|
||||
proxyAuthorization := ctx.Request.Header.Peek(AuthorizationHeader)
|
||||
isBasicAuth := proxyAuthorization != nil
|
||||
userSession := ctx.GetSession()
|
||||
|
||||
if isBasicAuth {
|
||||
username, name, groups, emails, authLevel, err = verifyBasicAuth(proxyAuthorization, *targetURL, ctx)
|
||||
} else {
|
||||
username, name, groups, emails, authLevel, err = verifySessionCookie(ctx, targetURL, &userSession,
|
||||
refreshProfile, refreshProfileInterval)
|
||||
|
||||
sessionUsername := ctx.Request.Header.Peek(SessionUsernameHeader)
|
||||
if sessionUsername != nil && !strings.EqualFold(string(sessionUsername), username) {
|
||||
ctx.Logger.Warnf(
|
||||
"Could not match user %s to their %s header with a value of %s when visiting %s, possible cookie hijack or attempt to bypass security detected destroying the session and sending 401 response",
|
||||
username, SessionUsernameHeader, sessionUsername, targetURL.String())
|
||||
|
||||
err := ctx.Providers.SessionProvider.DestroySession(ctx.RequestCtx)
|
||||
if err != nil {
|
||||
ctx.Logger.Error(
|
||||
fmt.Errorf(
|
||||
"Unable to destroy user session after handler could not match them to their %s header: %s",
|
||||
SessionUsernameHeader, err))
|
||||
}
|
||||
|
||||
ctx.ReplyUnauthorized()
|
||||
return
|
||||
}
|
||||
}
|
||||
isBasicAuth, username, name, groups, emails, authLevel, err := verifyAuth(ctx, targetURL, refreshProfile, refreshProfileInterval)
|
||||
|
||||
if err != nil {
|
||||
ctx.Logger.Error(fmt.Sprintf("Error caught when verifying user authorization: %s", err))
|
||||
|
@ -466,7 +483,7 @@ func VerifyGet(cfg schema.AuthenticationBackendConfiguration) middlewares.Reques
|
|||
return
|
||||
}
|
||||
|
||||
handleUnauthorized(ctx, targetURL, username)
|
||||
handleUnauthorized(ctx, targetURL, isBasicAuth, username)
|
||||
|
||||
return
|
||||
}
|
||||
|
@ -479,7 +496,7 @@ func VerifyGet(cfg schema.AuthenticationBackendConfiguration) middlewares.Reques
|
|||
ctx.Logger.Infof("Access to %s is forbidden to user %s", targetURL.String(), username)
|
||||
ctx.ReplyForbidden()
|
||||
case NotAuthorized:
|
||||
handleUnauthorized(ctx, targetURL, username)
|
||||
handleUnauthorized(ctx, targetURL, isBasicAuth, username)
|
||||
case Authorized:
|
||||
setForwardedHeaders(&ctx.Response.Header, username, name, groups, emails)
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
@ -120,27 +121,34 @@ func TestShouldRaiseWhenXForwardedURIIsNotParsable(t *testing.T) {
|
|||
|
||||
// Test parseBasicAuth.
|
||||
func TestShouldRaiseWhenHeaderDoesNotContainBasicPrefix(t *testing.T) {
|
||||
_, _, err := parseBasicAuth("alzefzlfzemjfej==")
|
||||
_, _, err := parseBasicAuth(ProxyAuthorizationHeader, "alzefzlfzemjfej==")
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "Basic prefix not found in Proxy-Authorization header", err.Error())
|
||||
}
|
||||
|
||||
func TestShouldRaiseWhenCredentialsAreNotInBase64(t *testing.T) {
|
||||
_, _, err := parseBasicAuth("Basic alzefzlfzemjfej==")
|
||||
_, _, err := parseBasicAuth(ProxyAuthorizationHeader, "Basic alzefzlfzemjfej==")
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "illegal base64 data at input byte 16", err.Error())
|
||||
}
|
||||
|
||||
func TestShouldRaiseWhenCredentialsAreNotInCorrectForm(t *testing.T) {
|
||||
// The decoded format should be user:password.
|
||||
_, _, err := parseBasicAuth("Basic am9obiBwYXNzd29yZA==")
|
||||
_, _, err := parseBasicAuth(ProxyAuthorizationHeader, "Basic am9obiBwYXNzd29yZA==")
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "Format of Proxy-Authorization header must be user:password", err.Error())
|
||||
}
|
||||
|
||||
func TestShouldUseProvidedHeaderName(t *testing.T) {
|
||||
// The decoded format should be user:password.
|
||||
_, _, err := parseBasicAuth("HeaderName", "")
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "Basic prefix not found in HeaderName header", err.Error())
|
||||
}
|
||||
|
||||
func TestShouldReturnUsernameAndPassword(t *testing.T) {
|
||||
// the decoded format should be user:password.
|
||||
user, password, err := parseBasicAuth("Basic am9objpwYXNzd29yZA==")
|
||||
user, password, err := parseBasicAuth(ProxyAuthorizationHeader, "Basic am9objpwYXNzd29yZA==")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "john", user)
|
||||
assert.Equal(t, "password", password)
|
||||
|
@ -204,7 +212,7 @@ func TestShouldVerifyWrongCredentials(t *testing.T) {
|
|||
Return(false, nil)
|
||||
|
||||
url, _ := url.ParseRequestURI("https://test.example.com")
|
||||
_, _, _, _, _, err := verifyBasicAuth([]byte("Basic am9objpwYXNzd29yZA=="), *url, mock.Ctx)
|
||||
_, _, _, _, _, err := verifyBasicAuth(ProxyAuthorizationHeader, []byte("Basic am9objpwYXNzd29yZA=="), *url, mock.Ctx)
|
||||
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
@ -354,6 +362,97 @@ func (s *BasicAuthorizationSuite) TestShouldApplyPolicyOfDenyDomain() {
|
|||
assert.Equal(s.T(), 403, mock.Ctx.Response.StatusCode())
|
||||
}
|
||||
|
||||
func (s *BasicAuthorizationSuite) TestShouldVerifyAuthBasicArgOk() {
|
||||
mock := mocks.NewMockAutheliaCtx(s.T())
|
||||
defer mock.Close()
|
||||
|
||||
mock.Ctx.QueryArgs().Add("auth", "basic")
|
||||
mock.Ctx.Request.Header.Set("Authorization", "Basic am9objpwYXNzd29yZA==")
|
||||
mock.Ctx.Request.Header.Set("X-Original-URL", "https://one-factor.example.com")
|
||||
|
||||
mock.UserProviderMock.EXPECT().
|
||||
CheckUserPassword(gomock.Eq("john"), gomock.Eq("password")).
|
||||
Return(true, nil)
|
||||
|
||||
mock.UserProviderMock.EXPECT().
|
||||
GetDetails(gomock.Eq("john")).
|
||||
Return(&authentication.UserDetails{
|
||||
Emails: []string{"john@example.com"},
|
||||
Groups: []string{"dev", "admins"},
|
||||
}, nil)
|
||||
|
||||
VerifyGet(verifyGetCfg)(mock.Ctx)
|
||||
|
||||
assert.Equal(s.T(), 200, mock.Ctx.Response.StatusCode())
|
||||
}
|
||||
|
||||
func (s *BasicAuthorizationSuite) TestShouldVerifyAuthBasicArgFailingNoHeader() {
|
||||
mock := mocks.NewMockAutheliaCtx(s.T())
|
||||
defer mock.Close()
|
||||
|
||||
mock.Ctx.QueryArgs().Add("auth", "basic")
|
||||
mock.Ctx.Request.Header.Set("X-Original-URL", "https://one-factor.example.com")
|
||||
|
||||
VerifyGet(verifyGetCfg)(mock.Ctx)
|
||||
|
||||
assert.Equal(s.T(), 401, mock.Ctx.Response.StatusCode())
|
||||
assert.Equal(s.T(), "Unauthorized", string(mock.Ctx.Response.Body()))
|
||||
assert.NotEmpty(s.T(), mock.Ctx.Response.Header.Peek("WWW-Authenticate"))
|
||||
assert.Regexp(s.T(), regexp.MustCompile("^Basic realm="), string(mock.Ctx.Response.Header.Peek("WWW-Authenticate")))
|
||||
}
|
||||
|
||||
func (s *BasicAuthorizationSuite) TestShouldVerifyAuthBasicArgFailingEmptyHeader() {
|
||||
mock := mocks.NewMockAutheliaCtx(s.T())
|
||||
defer mock.Close()
|
||||
|
||||
mock.Ctx.QueryArgs().Add("auth", "basic")
|
||||
mock.Ctx.Request.Header.Set("Authorization", "")
|
||||
mock.Ctx.Request.Header.Set("X-Original-URL", "https://one-factor.example.com")
|
||||
|
||||
VerifyGet(verifyGetCfg)(mock.Ctx)
|
||||
|
||||
assert.Equal(s.T(), 401, mock.Ctx.Response.StatusCode())
|
||||
assert.Equal(s.T(), "Unauthorized", string(mock.Ctx.Response.Body()))
|
||||
assert.NotEmpty(s.T(), mock.Ctx.Response.Header.Peek("WWW-Authenticate"))
|
||||
assert.Regexp(s.T(), regexp.MustCompile("^Basic realm="), string(mock.Ctx.Response.Header.Peek("WWW-Authenticate")))
|
||||
}
|
||||
|
||||
func (s *BasicAuthorizationSuite) TestShouldVerifyAuthBasicArgFailingWrongPassword() {
|
||||
mock := mocks.NewMockAutheliaCtx(s.T())
|
||||
defer mock.Close()
|
||||
|
||||
mock.Ctx.QueryArgs().Add("auth", "basic")
|
||||
mock.Ctx.Request.Header.Set("Authorization", "Basic am9objpwYXNzd29yZA==")
|
||||
mock.Ctx.Request.Header.Set("X-Original-URL", "https://one-factor.example.com")
|
||||
|
||||
mock.UserProviderMock.EXPECT().
|
||||
CheckUserPassword(gomock.Eq("john"), gomock.Eq("password")).
|
||||
Return(false, nil)
|
||||
|
||||
VerifyGet(verifyGetCfg)(mock.Ctx)
|
||||
|
||||
assert.Equal(s.T(), 401, mock.Ctx.Response.StatusCode())
|
||||
assert.Equal(s.T(), "Unauthorized", string(mock.Ctx.Response.Body()))
|
||||
assert.NotEmpty(s.T(), mock.Ctx.Response.Header.Peek("WWW-Authenticate"))
|
||||
assert.Regexp(s.T(), regexp.MustCompile("^Basic realm="), string(mock.Ctx.Response.Header.Peek("WWW-Authenticate")))
|
||||
}
|
||||
|
||||
func (s *BasicAuthorizationSuite) TestShouldVerifyAuthBasicArgFailingWrongHeader() {
|
||||
mock := mocks.NewMockAutheliaCtx(s.T())
|
||||
defer mock.Close()
|
||||
|
||||
mock.Ctx.QueryArgs().Add("auth", "basic")
|
||||
mock.Ctx.Request.Header.Set("Proxy-Authorization", "Basic am9objpwYXNzd29yZA==")
|
||||
mock.Ctx.Request.Header.Set("X-Original-URL", "https://one-factor.example.com")
|
||||
|
||||
VerifyGet(verifyGetCfg)(mock.Ctx)
|
||||
|
||||
assert.Equal(s.T(), 401, mock.Ctx.Response.StatusCode())
|
||||
assert.Equal(s.T(), "Unauthorized", string(mock.Ctx.Response.Body()))
|
||||
assert.NotEmpty(s.T(), mock.Ctx.Response.Header.Peek("WWW-Authenticate"))
|
||||
assert.Regexp(s.T(), regexp.MustCompile("^Basic realm="), string(mock.Ctx.Response.Header.Peek("WWW-Authenticate")))
|
||||
}
|
||||
|
||||
func TestShouldVerifyAuthorizationsUsingBasicAuth(t *testing.T) {
|
||||
suite.Run(t, NewBasicAuthorizationSuite())
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue