Merge pull request #193 from clems4ever/feature/support-basic-auth

Add support of basic auth for single-factor protected endpoints
This commit is contained in:
Clément Michaud 2017-11-01 20:33:09 +01:00 committed by GitHub
commit b37c0293b8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 618 additions and 428 deletions

View File

@ -22,7 +22,7 @@ used in production to secure internal services in a small docker swarm cluster.
3. [Second factor with U2F security keys](#second-factor-with-u2f-security-keys) 3. [Second factor with U2F security keys](#second-factor-with-u2f-security-keys)
4. [Password reset](#password-reset) 4. [Password reset](#password-reset)
5. [Access control](#access-control) 5. [Access control](#access-control)
6. [Basic authentication](#basic-authentication) 6. [Single factor authentication](#single-factor-authentication)
7. [Session management with Redis](#session-management-with-redis) 7. [Session management with Redis](#session-management-with-redis)
4. [Security](#security) 4. [Security](#security)
5. [Documentation](#documentation) 5. [Documentation](#documentation)
@ -37,12 +37,12 @@ used in production to secure internal services in a small docker swarm cluster.
* Two-factor authentication using either * Two-factor authentication using either
**[TOTP] - Time-Base One Time password -** or **[U2F] - Universal 2-Factor -** **[TOTP] - Time-Base One Time password -** or **[U2F] - Universal 2-Factor -**
as 2nd factor. as 2nd factor.
* Password reset with identity verification by sending links to user email * Password reset with identity verification using email.
address. * Single and two factors authentication methods available.
* Two-factor and basic authentication methods available.
* Access restriction after too many authentication attempts. * Access restriction after too many authentication attempts.
* Session management using Redis key/value store.
* User-defined access control per subdomain and resource. * User-defined access control per subdomain and resource.
* Support of [basic authentication] for endpoints protected by single factor.
* High-availability using a highly-available distributed database and KV store.
## Deployment ## Deployment
@ -190,11 +190,14 @@ user access to some resources and subdomains. Those rules are defined and fully
in the configuration file. They can apply to users, groups or everyone. in the configuration file. They can apply to users, groups or everyone.
Check out [config.template.yml] to see how they are defined. Check out [config.template.yml] to see how they are defined.
### Basic Authentication ### Single factor authentication
Authelia allows you to customize the authentication method to use for each sub-domain. Authelia allows you to customize the authentication method to use for each
The supported methods are either "single_factor" and "two_factor". sub-domain.The supported methods are either "single_factor" or "two_factor".
Please see [config.template.yml] to see an example of configuration. Please see [config.template.yml] to see an example of configuration.
It is also possible to use [basic authentication] to access a resource
protected by a single factor.
### Session management with Redis ### Session management with Redis
When your users authenticate against Authelia, sessions are stored in a Redis key/value store. You can specify your own Redis instance in [config.template.yml]. When your users authenticate against Authelia, sessions are stored in a Redis key/value store. You can specify your own Redis instance in [config.template.yml].
@ -293,3 +296,4 @@ Follow [contributing](CONTRIBUTORS.md) file.
[Google Authenticator]: https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en [Google Authenticator]: https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en
[config.template.yml]: https://github.com/clems4ever/authelia/blob/master/config.template.yml [config.template.yml]: https://github.com/clems4ever/authelia/blob/master/config.template.yml
[HSTS]: https://www.nginx.com/blog/http-strict-transport-security-hsts-and-nginx/ [HSTS]: https://www.nginx.com/blog/http-strict-transport-security-hsts-and-nginx/
[basic authentication]: https://en.wikipedia.org/wiki/Basic_access_authentication

View File

@ -262,6 +262,7 @@ http {
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host; proxy_set_header Host $http_host;
proxy_set_header Content-Length ""; proxy_set_header Content-Length "";
proxy_set_header Proxy-Authorization $http_authorization;
proxy_pass http://authelia/api/verify; proxy_pass http://authelia/api/verify;
} }
@ -280,6 +281,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;
}
} }
} }

View File

@ -70,7 +70,7 @@
"@types/request": "^2.0.5", "@types/request": "^2.0.5",
"@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.3.7",
"@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",
@ -101,8 +101,7 @@
"request-promise": "^4.2.2", "request-promise": "^4.2.2",
"selenium-webdriver": "^3.5.0", "selenium-webdriver": "^3.5.0",
"should": "^11.1.1", "should": "^11.1.1",
"sinon": "^2.3.8", "sinon": "^4.0.2",
"sinon-promise": "^0.1.3",
"tmp": "0.0.31", "tmp": "0.0.31",
"ts-node": "^3.3.0", "ts-node": "^3.3.0",
"tslint": "^5.2.0", "tslint": "^5.2.0",

View File

@ -1,17 +0,0 @@
import BluebirdPromise = require("bluebird");
import express = require("express");
import objectPath = require("object-path");
import FirstFactorValidator = require("./FirstFactorValidator");
import { AuthenticationSessionHandler } from "./AuthenticationSessionHandler";
import { IRequestLogger } from "./logging/IRequestLogger";
export function validate(req: express.Request, logger: IRequestLogger): BluebirdPromise<void> {
return FirstFactorValidator.validate(req, logger)
.then(function () {
const authSession = AuthenticationSessionHandler.get(req, logger);
if (!authSession.second_factor)
return BluebirdPromise.reject("No second factor variable.");
return BluebirdPromise.resolve();
});
}

View File

@ -1,39 +0,0 @@
import BluebirdPromise = require("bluebird");
import exceptions = require("../Exceptions");
import ldapjs = require("ldapjs");
import { IClient } from "./IClient";
import { IClientFactory } from "./IClientFactory";
import { IGroupsRetriever } from "./IGroupsRetriever";
import { LdapConfiguration } from "../configuration/Configuration";
export class GroupsRetriever implements IGroupsRetriever {
private options: LdapConfiguration;
private clientFactory: IClientFactory;
constructor(options: LdapConfiguration, clientFactory: IClientFactory) {
this.options = options;
this.clientFactory = clientFactory;
}
retrieve(username: string, client?: IClient): BluebirdPromise<string[]> {
client = this.clientFactory.create(this.options.user, this.options.password);
let groups: string[];
return client.open()
.then(function () {
return client.searchGroups(username);
})
.then(function (groups_: string[]) {
groups = groups_;
return client.close();
})
.then(function () {
return BluebirdPromise.resolve(groups);
})
.catch(function (err: Error) {
return BluebirdPromise.reject(new exceptions.LdapError("Failed during groups retrieval: " + err.message));
});
}
}

View File

@ -1,6 +0,0 @@
import BluebirdPromise = require("bluebird");
import { IClient } from "./IClient";
export interface IGroupsRetriever {
retrieve(username: string): BluebirdPromise<string[]>;
}

View File

@ -17,8 +17,10 @@ export class PasswordUpdater implements IPasswordUpdater {
this.clientFactory = clientFactory; this.clientFactory = clientFactory;
} }
updatePassword(username: string, newPassword: string): BluebirdPromise<void> { updatePassword(username: string, newPassword: string)
const adminClient = this.clientFactory.create(this.options.user, this.options.password); : BluebirdPromise<void> {
const adminClient = this.clientFactory.create(this.options.user,
this.options.password);
return adminClient.open() return adminClient.open()
.then(function () { .then(function () {
@ -27,8 +29,10 @@ export class PasswordUpdater implements IPasswordUpdater {
.then(function () { .then(function () {
return adminClient.close(); return adminClient.close();
}) })
.error(function (err: Error) { .catch(function (err: Error) {
return BluebirdPromise.reject(new exceptions.LdapError("Failed during password update: " + err.message)); return BluebirdPromise.reject(
new exceptions.LdapError(
"Error while updating password: " + err.message));
}); });
} }
} }

View File

@ -3,7 +3,6 @@ import express = require("express");
import objectPath = require("object-path"); import objectPath = require("object-path");
import winston = require("winston"); import winston = require("winston");
import Endpoints = require("../../../../../shared/api"); import Endpoints = require("../../../../../shared/api");
import AuthenticationValidator = require("../../AuthenticationValidator");
import BluebirdPromise = require("bluebird"); import BluebirdPromise = require("bluebird");
import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler";
import Constants = require("../../../../../shared/constants"); import Constants = require("../../../../../shared/constants");

View File

@ -0,0 +1,22 @@
import Express = require("express");
import BluebirdPromise = require("bluebird");
import Util = require("util");
import { ServerVariables } from "../../ServerVariables";
import Exceptions = require("../../Exceptions");
export default function (req: Express.Request, vars: ServerVariables,
domain: string, path: string, username: string, groups: string[]) {
return new BluebirdPromise(function (resolve, reject) {
const isAllowed = vars.accessController
.isAccessAllowed(domain, path, username, groups);
if (!isAllowed) {
reject(new Exceptions.DomainAccessDenied(Util.format(
"User '%s' does not have access to '%s'", username, domain)));
return;
}
resolve();
});
}

View File

@ -1,119 +1,69 @@
import objectPath = require("object-path");
import BluebirdPromise = require("bluebird"); import BluebirdPromise = require("bluebird");
import express = require("express"); import Express = require("express");
import exceptions = require("../../Exceptions"); import Exceptions = require("../../Exceptions");
import winston = require("winston");
import AuthenticationValidator = require("../../AuthenticationValidator");
import ErrorReplies = require("../../ErrorReplies"); import ErrorReplies = require("../../ErrorReplies");
import { AppConfiguration } from "../../configuration/Configuration";
import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler";
import { AuthenticationSession } from "../../../../types/AuthenticationSession";
import Constants = require("../../../../../shared/constants");
import Util = require("util");
import { DomainExtractor } from "../../utils/DomainExtractor";
import { ServerVariables } from "../../ServerVariables"; import { ServerVariables } from "../../ServerVariables";
import { MethodCalculator } from "../../authentication/MethodCalculator"; import GetWithSessionCookieMethod from "./get_session_cookie";
import { IRequestLogger } from "../../logging/IRequestLogger"; import GetWithBasicAuthMethod from "./get_basic_auth";
const FIRST_FACTOR_NOT_VALIDATED_MESSAGE = "First factor not yet validated"; import { AuthenticationSessionHandler }
const SECOND_FACTOR_NOT_VALIDATED_MESSAGE = "Second factor not yet validated"; from "../../AuthenticationSessionHandler";
import { AuthenticationSession }
from "../../../../types/AuthenticationSession";
const REMOTE_USER = "Remote-User"; const REMOTE_USER = "Remote-User";
const REMOTE_GROUPS = "Remote-Groups"; const REMOTE_GROUPS = "Remote-Groups";
function verify_inactivity(req: express.Request,
authSession: AuthenticationSession,
configuration: AppConfiguration, logger: IRequestLogger)
: BluebirdPromise<void> {
const lastActivityTime = authSession.last_activity_datetime; function verifyWithSelectedMethod(req: Express.Request, res: Express.Response,
const currentTime = new Date().getTime(); vars: ServerVariables, authSession: AuthenticationSession)
authSession.last_activity_datetime = currentTime; : () => BluebirdPromise<{ username: string, groups: string[] }> {
return function () {
const authorization: string = "" + req.headers["proxy-authorization"];
if (authorization && authorization.startsWith("Basic "))
return GetWithBasicAuthMethod(req, res, vars, authorization);
// If inactivity is not specified, then inactivity timeout does not apply return GetWithSessionCookieMethod(req, res, vars, authSession);
if (!configuration.session.inactivity) { };
return BluebirdPromise.resolve();
}
const inactivityPeriodMs = currentTime - lastActivityTime;
logger.debug(req, "Inactivity period was %s s and max period was %s.",
inactivityPeriodMs / 1000, configuration.session.inactivity / 1000);
if (inactivityPeriodMs < configuration.session.inactivity) {
return BluebirdPromise.resolve();
}
logger.debug(req, "Session has been reset after too long inactivity period.");
AuthenticationSessionHandler.reset(req);
return BluebirdPromise.reject(new Error("Inactivity period exceeded."));
} }
function verify_filter(req: express.Request, res: express.Response, function setRedirectHeader(req: Express.Request, res: Express.Response) {
vars: ServerVariables): BluebirdPromise<void> { return function () {
let authSession: AuthenticationSession;
let username: string;
let groups: string[];
return new BluebirdPromise(function (resolve, reject) {
authSession = AuthenticationSessionHandler.get(req, vars.logger);
username = authSession.userid;
groups = authSession.groups;
res.set("Redirect", encodeURIComponent("https://" + req.headers["host"] + res.set("Redirect", encodeURIComponent("https://" + req.headers["host"] +
req.headers["x-original-uri"])); req.headers["x-original-uri"]));
if (!authSession.userid) {
reject(new exceptions.AccessDeniedError(
Util.format("%s: %s.", FIRST_FACTOR_NOT_VALIDATED_MESSAGE, "userid is missing")));
return;
}
const host = objectPath.get<express.Request, string>(req, "headers.host");
const path = objectPath.get<express.Request, string>(req, "headers.x-original-uri");
const domain = DomainExtractor.fromHostHeader(host);
const authenticationMethod =
MethodCalculator.compute(vars.config.authentication_methods, domain);
vars.logger.debug(req, "domain=%s, path=%s, user=%s, groups=%s", domain, path,
username, groups.join(","));
if (!authSession.first_factor)
return reject(new exceptions.AccessDeniedError(
Util.format("%s: %s.", FIRST_FACTOR_NOT_VALIDATED_MESSAGE, "first factor is false")));
if (authenticationMethod == "two_factor" && !authSession.second_factor)
return reject(new exceptions.AccessDeniedError(
Util.format("%s: %s.", SECOND_FACTOR_NOT_VALIDATED_MESSAGE, "second factor is false")));
const isAllowed = vars.accessController.isAccessAllowed(domain, path, username, groups);
if (!isAllowed) return reject(
new exceptions.DomainAccessDenied(Util.format("User '%s' does not have access to '%s'",
username, domain)));
resolve();
})
.then(function () {
return verify_inactivity(req, authSession,
vars.config, vars.logger);
})
.then(function () {
res.setHeader(REMOTE_USER, username);
res.setHeader(REMOTE_GROUPS, groups.join(","));
return BluebirdPromise.resolve(); return BluebirdPromise.resolve();
}); };
}
function setUserAndGroupsHeaders(res: Express.Response) {
return function (u: { username: string, groups: string[] }) {
res.setHeader(REMOTE_USER, u.username);
res.setHeader(REMOTE_GROUPS, u.groups.join(","));
return BluebirdPromise.resolve();
};
}
function replyWith200(res: Express.Response) {
return function () {
res.status(204);
res.send();
};
} }
export default function (vars: ServerVariables) { export default function (vars: ServerVariables) {
return function (req: express.Request, res: express.Response) return function (req: Express.Request, res: Express.Response)
: BluebirdPromise<void> { : BluebirdPromise<void> {
return verify_filter(req, res, vars) let authSession: AuthenticationSession;
.then(function () { return new BluebirdPromise(function (resolve, reject) {
res.status(204); authSession = AuthenticationSessionHandler.get(req, vars.logger);
res.send(); resolve();
return BluebirdPromise.resolve();
}) })
.then(setRedirectHeader(req, res))
.then(verifyWithSelectedMethod(req, res, vars, authSession))
.then(setUserAndGroupsHeaders(res))
.then(replyWith200(res))
// The user is authenticated but has restricted access -> 403 // The user is authenticated but has restricted access -> 403
.catch(exceptions.DomainAccessDenied, ErrorReplies .catch(Exceptions.DomainAccessDenied, ErrorReplies
.replyWithError403(req, res, vars.logger)) .replyWithError403(req, res, vars.logger))
// The user is not yet authenticated -> 401 // The user is not yet authenticated -> 401
.catch(ErrorReplies.replyWithError401(req, res, vars.logger)); .catch(ErrorReplies.replyWithError401(req, res, vars.logger));

View File

@ -0,0 +1,75 @@
import Express = require("express");
import BluebirdPromise = require("bluebird");
import ObjectPath = require("object-path");
import { ServerVariables } from "../../ServerVariables";
import { AuthenticationSession }
from "../../../../types/AuthenticationSession";
import { DomainExtractor } from "../../utils/DomainExtractor";
import { MethodCalculator } from "../../authentication/MethodCalculator";
import AccessControl from "./access_control";
export default function (req: Express.Request, res: Express.Response,
vars: ServerVariables, authorizationHeader: string)
: BluebirdPromise<{ username: string, groups: string[] }> {
let username: string;
let groups: string[];
let domain: string;
let path: string;
return new BluebirdPromise<[string, string]>(function (resolve, reject) {
const host = ObjectPath.get<Express.Request, string>(req, "headers.host");
domain = DomainExtractor.fromHostHeader(host);
path =
ObjectPath.get<Express.Request, string>(req, "headers.x-original-uri");
const authenticationMethod =
MethodCalculator.compute(vars.config.authentication_methods, domain);
if (authenticationMethod != "single_factor") {
reject(new Error("This domain is not protected with single factor. " +
"You cannot log in with basic authentication."));
return;
}
const base64Re = new RegExp("^Basic ((?:[A-Za-z0-9+/]{4})*" +
"(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?)$");
const isTokenValidBase64 = base64Re.test(authorizationHeader);
if (!isTokenValidBase64) {
reject(new Error("No valid base64 token found in the header"));
return;
}
const tokenMatches = authorizationHeader.match(base64Re);
const base64Token = tokenMatches[1];
const decodedToken = Buffer.from(base64Token, "base64").toString();
const splittedToken = decodedToken.split(":");
if (splittedToken.length != 2) {
reject(new Error(
"The authorization token is invalid. Expecting 'userid:password'"));
return;
}
username = splittedToken[0];
const password = splittedToken[1];
resolve([username, password]);
})
.then(function ([userid, password]) {
return vars.ldapAuthenticator.authenticate(userid, password);
})
.then(function (groupsAndEmails) {
groups = groupsAndEmails.groups;
return AccessControl(req, vars, domain, path, username, groups);
})
.then(function () {
return BluebirdPromise.resolve({
username: username,
groups: groups
});
})
.catch(function (err: Error) {
return BluebirdPromise.reject(
new Error("Unable to authenticate the user with basic auth. Cause: "
+ err.message));
});
}

View File

@ -0,0 +1,102 @@
import Express = require("express");
import BluebirdPromise = require("bluebird");
import Util = require("util");
import ObjectPath = require("object-path");
import Exceptions = require("../../Exceptions");
import { AppConfiguration } from "../../configuration/Configuration";
import Constants = require("../../../../../shared/constants");
import { DomainExtractor } from "../../utils/DomainExtractor";
import { ServerVariables } from "../../ServerVariables";
import { MethodCalculator } from "../../authentication/MethodCalculator";
import { IRequestLogger } from "../../logging/IRequestLogger";
import { AuthenticationSession }
from "../../../../types/AuthenticationSession";
import { AuthenticationSessionHandler }
from "../../AuthenticationSessionHandler";
import AccessControl from "./access_control";
const FIRST_FACTOR_NOT_VALIDATED_MESSAGE = "First factor not yet validated";
const SECOND_FACTOR_NOT_VALIDATED_MESSAGE = "Second factor not yet validated";
function verify_inactivity(req: Express.Request,
authSession: AuthenticationSession,
configuration: AppConfiguration, logger: IRequestLogger)
: BluebirdPromise<void> {
// If inactivity is not specified, then inactivity timeout does not apply
if (!configuration.session.inactivity) {
return BluebirdPromise.resolve();
}
const lastActivityTime = authSession.last_activity_datetime;
const currentTime = new Date().getTime();
authSession.last_activity_datetime = currentTime;
const inactivityPeriodMs = currentTime - lastActivityTime;
logger.debug(req, "Inactivity period was %s s and max period was %s.",
inactivityPeriodMs / 1000, configuration.session.inactivity / 1000);
if (inactivityPeriodMs < configuration.session.inactivity) {
return BluebirdPromise.resolve();
}
logger.debug(req, "Session has been reset after too long inactivity period.");
AuthenticationSessionHandler.reset(req);
return BluebirdPromise.reject(new Error("Inactivity period exceeded."));
}
export default function (req: Express.Request, res: Express.Response,
vars: ServerVariables, authSession: AuthenticationSession)
: BluebirdPromise<{ username: string, groups: string[] }> {
let username: string;
let groups: string[];
let domain: string;
let path: string;
return new BluebirdPromise(function (resolve, reject) {
username = authSession.userid;
groups = authSession.groups;
if (!authSession.userid) {
reject(new Exceptions.AccessDeniedError(
Util.format("%s: %s.", FIRST_FACTOR_NOT_VALIDATED_MESSAGE,
"userid is missing")));
return;
}
const host = ObjectPath.get<Express.Request, string>(req, "headers.host");
path =
ObjectPath.get<Express.Request, string>(req, "headers.x-original-uri");
domain = DomainExtractor.fromHostHeader(host);
const authenticationMethod =
MethodCalculator.compute(vars.config.authentication_methods, domain);
vars.logger.debug(req, "domain=%s, path=%s, user=%s, groups=%s", domain,
path, username, groups.join(","));
if (!authSession.first_factor)
return reject(new Exceptions.AccessDeniedError(
Util.format("%s: %s.", FIRST_FACTOR_NOT_VALIDATED_MESSAGE,
"first factor is false")));
if (authenticationMethod == "two_factor" && !authSession.second_factor)
return reject(new Exceptions.AccessDeniedError(
Util.format("%s: %s.", SECOND_FACTOR_NOT_VALIDATED_MESSAGE,
"second factor is false")));
resolve();
})
.then(function () {
return AccessControl(req, vars, domain, path, username, groups);
})
.then(function () {
return verify_inactivity(req, authSession,
vars.config, vars.logger);
})
.then(function () {
return BluebirdPromise.resolve({
username: authSession.userid,
groups: authSession.groups
});
});
}

View File

@ -87,11 +87,13 @@ describe("test ldap authentication", function () {
.returns(BluebirdPromise.resolve("cn=" + USERNAME + ",ou=users,dc=example,dc=com")); .returns(BluebirdPromise.resolve("cn=" + USERNAME + ",ou=users,dc=example,dc=com"));
// user connects successfully // user connects successfully
userClientStub.openStub.returns(BluebirdPromise.reject(new Error("Error while binding"))); userClientStub.openStub.rejects(new Error("Error while binding"));
userClientStub.closeStub.returns(BluebirdPromise.resolve()); userClientStub.closeStub.returns(BluebirdPromise.resolve());
return authenticator.authenticate(USERNAME, PASSWORD) return authenticator.authenticate(USERNAME, PASSWORD)
.then(function () { return BluebirdPromise.reject("Should not be here!"); }) .then(function () {
return BluebirdPromise.reject("Should not be here!");
})
.catch(function () { .catch(function () {
return BluebirdPromise.resolve(); return BluebirdPromise.resolve();
}); });
@ -118,10 +120,12 @@ describe("test ldap authentication", function () {
adminClientStub.searchEmailsStub.returns(BluebirdPromise.resolve(["group1"])); adminClientStub.searchEmailsStub.returns(BluebirdPromise.resolve(["group1"]));
// admin retrieves emails and groups of user // admin retrieves emails and groups of user
adminClientStub.searchGroupsStub adminClientStub.searchGroupsStub
.returns(BluebirdPromise.reject(new Error("Error while retrieving emails and groups"))); .rejects(new Error("Error while retrieving emails and groups"));
return authenticator.authenticate(USERNAME, PASSWORD) return authenticator.authenticate(USERNAME, PASSWORD)
.then(function () { return BluebirdPromise.reject("Should not be here!"); }) .then(function () {
return BluebirdPromise.reject("Should not be here!");
})
.catch(function () { .catch(function () {
return BluebirdPromise.resolve(); return BluebirdPromise.resolve();
}); });

View File

@ -56,7 +56,8 @@ describe("test emails retriever", function () {
}); });
describe("failure", function () { describe("failure", function () {
it("should fail retrieving emails when search operation fails", function () { it("should fail retrieving emails when search operation fails",
function () {
clientFactoryStub.createStub.withArgs(ADMIN_USER_DN, ADMIN_PASSWORD) clientFactoryStub.createStub.withArgs(ADMIN_USER_DN, ADMIN_PASSWORD)
.returns(adminClientStub); .returns(adminClientStub);
@ -65,11 +66,15 @@ describe("test emails retriever", function () {
adminClientStub.closeStub.returns(BluebirdPromise.resolve()); adminClientStub.closeStub.returns(BluebirdPromise.resolve());
adminClientStub.searchEmailsStub.withArgs(USERNAME) adminClientStub.searchEmailsStub.withArgs(USERNAME)
.returns(BluebirdPromise.reject(new Error("Error while searching emails"))); .rejects(new Error("Error while searching emails"));
return emailsRetriever.retrieve(USERNAME) return emailsRetriever.retrieve(USERNAME)
.then(function () { return BluebirdPromise.reject(new Error("Should not be here")); }) .then(function () {
.catch(function () { return BluebirdPromise.resolve(); }); return BluebirdPromise.reject(new Error("Should not be here"));
})
.catch(function () {
return BluebirdPromise.resolve();
});
}); });
}); });
}); });

View File

@ -1,75 +0,0 @@
import { GroupsRetriever } from "../../src/lib/ldap/GroupsRetriever";
import { LdapConfiguration } from "../../src/lib/configuration/Configuration";
import Sinon = require("sinon");
import BluebirdPromise = require("bluebird");
import Assert = require("assert");
import { ClientFactoryStub } from "../mocks/ldap/ClientFactoryStub";
import { ClientStub } from "../mocks/ldap/ClientStub";
describe("test groups retriever", function () {
const USERNAME = "username";
const ADMIN_USER_DN = "cn=admin,dc=example,dc=com";
const ADMIN_PASSWORD = "password";
let clientFactoryStub: ClientFactoryStub;
let adminClientStub: ClientStub;
let groupsRetriever: GroupsRetriever;
let ldapConfig: LdapConfiguration;
beforeEach(function () {
clientFactoryStub = new ClientFactoryStub();
adminClientStub = new ClientStub();
ldapConfig = {
url: "http://ldap",
user: ADMIN_USER_DN,
password: ADMIN_PASSWORD,
users_dn: "ou=users,dc=example,dc=com",
groups_dn: "ou=groups,dc=example,dc=com",
group_name_attribute: "cn",
groups_filter: "member=cn={0},ou=users,dc=example,dc=com",
mail_attribute: "mail",
users_filter: "cn={0}"
};
groupsRetriever = new GroupsRetriever(ldapConfig, clientFactoryStub);
});
describe("success", function () {
it("should retrieve groups successfully", function () {
clientFactoryStub.createStub.withArgs(ADMIN_USER_DN, ADMIN_PASSWORD)
.returns(adminClientStub);
// admin connects successfully
adminClientStub.openStub.returns(BluebirdPromise.resolve());
adminClientStub.closeStub.returns(BluebirdPromise.resolve());
adminClientStub.searchGroupsStub.withArgs(USERNAME)
.returns(BluebirdPromise.resolve(["user@example.com"]));
return groupsRetriever.retrieve(USERNAME);
});
});
describe("failure", function () {
it("should fail retrieving groups when search operation fails", function () {
clientFactoryStub.createStub.withArgs(ADMIN_USER_DN, ADMIN_PASSWORD)
.returns(adminClientStub);
// admin connects successfully
adminClientStub.openStub.returns(BluebirdPromise.resolve());
adminClientStub.closeStub.returns(BluebirdPromise.resolve());
adminClientStub.searchGroupsStub.withArgs(USERNAME)
.returns(BluebirdPromise.reject(new Error("Error while searching groups")));
return groupsRetriever.retrieve(USERNAME)
.then(function () { return BluebirdPromise.reject(new Error("Should not be here")); })
.catch(function () { return BluebirdPromise.resolve(); });
});
});
});

View File

@ -37,6 +37,9 @@ describe("test password update", function () {
}; };
ssha512HashGenerator = Sinon.stub(HashGenerator, "ssha512"); ssha512HashGenerator = Sinon.stub(HashGenerator, "ssha512");
clientFactoryStub.createStub.withArgs(ADMIN_USER_DN, ADMIN_PASSWORD)
.returns(adminClientStub);
passwordUpdater = new PasswordUpdater(ldapConfig, clientFactoryStub); passwordUpdater = new PasswordUpdater(ldapConfig, clientFactoryStub);
}); });
@ -46,11 +49,10 @@ describe("test password update", function () {
describe("success", function () { describe("success", function () {
it("should update the password successfully", function () { it("should update the password successfully", function () {
clientFactoryStub.createStub.withArgs(ADMIN_USER_DN, ADMIN_PASSWORD) ssha512HashGenerator
.returns(adminClientStub); .returns("{CRYPT}$6$abcdefghijklm$AQmxaKfobGY9HSQa6aDYkAWOgPGNhGYn");
adminClientStub.modifyPasswordStub.withArgs(USERNAME, NEW_PASSWORD)
ssha512HashGenerator.returns("{CRYPT}$6$abcdefghijklm$AQmxaKfobGY9HSQa6aDYkAWOgPGNhGYn"); .returns(BluebirdPromise.resolve());
adminClientStub.modifyPasswordStub.withArgs(USERNAME, NEW_PASSWORD).returns(BluebirdPromise.resolve());
adminClientStub.openStub.returns(BluebirdPromise.resolve()); adminClientStub.openStub.returns(BluebirdPromise.resolve());
adminClientStub.closeStub.returns(BluebirdPromise.resolve()); adminClientStub.closeStub.returns(BluebirdPromise.resolve());
@ -59,19 +61,22 @@ describe("test password update", function () {
}); });
describe("failure", function () { describe("failure", function () {
it("should fail updating password when modify operation fails", function () { it("should fail updating password when modify operation fails",
clientFactoryStub.createStub.withArgs(ADMIN_USER_DN, ADMIN_PASSWORD) function () {
.returns(adminClientStub); ssha512HashGenerator
.returns("{CRYPT}$6$abcdefghijklm$AQmxaKfobGY9HSQa6aDYkAWOgPGNhGYn");
ssha512HashGenerator.returns("{CRYPT}$6$abcdefghijklm$AQmxaKfobGY9HSQa6aDYkAWOgPGNhGYn");
adminClientStub.modifyPasswordStub.withArgs(USERNAME, NEW_PASSWORD) adminClientStub.modifyPasswordStub.withArgs(USERNAME, NEW_PASSWORD)
.returns(BluebirdPromise.reject(new Error("Error while updating password"))); .rejects(new Error("Error while updating password"));
adminClientStub.openStub.returns(BluebirdPromise.resolve()); adminClientStub.openStub.returns(BluebirdPromise.resolve());
adminClientStub.closeStub.returns(BluebirdPromise.resolve()); adminClientStub.closeStub.returns(BluebirdPromise.resolve());
return passwordUpdater.updatePassword(USERNAME, NEW_PASSWORD) return passwordUpdater.updatePassword(USERNAME, NEW_PASSWORD)
.then(function () { return BluebirdPromise.reject(new Error("should not be here")); }) .then(function () {
.catch(function () { return BluebirdPromise.resolve(); }); return BluebirdPromise.reject(new Error("should not be here"));
})
.catch(function(err: Error) {
return BluebirdPromise.resolve();
});
}); });
}); });
}); });

View File

@ -28,14 +28,17 @@ describe("test /api/verify endpoint", function () {
}; };
AuthenticationSessionHandler.reset(req as any); AuthenticationSessionHandler.reset(req as any);
req.headers.host = "secret.example.com"; req.headers.host = "secret.example.com";
const s = ServerVariablesMockBuilder.build(); const s = ServerVariablesMockBuilder.build(true);
mocks = s.mocks; mocks = s.mocks;
vars = s.variables; vars = s.variables;
vars.config.authentication_methods.default_method = "two_factor";
authSession = AuthenticationSessionHandler.get(req as any, vars.logger); authSession = AuthenticationSessionHandler.get(req as any, vars.logger);
}); });
describe("with session cookie", function () {
beforeEach(function () {
vars.config.authentication_methods.default_method = "two_factor";
});
it("should be already authenticated", function () { it("should be already authenticated", function () {
mocks.accessController.isAccessAllowedMock.returns(true); mocks.accessController.isAccessAllowedMock.returns(true);
authSession.first_factor = true; authSession.first_factor = true;
@ -142,7 +145,7 @@ describe("test /api/verify endpoint", function () {
}); });
}); });
describe("given user tries to access a basic auth endpoint", function () { describe("given user tries to access a single factor endpoint", function () {
beforeEach(function () { beforeEach(function () {
req.query = { req.query = {
redirect: "http://redirect.url" redirect: "http://redirect.url"
@ -215,5 +218,96 @@ describe("test /api/verify endpoint", function () {
}); });
}); });
}); });
});
describe("with basic auth", function () {
it("should authenticate correctly", function () {
mocks.accessController.isAccessAllowedMock.returns(true);
mocks.config.authentication_methods.default_method = "single_factor";
mocks.ldapAuthenticator.authenticateStub.returns({
groups: ["mygroup", "othergroup"],
});
req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA==";
return VerifyGet.default(vars)(req as express.Request, res as any)
.then(function () {
Sinon.assert.calledWithExactly(res.setHeader, "Remote-User", "john");
Sinon.assert.calledWithExactly(res.setHeader, "Remote-Groups", "mygroup,othergroup");
Assert.equal(204, res.status.getCall(0).args[0]);
});
});
it("should fail when endpoint is protected by two factors", function () {
mocks.accessController.isAccessAllowedMock.returns(true);
mocks.config.authentication_methods.default_method = "single_factor";
mocks.config.authentication_methods.per_subdomain_methods = {
"secret.example.com": "two_factor"
};
mocks.ldapAuthenticator.authenticateStub.resolves({
groups: ["mygroup", "othergroup"],
});
req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA==";
return VerifyGet.default(vars)(req as express.Request, res as any)
.then(function () {
Assert(res.status.calledWithExactly(401));
});
});
it("should fail when base64 token is not valid", function () {
mocks.accessController.isAccessAllowedMock.returns(true);
mocks.config.authentication_methods.default_method = "single_factor";
mocks.ldapAuthenticator.authenticateStub.resolves({
groups: ["mygroup", "othergroup"],
});
req.headers["proxy-authorization"] = "Basic i_m*not_a_base64*token";
return VerifyGet.default(vars)(req as express.Request, res as any)
.then(function () {
Assert(res.status.calledWithExactly(401));
});
});
it("should fail when base64 token has not format user:psswd", function () {
mocks.accessController.isAccessAllowedMock.returns(true);
mocks.config.authentication_methods.default_method = "single_factor";
mocks.ldapAuthenticator.authenticateStub.resolves({
groups: ["mygroup", "othergroup"],
});
req.headers["proxy-authorization"] = "Basic am9objpwYXNzOmJhZA==";
return VerifyGet.default(vars)(req as express.Request, res as any)
.then(function () {
Assert(res.status.calledWithExactly(401));
});
});
it("should fail when bad user password is provided", function () {
mocks.accessController.isAccessAllowedMock.returns(true);
mocks.config.authentication_methods.default_method = "single_factor";
mocks.ldapAuthenticator.authenticateStub.rejects(new Error(
"Invalid credentials"));
req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA==";
return VerifyGet.default(vars)(req as express.Request, res as any)
.then(function () {
Assert(res.status.calledWithExactly(401));
});
});
it("should fail when resource is restricted", function () {
mocks.accessController.isAccessAllowedMock.returns(false);
mocks.config.authentication_methods.default_method = "single_factor";
mocks.ldapAuthenticator.authenticateStub.resolves({
groups: ["mygroup", "othergroup"],
});
req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA==";
return VerifyGet.default(vars)(req as express.Request, res as any)
.then(function () {
Assert(res.status.calledWithExactly(401));
});
});
});
}); });

View File

@ -4,3 +4,8 @@ Feature: User and groups headers are correctly forwarded to backend
When I visit "https://public.test.local:8080/headers" 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-User" set to "john"
Then I see header "Custom-Forwarded-Groups" set to "dev,admin" Then I see header "Custom-Forwarded-Groups" set to "dev,admin"
Scenario: Custom-Forwarded-User and Custom-Forwarded-Groups are correctly forwarded to protected backend when basic auth is used
When I request "https://single_factor.test.local:8080/headers" with username "john" and password "password" using basic authentication
Then I received header "Custom-Forwarded-User" set to "john"
And I received header "Custom-Forwarded-Groups" set to "dev,admin"

View File

@ -1,13 +1,15 @@
Feature: User can access certain subdomains with single factor Feature: User can access certain subdomains with single factor
@need-registered-user-john
Scenario: User is redirected to service after first factor if allowed Scenario: User is redirected to service after first factor if allowed
When I visit "https://auth.test.local:8080/?redirect=https%3A%2F%2Fsingle_factor.test.local%3A8080%2Fsecret.html" When I visit "https://auth.test.local:8080/?redirect=https%3A%2F%2Fsingle_factor.test.local%3A8080%2Fsecret.html"
And I login with user "john" and password "password" And I login with user "john" and password "password"
Then I'm redirected to "https://single_factor.test.local:8080/secret.html" Then I'm redirected to "https://single_factor.test.local:8080/secret.html"
@need-registered-user-john
Scenario: Redirection after first factor fails if single_factor not allowed. It redirects user to first factor. Scenario: Redirection after first factor fails if single_factor not allowed. It redirects user to first factor.
When I visit "https://auth.test.local:8080/?redirect=https%3A%2F%2Fadmin.test.local%3A8080%2Fsecret.html" When I visit "https://auth.test.local:8080/?redirect=https%3A%2F%2Fadmin.test.local%3A8080%2Fsecret.html"
And I login with user "john" and password "password" And I login with user "john" and password "password"
Then I'm redirected to "https://auth.test.local:8080/?redirect=https%3A%2F%2Fadmin.test.local%3A8080%2Fsecret.html" Then I'm redirected to "https://auth.test.local:8080/?redirect=https%3A%2F%2Fadmin.test.local%3A8080%2Fsecret.html"
Scenario: User can login using basic authentication
When I request "https://single_factor.test.local:8080/secret.html" with username "john" and password "password" using basic authentication
Then I receive the secret page

View File

@ -0,0 +1,39 @@
import Cucumber = require("cucumber");
import seleniumWebdriver = require("selenium-webdriver");
import Request = require("request-promise");
import BluebirdPromise = require("bluebird");
import Util = require("util");
Cucumber.defineSupportCode(function ({ Given, When, Then }) {
When("I request {stringInDoubleQuotes} with username {stringInDoubleQuotes}" +
" and password {stringInDoubleQuotes} using basic authentication",
function (url: string, username: string, password: string) {
const that = this;
return Request(url, {
auth: {
username: username,
password: password
},
resolveWithFullResponse: true
})
.then(function (response: any) {
that.response = response;
});
});
Then("I receive the secret page", function () {
if (this.response.body.match("This is a very important secret!"))
return BluebirdPromise.resolve();
return BluebirdPromise.reject(new Error("Secret page not received."));
});
Then("I received header {stringInDoubleQuotes} set to {stringInDoubleQuotes}",
function (expectedHeaderName: string, expectedValue: string) {
const expectedLine = Util.format("\"%s\": \"%s\"", expectedHeaderName,
expectedValue);
if (this.response.body.indexOf(expectedLine) > 0)
return BluebirdPromise.resolve();
return BluebirdPromise.reject(new Error(
Util.format("No such header or with unexpected value.")));
})
});