[BREAKING] Create a suite for Traefik proxy.
* Removal of the Redirect header sent by Authelia /api/verify endpoint. * Authelia does not consume Host header anymore but X-Forwarded-Proto and X-Forwarded-Host to compute the link sent in identity verification emails. * Authelia used Host header as the application name for U2F authentication but it's now using X-Forwarded-* headers.pull/355/head
parent
617e929e1a
commit
4016ff1bba
|
@ -2,7 +2,7 @@ import { connect } from 'react-redux';
|
||||||
import SecurityKeyRegistrationView from '../../../views/SecurityKeyRegistrationView/SecurityKeyRegistrationView';
|
import SecurityKeyRegistrationView from '../../../views/SecurityKeyRegistrationView/SecurityKeyRegistrationView';
|
||||||
import { RootState } from '../../../reducers';
|
import { RootState } from '../../../reducers';
|
||||||
import { Dispatch } from 'redux';
|
import { Dispatch } from 'redux';
|
||||||
import * as U2fApi from "u2f-api";
|
import U2fApi from "u2f-api";
|
||||||
import { Props } from '../../../views/SecurityKeyRegistrationView/SecurityKeyRegistrationView';
|
import { Props } from '../../../views/SecurityKeyRegistrationView/SecurityKeyRegistrationView';
|
||||||
import { registerSecurityKey, registerSecurityKeyFailure, registerSecurityKeySuccess } from '../../../reducers/Portal/SecurityKeyRegistration/actions';
|
import { registerSecurityKey, registerSecurityKeyFailure, registerSecurityKeySuccess } from '../../../reducers/Portal/SecurityKeyRegistration/actions';
|
||||||
import AutheliaService from '../../../services/AutheliaService';
|
import AutheliaService from '../../../services/AutheliaService';
|
||||||
|
@ -12,13 +12,8 @@ const mapStateToProps = (state: RootState) => ({
|
||||||
error: state.securityKeyRegistration.error,
|
error: state.securityKeyRegistration.error,
|
||||||
});
|
});
|
||||||
|
|
||||||
async function checkIdentity(token: string) {
|
|
||||||
return fetch(`/api/secondfactor/u2f/identity/finish?token=${token}`, {
|
|
||||||
method: 'POST',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function fail(dispatch: Dispatch, err: Error) {
|
function fail(dispatch: Dispatch, err: Error) {
|
||||||
|
console.error(err);
|
||||||
dispatch(registerSecurityKeyFailure(err.message));
|
dispatch(registerSecurityKeyFailure(err.message));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,9 +22,9 @@ const mapDispatchToProps = (dispatch: Dispatch, ownProps: Props) => {
|
||||||
onInit: async (token: string) => {
|
onInit: async (token: string) => {
|
||||||
try {
|
try {
|
||||||
dispatch(registerSecurityKey());
|
dispatch(registerSecurityKey());
|
||||||
await checkIdentity(token);
|
await AutheliaService.completeSecurityKeyRegistrationIdentityValidation(token);
|
||||||
const requestRegister = await AutheliaService.requestSecurityKeyRegistration();
|
const registerRequest = await AutheliaService.requestSecurityKeyRegistration();
|
||||||
const registerResponse = await U2fApi.register(requestRegister, [], 60);
|
const registerResponse = await U2fApi.register([registerRequest], [], 60);
|
||||||
await AutheliaService.completeSecurityKeyRegistration(registerResponse);
|
await AutheliaService.completeSecurityKeyRegistration(registerResponse);
|
||||||
dispatch(registerSecurityKeySuccess());
|
dispatch(registerSecurityKeySuccess());
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import RemoteState from "../views/AuthenticationView/RemoteState";
|
import RemoteState from "../views/AuthenticationView/RemoteState";
|
||||||
import u2fApi, { SignRequest } from "u2f-api";
|
import U2fApi, { SignRequest } from "u2f-api";
|
||||||
import Method2FA from "../types/Method2FA";
|
import Method2FA from "../types/Method2FA";
|
||||||
import RedirectResponse from "./RedirectResponse";
|
import RedirectResponse from "./RedirectResponse";
|
||||||
import PreferedMethodResponse from "./PreferedMethodResponse";
|
import PreferedMethodResponse from "./PreferedMethodResponse";
|
||||||
|
@ -74,15 +74,11 @@ class AutheliaService {
|
||||||
}
|
}
|
||||||
|
|
||||||
static async requestSigning() {
|
static async requestSigning() {
|
||||||
return this.fetchSafe('/api/u2f/sign_request')
|
return this.fetchSafeJson<SignRequest>('/api/u2f/sign_request');
|
||||||
.then(async (res) => {
|
|
||||||
const body = await res.json();
|
|
||||||
return body as SignRequest;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static async completeSecurityKeySigning(
|
static async completeSecurityKeySigning(
|
||||||
response: u2fApi.SignResponse, redirectionUrl: string | null) {
|
response: U2fApi.SignResponse, redirectionUrl: string | null) {
|
||||||
|
|
||||||
const headers: Record<string, string> = {
|
const headers: Record<string, string> = {
|
||||||
'Accept': 'application/json',
|
'Accept': 'application/json',
|
||||||
|
@ -199,7 +195,7 @@ class AutheliaService {
|
||||||
return await this.fetchSafeJson('/api/secondfactor/available');
|
return await this.fetchSafeJson('/api/secondfactor/available');
|
||||||
}
|
}
|
||||||
|
|
||||||
static async completeSecurityKeyRegistration(response: u2fApi.RegisterResponse): Promise<Response> {
|
static async completeSecurityKeyRegistration(response: U2fApi.RegisterResponse): Promise<Response> {
|
||||||
return await this.fetchSafe('/api/u2f/register', {
|
return await this.fetchSafe('/api/u2f/register', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -211,7 +207,18 @@ class AutheliaService {
|
||||||
}
|
}
|
||||||
|
|
||||||
static async requestSecurityKeyRegistration() {
|
static async requestSecurityKeyRegistration() {
|
||||||
return this.fetchSafeJson<u2fApi.RegisterRequest>('/api/u2f/register_request')
|
return this.fetchSafeJson<U2fApi.RegisterRequest>('/api/u2f/register_request')
|
||||||
|
}
|
||||||
|
|
||||||
|
static async completeSecurityKeyRegistrationIdentityValidation(token: string) {
|
||||||
|
const res = await this.fetchSafeJson(`/api/secondfactor/u2f/identity/finish?token=${token}`, {
|
||||||
|
method: 'POST',
|
||||||
|
});
|
||||||
|
|
||||||
|
if ('error' in res) {
|
||||||
|
throw new Error(res['error']);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
version: '2'
|
version: '2'
|
||||||
# services: {}
|
|
||||||
networks:
|
networks:
|
||||||
authelianet:
|
authelianet:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
version: '2'
|
|
||||||
networks:
|
|
||||||
authelianet:
|
|
||||||
external: true
|
|
|
@ -2,5 +2,9 @@ version: '2'
|
||||||
services:
|
services:
|
||||||
nginx-backend:
|
nginx-backend:
|
||||||
image: authelia-example-backend
|
image: authelia-example-backend
|
||||||
|
labels:
|
||||||
|
- traefik.frontend.rule=Host:home.example.com,public.example.com,secure.example.com,admin.example.com,singlefactor.example.com
|
||||||
|
- traefik.frontend.auth.forward.address=http://192.168.240.1:9091/api/verify?rd=https://login.example.com:8080/%23/
|
||||||
|
- traefik.frontend.auth.forward.tls.insecureSkipVerify=true
|
||||||
networks:
|
networks:
|
||||||
- authelianet
|
- authelianet
|
||||||
|
|
|
@ -35,10 +35,15 @@ http {
|
||||||
|
|
||||||
# Serve the backend API for the portal.
|
# Serve the backend API for the portal.
|
||||||
location /api {
|
location /api {
|
||||||
proxy_set_header Host $http_host;
|
|
||||||
proxy_set_header X-Original-URI $request_uri;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
|
||||||
|
# Required by Authelia because "trust proxy" option is used.
|
||||||
|
# See https://expressjs.com/en/guide/behind-proxies.html
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
|
# Required by Authelia to build correct links for identity validation.
|
||||||
|
proxy_set_header X-Forwarded-Host $http_host;
|
||||||
|
|
||||||
# Needed for network ACLs to work. It appends the IP of the client to the list of IPs
|
# Needed for network ACLs to work. It appends the IP of the client to the list of IPs
|
||||||
# and allows Authelia to use it to match the network-based ACLs.
|
# and allows Authelia to use it to match the network-based ACLs.
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
@ -65,11 +70,15 @@ http {
|
||||||
|
|
||||||
# Serve the backend API for the portal.
|
# Serve the backend API for the portal.
|
||||||
location /api {
|
location /api {
|
||||||
proxy_set_header Host $http_host;
|
|
||||||
proxy_set_header X-Original-URI $request_uri;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
|
||||||
|
# Required by Authelia because "trust proxy" option is used.
|
||||||
|
# See https://expressjs.com/en/guide/behind-proxies.html
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
|
# Required by Authelia to build correct links for identity validation.
|
||||||
|
proxy_set_header X-Forwarded-Host $http_host;
|
||||||
|
|
||||||
# Needed for network ACLs to work. It appends the IP of the client to the list of IPs
|
# Needed for network ACLs to work. It appends the IP of the client to the list of IPs
|
||||||
# and allows Authelia to use it to match the network-based ACLs.
|
# and allows Authelia to use it to match the network-based ACLs.
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
@ -136,16 +145,15 @@ http {
|
||||||
# to the virtual endpoint introduced by nginx and declared in the next block.
|
# to the virtual endpoint introduced by nginx and declared in the next block.
|
||||||
location / {
|
location / {
|
||||||
auth_request /auth_verify;
|
auth_request /auth_verify;
|
||||||
|
|
||||||
auth_request_set $redirect $upstream_http_redirect;
|
|
||||||
|
|
||||||
auth_request_set $user $upstream_http_remote_user;
|
auth_request_set $user $upstream_http_remote_user;
|
||||||
proxy_set_header X-Forwarded-User $user;
|
proxy_set_header X-Forwarded-User $user;
|
||||||
|
|
||||||
auth_request_set $groups $upstream_http_remote_groups;
|
auth_request_set $groups $upstream_http_remote_groups;
|
||||||
proxy_set_header Remote-Groups $groups;
|
proxy_set_header Remote-Groups $groups;
|
||||||
|
|
||||||
proxy_set_header Host $http_host;
|
# Route the request to the correct virtual host in the backend.
|
||||||
|
proxy_set_header Host $http_host;
|
||||||
|
|
||||||
# Authelia relies on Proxy-Authorization header to authenticate in basic auth.
|
# Authelia relies on Proxy-Authorization header to authenticate in basic auth.
|
||||||
# but for the sake of simplicity (because Authorization in supported in most
|
# but for the sake of simplicity (because Authorization in supported in most
|
||||||
|
@ -153,20 +161,35 @@ http {
|
||||||
# Proxy-Authorization before sending it to Authelia.
|
# Proxy-Authorization before sending it to Authelia.
|
||||||
proxy_set_header Proxy-Authorization $http_authorization;
|
proxy_set_header Proxy-Authorization $http_authorization;
|
||||||
|
|
||||||
error_page 401 =302 https://login.example.com:8080/#/?rd=$redirect;
|
# mitigate HTTPoxy Vulnerability
|
||||||
|
# https://www.nginx.com/blog/mitigating-the-httpoxy-vulnerability-with-nginx/
|
||||||
|
proxy_set_header Proxy "";
|
||||||
|
|
||||||
proxy_pass $upstream_endpoint;
|
# Set the `target_url` variable based on the request. It will be used to build the portal
|
||||||
|
# URL with the correct redirection parameter.
|
||||||
|
set $target_url $scheme://$http_host$request_uri;
|
||||||
|
error_page 401 =302 https://login.example.com:8080/#/?rd=$target_url;
|
||||||
|
|
||||||
|
proxy_pass $upstream_endpoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Virtual endpoint forwarding requests to Authelia server.
|
# Virtual endpoint forwarding requests to Authelia server.
|
||||||
location /auth_verify {
|
location /auth_verify {
|
||||||
internal;
|
internal;
|
||||||
proxy_set_header Host $http_host;
|
|
||||||
|
|
||||||
proxy_set_header X-Original-URI $request_uri;
|
|
||||||
proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
#
|
||||||
|
# X-Forwarded-Proto is mandatory since Authelia uses the "trust proxy" option.
|
||||||
|
# See https://expressjs.com/en/guide/behind-proxies.html
|
||||||
|
proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
|
||||||
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
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-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
|
||||||
# Authelia can receive Proxy-Authorization to authenticate however most of the clients
|
# Authelia can receive Proxy-Authorization to authenticate however most of the clients
|
||||||
|
@ -176,24 +199,23 @@ http {
|
||||||
proxy_pass_request_body off;
|
proxy_pass_request_body off;
|
||||||
proxy_set_header Content-Length "";
|
proxy_set_header Content-Length "";
|
||||||
|
|
||||||
proxy_pass $upstream_verify;
|
proxy_pass $upstream_verify;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Used by suites to test the forwarded users and groups headers produced by Authelia.
|
# Used by suites to test the forwarded users and groups headers produced by Authelia.
|
||||||
location /headers {
|
location /headers {
|
||||||
auth_request /auth_verify;
|
auth_request /auth_verify;
|
||||||
|
|
||||||
auth_request_set $redirect $upstream_http_redirect;
|
auth_request_set $user $upstream_http_remote_user;
|
||||||
|
proxy_set_header Custom-Forwarded-User $user;
|
||||||
auth_request_set $user $upstream_http_remote_user;
|
|
||||||
proxy_set_header Custom-Forwarded-User $user;
|
|
||||||
|
|
||||||
auth_request_set $groups $upstream_http_remote_groups;
|
auth_request_set $groups $upstream_http_remote_groups;
|
||||||
proxy_set_header Custom-Forwarded-Groups $groups;
|
proxy_set_header Custom-Forwarded-Groups $groups;
|
||||||
|
|
||||||
error_page 401 =302 https://login.example.com:8080/#/?rd=$redirect;
|
set $target_url $scheme://$http_host$request_uri;
|
||||||
|
error_page 401 =302 https://login.example.com:8080/#/?rd=$target_url;
|
||||||
|
|
||||||
proxy_pass $upstream_headers;
|
proxy_pass $upstream_headers;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,5 +4,8 @@ services:
|
||||||
image: schickling/mailcatcher
|
image: schickling/mailcatcher
|
||||||
ports:
|
ports:
|
||||||
- "1025:1025"
|
- "1025:1025"
|
||||||
|
labels:
|
||||||
|
- traefik.frontend.rule=Host:mail.example.com
|
||||||
|
- traefik.port=1080
|
||||||
networks:
|
networks:
|
||||||
- authelianet
|
- authelianet
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
traefik.toml
|
|
@ -0,0 +1,14 @@
|
||||||
|
version: '2'
|
||||||
|
services:
|
||||||
|
traefik:
|
||||||
|
image: traefik:v1.7.9-alpine
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
- ./example/compose/traefik/traefik.toml:/etc/traefik/traefik.toml
|
||||||
|
labels:
|
||||||
|
- traefik.frontend.rule=Host:traefik.example.com
|
||||||
|
- traefik.port=8081
|
||||||
|
networks:
|
||||||
|
authelianet:
|
||||||
|
# Set the IP to be able to query on port 443
|
||||||
|
ipv4_address: 192.168.240.100
|
|
@ -0,0 +1,40 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const ejs = require('ejs');
|
||||||
|
const fs = require('fs');
|
||||||
|
const program = require('commander');
|
||||||
|
|
||||||
|
let backend;
|
||||||
|
|
||||||
|
program
|
||||||
|
.version('0.1.0')
|
||||||
|
.option('-p, --production', 'Render template for production.')
|
||||||
|
.arguments('[backend]')
|
||||||
|
.action((backendArg) => backend = backendArg)
|
||||||
|
.parse(process.argv)
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
production: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!backend) {
|
||||||
|
backend = 'http://192.168.240.1:9091'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (program.production) {
|
||||||
|
options['production'] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
options['authelia_backend'] = backend;
|
||||||
|
|
||||||
|
const templatePath = __dirname + '/traefik.toml.ejs';
|
||||||
|
const outputPath = __dirname + '/traefik.toml';
|
||||||
|
|
||||||
|
html = ejs.renderFile(templatePath, options, (err, conf) => {
|
||||||
|
try {
|
||||||
|
var fd = fs.openSync(outputPath, 'w');
|
||||||
|
fs.writeFileSync(fd, conf);
|
||||||
|
} catch (e) {
|
||||||
|
fs.writeFileSync(outputPath, conf);
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,75 @@
|
||||||
|
defaultEntryPoints = ["http", "https"]
|
||||||
|
logLevel = "DEBUG"
|
||||||
|
|
||||||
|
[traefikLog]
|
||||||
|
filePath = "/var/log/traefik.log"
|
||||||
|
|
||||||
|
[entryPoints]
|
||||||
|
[entryPoints.http]
|
||||||
|
address = ":80"
|
||||||
|
[entryPoints.http.redirect]
|
||||||
|
entryPoint = "https"
|
||||||
|
[entryPoints.https]
|
||||||
|
address = ":8080"
|
||||||
|
[entryPoints.https.tls]
|
||||||
|
[entryPoints.api]
|
||||||
|
address = ":8081"
|
||||||
|
|
||||||
|
[file]
|
||||||
|
|
||||||
|
# TODO(c.michaud): remove this template by providing a proxy doing
|
||||||
|
# the routing depending on the mode (production or dev)
|
||||||
|
<% if (!production) { %>
|
||||||
|
|
||||||
|
[frontends]
|
||||||
|
[frontends.authelia_api]
|
||||||
|
backend = "authelia_api_backend"
|
||||||
|
[frontends.authelia_api.routes.route0]
|
||||||
|
rule = "Host:login.example.com; PathPrefix:/api;"
|
||||||
|
|
||||||
|
[frontends.authelia_front]
|
||||||
|
backend = "authelia_front_backend"
|
||||||
|
[frontends.authelia_front.routes.route0]
|
||||||
|
rule = "Host:login.example.com"
|
||||||
|
|
||||||
|
[backends]
|
||||||
|
[backends.authelia_api_backend]
|
||||||
|
[backends.authelia_api_backend.servers.server]
|
||||||
|
url = "http://192.168.240.1:9091"
|
||||||
|
|
||||||
|
[backends.authelia_front_backend]
|
||||||
|
[backends.authelia_front_backend.servers.server]
|
||||||
|
url = "http://192.168.240.1:3000"
|
||||||
|
|
||||||
|
<% } else { %>
|
||||||
|
|
||||||
|
[frontends]
|
||||||
|
[frontends.authelia]
|
||||||
|
backend = "authelia_backend"
|
||||||
|
[frontends.authelia.routes.route0]
|
||||||
|
rule = "Host:login.example.com"
|
||||||
|
|
||||||
|
[backends]
|
||||||
|
[backends.authelia_backend]
|
||||||
|
[backends.authelia_backend.servers.server]
|
||||||
|
url = "http://192.168.240.1:9091"
|
||||||
|
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
|
||||||
|
[api]
|
||||||
|
# This is exposed via a subdomain and a proxy
|
||||||
|
entryPoint = "api"
|
||||||
|
dashboard = true
|
||||||
|
|
||||||
|
[docker]
|
||||||
|
# Docker server endpoint. Can be a tcp or a unix socket endpoint.
|
||||||
|
endpoint = "unix:///var/run/docker.sock"
|
||||||
|
# network = "traefik_default"
|
||||||
|
|
||||||
|
# Default domain used.
|
||||||
|
# Can be overridden by setting the "traefik.domain" label on a container.
|
||||||
|
domain = "localhost"
|
||||||
|
|
||||||
|
# Enable watch docker changes
|
||||||
|
watch = true
|
|
@ -1,61 +0,0 @@
|
||||||
###############################################################
|
|
||||||
# Authelia minimal configuration #
|
|
||||||
###############################################################
|
|
||||||
|
|
||||||
logs_level: debug
|
|
||||||
|
|
||||||
authentication_backend:
|
|
||||||
file:
|
|
||||||
path: /etc/authelia/users_database.yml
|
|
||||||
|
|
||||||
session:
|
|
||||||
secret: unsecure_session_secret
|
|
||||||
domain: example.com
|
|
||||||
|
|
||||||
# Configuration of the storage backend used to store data and secrets. i.e. totp data
|
|
||||||
storage:
|
|
||||||
local:
|
|
||||||
path: /etc/authelia/storage
|
|
||||||
|
|
||||||
# TOTP Issuer Name
|
|
||||||
#
|
|
||||||
# This will be the issuer name displayed in Google Authenticator
|
|
||||||
# See: https://github.com/google/google-authenticator/wiki/Key-Uri-Format for more info on issuer names
|
|
||||||
totp:
|
|
||||||
issuer: example.com
|
|
||||||
|
|
||||||
# Access Control
|
|
||||||
#
|
|
||||||
# Access control is a set of rules you can use to restrict user access to certain
|
|
||||||
# resources.
|
|
||||||
access_control:
|
|
||||||
# Default policy can either be `bypass`, `one_factor`, `two_factor` or `deny`.
|
|
||||||
default_policy: deny
|
|
||||||
|
|
||||||
rules:
|
|
||||||
- domain: traefik.example.com
|
|
||||||
policy: two_factor
|
|
||||||
- domain: who.example.com
|
|
||||||
policy: two_factor
|
|
||||||
|
|
||||||
# Configuration of the authentication regulation mechanism.
|
|
||||||
regulation:
|
|
||||||
# Set it to 0 to disable max_retries.
|
|
||||||
max_retries: 3
|
|
||||||
|
|
||||||
# The user is banned if the authenticaction failed `max_retries` times in a `find_time` seconds window.
|
|
||||||
find_time: 120
|
|
||||||
|
|
||||||
# The length of time before a banned user can login again.
|
|
||||||
ban_time: 300
|
|
||||||
|
|
||||||
# Default redirection URL
|
|
||||||
#
|
|
||||||
# Note: this parameter is optional. If not provided, user won't
|
|
||||||
# be redirected upon successful authentication.
|
|
||||||
#default_redirection_url: https://authelia.example.domain
|
|
||||||
|
|
||||||
notifier:
|
|
||||||
# For testing purpose, notifications can be sent in a file
|
|
||||||
filesystem:
|
|
||||||
filename: /tmp/authelia/notification.txt
|
|
|
@ -1,38 +0,0 @@
|
||||||
version: '2'
|
|
||||||
services:
|
|
||||||
traefik:
|
|
||||||
image: traefik
|
|
||||||
ports:
|
|
||||||
- "80:80"
|
|
||||||
- "443:443"
|
|
||||||
volumes:
|
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
|
||||||
- ./traefik.toml:/etc/traefik/traefik.toml
|
|
||||||
- /etc/traefik
|
|
||||||
labels:
|
|
||||||
- traefik.frontend.rule=Host:traefik.example.com
|
|
||||||
- traefik.port=8080
|
|
||||||
- traefik.frontend.auth.forward.address=https://auth.example.com/api/verify?rd=https://auth.example.com
|
|
||||||
- traefik.frontend.auth.forward.tls.insecureSkipVerify=true
|
|
||||||
|
|
||||||
authelia:
|
|
||||||
# image: clems4ever/authelia:latest
|
|
||||||
build:
|
|
||||||
context: ../..
|
|
||||||
dockerfile: Dockerfile.dev
|
|
||||||
restart: always
|
|
||||||
volumes:
|
|
||||||
- ./config.minimal.yml:/etc/authelia/config.yml:ro
|
|
||||||
- ./users_database.yml:/etc/authelia/users_database.yml:rw
|
|
||||||
- /tmp/authelia:/tmp/authelia
|
|
||||||
environment:
|
|
||||||
- NODE_TLS_REJECT_UNAUTHORIZED=1
|
|
||||||
labels:
|
|
||||||
- traefik.frontend.rule=Host:auth.example.com
|
|
||||||
|
|
||||||
whoami:
|
|
||||||
image: emilevauge/whoami
|
|
||||||
labels:
|
|
||||||
- traefik.frontend.rule=Host:who.example.com
|
|
||||||
- traefik.frontend.auth.forward.address=https://auth.example.com/api/verify?rd=https://auth.example.com
|
|
||||||
- traefik.frontend.auth.forward.tls.insecureSkipVerify=true
|
|
|
@ -1,30 +0,0 @@
|
||||||
defaultEntryPoints = ["http", "https"]
|
|
||||||
# logLevel = "DEBUG"
|
|
||||||
|
|
||||||
[entryPoints]
|
|
||||||
[entryPoints.http]
|
|
||||||
address = ":80"
|
|
||||||
[entryPoints.http.redirect]
|
|
||||||
entryPoint = "https"
|
|
||||||
[entryPoints.https]
|
|
||||||
address = ":443"
|
|
||||||
[entryPoints.https.tls]
|
|
||||||
[entryPoints.api]
|
|
||||||
address = ":8080"
|
|
||||||
|
|
||||||
[api]
|
|
||||||
# This is exposed via a subdomain and a proxy
|
|
||||||
entryPoint = "api"
|
|
||||||
dashboard = true
|
|
||||||
|
|
||||||
[docker]
|
|
||||||
# Docker server endpoint. Can be a tcp or a unix socket endpoint.
|
|
||||||
endpoint = "unix:///var/run/docker.sock"
|
|
||||||
# network = "traefik_default"
|
|
||||||
|
|
||||||
# Default domain used.
|
|
||||||
# Can be overridden by setting the "traefik.domain" label on a container.
|
|
||||||
domain = "localhost"
|
|
||||||
|
|
||||||
# Enable watch docker changes
|
|
||||||
watch = true
|
|
|
@ -51,6 +51,9 @@ async function checkHostsFile() {
|
||||||
await checkAndFixEntry(actualEntries, 'mail.example.com', '192.168.240.100');
|
await checkAndFixEntry(actualEntries, 'mail.example.com', '192.168.240.100');
|
||||||
await checkAndFixEntry(actualEntries, 'duo.example.com', '192.168.240.100');
|
await checkAndFixEntry(actualEntries, 'duo.example.com', '192.168.240.100');
|
||||||
|
|
||||||
|
// For Traefik suite.
|
||||||
|
await checkAndFixEntry(actualEntries, 'traefik.example.com', '192.168.240.100');
|
||||||
|
|
||||||
// For testing network ACLs.
|
// For testing network ACLs.
|
||||||
await checkAndFixEntry(actualEntries, 'proxy-client1.example.com', '192.168.240.201');
|
await checkAndFixEntry(actualEntries, 'proxy-client1.example.com', '192.168.240.201');
|
||||||
await checkAndFixEntry(actualEntries, 'proxy-client2.example.com', '192.168.240.202');
|
await checkAndFixEntry(actualEntries, 'proxy-client2.example.com', '192.168.240.202');
|
||||||
|
|
|
@ -10,11 +10,13 @@ import { AuthenticationSessionHandler } from "./AuthenticationSessionHandler";
|
||||||
import { AuthenticationSession } from "../../types/AuthenticationSession";
|
import { AuthenticationSession } from "../../types/AuthenticationSession";
|
||||||
import { ServerVariables } from "./ServerVariables";
|
import { ServerVariables } from "./ServerVariables";
|
||||||
import { IdentityValidable } from "./IdentityValidable";
|
import { IdentityValidable } from "./IdentityValidable";
|
||||||
|
import * as Constants from "../../../shared/constants";
|
||||||
|
|
||||||
import Identity = require("../../types/Identity");
|
import Identity = require("../../types/Identity");
|
||||||
import { IdentityValidationDocument }
|
import { IdentityValidationDocument }
|
||||||
from "./storage/IdentityValidationDocument";
|
from "./storage/IdentityValidationDocument";
|
||||||
import { OPERATION_FAILED } from "../../../shared/UserMessages";
|
import { OPERATION_FAILED } from "../../../shared/UserMessages";
|
||||||
|
import GetHeader from "./utils/GetHeader";
|
||||||
|
|
||||||
function createAndSaveToken(userid: string, challenge: string,
|
function createAndSaveToken(userid: string, challenge: string,
|
||||||
userDataStore: IUserDataStore): BluebirdPromise<string> {
|
userDataStore: IUserDataStore): BluebirdPromise<string> {
|
||||||
|
@ -111,8 +113,9 @@ export function post_start_validation(handler: IdentityValidable,
|
||||||
vars.userDataStore);
|
vars.userDataStore);
|
||||||
})
|
})
|
||||||
.then((token: string) => {
|
.then((token: string) => {
|
||||||
const host = req.get("Host");
|
const scheme = GetHeader(req, Constants.HEADER_X_FORWARDED_PROTO);
|
||||||
const link_url = util.format("https://%s/#%s?token=%s", host,
|
const host = GetHeader(req, Constants.HEADER_X_FORWARDED_HOST);
|
||||||
|
const link_url = util.format("%s://%s/#%s?token=%s", scheme, host,
|
||||||
handler.destinationPath(), token);
|
handler.destinationPath(), token);
|
||||||
vars.logger.info(req, "Notification sent to user \"%s\"",
|
vars.logger.info(req, "Notification sent to user \"%s\"",
|
||||||
identity.userid);
|
identity.userid);
|
||||||
|
|
|
@ -63,8 +63,7 @@ export default class RegistrationHandler implements IdentityValidable {
|
||||||
}
|
}
|
||||||
|
|
||||||
preValidationResponse(req: Express.Request, res: Express.Response) {
|
preValidationResponse(req: Express.Request, res: Express.Response) {
|
||||||
res.status(204);
|
res.json({message: "OK"});
|
||||||
res.send();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
postValidationInit(req: Express.Request) {
|
postValidationInit(req: Express.Request) {
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
|
|
||||||
import util = require("util");
|
|
||||||
import express = require("express");
|
|
||||||
|
|
||||||
function extract_app_id(req: express.Request): string {
|
|
||||||
return util.format("https://%s", req.headers.host);
|
|
||||||
}
|
|
||||||
|
|
||||||
export = {
|
|
||||||
extract_app_id: extract_app_id
|
|
||||||
};
|
|
|
@ -51,8 +51,7 @@ export default class RegistrationHandler implements IdentityValidable {
|
||||||
}
|
}
|
||||||
|
|
||||||
preValidationResponse(req: express.Request, res: express.Response) {
|
preValidationResponse(req: express.Request, res: express.Response) {
|
||||||
res.status(204);
|
res.json({message: "OK"});
|
||||||
res.send();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
postValidationInit(req: express.Request) {
|
postValidationInit(req: express.Request) {
|
||||||
|
@ -60,8 +59,7 @@ export default class RegistrationHandler implements IdentityValidable {
|
||||||
}
|
}
|
||||||
|
|
||||||
postValidationResponse(req: express.Request, res: express.Response) {
|
postValidationResponse(req: express.Request, res: express.Response) {
|
||||||
res.status(204);
|
res.json({message: "OK"});
|
||||||
res.send();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mailSubject(): string {
|
mailSubject(): string {
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import objectPath = require("object-path");
|
import objectPath = require("object-path");
|
||||||
import u2f_common = require("../U2FCommon");
|
|
||||||
import BluebirdPromise = require("bluebird");
|
import BluebirdPromise = require("bluebird");
|
||||||
import express = require("express");
|
import express = require("express");
|
||||||
import U2f = require("u2f");
|
import U2f = require("u2f");
|
||||||
|
@ -10,12 +9,16 @@ import { ServerVariables } from "../../../../ServerVariables";
|
||||||
import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler";
|
import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler";
|
||||||
import UserMessages = require("../../../../../../../shared/UserMessages");
|
import UserMessages = require("../../../../../../../shared/UserMessages");
|
||||||
import { AuthenticationSession } from "../../../../../../types/AuthenticationSession";
|
import { AuthenticationSession } from "../../../../../../types/AuthenticationSession";
|
||||||
|
import GetHeader from "../../../../utils/GetHeader";
|
||||||
|
import * as Constants from "../../../../../../../shared/constants";
|
||||||
|
|
||||||
|
|
||||||
export default function (vars: ServerVariables) {
|
export default function (vars: ServerVariables) {
|
||||||
function handler(req: express.Request, res: express.Response): BluebirdPromise<void> {
|
function handler(req: express.Request, res: express.Response): BluebirdPromise<void> {
|
||||||
let authSession: AuthenticationSession;
|
let authSession: AuthenticationSession;
|
||||||
const appid = u2f_common.extract_app_id(req);
|
const scheme = GetHeader(req, Constants.HEADER_X_FORWARDED_PROTO);
|
||||||
|
const host = GetHeader(req, Constants.HEADER_X_FORWARDED_HOST);
|
||||||
|
const appid = scheme + "://" + host;
|
||||||
const registrationResponse: U2f.RegistrationData = req.body;
|
const registrationResponse: U2f.RegistrationData = req.body;
|
||||||
|
|
||||||
return new BluebirdPromise(function (resolve, reject) {
|
return new BluebirdPromise(function (resolve, reject) {
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
|
|
||||||
import u2f_common = require("../U2FCommon");
|
|
||||||
import BluebirdPromise = require("bluebird");
|
import BluebirdPromise = require("bluebird");
|
||||||
import express = require("express");
|
import express = require("express");
|
||||||
import U2f = require("u2f");
|
import U2f = require("u2f");
|
||||||
|
@ -8,11 +7,15 @@ import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionH
|
||||||
import { AuthenticationSession } from "../../../../../../types/AuthenticationSession";
|
import { AuthenticationSession } from "../../../../../../types/AuthenticationSession";
|
||||||
import UserMessages = require("../../../../../../../shared/UserMessages");
|
import UserMessages = require("../../../../../../../shared/UserMessages");
|
||||||
import { ServerVariables } from "../../../../ServerVariables";
|
import { ServerVariables } from "../../../../ServerVariables";
|
||||||
|
import GetHeader from "../../../../utils/GetHeader";
|
||||||
|
import * as Constants from "../../../../../../../shared/constants";
|
||||||
|
|
||||||
export default function (vars: ServerVariables) {
|
export default function (vars: ServerVariables) {
|
||||||
function handler(req: express.Request, res: express.Response): BluebirdPromise<void> {
|
function handler(req: express.Request, res: express.Response): BluebirdPromise<void> {
|
||||||
let authSession: AuthenticationSession;
|
let authSession: AuthenticationSession;
|
||||||
const appid: string = u2f_common.extract_app_id(req);
|
const scheme = GetHeader(req, Constants.HEADER_X_FORWARDED_PROTO);
|
||||||
|
const host = GetHeader(req, Constants.HEADER_X_FORWARDED_HOST);
|
||||||
|
const appid = scheme + "://" + host;
|
||||||
|
|
||||||
return new BluebirdPromise(function (resolve, reject) {
|
return new BluebirdPromise(function (resolve, reject) {
|
||||||
authSession = AuthenticationSessionHandler.get(req, vars.logger);
|
authSession = AuthenticationSessionHandler.get(req, vars.logger);
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import objectPath = require("object-path");
|
import objectPath = require("object-path");
|
||||||
import u2f_common = require("../U2FCommon");
|
|
||||||
import BluebirdPromise = require("bluebird");
|
import BluebirdPromise = require("bluebird");
|
||||||
import express = require("express");
|
import express = require("express");
|
||||||
import { U2FRegistrationDocument } from "../../../../storage/U2FRegistrationDocument";
|
import { U2FRegistrationDocument } from "../../../../storage/U2FRegistrationDocument";
|
||||||
|
@ -11,11 +10,15 @@ import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionH
|
||||||
import UserMessages = require("../../../../../../../shared/UserMessages");
|
import UserMessages = require("../../../../../../../shared/UserMessages");
|
||||||
import { AuthenticationSession } from "../../../../../../types/AuthenticationSession";
|
import { AuthenticationSession } from "../../../../../../types/AuthenticationSession";
|
||||||
import { Level } from "../../../../authentication/Level";
|
import { Level } from "../../../../authentication/Level";
|
||||||
|
import GetHeader from "../../../../utils/GetHeader";
|
||||||
|
import * as Constants from "../../../../../../../shared/constants";
|
||||||
|
|
||||||
export default function (vars: ServerVariables) {
|
export default function (vars: ServerVariables) {
|
||||||
function handler(req: express.Request, res: express.Response): BluebirdPromise<void> {
|
function handler(req: express.Request, res: express.Response): BluebirdPromise<void> {
|
||||||
let authSession: AuthenticationSession;
|
let authSession: AuthenticationSession;
|
||||||
const appId = u2f_common.extract_app_id(req);
|
const scheme = GetHeader(req, Constants.HEADER_X_FORWARDED_PROTO);
|
||||||
|
const host = GetHeader(req, Constants.HEADER_X_FORWARDED_HOST);
|
||||||
|
const appid = scheme + "://" + host;
|
||||||
|
|
||||||
return new BluebirdPromise(function (resolve, reject) {
|
return new BluebirdPromise(function (resolve, reject) {
|
||||||
authSession = AuthenticationSessionHandler.get(req, vars.logger);
|
authSession = AuthenticationSessionHandler.get(req, vars.logger);
|
||||||
|
@ -28,7 +31,7 @@ export default function (vars: ServerVariables) {
|
||||||
})
|
})
|
||||||
.then(function () {
|
.then(function () {
|
||||||
const userid = authSession.userid;
|
const userid = authSession.userid;
|
||||||
return vars.userDataStore.retrieveU2FRegistration(userid, appId);
|
return vars.userDataStore.retrieveU2FRegistration(userid, appid);
|
||||||
})
|
})
|
||||||
.then(function (doc: U2FRegistrationDocument): BluebirdPromise<U2f.SignatureResult | U2f.Error> {
|
.then(function (doc: U2FRegistrationDocument): BluebirdPromise<U2f.SignatureResult | U2f.Error> {
|
||||||
const signRequest = authSession.sign_request;
|
const signRequest = authSession.sign_request;
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
|
|
||||||
import u2f_common = require("../../../secondfactor/u2f/U2FCommon");
|
|
||||||
import BluebirdPromise = require("bluebird");
|
import BluebirdPromise = require("bluebird");
|
||||||
import express = require("express");
|
import express = require("express");
|
||||||
import { U2FRegistrationDocument } from "../../../../storage/U2FRegistrationDocument";
|
import { U2FRegistrationDocument } from "../../../../storage/U2FRegistrationDocument";
|
||||||
|
@ -9,28 +7,31 @@ import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionH
|
||||||
import UserMessages = require("../../../../../../../shared/UserMessages");
|
import UserMessages = require("../../../../../../../shared/UserMessages");
|
||||||
import { ServerVariables } from "../../../../ServerVariables";
|
import { ServerVariables } from "../../../../ServerVariables";
|
||||||
import { AuthenticationSession } from "../../../../../../types/AuthenticationSession";
|
import { AuthenticationSession } from "../../../../../../types/AuthenticationSession";
|
||||||
|
import GetHeader from "../../../../utils/GetHeader";
|
||||||
|
import * as Constants from "../../../../../../../shared/constants";
|
||||||
|
|
||||||
export default function (vars: ServerVariables) {
|
export default function (vars: ServerVariables) {
|
||||||
function handler(req: express.Request, res: express.Response): BluebirdPromise<void> {
|
function handler(req: express.Request, res: express.Response): BluebirdPromise<void> {
|
||||||
let authSession: AuthenticationSession;
|
let authSession: AuthenticationSession;
|
||||||
const appId = u2f_common.extract_app_id(req);
|
const scheme = GetHeader(req, Constants.HEADER_X_FORWARDED_PROTO);
|
||||||
|
const host = GetHeader(req, Constants.HEADER_X_FORWARDED_HOST);
|
||||||
|
const appid = scheme + "://" + host;
|
||||||
|
|
||||||
return new BluebirdPromise(function (resolve, reject) {
|
return new BluebirdPromise(function (resolve, reject) {
|
||||||
authSession = AuthenticationSessionHandler.get(req, vars.logger);
|
authSession = AuthenticationSessionHandler.get(req, vars.logger);
|
||||||
resolve();
|
resolve();
|
||||||
})
|
})
|
||||||
.then(function () {
|
.then(function () {
|
||||||
return vars.userDataStore.retrieveU2FRegistration(authSession.userid, appId);
|
return vars.userDataStore.retrieveU2FRegistration(authSession.userid, appid);
|
||||||
})
|
})
|
||||||
.then(function (doc: U2FRegistrationDocument): BluebirdPromise<void> {
|
.then(function (doc: U2FRegistrationDocument): BluebirdPromise<void> {
|
||||||
if (!doc)
|
if (!doc)
|
||||||
return BluebirdPromise.reject(new exceptions.AccessDeniedError("No U2F registration document found."));
|
return BluebirdPromise.reject(new exceptions.AccessDeniedError("No U2F registration document found."));
|
||||||
|
|
||||||
const appId: string = u2f_common.extract_app_id(req);
|
vars.logger.info(req, "Start authentication of app '%s'", appid);
|
||||||
vars.logger.info(req, "Start authentication of app '%s'", appId);
|
vars.logger.debug(req, "AppId = %s, keyHandle = %s", appid, JSON.stringify(doc.registration.keyHandle));
|
||||||
vars.logger.debug(req, "AppId = %s, keyHandle = %s", appId, JSON.stringify(doc.registration.keyHandle));
|
|
||||||
|
|
||||||
const request = vars.u2f.request(appId, doc.registration.keyHandle);
|
const request = vars.u2f.request(appid, doc.registration.keyHandle);
|
||||||
authSession.sign_request = request;
|
authSession.sign_request = request;
|
||||||
res.json(request);
|
res.json(request);
|
||||||
return BluebirdPromise.resolve();
|
return BluebirdPromise.resolve();
|
||||||
|
|
|
@ -88,7 +88,7 @@ describe("routes/verify/get", function () {
|
||||||
req.query.rd = 'https://login.example.com/';
|
req.query.rd = 'https://login.example.com/';
|
||||||
const mock = ImportMock.mockOther(GetBasicAuth, "default", () => Promise.reject(new NotAuthenticatedError('No!')));
|
const mock = ImportMock.mockOther(GetBasicAuth, "default", () => Promise.reject(new NotAuthenticatedError('No!')));
|
||||||
await Get(vars)(req, res as any);
|
await Get(vars)(req, res as any);
|
||||||
Assert(res.redirect.calledWith('https://login.example.com/'));
|
Assert(res.redirect.calledWith('https://login.example.com/?rd=https://secret.example.com/'));
|
||||||
mock.restore();
|
mock.restore();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { AuthenticationSessionHandler }
|
||||||
import { AuthenticationSession }
|
import { AuthenticationSession }
|
||||||
from "../../../../types/AuthenticationSession";
|
from "../../../../types/AuthenticationSession";
|
||||||
import HasHeader from "../..//utils/HasHeader";
|
import HasHeader from "../..//utils/HasHeader";
|
||||||
import { RequestUrlGetter } from "../../utils/RequestUrlGetter";
|
import RequestUrlGetter from "../../utils/RequestUrlGetter";
|
||||||
|
|
||||||
|
|
||||||
async function verifyWithSelectedMethod(req: Express.Request, res: Express.Response,
|
async function verifyWithSelectedMethod(req: Express.Request, res: Express.Response,
|
||||||
|
@ -24,42 +24,41 @@ async function verifyWithSelectedMethod(req: Express.Request, res: Express.Respo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* The Redirect header is used to set the target URL in the login portal.
|
|
||||||
*
|
|
||||||
* @param req The request to extract X-Original-Url from.
|
|
||||||
* @param res The response to write Redirect header to.
|
|
||||||
*/
|
|
||||||
function setRedirectHeader(req: Express.Request, res: Express.Response) {
|
|
||||||
const originalUrl = RequestUrlGetter.getOriginalUrl(req);
|
|
||||||
res.set(Constants.HEADER_REDIRECT, originalUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRedirectParam(req: Express.Request) {
|
function getRedirectParam(req: Express.Request) {
|
||||||
return req.query[Constants.REDIRECT_QUERY_PARAM] != "undefined"
|
return req.query[Constants.REDIRECT_QUERY_PARAM] != "undefined"
|
||||||
? req.query[Constants.REDIRECT_QUERY_PARAM]
|
? req.query[Constants.REDIRECT_QUERY_PARAM]
|
||||||
: undefined;
|
: undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function unsafeGet(vars: ServerVariables, req: Express.Request, res: Express.Response) {
|
||||||
|
const authSession = AuthenticationSessionHandler.get(req, vars.logger);
|
||||||
|
try {
|
||||||
|
await verifyWithSelectedMethod(req, res, vars, authSession);
|
||||||
|
res.status(204);
|
||||||
|
res.send();
|
||||||
|
} catch (err) {
|
||||||
|
// 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-Fowarded-* headers or X-Original-Url.
|
||||||
|
let redirectUrl = getRedirectParam(req);
|
||||||
|
const originalUrl = RequestUrlGetter.getOriginalUrl(req);
|
||||||
|
if (redirectUrl && originalUrl) {
|
||||||
|
redirectUrl = redirectUrl + `?${Constants.REDIRECT_QUERY_PARAM}=` + originalUrl;
|
||||||
|
ErrorReplies.redirectTo(redirectUrl, req, res, vars.logger)(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reply with an error.
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default function (vars: ServerVariables) {
|
export default function (vars: ServerVariables) {
|
||||||
return async function (req: Express.Request, res: Express.Response)
|
return async function (req: Express.Request, res: Express.Response)
|
||||||
: Promise<void> {
|
: Promise<void> {
|
||||||
const authSession = AuthenticationSessionHandler.get(req, vars.logger);
|
|
||||||
setRedirectHeader(req, res);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await verifyWithSelectedMethod(req, res, vars, authSession);
|
await unsafeGet(vars, req, res);
|
||||||
res.status(204);
|
|
||||||
res.send();
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// This redirect parameter is used in Kubernetes to annotate the ingress with
|
|
||||||
// the url to the authentication portal.
|
|
||||||
const redirectUrl = getRedirectParam(req);
|
|
||||||
if (redirectUrl) {
|
|
||||||
ErrorReplies.redirectTo(redirectUrl, req, res, vars.logger)(err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (err instanceof Exceptions.NotAuthorizedError) {
|
if (err instanceof Exceptions.NotAuthorizedError) {
|
||||||
ErrorReplies.replyWithError403(req, res, vars.logger)(err);
|
ErrorReplies.replyWithError403(req, res, vars.logger)(err);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -6,8 +6,7 @@ import GetHeader from "../../utils/GetHeader";
|
||||||
import { HEADER_PROXY_AUTHORIZATION } from "../../../../../shared/constants";
|
import { HEADER_PROXY_AUTHORIZATION } from "../../../../../shared/constants";
|
||||||
import setUserAndGroupsHeaders from "./SetUserAndGroupsHeaders";
|
import setUserAndGroupsHeaders from "./SetUserAndGroupsHeaders";
|
||||||
import CheckAuthorizations from "./CheckAuthorizations";
|
import CheckAuthorizations from "./CheckAuthorizations";
|
||||||
import { Level as AuthorizationLevel } from "../../authorization/Level";
|
import RequestUrlGetter from "../../utils/RequestUrlGetter";
|
||||||
import { RequestUrlGetter } from "../../utils/RequestUrlGetter";
|
|
||||||
|
|
||||||
export default async function(req: Express.Request, res: Express.Response,
|
export default async function(req: Express.Request, res: Express.Response,
|
||||||
vars: ServerVariables)
|
vars: ServerVariables)
|
||||||
|
|
|
@ -2,11 +2,11 @@ import Express = require("express");
|
||||||
import { ServerVariables } from "../../ServerVariables";
|
import { ServerVariables } from "../../ServerVariables";
|
||||||
import { AuthenticationSession }
|
import { AuthenticationSession }
|
||||||
from "../../../../types/AuthenticationSession";
|
from "../../../../types/AuthenticationSession";
|
||||||
import { URLDecomposer } from "../../utils/URLDecomposer";
|
|
||||||
import setUserAndGroupsHeaders from "./SetUserAndGroupsHeaders";
|
import setUserAndGroupsHeaders from "./SetUserAndGroupsHeaders";
|
||||||
import CheckAuthorizations from "./CheckAuthorizations";
|
import CheckAuthorizations from "./CheckAuthorizations";
|
||||||
import CheckInactivity from "./CheckInactivity";
|
import CheckInactivity from "./CheckInactivity";
|
||||||
import { RequestUrlGetter } from "../../utils/RequestUrlGetter";
|
import RequestUrlGetter from "../../utils/RequestUrlGetter";
|
||||||
|
import * as URLParse from "url-parse";
|
||||||
|
|
||||||
|
|
||||||
export default async function (req: Express.Request, res: Express.Response,
|
export default async function (req: Express.Request, res: Express.Response,
|
||||||
|
@ -22,15 +22,16 @@ export default async function (req: Express.Request, res: Express.Response,
|
||||||
throw new Error("Cannot detect the original URL from headers.");
|
throw new Error("Cannot detect the original URL from headers.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const d = URLDecomposer.fromUrl(originalUrl);
|
const url = new URLParse(originalUrl);
|
||||||
|
|
||||||
const username = authSession.userid;
|
const username = authSession.userid;
|
||||||
const groups = authSession.groups;
|
const groups = authSession.groups;
|
||||||
|
|
||||||
vars.logger.debug(req, "domain=%s, path=%s, user=%s, groups=%s, ip=%s", d.domain,
|
vars.logger.debug(req, "domain=%s, path=%s, user=%s, groups=%s, ip=%s", url.hostname,
|
||||||
d.path, (username) ? username : "unknown", (groups instanceof Array && groups.length > 0) ? groups.join(",") : "unknown", req.ip);
|
url.pathname, (username) ? username : "unknown",
|
||||||
|
(groups instanceof Array && groups.length > 0) ? groups.join(",") : "unknown", req.ip);
|
||||||
|
|
||||||
CheckAuthorizations(vars.authorizer, d.domain, d.path, username, groups, req.ip, authSession.authentication_level);
|
CheckAuthorizations(vars.authorizer, url.hostname, url.pathname, username, groups,
|
||||||
|
req.ip, authSession.authentication_level);
|
||||||
CheckInactivity(req, authSession, vars.config, vars.logger);
|
CheckInactivity(req, authSession, vars.config, vars.logger);
|
||||||
setUserAndGroupsHeaders(res, username, groups);
|
setUserAndGroupsHeaders(res, username, groups);
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ import { GET_VARIABLE_KEY } from "../../../../shared/constants";
|
||||||
*/
|
*/
|
||||||
export default function(req: Express.Request, header: string): string | undefined {
|
export default function(req: Express.Request, header: string): string | undefined {
|
||||||
const variables: ServerVariables = req.app.get(GET_VARIABLE_KEY);
|
const variables: ServerVariables = req.app.get(GET_VARIABLE_KEY);
|
||||||
if (!variables) return undefined;
|
if (!variables) throw new Error("There are no server variables set.");
|
||||||
|
|
||||||
const value = ObjectPath.get<Express.Request, string>(req, "headers." + header, undefined);
|
const value = ObjectPath.get<Express.Request, string>(req, "headers." + header, undefined);
|
||||||
variables.logger.debug(req, "Header %s is set to %s", header, value);
|
variables.logger.debug(req, "Header %s is set to %s", header, value);
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
import RequestUrlGetter from "./RequestUrlGetter";
|
||||||
|
import * as Assert from "assert";
|
||||||
|
import * as Sinon from "sinon";
|
||||||
|
import { RequestLoggerStub } from "../logging/RequestLoggerStub.spec";
|
||||||
|
|
||||||
|
describe('RequestUrlGetter', function() {
|
||||||
|
let req: any;
|
||||||
|
beforeEach(function() {
|
||||||
|
req = {
|
||||||
|
app: {
|
||||||
|
get: Sinon.stub().returns({
|
||||||
|
logger: new RequestLoggerStub()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
headers: {}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return the content of X-Original-Uri header", function() {
|
||||||
|
req.headers["x-original-url"] = "https://mytarget.example.com";
|
||||||
|
Assert.equal(RequestUrlGetter.getOriginalUrl(req), "https://mytarget.example.com");
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("Use X-Forwarded-Proto, X-Forwarded-Host and X-Forwarded-Uri headers", function() {
|
||||||
|
it("should get URL from Forwarded headers", function() {
|
||||||
|
req.headers["x-forwarded-proto"] = "https";
|
||||||
|
req.headers["x-forwarded-host"] = "mytarget.example.com";
|
||||||
|
req.headers["x-forwarded-uri"] = "/"
|
||||||
|
Assert.equal(RequestUrlGetter.getOriginalUrl(req), "https://mytarget.example.com/");
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should get URL from Forwarded headers without URI", function() {
|
||||||
|
req.headers["x-forwarded-proto"] = "https";
|
||||||
|
req.headers["x-forwarded-host"] = "mytarget.example.com";
|
||||||
|
Assert.equal(RequestUrlGetter.getOriginalUrl(req), "https://mytarget.example.com");
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw when no header is provided", function() {
|
||||||
|
Assert.throws(() => {
|
||||||
|
RequestUrlGetter.getOriginalUrl(req)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should throw when only some of X-Forwarded-* headers are provided", function() {
|
||||||
|
req.headers["x-forwarded-proto"] = "https";
|
||||||
|
Assert.throws(() => {
|
||||||
|
RequestUrlGetter.getOriginalUrl(req)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -3,23 +3,27 @@ import Express = require("express");
|
||||||
import GetHeader from "./GetHeader";
|
import GetHeader from "./GetHeader";
|
||||||
import HasHeader from "./HasHeader";
|
import HasHeader from "./HasHeader";
|
||||||
|
|
||||||
export class RequestUrlGetter {
|
export default class RequestUrlGetter {
|
||||||
static getOriginalUrl(req: Express.Request): string {
|
static getOriginalUrl(req: Express.Request): string {
|
||||||
|
|
||||||
if (HasHeader(req, Constants.HEADER_X_ORIGINAL_URL)) {
|
if (HasHeader(req, Constants.HEADER_X_ORIGINAL_URL)) {
|
||||||
return GetHeader(req, Constants.HEADER_X_ORIGINAL_URL);
|
return GetHeader(req, Constants.HEADER_X_ORIGINAL_URL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// X-Forwarded-Port is not mandatory since the port is included in X-Forwarded-Host
|
||||||
|
// at least in nginx and Traefik.
|
||||||
const proto = GetHeader(req, Constants.HEADER_X_FORWARDED_PROTO);
|
const proto = GetHeader(req, Constants.HEADER_X_FORWARDED_PROTO);
|
||||||
const host = GetHeader(req, Constants.HEADER_X_FORWARDED_HOST);
|
const host = GetHeader(req, Constants.HEADER_X_FORWARDED_HOST);
|
||||||
const port = GetHeader(req, Constants.HEADER_X_FORWARDED_PORT);
|
|
||||||
const uri = GetHeader(req, Constants.HEADER_X_FORWARDED_URI);
|
const uri = GetHeader(req, Constants.HEADER_X_FORWARDED_URI);
|
||||||
|
|
||||||
if (!proto || !host || !port) {
|
if (!proto || !host) {
|
||||||
throw new Error("Missing headers holding requested URL. Requires X-Original-Url or X-Forwarded-Proto, X-Forwarded-Host, and X-Forwarded-Port")
|
throw new Error("Missing headers holding requested URL. Requires either X-Original-Url or X-Forwarded-Proto, X-Forwarded-Host and X-Forwarded-Uri.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return "${proto}://${host}:${port}${uri}";
|
if (!uri) {
|
||||||
|
return `${proto}://${host}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${proto}://${host}${uri}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,6 @@ export const HEADER_X_TARGET_URL = "x-target-url";
|
||||||
export const HEADER_X_ORIGINAL_URL = "x-original-url";
|
export const HEADER_X_ORIGINAL_URL = "x-original-url";
|
||||||
export const HEADER_X_FORWARDED_PROTO = "x-forwarded-proto";
|
export const HEADER_X_FORWARDED_PROTO = "x-forwarded-proto";
|
||||||
export const HEADER_X_FORWARDED_HOST = "x-forwarded-host";
|
export const HEADER_X_FORWARDED_HOST = "x-forwarded-host";
|
||||||
export const HEADER_X_FORWARDED_PORT = "x-forwarded-port";
|
|
||||||
export const HEADER_X_FORWARDED_URI = "x-forwarded-uri";
|
export const HEADER_X_FORWARDED_URI = "x-forwarded-uri";
|
||||||
export const HEADER_PROXY_AUTHORIZATION = "proxy-authorization";
|
export const HEADER_PROXY_AUTHORIZATION = "proxy-authorization";
|
||||||
export const HEADER_REDIRECT = "redirect";
|
export const HEADER_REDIRECT = "redirect";
|
||||||
|
|
|
@ -5,11 +5,12 @@ import { StatusCodeError } from 'request-promise/errors';
|
||||||
|
|
||||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
||||||
|
|
||||||
export async function GET_ExpectError(url: string, statusCode: number) {
|
export async function GET_ExpectError(url: string, headers: {[key: string]: string}, statusCode: number) {
|
||||||
try {
|
try {
|
||||||
await Request.get(url, {
|
await Request.get(url, {
|
||||||
json: true,
|
json: true,
|
||||||
rejectUnauthorized: false,
|
rejectUnauthorized: false,
|
||||||
|
headers: headers,
|
||||||
});
|
});
|
||||||
throw new Error('No response');
|
throw new Error('No response');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -22,12 +23,12 @@ export async function GET_ExpectError(url: string, statusCode: number) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sent a GET request to the url and expect a 401
|
// Sent a GET request to the url and expect a 401
|
||||||
export async function GET_Expect401(url: string) {
|
export async function GET_Expect401(url: string, headers: {[key: string]: string} = {}) {
|
||||||
return await GET_ExpectError(url, 401);
|
return await GET_ExpectError(url, headers, 401);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function GET_Expect502(url: string) {
|
export async function GET_Expect502(url: string, headers: {[key: string]: string} = {}) {
|
||||||
return await GET_ExpectError(url, 502);
|
return await GET_ExpectError(url, headers, 502);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function POST_Expect401(url: string, body?: any) {
|
export async function POST_Expect401(url: string, body?: any) {
|
||||||
|
@ -47,8 +48,8 @@ export async function POST_Expect401(url: string, body?: any) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function GET_ExpectRedirect(url: string, redirectionUrl: string) {
|
export async function GET_ExpectRedirect(url: string, redirectionUrl: string, headers: {[key: string]: string} = {}) {
|
||||||
const response = await Fetch(url, {redirect: 'manual'});
|
const response = await Fetch(url, {redirect: 'manual', headers: headers});
|
||||||
|
|
||||||
if (response.status == 302) {
|
if (response.status == 302) {
|
||||||
const body = await response.text();
|
const body = await response.text();
|
||||||
|
|
|
@ -6,6 +6,8 @@ import CustomHeadersForwarded from "./scenarii/CustomHeadersForwarded";
|
||||||
|
|
||||||
process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = 0 as any;
|
process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = 0 as any;
|
||||||
|
|
||||||
|
process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = 0 as any;
|
||||||
|
|
||||||
AutheliaSuite(__dirname, function() {
|
AutheliaSuite(__dirname, function() {
|
||||||
this.timeout(10000);
|
this.timeout(10000);
|
||||||
|
|
||||||
|
|
|
@ -2,14 +2,32 @@ import { GET_Expect401, GET_ExpectRedirect } from "../../../helpers/utils/Reques
|
||||||
|
|
||||||
export default function() {
|
export default function() {
|
||||||
describe('Query without authenticated cookie', function() {
|
describe('Query without authenticated cookie', function() {
|
||||||
it('should get a 401 on GET to https://login.example.com:8080/api/verify', async function() {
|
it('should get a 401 on GET to http://192.168.240.1:9091/api/verify', async function() {
|
||||||
await GET_Expect401('https://login.example.com:8080/api/verify');
|
await GET_Expect401('http://192.168.240.1:9091/api/verify', {
|
||||||
|
'X-Forwarded-Proto': 'https',
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Parameter `rd` required by Kubernetes ingress controller', async function() {
|
describe('Kubernetes nginx ingress controller', async function() {
|
||||||
it('should redirect to https://login.example.com:8080', async function() {
|
it('should redirect to https://login.example.com:8080', async function() {
|
||||||
await GET_ExpectRedirect('https://login.example.com:8080/api/verify?rd=https://login.example.com:8080',
|
await GET_ExpectRedirect('http://192.168.240.1:9091/api/verify?rd=https://login.example.com:8080/%23/',
|
||||||
'https://login.example.com:8080');
|
'https://login.example.com:8080/#/?rd=https://secure.example.com:8080',
|
||||||
|
{
|
||||||
|
'X-Original-Url': 'https://secure.example.com:8080',
|
||||||
|
'X-Forwarded-Proto': 'https'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Traefik proxy', async function() {
|
||||||
|
it('should redirect to https://login.example.com:8080', async function() {
|
||||||
|
await GET_ExpectRedirect('http://192.168.240.1:9091/api/verify?rd=https://login.example.com:8080/%23/',
|
||||||
|
'https://login.example.com:8080/#/?rd=https://secure.example.com:8080/',
|
||||||
|
{
|
||||||
|
'X-Forwarded-Proto': 'https',
|
||||||
|
'X-Forwarded-Host': 'secure.example.com:8080',
|
||||||
|
'X-Forwarded-Uri': '/',
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
# Traefik suite
|
||||||
|
|
||||||
|
This suite has been created to test Authelia against Traefik.
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
Authelia, Traefik, fake webmail for registering devices.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
Authentication tests.
|
|
@ -0,0 +1,43 @@
|
||||||
|
###############################################################
|
||||||
|
# Authelia minimal configuration #
|
||||||
|
###############################################################
|
||||||
|
|
||||||
|
port: 9091
|
||||||
|
|
||||||
|
logs_level: debug
|
||||||
|
|
||||||
|
authentication_backend:
|
||||||
|
file:
|
||||||
|
path: ./test/suites/basic/users_database.test.yml
|
||||||
|
|
||||||
|
session:
|
||||||
|
secret: unsecure_session_secret
|
||||||
|
domain: example.com
|
||||||
|
expiration: 3600000 # 1 hour
|
||||||
|
inactivity: 300000 # 5 minutes
|
||||||
|
|
||||||
|
storage:
|
||||||
|
local:
|
||||||
|
path: /tmp/authelia/db
|
||||||
|
|
||||||
|
access_control:
|
||||||
|
default_policy: bypass
|
||||||
|
rules:
|
||||||
|
- domain: 'public.example.com'
|
||||||
|
policy: bypass
|
||||||
|
- domain: 'admin.example.com'
|
||||||
|
policy: two_factor
|
||||||
|
- domain: 'secure.example.com'
|
||||||
|
policy: two_factor
|
||||||
|
- domain: 'singlefactor.example.com'
|
||||||
|
policy: one_factor
|
||||||
|
|
||||||
|
notifier:
|
||||||
|
smtp:
|
||||||
|
username: test
|
||||||
|
password: password
|
||||||
|
secure: false
|
||||||
|
host: 127.0.0.1
|
||||||
|
port: 1025
|
||||||
|
sender: admin@example.com
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { exec } from "../../helpers/utils/exec";
|
||||||
|
import AutheliaServer from "../../helpers/context/AutheliaServer";
|
||||||
|
import DockerEnvironment from "../../helpers/context/DockerEnvironment";
|
||||||
|
import * as fs from "fs";
|
||||||
|
|
||||||
|
const autheliaServer = new AutheliaServer(__dirname + '/config.yml');
|
||||||
|
const dockerEnv = new DockerEnvironment([
|
||||||
|
'docker-compose.yml',
|
||||||
|
'example/compose/nginx/backend/docker-compose.yml',
|
||||||
|
'example/compose/traefik/docker-compose.yml',
|
||||||
|
'example/compose/smtp/docker-compose.yml',
|
||||||
|
])
|
||||||
|
|
||||||
|
async function setup() {
|
||||||
|
await exec('./example/compose/traefik/render.js ' + (fs.existsSync('.suite') ? '': '--production'));
|
||||||
|
await exec(`cp ${__dirname}/users_database.yml ${__dirname}/users_database.test.yml`);
|
||||||
|
await exec('mkdir -p /tmp/authelia/db');
|
||||||
|
await dockerEnv.start();
|
||||||
|
await autheliaServer.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function teardown() {
|
||||||
|
await autheliaServer.stop();
|
||||||
|
await dockerEnv.stop();
|
||||||
|
await exec('rm -rf /tmp/authelia/db');
|
||||||
|
}
|
||||||
|
|
||||||
|
const setup_timeout = 30000;
|
||||||
|
const teardown_timeout = 30000;
|
||||||
|
|
||||||
|
export {
|
||||||
|
setup,
|
||||||
|
setup_timeout,
|
||||||
|
teardown,
|
||||||
|
teardown_timeout
|
||||||
|
};
|
|
@ -0,0 +1,17 @@
|
||||||
|
import AutheliaSuite from "../../helpers/context/AutheliaSuite";
|
||||||
|
import { exec } from '../../helpers/utils/exec';
|
||||||
|
import TwoFactorAuthentication from "../../helpers/scenarii/TwoFactorAuthentication";
|
||||||
|
import SingleFactorAuthentication from "../../helpers/scenarii/SingleFactorAuthentication";
|
||||||
|
import * as fs from "fs";
|
||||||
|
|
||||||
|
AutheliaSuite(__dirname, function() {
|
||||||
|
this.timeout(10000);
|
||||||
|
|
||||||
|
beforeEach(async function() {
|
||||||
|
await exec('./example/compose/traefik/render.js ' + (fs.existsSync('.suite') ? '': '--production'));
|
||||||
|
await exec(`cp ${__dirname}/users_database.yml ${__dirname}/users_database.test.yml`);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Single-factor authentication', SingleFactorAuthentication());
|
||||||
|
describe('Second factor authentication', TwoFactorAuthentication());
|
||||||
|
});
|
|
@ -15,7 +15,7 @@ users:
|
||||||
|
|
||||||
harry:
|
harry:
|
||||||
password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
|
password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
|
||||||
emails: harry.potter@authelia.com
|
email: harry.potter@authelia.com
|
||||||
groups: []
|
groups: []
|
||||||
|
|
||||||
bob:
|
bob:
|
Loading…
Reference in New Issue