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

Add test for headers forwarding feature
pull/144/head
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-Real-IP $remote_addr;
proxy_set_header Host $http_host;
proxy_set_header Content-Length "";
proxy_pass http://authelia/verify;
}
@ -82,7 +83,6 @@ http {
auth_request /auth_verify;
auth_request_set $redirect $upstream_http_redirect;
proxy_set_header Redirect $redirect;
auth_request_set $user $upstream_http_remote_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 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 {
@ -110,6 +127,7 @@ http {
proxy_set_header X-Original-URI $request_uri;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $http_host;
proxy_set_header Content-Length "";
proxy_pass http://authelia/verify;
}
@ -118,7 +136,6 @@ http {
auth_request /auth_verify;
auth_request_set $redirect $upstream_http_redirect;
proxy_set_header Redirect $redirect;
auth_request_set $user $upstream_http_remote_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-Real-IP $remote_addr;
proxy_set_header Host $http_host;
proxy_set_header Content-Length "";
proxy_pass http://authelia/verify;
}
@ -154,7 +172,6 @@ http {
auth_request /auth_verify;
auth_request_set $redirect $upstream_http_redirect;
proxy_set_header Redirect $redirect;
auth_request_set $user $upstream_http_remote_user;
proxy_set_header X-Forwarded-User $user;
@ -191,7 +208,6 @@ http {
auth_request /auth_verify;
auth_request_set $redirect $upstream_http_redirect;
proxy_set_header Redirect $redirect;
auth_request_set $user $upstream_http_remote_user;
proxy_set_header X-Forwarded-User $user;
@ -228,7 +244,6 @@ http {
auth_request /auth_verify;
auth_request_set $redirect $upstream_http_redirect;
proxy_set_header Redirect $redirect;
auth_request_set $user $upstream_http_remote_user;
proxy_set_header X-Forwarded-User $user;

View File

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

View File

@ -10,5 +10,6 @@ docker-compose \
-f example/redis/docker-compose.yml \
-f example/nginx/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.yml $*

View File

@ -9,4 +9,5 @@ docker-compose \
-f example/redis/docker-compose.yml \
-f example/nginx/docker-compose.yml \
-f example/smtp/docker-compose.yml \
-f example/httpbin/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 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/nginx/docker-compose.yml \
-f example/smtp/docker-compose.yml \
-f example/httpbin/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 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
DC_SCRIPT=./scripts/example-commit/dc-example.sh
EXPECTED_SERVICES_COUNT=6
EXPECTED_SERVICES_COUNT=7
build_services() {
$DC_SCRIPT build authelia
}
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
}

View File

@ -1,16 +1,24 @@
import * as speakeasy from "speakeasy";
import { Speakeasy } from "../../types/Dependencies";
import Speakeasy = require("speakeasy");
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 {
private speakeasy: Speakeasy;
private speakeasy: typeof Speakeasy;
constructor(speakeasy: Speakeasy) {
constructor(speakeasy: typeof Speakeasy) {
this.speakeasy = speakeasy;
}
generate(options?: speakeasy.GenerateOptions): speakeasy.Key {
generate(options?: GenerateSecretOptions): TOTPSecret {
return this.speakeasy.generateSecret(options);
}
}

View File

@ -1,23 +1,27 @@
import { Speakeasy } from "../../types/Dependencies";
import Speakeasy = require("speakeasy");
import BluebirdPromise = require("bluebird");
const TOTP_ENCODING = "base32";
const WINDOW: number = 1;
export class TOTPValidator {
private speakeasy: Speakeasy;
private speakeasy: typeof Speakeasy;
constructor(speakeasy: Speakeasy) {
constructor(speakeasy: typeof Speakeasy) {
this.speakeasy = speakeasy;
}
validate(token: string, secret: string): BluebirdPromise<void> {
const real_token = this.speakeasy.totp({
const isValid = this.speakeasy.totp.verify({
secret: secret,
encoding: TOTP_ENCODING
});
encoding: TOTP_ENCODING,
token: token,
window: WINDOW
} as any);
if (token == real_token) return BluebirdPromise.resolve();
return BluebirdPromise.reject(new Error("Wrong challenge"));
if (isValid)
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 sinon = require("sinon");
import Promise = require("bluebird");
import SpeakeasyMock = require("./mocks/speakeasy");
import Sinon = require("sinon");
import Speakeasy = require("speakeasy");
describe("test TOTP validation", function() {
let totpValidator: TOTPValidator;
let totpValidateStub: Sinon.SinonStub;
beforeEach(() => {
SpeakeasyMock.totp.returns("token");
totpValidator = new TOTPValidator(SpeakeasyMock as any);
totpValidateStub = Sinon.stub(Speakeasy.totp, "verify");
totpValidator = new TOTPValidator(Speakeasy);
});
afterEach(function() {
totpValidateStub.restore();
});
it("should validate the TOTP token", function() {
const totp_secret = "NBD2ZV64R9UV1O7K";
const token = "token";
totpValidateStub.withArgs({
secret: totp_secret,
token: token,
encoding: "base32",
window: 1
}).returns(true);
return totpValidator.validate(token, totp_secret);
});
it("should not validate a wrong TOTP token", function(done) {
const totp_secret = "NBD2ZV64R9UV1O7K";
const token = "wrong token";
totpValidateStub.returns(false);
totpValidator.validate(token, totp_secret)
.catch(function() {
done();

View File

@ -29,7 +29,12 @@ describe("test user data store", function () {
totpSecret = {
ascii: "abc",
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 = {

View File

@ -1,6 +1,11 @@
export interface TOTPSecret {
base32: 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 100644
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);
});
});
});

View File

@ -21,6 +21,7 @@ function CustomWorld() {
};
this.setFieldTo = function (fieldName: string, content: string) {
const that = this;
return this.driver.findElement(seleniumWebdriver.By.id(fieldName))
.sendKeys(content);
};
@ -49,14 +50,19 @@ function CustomWorld() {
.findElement(seleniumWebdriver.By.tagName("button"))
.findElement(seleniumWebdriver.By.xpath("//button[contains(.,'" + buttonText + "')]"))
.click();
})
.then(function () {
return that.driver.sleep(1000);
});
};
this.waitUntilUrlContains = function(url: string) {
return this.driver.wait(seleniumWebdriver.until.urlIs(url), 15000);
this.waitUntilUrlContains = function (url: string) {
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) {