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
|
* Authelia portal
|
||||||
* Protected endpoint (Nextcloud)
|
* 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)
|
* [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:
|
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:
|
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 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
|
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:
|
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-jenkins hdr(host) -i jenkins.example.com
|
||||||
acl host-nextcloud hdr(host) -i nextcloud.example.com
|
acl host-nextcloud hdr(host) -i nextcloud.example.com
|
||||||
acl host-phpmyadmin hdr(host) -i phpmyadmin.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:
|
3. Add backend route for your service(s), for example:
|
||||||
```
|
```
|
||||||
use_backend be_jenkins if host-jenkins
|
use_backend be_jenkins if host-jenkins
|
||||||
use_backend be_nextcloud if host-nextcloud
|
use_backend be_nextcloud if host-nextcloud
|
||||||
use_backend be_phpmyadmin if host-phpmyadmin
|
use_backend be_phpmyadmin if host-phpmyadmin
|
||||||
|
use_backend be_heimdall if host-heimdall
|
||||||
```
|
```
|
||||||
4. Add backend definitions for your service(s), for example:
|
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
|
server nextcloud nextcloud:443 ssl verify none
|
||||||
backend be_phpmyadmin
|
backend be_phpmyadmin
|
||||||
server phpmyadmin phpmyadmin:80
|
server phpmyadmin phpmyadmin:80
|
||||||
|
backend be_heimdall
|
||||||
|
server heimdall heimdall:443 ssl verify none
|
||||||
```
|
```
|
||||||
|
|
||||||
### Secure Authelia with TLS
|
### Secure Authelia with TLS
|
||||||
|
@ -87,8 +93,10 @@ frontend fe_http
|
||||||
|
|
||||||
# Host ACLs
|
# Host ACLs
|
||||||
acl protected-frontends hdr(host) -m reg -i ^(?i)(nextcloud)\.example\.com
|
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-authelia hdr(host) -i auth.example.com
|
||||||
acl host-nextcloud hdr(host) -i nextcloud.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(https) if { ssl_fc }
|
||||||
http-request set-var(req.scheme) str(http) 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
|
# Protect endpoints with haproxy-auth-request and Authelia
|
||||||
http-request lua.auth-request be_authelia /api/verify if protected-frontends
|
http-request lua.auth-request be_authelia /api/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
|
# Authelia backend route
|
||||||
use_backend be_authelia if host-authelia
|
use_backend be_authelia if host-authelia
|
||||||
# Redirect protected-frontends to Authelia if not authenticated
|
# 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)
|
# Service backend route(s)
|
||||||
use_backend be_nextcloud if host-nextcloud
|
use_backend be_nextcloud if host-nextcloud
|
||||||
|
use_backend be_heimdall if host-heimdall
|
||||||
|
|
||||||
backend be_authelia
|
backend be_authelia
|
||||||
server authelia authelia:9091
|
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
|
http-request set-header Remote-Email %[var(req.auth_response_header.remote_email)] if remote_email_exist
|
||||||
|
|
||||||
server nextcloud nextcloud:443 ssl verify none
|
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)
|
##### haproxy.cfg (TLS enabled Authelia)
|
||||||
|
@ -147,8 +171,10 @@ frontend fe_http
|
||||||
|
|
||||||
# Host ACLs
|
# Host ACLs
|
||||||
acl protected-frontends hdr(host) -m reg -i ^(?i)(nextcloud)\.example\.com
|
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-authelia hdr(host) -i auth.example.com
|
||||||
acl host-nextcloud hdr(host) -i nextcloud.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(https) if { ssl_fc }
|
||||||
http-request set-var(req.scheme) str(http) 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
|
# Protect endpoints with haproxy-auth-request and Authelia
|
||||||
http-request lua.auth-request be_authelia_proxy /api/verify if protected-frontends
|
http-request lua.auth-request be_authelia_proxy /api/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
|
# Authelia backend route
|
||||||
use_backend be_authelia if host-authelia
|
use_backend be_authelia if host-authelia
|
||||||
# Redirect protected-frontends to Authelia if not authenticated
|
# 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)
|
# Service backend route(s)
|
||||||
use_backend be_nextcloud if host-nextcloud
|
use_backend be_nextcloud if host-nextcloud
|
||||||
|
use_backend be_heimdall if host-heimdall
|
||||||
|
|
||||||
backend be_authelia
|
backend be_authelia
|
||||||
server authelia authelia:9091
|
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
|
http-request set-header Remote-Email %[var(req.auth_response_header.remote_email)] if remote_email_exist
|
||||||
|
|
||||||
server nextcloud nextcloud:443 ssl verify none
|
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/
|
[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
|
#### Supplementary config
|
||||||
|
|
||||||
##### authelia.conf
|
##### authelia.conf
|
||||||
|
|
||||||
```nginx
|
```nginx
|
||||||
|
set $upstream_authelia http://authelia:9091/api/verify;
|
||||||
|
|
||||||
# Virtual endpoint created by nginx to forward auth requests.
|
# Virtual endpoint created by nginx to forward auth requests.
|
||||||
location /authelia {
|
location /authelia {
|
||||||
internal;
|
internal;
|
||||||
set $upstream_authelia http://authelia:9091/api/verify;
|
|
||||||
proxy_pass_request_body off;
|
proxy_pass_request_body off;
|
||||||
proxy_pass $upstream_authelia;
|
proxy_pass $upstream_authelia;
|
||||||
proxy_set_header Content-Length "";
|
proxy_set_header Content-Length "";
|
||||||
|
@ -67,6 +69,7 @@ location /authelia {
|
||||||
```
|
```
|
||||||
|
|
||||||
##### auth.conf
|
##### auth.conf
|
||||||
|
|
||||||
```nginx
|
```nginx
|
||||||
# Basic Authelia Config
|
# Basic Authelia Config
|
||||||
# Send a subsequent request to Authelia to verify if the user is authenticated
|
# 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
|
##### proxy.conf
|
||||||
|
|
||||||
```nginx
|
```nginx
|
||||||
client_body_buffer_size 128k;
|
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/
|
[nginx]: https://www.nginx.com/
|
||||||
|
|
|
@ -17,11 +17,17 @@ Below you will find commented examples of the following configuration:
|
||||||
* Traefik 1.x
|
* Traefik 1.x
|
||||||
* Authelia portal
|
* Authelia portal
|
||||||
* Protected endpoint (Nextcloud)
|
* 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).
|
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.
|
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
|
##### docker-compose.yml
|
||||||
```yml
|
```yml
|
||||||
version: '3'
|
version: '3'
|
||||||
|
@ -94,6 +100,26 @@ services:
|
||||||
- PUID=1000
|
- PUID=1000
|
||||||
- PGID=1000
|
- PGID=1000
|
||||||
- TZ=Australia/Melbourne
|
- 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/
|
[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
|
* Traefik 2.x
|
||||||
* Authelia portal
|
* Authelia portal
|
||||||
* Protected endpoint (Nextcloud)
|
* 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).
|
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.
|
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
|
##### docker-compose.yml
|
||||||
```yml
|
```yml
|
||||||
version: '3'
|
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.address=http://authelia:9091/api/verify?rd=https://login.example.com/'
|
||||||
- 'traefik.http.middlewares.authelia.forwardauth.trustForwardHeader=true'
|
- 'traefik.http.middlewares.authelia.forwardauth.trustForwardHeader=true'
|
||||||
- 'traefik.http.middlewares.authelia.forwardauth.authResponseHeaders=Remote-User, Remote-Groups, Remote-Name, Remote-Email'
|
- 'traefik.http.middlewares.authelia.forwardauth.authResponseHeaders=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:
|
expose:
|
||||||
- 9091
|
- 9091
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
@ -104,6 +113,27 @@ services:
|
||||||
- PUID=1000
|
- PUID=1000
|
||||||
- PGID=1000
|
- PGID=1000
|
||||||
- TZ=Australia/Melbourne
|
- 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
|
## 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).
|
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
|
Authelia reads credentials from the header `Proxy-Authorization` instead of
|
||||||
the usual `Authorization` header. This is because in some circumstances both Authelia
|
the usual `Authorization` header. This is because in some circumstances both Authelia
|
||||||
and the application could require authentication in order to provide specific
|
and the application could require authentication in order to provide specific
|
||||||
authorizations at the level of the application.
|
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
|
## Session-Username header
|
||||||
|
|
||||||
|
|
|
@ -11,8 +11,11 @@ const ResetPasswordAction = "ResetPassword"
|
||||||
|
|
||||||
const authPrefix = "Basic "
|
const authPrefix = "Basic "
|
||||||
|
|
||||||
// AuthorizationHeader is the basic-auth HTTP header Authelia utilises.
|
// ProxyAuthorizationHeader is the basic-auth HTTP header Authelia utilises.
|
||||||
const AuthorizationHeader = "Proxy-Authorization"
|
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.
|
// SessionUsernameHeader is used as additional protection to validate a user for things like pam_exec.
|
||||||
const SessionUsernameHeader = "Session-Username"
|
const SessionUsernameHeader = "Session-Username"
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
@ -75,9 +76,9 @@ func getOriginalURL(ctx *middlewares.AutheliaCtx) (*url.URL, error) {
|
||||||
|
|
||||||
// parseBasicAuth parses an HTTP Basic Authentication string.
|
// parseBasicAuth parses an HTTP Basic Authentication string.
|
||||||
// "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==" returns ("Aladdin", "open sesame", true).
|
// "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) {
|
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):])
|
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, ':')
|
s := strings.IndexByte(cs, ':')
|
||||||
|
|
||||||
if s < 0 {
|
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
|
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
|
// verifyBasicAuth verify that the provided username and password are correct and
|
||||||
// that the user is authorized to target the resource.
|
// 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
|
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(string(auth))
|
username, password, err := parseBasicAuth(header, string(auth))
|
||||||
|
|
||||||
if err != nil {
|
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)
|
authenticated, err := ctx.Providers.UserProvider.CheckUserPassword(username, password)
|
||||||
|
|
||||||
if err != nil {
|
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.
|
// 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
|
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
|
// 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
|
// 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.
|
// is computed from X-Forwarded-* headers or X-Original-URL.
|
||||||
|
@ -394,6 +403,47 @@ func getProfileRefreshSettings(cfg schema.AuthenticationBackendConfiguration) (r
|
||||||
return refresh, refreshInterval
|
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.
|
// VerifyGet returns the handler verifying if a request is allowed to go through.
|
||||||
func VerifyGet(cfg schema.AuthenticationBackendConfiguration) middlewares.RequestHandler {
|
func VerifyGet(cfg schema.AuthenticationBackendConfiguration) middlewares.RequestHandler {
|
||||||
refreshProfile, refreshProfileInterval := getProfileRefreshSettings(cfg)
|
refreshProfile, refreshProfileInterval := getProfileRefreshSettings(cfg)
|
||||||
|
@ -423,40 +473,7 @@ func VerifyGet(cfg schema.AuthenticationBackendConfiguration) middlewares.Reques
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var username, name string
|
isBasicAuth, username, name, groups, emails, authLevel, err := verifyAuth(ctx, targetURL, refreshProfile, refreshProfileInterval)
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Logger.Error(fmt.Sprintf("Error caught when verifying user authorization: %s", err))
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
handleUnauthorized(ctx, targetURL, username)
|
handleUnauthorized(ctx, targetURL, isBasicAuth, username)
|
||||||
|
|
||||||
return
|
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.Logger.Infof("Access to %s is forbidden to user %s", targetURL.String(), username)
|
||||||
ctx.ReplyForbidden()
|
ctx.ReplyForbidden()
|
||||||
case NotAuthorized:
|
case NotAuthorized:
|
||||||
handleUnauthorized(ctx, targetURL, username)
|
handleUnauthorized(ctx, targetURL, isBasicAuth, username)
|
||||||
case Authorized:
|
case Authorized:
|
||||||
setForwardedHeaders(&ctx.Response.Header, username, name, groups, emails)
|
setForwardedHeaders(&ctx.Response.Header, username, name, groups, emails)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"regexp"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -120,27 +121,34 @@ func TestShouldRaiseWhenXForwardedURIIsNotParsable(t *testing.T) {
|
||||||
|
|
||||||
// Test parseBasicAuth.
|
// Test parseBasicAuth.
|
||||||
func TestShouldRaiseWhenHeaderDoesNotContainBasicPrefix(t *testing.T) {
|
func TestShouldRaiseWhenHeaderDoesNotContainBasicPrefix(t *testing.T) {
|
||||||
_, _, err := parseBasicAuth("alzefzlfzemjfej==")
|
_, _, err := parseBasicAuth(ProxyAuthorizationHeader, "alzefzlfzemjfej==")
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Equal(t, "Basic prefix not found in Proxy-Authorization header", err.Error())
|
assert.Equal(t, "Basic prefix not found in Proxy-Authorization header", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestShouldRaiseWhenCredentialsAreNotInBase64(t *testing.T) {
|
func TestShouldRaiseWhenCredentialsAreNotInBase64(t *testing.T) {
|
||||||
_, _, err := parseBasicAuth("Basic alzefzlfzemjfej==")
|
_, _, err := parseBasicAuth(ProxyAuthorizationHeader, "Basic alzefzlfzemjfej==")
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Equal(t, "illegal base64 data at input byte 16", err.Error())
|
assert.Equal(t, "illegal base64 data at input byte 16", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestShouldRaiseWhenCredentialsAreNotInCorrectForm(t *testing.T) {
|
func TestShouldRaiseWhenCredentialsAreNotInCorrectForm(t *testing.T) {
|
||||||
// The decoded format should be user:password.
|
// The decoded format should be user:password.
|
||||||
_, _, err := parseBasicAuth("Basic am9obiBwYXNzd29yZA==")
|
_, _, err := parseBasicAuth(ProxyAuthorizationHeader, "Basic am9obiBwYXNzd29yZA==")
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Equal(t, "Format of Proxy-Authorization header must be user:password", err.Error())
|
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) {
|
func TestShouldReturnUsernameAndPassword(t *testing.T) {
|
||||||
// the decoded format should be user:password.
|
// 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.NoError(t, err)
|
||||||
assert.Equal(t, "john", user)
|
assert.Equal(t, "john", user)
|
||||||
assert.Equal(t, "password", password)
|
assert.Equal(t, "password", password)
|
||||||
|
@ -204,7 +212,7 @@ func TestShouldVerifyWrongCredentials(t *testing.T) {
|
||||||
Return(false, nil)
|
Return(false, nil)
|
||||||
|
|
||||||
url, _ := url.ParseRequestURI("https://test.example.com")
|
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)
|
assert.Error(t, err)
|
||||||
}
|
}
|
||||||
|
@ -354,6 +362,97 @@ func (s *BasicAuthorizationSuite) TestShouldApplyPolicyOfDenyDomain() {
|
||||||
assert.Equal(s.T(), 403, mock.Ctx.Response.StatusCode())
|
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) {
|
func TestShouldVerifyAuthorizationsUsingBasicAuth(t *testing.T) {
|
||||||
suite.Run(t, NewBasicAuthorizationSuite())
|
suite.Run(t, NewBasicAuthorizationSuite())
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue