[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 { RootState } from '../../../reducers';
|
||||
import { Dispatch } from 'redux';
|
||||
import * as U2fApi from "u2f-api";
|
||||
import U2fApi from "u2f-api";
|
||||
import { Props } from '../../../views/SecurityKeyRegistrationView/SecurityKeyRegistrationView';
|
||||
import { registerSecurityKey, registerSecurityKeyFailure, registerSecurityKeySuccess } from '../../../reducers/Portal/SecurityKeyRegistration/actions';
|
||||
import AutheliaService from '../../../services/AutheliaService';
|
||||
|
@ -12,13 +12,8 @@ const mapStateToProps = (state: RootState) => ({
|
|||
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) {
|
||||
console.error(err);
|
||||
dispatch(registerSecurityKeyFailure(err.message));
|
||||
}
|
||||
|
||||
|
@ -27,9 +22,9 @@ const mapDispatchToProps = (dispatch: Dispatch, ownProps: Props) => {
|
|||
onInit: async (token: string) => {
|
||||
try {
|
||||
dispatch(registerSecurityKey());
|
||||
await checkIdentity(token);
|
||||
const requestRegister = await AutheliaService.requestSecurityKeyRegistration();
|
||||
const registerResponse = await U2fApi.register(requestRegister, [], 60);
|
||||
await AutheliaService.completeSecurityKeyRegistrationIdentityValidation(token);
|
||||
const registerRequest = await AutheliaService.requestSecurityKeyRegistration();
|
||||
const registerResponse = await U2fApi.register([registerRequest], [], 60);
|
||||
await AutheliaService.completeSecurityKeyRegistration(registerResponse);
|
||||
dispatch(registerSecurityKeySuccess());
|
||||
setTimeout(() => {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import RemoteState from "../views/AuthenticationView/RemoteState";
|
||||
import u2fApi, { SignRequest } from "u2f-api";
|
||||
import U2fApi, { SignRequest } from "u2f-api";
|
||||
import Method2FA from "../types/Method2FA";
|
||||
import RedirectResponse from "./RedirectResponse";
|
||||
import PreferedMethodResponse from "./PreferedMethodResponse";
|
||||
|
@ -74,15 +74,11 @@ class AutheliaService {
|
|||
}
|
||||
|
||||
static async requestSigning() {
|
||||
return this.fetchSafe('/api/u2f/sign_request')
|
||||
.then(async (res) => {
|
||||
const body = await res.json();
|
||||
return body as SignRequest;
|
||||
});
|
||||
return this.fetchSafeJson<SignRequest>('/api/u2f/sign_request');
|
||||
}
|
||||
|
||||
static async completeSecurityKeySigning(
|
||||
response: u2fApi.SignResponse, redirectionUrl: string | null) {
|
||||
response: U2fApi.SignResponse, redirectionUrl: string | null) {
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Accept': 'application/json',
|
||||
|
@ -199,7 +195,7 @@ class AutheliaService {
|
|||
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', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
|
@ -211,7 +207,18 @@ class AutheliaService {
|
|||
}
|
||||
|
||||
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'
|
||||
# services: {}
|
||||
networks:
|
||||
authelianet:
|
||||
driver: bridge
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
version: '2'
|
||||
networks:
|
||||
authelianet:
|
||||
external: true
|
|
@ -2,5 +2,9 @@ version: '2'
|
|||
services:
|
||||
nginx-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:
|
||||
- authelianet
|
||||
|
|
|
@ -35,10 +35,15 @@ http {
|
|||
|
||||
# Serve the backend API for the portal.
|
||||
location /api {
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Original-URI $request_uri;
|
||||
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;
|
||||
|
||||
# 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
|
||||
# and allows Authelia to use it to match the network-based ACLs.
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
@ -65,11 +70,15 @@ http {
|
|||
|
||||
# Serve the backend API for the portal.
|
||||
location /api {
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Original-URI $request_uri;
|
||||
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-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
|
||||
# and allows Authelia to use it to match the network-based ACLs.
|
||||
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.
|
||||
location / {
|
||||
auth_request /auth_verify;
|
||||
|
||||
auth_request_set $redirect $upstream_http_redirect;
|
||||
|
||||
auth_request_set $user $upstream_http_remote_user;
|
||||
proxy_set_header X-Forwarded-User $user;
|
||||
auth_request_set $user $upstream_http_remote_user;
|
||||
proxy_set_header X-Forwarded-User $user;
|
||||
|
||||
auth_request_set $groups $upstream_http_remote_groups;
|
||||
proxy_set_header Remote-Groups $groups;
|
||||
auth_request_set $groups $upstream_http_remote_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.
|
||||
# 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_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.
|
||||
location /auth_verify {
|
||||
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;
|
||||
|
||||
# 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-Host $http_host;
|
||||
proxy_set_header X-Forwarded-Uri $request_uri;
|
||||
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
||||
# Authelia can receive Proxy-Authorization to authenticate however most of the clients
|
||||
|
@ -176,24 +199,23 @@ http {
|
|||
proxy_pass_request_body off;
|
||||
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.
|
||||
location /headers {
|
||||
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;
|
||||
proxy_set_header Custom-Forwarded-Groups $groups;
|
||||
auth_request_set $groups $upstream_http_remote_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
|
||||
ports:
|
||||
- "1025:1025"
|
||||
labels:
|
||||
- traefik.frontend.rule=Host:mail.example.com
|
||||
- traefik.port=1080
|
||||
networks:
|
||||
- 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, 'duo.example.com', '192.168.240.100');
|
||||
|
||||
// For Traefik suite.
|
||||
await checkAndFixEntry(actualEntries, 'traefik.example.com', '192.168.240.100');
|
||||
|
||||
// For testing network ACLs.
|
||||
await checkAndFixEntry(actualEntries, 'proxy-client1.example.com', '192.168.240.201');
|
||||
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 { ServerVariables } from "./ServerVariables";
|
||||
import { IdentityValidable } from "./IdentityValidable";
|
||||
import * as Constants from "../../../shared/constants";
|
||||
|
||||
import Identity = require("../../types/Identity");
|
||||
import { IdentityValidationDocument }
|
||||
from "./storage/IdentityValidationDocument";
|
||||
import { OPERATION_FAILED } from "../../../shared/UserMessages";
|
||||
import GetHeader from "./utils/GetHeader";
|
||||
|
||||
function createAndSaveToken(userid: string, challenge: string,
|
||||
userDataStore: IUserDataStore): BluebirdPromise<string> {
|
||||
|
@ -111,8 +113,9 @@ export function post_start_validation(handler: IdentityValidable,
|
|||
vars.userDataStore);
|
||||
})
|
||||
.then((token: string) => {
|
||||
const host = req.get("Host");
|
||||
const link_url = util.format("https://%s/#%s?token=%s", host,
|
||||
const scheme = GetHeader(req, Constants.HEADER_X_FORWARDED_PROTO);
|
||||
const host = GetHeader(req, Constants.HEADER_X_FORWARDED_HOST);
|
||||
const link_url = util.format("%s://%s/#%s?token=%s", scheme, host,
|
||||
handler.destinationPath(), token);
|
||||
vars.logger.info(req, "Notification sent to user \"%s\"",
|
||||
identity.userid);
|
||||
|
|
|
@ -63,8 +63,7 @@ export default class RegistrationHandler implements IdentityValidable {
|
|||
}
|
||||
|
||||
preValidationResponse(req: Express.Request, res: Express.Response) {
|
||||
res.status(204);
|
||||
res.send();
|
||||
res.json({message: "OK"});
|
||||
}
|
||||
|
||||
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) {
|
||||
res.status(204);
|
||||
res.send();
|
||||
res.json({message: "OK"});
|
||||
}
|
||||
|
||||
postValidationInit(req: express.Request) {
|
||||
|
@ -60,8 +59,7 @@ export default class RegistrationHandler implements IdentityValidable {
|
|||
}
|
||||
|
||||
postValidationResponse(req: express.Request, res: express.Response) {
|
||||
res.status(204);
|
||||
res.send();
|
||||
res.json({message: "OK"});
|
||||
}
|
||||
|
||||
mailSubject(): string {
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import objectPath = require("object-path");
|
||||
import u2f_common = require("../U2FCommon");
|
||||
import BluebirdPromise = require("bluebird");
|
||||
import express = require("express");
|
||||
import U2f = require("u2f");
|
||||
|
@ -10,12 +9,16 @@ import { ServerVariables } from "../../../../ServerVariables";
|
|||
import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler";
|
||||
import UserMessages = require("../../../../../../../shared/UserMessages");
|
||||
import { AuthenticationSession } from "../../../../../../types/AuthenticationSession";
|
||||
import GetHeader from "../../../../utils/GetHeader";
|
||||
import * as Constants from "../../../../../../../shared/constants";
|
||||
|
||||
|
||||
export default function (vars: ServerVariables) {
|
||||
function handler(req: express.Request, res: express.Response): BluebirdPromise<void> {
|
||||
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;
|
||||
|
||||
return new BluebirdPromise(function (resolve, reject) {
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
|
||||
import u2f_common = require("../U2FCommon");
|
||||
import BluebirdPromise = require("bluebird");
|
||||
import express = require("express");
|
||||
import U2f = require("u2f");
|
||||
|
@ -8,11 +7,15 @@ import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionH
|
|||
import { AuthenticationSession } from "../../../../../../types/AuthenticationSession";
|
||||
import UserMessages = require("../../../../../../../shared/UserMessages");
|
||||
import { ServerVariables } from "../../../../ServerVariables";
|
||||
import GetHeader from "../../../../utils/GetHeader";
|
||||
import * as Constants from "../../../../../../../shared/constants";
|
||||
|
||||
export default function (vars: ServerVariables) {
|
||||
function handler(req: express.Request, res: express.Response): BluebirdPromise<void> {
|
||||
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) {
|
||||
authSession = AuthenticationSessionHandler.get(req, vars.logger);
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import objectPath = require("object-path");
|
||||
import u2f_common = require("../U2FCommon");
|
||||
import BluebirdPromise = require("bluebird");
|
||||
import express = require("express");
|
||||
import { U2FRegistrationDocument } from "../../../../storage/U2FRegistrationDocument";
|
||||
|
@ -11,11 +10,15 @@ import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionH
|
|||
import UserMessages = require("../../../../../../../shared/UserMessages");
|
||||
import { AuthenticationSession } from "../../../../../../types/AuthenticationSession";
|
||||
import { Level } from "../../../../authentication/Level";
|
||||
import GetHeader from "../../../../utils/GetHeader";
|
||||
import * as Constants from "../../../../../../../shared/constants";
|
||||
|
||||
export default function (vars: ServerVariables) {
|
||||
function handler(req: express.Request, res: express.Response): BluebirdPromise<void> {
|
||||
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) {
|
||||
authSession = AuthenticationSessionHandler.get(req, vars.logger);
|
||||
|
@ -28,7 +31,7 @@ export default function (vars: ServerVariables) {
|
|||
})
|
||||
.then(function () {
|
||||
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> {
|
||||
const signRequest = authSession.sign_request;
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
|
||||
import u2f_common = require("../../../secondfactor/u2f/U2FCommon");
|
||||
import BluebirdPromise = require("bluebird");
|
||||
import express = require("express");
|
||||
import { U2FRegistrationDocument } from "../../../../storage/U2FRegistrationDocument";
|
||||
|
@ -9,28 +7,31 @@ import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionH
|
|||
import UserMessages = require("../../../../../../../shared/UserMessages");
|
||||
import { ServerVariables } from "../../../../ServerVariables";
|
||||
import { AuthenticationSession } from "../../../../../../types/AuthenticationSession";
|
||||
import GetHeader from "../../../../utils/GetHeader";
|
||||
import * as Constants from "../../../../../../../shared/constants";
|
||||
|
||||
export default function (vars: ServerVariables) {
|
||||
function handler(req: express.Request, res: express.Response): BluebirdPromise<void> {
|
||||
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) {
|
||||
authSession = AuthenticationSessionHandler.get(req, vars.logger);
|
||||
resolve();
|
||||
})
|
||||
.then(function () {
|
||||
return vars.userDataStore.retrieveU2FRegistration(authSession.userid, appId);
|
||||
return vars.userDataStore.retrieveU2FRegistration(authSession.userid, appid);
|
||||
})
|
||||
.then(function (doc: U2FRegistrationDocument): BluebirdPromise<void> {
|
||||
if (!doc)
|
||||
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.debug(req, "AppId = %s, keyHandle = %s", appId, JSON.stringify(doc.registration.keyHandle));
|
||||
vars.logger.info(req, "Start authentication of app '%s'", appid);
|
||||
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;
|
||||
res.json(request);
|
||||
return BluebirdPromise.resolve();
|
||||
|
|
|
@ -88,7 +88,7 @@ describe("routes/verify/get", function () {
|
|||
req.query.rd = 'https://login.example.com/';
|
||||
const mock = ImportMock.mockOther(GetBasicAuth, "default", () => Promise.reject(new NotAuthenticatedError('No!')));
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -10,7 +10,7 @@ import { AuthenticationSessionHandler }
|
|||
import { AuthenticationSession }
|
||||
from "../../../../types/AuthenticationSession";
|
||||
import HasHeader from "../..//utils/HasHeader";
|
||||
import { RequestUrlGetter } from "../../utils/RequestUrlGetter";
|
||||
import RequestUrlGetter from "../../utils/RequestUrlGetter";
|
||||
|
||||
|
||||
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) {
|
||||
return req.query[Constants.REDIRECT_QUERY_PARAM] != "undefined"
|
||||
? req.query[Constants.REDIRECT_QUERY_PARAM]
|
||||
: 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) {
|
||||
return async function (req: Express.Request, res: Express.Response)
|
||||
: Promise<void> {
|
||||
const authSession = AuthenticationSessionHandler.get(req, vars.logger);
|
||||
setRedirectHeader(req, res);
|
||||
|
||||
try {
|
||||
await verifyWithSelectedMethod(req, res, vars, authSession);
|
||||
res.status(204);
|
||||
res.send();
|
||||
await unsafeGet(vars, req, res);
|
||||
} 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) {
|
||||
ErrorReplies.replyWithError403(req, res, vars.logger)(err);
|
||||
} else {
|
||||
|
|
|
@ -6,8 +6,7 @@ import GetHeader from "../../utils/GetHeader";
|
|||
import { HEADER_PROXY_AUTHORIZATION } from "../../../../../shared/constants";
|
||||
import setUserAndGroupsHeaders from "./SetUserAndGroupsHeaders";
|
||||
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,
|
||||
vars: ServerVariables)
|
||||
|
|
|
@ -2,11 +2,11 @@ import Express = require("express");
|
|||
import { ServerVariables } from "../../ServerVariables";
|
||||
import { AuthenticationSession }
|
||||
from "../../../../types/AuthenticationSession";
|
||||
import { URLDecomposer } from "../../utils/URLDecomposer";
|
||||
import setUserAndGroupsHeaders from "./SetUserAndGroupsHeaders";
|
||||
import CheckAuthorizations from "./CheckAuthorizations";
|
||||
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,
|
||||
|
@ -22,15 +22,16 @@ export default async function (req: Express.Request, res: Express.Response,
|
|||
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 groups = authSession.groups;
|
||||
|
||||
vars.logger.debug(req, "domain=%s, path=%s, user=%s, groups=%s, ip=%s", d.domain,
|
||||
d.path, (username) ? username : "unknown", (groups instanceof Array && groups.length > 0) ? groups.join(",") : "unknown", req.ip);
|
||||
vars.logger.debug(req, "domain=%s, path=%s, user=%s, groups=%s, ip=%s", url.hostname,
|
||||
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);
|
||||
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 {
|
||||
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);
|
||||
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 HasHeader from "./HasHeader";
|
||||
|
||||
export class RequestUrlGetter {
|
||||
export default class RequestUrlGetter {
|
||||
static getOriginalUrl(req: Express.Request): string {
|
||||
|
||||
if (HasHeader(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 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);
|
||||
|
||||
if (!proto || !host || !port) {
|
||||
throw new Error("Missing headers holding requested URL. Requires X-Original-Url or X-Forwarded-Proto, X-Forwarded-Host, and X-Forwarded-Port")
|
||||
if (!proto || !host) {
|
||||
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_FORWARDED_PROTO = "x-forwarded-proto";
|
||||
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_PROXY_AUTHORIZATION = "proxy-authorization";
|
||||
export const HEADER_REDIRECT = "redirect";
|
||||
|
|
|
@ -5,11 +5,12 @@ import { StatusCodeError } from 'request-promise/errors';
|
|||
|
||||
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 {
|
||||
await Request.get(url, {
|
||||
json: true,
|
||||
rejectUnauthorized: false,
|
||||
headers: headers,
|
||||
});
|
||||
throw new Error('No response');
|
||||
} 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
|
||||
export async function GET_Expect401(url: string) {
|
||||
return await GET_ExpectError(url, 401);
|
||||
export async function GET_Expect401(url: string, headers: {[key: string]: string} = {}) {
|
||||
return await GET_ExpectError(url, headers, 401);
|
||||
}
|
||||
|
||||
export async function GET_Expect502(url: string) {
|
||||
return await GET_ExpectError(url, 502);
|
||||
export async function GET_Expect502(url: string, headers: {[key: string]: string} = {}) {
|
||||
return await GET_ExpectError(url, headers, 502);
|
||||
}
|
||||
|
||||
export async function POST_Expect401(url: string, body?: any) {
|
||||
|
@ -47,8 +48,8 @@ export async function POST_Expect401(url: string, body?: any) {
|
|||
return;
|
||||
}
|
||||
|
||||
export async function GET_ExpectRedirect(url: string, redirectionUrl: string) {
|
||||
const response = await Fetch(url, {redirect: 'manual'});
|
||||
export async function GET_ExpectRedirect(url: string, redirectionUrl: string, headers: {[key: string]: string} = {}) {
|
||||
const response = await Fetch(url, {redirect: 'manual', headers: headers});
|
||||
|
||||
if (response.status == 302) {
|
||||
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;
|
||||
|
||||
AutheliaSuite(__dirname, function() {
|
||||
this.timeout(10000);
|
||||
|
||||
|
|
|
@ -2,14 +2,32 @@ import { GET_Expect401, GET_ExpectRedirect } from "../../../helpers/utils/Reques
|
|||
|
||||
export default function() {
|
||||
describe('Query without authenticated cookie', function() {
|
||||
it('should get a 401 on GET to https://login.example.com:8080/api/verify', async function() {
|
||||
await GET_Expect401('https://login.example.com:8080/api/verify');
|
||||
it('should get a 401 on GET to http://192.168.240.1:9091/api/verify', async function() {
|
||||
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() {
|
||||
await GET_ExpectRedirect('https://login.example.com:8080/api/verify?rd=https://login.example.com:8080',
|
||||
'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/#/?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:
|
||||
password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
|
||||
emails: harry.potter@authelia.com
|
||||
email: harry.potter@authelia.com
|
||||
groups: []
|
||||
|
||||
bob:
|
Loading…
Reference in New Issue