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
ThinkChaos 2021-02-23 23:35:04 +00:00 committed by GitHub
parent 4f099b76d7
commit ba65a3db82
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 464 additions and 60 deletions

View File

@ -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/

View File

@ -25,13 +25,15 @@ 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_pass $upstream_authelia;
proxy_set_header Content-Length "";
# Timeout if the real server is dead
@ -46,7 +48,7 @@ location /authelia {
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-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;
@ -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;
@ -145,7 +149,7 @@ server {
include /config/nginx/ssl.conf;
location / {
set $upstream_authelia http://authelia:9091; # This example assumes a Docker deployment
set $upstream_authelia http://authelia:9091; # This example assumes a Docker deployment
proxy_pass $upstream_authelia;
include /config/nginx/proxy.conf;
}
@ -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/

View File

@ -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/

View File

@ -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

View File

@ -21,20 +21,33 @@ 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
Authelia by default only verifies the cookie and the associated user with that cookie can
access a protected resource. The client browser does not know the username and does not send
this to Authelia, it's stored by Authelia for security reasons.
The Session-Username header has been implemented as a means
to use Authelia with non-web services such as PAM. Basically how it works is if the
Session-Username header is sent in the request to the /api/verify endpoint it will

View File

@ -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"

View File

@ -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)
}

View File

@ -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())
}