mirror of
https://github.com/0rangebananaspy/authelia.git
synced 2024-09-14 22:47:21 +07:00
Merge pull request #193 from clems4ever/feature/support-basic-auth
Add support of basic auth for single-factor protected endpoints
This commit is contained in:
commit
b37c0293b8
20
README.md
20
README.md
|
@ -22,7 +22,7 @@ used in production to secure internal services in a small docker swarm cluster.
|
|||
3. [Second factor with U2F security keys](#second-factor-with-u2f-security-keys)
|
||||
4. [Password reset](#password-reset)
|
||||
5. [Access control](#access-control)
|
||||
6. [Basic authentication](#basic-authentication)
|
||||
6. [Single factor authentication](#single-factor-authentication)
|
||||
7. [Session management with Redis](#session-management-with-redis)
|
||||
4. [Security](#security)
|
||||
5. [Documentation](#documentation)
|
||||
|
@ -37,12 +37,12 @@ used in production to secure internal services in a small docker swarm cluster.
|
|||
* Two-factor authentication using either
|
||||
**[TOTP] - Time-Base One Time password -** or **[U2F] - Universal 2-Factor -**
|
||||
as 2nd factor.
|
||||
* Password reset with identity verification by sending links to user email
|
||||
address.
|
||||
* Two-factor and basic authentication methods available.
|
||||
* Password reset with identity verification using email.
|
||||
* Single and two factors authentication methods available.
|
||||
* Access restriction after too many authentication attempts.
|
||||
* Session management using Redis key/value store.
|
||||
* User-defined access control per subdomain and resource.
|
||||
* Support of [basic authentication] for endpoints protected by single factor.
|
||||
* High-availability using a highly-available distributed database and KV store.
|
||||
|
||||
## Deployment
|
||||
|
||||
|
@ -190,11 +190,14 @@ user access to some resources and subdomains. Those rules are defined and fully
|
|||
in the configuration file. They can apply to users, groups or everyone.
|
||||
Check out [config.template.yml] to see how they are defined.
|
||||
|
||||
### Basic Authentication
|
||||
Authelia allows you to customize the authentication method to use for each sub-domain.
|
||||
The supported methods are either "single_factor" and "two_factor".
|
||||
### Single factor authentication
|
||||
Authelia allows you to customize the authentication method to use for each
|
||||
sub-domain.The supported methods are either "single_factor" or "two_factor".
|
||||
Please see [config.template.yml] to see an example of configuration.
|
||||
|
||||
It is also possible to use [basic authentication] to access a resource
|
||||
protected by a single factor.
|
||||
|
||||
### Session management with Redis
|
||||
When your users authenticate against Authelia, sessions are stored in a Redis key/value store. You can specify your own Redis instance in [config.template.yml].
|
||||
|
||||
|
@ -293,3 +296,4 @@ Follow [contributing](CONTRIBUTORS.md) file.
|
|||
[Google Authenticator]: https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en
|
||||
[config.template.yml]: https://github.com/clems4ever/authelia/blob/master/config.template.yml
|
||||
[HSTS]: https://www.nginx.com/blog/http-strict-transport-security-hsts-and-nginx/
|
||||
[basic authentication]: https://en.wikipedia.org/wiki/Basic_access_authentication
|
||||
|
|
|
@ -119,7 +119,7 @@ http {
|
|||
|
||||
error_page 401 =302 https://auth.test.local:8080?redirect=$redirect;
|
||||
error_page 403 = https://auth.test.local:8080/error/403;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
|
@ -262,6 +262,7 @@ http {
|
|||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header Content-Length "";
|
||||
proxy_set_header Proxy-Authorization $http_authorization;
|
||||
|
||||
proxy_pass http://authelia/api/verify;
|
||||
}
|
||||
|
@ -280,6 +281,23 @@ http {
|
|||
error_page 401 =302 https://auth.test.local:8080?redirect=$redirect;
|
||||
error_page 403 = https://auth.test.local:8080/error/403;
|
||||
}
|
||||
|
||||
location /headers {
|
||||
auth_request /auth_verify;
|
||||
|
||||
auth_request_set $redirect $upstream_http_redirect;
|
||||
|
||||
auth_request_set $user $upstream_http_remote_user;
|
||||
proxy_set_header Custom-Forwarded-User $user;
|
||||
|
||||
auth_request_set $groups $upstream_http_remote_groups;
|
||||
proxy_set_header Custom-Forwarded-Groups $groups;
|
||||
|
||||
proxy_pass http://httpbin:8000/headers;
|
||||
|
||||
error_page 401 =302 https://auth.test.local:8080?redirect=$redirect;
|
||||
error_page 403 = https://auth.test.local:8080/error/403;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -70,7 +70,7 @@
|
|||
"@types/request": "^2.0.5",
|
||||
"@types/request-promise": "^4.1.38",
|
||||
"@types/selenium-webdriver": "^3.0.4",
|
||||
"@types/sinon": "^2.2.1",
|
||||
"@types/sinon": "^2.3.7",
|
||||
"@types/tmp": "0.0.33",
|
||||
"@types/winston": "^2.3.2",
|
||||
"@types/yamljs": "^0.2.30",
|
||||
|
@ -101,8 +101,7 @@
|
|||
"request-promise": "^4.2.2",
|
||||
"selenium-webdriver": "^3.5.0",
|
||||
"should": "^11.1.1",
|
||||
"sinon": "^2.3.8",
|
||||
"sinon-promise": "^0.1.3",
|
||||
"sinon": "^4.0.2",
|
||||
"tmp": "0.0.31",
|
||||
"ts-node": "^3.3.0",
|
||||
"tslint": "^5.2.0",
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
|
||||
import BluebirdPromise = require("bluebird");
|
||||
import express = require("express");
|
||||
import objectPath = require("object-path");
|
||||
import FirstFactorValidator = require("./FirstFactorValidator");
|
||||
import { AuthenticationSessionHandler } from "./AuthenticationSessionHandler";
|
||||
import { IRequestLogger } from "./logging/IRequestLogger";
|
||||
|
||||
export function validate(req: express.Request, logger: IRequestLogger): BluebirdPromise<void> {
|
||||
return FirstFactorValidator.validate(req, logger)
|
||||
.then(function () {
|
||||
const authSession = AuthenticationSessionHandler.get(req, logger);
|
||||
if (!authSession.second_factor)
|
||||
return BluebirdPromise.reject("No second factor variable.");
|
||||
return BluebirdPromise.resolve();
|
||||
});
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
import BluebirdPromise = require("bluebird");
|
||||
import exceptions = require("../Exceptions");
|
||||
import ldapjs = require("ldapjs");
|
||||
import { IClient } from "./IClient";
|
||||
|
||||
import { IClientFactory } from "./IClientFactory";
|
||||
import { IGroupsRetriever } from "./IGroupsRetriever";
|
||||
import { LdapConfiguration } from "../configuration/Configuration";
|
||||
|
||||
|
||||
export class GroupsRetriever implements IGroupsRetriever {
|
||||
private options: LdapConfiguration;
|
||||
private clientFactory: IClientFactory;
|
||||
|
||||
constructor(options: LdapConfiguration, clientFactory: IClientFactory) {
|
||||
this.options = options;
|
||||
this.clientFactory = clientFactory;
|
||||
}
|
||||
|
||||
retrieve(username: string, client?: IClient): BluebirdPromise<string[]> {
|
||||
client = this.clientFactory.create(this.options.user, this.options.password);
|
||||
let groups: string[];
|
||||
|
||||
return client.open()
|
||||
.then(function () {
|
||||
return client.searchGroups(username);
|
||||
})
|
||||
.then(function (groups_: string[]) {
|
||||
groups = groups_;
|
||||
return client.close();
|
||||
})
|
||||
.then(function () {
|
||||
return BluebirdPromise.resolve(groups);
|
||||
})
|
||||
.catch(function (err: Error) {
|
||||
return BluebirdPromise.reject(new exceptions.LdapError("Failed during groups retrieval: " + err.message));
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
import BluebirdPromise = require("bluebird");
|
||||
import { IClient } from "./IClient";
|
||||
|
||||
export interface IGroupsRetriever {
|
||||
retrieve(username: string): BluebirdPromise<string[]>;
|
||||
}
|
|
@ -17,8 +17,10 @@ export class PasswordUpdater implements IPasswordUpdater {
|
|||
this.clientFactory = clientFactory;
|
||||
}
|
||||
|
||||
updatePassword(username: string, newPassword: string): BluebirdPromise<void> {
|
||||
const adminClient = this.clientFactory.create(this.options.user, this.options.password);
|
||||
updatePassword(username: string, newPassword: string)
|
||||
: BluebirdPromise<void> {
|
||||
const adminClient = this.clientFactory.create(this.options.user,
|
||||
this.options.password);
|
||||
|
||||
return adminClient.open()
|
||||
.then(function () {
|
||||
|
@ -27,8 +29,10 @@ export class PasswordUpdater implements IPasswordUpdater {
|
|||
.then(function () {
|
||||
return adminClient.close();
|
||||
})
|
||||
.error(function (err: Error) {
|
||||
return BluebirdPromise.reject(new exceptions.LdapError("Failed during password update: " + err.message));
|
||||
.catch(function (err: Error) {
|
||||
return BluebirdPromise.reject(
|
||||
new exceptions.LdapError(
|
||||
"Error while updating password: " + err.message));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@ import express = require("express");
|
|||
import objectPath = require("object-path");
|
||||
import winston = require("winston");
|
||||
import Endpoints = require("../../../../../shared/api");
|
||||
import AuthenticationValidator = require("../../AuthenticationValidator");
|
||||
import BluebirdPromise = require("bluebird");
|
||||
import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler";
|
||||
import Constants = require("../../../../../shared/constants");
|
||||
|
|
22
server/src/lib/routes/verify/access_control.ts
Normal file
22
server/src/lib/routes/verify/access_control.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import Express = require("express");
|
||||
import BluebirdPromise = require("bluebird");
|
||||
import Util = require("util");
|
||||
|
||||
import { ServerVariables } from "../../ServerVariables";
|
||||
import Exceptions = require("../../Exceptions");
|
||||
|
||||
export default function (req: Express.Request, vars: ServerVariables,
|
||||
domain: string, path: string, username: string, groups: string[]) {
|
||||
|
||||
return new BluebirdPromise(function (resolve, reject) {
|
||||
const isAllowed = vars.accessController
|
||||
.isAccessAllowed(domain, path, username, groups);
|
||||
|
||||
if (!isAllowed) {
|
||||
reject(new Exceptions.DomainAccessDenied(Util.format(
|
||||
"User '%s' does not have access to '%s'", username, domain)));
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
}
|
|
@ -1,119 +1,69 @@
|
|||
|
||||
import objectPath = require("object-path");
|
||||
import BluebirdPromise = require("bluebird");
|
||||
import express = require("express");
|
||||
import exceptions = require("../../Exceptions");
|
||||
import winston = require("winston");
|
||||
import AuthenticationValidator = require("../../AuthenticationValidator");
|
||||
import Express = require("express");
|
||||
import Exceptions = require("../../Exceptions");
|
||||
import ErrorReplies = require("../../ErrorReplies");
|
||||
import { AppConfiguration } from "../../configuration/Configuration";
|
||||
import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler";
|
||||
import { AuthenticationSession } from "../../../../types/AuthenticationSession";
|
||||
import Constants = require("../../../../../shared/constants");
|
||||
import Util = require("util");
|
||||
import { DomainExtractor } from "../../utils/DomainExtractor";
|
||||
import { ServerVariables } from "../../ServerVariables";
|
||||
import { MethodCalculator } from "../../authentication/MethodCalculator";
|
||||
import { IRequestLogger } from "../../logging/IRequestLogger";
|
||||
import GetWithSessionCookieMethod from "./get_session_cookie";
|
||||
import GetWithBasicAuthMethod from "./get_basic_auth";
|
||||
|
||||
const FIRST_FACTOR_NOT_VALIDATED_MESSAGE = "First factor not yet validated";
|
||||
const SECOND_FACTOR_NOT_VALIDATED_MESSAGE = "Second factor not yet validated";
|
||||
import { AuthenticationSessionHandler }
|
||||
from "../../AuthenticationSessionHandler";
|
||||
import { AuthenticationSession }
|
||||
from "../../../../types/AuthenticationSession";
|
||||
|
||||
const REMOTE_USER = "Remote-User";
|
||||
const REMOTE_GROUPS = "Remote-Groups";
|
||||
|
||||
function verify_inactivity(req: express.Request,
|
||||
authSession: AuthenticationSession,
|
||||
configuration: AppConfiguration, logger: IRequestLogger)
|
||||
: BluebirdPromise<void> {
|
||||
|
||||
const lastActivityTime = authSession.last_activity_datetime;
|
||||
const currentTime = new Date().getTime();
|
||||
authSession.last_activity_datetime = currentTime;
|
||||
function verifyWithSelectedMethod(req: Express.Request, res: Express.Response,
|
||||
vars: ServerVariables, authSession: AuthenticationSession)
|
||||
: () => BluebirdPromise<{ username: string, groups: string[] }> {
|
||||
return function () {
|
||||
const authorization: string = "" + req.headers["proxy-authorization"];
|
||||
if (authorization && authorization.startsWith("Basic "))
|
||||
return GetWithBasicAuthMethod(req, res, vars, authorization);
|
||||
|
||||
// If inactivity is not specified, then inactivity timeout does not apply
|
||||
if (!configuration.session.inactivity) {
|
||||
return BluebirdPromise.resolve();
|
||||
}
|
||||
|
||||
const inactivityPeriodMs = currentTime - lastActivityTime;
|
||||
logger.debug(req, "Inactivity period was %s s and max period was %s.",
|
||||
inactivityPeriodMs / 1000, configuration.session.inactivity / 1000);
|
||||
if (inactivityPeriodMs < configuration.session.inactivity) {
|
||||
return BluebirdPromise.resolve();
|
||||
}
|
||||
|
||||
logger.debug(req, "Session has been reset after too long inactivity period.");
|
||||
AuthenticationSessionHandler.reset(req);
|
||||
return BluebirdPromise.reject(new Error("Inactivity period exceeded."));
|
||||
return GetWithSessionCookieMethod(req, res, vars, authSession);
|
||||
};
|
||||
}
|
||||
|
||||
function verify_filter(req: express.Request, res: express.Response,
|
||||
vars: ServerVariables): BluebirdPromise<void> {
|
||||
let authSession: AuthenticationSession;
|
||||
let username: string;
|
||||
let groups: string[];
|
||||
|
||||
return new BluebirdPromise(function (resolve, reject) {
|
||||
authSession = AuthenticationSessionHandler.get(req, vars.logger);
|
||||
username = authSession.userid;
|
||||
groups = authSession.groups;
|
||||
|
||||
function setRedirectHeader(req: Express.Request, res: Express.Response) {
|
||||
return function () {
|
||||
res.set("Redirect", encodeURIComponent("https://" + req.headers["host"] +
|
||||
req.headers["x-original-uri"]));
|
||||
return BluebirdPromise.resolve();
|
||||
};
|
||||
}
|
||||
|
||||
if (!authSession.userid) {
|
||||
reject(new exceptions.AccessDeniedError(
|
||||
Util.format("%s: %s.", FIRST_FACTOR_NOT_VALIDATED_MESSAGE, "userid is missing")));
|
||||
return;
|
||||
}
|
||||
function setUserAndGroupsHeaders(res: Express.Response) {
|
||||
return function (u: { username: string, groups: string[] }) {
|
||||
res.setHeader(REMOTE_USER, u.username);
|
||||
res.setHeader(REMOTE_GROUPS, u.groups.join(","));
|
||||
return BluebirdPromise.resolve();
|
||||
};
|
||||
}
|
||||
|
||||
const host = objectPath.get<express.Request, string>(req, "headers.host");
|
||||
const path = objectPath.get<express.Request, string>(req, "headers.x-original-uri");
|
||||
|
||||
const domain = DomainExtractor.fromHostHeader(host);
|
||||
const authenticationMethod =
|
||||
MethodCalculator.compute(vars.config.authentication_methods, domain);
|
||||
vars.logger.debug(req, "domain=%s, path=%s, user=%s, groups=%s", domain, path,
|
||||
username, groups.join(","));
|
||||
|
||||
if (!authSession.first_factor)
|
||||
return reject(new exceptions.AccessDeniedError(
|
||||
Util.format("%s: %s.", FIRST_FACTOR_NOT_VALIDATED_MESSAGE, "first factor is false")));
|
||||
|
||||
if (authenticationMethod == "two_factor" && !authSession.second_factor)
|
||||
return reject(new exceptions.AccessDeniedError(
|
||||
Util.format("%s: %s.", SECOND_FACTOR_NOT_VALIDATED_MESSAGE, "second factor is false")));
|
||||
|
||||
const isAllowed = vars.accessController.isAccessAllowed(domain, path, username, groups);
|
||||
if (!isAllowed) return reject(
|
||||
new exceptions.DomainAccessDenied(Util.format("User '%s' does not have access to '%s'",
|
||||
username, domain)));
|
||||
|
||||
resolve();
|
||||
})
|
||||
.then(function () {
|
||||
return verify_inactivity(req, authSession,
|
||||
vars.config, vars.logger);
|
||||
})
|
||||
.then(function () {
|
||||
res.setHeader(REMOTE_USER, username);
|
||||
res.setHeader(REMOTE_GROUPS, groups.join(","));
|
||||
return BluebirdPromise.resolve();
|
||||
});
|
||||
function replyWith200(res: Express.Response) {
|
||||
return function () {
|
||||
res.status(204);
|
||||
res.send();
|
||||
};
|
||||
}
|
||||
|
||||
export default function (vars: ServerVariables) {
|
||||
return function (req: express.Request, res: express.Response)
|
||||
return function (req: Express.Request, res: Express.Response)
|
||||
: BluebirdPromise<void> {
|
||||
return verify_filter(req, res, vars)
|
||||
.then(function () {
|
||||
res.status(204);
|
||||
res.send();
|
||||
return BluebirdPromise.resolve();
|
||||
})
|
||||
let authSession: AuthenticationSession;
|
||||
return new BluebirdPromise(function (resolve, reject) {
|
||||
authSession = AuthenticationSessionHandler.get(req, vars.logger);
|
||||
resolve();
|
||||
})
|
||||
.then(setRedirectHeader(req, res))
|
||||
.then(verifyWithSelectedMethod(req, res, vars, authSession))
|
||||
.then(setUserAndGroupsHeaders(res))
|
||||
.then(replyWith200(res))
|
||||
// The user is authenticated but has restricted access -> 403
|
||||
.catch(exceptions.DomainAccessDenied, ErrorReplies
|
||||
.catch(Exceptions.DomainAccessDenied, ErrorReplies
|
||||
.replyWithError403(req, res, vars.logger))
|
||||
// The user is not yet authenticated -> 401
|
||||
.catch(ErrorReplies.replyWithError401(req, res, vars.logger));
|
||||
|
|
75
server/src/lib/routes/verify/get_basic_auth.ts
Normal file
75
server/src/lib/routes/verify/get_basic_auth.ts
Normal file
|
@ -0,0 +1,75 @@
|
|||
import Express = require("express");
|
||||
import BluebirdPromise = require("bluebird");
|
||||
import ObjectPath = require("object-path");
|
||||
import { ServerVariables } from "../../ServerVariables";
|
||||
import { AuthenticationSession }
|
||||
from "../../../../types/AuthenticationSession";
|
||||
import { DomainExtractor } from "../../utils/DomainExtractor";
|
||||
import { MethodCalculator } from "../../authentication/MethodCalculator";
|
||||
import AccessControl from "./access_control";
|
||||
|
||||
export default function (req: Express.Request, res: Express.Response,
|
||||
vars: ServerVariables, authorizationHeader: string)
|
||||
: BluebirdPromise<{ username: string, groups: string[] }> {
|
||||
let username: string;
|
||||
let groups: string[];
|
||||
let domain: string;
|
||||
let path: string;
|
||||
|
||||
return new BluebirdPromise<[string, string]>(function (resolve, reject) {
|
||||
const host = ObjectPath.get<Express.Request, string>(req, "headers.host");
|
||||
domain = DomainExtractor.fromHostHeader(host);
|
||||
path =
|
||||
ObjectPath.get<Express.Request, string>(req, "headers.x-original-uri");
|
||||
const authenticationMethod =
|
||||
MethodCalculator.compute(vars.config.authentication_methods, domain);
|
||||
|
||||
if (authenticationMethod != "single_factor") {
|
||||
reject(new Error("This domain is not protected with single factor. " +
|
||||
"You cannot log in with basic authentication."));
|
||||
return;
|
||||
}
|
||||
|
||||
const base64Re = new RegExp("^Basic ((?:[A-Za-z0-9+/]{4})*" +
|
||||
"(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?)$");
|
||||
const isTokenValidBase64 = base64Re.test(authorizationHeader);
|
||||
|
||||
if (!isTokenValidBase64) {
|
||||
reject(new Error("No valid base64 token found in the header"));
|
||||
return;
|
||||
}
|
||||
|
||||
const tokenMatches = authorizationHeader.match(base64Re);
|
||||
const base64Token = tokenMatches[1];
|
||||
const decodedToken = Buffer.from(base64Token, "base64").toString();
|
||||
const splittedToken = decodedToken.split(":");
|
||||
|
||||
if (splittedToken.length != 2) {
|
||||
reject(new Error(
|
||||
"The authorization token is invalid. Expecting 'userid:password'"));
|
||||
return;
|
||||
}
|
||||
|
||||
username = splittedToken[0];
|
||||
const password = splittedToken[1];
|
||||
resolve([username, password]);
|
||||
})
|
||||
.then(function ([userid, password]) {
|
||||
return vars.ldapAuthenticator.authenticate(userid, password);
|
||||
})
|
||||
.then(function (groupsAndEmails) {
|
||||
groups = groupsAndEmails.groups;
|
||||
return AccessControl(req, vars, domain, path, username, groups);
|
||||
})
|
||||
.then(function () {
|
||||
return BluebirdPromise.resolve({
|
||||
username: username,
|
||||
groups: groups
|
||||
});
|
||||
})
|
||||
.catch(function (err: Error) {
|
||||
return BluebirdPromise.reject(
|
||||
new Error("Unable to authenticate the user with basic auth. Cause: "
|
||||
+ err.message));
|
||||
});
|
||||
}
|
102
server/src/lib/routes/verify/get_session_cookie.ts
Normal file
102
server/src/lib/routes/verify/get_session_cookie.ts
Normal file
|
@ -0,0 +1,102 @@
|
|||
import Express = require("express");
|
||||
import BluebirdPromise = require("bluebird");
|
||||
import Util = require("util");
|
||||
import ObjectPath = require("object-path");
|
||||
|
||||
import Exceptions = require("../../Exceptions");
|
||||
import { AppConfiguration } from "../../configuration/Configuration";
|
||||
import Constants = require("../../../../../shared/constants");
|
||||
import { DomainExtractor } from "../../utils/DomainExtractor";
|
||||
import { ServerVariables } from "../../ServerVariables";
|
||||
import { MethodCalculator } from "../../authentication/MethodCalculator";
|
||||
import { IRequestLogger } from "../../logging/IRequestLogger";
|
||||
import { AuthenticationSession }
|
||||
from "../../../../types/AuthenticationSession";
|
||||
import { AuthenticationSessionHandler }
|
||||
from "../../AuthenticationSessionHandler";
|
||||
import AccessControl from "./access_control";
|
||||
|
||||
const FIRST_FACTOR_NOT_VALIDATED_MESSAGE = "First factor not yet validated";
|
||||
const SECOND_FACTOR_NOT_VALIDATED_MESSAGE = "Second factor not yet validated";
|
||||
|
||||
function verify_inactivity(req: Express.Request,
|
||||
authSession: AuthenticationSession,
|
||||
configuration: AppConfiguration, logger: IRequestLogger)
|
||||
: BluebirdPromise<void> {
|
||||
|
||||
// If inactivity is not specified, then inactivity timeout does not apply
|
||||
if (!configuration.session.inactivity) {
|
||||
return BluebirdPromise.resolve();
|
||||
}
|
||||
|
||||
const lastActivityTime = authSession.last_activity_datetime;
|
||||
const currentTime = new Date().getTime();
|
||||
authSession.last_activity_datetime = currentTime;
|
||||
|
||||
const inactivityPeriodMs = currentTime - lastActivityTime;
|
||||
logger.debug(req, "Inactivity period was %s s and max period was %s.",
|
||||
inactivityPeriodMs / 1000, configuration.session.inactivity / 1000);
|
||||
if (inactivityPeriodMs < configuration.session.inactivity) {
|
||||
return BluebirdPromise.resolve();
|
||||
}
|
||||
|
||||
logger.debug(req, "Session has been reset after too long inactivity period.");
|
||||
AuthenticationSessionHandler.reset(req);
|
||||
return BluebirdPromise.reject(new Error("Inactivity period exceeded."));
|
||||
}
|
||||
|
||||
export default function (req: Express.Request, res: Express.Response,
|
||||
vars: ServerVariables, authSession: AuthenticationSession)
|
||||
: BluebirdPromise<{ username: string, groups: string[] }> {
|
||||
let username: string;
|
||||
let groups: string[];
|
||||
let domain: string;
|
||||
let path: string;
|
||||
|
||||
return new BluebirdPromise(function (resolve, reject) {
|
||||
username = authSession.userid;
|
||||
groups = authSession.groups;
|
||||
|
||||
if (!authSession.userid) {
|
||||
reject(new Exceptions.AccessDeniedError(
|
||||
Util.format("%s: %s.", FIRST_FACTOR_NOT_VALIDATED_MESSAGE,
|
||||
"userid is missing")));
|
||||
return;
|
||||
}
|
||||
|
||||
const host = ObjectPath.get<Express.Request, string>(req, "headers.host");
|
||||
path =
|
||||
ObjectPath.get<Express.Request, string>(req, "headers.x-original-uri");
|
||||
|
||||
domain = DomainExtractor.fromHostHeader(host);
|
||||
const authenticationMethod =
|
||||
MethodCalculator.compute(vars.config.authentication_methods, domain);
|
||||
vars.logger.debug(req, "domain=%s, path=%s, user=%s, groups=%s", domain,
|
||||
path, username, groups.join(","));
|
||||
|
||||
if (!authSession.first_factor)
|
||||
return reject(new Exceptions.AccessDeniedError(
|
||||
Util.format("%s: %s.", FIRST_FACTOR_NOT_VALIDATED_MESSAGE,
|
||||
"first factor is false")));
|
||||
|
||||
if (authenticationMethod == "two_factor" && !authSession.second_factor)
|
||||
return reject(new Exceptions.AccessDeniedError(
|
||||
Util.format("%s: %s.", SECOND_FACTOR_NOT_VALIDATED_MESSAGE,
|
||||
"second factor is false")));
|
||||
|
||||
resolve();
|
||||
})
|
||||
.then(function () {
|
||||
return AccessControl(req, vars, domain, path, username, groups);
|
||||
})
|
||||
.then(function () {
|
||||
return verify_inactivity(req, authSession,
|
||||
vars.config, vars.logger);
|
||||
})
|
||||
.then(function () {
|
||||
return BluebirdPromise.resolve({
|
||||
username: authSession.userid,
|
||||
groups: authSession.groups
|
||||
});
|
||||
});
|
||||
}
|
|
@ -87,11 +87,13 @@ describe("test ldap authentication", function () {
|
|||
.returns(BluebirdPromise.resolve("cn=" + USERNAME + ",ou=users,dc=example,dc=com"));
|
||||
|
||||
// user connects successfully
|
||||
userClientStub.openStub.returns(BluebirdPromise.reject(new Error("Error while binding")));
|
||||
userClientStub.openStub.rejects(new Error("Error while binding"));
|
||||
userClientStub.closeStub.returns(BluebirdPromise.resolve());
|
||||
|
||||
return authenticator.authenticate(USERNAME, PASSWORD)
|
||||
.then(function () { return BluebirdPromise.reject("Should not be here!"); })
|
||||
.then(function () {
|
||||
return BluebirdPromise.reject("Should not be here!");
|
||||
})
|
||||
.catch(function () {
|
||||
return BluebirdPromise.resolve();
|
||||
});
|
||||
|
@ -118,10 +120,12 @@ describe("test ldap authentication", function () {
|
|||
adminClientStub.searchEmailsStub.returns(BluebirdPromise.resolve(["group1"]));
|
||||
// admin retrieves emails and groups of user
|
||||
adminClientStub.searchGroupsStub
|
||||
.returns(BluebirdPromise.reject(new Error("Error while retrieving emails and groups")));
|
||||
.rejects(new Error("Error while retrieving emails and groups"));
|
||||
|
||||
return authenticator.authenticate(USERNAME, PASSWORD)
|
||||
.then(function () { return BluebirdPromise.reject("Should not be here!"); })
|
||||
.then(function () {
|
||||
return BluebirdPromise.reject("Should not be here!");
|
||||
})
|
||||
.catch(function () {
|
||||
return BluebirdPromise.resolve();
|
||||
});
|
||||
|
|
|
@ -56,20 +56,25 @@ describe("test emails retriever", function () {
|
|||
});
|
||||
|
||||
describe("failure", function () {
|
||||
it("should fail retrieving emails when search operation fails", function () {
|
||||
clientFactoryStub.createStub.withArgs(ADMIN_USER_DN, ADMIN_PASSWORD)
|
||||
.returns(adminClientStub);
|
||||
it("should fail retrieving emails when search operation fails",
|
||||
function () {
|
||||
clientFactoryStub.createStub.withArgs(ADMIN_USER_DN, ADMIN_PASSWORD)
|
||||
.returns(adminClientStub);
|
||||
|
||||
// admin connects successfully
|
||||
adminClientStub.openStub.returns(BluebirdPromise.resolve());
|
||||
adminClientStub.closeStub.returns(BluebirdPromise.resolve());
|
||||
// admin connects successfully
|
||||
adminClientStub.openStub.returns(BluebirdPromise.resolve());
|
||||
adminClientStub.closeStub.returns(BluebirdPromise.resolve());
|
||||
|
||||
adminClientStub.searchEmailsStub.withArgs(USERNAME)
|
||||
.returns(BluebirdPromise.reject(new Error("Error while searching emails")));
|
||||
adminClientStub.searchEmailsStub.withArgs(USERNAME)
|
||||
.rejects(new Error("Error while searching emails"));
|
||||
|
||||
return emailsRetriever.retrieve(USERNAME)
|
||||
.then(function () { return BluebirdPromise.reject(new Error("Should not be here")); })
|
||||
.catch(function () { return BluebirdPromise.resolve(); });
|
||||
});
|
||||
return emailsRetriever.retrieve(USERNAME)
|
||||
.then(function () {
|
||||
return BluebirdPromise.reject(new Error("Should not be here"));
|
||||
})
|
||||
.catch(function () {
|
||||
return BluebirdPromise.resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,75 +0,0 @@
|
|||
|
||||
import { GroupsRetriever } from "../../src/lib/ldap/GroupsRetriever";
|
||||
import { LdapConfiguration } from "../../src/lib/configuration/Configuration";
|
||||
|
||||
import Sinon = require("sinon");
|
||||
import BluebirdPromise = require("bluebird");
|
||||
import Assert = require("assert");
|
||||
|
||||
import { ClientFactoryStub } from "../mocks/ldap/ClientFactoryStub";
|
||||
import { ClientStub } from "../mocks/ldap/ClientStub";
|
||||
|
||||
describe("test groups retriever", function () {
|
||||
const USERNAME = "username";
|
||||
const ADMIN_USER_DN = "cn=admin,dc=example,dc=com";
|
||||
const ADMIN_PASSWORD = "password";
|
||||
|
||||
let clientFactoryStub: ClientFactoryStub;
|
||||
let adminClientStub: ClientStub;
|
||||
|
||||
let groupsRetriever: GroupsRetriever;
|
||||
let ldapConfig: LdapConfiguration;
|
||||
|
||||
beforeEach(function () {
|
||||
clientFactoryStub = new ClientFactoryStub();
|
||||
adminClientStub = new ClientStub();
|
||||
|
||||
ldapConfig = {
|
||||
url: "http://ldap",
|
||||
user: ADMIN_USER_DN,
|
||||
password: ADMIN_PASSWORD,
|
||||
users_dn: "ou=users,dc=example,dc=com",
|
||||
groups_dn: "ou=groups,dc=example,dc=com",
|
||||
group_name_attribute: "cn",
|
||||
groups_filter: "member=cn={0},ou=users,dc=example,dc=com",
|
||||
mail_attribute: "mail",
|
||||
users_filter: "cn={0}"
|
||||
};
|
||||
|
||||
groupsRetriever = new GroupsRetriever(ldapConfig, clientFactoryStub);
|
||||
});
|
||||
|
||||
describe("success", function () {
|
||||
it("should retrieve groups successfully", function () {
|
||||
clientFactoryStub.createStub.withArgs(ADMIN_USER_DN, ADMIN_PASSWORD)
|
||||
.returns(adminClientStub);
|
||||
|
||||
// admin connects successfully
|
||||
adminClientStub.openStub.returns(BluebirdPromise.resolve());
|
||||
adminClientStub.closeStub.returns(BluebirdPromise.resolve());
|
||||
|
||||
adminClientStub.searchGroupsStub.withArgs(USERNAME)
|
||||
.returns(BluebirdPromise.resolve(["user@example.com"]));
|
||||
|
||||
return groupsRetriever.retrieve(USERNAME);
|
||||
});
|
||||
});
|
||||
|
||||
describe("failure", function () {
|
||||
it("should fail retrieving groups when search operation fails", function () {
|
||||
clientFactoryStub.createStub.withArgs(ADMIN_USER_DN, ADMIN_PASSWORD)
|
||||
.returns(adminClientStub);
|
||||
|
||||
// admin connects successfully
|
||||
adminClientStub.openStub.returns(BluebirdPromise.resolve());
|
||||
adminClientStub.closeStub.returns(BluebirdPromise.resolve());
|
||||
|
||||
adminClientStub.searchGroupsStub.withArgs(USERNAME)
|
||||
.returns(BluebirdPromise.reject(new Error("Error while searching groups")));
|
||||
|
||||
return groupsRetriever.retrieve(USERNAME)
|
||||
.then(function () { return BluebirdPromise.reject(new Error("Should not be here")); })
|
||||
.catch(function () { return BluebirdPromise.resolve(); });
|
||||
});
|
||||
});
|
||||
});
|
|
@ -37,6 +37,9 @@ describe("test password update", function () {
|
|||
};
|
||||
|
||||
ssha512HashGenerator = Sinon.stub(HashGenerator, "ssha512");
|
||||
clientFactoryStub.createStub.withArgs(ADMIN_USER_DN, ADMIN_PASSWORD)
|
||||
.returns(adminClientStub);
|
||||
|
||||
passwordUpdater = new PasswordUpdater(ldapConfig, clientFactoryStub);
|
||||
});
|
||||
|
||||
|
@ -46,11 +49,10 @@ describe("test password update", function () {
|
|||
|
||||
describe("success", function () {
|
||||
it("should update the password successfully", function () {
|
||||
clientFactoryStub.createStub.withArgs(ADMIN_USER_DN, ADMIN_PASSWORD)
|
||||
.returns(adminClientStub);
|
||||
|
||||
ssha512HashGenerator.returns("{CRYPT}$6$abcdefghijklm$AQmxaKfobGY9HSQa6aDYkAWOgPGNhGYn");
|
||||
adminClientStub.modifyPasswordStub.withArgs(USERNAME, NEW_PASSWORD).returns(BluebirdPromise.resolve());
|
||||
ssha512HashGenerator
|
||||
.returns("{CRYPT}$6$abcdefghijklm$AQmxaKfobGY9HSQa6aDYkAWOgPGNhGYn");
|
||||
adminClientStub.modifyPasswordStub.withArgs(USERNAME, NEW_PASSWORD)
|
||||
.returns(BluebirdPromise.resolve());
|
||||
adminClientStub.openStub.returns(BluebirdPromise.resolve());
|
||||
adminClientStub.closeStub.returns(BluebirdPromise.resolve());
|
||||
|
||||
|
@ -59,19 +61,22 @@ describe("test password update", function () {
|
|||
});
|
||||
|
||||
describe("failure", function () {
|
||||
it("should fail updating password when modify operation fails", function () {
|
||||
clientFactoryStub.createStub.withArgs(ADMIN_USER_DN, ADMIN_PASSWORD)
|
||||
.returns(adminClientStub);
|
||||
it("should fail updating password when modify operation fails",
|
||||
function () {
|
||||
ssha512HashGenerator
|
||||
.returns("{CRYPT}$6$abcdefghijklm$AQmxaKfobGY9HSQa6aDYkAWOgPGNhGYn");
|
||||
adminClientStub.modifyPasswordStub.withArgs(USERNAME, NEW_PASSWORD)
|
||||
.rejects(new Error("Error while updating password"));
|
||||
adminClientStub.openStub.returns(BluebirdPromise.resolve());
|
||||
adminClientStub.closeStub.returns(BluebirdPromise.resolve());
|
||||
|
||||
ssha512HashGenerator.returns("{CRYPT}$6$abcdefghijklm$AQmxaKfobGY9HSQa6aDYkAWOgPGNhGYn");
|
||||
adminClientStub.modifyPasswordStub.withArgs(USERNAME, NEW_PASSWORD)
|
||||
.returns(BluebirdPromise.reject(new Error("Error while updating password")));
|
||||
adminClientStub.openStub.returns(BluebirdPromise.resolve());
|
||||
adminClientStub.closeStub.returns(BluebirdPromise.resolve());
|
||||
|
||||
return passwordUpdater.updatePassword(USERNAME, NEW_PASSWORD)
|
||||
.then(function () { return BluebirdPromise.reject(new Error("should not be here")); })
|
||||
.catch(function () { return BluebirdPromise.resolve(); });
|
||||
});
|
||||
return passwordUpdater.updatePassword(USERNAME, NEW_PASSWORD)
|
||||
.then(function () {
|
||||
return BluebirdPromise.reject(new Error("should not be here"));
|
||||
})
|
||||
.catch(function(err: Error) {
|
||||
return BluebirdPromise.resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -28,190 +28,284 @@ describe("test /api/verify endpoint", function () {
|
|||
};
|
||||
AuthenticationSessionHandler.reset(req as any);
|
||||
req.headers.host = "secret.example.com";
|
||||
const s = ServerVariablesMockBuilder.build();
|
||||
const s = ServerVariablesMockBuilder.build(true);
|
||||
mocks = s.mocks;
|
||||
vars = s.variables;
|
||||
vars.config.authentication_methods.default_method = "two_factor";
|
||||
|
||||
authSession = AuthenticationSessionHandler.get(req as any, vars.logger);
|
||||
});
|
||||
|
||||
it("should be already authenticated", function () {
|
||||
mocks.accessController.isAccessAllowedMock.returns(true);
|
||||
authSession.first_factor = true;
|
||||
authSession.second_factor = true;
|
||||
authSession.userid = "myuser";
|
||||
authSession.groups = ["mygroup", "othergroup"];
|
||||
return VerifyGet.default(vars)(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]);
|
||||
});
|
||||
});
|
||||
|
||||
function test_session(_authSession: AuthenticationSession, status_code: number) {
|
||||
return VerifyGet.default(vars)(req as express.Request, res as any)
|
||||
.then(function () {
|
||||
Assert.equal(status_code, res.status.getCall(0).args[0]);
|
||||
});
|
||||
}
|
||||
|
||||
function test_non_authenticated_401(authSession: AuthenticationSession) {
|
||||
return test_session(authSession, 401);
|
||||
}
|
||||
|
||||
function test_unauthorized_403(authSession: AuthenticationSession) {
|
||||
return test_session(authSession, 403);
|
||||
}
|
||||
|
||||
function test_authorized(authSession: AuthenticationSession) {
|
||||
return test_session(authSession, 204);
|
||||
}
|
||||
|
||||
describe("given user tries to access a 2-factor endpoint", function () {
|
||||
before(function () {
|
||||
mocks.accessController.isAccessAllowedMock.returns(true);
|
||||
describe("with session cookie", function () {
|
||||
beforeEach(function () {
|
||||
vars.config.authentication_methods.default_method = "two_factor";
|
||||
});
|
||||
|
||||
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: [],
|
||||
last_activity_datetime: new Date().getTime()
|
||||
it("should be already authenticated", function () {
|
||||
mocks.accessController.isAccessAllowedMock.returns(true);
|
||||
authSession.first_factor = true;
|
||||
authSession.second_factor = true;
|
||||
authSession.userid = "myuser";
|
||||
authSession.groups = ["mygroup", "othergroup"];
|
||||
return VerifyGet.default(vars)(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]);
|
||||
});
|
||||
});
|
||||
|
||||
function test_session(_authSession: AuthenticationSession, status_code: number) {
|
||||
return VerifyGet.default(vars)(req as express.Request, res as any)
|
||||
.then(function () {
|
||||
Assert.equal(status_code, res.status.getCall(0).args[0]);
|
||||
});
|
||||
}
|
||||
|
||||
function test_non_authenticated_401(authSession: AuthenticationSession) {
|
||||
return test_session(authSession, 401);
|
||||
}
|
||||
|
||||
function test_unauthorized_403(authSession: AuthenticationSession) {
|
||||
return test_session(authSession, 403);
|
||||
}
|
||||
|
||||
function test_authorized(authSession: AuthenticationSession) {
|
||||
return test_session(authSession, 204);
|
||||
}
|
||||
|
||||
describe("given user tries to access a 2-factor endpoint", function () {
|
||||
before(function () {
|
||||
mocks.accessController.isAccessAllowedMock.returns(true);
|
||||
});
|
||||
|
||||
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: [],
|
||||
last_activity_datetime: new Date().getTime()
|
||||
});
|
||||
});
|
||||
|
||||
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: [],
|
||||
last_activity_datetime: new Date().getTime()
|
||||
});
|
||||
});
|
||||
|
||||
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: [],
|
||||
last_activity_datetime: new Date().getTime()
|
||||
});
|
||||
});
|
||||
|
||||
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: [],
|
||||
last_activity_datetime: new Date().getTime()
|
||||
});
|
||||
});
|
||||
|
||||
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 () {
|
||||
authSession.first_factor = true;
|
||||
authSession.second_factor = true;
|
||||
authSession.userid = "myuser";
|
||||
req.headers.host = "test.example.com";
|
||||
mocks.accessController.isAccessAllowedMock.returns(false);
|
||||
|
||||
return test_unauthorized_403({
|
||||
first_factor: true,
|
||||
second_factor: true,
|
||||
userid: "user",
|
||||
groups: ["group1", "group2"],
|
||||
email: undefined,
|
||||
last_activity_datetime: new Date().getTime()
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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: [],
|
||||
last_activity_datetime: new Date().getTime()
|
||||
});
|
||||
describe("given user tries to access a single factor endpoint", function () {
|
||||
beforeEach(function () {
|
||||
req.query = {
|
||||
redirect: "http://redirect.url"
|
||||
};
|
||||
req.headers["host"] = "redirect.url";
|
||||
mocks.config.authentication_methods.per_subdomain_methods = {
|
||||
"redirect.url": "single_factor"
|
||||
};
|
||||
});
|
||||
|
||||
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: [],
|
||||
last_activity_datetime: new Date().getTime()
|
||||
});
|
||||
it("should be authenticated when first factor is validated and second factor is not", function () {
|
||||
mocks.accessController.isAccessAllowedMock.returns(true);
|
||||
authSession.first_factor = true;
|
||||
authSession.userid = "user1";
|
||||
return VerifyGet.default(vars)(req as express.Request, res as any)
|
||||
.then(function () {
|
||||
Assert(res.status.calledWith(204));
|
||||
Assert(res.send.calledOnce);
|
||||
});
|
||||
});
|
||||
|
||||
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: [],
|
||||
last_activity_datetime: new Date().getTime()
|
||||
});
|
||||
it("should be rejected with 401 when first factor is not validated", function () {
|
||||
mocks.accessController.isAccessAllowedMock.returns(true);
|
||||
authSession.first_factor = false;
|
||||
return VerifyGet.default(vars)(req as express.Request, res as any)
|
||||
.then(function () {
|
||||
Assert(res.status.calledWith(401));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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 () {
|
||||
describe("inactivity period", function () {
|
||||
it("should update last inactivity period on requests on /api/verify", function () {
|
||||
mocks.config.session.inactivity = 200000;
|
||||
mocks.accessController.isAccessAllowedMock.returns(true);
|
||||
const currentTime = new Date().getTime() - 1000;
|
||||
AuthenticationSessionHandler.reset(req as any);
|
||||
authSession.first_factor = true;
|
||||
authSession.second_factor = true;
|
||||
authSession.userid = "myuser";
|
||||
req.headers.host = "test.example.com";
|
||||
mocks.accessController.isAccessAllowedMock.returns(false);
|
||||
authSession.groups = ["mygroup", "othergroup"];
|
||||
authSession.last_activity_datetime = currentTime;
|
||||
return VerifyGet.default(vars)(req as express.Request, res as any)
|
||||
.then(function () {
|
||||
return AuthenticationSessionHandler.get(req as any, vars.logger);
|
||||
})
|
||||
.then(function (authSession) {
|
||||
Assert(authSession.last_activity_datetime > currentTime);
|
||||
});
|
||||
});
|
||||
|
||||
return test_unauthorized_403({
|
||||
first_factor: true,
|
||||
second_factor: true,
|
||||
userid: "user",
|
||||
groups: ["group1", "group2"],
|
||||
email: undefined,
|
||||
last_activity_datetime: new Date().getTime()
|
||||
});
|
||||
it("should reset session when max inactivity period has been reached", function () {
|
||||
mocks.config.session.inactivity = 1;
|
||||
mocks.accessController.isAccessAllowedMock.returns(true);
|
||||
const currentTime = new Date().getTime() - 1000;
|
||||
AuthenticationSessionHandler.reset(req as any);
|
||||
authSession.first_factor = true;
|
||||
authSession.second_factor = true;
|
||||
authSession.userid = "myuser";
|
||||
authSession.groups = ["mygroup", "othergroup"];
|
||||
authSession.last_activity_datetime = currentTime;
|
||||
return VerifyGet.default(vars)(req as express.Request, res as any)
|
||||
.then(function () {
|
||||
return AuthenticationSessionHandler.get(req as any, vars.logger);
|
||||
})
|
||||
.then(function (authSession) {
|
||||
Assert.equal(authSession.first_factor, false);
|
||||
Assert.equal(authSession.second_factor, false);
|
||||
Assert.equal(authSession.userid, undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("given user tries to access a basic auth endpoint", function () {
|
||||
beforeEach(function () {
|
||||
req.query = {
|
||||
redirect: "http://redirect.url"
|
||||
};
|
||||
req.headers["host"] = "redirect.url";
|
||||
describe("with basic auth", function () {
|
||||
it("should authenticate correctly", function () {
|
||||
mocks.accessController.isAccessAllowedMock.returns(true);
|
||||
mocks.config.authentication_methods.default_method = "single_factor";
|
||||
mocks.ldapAuthenticator.authenticateStub.returns({
|
||||
groups: ["mygroup", "othergroup"],
|
||||
});
|
||||
req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA==";
|
||||
|
||||
return VerifyGet.default(vars)(req as express.Request, res as any)
|
||||
.then(function () {
|
||||
Sinon.assert.calledWithExactly(res.setHeader, "Remote-User", "john");
|
||||
Sinon.assert.calledWithExactly(res.setHeader, "Remote-Groups", "mygroup,othergroup");
|
||||
Assert.equal(204, res.status.getCall(0).args[0]);
|
||||
});
|
||||
});
|
||||
|
||||
it("should fail when endpoint is protected by two factors", function () {
|
||||
mocks.accessController.isAccessAllowedMock.returns(true);
|
||||
mocks.config.authentication_methods.default_method = "single_factor";
|
||||
mocks.config.authentication_methods.per_subdomain_methods = {
|
||||
"redirect.url": "single_factor"
|
||||
"secret.example.com": "two_factor"
|
||||
};
|
||||
});
|
||||
mocks.ldapAuthenticator.authenticateStub.resolves({
|
||||
groups: ["mygroup", "othergroup"],
|
||||
});
|
||||
req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA==";
|
||||
|
||||
it("should be authenticated when first factor is validated and second factor is not", function () {
|
||||
mocks.accessController.isAccessAllowedMock.returns(true);
|
||||
authSession.first_factor = true;
|
||||
authSession.userid = "user1";
|
||||
return VerifyGet.default(vars)(req as express.Request, res as any)
|
||||
.then(function () {
|
||||
Assert(res.status.calledWith(204));
|
||||
Assert(res.send.calledOnce);
|
||||
Assert(res.status.calledWithExactly(401));
|
||||
});
|
||||
});
|
||||
|
||||
it("should be rejected with 401 when first factor is not validated", function () {
|
||||
it("should fail when base64 token is not valid", function () {
|
||||
mocks.accessController.isAccessAllowedMock.returns(true);
|
||||
authSession.first_factor = false;
|
||||
return VerifyGet.default(vars)(req as express.Request, res as any)
|
||||
.then(function () {
|
||||
Assert(res.status.calledWith(401));
|
||||
});
|
||||
});
|
||||
});
|
||||
mocks.config.authentication_methods.default_method = "single_factor";
|
||||
mocks.ldapAuthenticator.authenticateStub.resolves({
|
||||
groups: ["mygroup", "othergroup"],
|
||||
});
|
||||
req.headers["proxy-authorization"] = "Basic i_m*not_a_base64*token";
|
||||
|
||||
describe("inactivity period", function () {
|
||||
it("should update last inactivity period on requests on /api/verify", function () {
|
||||
mocks.config.session.inactivity = 200000;
|
||||
mocks.accessController.isAccessAllowedMock.returns(true);
|
||||
const currentTime = new Date().getTime() - 1000;
|
||||
AuthenticationSessionHandler.reset(req as any);
|
||||
authSession.first_factor = true;
|
||||
authSession.second_factor = true;
|
||||
authSession.userid = "myuser";
|
||||
authSession.groups = ["mygroup", "othergroup"];
|
||||
authSession.last_activity_datetime = currentTime;
|
||||
return VerifyGet.default(vars)(req as express.Request, res as any)
|
||||
.then(function () {
|
||||
return AuthenticationSessionHandler.get(req as any, vars.logger);
|
||||
})
|
||||
.then(function (authSession) {
|
||||
Assert(authSession.last_activity_datetime > currentTime);
|
||||
Assert(res.status.calledWithExactly(401));
|
||||
});
|
||||
});
|
||||
|
||||
it("should reset session when max inactivity period has been reached", function () {
|
||||
mocks.config.session.inactivity = 1;
|
||||
it("should fail when base64 token has not format user:psswd", function () {
|
||||
mocks.accessController.isAccessAllowedMock.returns(true);
|
||||
const currentTime = new Date().getTime() - 1000;
|
||||
AuthenticationSessionHandler.reset(req as any);
|
||||
authSession.first_factor = true;
|
||||
authSession.second_factor = true;
|
||||
authSession.userid = "myuser";
|
||||
authSession.groups = ["mygroup", "othergroup"];
|
||||
authSession.last_activity_datetime = currentTime;
|
||||
mocks.config.authentication_methods.default_method = "single_factor";
|
||||
mocks.ldapAuthenticator.authenticateStub.resolves({
|
||||
groups: ["mygroup", "othergroup"],
|
||||
});
|
||||
req.headers["proxy-authorization"] = "Basic am9objpwYXNzOmJhZA==";
|
||||
|
||||
return VerifyGet.default(vars)(req as express.Request, res as any)
|
||||
.then(function () {
|
||||
return AuthenticationSessionHandler.get(req as any, vars.logger);
|
||||
})
|
||||
.then(function (authSession) {
|
||||
Assert.equal(authSession.first_factor, false);
|
||||
Assert.equal(authSession.second_factor, false);
|
||||
Assert.equal(authSession.userid, undefined);
|
||||
Assert(res.status.calledWithExactly(401));
|
||||
});
|
||||
});
|
||||
|
||||
it("should fail when bad user password is provided", function () {
|
||||
mocks.accessController.isAccessAllowedMock.returns(true);
|
||||
mocks.config.authentication_methods.default_method = "single_factor";
|
||||
mocks.ldapAuthenticator.authenticateStub.rejects(new Error(
|
||||
"Invalid credentials"));
|
||||
req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA==";
|
||||
|
||||
return VerifyGet.default(vars)(req as express.Request, res as any)
|
||||
.then(function () {
|
||||
Assert(res.status.calledWithExactly(401));
|
||||
});
|
||||
});
|
||||
|
||||
it("should fail when resource is restricted", function () {
|
||||
mocks.accessController.isAccessAllowedMock.returns(false);
|
||||
mocks.config.authentication_methods.default_method = "single_factor";
|
||||
mocks.ldapAuthenticator.authenticateStub.resolves({
|
||||
groups: ["mygroup", "othergroup"],
|
||||
});
|
||||
req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA==";
|
||||
|
||||
return VerifyGet.default(vars)(req as express.Request, res as any)
|
||||
.then(function () {
|
||||
Assert(res.status.calledWithExactly(401));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,3 +4,8 @@ Feature: User and groups headers are correctly forwarded to backend
|
|||
When I visit "https://public.test.local:8080/headers"
|
||||
Then I see header "Custom-Forwarded-User" set to "john"
|
||||
Then I see header "Custom-Forwarded-Groups" set to "dev,admin"
|
||||
|
||||
Scenario: Custom-Forwarded-User and Custom-Forwarded-Groups are correctly forwarded to protected backend when basic auth is used
|
||||
When I request "https://single_factor.test.local:8080/headers" with username "john" and password "password" using basic authentication
|
||||
Then I received header "Custom-Forwarded-User" set to "john"
|
||||
And I received header "Custom-Forwarded-Groups" set to "dev,admin"
|
|
@ -1,13 +1,15 @@
|
|||
Feature: User can access certain subdomains with single factor
|
||||
|
||||
@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%2Fsingle_factor.test.local%3A8080%2Fsecret.html"
|
||||
And I login with user "john" and password "password"
|
||||
Then I'm redirected to "https://single_factor.test.local:8080/secret.html"
|
||||
|
||||
@need-registered-user-john
|
||||
Scenario: Redirection after first factor fails if single_factor not allowed. It redirects user to first factor.
|
||||
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/?redirect=https%3A%2F%2Fadmin.test.local%3A8080%2Fsecret.html"
|
||||
|
||||
Scenario: User can login using basic authentication
|
||||
When I request "https://single_factor.test.local:8080/secret.html" with username "john" and password "password" using basic authentication
|
||||
Then I receive the secret page
|
|
@ -10,7 +10,7 @@ Cucumber.defineSupportCode(function ({ Given, When, Then }) {
|
|||
function (expectedHeaderName: string, expectedValue: string) {
|
||||
return this.driver.findElement(seleniumWebdriver.By.tagName("body")).getText()
|
||||
.then(function (txt: string) {
|
||||
const expectedLine = Util.format("\"%s\": \"%s\"", expectedHeaderName, expectedValue);
|
||||
const expectedLine = Util.format("\"%s\": \"%s\"", expectedHeaderName, expectedValue);
|
||||
if (txt.indexOf(expectedLine) > 0)
|
||||
return BluebirdPromise.resolve();
|
||||
else
|
||||
|
|
39
test/features/step_definitions/single-factor.ts
Normal file
39
test/features/step_definitions/single-factor.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
import Cucumber = require("cucumber");
|
||||
import seleniumWebdriver = require("selenium-webdriver");
|
||||
import Request = require("request-promise");
|
||||
import BluebirdPromise = require("bluebird");
|
||||
import Util = require("util");
|
||||
|
||||
Cucumber.defineSupportCode(function ({ Given, When, Then }) {
|
||||
When("I request {stringInDoubleQuotes} with username {stringInDoubleQuotes}" +
|
||||
" and password {stringInDoubleQuotes} using basic authentication",
|
||||
function (url: string, username: string, password: string) {
|
||||
const that = this;
|
||||
return Request(url, {
|
||||
auth: {
|
||||
username: username,
|
||||
password: password
|
||||
},
|
||||
resolveWithFullResponse: true
|
||||
})
|
||||
.then(function (response: any) {
|
||||
that.response = response;
|
||||
});
|
||||
});
|
||||
|
||||
Then("I receive the secret page", function () {
|
||||
if (this.response.body.match("This is a very important secret!"))
|
||||
return BluebirdPromise.resolve();
|
||||
return BluebirdPromise.reject(new Error("Secret page not received."));
|
||||
});
|
||||
|
||||
Then("I received header {stringInDoubleQuotes} set to {stringInDoubleQuotes}",
|
||||
function (expectedHeaderName: string, expectedValue: string) {
|
||||
const expectedLine = Util.format("\"%s\": \"%s\"", expectedHeaderName,
|
||||
expectedValue);
|
||||
if (this.response.body.indexOf(expectedLine) > 0)
|
||||
return BluebirdPromise.resolve();
|
||||
return BluebirdPromise.reject(new Error(
|
||||
Util.format("No such header or with unexpected value.")));
|
||||
})
|
||||
});
|
Loading…
Reference in New Issue
Block a user