Merge pull request #142 from clems4ever/test-forward-headers

Add test for headers forwarding feature
This commit is contained in:
Clément Michaud 2017-10-15 01:13:57 +02:00 committed by GitHub
commit cb139997d2
19 changed files with 228 additions and 45 deletions

View File

@ -0,0 +1,6 @@
version: '2'
services:
httpbin:
image: citizenstig/httpbin
networks:
- example-network

View File

@ -74,6 +74,7 @@ http {
proxy_set_header X-Original-URI $request_uri; proxy_set_header X-Original-URI $request_uri;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $http_host; proxy_set_header Host $http_host;
proxy_set_header Content-Length "";
proxy_pass http://authelia/verify; proxy_pass http://authelia/verify;
} }
@ -82,7 +83,6 @@ http {
auth_request /auth_verify; auth_request /auth_verify;
auth_request_set $redirect $upstream_http_redirect; auth_request_set $redirect $upstream_http_redirect;
proxy_set_header Redirect $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;
@ -93,6 +93,23 @@ http {
error_page 401 =302 https://auth.test.local:8080?redirect=$redirect; error_page 401 =302 https://auth.test.local:8080?redirect=$redirect;
error_page 403 = https://auth.test.local:8080/error/403; error_page 403 = https://auth.test.local:8080/error/403;
} }
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 $groups $upstream_http_remote_groups;
proxy_set_header Custom-Forwarded-Groups $groups;
proxy_pass http://httpbin:8000/headers;
error_page 401 =302 https://auth.test.local:8080?redirect=$redirect;
error_page 403 = https://auth.test.local:8080/error/403;
}
} }
server { server {
@ -110,6 +127,7 @@ http {
proxy_set_header X-Original-URI $request_uri; proxy_set_header X-Original-URI $request_uri;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $http_host; proxy_set_header Host $http_host;
proxy_set_header Content-Length "";
proxy_pass http://authelia/verify; proxy_pass http://authelia/verify;
} }
@ -118,7 +136,6 @@ http {
auth_request /auth_verify; auth_request /auth_verify;
auth_request_set $redirect $upstream_http_redirect; auth_request_set $redirect $upstream_http_redirect;
proxy_set_header Redirect $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;
@ -146,6 +163,7 @@ http {
proxy_set_header X-Original-URI $request_uri; proxy_set_header X-Original-URI $request_uri;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $http_host; proxy_set_header Host $http_host;
proxy_set_header Content-Length "";
proxy_pass http://authelia/verify; proxy_pass http://authelia/verify;
} }
@ -154,7 +172,6 @@ http {
auth_request /auth_verify; auth_request /auth_verify;
auth_request_set $redirect $upstream_http_redirect; auth_request_set $redirect $upstream_http_redirect;
proxy_set_header Redirect $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;
@ -191,7 +208,6 @@ http {
auth_request /auth_verify; auth_request /auth_verify;
auth_request_set $redirect $upstream_http_redirect; auth_request_set $redirect $upstream_http_redirect;
proxy_set_header Redirect $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;
@ -228,7 +244,6 @@ http {
auth_request /auth_verify; auth_request /auth_verify;
auth_request_set $redirect $upstream_http_redirect; auth_request_set $redirect $upstream_http_redirect;
proxy_set_header Redirect $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;

View File

@ -71,7 +71,6 @@
"@types/request-promise": "^4.1.38", "@types/request-promise": "^4.1.38",
"@types/selenium-webdriver": "^3.0.4", "@types/selenium-webdriver": "^3.0.4",
"@types/sinon": "^2.2.1", "@types/sinon": "^2.2.1",
"@types/speakeasy": "^2.0.1",
"@types/tmp": "0.0.33", "@types/tmp": "0.0.33",
"@types/winston": "^2.3.2", "@types/winston": "^2.3.2",
"@types/yamljs": "^0.2.30", "@types/yamljs": "^0.2.30",

View File

@ -10,5 +10,6 @@ docker-compose \
-f example/redis/docker-compose.yml \ -f example/redis/docker-compose.yml \
-f example/nginx/docker-compose.yml \ -f example/nginx/docker-compose.yml \
-f example/smtp/docker-compose.yml \ -f example/smtp/docker-compose.yml \
-f example/httpbin/docker-compose.yml \
-f example/ldap/docker-compose.admin.yml \ -f example/ldap/docker-compose.admin.yml \
-f example/ldap/docker-compose.yml $* -f example/ldap/docker-compose.yml $*

View File

@ -9,4 +9,5 @@ docker-compose \
-f example/redis/docker-compose.yml \ -f example/redis/docker-compose.yml \
-f example/nginx/docker-compose.yml \ -f example/nginx/docker-compose.yml \
-f example/smtp/docker-compose.yml \ -f example/smtp/docker-compose.yml \
-f example/httpbin/docker-compose.yml \
-f example/ldap/docker-compose.yml $* -f example/ldap/docker-compose.yml $*

View File

@ -3,4 +3,4 @@
DC_SCRIPT=./scripts/example-commit/dc-example.sh DC_SCRIPT=./scripts/example-commit/dc-example.sh
$DC_SCRIPT build $DC_SCRIPT build
$DC_SCRIPT up -d mongo redis openldap authelia nginx smtp $DC_SCRIPT up -d httpbin mongo redis openldap authelia nginx smtp

View File

@ -9,4 +9,5 @@ docker-compose \
-f example/redis/docker-compose.yml \ -f example/redis/docker-compose.yml \
-f example/nginx/docker-compose.yml \ -f example/nginx/docker-compose.yml \
-f example/smtp/docker-compose.yml \ -f example/smtp/docker-compose.yml \
-f example/httpbin/docker-compose.yml \
-f example/ldap/docker-compose.yml $* -f example/ldap/docker-compose.yml $*

View File

@ -3,4 +3,4 @@
DC_SCRIPT=./scripts/example-dockerhub/dc-example.sh DC_SCRIPT=./scripts/example-dockerhub/dc-example.sh
#$DC_SCRIPT build #$DC_SCRIPT build
$DC_SCRIPT up -d mongo redis openldap authelia nginx smtp $DC_SCRIPT up -d httpbin mongo redis openldap authelia nginx smtp

View File

@ -1,14 +1,14 @@
#!/bin/bash #!/bin/bash
DC_SCRIPT=./scripts/example-commit/dc-example.sh DC_SCRIPT=./scripts/example-commit/dc-example.sh
EXPECTED_SERVICES_COUNT=6 EXPECTED_SERVICES_COUNT=7
build_services() { build_services() {
$DC_SCRIPT build authelia $DC_SCRIPT build authelia
} }
start_services() { start_services() {
$DC_SCRIPT up -d mongo redis openldap authelia nginx smtp $DC_SCRIPT up -d httpbin mongo redis openldap authelia nginx smtp
sleep 3 sleep 3
} }

View File

@ -1,16 +1,24 @@
import * as speakeasy from "speakeasy"; import Speakeasy = require("speakeasy");
import { Speakeasy } from "../../types/Dependencies";
import BluebirdPromise = require("bluebird"); import BluebirdPromise = require("bluebird");
import { TOTPSecret } from "../../types/TOTPSecret";
interface GenerateSecretOptions {
length?: number;
symbols?: boolean;
otpauth_url?: boolean;
name?: string;
issuer?: string;
}
export class TOTPGenerator { export class TOTPGenerator {
private speakeasy: Speakeasy; private speakeasy: typeof Speakeasy;
constructor(speakeasy: Speakeasy) { constructor(speakeasy: typeof Speakeasy) {
this.speakeasy = speakeasy; this.speakeasy = speakeasy;
} }
generate(options?: speakeasy.GenerateOptions): speakeasy.Key { generate(options?: GenerateSecretOptions): TOTPSecret {
return this.speakeasy.generateSecret(options); return this.speakeasy.generateSecret(options);
} }
} }

View File

@ -1,23 +1,27 @@
import Speakeasy = require("speakeasy");
import { Speakeasy } from "../../types/Dependencies";
import BluebirdPromise = require("bluebird"); import BluebirdPromise = require("bluebird");
const TOTP_ENCODING = "base32"; const TOTP_ENCODING = "base32";
const WINDOW: number = 1;
export class TOTPValidator { export class TOTPValidator {
private speakeasy: Speakeasy; private speakeasy: typeof Speakeasy;
constructor(speakeasy: Speakeasy) { constructor(speakeasy: typeof Speakeasy) {
this.speakeasy = speakeasy; this.speakeasy = speakeasy;
} }
validate(token: string, secret: string): BluebirdPromise<void> { validate(token: string, secret: string): BluebirdPromise<void> {
const real_token = this.speakeasy.totp({ const isValid = this.speakeasy.totp.verify({
secret: secret, secret: secret,
encoding: TOTP_ENCODING encoding: TOTP_ENCODING,
}); token: token,
window: WINDOW
} as any);
if (token == real_token) return BluebirdPromise.resolve(); if (isValid)
return BluebirdPromise.reject(new Error("Wrong challenge")); return BluebirdPromise.resolve();
else
return BluebirdPromise.reject(new Error("Wrong TOTP token."));
} }
} }

View File

@ -1,26 +1,37 @@
import { TOTPValidator } from "../src/lib/TOTPValidator"; import { TOTPValidator } from "../src/lib/TOTPValidator";
import sinon = require("sinon"); import Sinon = require("sinon");
import Promise = require("bluebird"); import Speakeasy = require("speakeasy");
import SpeakeasyMock = require("./mocks/speakeasy");
describe("test TOTP validation", function() { describe("test TOTP validation", function() {
let totpValidator: TOTPValidator; let totpValidator: TOTPValidator;
let totpValidateStub: Sinon.SinonStub;
beforeEach(() => { beforeEach(() => {
SpeakeasyMock.totp.returns("token"); totpValidateStub = Sinon.stub(Speakeasy.totp, "verify");
totpValidator = new TOTPValidator(SpeakeasyMock as any); totpValidator = new TOTPValidator(Speakeasy);
});
afterEach(function() {
totpValidateStub.restore();
}); });
it("should validate the TOTP token", function() { it("should validate the TOTP token", function() {
const totp_secret = "NBD2ZV64R9UV1O7K"; const totp_secret = "NBD2ZV64R9UV1O7K";
const token = "token"; const token = "token";
totpValidateStub.withArgs({
secret: totp_secret,
token: token,
encoding: "base32",
window: 1
}).returns(true);
return totpValidator.validate(token, totp_secret); return totpValidator.validate(token, totp_secret);
}); });
it("should not validate a wrong TOTP token", function(done) { it("should not validate a wrong TOTP token", function(done) {
const totp_secret = "NBD2ZV64R9UV1O7K"; const totp_secret = "NBD2ZV64R9UV1O7K";
const token = "wrong token"; const token = "wrong token";
totpValidateStub.returns(false);
totpValidator.validate(token, totp_secret) totpValidator.validate(token, totp_secret)
.catch(function() { .catch(function() {
done(); done();

View File

@ -29,7 +29,12 @@ describe("test user data store", function () {
totpSecret = { totpSecret = {
ascii: "abc", ascii: "abc",
base32: "ABCDKZLEFZGREJK", base32: "ABCDKZLEFZGREJK",
otpauth_url: "totp://test" otpauth_url: "totp://test",
google_auth_qr: "dummy",
hex: "dummy",
qr_code_ascii: "dummy",
qr_code_base32: "dummy",
qr_code_hex: "dummy"
}; };
u2fRegistration = { u2fRegistration = {

View File

@ -1,6 +1,11 @@
export interface TOTPSecret { export interface TOTPSecret {
base32: string;
ascii: string; ascii: string;
otpauth_url?: string; hex: string;
} base32: string;
qr_code_ascii: string;
qr_code_hex: string;
qr_code_base32: string;
google_auth_qr: string;
otpauth_url: string;
}

96
server/types/speakeasy.d.ts vendored Normal file
View File

@ -0,0 +1,96 @@
declare module "speakeasy" {
export = speakeasy
interface SharedOptions {
encoding?: string
algorithm?: string
}
interface DigestOptions extends SharedOptions {
secret: string
counter: number
}
interface HOTPOptions extends SharedOptions {
secret: string
counter: number
digest?: Buffer
digits?: number
}
interface HOTPVerifyOptions extends SharedOptions {
secret: string
token: string
counter: number
digits?: number
window?: number
}
interface TOTPOptions extends SharedOptions {
secret: string
time?: number
step?: number
epoch?: number
counter?: number
digits?: number
}
interface TOTPVerifyOptions extends SharedOptions {
secret: string
token: string
time?: number
step?: number
epoch?: number
counter?: number
digits?: number
window?: number
}
interface GenerateSecretOptions {
length?: number
symbols?: boolean
otpauth_url?: boolean
name?: string
issuer?: string
}
interface GeneratedSecret {
ascii: string
hex: string
base32: string
qr_code_ascii: string
qr_code_hex: string
qr_code_base32: string
google_auth_qr: string
otpauth_url: string
}
interface OTPAuthURLOptions extends SharedOptions {
secret: string
label: string
type?: string
counter?: number
issuer?: string
digits?: number
period?: number
}
interface Speakeasy {
digest: (options: DigestOptions) => Buffer
hotp: {
(options: HOTPOptions): string,
verifyDelta: (options: HOTPVerifyOptions) => boolean,
verify: (options: HOTPVerifyOptions) => boolean,
}
totp: {
(options: TOTPOptions): string
verifyDelta: (options: TOTPVerifyOptions) => boolean,
verify: (options: TOTPVerifyOptions) => boolean,
}
generateSecret: (options?: GenerateSecretOptions) => GeneratedSecret
generateSecretASCII: (length?: number, symbols?: boolean) => string
otpauthURL: (options: OTPAuthURLOptions) => string
}
const speakeasy: Speakeasy
}

View File

@ -0,0 +1,6 @@
Feature: User and groups headers are correctly forwarded to backend
@need-authenticated-user-john
Scenario: Custom-Forwarded-User and Custom-Forwarded-Groups are correctly forwarded to protected backend
When I visit "https://public.test.local:8080/headers"
Then I see header "Custom-Forwarded-User" set to "john"
Then I see header "Custom-Forwarded-Groups" set to "dev,admin"

View File

@ -0,0 +1,20 @@
import Cucumber = require("cucumber");
import seleniumWebdriver = require("selenium-webdriver");
import CustomWorld = require("../support/world");
import Util = require("util");
import BluebirdPromise = require("bluebird");
Cucumber.defineSupportCode(function ({ Given, When, Then }) {
Then("I see header {stringInDoubleQuotes} set to {stringInDoubleQuotes}",
{ timeout: 5000 },
function (expectedHeaderName: string, expectedValue: string) {
return this.driver.findElement(seleniumWebdriver.By.tagName("body")).getText()
.then(function (txt: string) {
const expectedLine = Util.format("\"%s\": \"%s\"", expectedHeaderName, expectedValue);
if (txt.indexOf(expectedLine) > 0)
return BluebirdPromise.resolve();
else
return BluebirdPromise.reject(new Error(Util.format("No such header or with unexpected value.")));
});
})
});

View File

@ -23,5 +23,4 @@ Cucumber.defineSupportCode(function ({ Given, When, Then }) {
return that.driver.sleep(500); return that.driver.sleep(500);
}); });
}); });
}); });

View File

@ -21,6 +21,7 @@ function CustomWorld() {
}; };
this.setFieldTo = function (fieldName: string, content: string) { this.setFieldTo = function (fieldName: string, content: string) {
const that = this;
return this.driver.findElement(seleniumWebdriver.By.id(fieldName)) return this.driver.findElement(seleniumWebdriver.By.id(fieldName))
.sendKeys(content); .sendKeys(content);
}; };
@ -49,14 +50,19 @@ function CustomWorld() {
.findElement(seleniumWebdriver.By.tagName("button")) .findElement(seleniumWebdriver.By.tagName("button"))
.findElement(seleniumWebdriver.By.xpath("//button[contains(.,'" + buttonText + "')]")) .findElement(seleniumWebdriver.By.xpath("//button[contains(.,'" + buttonText + "')]"))
.click(); .click();
})
.then(function () {
return that.driver.sleep(1000);
}); });
}; };
this.waitUntilUrlContains = function(url: string) { this.waitUntilUrlContains = function (url: string) {
return this.driver.wait(seleniumWebdriver.until.urlIs(url), 15000); const that = this;
return this.driver.wait(seleniumWebdriver.until.urlIs(url), 15000)
.then(function () { }, function (err: Error) {
that.driver.getCurrentUrl()
.then(function (current: string) {
console.error("====> Error due to: %s (current) != %s (expected)", current, url);
});
return BluebirdPromise.reject(err);
});
}; };
this.loginWithUserPassword = function (username: string, password: string) { this.loginWithUserPassword = function (username: string, password: string) {