From 4cbf6efa421468ae4765d30f48ad38562a5e2dc7 Mon Sep 17 00:00:00 2001 From: Clement Michaud Date: Sun, 24 Sep 2017 23:19:03 +0200 Subject: [PATCH] Disable second factor for certain subdomain --- .travis.yml | 15 +- Gruntfile.js | 2 +- docker-compose.dev.yml | 2 +- .../html/basicauth.test.local/secret.html | 10 + example/nginx/html/home.test.local/index.html | 19 +- example/nginx/nginx.conf | 37 ++ .../lib/firstfactor/FirstFactorValidator.ts | 35 +- src/client/lib/firstfactor/index.ts | 17 +- src/client/lib/secondfactor/index.ts | 11 +- src/server/constants.ts | 4 + src/server/lib/ErrorReplies.ts | 2 +- .../lib/access_control/AccessController.ts | 2 + src/server/lib/ldap/Client.ts | 2 +- src/server/lib/routes/firstfactor/get.ts | 23 +- src/server/lib/routes/firstfactor/post.ts | 24 +- .../lib/routes/password-reset/form/post.ts | 53 +- src/server/lib/routes/verify/get.ts | 21 +- test/features/access-control.feature | 3 + test/features/basic-auth.feature | 19 + test/features/redirection.feature | 2 +- test/features/resilience.feature | 4 +- .../step_definitions/authentication.ts | 7 +- test/features/step_definitions/redirection.ts | 2 +- test/features/support/world.ts | 4 + .../firstfactor/FirstFactorValidator.test.ts | 10 +- .../access_control/AccessController.test.ts | 626 +++++++++--------- test/unit/server/mocks/ServerVariablesMock.ts | 60 +- .../server/routes/firstfactor/post.test.ts | 4 +- test/unit/server/routes/verify/get.test.ts | 195 +++--- 29 files changed, 683 insertions(+), 532 deletions(-) create mode 100644 example/nginx/html/basicauth.test.local/secret.html create mode 100644 src/server/constants.ts create mode 100644 test/features/basic-auth.feature diff --git a/.travis.yml b/.travis.yml index ab6c78eab..61a611250 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,13 +15,14 @@ addons: - libgif-dev - google-chrome-stable hosts: - - auth.test.local - - home.test.local - - public.test.local - - admin.test.local - - dev.test.local - - mx1.mail.test.local - - mx2.mail.test.local + - admin.test.local + - auth.test.local + - basicauth.test.local + - dev.test.local + - home.test.local + - mx1.mail.test.local + - mx2.mail.test.local + - public.test.local before_install: - npm install -g npm@'>=2.13.5' diff --git a/Gruntfile.js b/Gruntfile.js index d8593e43a..1dd853675 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -26,7 +26,7 @@ module.exports = function (grunt) { }, "docker-restart": { cmd: "./scripts/dc-dev.sh", - args: ['up', '-d'] + args: ['restart', 'authelia'] }, "minify": { cmd: "./node_modules/.bin/uglifyjs", diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 6f48df1f4..196ecac5f 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -5,6 +5,6 @@ services: - ./test:/usr/src/test - ./dist/src/server:/usr/src - ./node_modules:/usr/src/node_modules - - ./config.yml:/etc/authelia/config.yml:ro + - ./config.template.yml:/etc/authelia/config.yml:ro networks: - example-network diff --git a/example/nginx/html/basicauth.test.local/secret.html b/example/nginx/html/basicauth.test.local/secret.html new file mode 100644 index 000000000..386bd8931 --- /dev/null +++ b/example/nginx/html/basicauth.test.local/secret.html @@ -0,0 +1,10 @@ + + + Secret + + + + This is a very important secret!
+ Go back to home page. + + diff --git a/example/nginx/html/home.test.local/index.html b/example/nginx/html/home.test.local/index.html index a96dee5cc..ad0f2069b 100644 --- a/example/nginx/html/home.test.local/index.html +++ b/example/nginx/html/home.test.local/index.html @@ -8,8 +8,8 @@

Access the secret

- You need to log in to access the secret!

- Try to access it using one of the following links to test access control powered by Authelia.
+ You need to log in to access the secret!

Try to access it using + one of the following links to test access control powered by Authelia.
You can also log off by visiting the following link.

List of users

Here is the list of credentials you can log in with to test access control.
-
- Once first factor is passed, you will need to follow the links to register a secret for the second factor.
- Authelia will send you a fictituous email that will be in the file - /tmp/notifications/notification.txt.
- It will provide you with the link to complete the registration allowing you to authenticate with 2-factor. +
Once first factor is passed, you will need to follow the links to register a secret for the second factor.
Authelia + will send you a fictituous email that will be in the file + /tmp/notifications/notification.txt.
It will provide you with the link to complete the registration + allowing you to authenticate with 2-factor.

Access control rules

-

These rules are extracted from the configuration file +

These rules are extracted from the configuration file config.template.yml.

 # Default policy can either be `allow` or `deny`.
@@ -129,4 +131,5 @@ users:
       resources:
         - '^/users/harry/.*$'
+ \ No newline at end of file diff --git a/example/nginx/nginx.conf b/example/nginx/nginx.conf index cf8c01914..5a19ec5ff 100644 --- a/example/nginx/nginx.conf +++ b/example/nginx/nginx.conf @@ -203,5 +203,42 @@ http { 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; + } + } } diff --git a/src/client/lib/firstfactor/FirstFactorValidator.ts b/src/client/lib/firstfactor/FirstFactorValidator.ts index 71ebc7ab4..32dc421c8 100644 --- a/src/client/lib/firstfactor/FirstFactorValidator.ts +++ b/src/client/lib/firstfactor/FirstFactorValidator.ts @@ -1,18 +1,27 @@ import BluebirdPromise = require("bluebird"); import Endpoints = require("../../../server/endpoints"); +import Constants = require("../../../server/constants"); -export function validate(username: string, password: string, $: JQueryStatic): BluebirdPromise { - return new BluebirdPromise(function (resolve, reject) { - $.post(Endpoints.FIRST_FACTOR_POST, { - username: username, - password: password, - }) - .done(function () { - resolve(); - }) - .fail(function (xhr: JQueryXHR, textStatus: string) { - reject(new Error("Authetication failed. Please check your credentials.")); - }); - }); +export function validate(username: string, password: string, + redirectUrl: string, onlyBasicAuth: boolean, $: JQueryStatic): BluebirdPromise { + return new BluebirdPromise(function (resolve, reject) { + const url = Endpoints.FIRST_FACTOR_POST + "?" + Constants.REDIRECT_QUERY_PARAM + "=" + redirectUrl + + "&" + Constants.ONLY_BASIC_AUTH_QUERY_PARAM + "=" + onlyBasicAuth; + + $.ajax({ + method: "POST", + url: url, + data: { + 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.")); + }); + }); } diff --git a/src/client/lib/firstfactor/index.ts b/src/client/lib/firstfactor/index.ts index 1dfb03154..a4fd48299 100644 --- a/src/client/lib/firstfactor/index.ts +++ b/src/client/lib/firstfactor/index.ts @@ -3,7 +3,7 @@ import JSLogger = require("js-logger"); import UISelectors = require("./UISelectors"); import { Notifier } from "../Notifier"; import { QueryParametersRetriever } from "../QueryParametersRetriever"; - +import Constants = require("../../../server/constants"); import Endpoints = require("../../../server/endpoints"); 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 password: string = $(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); return false; } - function onFirstFactorSuccess() { + function onFirstFactorSuccess(redirectUrl: string) { jslogger.debug("First factor validated."); - const redirectUrl = QueryParametersRetriever.get("redirect"); - if (redirectUrl) - window.location.href = Endpoints.SECOND_FACTOR_GET + "?redirect=" - + encodeURIComponent(redirectUrl); - else - window.location.href = Endpoints.SECOND_FACTOR_GET; + window.location.href = redirectUrl; } function onFirstFactorFailure(err: Error) { diff --git a/src/client/lib/secondfactor/index.ts b/src/client/lib/secondfactor/index.ts index ca5aea4b8..4fe9ae940 100644 --- a/src/client/lib/secondfactor/index.ts +++ b/src/client/lib/secondfactor/index.ts @@ -8,22 +8,23 @@ import Endpoints = require("../../../server/endpoints"); import Constants = require("./constants"); import { Notifier } from "../Notifier"; import { QueryParametersRetriever } from "../QueryParametersRetriever"; +import ServerConstants = require("../../../server/constants"); export default function (window: Window, $: JQueryStatic, u2fApi: typeof U2fApi) { const notifierTotp = new Notifier(".notification-totp", $); const notifierU2f = new Notifier(".notification-u2f", $); - function onAuthenticationSuccess(data: any) { - const redirectUrl = QueryParametersRetriever.get("redirect"); + function onAuthenticationSuccess(data: any, notifier: Notifier) { + const redirectUrl = QueryParametersRetriever.get(ServerConstants.REDIRECT_QUERY_PARAM); if (redirectUrl) window.location.href = redirectUrl; else - window.location.href = Endpoints.FIRST_FACTOR_GET; + notifier.success("Authentication succeeded. You can now access your services."); } function onSecondFactorTotpSuccess(data: any) { - onAuthenticationSuccess(data); + onAuthenticationSuccess(data, notifierTotp); } function onSecondFactorTotpFailure(err: Error) { @@ -31,7 +32,7 @@ export default function (window: Window, $: JQueryStatic, u2fApi: typeof U2fApi) } function onU2fAuthenticationSuccess(data: any) { - onAuthenticationSuccess(data); + onAuthenticationSuccess(data, notifierU2f); } function onU2fAuthenticationFailure() { diff --git a/src/server/constants.ts b/src/server/constants.ts new file mode 100644 index 000000000..154508de9 --- /dev/null +++ b/src/server/constants.ts @@ -0,0 +1,4 @@ + + +export const ONLY_BASIC_AUTH_QUERY_PARAM = "only_basic_auth"; +export const REDIRECT_QUERY_PARAM = "redirect"; \ No newline at end of file diff --git a/src/server/lib/ErrorReplies.ts b/src/server/lib/ErrorReplies.ts index 24a400310..ae8d2ecbb 100644 --- a/src/server/lib/ErrorReplies.ts +++ b/src/server/lib/ErrorReplies.ts @@ -4,7 +4,7 @@ import BluebirdPromise = require("bluebird"); function replyWithError(res: express.Response, code: number, logger: Winston): (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.send(); }; diff --git a/src/server/lib/access_control/AccessController.ts b/src/server/lib/access_control/AccessController.ts index 9408768c2..192153a6e 100644 --- a/src/server/lib/access_control/AccessController.ts +++ b/src/server/lib/access_control/AccessController.ts @@ -93,6 +93,8 @@ export class AccessController implements IAccessController { } isAccessAllowed(domain: string, resource: string, user: string, groups: string[]): boolean { + if (!this.configuration) return true; + const allRules = this.getMatchingAllRules(domain, resource); const groupRules = this.getMatchingGroupRules(groups, domain, resource); const userRules = this.getMatchingUserRules(user, domain, resource); diff --git a/src/server/lib/ldap/Client.ts b/src/server/lib/ldap/Client.ts index 4d457ec08..ea3da1b60 100644 --- a/src/server/lib/ldap/Client.ts +++ b/src/server/lib/ldap/Client.ts @@ -188,7 +188,7 @@ export class Client implements IClient { this.logger.debug("LDAP: update password of user '%s'", username); return this.searchUserDn(username) .then(function (userDN: string) { - this.client.modifyAsync(userDN, change); + that.client.modifyAsync(userDN, change); }) .then(function () { return that.client.unbindAsync(); diff --git a/src/server/lib/routes/firstfactor/get.ts b/src/server/lib/routes/firstfactor/get.ts index 981012cca..01d8ac73f 100644 --- a/src/server/lib/routes/firstfactor/get.ts +++ b/src/server/lib/routes/firstfactor/get.ts @@ -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)); - return AuthenticationValidator.validate(req) - .then(function () { - const redirectUrl = req.query.redirect; - if (redirectUrl) { - res.redirect(redirectUrl); - 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(); - }); + res.render("firstfactor", { + first_factor_post_endpoint: Endpoints.FIRST_FACTOR_POST, + reset_password_request_endpoint: Endpoints.RESET_PASSWORD_REQUEST_GET + }); + return BluebirdPromise.resolve(); } \ No newline at end of file diff --git a/src/server/lib/routes/firstfactor/post.ts b/src/server/lib/routes/firstfactor/post.ts index 62aa2fa24..869e9d4f9 100644 --- a/src/server/lib/routes/firstfactor/post.ts +++ b/src/server/lib/routes/firstfactor/post.ts @@ -10,6 +10,7 @@ import Endpoint = require("../../../endpoints"); import ErrorReplies = require("../../ErrorReplies"); import { ServerVariablesHandler } from "../../ServerVariablesHandler"; import AuthenticationSession = require("../../AuthenticationSession"); +import Constants = require("../../../constants"); export default function (req: express.Request, res: express.Response): BluebirdPromise { const username: string = req.body.username; @@ -47,6 +48,8 @@ export default function (req: express.Request, res: express.Response): BluebirdP JSON.stringify(groupsAndEmails)); authSession.userid = username; 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 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."); regulator.mark(username, true); - res.status(204); - res.send(); + logger.debug("1st factor: Redirect URL is %s", redirectUrl); + 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(); }) .catch(exceptions.LdapSearchError, ErrorReplies.replyWithError500(res, logger)) diff --git a/src/server/lib/routes/password-reset/form/post.ts b/src/server/lib/routes/password-reset/form/post.ts index fe11c6601..e3fb36757 100644 --- a/src/server/lib/routes/password-reset/form/post.ts +++ b/src/server/lib/routes/password-reset/form/post.ts @@ -10,33 +10,32 @@ import ErrorReplies = require("../../../ErrorReplies"); import Constants = require("./../constants"); export default function (req: express.Request, res: express.Response): BluebirdPromise { - const logger = ServerVariablesHandler.getLogger(req.app); - const ldapPasswordUpdater = ServerVariablesHandler.getLdapPasswordUpdater(req.app); - let authSession: AuthenticationSession.AuthenticationSession; - const newPassword = objectPath.get(req, "body.password"); + const logger = ServerVariablesHandler.getLogger(req.app); + const ldapPasswordUpdater = ServerVariablesHandler.getLdapPasswordUpdater(req.app); + let authSession: AuthenticationSession.AuthenticationSession; + const newPassword = objectPath.get(req, "body.password"); - return AuthenticationSession.get(req) - .then(function (_authSession: AuthenticationSession.AuthenticationSession) { - authSession = _authSession; - logger.info("POST reset-password: User %s wants to reset his/her password.", - authSession.identity_check.userid); - logger.info("POST reset-password: Challenge %s", authSession.identity_check.challenge); + return AuthenticationSession.get(req) + .then(function (_authSession) { + authSession = _authSession; + logger.info("POST reset-password: User %s wants to reset his/her password.", + authSession.identity_check.userid); + logger.info("POST reset-password: Challenge %s", authSession.identity_check.challenge); - if (authSession.identity_check.challenge != Constants.CHALLENGE) { - res.status(403); - res.send(); - return BluebirdPromise.reject(new Error("Bad challenge.")); - } - - return ldapPasswordUpdater.updatePassword(authSession.identity_check.userid, newPassword); - }) - .then(function () { - logger.info("POST reset-password: Password reset for user '%s'", - authSession.identity_check.userid); - AuthenticationSession.reset(req); - res.status(204); - res.send(); - return BluebirdPromise.resolve(); - }) - .catch(ErrorReplies.replyWithError500(res, logger)); + if (authSession.identity_check.challenge != Constants.CHALLENGE) { + res.status(403); + res.send(); + return BluebirdPromise.reject(new Error("Bad challenge.")); + } + return ldapPasswordUpdater.updatePassword(authSession.identity_check.userid, newPassword); + }) + .then(function () { + logger.info("POST reset-password: Password reset for user '%s'", + authSession.identity_check.userid); + AuthenticationSession.reset(req); + res.status(204); + res.send(); + return BluebirdPromise.resolve(); + }) + .catch(ErrorReplies.replyWithError500(res, logger)); } diff --git a/src/server/lib/routes/verify/get.ts b/src/server/lib/routes/verify/get.ts index 5705b8554..390b198a4 100644 --- a/src/server/lib/routes/verify/get.ts +++ b/src/server/lib/routes/verify/get.ts @@ -8,35 +8,38 @@ import AuthenticationValidator = require("../../AuthenticationValidator"); import ErrorReplies = require("../../ErrorReplies"); import { ServerVariablesHandler } from "../../ServerVariablesHandler"; import AuthenticationSession = require("../../AuthenticationSession"); +import Constants = require("../../../constants"); function verify_filter(req: express.Request, res: express.Response): BluebirdPromise { const logger = ServerVariablesHandler.getLogger(req.app); const accessController = ServerVariablesHandler.getAccessController(req.app); - let authSession: AuthenticationSession.AuthenticationSession; return AuthenticationSession.get(req) - .then(function (_authSession: AuthenticationSession.AuthenticationSession) { - authSession = _authSession; + .then(function (authSession) { logger.debug("Verify: headers are %s", JSON.stringify(req.headers)); res.set("Redirect", encodeURIComponent("https://" + req.headers["host"] + req.headers["x-original-uri"])); - return AuthenticationValidator.validate(req); - }) - .then(function () { const username = authSession.userid; 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(req, "headers.host"); const path = objectPath.get(req, "headers.x-original-uri"); 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); 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) - return BluebirdPromise.reject(new exceptions.AccessDeniedError("First or second factor not validated")); + if (!onlyBasicAuth && !authSession.second_factor) + return BluebirdPromise.reject(new exceptions.AccessDeniedError("Second factor not validated.")); res.setHeader("Remote-User", username); res.setHeader("Remote-Groups", groups.join(",")); diff --git a/test/features/access-control.feature b/test/features/access-control.feature index d760cbf08..4a3a8554f 100644 --- a/test/features/access-control.feature +++ b/test/features/access-control.feature @@ -16,6 +16,7 @@ Feature: User has access restricted access to domains | https://dev.test.local:8080/users/bob/secret.html | | https://admin.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: | url | | 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://dev.test.local:8080/users/john/secret.html | | https://dev.test.local:8080/users/harry/secret.html | + | https://basicauth.test.local:8080/secret.html | @need-registered-user-harry 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://mx1.mail.test.local:8080/secret.html | | https://mx2.mail.test.local:8080/secret.html | + | https://basicauth.test.local:8080/secret.html | diff --git a/test/features/basic-auth.feature b/test/features/basic-auth.feature new file mode 100644 index 000000000..34a2ae05d --- /dev/null +++ b/test/features/basic-auth.feature @@ -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" diff --git a/test/features/redirection.feature b/test/features/redirection.feature index ab508f94b..2b5a66026 100644 --- a/test/features/redirection.feature +++ b/test/features/redirection.feature @@ -2,7 +2,7 @@ Feature: User is correctly redirected Scenario: User is redirected to authelia when he is not authenticated 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 Scenario: User is redirected to home page after several authentication tries diff --git a/test/features/resilience.feature b/test/features/resilience.feature index ec4b46034..0b444c75e 100644 --- a/test/features/resilience.feature +++ b/test/features/resilience.feature @@ -14,6 +14,4 @@ Feature: Authelia keeps user sessions despite the application restart And I login with user "john" and password "password" And I use "REGISTERED" as TOTP token handle And I click on "TOTP" - Then I have access to: - | url | - | https://admin.test.local:8080/secret.html | \ No newline at end of file + Then I'm redirected to "https://admin.test.local:8080/secret.html" \ No newline at end of file diff --git a/test/features/step_definitions/authentication.ts b/test/features/step_definitions/authentication.ts index 0f5ad9059..ac3f398c4 100644 --- a/test/features/step_definitions/authentication.ts +++ b/test/features/step_definitions/authentication.ts @@ -6,7 +6,7 @@ import Speakeasy = require("speakeasy"); import CustomWorld = require("../support/world"); 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); }); @@ -66,10 +66,7 @@ and I use TOTP token handle {stringInDoubleQuotes}", function hasAccessToSecret(link: string, that: any) { return that.driver.get(link) .then(function () { - return that.driver.findElement(seleniumWebdriver.By.tagName("body")).getText() - .then(function (body: string) { - Assert(body.indexOf("This is a very important secret!") > -1, body); - }); + return that.waitUntilUrlContains(link); }); } diff --git a/test/features/step_definitions/redirection.ts b/test/features/step_definitions/redirection.ts index 8b29ee726..988bc64ee 100644 --- a/test/features/step_definitions/redirection.ts +++ b/test/features/step_definitions/redirection.ts @@ -12,6 +12,6 @@ Cucumber.defineSupportCode(function ({ Given, When, Then }) { }); Then("I'm redirected to {stringInDoubleQuotes}", function (link: string) { - return this.driver.wait(seleniumWebdriver.until.urlContains(link), 15000); + return this.waitUntilUrlContains(link); }); }); \ No newline at end of file diff --git a/test/features/support/world.ts b/test/features/support/world.ts index 707317163..e3302fda5 100644 --- a/test/features/support/world.ts +++ b/test/features/support/world.ts @@ -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) { return that.driver.wait(seleniumWebdriver.until.elementLocated(seleniumWebdriver.By.id("username")), 4000) .then(function () { diff --git a/test/unit/client/firstfactor/FirstFactorValidator.test.ts b/test/unit/client/firstfactor/FirstFactorValidator.test.ts index 5ce7533fc..cf3a954cb 100644 --- a/test/unit/client/firstfactor/FirstFactorValidator.test.ts +++ b/test/unit/client/firstfactor/FirstFactorValidator.test.ts @@ -7,13 +7,13 @@ import Assert = require("assert"); describe("test FirstFactorValidator", function () { it("should validate first factor successfully", () => { const postPromise = JQueryMock.JQueryDeferredMock(); - postPromise.done.yields(); + postPromise.done.yields({ redirect: "http://redirect" }); postPromise.done.returns(postPromise); 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) { @@ -25,9 +25,9 @@ describe("test FirstFactorValidator", function () { postPromise.done.returns(postPromise); 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 () { return BluebirdPromise.reject(new Error("First factor validation successfully finished while it should have not.")); }, function (err: Error) { diff --git a/test/unit/server/access_control/AccessController.test.ts b/test/unit/server/access_control/AccessController.test.ts index d15860392..97c91fcd3 100644 --- a/test/unit/server/access_control/AccessController.test.ts +++ b/test/unit/server/access_control/AccessController.test.ts @@ -8,346 +8,360 @@ describe("test access control manager", function () { let accessController: AccessController; let configuration: ACLConfiguration; - beforeEach(function () { - configuration = { - default_policy: "deny", - any: [], - users: {}, - groups: {} - }; - accessController = new AccessController(configuration, winston); + describe("configuration is null", function() { + it("should allow access to anything, anywhere for anybody", function() { + configuration = undefined; + accessController = new AccessController(configuration, winston); + + Assert(accessController.isAccessAllowed("home.test.local", "/", "user1", ["group1", "group2"])); + Assert(accessController.isAccessAllowed("home.test.local", "/abc", "user1", ["group1", "group2"])); + 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 () { - 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 () { - Assert(!accessController.isAccessAllowed("home.example.com", "/", "user1", ["group1"])); - }); - - 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"])); + describe("check access control with default policy to deny", function () { + beforeEach(function () { + configuration.default_policy = "deny"; }); - it("should deny to other users", function () { - configuration.users["user1"] = [{ - 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 deny access when no rule is provided", function () { + Assert(!accessController.isAccessAllowed("home.example.com", "/", "user1", ["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"] = [{ - domain: "home.example.com", + domain: "*.mail.example.com", policy: "allow", - resources: ["/private/.*", "^/begin", "/end$"] + resources: [".*"] }]; 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"])); + 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 multiple domains", function () { + it("should allow access to all resources when resources is not provided", 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: [".*"] + domain: "*.mail.example.com", + policy: "allow" }]; - 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"])); + 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 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"] - }]; + 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"])); + }); - 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"])); + it("should deny to other users", function () { + configuration.users["user1"] = [{ + 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 () { - it("should allow access when user is in group having a matching allowing rule", function () { - configuration.groups["group1"] = [{ + describe("check all rules", function () { + it("should control access when all rules are defined", function () { + configuration.any = [{ domain: "home.example.com", policy: "allow", - resources: ["^/$"] - }]; - configuration.groups["group2"] = [{ - domain: "home.example.com", - policy: "allow", - resources: ["^/test$"] + resources: ["^/public$"] }, { 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", + 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("another.home.example.com", "/", "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"])); }); }); }); - - 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"])); - }); - }); }); diff --git a/test/unit/server/mocks/ServerVariablesMock.ts b/test/unit/server/mocks/ServerVariablesMock.ts index a59ab1580..ea3cdc8fe 100644 --- a/test/unit/server/mocks/ServerVariablesMock.ts +++ b/test/unit/server/mocks/ServerVariablesMock.ts @@ -1,40 +1,40 @@ -import sinon = require("sinon"); +import Sinon = require("sinon"); import express = require("express"); import winston = require("winston"); import { UserDataStoreStub } from "./storage/UserDataStoreStub"; -import { VARIABLES_KEY }  from "../../../../src/server/lib/ServerVariablesHandler"; +import { VARIABLES_KEY } from "../../../../src/server/lib/ServerVariablesHandler"; export interface ServerVariablesMock { - logger: any; - ldapAuthenticator: any; - ldapEmailsRetriever: any; - ldapPasswordUpdater: any; - totpValidator: any; - totpGenerator: any; - u2f: any; - userDataStore: UserDataStoreStub; - notifier: any; - regulator: any; - config: any; - accessController: any; + logger: any; + ldapAuthenticator: any; + ldapEmailsRetriever: any; + ldapPasswordUpdater: any; + totpValidator: any; + totpGenerator: any; + u2f: any; + userDataStore: UserDataStoreStub; + notifier: any; + regulator: any; + config: any; + accessController: any; } export function mock(app: express.Application): ServerVariablesMock { - const mocks: ServerVariablesMock = { - accessController: sinon.stub(), - config: sinon.stub(), - ldapAuthenticator: sinon.stub() as any, - ldapEmailsRetriever: sinon.stub() as any, - ldapPasswordUpdater: sinon.stub() as any, - logger: winston, - notifier: sinon.stub(), - regulator: sinon.stub(), - totpGenerator: sinon.stub(), - totpValidator: sinon.stub(), - u2f: sinon.stub(), - userDataStore: new UserDataStoreStub() - }; - app.get = sinon.stub().withArgs(VARIABLES_KEY).returns(mocks); - return mocks; + const mocks: ServerVariablesMock = { + accessController: Sinon.stub(), + config: Sinon.stub(), + ldapAuthenticator: Sinon.stub() as any, + ldapEmailsRetriever: Sinon.stub() as any, + ldapPasswordUpdater: Sinon.stub() as any, + logger: winston, + notifier: Sinon.stub(), + regulator: Sinon.stub(), + totpGenerator: Sinon.stub(), + totpValidator: Sinon.stub(), + u2f: Sinon.stub(), + userDataStore: new UserDataStoreStub() + }; + app.get = Sinon.stub().withArgs(VARIABLES_KEY).returns(mocks); + return mocks; } \ No newline at end of file diff --git a/test/unit/server/routes/firstfactor/post.test.ts b/test/unit/server/routes/firstfactor/post.test.ts index 486b3c52b..b5329b046 100644 --- a/test/unit/server/routes/firstfactor/post.test.ts +++ b/test/unit/server/routes/firstfactor/post.test.ts @@ -51,6 +51,9 @@ describe("test the first factor validation route", function () { username: "username", password: "password" }, + query: { + redirect: "http://redirect.url" + }, session: { }, headers: { @@ -87,7 +90,6 @@ describe("test the first factor validation route", function () { .then(function () { assert.equal("username", authSession.userid); assert(res.send.calledOnce); - assert(res.status.calledWith(204)); }); }); diff --git a/test/unit/server/routes/verify/get.test.ts b/test/unit/server/routes/verify/get.test.ts index 875c23bc6..deee7d54a 100644 --- a/test/unit/server/routes/verify/get.test.ts +++ b/test/unit/server/routes/verify/get.test.ts @@ -1,9 +1,9 @@ -import assert = require("assert"); +import Assert = require("assert"); import VerifyGet = require("../../../../../src/server/lib/routes/verify/get"); import AuthenticationSession = require("../../../../../src/server/lib/AuthenticationSession"); -import sinon = require("sinon"); +import Sinon = require("sinon"); import winston = require("winston"); import BluebirdPromise = require("bluebird"); @@ -24,10 +24,13 @@ describe("test authentication token verification", function () { req = ExpressMock.RequestMock(); res = ExpressMock.ResponseMock(); - req.app = { - get: sinon.stub().returns({ logger: winston }) - }; req.session = {}; + req.query = { + redirect: "http://redirect.url" + }; + req.app = { + get: Sinon.stub().returns({ logger: winston }) + }; AuthenticationSession.reset(req as any); req.headers = {}; 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); }) .then(function () { - sinon.assert.calledWithExactly(res.setHeader, "Remote-User", "myuser"); - sinon.assert.calledWithExactly(res.setHeader, "Remote-Groups", "mygroup,othergroup"); - assert.equal(204, res.status.getCall(0).args[0]); + Sinon.assert.calledWithExactly(res.setHeader, "Remote-User", "myuser"); + Sinon.assert.calledWithExactly(res.setHeader, "Remote-Groups", "mygroup,othergroup"); + Assert.equal(204, res.status.getCall(0).args[0]); }); }); - describe("given different cases of session", function () { - function test_session(auth_session: AuthenticationSession.AuthenticationSession, status_code: number) { - return VerifyGet.default(req as express.Request, res as any) - .then(function () { - assert.equal(status_code, res.status.getCall(0).args[0]); + function test_session(_authSession: AuthenticationSession.AuthenticationSession, status_code: number) { + return AuthenticationSession.get(req as any) + .then(function (authSession) { + authSession = _authSession; + 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) { - return test_session(auth_session, 401); - } + it("should not be authenticated when first factor is missing", function () { + return test_non_authenticated_401({ + userid: "user", + first_factor: false, + second_factor: true, + email: undefined, + groups: [], + }); + }); - function test_unauthorized_403(auth_session: AuthenticationSession.AuthenticationSession) { - return test_session(auth_session, 403); - } + it("should not be authenticated when userid is missing", function () { + return test_non_authenticated_401({ + userid: undefined, + first_factor: true, + second_factor: false, + email: undefined, + groups: [], + }); + }); - function test_authorized(auth_session: AuthenticationSession.AuthenticationSession) { - return test_session(auth_session, 204); - } + 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 second factor is missing", function () { - return test_non_authenticated_401({ - userid: "user", - first_factor: true, - 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) + .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 () { - return test_non_authenticated_401({ - userid: "user", - first_factor: false, - second_factor: true, - email: undefined, - groups: [], - }); + describe("given user tries to access a basic auth endpoint", function () { + beforeEach(function () { + req.query = { + redirect: "http://redirect.url", + only_basic_auth: "true" + }; }); - it("should not be authenticated when userid is missing", 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 () { + it("should be authenticated when first factor is validated and not second factor", function () { return AuthenticationSession.get(req as any) .then(function (authSession: AuthenticationSession.AuthenticationSession) { authSession.first_factor = true; - authSession.second_factor = true; - authSession.userid = "myuser"; + return VerifyGet.default(req as express.Request, res as any); + }) + .then(function () { + Assert(res.status.calledWith(204)); + Assert(res.send.calledOnce); + }); + }); - 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 be rejected with 401 when first factor is not validated", function () { + return AuthenticationSession.get(req as any) + .then(function (authSession: AuthenticationSession.AuthenticationSession) { + authSession.first_factor = false; + return VerifyGet.default(req as express.Request, res as any); + }) + .then(function () { + Assert(res.status.calledWith(401)); }); }); });