Merge pull request #98 from clems4ever/disable-second-factor

Allow basic auth for certain subdomains
pull/105/head
Clément Michaud 2017-09-26 23:25:07 +02:00 committed by GitHub
commit 89de19bb35
29 changed files with 683 additions and 532 deletions

View File

@ -15,13 +15,14 @@ addons:
- libgif-dev - libgif-dev
- google-chrome-stable - google-chrome-stable
hosts: hosts:
- auth.test.local - admin.test.local
- home.test.local - auth.test.local
- public.test.local - basicauth.test.local
- admin.test.local - dev.test.local
- dev.test.local - home.test.local
- mx1.mail.test.local - mx1.mail.test.local
- mx2.mail.test.local - mx2.mail.test.local
- public.test.local
before_install: before_install:
- npm install -g npm@'>=2.13.5' - npm install -g npm@'>=2.13.5'

View File

@ -26,7 +26,7 @@ module.exports = function (grunt) {
}, },
"docker-restart": { "docker-restart": {
cmd: "./scripts/dc-dev.sh", cmd: "./scripts/dc-dev.sh",
args: ['up', '-d'] args: ['restart', 'authelia']
}, },
"minify": { "minify": {
cmd: "./node_modules/.bin/uglifyjs", cmd: "./node_modules/.bin/uglifyjs",

View File

@ -5,6 +5,6 @@ services:
- ./test:/usr/src/test - ./test:/usr/src/test
- ./dist/src/server:/usr/src - ./dist/src/server:/usr/src
- ./node_modules:/usr/src/node_modules - ./node_modules:/usr/src/node_modules
- ./config.yml:/etc/authelia/config.yml:ro - ./config.template.yml:/etc/authelia/config.yml:ro
networks: networks:
- example-network - example-network

View File

@ -0,0 +1,10 @@
<html>
<head>
<title>Secret</title>
<link rel="icon" href="/icon.png" type="image/png" />
</head>
<body>
This is a very important secret!<br/>
Go back to <a href="https://home.test.local:8080/">home page</a>.
</body>
</html>

View File

@ -8,8 +8,8 @@
<body> <body>
<h1>Access the secret</h1> <h1>Access the secret</h1>
<span style="font-size: 1.2em; color: red">You need to log in to access the secret!</span><br/><br/> <span style="font-size: 1.2em; color: red">You need to log in to access the secret!</span><br/><br/> Try to access it using
Try to access it using one of the following links to test access control powered by Authelia.<br/> one of the following links to test access control powered by Authelia.<br/>
<ul> <ul>
<li> <li>
public.test.local <a href="https://public.test.local:8080/"> / index.html</a> public.test.local <a href="https://public.test.local:8080/"> / index.html</a>
@ -51,17 +51,19 @@
<li> <li>
mx2.main.test.local <a href="https://mx2.mail.test.local:8080/secret.html"> / secret.html</a> mx2.main.test.local <a href="https://mx2.mail.test.local:8080/secret.html"> / secret.html</a>
</li> </li>
<li>
basicauth.test.local <a href="https://basicauth.test.local:8080/secret.html"> / secret.html</a>
</li>
</ul> </ul>
You can also log off by visiting the following <a href="https://auth.test.local:8080/logout?redirect=https://home.test.local:8080/">link</a>. You can also log off by visiting the following <a href="https://auth.test.local:8080/logout?redirect=https://home.test.local:8080/">link</a>.
<h1>List of users</h1> <h1>List of users</h1>
Here is the list of credentials you can log in with to test access control.<br/> Here is the list of credentials you can log in with to test access control.<br/>
<br/> <br/> Once first factor is passed, you will need to follow the links to register a secret for the second factor.<br/> Authelia
Once first factor is passed, you will need to follow the links to register a secret for the second factor.<br/> will send you a fictituous email that will be in the file
Authelia will send you a fictituous email that will be in the file <strong>/tmp/notifications/notification.txt</strong>.<br/> It will provide you with the link to complete the registration
<strong>/tmp/notifications/notification.txt</strong>.<br/> allowing you to authenticate with 2-factor.
It will provide you with the link to complete the registration allowing you to authenticate with 2-factor.
<ul> <ul>
<li><strong>john / password</strong>: belongs to <em>admin</em> and <em>dev</em> groups.</li> <li><strong>john / password</strong>: belongs to <em>admin</em> and <em>dev</em> groups.</li>
@ -70,7 +72,7 @@
</ul> </ul>
<h1>Access control rules</h1> <h1>Access control rules</h1>
<p></p>These rules are extracted from the configuration file <p></p>These rules are extracted from the configuration file
<a href="https://github.com/clems4ever/authelia/blob/master/config.template.yml">config.template.yml</a>.</p> <a href="https://github.com/clems4ever/authelia/blob/master/config.template.yml">config.template.yml</a>.</p>
<pre id="rules" style="border: 1px grey solid; padding: 20px; display: inline-block;"> <pre id="rules" style="border: 1px grey solid; padding: 20px; display: inline-block;">
# Default policy can either be `allow` or `deny`. # Default policy can either be `allow` or `deny`.
@ -129,4 +131,5 @@ users:
resources: resources:
- '^/users/harry/.*$'</pre> - '^/users/harry/.*$'</pre>
</body> </body>
</html> </html>

View File

@ -203,5 +203,42 @@ http {
error_page 403 = https://auth.test.local:8080/error/403; error_page 403 = https://auth.test.local:8080/error/403;
} }
} }
server {
listen 443 ssl;
root /usr/share/nginx/html/basicauth.test.local;
server_name basicauth.test.local;
ssl on;
ssl_certificate /etc/ssl/server.crt;
ssl_certificate_key /etc/ssl/server.key;
location /auth_verify {
internal;
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?only_basic_auth=true;
}
location / {
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;
auth_request_set $groups $upstream_http_remote_groups;
proxy_set_header Remote-Groups $groups;
error_page 401 =302 https://auth.test.local:8080?redirect=$redirect&only_basic_auth=true;
error_page 403 = https://auth.test.local:8080/error/403;
}
}
} }

View File

@ -1,18 +1,27 @@
import BluebirdPromise = require("bluebird"); import BluebirdPromise = require("bluebird");
import Endpoints = require("../../../server/endpoints"); import Endpoints = require("../../../server/endpoints");
import Constants = require("../../../server/constants");
export function validate(username: string, password: string, $: JQueryStatic): BluebirdPromise<void> { export function validate(username: string, password: string,
return new BluebirdPromise<void>(function (resolve, reject) { redirectUrl: string, onlyBasicAuth: boolean, $: JQueryStatic): BluebirdPromise<string> {
$.post(Endpoints.FIRST_FACTOR_POST, { return new BluebirdPromise<string>(function (resolve, reject) {
username: username, const url = Endpoints.FIRST_FACTOR_POST + "?" + Constants.REDIRECT_QUERY_PARAM + "=" + redirectUrl
password: password, + "&" + Constants.ONLY_BASIC_AUTH_QUERY_PARAM + "=" + onlyBasicAuth;
})
.done(function () { $.ajax({
resolve(); method: "POST",
}) url: url,
.fail(function (xhr: JQueryXHR, textStatus: string) { data: {
reject(new Error("Authetication failed. Please check your credentials.")); username: username,
}); password: password,
}); }
})
.done(function (data: { redirect: string }) {
resolve(data.redirect);
})
.fail(function (xhr: JQueryXHR, textStatus: string) {
reject(new Error("Authetication failed. Please check your credentials."));
});
});
} }

View File

@ -3,7 +3,7 @@ import JSLogger = require("js-logger");
import UISelectors = require("./UISelectors"); import UISelectors = require("./UISelectors");
import { Notifier } from "../Notifier"; import { Notifier } from "../Notifier";
import { QueryParametersRetriever } from "../QueryParametersRetriever"; import { QueryParametersRetriever } from "../QueryParametersRetriever";
import Constants = require("../../../server/constants");
import Endpoints = require("../../../server/endpoints"); import Endpoints = require("../../../server/endpoints");
export default function (window: Window, $: JQueryStatic, export default function (window: Window, $: JQueryStatic,
@ -15,20 +15,17 @@ export default function (window: Window, $: JQueryStatic,
const username: string = $(UISelectors.USERNAME_FIELD_ID).val(); const username: string = $(UISelectors.USERNAME_FIELD_ID).val();
const password: string = $(UISelectors.PASSWORD_FIELD_ID).val(); const password: string = $(UISelectors.PASSWORD_FIELD_ID).val();
$(UISelectors.PASSWORD_FIELD_ID).val(""); $(UISelectors.PASSWORD_FIELD_ID).val("");
jslogger.debug("Form submitted");
firstFactorValidator.validate(username, password, $) const redirectUrl = QueryParametersRetriever.get(Constants.REDIRECT_QUERY_PARAM);
const onlyBasicAuth = QueryParametersRetriever.get(Constants.ONLY_BASIC_AUTH_QUERY_PARAM) ? true : false;
firstFactorValidator.validate(username, password, redirectUrl, onlyBasicAuth, $)
.then(onFirstFactorSuccess, onFirstFactorFailure); .then(onFirstFactorSuccess, onFirstFactorFailure);
return false; return false;
} }
function onFirstFactorSuccess() { function onFirstFactorSuccess(redirectUrl: string) {
jslogger.debug("First factor validated."); jslogger.debug("First factor validated.");
const redirectUrl = QueryParametersRetriever.get("redirect"); window.location.href = redirectUrl;
if (redirectUrl)
window.location.href = Endpoints.SECOND_FACTOR_GET + "?redirect="
+ encodeURIComponent(redirectUrl);
else
window.location.href = Endpoints.SECOND_FACTOR_GET;
} }
function onFirstFactorFailure(err: Error) { function onFirstFactorFailure(err: Error) {

View File

@ -8,22 +8,23 @@ import Endpoints = require("../../../server/endpoints");
import Constants = require("./constants"); import Constants = require("./constants");
import { Notifier } from "../Notifier"; import { Notifier } from "../Notifier";
import { QueryParametersRetriever } from "../QueryParametersRetriever"; import { QueryParametersRetriever } from "../QueryParametersRetriever";
import ServerConstants = require("../../../server/constants");
export default function (window: Window, $: JQueryStatic, u2fApi: typeof U2fApi) { export default function (window: Window, $: JQueryStatic, u2fApi: typeof U2fApi) {
const notifierTotp = new Notifier(".notification-totp", $); const notifierTotp = new Notifier(".notification-totp", $);
const notifierU2f = new Notifier(".notification-u2f", $); const notifierU2f = new Notifier(".notification-u2f", $);
function onAuthenticationSuccess(data: any) { function onAuthenticationSuccess(data: any, notifier: Notifier) {
const redirectUrl = QueryParametersRetriever.get("redirect"); const redirectUrl = QueryParametersRetriever.get(ServerConstants.REDIRECT_QUERY_PARAM);
if (redirectUrl) if (redirectUrl)
window.location.href = redirectUrl; window.location.href = redirectUrl;
else else
window.location.href = Endpoints.FIRST_FACTOR_GET; notifier.success("Authentication succeeded. You can now access your services.");
} }
function onSecondFactorTotpSuccess(data: any) { function onSecondFactorTotpSuccess(data: any) {
onAuthenticationSuccess(data); onAuthenticationSuccess(data, notifierTotp);
} }
function onSecondFactorTotpFailure(err: Error) { function onSecondFactorTotpFailure(err: Error) {
@ -31,7 +32,7 @@ export default function (window: Window, $: JQueryStatic, u2fApi: typeof U2fApi)
} }
function onU2fAuthenticationSuccess(data: any) { function onU2fAuthenticationSuccess(data: any) {
onAuthenticationSuccess(data); onAuthenticationSuccess(data, notifierU2f);
} }
function onU2fAuthenticationFailure() { function onU2fAuthenticationFailure() {

View File

@ -0,0 +1,4 @@
export const ONLY_BASIC_AUTH_QUERY_PARAM = "only_basic_auth";
export const REDIRECT_QUERY_PARAM = "redirect";

View File

@ -4,7 +4,7 @@ import BluebirdPromise = require("bluebird");
function replyWithError(res: express.Response, code: number, logger: Winston): (err: Error) => void { function replyWithError(res: express.Response, code: number, logger: Winston): (err: Error) => void {
return function (err: Error): void { return function (err: Error): void {
logger.error("Reply with error %d: %s", code, err); logger.error("Reply with error %d: %s", code, err.stack);
res.status(code); res.status(code);
res.send(); res.send();
}; };

View File

@ -93,6 +93,8 @@ export class AccessController implements IAccessController {
} }
isAccessAllowed(domain: string, resource: string, user: string, groups: string[]): boolean { isAccessAllowed(domain: string, resource: string, user: string, groups: string[]): boolean {
if (!this.configuration) return true;
const allRules = this.getMatchingAllRules(domain, resource); const allRules = this.getMatchingAllRules(domain, resource);
const groupRules = this.getMatchingGroupRules(groups, domain, resource); const groupRules = this.getMatchingGroupRules(groups, domain, resource);
const userRules = this.getMatchingUserRules(user, domain, resource); const userRules = this.getMatchingUserRules(user, domain, resource);

View File

@ -188,7 +188,7 @@ export class Client implements IClient {
this.logger.debug("LDAP: update password of user '%s'", username); this.logger.debug("LDAP: update password of user '%s'", username);
return this.searchUserDn(username) return this.searchUserDn(username)
.then(function (userDN: string) { .then(function (userDN: string) {
this.client.modifyAsync(userDN, change); that.client.modifyAsync(userDN, change);
}) })
.then(function () { .then(function () {
return that.client.unbindAsync(); return that.client.unbindAsync();

View File

@ -12,22 +12,9 @@ export default function (req: express.Request, res: express.Response): BluebirdP
logger.debug("First factor: headers are %s", JSON.stringify(req.headers)); logger.debug("First factor: headers are %s", JSON.stringify(req.headers));
return AuthenticationValidator.validate(req) res.render("firstfactor", {
.then(function () { first_factor_post_endpoint: Endpoints.FIRST_FACTOR_POST,
const redirectUrl = req.query.redirect; reset_password_request_endpoint: Endpoints.RESET_PASSWORD_REQUEST_GET
if (redirectUrl) { });
res.redirect(redirectUrl); return BluebirdPromise.resolve();
return BluebirdPromise.resolve();
}
else {
res.render("already-logged-in", { logout_endpoint: Endpoints.LOGOUT_GET });
return BluebirdPromise.resolve();
}
}, function () {
res.render("firstfactor", {
first_factor_post_endpoint: Endpoints.FIRST_FACTOR_POST,
reset_password_request_endpoint: Endpoints.RESET_PASSWORD_REQUEST_GET
});
return BluebirdPromise.resolve();
});
} }

View File

@ -10,6 +10,7 @@ import Endpoint = require("../../../endpoints");
import ErrorReplies = require("../../ErrorReplies"); import ErrorReplies = require("../../ErrorReplies");
import { ServerVariablesHandler } from "../../ServerVariablesHandler"; import { ServerVariablesHandler } from "../../ServerVariablesHandler";
import AuthenticationSession = require("../../AuthenticationSession"); import AuthenticationSession = require("../../AuthenticationSession");
import Constants = require("../../../constants");
export default function (req: express.Request, res: express.Response): BluebirdPromise<void> { export default function (req: express.Request, res: express.Response): BluebirdPromise<void> {
const username: string = req.body.username; const username: string = req.body.username;
@ -47,6 +48,8 @@ export default function (req: express.Request, res: express.Response): BluebirdP
JSON.stringify(groupsAndEmails)); JSON.stringify(groupsAndEmails));
authSession.userid = username; authSession.userid = username;
authSession.first_factor = true; authSession.first_factor = true;
const redirectUrl = req.query[Constants.REDIRECT_QUERY_PARAM];
const onlyBasicAuth = req.query[Constants.ONLY_BASIC_AUTH_QUERY_PARAM] === "true";
const emails: string[] = groupsAndEmails.emails; const emails: string[] = groupsAndEmails.emails;
const groups: string[] = groupsAndEmails.groups; const groups: string[] = groupsAndEmails.groups;
@ -63,8 +66,25 @@ export default function (req: express.Request, res: express.Response): BluebirdP
logger.debug("1st factor: Mark successful authentication to regulator."); logger.debug("1st factor: Mark successful authentication to regulator.");
regulator.mark(username, true); regulator.mark(username, true);
res.status(204); logger.debug("1st factor: Redirect URL is %s", redirectUrl);
res.send(); logger.debug("1st factor: %s? %s", Constants.ONLY_BASIC_AUTH_QUERY_PARAM, onlyBasicAuth);
if (onlyBasicAuth) {
res.send({
redirect: redirectUrl
});
logger.debug("1st factor: redirect to '%s'", redirectUrl);
}
else {
let newRedirectUrl = Endpoint.SECOND_FACTOR_GET;
if (redirectUrl !== "undefined") {
newRedirectUrl += "?redirect=" + encodeURIComponent(redirectUrl);
}
logger.debug("1st factor: redirect to '%s'", newRedirectUrl, typeof redirectUrl);
res.send({
redirect: newRedirectUrl
});
}
return BluebirdPromise.resolve(); return BluebirdPromise.resolve();
}) })
.catch(exceptions.LdapSearchError, ErrorReplies.replyWithError500(res, logger)) .catch(exceptions.LdapSearchError, ErrorReplies.replyWithError500(res, logger))

View File

@ -10,33 +10,32 @@ import ErrorReplies = require("../../../ErrorReplies");
import Constants = require("./../constants"); import Constants = require("./../constants");
export default function (req: express.Request, res: express.Response): BluebirdPromise<void> { export default function (req: express.Request, res: express.Response): BluebirdPromise<void> {
const logger = ServerVariablesHandler.getLogger(req.app); const logger = ServerVariablesHandler.getLogger(req.app);
const ldapPasswordUpdater = ServerVariablesHandler.getLdapPasswordUpdater(req.app); const ldapPasswordUpdater = ServerVariablesHandler.getLdapPasswordUpdater(req.app);
let authSession: AuthenticationSession.AuthenticationSession; let authSession: AuthenticationSession.AuthenticationSession;
const newPassword = objectPath.get<express.Request, string>(req, "body.password"); const newPassword = objectPath.get<express.Request, string>(req, "body.password");
return AuthenticationSession.get(req) return AuthenticationSession.get(req)
.then(function (_authSession: AuthenticationSession.AuthenticationSession) { .then(function (_authSession) {
authSession = _authSession; authSession = _authSession;
logger.info("POST reset-password: User %s wants to reset his/her password.", logger.info("POST reset-password: User %s wants to reset his/her password.",
authSession.identity_check.userid); authSession.identity_check.userid);
logger.info("POST reset-password: Challenge %s", authSession.identity_check.challenge); logger.info("POST reset-password: Challenge %s", authSession.identity_check.challenge);
if (authSession.identity_check.challenge != Constants.CHALLENGE) { if (authSession.identity_check.challenge != Constants.CHALLENGE) {
res.status(403); res.status(403);
res.send(); res.send();
return BluebirdPromise.reject(new Error("Bad challenge.")); return BluebirdPromise.reject(new Error("Bad challenge."));
} }
return ldapPasswordUpdater.updatePassword(authSession.identity_check.userid, newPassword);
return ldapPasswordUpdater.updatePassword(authSession.identity_check.userid, newPassword); })
}) .then(function () {
.then(function () { logger.info("POST reset-password: Password reset for user '%s'",
logger.info("POST reset-password: Password reset for user '%s'", authSession.identity_check.userid);
authSession.identity_check.userid); AuthenticationSession.reset(req);
AuthenticationSession.reset(req); res.status(204);
res.status(204); res.send();
res.send(); return BluebirdPromise.resolve();
return BluebirdPromise.resolve(); })
}) .catch(ErrorReplies.replyWithError500(res, logger));
.catch(ErrorReplies.replyWithError500(res, logger));
} }

View File

@ -8,35 +8,38 @@ import AuthenticationValidator = require("../../AuthenticationValidator");
import ErrorReplies = require("../../ErrorReplies"); import ErrorReplies = require("../../ErrorReplies");
import { ServerVariablesHandler } from "../../ServerVariablesHandler"; import { ServerVariablesHandler } from "../../ServerVariablesHandler";
import AuthenticationSession = require("../../AuthenticationSession"); import AuthenticationSession = require("../../AuthenticationSession");
import Constants = require("../../../constants");
function verify_filter(req: express.Request, res: express.Response): BluebirdPromise<void> { function verify_filter(req: express.Request, res: express.Response): BluebirdPromise<void> {
const logger = ServerVariablesHandler.getLogger(req.app); const logger = ServerVariablesHandler.getLogger(req.app);
const accessController = ServerVariablesHandler.getAccessController(req.app); const accessController = ServerVariablesHandler.getAccessController(req.app);
let authSession: AuthenticationSession.AuthenticationSession;
return AuthenticationSession.get(req) return AuthenticationSession.get(req)
.then(function (_authSession: AuthenticationSession.AuthenticationSession) { .then(function (authSession) {
authSession = _authSession;
logger.debug("Verify: headers are %s", JSON.stringify(req.headers)); logger.debug("Verify: headers are %s", JSON.stringify(req.headers));
res.set("Redirect", encodeURIComponent("https://" + req.headers["host"] + req.headers["x-original-uri"])); res.set("Redirect", encodeURIComponent("https://" + req.headers["host"] + req.headers["x-original-uri"]));
return AuthenticationValidator.validate(req);
})
.then(function () {
const username = authSession.userid; const username = authSession.userid;
const groups = authSession.groups; const groups = authSession.groups;
const onlyBasicAuth = req.query[Constants.ONLY_BASIC_AUTH_QUERY_PARAM] === "true";
logger.debug("Verify: %s=%s", Constants.ONLY_BASIC_AUTH_QUERY_PARAM, onlyBasicAuth);
const host = objectPath.get<express.Request, string>(req, "headers.host"); const host = objectPath.get<express.Request, string>(req, "headers.host");
const path = objectPath.get<express.Request, string>(req, "headers.x-original-uri"); const path = objectPath.get<express.Request, string>(req, "headers.x-original-uri");
const domain = host.split(":")[0]; const domain = host.split(":")[0];
logger.debug("Verify: domain=%s, path=%s", domain, path);
logger.debug("Verify: user=%s, groups=%s", username, groups.join(","));
if (!authSession.first_factor)
return BluebirdPromise.reject(new exceptions.AccessDeniedError("First factor not validated."));
const isAllowed = accessController.isAccessAllowed(domain, path, username, groups); const isAllowed = accessController.isAccessAllowed(domain, path, username, groups);
if (!isAllowed) return BluebirdPromise.reject( if (!isAllowed) return BluebirdPromise.reject(
new exceptions.DomainAccessDenied("User '" + username + "' does not have access to " + domain)); new exceptions.DomainAccessDenied("User '" + username + "' does not have access to " + domain));
if (!authSession.first_factor || !authSession.second_factor) if (!onlyBasicAuth && !authSession.second_factor)
return BluebirdPromise.reject(new exceptions.AccessDeniedError("First or second factor not validated")); return BluebirdPromise.reject(new exceptions.AccessDeniedError("Second factor not validated."));
res.setHeader("Remote-User", username); res.setHeader("Remote-User", username);
res.setHeader("Remote-Groups", groups.join(",")); res.setHeader("Remote-Groups", groups.join(","));

View File

@ -16,6 +16,7 @@ Feature: User has access restricted access to domains
| https://dev.test.local:8080/users/bob/secret.html | | https://dev.test.local:8080/users/bob/secret.html |
| https://admin.test.local:8080/secret.html | | https://admin.test.local:8080/secret.html |
| https://mx1.mail.test.local:8080/secret.html | | https://mx1.mail.test.local:8080/secret.html |
| https://basicauth.test.local:8080/secret.html |
And I have no access to: And I have no access to:
| url | | url |
| https://mx2.mail.test.local:8080/secret.html | | https://mx2.mail.test.local:8080/secret.html |
@ -39,6 +40,7 @@ Feature: User has access restricted access to domains
| https://admin.test.local:8080/secret.html | | https://admin.test.local:8080/secret.html |
| https://dev.test.local:8080/users/john/secret.html | | https://dev.test.local:8080/users/john/secret.html |
| https://dev.test.local:8080/users/harry/secret.html | | https://dev.test.local:8080/users/harry/secret.html |
| https://basicauth.test.local:8080/secret.html |
@need-registered-user-harry @need-registered-user-harry
Scenario: User harry has restricted access Scenario: User harry has restricted access
@ -59,3 +61,4 @@ Feature: User has access restricted access to domains
| https://dev.test.local:8080/users/john/secret.html | | https://dev.test.local:8080/users/john/secret.html |
| https://mx1.mail.test.local:8080/secret.html | | https://mx1.mail.test.local:8080/secret.html |
| https://mx2.mail.test.local:8080/secret.html | | https://mx2.mail.test.local:8080/secret.html |
| https://basicauth.test.local:8080/secret.html |

View File

@ -0,0 +1,19 @@
Feature: User can access certain subdomains with basic auth
@need-registered-user-john
Scenario: User is redirected to service after first factor if allowed
When I visit "https://auth.test.local:8080/?redirect=https%3A%2F%2Fbasicauth.test.local%3A8080%2Fsecret.html&only_basic_auth=true"
And I login with user "john" and password "password"
Then I'm redirected to "https://basicauth.test.local:8080/secret.html"
@need-registered-user-john
Scenario: Redirection after first factor fails if basic_auth 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&only_basic_auth=true"
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"
@need-registered-user-john
Scenario: User is redirected to second factor after first factor
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"
Then I'm redirected to "https://auth.test.local:8080/secondfactor?redirect=https%3A%2F%2Fadmin.test.local%3A8080%2Fsecret.html"

View File

@ -2,7 +2,7 @@ Feature: User is correctly redirected
Scenario: User is redirected to authelia when he is not authenticated Scenario: User is redirected to authelia when he is not authenticated
When I visit "https://public.test.local:8080" When I visit "https://public.test.local:8080"
Then I'm redirected to "https://auth.test.local:8080/" Then I'm redirected to "https://auth.test.local:8080/?redirect=https%3A%2F%2Fpublic.test.local%3A8080%2F"
@need-registered-user-john @need-registered-user-john
Scenario: User is redirected to home page after several authentication tries Scenario: User is redirected to home page after several authentication tries

View File

@ -14,6 +14,4 @@ Feature: Authelia keeps user sessions despite the application restart
And I login with user "john" and password "password" And I login with user "john" and password "password"
And I use "REGISTERED" as TOTP token handle And I use "REGISTERED" as TOTP token handle
And I click on "TOTP" And I click on "TOTP"
Then I have access to: Then I'm redirected to "https://admin.test.local:8080/secret.html"
| url |
| https://admin.test.local:8080/secret.html |

View File

@ -6,7 +6,7 @@ import Speakeasy = require("speakeasy");
import CustomWorld = require("../support/world"); import CustomWorld = require("../support/world");
Cucumber.defineSupportCode(function ({ Given, When, Then }) { Cucumber.defineSupportCode(function ({ Given, When, Then }) {
When(/^I visit "(https:\/\/[a-zA-Z0-9:%.\/=?-]+)"$/, function (link: string) { When(/^I visit "(https:\/\/[a-zA-Z0-9:%&._\/=?-]+)"$/, function (link: string) {
return this.visit(link); return this.visit(link);
}); });
@ -66,10 +66,7 @@ and I use TOTP token handle {stringInDoubleQuotes}",
function hasAccessToSecret(link: string, that: any) { function hasAccessToSecret(link: string, that: any) {
return that.driver.get(link) return that.driver.get(link)
.then(function () { .then(function () {
return that.driver.findElement(seleniumWebdriver.By.tagName("body")).getText() return that.waitUntilUrlContains(link);
.then(function (body: string) {
Assert(body.indexOf("This is a very important secret!") > -1, body);
});
}); });
} }

View File

@ -12,6 +12,6 @@ Cucumber.defineSupportCode(function ({ Given, When, Then }) {
}); });
Then("I'm redirected to {stringInDoubleQuotes}", function (link: string) { Then("I'm redirected to {stringInDoubleQuotes}", function (link: string) {
return this.driver.wait(seleniumWebdriver.until.urlContains(link), 15000); return this.waitUntilUrlContains(link);
}); });
}); });

View File

@ -55,6 +55,10 @@ function CustomWorld() {
}); });
}; };
this.waitUntilUrlContains = function(url: string) {
return this.driver.wait(seleniumWebdriver.until.urlIs(url), 15000);
};
this.loginWithUserPassword = function (username: string, password: string) { this.loginWithUserPassword = function (username: string, password: string) {
return that.driver.wait(seleniumWebdriver.until.elementLocated(seleniumWebdriver.By.id("username")), 4000) return that.driver.wait(seleniumWebdriver.until.elementLocated(seleniumWebdriver.By.id("username")), 4000)
.then(function () { .then(function () {

View File

@ -7,13 +7,13 @@ import Assert = require("assert");
describe("test FirstFactorValidator", function () { describe("test FirstFactorValidator", function () {
it("should validate first factor successfully", () => { it("should validate first factor successfully", () => {
const postPromise = JQueryMock.JQueryDeferredMock(); const postPromise = JQueryMock.JQueryDeferredMock();
postPromise.done.yields(); postPromise.done.yields({ redirect: "http://redirect" });
postPromise.done.returns(postPromise); postPromise.done.returns(postPromise);
const jqueryMock = JQueryMock.JQueryMock(); const jqueryMock = JQueryMock.JQueryMock();
jqueryMock.jquery.post.returns(postPromise); jqueryMock.jquery.ajax.returns(postPromise);
return FirstFactorValidator.validate("username", "password", jqueryMock.jquery as any); return FirstFactorValidator.validate("username", "password", "http://redirect", false, jqueryMock.jquery as any);
}); });
function should_fail_first_factor_validation(errorMessage: string) { function should_fail_first_factor_validation(errorMessage: string) {
@ -25,9 +25,9 @@ describe("test FirstFactorValidator", function () {
postPromise.done.returns(postPromise); postPromise.done.returns(postPromise);
const jqueryMock = JQueryMock.JQueryMock(); const jqueryMock = JQueryMock.JQueryMock();
jqueryMock.jquery.post.returns(postPromise); jqueryMock.jquery.ajax.returns(postPromise);
return FirstFactorValidator.validate("username", "password", jqueryMock.jquery as any) return FirstFactorValidator.validate("username", "password", "http://redirect", false, jqueryMock.jquery as any)
.then(function () { .then(function () {
return BluebirdPromise.reject(new Error("First factor validation successfully finished while it should have not.")); return BluebirdPromise.reject(new Error("First factor validation successfully finished while it should have not."));
}, function (err: Error) { }, function (err: Error) {

View File

@ -8,346 +8,360 @@ describe("test access control manager", function () {
let accessController: AccessController; let accessController: AccessController;
let configuration: ACLConfiguration; let configuration: ACLConfiguration;
beforeEach(function () { describe("configuration is null", function() {
configuration = { it("should allow access to anything, anywhere for anybody", function() {
default_policy: "deny", configuration = undefined;
any: [], accessController = new AccessController(configuration, winston);
users: {},
groups: {} Assert(accessController.isAccessAllowed("home.test.local", "/", "user1", ["group1", "group2"]));
}; Assert(accessController.isAccessAllowed("home.test.local", "/abc", "user1", ["group1", "group2"]));
accessController = new AccessController(configuration, winston); Assert(accessController.isAccessAllowed("home.test.local", "/", "user2", ["group1", "group2"]));
Assert(accessController.isAccessAllowed("admin.test.local", "/", "user3", ["group3"]));
});
}); });
describe("check access control with default policy to deny", function () { describe("configuration is not null", function () {
beforeEach(function () { beforeEach(function () {
configuration.default_policy = "deny"; configuration = {
default_policy: "deny",
any: [],
users: {},
groups: {}
};
accessController = new AccessController(configuration, winston);
}); });
it("should deny access when no rule is provided", function () { describe("check access control with default policy to deny", function () {
Assert(!accessController.isAccessAllowed("home.example.com", "/", "user1", ["group1"])); beforeEach(function () {
}); configuration.default_policy = "deny";
it("should control access when multiple domain matcher is provided", function () {
configuration.users["user1"] = [{
domain: "*.mail.example.com",
policy: "allow",
resources: [".*"]
}];
Assert(!accessController.isAccessAllowed("home.example.com", "/", "user1", ["group1"]));
Assert(accessController.isAccessAllowed("mx1.mail.example.com", "/", "user1", ["group1"]));
Assert(accessController.isAccessAllowed("mx1.server.mail.example.com", "/", "user1", ["group1"]));
Assert(!accessController.isAccessAllowed("mail.example.com", "/", "user1", ["group1"]));
});
it("should allow access to all resources when resources is not provided", function () {
configuration.users["user1"] = [{
domain: "*.mail.example.com",
policy: "allow"
}];
Assert(!accessController.isAccessAllowed("home.example.com", "/", "user1", ["group1"]));
Assert(accessController.isAccessAllowed("mx1.mail.example.com", "/", "user1", ["group1"]));
Assert(accessController.isAccessAllowed("mx1.server.mail.example.com", "/", "user1", ["group1"]));
Assert(!accessController.isAccessAllowed("mail.example.com", "/", "user1", ["group1"]));
});
describe("check user rules", function () {
it("should allow access when user has a matching allowing rule", function () {
configuration.users["user1"] = [{
domain: "home.example.com",
policy: "allow",
resources: [".*"]
}];
Assert(accessController.isAccessAllowed("home.example.com", "/", "user1", ["group1"]));
Assert(accessController.isAccessAllowed("home.example.com", "/another/resource", "user1", ["group1"]));
Assert(!accessController.isAccessAllowed("another.home.example.com", "/", "user1", ["group1"]));
}); });
it("should deny to other users", function () { it("should deny access when no rule is provided", function () {
configuration.users["user1"] = [{ Assert(!accessController.isAccessAllowed("home.example.com", "/", "user1", ["group1"]));
domain: "home.example.com",
policy: "allow",
resources: [".*"]
}];
Assert(!accessController.isAccessAllowed("home.example.com", "/", "user2", ["group1"]));
Assert(!accessController.isAccessAllowed("home.example.com", "/another/resource", "user2", ["group1"]));
Assert(!accessController.isAccessAllowed("another.home.example.com", "/", "user2", ["group1"]));
}); });
it("should allow user access only to specific resources", function () { it("should control access when multiple domain matcher is provided", function () {
configuration.users["user1"] = [{ configuration.users["user1"] = [{
domain: "home.example.com", domain: "*.mail.example.com",
policy: "allow", policy: "allow",
resources: ["/private/.*", "^/begin", "/end$"] resources: [".*"]
}]; }];
Assert(!accessController.isAccessAllowed("home.example.com", "/", "user1", ["group1"])); Assert(!accessController.isAccessAllowed("home.example.com", "/", "user1", ["group1"]));
Assert(!accessController.isAccessAllowed("home.example.com", "/private", "user1", ["group1"])); Assert(accessController.isAccessAllowed("mx1.mail.example.com", "/", "user1", ["group1"]));
Assert(accessController.isAccessAllowed("home.example.com", "/private/class", "user1", ["group1"])); Assert(accessController.isAccessAllowed("mx1.server.mail.example.com", "/", "user1", ["group1"]));
Assert(accessController.isAccessAllowed("home.example.com", "/middle/private/class", "user1", ["group1"])); Assert(!accessController.isAccessAllowed("mail.example.com", "/", "user1", ["group1"]));
Assert(accessController.isAccessAllowed("home.example.com", "/begin", "user1", ["group1"]));
Assert(!accessController.isAccessAllowed("home.example.com", "/not/begin", "user1", ["group1"]));
Assert(accessController.isAccessAllowed("home.example.com", "/abc/end", "user1", ["group1"]));
Assert(!accessController.isAccessAllowed("home.example.com", "/abc/end/x", "user1", ["group1"]));
}); });
it("should allow access to multiple domains", function () { it("should allow access to all resources when resources is not provided", function () {
configuration.users["user1"] = [{ configuration.users["user1"] = [{
domain: "home.example.com", domain: "*.mail.example.com",
policy: "allow", policy: "allow"
resources: [".*"]
}, {
domain: "home1.example.com",
policy: "allow",
resources: [".*"]
}, {
domain: "home2.example.com",
policy: "deny",
resources: [".*"]
}]; }];
Assert(accessController.isAccessAllowed("home.example.com", "/", "user1", ["group1"])); Assert(!accessController.isAccessAllowed("home.example.com", "/", "user1", ["group1"]));
Assert(accessController.isAccessAllowed("home1.example.com", "/", "user1", ["group1"])); Assert(accessController.isAccessAllowed("mx1.mail.example.com", "/", "user1", ["group1"]));
Assert(!accessController.isAccessAllowed("home2.example.com", "/", "user1", ["group1"])); Assert(accessController.isAccessAllowed("mx1.server.mail.example.com", "/", "user1", ["group1"]));
Assert(!accessController.isAccessAllowed("home3.example.com", "/", "user1", ["group1"])); Assert(!accessController.isAccessAllowed("mail.example.com", "/", "user1", ["group1"]));
}); });
it("should always apply latest rule", function () { describe("check user rules", function () {
configuration.users["user1"] = [{ it("should allow access when user has a matching allowing rule", function () {
domain: "home.example.com", configuration.users["user1"] = [{
policy: "allow", domain: "home.example.com",
resources: ["^/my/.*"] policy: "allow",
}, { resources: [".*"]
domain: "home.example.com", }];
policy: "deny", Assert(accessController.isAccessAllowed("home.example.com", "/", "user1", ["group1"]));
resources: ["^/my/private/.*"] Assert(accessController.isAccessAllowed("home.example.com", "/another/resource", "user1", ["group1"]));
}, { Assert(!accessController.isAccessAllowed("another.home.example.com", "/", "user1", ["group1"]));
domain: "home.example.com", });
policy: "allow",
resources: ["/my/private/resource"]
}];
Assert(accessController.isAccessAllowed("home.example.com", "/my/poney", "user1", ["group1"])); it("should deny to other users", function () {
Assert(!accessController.isAccessAllowed("home.example.com", "/my/private/duck", "user1", ["group1"])); configuration.users["user1"] = [{
Assert(accessController.isAccessAllowed("home.example.com", "/my/private/resource", "user1", ["group1"])); domain: "home.example.com",
policy: "allow",
resources: [".*"]
}];
Assert(!accessController.isAccessAllowed("home.example.com", "/", "user2", ["group1"]));
Assert(!accessController.isAccessAllowed("home.example.com", "/another/resource", "user2", ["group1"]));
Assert(!accessController.isAccessAllowed("another.home.example.com", "/", "user2", ["group1"]));
});
it("should allow user access only to specific resources", function () {
configuration.users["user1"] = [{
domain: "home.example.com",
policy: "allow",
resources: ["/private/.*", "^/begin", "/end$"]
}];
Assert(!accessController.isAccessAllowed("home.example.com", "/", "user1", ["group1"]));
Assert(!accessController.isAccessAllowed("home.example.com", "/private", "user1", ["group1"]));
Assert(accessController.isAccessAllowed("home.example.com", "/private/class", "user1", ["group1"]));
Assert(accessController.isAccessAllowed("home.example.com", "/middle/private/class", "user1", ["group1"]));
Assert(accessController.isAccessAllowed("home.example.com", "/begin", "user1", ["group1"]));
Assert(!accessController.isAccessAllowed("home.example.com", "/not/begin", "user1", ["group1"]));
Assert(accessController.isAccessAllowed("home.example.com", "/abc/end", "user1", ["group1"]));
Assert(!accessController.isAccessAllowed("home.example.com", "/abc/end/x", "user1", ["group1"]));
});
it("should allow access to multiple domains", function () {
configuration.users["user1"] = [{
domain: "home.example.com",
policy: "allow",
resources: [".*"]
}, {
domain: "home1.example.com",
policy: "allow",
resources: [".*"]
}, {
domain: "home2.example.com",
policy: "deny",
resources: [".*"]
}];
Assert(accessController.isAccessAllowed("home.example.com", "/", "user1", ["group1"]));
Assert(accessController.isAccessAllowed("home1.example.com", "/", "user1", ["group1"]));
Assert(!accessController.isAccessAllowed("home2.example.com", "/", "user1", ["group1"]));
Assert(!accessController.isAccessAllowed("home3.example.com", "/", "user1", ["group1"]));
});
it("should always apply latest rule", function () {
configuration.users["user1"] = [{
domain: "home.example.com",
policy: "allow",
resources: ["^/my/.*"]
}, {
domain: "home.example.com",
policy: "deny",
resources: ["^/my/private/.*"]
}, {
domain: "home.example.com",
policy: "allow",
resources: ["/my/private/resource"]
}];
Assert(accessController.isAccessAllowed("home.example.com", "/my/poney", "user1", ["group1"]));
Assert(!accessController.isAccessAllowed("home.example.com", "/my/private/duck", "user1", ["group1"]));
Assert(accessController.isAccessAllowed("home.example.com", "/my/private/resource", "user1", ["group1"]));
});
});
describe("check group rules", function () {
it("should allow access when user is in group having a matching allowing rule", function () {
configuration.groups["group1"] = [{
domain: "home.example.com",
policy: "allow",
resources: ["^/$"]
}];
configuration.groups["group2"] = [{
domain: "home.example.com",
policy: "allow",
resources: ["^/test$"]
}, {
domain: "home.example.com",
policy: "deny",
resources: ["^/private$"]
}];
Assert(accessController.isAccessAllowed("home.example.com", "/", "user1",
["group1", "group2", "group3"]));
Assert(accessController.isAccessAllowed("home.example.com", "/test", "user1",
["group1", "group2", "group3"]));
Assert(!accessController.isAccessAllowed("home.example.com", "/private", "user1",
["group1", "group2", "group3"]));
Assert(!accessController.isAccessAllowed("another.home.example.com", "/", "user1",
["group1", "group2", "group3"]));
});
}); });
}); });
describe("check group rules", function () { describe("check all rules", function () {
it("should allow access when user is in group having a matching allowing rule", function () { it("should control access when all rules are defined", function () {
configuration.groups["group1"] = [{ configuration.any = [{
domain: "home.example.com", domain: "home.example.com",
policy: "allow", policy: "allow",
resources: ["^/$"] resources: ["^/public$"]
}];
configuration.groups["group2"] = [{
domain: "home.example.com",
policy: "allow",
resources: ["^/test$"]
}, { }, {
domain: "home.example.com", domain: "home.example.com",
policy: "deny", policy: "deny",
resources: ["^/private$"] resources: ["^/private$"]
}]; }];
Assert(accessController.isAccessAllowed("home.example.com", "/", "user1", Assert(accessController.isAccessAllowed("home.example.com", "/public", "user1",
["group1", "group2", "group3"]));
Assert(accessController.isAccessAllowed("home.example.com", "/test", "user1",
["group1", "group2", "group3"])); ["group1", "group2", "group3"]));
Assert(!accessController.isAccessAllowed("home.example.com", "/private", "user1", Assert(!accessController.isAccessAllowed("home.example.com", "/private", "user1",
["group1", "group2", "group3"])); ["group1", "group2", "group3"]));
Assert(!accessController.isAccessAllowed("another.home.example.com", "/", "user1", Assert(accessController.isAccessAllowed("home.example.com", "/public", "user4",
["group1", "group2", "group3"])); ["group5"]));
Assert(!accessController.isAccessAllowed("home.example.com", "/private", "user4",
["group5"]));
});
});
describe("check access control with default policy to allow", function () {
beforeEach(function () {
configuration.default_policy = "allow";
});
it("should allow access to anything when no rule is provided", function () {
Assert(accessController.isAccessAllowed("home.example.com", "/", "user1", ["group1"]));
Assert(accessController.isAccessAllowed("home.example.com", "/test", "user1", ["group1"]));
Assert(accessController.isAccessAllowed("home.example.com", "/dev", "user1", ["group1"]));
});
it("should deny access to one resource when defined", function () {
configuration.users["user1"] = [{
domain: "home.example.com",
policy: "deny",
resources: ["/test"]
}];
Assert(accessController.isAccessAllowed("home.example.com", "/", "user1", ["group1"]));
Assert(!accessController.isAccessAllowed("home.example.com", "/test", "user1", ["group1"]));
Assert(accessController.isAccessAllowed("home.example.com", "/dev", "user1", ["group1"]));
});
});
describe("check access control with complete use case", function () {
beforeEach(function () {
configuration.default_policy = "deny";
});
it("should control access of multiple user (real use case)", function () {
// Let say we have three users: admin, john, harry.
// admin is in groups ["admins"]
// john is in groups ["dev", "admin-private"]
// harry is in groups ["dev"]
configuration.any = [{
domain: "home.example.com",
policy: "allow",
resources: ["^/public$", "^/$"]
}];
configuration.groups["dev"] = [{
domain: "home.example.com",
policy: "allow",
resources: ["^/dev/?.*$"]
}];
configuration.groups["admins"] = [{
domain: "home.example.com",
policy: "allow",
resources: [".*"]
}];
configuration.groups["admin-private"] = [{
domain: "home.example.com",
policy: "allow",
resources: ["^/private/?.*"]
}];
configuration.users["john"] = [{
domain: "home.example.com",
policy: "allow",
resources: ["^/private/john$"]
}];
configuration.users["harry"] = [{
domain: "home.example.com",
policy: "allow",
resources: ["^/private/harry"]
}, {
domain: "home.example.com",
policy: "deny",
resources: ["^/dev/b.*$"]
}];
Assert(accessController.isAccessAllowed("home.example.com", "/", "admin", ["admins"]));
Assert(accessController.isAccessAllowed("home.example.com", "/public", "admin", ["admins"]));
Assert(accessController.isAccessAllowed("home.example.com", "/dev", "admin", ["admins"]));
Assert(accessController.isAccessAllowed("home.example.com", "/dev/bob", "admin", ["admins"]));
Assert(accessController.isAccessAllowed("home.example.com", "/admin", "admin", ["admins"]));
Assert(accessController.isAccessAllowed("home.example.com", "/private/josh", "admin", ["admins"]));
Assert(accessController.isAccessAllowed("home.example.com", "/private/john", "admin", ["admins"]));
Assert(accessController.isAccessAllowed("home.example.com", "/private/harry", "admin", ["admins"]));
Assert(accessController.isAccessAllowed("home.example.com", "/", "john", ["dev", "admin-private"]));
Assert(accessController.isAccessAllowed("home.example.com", "/public", "john", ["dev", "admin-private"]));
Assert(accessController.isAccessAllowed("home.example.com", "/dev", "john", ["dev", "admin-private"]));
Assert(accessController.isAccessAllowed("home.example.com", "/dev/bob", "john", ["dev", "admin-private"]));
Assert(!accessController.isAccessAllowed("home.example.com", "/admin", "john", ["dev", "admin-private"]));
Assert(accessController.isAccessAllowed("home.example.com", "/private/josh", "john", ["dev", "admin-private"]));
Assert(accessController.isAccessAllowed("home.example.com", "/private/john", "john", ["dev", "admin-private"]));
Assert(accessController.isAccessAllowed("home.example.com", "/private/harry", "john", ["dev", "admin-private"]));
Assert(accessController.isAccessAllowed("home.example.com", "/", "harry", ["dev"]));
Assert(accessController.isAccessAllowed("home.example.com", "/public", "harry", ["dev"]));
Assert(accessController.isAccessAllowed("home.example.com", "/dev", "harry", ["dev"]));
Assert(!accessController.isAccessAllowed("home.example.com", "/dev/bob", "harry", ["dev"]));
Assert(!accessController.isAccessAllowed("home.example.com", "/admin", "harry", ["dev"]));
Assert(!accessController.isAccessAllowed("home.example.com", "/private/josh", "harry", ["dev"]));
Assert(!accessController.isAccessAllowed("home.example.com", "/private/john", "harry", ["dev"]));
Assert(accessController.isAccessAllowed("home.example.com", "/private/harry", "harry", ["dev"]));
});
it("should control access when allowed at group level and denied at user level", function () {
configuration.groups["dev"] = [{
domain: "home.example.com",
policy: "allow",
resources: ["^/dev/?.*$"]
}];
configuration.users["john"] = [{
domain: "home.example.com",
policy: "deny",
resources: ["^/dev/bob$"]
}];
Assert(accessController.isAccessAllowed("home.example.com", "/dev/john", "john", ["dev"]));
Assert(!accessController.isAccessAllowed("home.example.com", "/dev/bob", "john", ["dev"]));
});
it("should control access when allowed at all level and denied at user level", function () {
configuration.any = [{
domain: "home.example.com",
policy: "allow",
resources: ["^/dev/?.*$"]
}];
configuration.users["john"] = [{
domain: "home.example.com",
policy: "deny",
resources: ["^/dev/bob$"]
}];
Assert(accessController.isAccessAllowed("home.example.com", "/dev/john", "john", ["dev"]));
Assert(!accessController.isAccessAllowed("home.example.com", "/dev/bob", "john", ["dev"]));
});
it("should control access when allowed at all level and denied at group level", function () {
configuration.any = [{
domain: "home.example.com",
policy: "allow",
resources: ["^/dev/?.*$"]
}];
configuration.groups["dev"] = [{
domain: "home.example.com",
policy: "deny",
resources: ["^/dev/bob$"]
}];
Assert(accessController.isAccessAllowed("home.example.com", "/dev/john", "john", ["dev"]));
Assert(!accessController.isAccessAllowed("home.example.com", "/dev/bob", "john", ["dev"]));
});
it("should respect rules precedence", function () {
// the priority from least to most is 'default_policy', 'all', 'group', 'user'
// and the first rules in each category as a lower priority than the latest.
// You can think of it that way: they override themselves inside each category.
configuration.any = [{
domain: "home.example.com",
policy: "allow",
resources: ["^/dev/?.*$"]
}];
configuration.groups["dev"] = [{
domain: "home.example.com",
policy: "deny",
resources: ["^/dev/bob$"]
}];
configuration.users["john"] = [{
domain: "home.example.com",
policy: "allow",
resources: ["^/dev/?.*$"]
}];
Assert(accessController.isAccessAllowed("home.example.com", "/dev/john", "john", ["dev"]));
Assert(accessController.isAccessAllowed("home.example.com", "/dev/bob", "john", ["dev"]));
}); });
}); });
}); });
describe("check all rules", function () {
it("should control access when all rules are defined", function () {
configuration.any = [{
domain: "home.example.com",
policy: "allow",
resources: ["^/public$"]
}, {
domain: "home.example.com",
policy: "deny",
resources: ["^/private$"]
}];
Assert(accessController.isAccessAllowed("home.example.com", "/public", "user1",
["group1", "group2", "group3"]));
Assert(!accessController.isAccessAllowed("home.example.com", "/private", "user1",
["group1", "group2", "group3"]));
Assert(accessController.isAccessAllowed("home.example.com", "/public", "user4",
["group5"]));
Assert(!accessController.isAccessAllowed("home.example.com", "/private", "user4",
["group5"]));
});
});
describe("check access control with default policy to allow", function () {
beforeEach(function () {
configuration.default_policy = "allow";
});
it("should allow access to anything when no rule is provided", function () {
Assert(accessController.isAccessAllowed("home.example.com", "/", "user1", ["group1"]));
Assert(accessController.isAccessAllowed("home.example.com", "/test", "user1", ["group1"]));
Assert(accessController.isAccessAllowed("home.example.com", "/dev", "user1", ["group1"]));
});
it("should deny access to one resource when defined", function () {
configuration.users["user1"] = [{
domain: "home.example.com",
policy: "deny",
resources: ["/test"]
}];
Assert(accessController.isAccessAllowed("home.example.com", "/", "user1", ["group1"]));
Assert(!accessController.isAccessAllowed("home.example.com", "/test", "user1", ["group1"]));
Assert(accessController.isAccessAllowed("home.example.com", "/dev", "user1", ["group1"]));
});
});
describe("check access control with complete use case", function () {
beforeEach(function () {
configuration.default_policy = "deny";
});
it("should control access of multiple user (real use case)", function () {
// Let say we have three users: admin, john, harry.
// admin is in groups ["admins"]
// john is in groups ["dev", "admin-private"]
// harry is in groups ["dev"]
configuration.any = [{
domain: "home.example.com",
policy: "allow",
resources: ["^/public$", "^/$"]
}];
configuration.groups["dev"] = [{
domain: "home.example.com",
policy: "allow",
resources: ["^/dev/?.*$"]
}];
configuration.groups["admins"] = [{
domain: "home.example.com",
policy: "allow",
resources: [".*"]
}];
configuration.groups["admin-private"] = [{
domain: "home.example.com",
policy: "allow",
resources: ["^/private/?.*"]
}];
configuration.users["john"] = [{
domain: "home.example.com",
policy: "allow",
resources: ["^/private/john$"]
}];
configuration.users["harry"] = [{
domain: "home.example.com",
policy: "allow",
resources: ["^/private/harry"]
}, {
domain: "home.example.com",
policy: "deny",
resources: ["^/dev/b.*$"]
}];
Assert(accessController.isAccessAllowed("home.example.com", "/", "admin", ["admins"]));
Assert(accessController.isAccessAllowed("home.example.com", "/public", "admin", ["admins"]));
Assert(accessController.isAccessAllowed("home.example.com", "/dev", "admin", ["admins"]));
Assert(accessController.isAccessAllowed("home.example.com", "/dev/bob", "admin", ["admins"]));
Assert(accessController.isAccessAllowed("home.example.com", "/admin", "admin", ["admins"]));
Assert(accessController.isAccessAllowed("home.example.com", "/private/josh", "admin", ["admins"]));
Assert(accessController.isAccessAllowed("home.example.com", "/private/john", "admin", ["admins"]));
Assert(accessController.isAccessAllowed("home.example.com", "/private/harry", "admin", ["admins"]));
Assert(accessController.isAccessAllowed("home.example.com", "/", "john", ["dev", "admin-private"]));
Assert(accessController.isAccessAllowed("home.example.com", "/public", "john", ["dev", "admin-private"]));
Assert(accessController.isAccessAllowed("home.example.com", "/dev", "john", ["dev", "admin-private"]));
Assert(accessController.isAccessAllowed("home.example.com", "/dev/bob", "john", ["dev", "admin-private"]));
Assert(!accessController.isAccessAllowed("home.example.com", "/admin", "john", ["dev", "admin-private"]));
Assert(accessController.isAccessAllowed("home.example.com", "/private/josh", "john", ["dev", "admin-private"]));
Assert(accessController.isAccessAllowed("home.example.com", "/private/john", "john", ["dev", "admin-private"]));
Assert(accessController.isAccessAllowed("home.example.com", "/private/harry", "john", ["dev", "admin-private"]));
Assert(accessController.isAccessAllowed("home.example.com", "/", "harry", ["dev"]));
Assert(accessController.isAccessAllowed("home.example.com", "/public", "harry", ["dev"]));
Assert(accessController.isAccessAllowed("home.example.com", "/dev", "harry", ["dev"]));
Assert(!accessController.isAccessAllowed("home.example.com", "/dev/bob", "harry", ["dev"]));
Assert(!accessController.isAccessAllowed("home.example.com", "/admin", "harry", ["dev"]));
Assert(!accessController.isAccessAllowed("home.example.com", "/private/josh", "harry", ["dev"]));
Assert(!accessController.isAccessAllowed("home.example.com", "/private/john", "harry", ["dev"]));
Assert(accessController.isAccessAllowed("home.example.com", "/private/harry", "harry", ["dev"]));
});
it("should control access when allowed at group level and denied at user level", function () {
configuration.groups["dev"] = [{
domain: "home.example.com",
policy: "allow",
resources: ["^/dev/?.*$"]
}];
configuration.users["john"] = [{
domain: "home.example.com",
policy: "deny",
resources: ["^/dev/bob$"]
}];
Assert(accessController.isAccessAllowed("home.example.com", "/dev/john", "john", ["dev"]));
Assert(!accessController.isAccessAllowed("home.example.com", "/dev/bob", "john", ["dev"]));
});
it("should control access when allowed at all level and denied at user level", function () {
configuration.any = [{
domain: "home.example.com",
policy: "allow",
resources: ["^/dev/?.*$"]
}];
configuration.users["john"] = [{
domain: "home.example.com",
policy: "deny",
resources: ["^/dev/bob$"]
}];
Assert(accessController.isAccessAllowed("home.example.com", "/dev/john", "john", ["dev"]));
Assert(!accessController.isAccessAllowed("home.example.com", "/dev/bob", "john", ["dev"]));
});
it("should control access when allowed at all level and denied at group level", function () {
configuration.any = [{
domain: "home.example.com",
policy: "allow",
resources: ["^/dev/?.*$"]
}];
configuration.groups["dev"] = [{
domain: "home.example.com",
policy: "deny",
resources: ["^/dev/bob$"]
}];
Assert(accessController.isAccessAllowed("home.example.com", "/dev/john", "john", ["dev"]));
Assert(!accessController.isAccessAllowed("home.example.com", "/dev/bob", "john", ["dev"]));
});
it("should respect rules precedence", function () {
// the priority from least to most is 'default_policy', 'all', 'group', 'user'
// and the first rules in each category as a lower priority than the latest.
// You can think of it that way: they override themselves inside each category.
configuration.any = [{
domain: "home.example.com",
policy: "allow",
resources: ["^/dev/?.*$"]
}];
configuration.groups["dev"] = [{
domain: "home.example.com",
policy: "deny",
resources: ["^/dev/bob$"]
}];
configuration.users["john"] = [{
domain: "home.example.com",
policy: "allow",
resources: ["^/dev/?.*$"]
}];
Assert(accessController.isAccessAllowed("home.example.com", "/dev/john", "john", ["dev"]));
Assert(accessController.isAccessAllowed("home.example.com", "/dev/bob", "john", ["dev"]));
});
});
}); });

View File

@ -1,40 +1,40 @@
import sinon = require("sinon"); import Sinon = require("sinon");
import express = require("express"); import express = require("express");
import winston = require("winston"); import winston = require("winston");
import { UserDataStoreStub } from "./storage/UserDataStoreStub"; import { UserDataStoreStub } from "./storage/UserDataStoreStub";
import { VARIABLES_KEY }  from "../../../../src/server/lib/ServerVariablesHandler"; import { VARIABLES_KEY } from "../../../../src/server/lib/ServerVariablesHandler";
export interface ServerVariablesMock { export interface ServerVariablesMock {
logger: any; logger: any;
ldapAuthenticator: any; ldapAuthenticator: any;
ldapEmailsRetriever: any; ldapEmailsRetriever: any;
ldapPasswordUpdater: any; ldapPasswordUpdater: any;
totpValidator: any; totpValidator: any;
totpGenerator: any; totpGenerator: any;
u2f: any; u2f: any;
userDataStore: UserDataStoreStub; userDataStore: UserDataStoreStub;
notifier: any; notifier: any;
regulator: any; regulator: any;
config: any; config: any;
accessController: any; accessController: any;
} }
export function mock(app: express.Application): ServerVariablesMock { export function mock(app: express.Application): ServerVariablesMock {
const mocks: ServerVariablesMock = { const mocks: ServerVariablesMock = {
accessController: sinon.stub(), accessController: Sinon.stub(),
config: sinon.stub(), config: Sinon.stub(),
ldapAuthenticator: sinon.stub() as any, ldapAuthenticator: Sinon.stub() as any,
ldapEmailsRetriever: sinon.stub() as any, ldapEmailsRetriever: Sinon.stub() as any,
ldapPasswordUpdater: sinon.stub() as any, ldapPasswordUpdater: Sinon.stub() as any,
logger: winston, logger: winston,
notifier: sinon.stub(), notifier: Sinon.stub(),
regulator: sinon.stub(), regulator: Sinon.stub(),
totpGenerator: sinon.stub(), totpGenerator: Sinon.stub(),
totpValidator: sinon.stub(), totpValidator: Sinon.stub(),
u2f: sinon.stub(), u2f: Sinon.stub(),
userDataStore: new UserDataStoreStub() userDataStore: new UserDataStoreStub()
}; };
app.get = sinon.stub().withArgs(VARIABLES_KEY).returns(mocks); app.get = Sinon.stub().withArgs(VARIABLES_KEY).returns(mocks);
return mocks; return mocks;
} }

View File

@ -51,6 +51,9 @@ describe("test the first factor validation route", function () {
username: "username", username: "username",
password: "password" password: "password"
}, },
query: {
redirect: "http://redirect.url"
},
session: { session: {
}, },
headers: { headers: {
@ -87,7 +90,6 @@ describe("test the first factor validation route", function () {
.then(function () { .then(function () {
assert.equal("username", authSession.userid); assert.equal("username", authSession.userid);
assert(res.send.calledOnce); assert(res.send.calledOnce);
assert(res.status.calledWith(204));
}); });
}); });

View File

@ -1,9 +1,9 @@
import assert = require("assert"); import Assert = require("assert");
import VerifyGet = require("../../../../../src/server/lib/routes/verify/get"); import VerifyGet = require("../../../../../src/server/lib/routes/verify/get");
import AuthenticationSession = require("../../../../../src/server/lib/AuthenticationSession"); import AuthenticationSession = require("../../../../../src/server/lib/AuthenticationSession");
import sinon = require("sinon"); import Sinon = require("sinon");
import winston = require("winston"); import winston = require("winston");
import BluebirdPromise = require("bluebird"); import BluebirdPromise = require("bluebird");
@ -24,10 +24,13 @@ describe("test authentication token verification", function () {
req = ExpressMock.RequestMock(); req = ExpressMock.RequestMock();
res = ExpressMock.ResponseMock(); res = ExpressMock.ResponseMock();
req.app = {
get: sinon.stub().returns({ logger: winston })
};
req.session = {}; req.session = {};
req.query = {
redirect: "http://redirect.url"
};
req.app = {
get: Sinon.stub().returns({ logger: winston })
};
AuthenticationSession.reset(req as any); AuthenticationSession.reset(req as any);
req.headers = {}; req.headers = {};
req.headers.host = "secret.example.com"; req.headers.host = "secret.example.com";
@ -49,95 +52,133 @@ describe("test authentication token verification", function () {
return VerifyGet.default(req as express.Request, res as any); return VerifyGet.default(req as express.Request, res as any);
}) })
.then(function () { .then(function () {
sinon.assert.calledWithExactly(res.setHeader, "Remote-User", "myuser"); Sinon.assert.calledWithExactly(res.setHeader, "Remote-User", "myuser");
sinon.assert.calledWithExactly(res.setHeader, "Remote-Groups", "mygroup,othergroup"); Sinon.assert.calledWithExactly(res.setHeader, "Remote-Groups", "mygroup,othergroup");
assert.equal(204, res.status.getCall(0).args[0]); Assert.equal(204, res.status.getCall(0).args[0]);
}); });
}); });
describe("given different cases of session", function () { function test_session(_authSession: AuthenticationSession.AuthenticationSession, status_code: number) {
function test_session(auth_session: AuthenticationSession.AuthenticationSession, status_code: number) { return AuthenticationSession.get(req as any)
return VerifyGet.default(req as express.Request, res as any) .then(function (authSession) {
.then(function () { authSession = _authSession;
assert.equal(status_code, res.status.getCall(0).args[0]); return VerifyGet.default(req as express.Request, res as any);
})
.then(function () {
Assert.equal(status_code, res.status.getCall(0).args[0]);
});
}
function test_non_authenticated_401(auth_session: AuthenticationSession.AuthenticationSession) {
return test_session(auth_session, 401);
}
function test_unauthorized_403(auth_session: AuthenticationSession.AuthenticationSession) {
return test_session(auth_session, 403);
}
function test_authorized(auth_session: AuthenticationSession.AuthenticationSession) {
return test_session(auth_session, 204);
}
describe("given user tries to access a 2-factor endpoint", function () {
describe("given different cases of session", function () {
it("should not be authenticated when second factor is missing", function () {
return test_non_authenticated_401({
userid: "user",
first_factor: true,
second_factor: false,
email: undefined,
groups: [],
}); });
} });
function test_non_authenticated_401(auth_session: AuthenticationSession.AuthenticationSession) { it("should not be authenticated when first factor is missing", function () {
return test_session(auth_session, 401); return test_non_authenticated_401({
} userid: "user",
first_factor: false,
second_factor: true,
email: undefined,
groups: [],
});
});
function test_unauthorized_403(auth_session: AuthenticationSession.AuthenticationSession) { it("should not be authenticated when userid is missing", function () {
return test_session(auth_session, 403); return test_non_authenticated_401({
} userid: undefined,
first_factor: true,
second_factor: false,
email: undefined,
groups: [],
});
});
function test_authorized(auth_session: AuthenticationSession.AuthenticationSession) { it("should not be authenticated when first and second factor are missing", function () {
return test_session(auth_session, 204); return test_non_authenticated_401({
} userid: "user",
first_factor: false,
second_factor: false,
email: undefined,
groups: [],
});
});
it("should not be authenticated when second factor is missing", function () { it("should not be authenticated when session has not be initiated", function () {
return test_non_authenticated_401({ return test_non_authenticated_401(undefined);
userid: "user", });
first_factor: true,
second_factor: false, it("should not be authenticated when domain is not allowed for user", function () {
email: undefined, return AuthenticationSession.get(req as any)
groups: [], .then(function (authSession: AuthenticationSession.AuthenticationSession) {
authSession.first_factor = true;
authSession.second_factor = true;
authSession.userid = "myuser";
req.headers.host = "test.example.com";
accessController.isAccessAllowedMock.returns(false);
accessController.isAccessAllowedMock.withArgs("test.example.com", "user", ["group1", "group2"]).returns(true);
return test_unauthorized_403({
first_factor: true,
second_factor: true,
userid: "user",
groups: ["group1", "group2"],
email: undefined
});
});
}); });
}); });
});
it("should not be authenticated when first factor is missing", function () { describe("given user tries to access a basic auth endpoint", function () {
return test_non_authenticated_401({ beforeEach(function () {
userid: "user", req.query = {
first_factor: false, redirect: "http://redirect.url",
second_factor: true, only_basic_auth: "true"
email: undefined, };
groups: [],
});
}); });
it("should not be authenticated when userid is missing", function () { it("should be authenticated when first factor is validated and not second factor", function () {
return test_non_authenticated_401({
userid: undefined,
first_factor: true,
second_factor: false,
email: undefined,
groups: [],
});
});
it("should not be authenticated when first and second factor are missing", function () {
return test_non_authenticated_401({
userid: "user",
first_factor: false,
second_factor: false,
email: undefined,
groups: [],
});
});
it("should not be authenticated when session has not be initiated", function () {
return test_non_authenticated_401(undefined);
});
it("should not be authenticated when domain is not allowed for user", function () {
return AuthenticationSession.get(req as any) return AuthenticationSession.get(req as any)
.then(function (authSession: AuthenticationSession.AuthenticationSession) { .then(function (authSession: AuthenticationSession.AuthenticationSession) {
authSession.first_factor = true; authSession.first_factor = true;
authSession.second_factor = true; return VerifyGet.default(req as express.Request, res as any);
authSession.userid = "myuser"; })
.then(function () {
Assert(res.status.calledWith(204));
Assert(res.send.calledOnce);
});
});
req.headers.host = "test.example.com"; it("should be rejected with 401 when first factor is not validated", function () {
return AuthenticationSession.get(req as any)
accessController.isAccessAllowedMock.returns(false); .then(function (authSession: AuthenticationSession.AuthenticationSession) {
accessController.isAccessAllowedMock.withArgs("test.example.com", "user", ["group1", "group2"]).returns(true); authSession.first_factor = false;
return VerifyGet.default(req as express.Request, res as any);
return test_unauthorized_403({ })
first_factor: true, .then(function () {
second_factor: true, Assert(res.status.calledWith(401));
userid: "user",
groups: ["group1", "group2"],
email: undefined
});
}); });
}); });
}); });