Merge pull request #75 from clems4ever/ldap-filters
Add LDAP filters to configuration file for flexibility and rework authentication regulationpull/80/head
commit
84c13c71e2
|
@ -29,4 +29,3 @@ dist/
|
||||||
|
|
||||||
# Specific files
|
# Specific files
|
||||||
/config.yml
|
/config.yml
|
||||||
/test/integration/nginx.conf
|
|
||||||
|
|
|
@ -151,7 +151,7 @@ In **Authelia**, you need to register a per user TOTP (Time-Based One Time Passw
|
||||||
authenticating. To do that, you need to click on the register button. It will
|
authenticating. To do that, you need to click on the register button. It will
|
||||||
send a link to the user email address. Since this is an example, no email will
|
send a link to the user email address. Since this is an example, no email will
|
||||||
be sent, the link is rather delivered in the file
|
be sent, the link is rather delivered in the file
|
||||||
**./notifications/notification.txt**. Paste the link in your browser and you'll get
|
**/tmp/notifications/notification.txt**. Paste the link in your browser and you'll get
|
||||||
your secret in QRCode and Base32 formats. You can use
|
your secret in QRCode and Base32 formats. You can use
|
||||||
[Google Authenticator]
|
[Google Authenticator]
|
||||||
to store them and get the generated tokens with the app.
|
to store them and get the generated tokens with the app.
|
||||||
|
@ -166,7 +166,7 @@ already available for Google, Facebook, Github accounts and more.
|
||||||
Like TOTP, U2F requires you register your security key before authenticating.
|
Like TOTP, U2F requires you register your security key before authenticating.
|
||||||
To do so, click on the register button. This will send a link to the
|
To do so, click on the register button. This will send a link to the
|
||||||
user email address. Since this is an example, no email will be sent, the
|
user email address. Since this is an example, no email will be sent, the
|
||||||
link is rather delivered in the file **./notifications/notification.txt**. Paste
|
link is rather delivered in the file **/tmp/notifications/notification.txt**. Paste
|
||||||
the link in your browser and you'll be asking to touch the token of your device
|
the link in your browser and you'll be asking to touch the token of your device
|
||||||
to register. Upon successful registration, you can authenticate using your U2F
|
to register. Upon successful registration, you can authenticate using your U2F
|
||||||
device by simply touching the token. Easy, right?!
|
device by simply touching the token. Easy, right?!
|
||||||
|
@ -178,7 +178,7 @@ With **Authelia**, you can also reset your password in no time. Click on the
|
||||||
**Forgot password?** link in the login page, provide the username of the user requiring
|
**Forgot password?** link in the login page, provide the username of the user requiring
|
||||||
a password reset and **Authelia** will send an email with an link to the user
|
a password reset and **Authelia** will send an email with an link to the user
|
||||||
email address. For the sake of the example, the email is delivered in the file
|
email address. For the sake of the example, the email is delivered in the file
|
||||||
**./notifications/notification.txt**.
|
**/tmp/notifications/notification.txt**.
|
||||||
Paste the link in your browser and you should be able to reset the password.
|
Paste the link in your browser and you should be able to reset the password.
|
||||||
|
|
||||||
<img src="https://raw.githubusercontent.com/clems4ever/authelia/master/images/reset_password.png" width="400">
|
<img src="https://raw.githubusercontent.com/clems4ever/authelia/master/images/reset_password.png" width="400">
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
###############################################################
|
||||||
|
# Authelia configuration #
|
||||||
|
###############################################################
|
||||||
|
|
||||||
# The port to listen on
|
# The port to listen on
|
||||||
port: 80
|
port: 80
|
||||||
|
@ -18,17 +21,27 @@ ldap:
|
||||||
base_dn: dc=example,dc=com
|
base_dn: dc=example,dc=com
|
||||||
|
|
||||||
# An additional dn to define the scope to all users
|
# An additional dn to define the scope to all users
|
||||||
additional_user_dn: ou=users
|
additional_users_dn: ou=users
|
||||||
|
|
||||||
# The user name attribute of users. Might uid for FreeIPA. 'cn' by default.
|
# The users filter.
|
||||||
user_name_attribute: cn
|
# {0} is the matcher replaced by username.
|
||||||
|
# 'cn={0}' by default.
|
||||||
|
users_filter: cn={0}
|
||||||
|
|
||||||
# An additional dn to define the scope of groups
|
# An additional dn to define the scope of groups
|
||||||
additional_group_dn: ou=groups
|
additional_groups_dn: ou=groups
|
||||||
|
|
||||||
# The group name attribute of group. 'cn' by default.
|
# The groups filter.
|
||||||
|
# {0} is the matcher replaced by user dn.
|
||||||
|
# 'member={0}' by default.
|
||||||
|
groups_filter: (&(member={0})(objectclass=groupOfNames))
|
||||||
|
|
||||||
|
# The attribute holding the name of the group
|
||||||
group_name_attribute: cn
|
group_name_attribute: cn
|
||||||
|
|
||||||
|
# The attribute holding the mail address of the user
|
||||||
|
mail_attribute: mail
|
||||||
|
|
||||||
# The username and password of the admin user.
|
# The username and password of the admin user.
|
||||||
user: cn=admin,dc=example,dc=com
|
user: cn=admin,dc=example,dc=com
|
||||||
password: password
|
password: password
|
||||||
|
@ -39,22 +52,30 @@ ldap:
|
||||||
# Access control is a set of rules you can use to restrict the user access.
|
# Access control is a set of rules you can use to restrict the user access.
|
||||||
# Default (anyone), per-user or per-group rules can be defined.
|
# Default (anyone), per-user or per-group rules can be defined.
|
||||||
#
|
#
|
||||||
# If 'access_control' is not defined, ACL rules are disabled and default policy
|
# If 'access_control' is not defined, ACL rules are disabled and a default policy
|
||||||
# is applied, i.e., access is allowed to anyone. Otherwise restrictions follow
|
# is applied, i.e., access is allowed to anyone. Otherwise restrictions follow
|
||||||
# the rules defined below.
|
# the rules defined below.
|
||||||
# If no rule is provided, all domains are denied.
|
# If no rule is provided, all domains are denied.
|
||||||
#
|
#
|
||||||
# '*' means 'any' subdomains and matches any string. It must stand at the
|
# One can use the wildcard * to match any subdomain.
|
||||||
# beginning of the pattern.
|
# Note 1: It must stand at the beginning of the pattern. (example: *.mydomain.com)
|
||||||
|
# Note 2: You must put the pattern in simple quotes when using the wildcard.
|
||||||
access_control:
|
access_control:
|
||||||
|
# The default policy. Applies to any user
|
||||||
default:
|
default:
|
||||||
- public.test.local
|
- public.test.local
|
||||||
|
|
||||||
|
# Group based policies. The key is a group name and the value
|
||||||
|
# is the domain to allow access to.
|
||||||
groups:
|
groups:
|
||||||
admin:
|
admin:
|
||||||
- '*.test.local'
|
- '*.test.local'
|
||||||
dev:
|
dev:
|
||||||
- secret.test.local
|
- secret.test.local
|
||||||
- secret2.test.local
|
- secret2.test.local
|
||||||
|
|
||||||
|
# Group based policies. The key is a group name and the value
|
||||||
|
# is the domain to allow access to.
|
||||||
users:
|
users:
|
||||||
harry:
|
harry:
|
||||||
- secret1.test.local
|
- secret1.test.local
|
||||||
|
@ -64,19 +85,43 @@ access_control:
|
||||||
|
|
||||||
# Configuration of session cookies
|
# Configuration of session cookies
|
||||||
#
|
#
|
||||||
# _secret_ the secret to encrypt session cookies
|
# The session cookies identify the user once logged in.
|
||||||
# _expiration_ the time before cookies expire
|
|
||||||
# _domain_ the domain to protect.
|
|
||||||
# Note: the authenticator must also be in that domain. If empty, the cookie
|
|
||||||
# is restricted to the subdomain of the issuer.
|
|
||||||
session:
|
session:
|
||||||
|
# The secret to encrypt the session cookie.
|
||||||
secret: unsecure_secret
|
secret: unsecure_secret
|
||||||
|
|
||||||
|
# The time before the cookie expires.
|
||||||
expiration: 3600000
|
expiration: 3600000
|
||||||
|
|
||||||
|
# The domain to protect.
|
||||||
|
# Note: the authenticator must also be in that domain. If empty, the cookie
|
||||||
|
# is restricted to the subdomain of the issuer.
|
||||||
domain: test.local
|
domain: test.local
|
||||||
|
|
||||||
|
# The redis connection details
|
||||||
redis:
|
redis:
|
||||||
host: redis
|
host: redis
|
||||||
port: 6379
|
port: 6379
|
||||||
|
|
||||||
|
# Configuration of the authentication regulation mechanism.
|
||||||
|
#
|
||||||
|
# This mechanism prevents attackers from brute forcing the first factor.
|
||||||
|
# It bans the user if too many attempts are done in a short period of
|
||||||
|
# time.
|
||||||
|
regulation:
|
||||||
|
# The number of failed login attempts before user is banned.
|
||||||
|
# Set it to 0 for disabling regulation.
|
||||||
|
max_retries: 3
|
||||||
|
|
||||||
|
# The length of time between login attempts before user is banned.
|
||||||
|
find_time: 120
|
||||||
|
|
||||||
|
# The length of time before a banned user can login again.
|
||||||
|
ban_time: 300
|
||||||
|
|
||||||
|
# Configuration of the storage backend used to store data and secrets.
|
||||||
|
#
|
||||||
|
# You must use only an available configuration: local, mongo
|
||||||
storage:
|
storage:
|
||||||
# The directory where the DB files will be saved
|
# The directory where the DB files will be saved
|
||||||
# local: /var/lib/authelia/store
|
# local: /var/lib/authelia/store
|
||||||
|
@ -85,9 +130,11 @@ storage:
|
||||||
mongo:
|
mongo:
|
||||||
url: mongodb://mongo/authelia
|
url: mongodb://mongo/authelia
|
||||||
|
|
||||||
|
# Configuration of the notification system.
|
||||||
|
#
|
||||||
# Notifications are sent to users when they require a password reset, a u2f
|
# Notifications are sent to users when they require a password reset, a u2f
|
||||||
# registration or a TOTP registration.
|
# registration or a TOTP registration.
|
||||||
# Use only one available configuration: filesystem, gmail
|
# Use only an available configuration: filesystem, gmail
|
||||||
notifier:
|
notifier:
|
||||||
# For testing purpose, notifications can be sent in a file
|
# For testing purpose, notifications can be sent in a file
|
||||||
filesystem:
|
filesystem:
|
||||||
|
|
|
@ -0,0 +1,147 @@
|
||||||
|
###############################################################
|
||||||
|
# Authelia configuration #
|
||||||
|
###############################################################
|
||||||
|
|
||||||
|
# The port to listen on
|
||||||
|
port: 80
|
||||||
|
|
||||||
|
# Log level
|
||||||
|
#
|
||||||
|
# Level of verbosity for logs
|
||||||
|
logs_level: debug
|
||||||
|
|
||||||
|
# LDAP configuration
|
||||||
|
#
|
||||||
|
# Example: for user john, the DN will be cn=john,ou=users,dc=example,dc=com
|
||||||
|
ldap:
|
||||||
|
# The url of the ldap server
|
||||||
|
url: ldap://openldap
|
||||||
|
|
||||||
|
# The base dn for every entries
|
||||||
|
base_dn: dc=example,dc=com
|
||||||
|
|
||||||
|
# An additional dn to define the scope to all users
|
||||||
|
additional_users_dn: ou=users
|
||||||
|
|
||||||
|
# The users filter.
|
||||||
|
# {0} is the matcher replaced by username.
|
||||||
|
# 'cn={0}' by default.
|
||||||
|
users_filter: cn={0}
|
||||||
|
|
||||||
|
# An additional dn to define the scope of groups
|
||||||
|
additional_groups_dn: ou=groups
|
||||||
|
|
||||||
|
# The groups filter.
|
||||||
|
# {0} is the matcher replaced by user dn.
|
||||||
|
# 'member={0}' by default.
|
||||||
|
groups_filter: (&(member={0})(objectclass=groupOfNames))
|
||||||
|
|
||||||
|
# The attribute holding the name of the group
|
||||||
|
group_name_attribute: cn
|
||||||
|
|
||||||
|
# The attribute holding the mail address of the user
|
||||||
|
mail_attribute: mail
|
||||||
|
|
||||||
|
# The username and password of the admin user.
|
||||||
|
user: cn=admin,dc=example,dc=com
|
||||||
|
password: password
|
||||||
|
|
||||||
|
|
||||||
|
# Access Control
|
||||||
|
#
|
||||||
|
# Access control is a set of rules you can use to restrict the user access.
|
||||||
|
# Default (anyone), per-user or per-group rules can be defined.
|
||||||
|
#
|
||||||
|
# If 'access_control' is not defined, ACL rules are disabled and a default policy
|
||||||
|
# is applied, i.e., access is allowed to anyone. Otherwise restrictions follow
|
||||||
|
# the rules defined below.
|
||||||
|
# If no rule is provided, all domains are denied.
|
||||||
|
#
|
||||||
|
# One can use the wildcard * to match any subdomain.
|
||||||
|
# Note 1: It must stand at the beginning of the pattern. (example: *.mydomain.com)
|
||||||
|
# Note 2: You must put the pattern in simple quotes when using the wildcard.
|
||||||
|
access_control:
|
||||||
|
# The default policy. Applies to any user
|
||||||
|
default:
|
||||||
|
- public.test.local
|
||||||
|
|
||||||
|
# Group based policies. The key is a group name and the value
|
||||||
|
# is the domain to allow access to.
|
||||||
|
groups:
|
||||||
|
admin:
|
||||||
|
- '*.test.local'
|
||||||
|
dev:
|
||||||
|
- secret.test.local
|
||||||
|
- secret2.test.local
|
||||||
|
|
||||||
|
# Group based policies. The key is a group name and the value
|
||||||
|
# is the domain to allow access to.
|
||||||
|
users:
|
||||||
|
harry:
|
||||||
|
- secret1.test.local
|
||||||
|
bob:
|
||||||
|
- '*.mail.test.local'
|
||||||
|
|
||||||
|
|
||||||
|
# Configuration of session cookies
|
||||||
|
#
|
||||||
|
# The session cookies identify the user once logged in.
|
||||||
|
session:
|
||||||
|
# The secret to encrypt the session cookie.
|
||||||
|
secret: unsecure_secret
|
||||||
|
|
||||||
|
# The time before the cookie expires.
|
||||||
|
expiration: 3600000
|
||||||
|
|
||||||
|
# The domain to protect.
|
||||||
|
# Note: the authenticator must also be in that domain. If empty, the cookie
|
||||||
|
# is restricted to the subdomain of the issuer.
|
||||||
|
domain: test.local
|
||||||
|
|
||||||
|
# The redis connection details
|
||||||
|
redis:
|
||||||
|
host: redis
|
||||||
|
port: 6379
|
||||||
|
|
||||||
|
# Configuration of the authentication regulation mechanism.
|
||||||
|
#
|
||||||
|
# This mechanism prevents attackers from brute forcing the first factor.
|
||||||
|
# It bans the user if too many attempts are done in a short period of
|
||||||
|
# time.
|
||||||
|
regulation:
|
||||||
|
# The number of failed login attempts before user is banned.
|
||||||
|
# Set it to 0 for disabling regulation.
|
||||||
|
max_retries: 3
|
||||||
|
|
||||||
|
# The length of time between login attempts before user is banned.
|
||||||
|
find_time: 15
|
||||||
|
|
||||||
|
# The length of time before a banned user can login again.
|
||||||
|
ban_time: 4
|
||||||
|
|
||||||
|
# Configuration of the storage backend used to store data and secrets.
|
||||||
|
#
|
||||||
|
# You must use only an available configuration: local, mongo
|
||||||
|
storage:
|
||||||
|
# The directory where the DB files will be saved
|
||||||
|
# local: /var/lib/authelia/store
|
||||||
|
|
||||||
|
# Settings to connect to mongo server
|
||||||
|
mongo:
|
||||||
|
url: mongodb://mongo/authelia
|
||||||
|
|
||||||
|
# Configuration of the notification system.
|
||||||
|
#
|
||||||
|
# Notifications are sent to users when they require a password reset, a u2f
|
||||||
|
# registration or a TOTP registration.
|
||||||
|
# Use only an available configuration: filesystem, gmail
|
||||||
|
notifier:
|
||||||
|
# For testing purpose, notifications can be sent in a file
|
||||||
|
filesystem:
|
||||||
|
filename: /var/lib/authelia/notifications/notification.txt
|
||||||
|
|
||||||
|
# Use your gmail account to send the notifications. You can use an app password.
|
||||||
|
# gmail:
|
||||||
|
# username: user@example.com
|
||||||
|
# password: yourpassword
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
version: '2'
|
||||||
|
services:
|
||||||
|
authelia:
|
||||||
|
volumes:
|
||||||
|
- ./config.test.yml:/etc/authelia/config.yml:ro
|
|
@ -5,7 +5,7 @@ services:
|
||||||
restart: always
|
restart: always
|
||||||
volumes:
|
volumes:
|
||||||
- ./config.template.yml:/etc/authelia/config.yml:ro
|
- ./config.template.yml:/etc/authelia/config.yml:ro
|
||||||
- ./notifications:/var/lib/authelia/notifications
|
- /tmp/notifications:/var/lib/authelia/notifications
|
||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
- redis
|
||||||
networks:
|
networks:
|
||||||
|
|
|
@ -52,3 +52,11 @@ objectclass: top
|
||||||
mail: james.dean@example.com
|
mail: james.dean@example.com
|
||||||
sn: James Dean
|
sn: James Dean
|
||||||
userpassword: {SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=
|
userpassword: {SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=
|
||||||
|
|
||||||
|
dn: cn=blackhat,ou=users,dc=example,dc=com
|
||||||
|
cn: blackhat
|
||||||
|
objectclass: inetOrgPerson
|
||||||
|
objectclass: top
|
||||||
|
mail: billy.blackhat@example.com
|
||||||
|
sn: Billy BlackHat
|
||||||
|
userpassword: {SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=
|
||||||
|
|
|
@ -9,4 +9,5 @@ docker-compose \
|
||||||
-f example/mongo/docker-compose.yml \
|
-f example/mongo/docker-compose.yml \
|
||||||
-f example/redis/docker-compose.yml \
|
-f example/redis/docker-compose.yml \
|
||||||
-f example/nginx/docker-compose.yml \
|
-f example/nginx/docker-compose.yml \
|
||||||
|
-f example/ldap/docker-compose.admin.yml \
|
||||||
-f example/ldap/docker-compose.yml $*
|
-f example/ldap/docker-compose.yml $*
|
||||||
|
|
|
@ -3,28 +3,66 @@
|
||||||
import util = require("util");
|
import util = require("util");
|
||||||
import { INotifier, Handlers } from "./INotifier";
|
import { INotifier, Handlers } from "./INotifier";
|
||||||
|
|
||||||
export class Notifier implements INotifier {
|
class NotificationEvent {
|
||||||
private element: JQuery;
|
private element: JQuery;
|
||||||
|
private message: string;
|
||||||
|
private statusType: string;
|
||||||
|
private timeoutId: any;
|
||||||
|
|
||||||
constructor(selector: string, $: JQueryStatic) {
|
constructor(element: JQuery, msg: string, statusType: string) {
|
||||||
this.element = $(selector);
|
this.message = msg;
|
||||||
|
this.statusType = statusType;
|
||||||
|
this.element = element;
|
||||||
}
|
}
|
||||||
|
|
||||||
private displayAndFadeout(msg: string, statusType: string, handlers?: Handlers): void {
|
private clearNotification() {
|
||||||
|
this.element.removeClass(this.statusType);
|
||||||
|
this.element.html("");
|
||||||
|
}
|
||||||
|
|
||||||
|
start(handlers?: Handlers) {
|
||||||
const that = this;
|
const that = this;
|
||||||
const FADE_TIME = 500;
|
const FADE_TIME = 500;
|
||||||
const html = util.format('<i><img src="/img/notifications/%s.png" alt="status %s"/></i>\
|
const html = util.format('<i><img src="/img/notifications/%s.png" alt="status %s"/></i>\
|
||||||
<span>%s</span>', statusType, statusType, msg);
|
<span>%s</span>', this.statusType, this.statusType, this.message);
|
||||||
this.element.html(html);
|
this.element.html(html);
|
||||||
this.element.addClass(statusType);
|
this.element.addClass(this.statusType);
|
||||||
this.element.fadeIn(FADE_TIME, function() {
|
this.element.fadeIn(FADE_TIME, function () {
|
||||||
handlers.onFadedIn();
|
if (handlers)
|
||||||
})
|
handlers.onFadedIn();
|
||||||
.delay(4000)
|
|
||||||
.fadeOut(FADE_TIME, function() {
|
|
||||||
that.element.removeClass(statusType);
|
|
||||||
handlers.onFadedOut();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.timeoutId = setTimeout(function () {
|
||||||
|
that.element.fadeOut(FADE_TIME, function () {
|
||||||
|
that.clearNotification();
|
||||||
|
if (handlers)
|
||||||
|
handlers.onFadedOut();
|
||||||
|
});
|
||||||
|
}, 4000);
|
||||||
|
}
|
||||||
|
|
||||||
|
interrupt() {
|
||||||
|
this.clearNotification();
|
||||||
|
this.element.hide();
|
||||||
|
clearTimeout(this.timeoutId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Notifier implements INotifier {
|
||||||
|
private element: JQuery;
|
||||||
|
private onGoingEvent: NotificationEvent;
|
||||||
|
|
||||||
|
constructor(selector: string, $: JQueryStatic) {
|
||||||
|
this.element = $(selector);
|
||||||
|
this.onGoingEvent = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
private displayAndFadeout(msg: string, statusType: string, handlers?: Handlers): void {
|
||||||
|
if (this.onGoingEvent)
|
||||||
|
this.onGoingEvent.interrupt();
|
||||||
|
|
||||||
|
this.onGoingEvent = new NotificationEvent(this.element, msg, statusType);
|
||||||
|
this.onGoingEvent.start(handlers);
|
||||||
}
|
}
|
||||||
|
|
||||||
success(msg: string, handlers?: Handlers) {
|
success(msg: string, handlers?: Handlers) {
|
||||||
|
|
|
@ -13,6 +13,7 @@ export default function (window: Window, $: JQueryStatic,
|
||||||
function onFormSubmitted() {
|
function onFormSubmitted() {
|
||||||
const username: string = $(UISelectors.USERNAME_FIELD_ID).val();
|
const username: string = $(UISelectors.USERNAME_FIELD_ID).val();
|
||||||
const password: string = $(UISelectors.PASSWORD_FIELD_ID).val();
|
const password: string = $(UISelectors.PASSWORD_FIELD_ID).val();
|
||||||
|
$(UISelectors.PASSWORD_FIELD_ID).val("");
|
||||||
jslogger.debug("Form submitted");
|
jslogger.debug("Form submitted");
|
||||||
firstFactorValidator.validate(username, password, $)
|
firstFactorValidator.validate(username, password, $)
|
||||||
.then(onFirstFactorSuccess, onFirstFactorFailure);
|
.then(onFirstFactorSuccess, onFirstFactorFailure);
|
||||||
|
@ -21,17 +22,12 @@ export default function (window: Window, $: JQueryStatic,
|
||||||
|
|
||||||
function onFirstFactorSuccess() {
|
function onFirstFactorSuccess() {
|
||||||
jslogger.debug("First factor validated.");
|
jslogger.debug("First factor validated.");
|
||||||
$(UISelectors.USERNAME_FIELD_ID).val("");
|
|
||||||
$(UISelectors.PASSWORD_FIELD_ID).val("");
|
|
||||||
|
|
||||||
// Redirect to second factor
|
// Redirect to second factor
|
||||||
window.location.href = Endpoints.SECOND_FACTOR_GET;
|
window.location.href = Endpoints.SECOND_FACTOR_GET;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onFirstFactorFailure(err: Error) {
|
function onFirstFactorFailure(err: Error) {
|
||||||
jslogger.debug("First factor failed.");
|
jslogger.debug("First factor failed.");
|
||||||
|
|
||||||
$(UISelectors.PASSWORD_FIELD_ID).val("");
|
|
||||||
notifier.error("Authentication failed. Please double check your credentials.");
|
notifier.error("Authentication failed. Please double check your credentials.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
||||||
|
|
||||||
import Server from "./lib/Server";
|
import Server from "./lib/Server";
|
||||||
import { GlobalDependencies } from "../types/Dependencies";
|
import { GlobalDependencies } from "../types/Dependencies";
|
||||||
const YAML = require("yamljs");
|
import YAML = require("yamljs");
|
||||||
|
|
||||||
const configurationFilepath = process.argv[2];
|
const configurationFilepath = process.argv[2];
|
||||||
if (!configurationFilepath) {
|
if (!configurationFilepath) {
|
||||||
|
|
|
@ -1,18 +1,20 @@
|
||||||
|
|
||||||
import * as BluebirdPromise from "bluebird";
|
import * as BluebirdPromise from "bluebird";
|
||||||
import exceptions = require("./Exceptions");
|
import exceptions = require("./Exceptions");
|
||||||
import { UserDataStore } from "./storage/UserDataStore";
|
import { IUserDataStore } from "./storage/IUserDataStore";
|
||||||
import { AuthenticationTraceDocument } from "./storage/AuthenticationTraceDocument";
|
import { AuthenticationTraceDocument } from "./storage/AuthenticationTraceDocument";
|
||||||
|
|
||||||
const MAX_AUTHENTICATION_COUNT_IN_TIME_RANGE = 3;
|
|
||||||
|
|
||||||
export class AuthenticationRegulator {
|
export class AuthenticationRegulator {
|
||||||
private userDataStore: UserDataStore;
|
private userDataStore: IUserDataStore;
|
||||||
private lockTimeInSeconds: number;
|
private banTime: number;
|
||||||
|
private findTime: number;
|
||||||
|
private maxRetries: number;
|
||||||
|
|
||||||
constructor(userDataStore: any, lockTimeInSeconds: number) {
|
constructor(userDataStore: any, maxRetries: number, findTime: number, banTime: number) {
|
||||||
this.userDataStore = userDataStore;
|
this.userDataStore = userDataStore;
|
||||||
this.lockTimeInSeconds = lockTimeInSeconds;
|
this.banTime = banTime;
|
||||||
|
this.findTime = findTime;
|
||||||
|
this.maxRetries = maxRetries;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark authentication
|
// Mark authentication
|
||||||
|
@ -21,18 +23,30 @@ export class AuthenticationRegulator {
|
||||||
}
|
}
|
||||||
|
|
||||||
regulate(userId: string): BluebirdPromise<void> {
|
regulate(userId: string): BluebirdPromise<void> {
|
||||||
return this.userDataStore.retrieveLatestAuthenticationTraces(userId, false, 3)
|
const that = this;
|
||||||
.then((docs: AuthenticationTraceDocument[]) => {
|
|
||||||
if (docs.length < MAX_AUTHENTICATION_COUNT_IN_TIME_RANGE) {
|
|
||||||
// less than the max authorized number of authentication in time range, thus authorizing access
|
|
||||||
return BluebirdPromise.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
const oldestDocument = docs[MAX_AUTHENTICATION_COUNT_IN_TIME_RANGE - 1];
|
if (that.maxRetries <= 0) return BluebirdPromise.resolve();
|
||||||
const noLockMinDate = new Date(new Date().getTime() - this.lockTimeInSeconds * 1000);
|
|
||||||
if (oldestDocument.date > noLockMinDate) {
|
return this.userDataStore.retrieveLatestAuthenticationTraces(userId, that.maxRetries)
|
||||||
|
.then((docs: AuthenticationTraceDocument[]) => {
|
||||||
|
// less than the max authorized number of authentication in time range, thus authorizing access
|
||||||
|
if (docs.length < that.maxRetries) return BluebirdPromise.resolve();
|
||||||
|
|
||||||
|
const numberOfFailedAuth = docs
|
||||||
|
.map(function (d: AuthenticationTraceDocument) { return d.isAuthenticationSuccessful == false ? 1 : 0; })
|
||||||
|
.reduce(function (acc, v) { return acc + v; }, 0);
|
||||||
|
|
||||||
|
if (numberOfFailedAuth < this.maxRetries) return BluebirdPromise.resolve();
|
||||||
|
|
||||||
|
const newestDocument = docs[0];
|
||||||
|
const oldestDocument = docs[that.maxRetries - 1];
|
||||||
|
|
||||||
|
const authenticationsTimeRangeInSeconds = (newestDocument.date.getTime() - oldestDocument.date.getTime()) / 1000;
|
||||||
|
const tooManyAuthInTimelapse = (authenticationsTimeRangeInSeconds < this.findTime);
|
||||||
|
const stillInBannedTimeRange = (new Date(new Date().getTime() - this.banTime * 1000) < newestDocument.date);
|
||||||
|
|
||||||
|
if (tooManyAuthInTimelapse && stillInBannedTimeRange)
|
||||||
throw new exceptions.AuthenticationRegulationError("Max number of authentication. Please retry in few minutes.");
|
throw new exceptions.AuthenticationRegulationError("Max number of authentication. Please retry in few minutes.");
|
||||||
}
|
|
||||||
|
|
||||||
return BluebirdPromise.resolve();
|
return BluebirdPromise.resolve();
|
||||||
});
|
});
|
||||||
|
|
|
@ -47,7 +47,7 @@ export default class Server {
|
||||||
RestApi.setup(app);
|
RestApi.setup(app);
|
||||||
}
|
}
|
||||||
|
|
||||||
private transformConfiguration(yamlConfiguration: UserConfiguration, deps: GlobalDependencies): AppConfiguration {
|
private adaptConfiguration(yamlConfiguration: UserConfiguration, deps: GlobalDependencies): AppConfiguration {
|
||||||
const config = ConfigurationAdapter.adapt(yamlConfiguration);
|
const config = ConfigurationAdapter.adapt(yamlConfiguration);
|
||||||
|
|
||||||
// by default the level of logs is info
|
// by default the level of logs is info
|
||||||
|
@ -76,7 +76,7 @@ export default class Server {
|
||||||
start(yamlConfiguration: UserConfiguration, deps: GlobalDependencies): BluebirdPromise<void> {
|
start(yamlConfiguration: UserConfiguration, deps: GlobalDependencies): BluebirdPromise<void> {
|
||||||
const that = this;
|
const that = this;
|
||||||
const app = Express();
|
const app = Express();
|
||||||
const config = this.transformConfiguration(yamlConfiguration, deps);
|
const config = this.adaptConfiguration(yamlConfiguration, deps);
|
||||||
return this.setup(config, app, deps)
|
return this.setup(config, app, deps)
|
||||||
.then(function () {
|
.then(function () {
|
||||||
return that.startServer(app, config.port);
|
return that.startServer(app, config.port);
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
|
|
||||||
import winston = require("winston");
|
import winston = require("winston");
|
||||||
import BluebirdPromise = require("bluebird");
|
import BluebirdPromise = require("bluebird");
|
||||||
|
import { IAuthenticator } from "./ldap/IAuthenticator";
|
||||||
|
import { IPasswordUpdater } from "./ldap/IPasswordUpdater";
|
||||||
|
import { IEmailsRetriever } from "./ldap/IEmailsRetriever";
|
||||||
import { Authenticator } from "./ldap/Authenticator";
|
import { Authenticator } from "./ldap/Authenticator";
|
||||||
import { PasswordUpdater } from "./ldap/PasswordUpdater";
|
import { PasswordUpdater } from "./ldap/PasswordUpdater";
|
||||||
import { EmailsRetriever } from "./ldap/EmailsRetriever";
|
import { EmailsRetriever } from "./ldap/EmailsRetriever";
|
||||||
|
import { ClientFactory } from "./ldap/ClientFactory";
|
||||||
|
|
||||||
import { TOTPValidator } from "./TOTPValidator";
|
import { TOTPValidator } from "./TOTPValidator";
|
||||||
import { TOTPGenerator } from "./TOTPGenerator";
|
import { TOTPGenerator } from "./TOTPGenerator";
|
||||||
|
@ -29,9 +33,9 @@ export const VARIABLES_KEY = "authelia-variables";
|
||||||
|
|
||||||
export interface ServerVariables {
|
export interface ServerVariables {
|
||||||
logger: typeof winston;
|
logger: typeof winston;
|
||||||
ldapAuthenticator: Authenticator;
|
ldapAuthenticator: IAuthenticator;
|
||||||
ldapPasswordUpdater: PasswordUpdater;
|
ldapPasswordUpdater: IPasswordUpdater;
|
||||||
ldapEmailsRetriever: EmailsRetriever;
|
ldapEmailsRetriever: IEmailsRetriever;
|
||||||
totpValidator: TOTPValidator;
|
totpValidator: TOTPValidator;
|
||||||
totpGenerator: TOTPGenerator;
|
totpGenerator: TOTPGenerator;
|
||||||
u2f: typeof U2F;
|
u2f: typeof U2F;
|
||||||
|
@ -68,19 +72,20 @@ class UserDataStoreFactory {
|
||||||
|
|
||||||
export class ServerVariablesHandler {
|
export class ServerVariablesHandler {
|
||||||
static initialize(app: express.Application, config: Configuration.AppConfiguration, deps: GlobalDependencies): BluebirdPromise<void> {
|
static initialize(app: express.Application, config: Configuration.AppConfiguration, deps: GlobalDependencies): BluebirdPromise<void> {
|
||||||
const five_minutes = 5 * 60;
|
|
||||||
|
|
||||||
const notifier = NotifierFactory.build(config.notifier, deps.nodemailer);
|
const notifier = NotifierFactory.build(config.notifier, deps.nodemailer);
|
||||||
const ldapAuthenticator = new Authenticator(config.ldap, deps.ldapjs, deps.winston);
|
const ldapClientFactory = new ClientFactory(config.ldap, deps.ldapjs, deps.dovehash, deps.winston);
|
||||||
const ldapPasswordUpdater = new PasswordUpdater(config.ldap, deps.ldapjs, deps.dovehash, deps.winston);
|
|
||||||
const ldapEmailsRetriever = new EmailsRetriever(config.ldap, deps.ldapjs, deps.winston);
|
const ldapAuthenticator = new Authenticator(config.ldap, ldapClientFactory);
|
||||||
|
const ldapPasswordUpdater = new PasswordUpdater(config.ldap, ldapClientFactory);
|
||||||
|
const ldapEmailsRetriever = new EmailsRetriever(config.ldap, ldapClientFactory);
|
||||||
const accessController = new AccessController(config.access_control, deps.winston);
|
const accessController = new AccessController(config.access_control, deps.winston);
|
||||||
const totpValidator = new TOTPValidator(deps.speakeasy);
|
const totpValidator = new TOTPValidator(deps.speakeasy);
|
||||||
const totpGenerator = new TOTPGenerator(deps.speakeasy);
|
const totpGenerator = new TOTPGenerator(deps.speakeasy);
|
||||||
|
|
||||||
return UserDataStoreFactory.create(config)
|
return UserDataStoreFactory.create(config)
|
||||||
.then(function (userDataStore: UserDataStore) {
|
.then(function (userDataStore: UserDataStore) {
|
||||||
const regulator = new AuthenticationRegulator(userDataStore, five_minutes);
|
const regulator = new AuthenticationRegulator(userDataStore, config.regulation.max_retries,
|
||||||
|
config.regulation.find_time, config.regulation.ban_time);
|
||||||
|
|
||||||
const variables: ServerVariables = {
|
const variables: ServerVariables = {
|
||||||
accessController: accessController,
|
accessController: accessController,
|
||||||
|
@ -113,15 +118,15 @@ export class ServerVariablesHandler {
|
||||||
return (app.get(VARIABLES_KEY) as ServerVariables).notifier;
|
return (app.get(VARIABLES_KEY) as ServerVariables).notifier;
|
||||||
}
|
}
|
||||||
|
|
||||||
static getLdapAuthenticator(app: express.Application): Authenticator {
|
static getLdapAuthenticator(app: express.Application): IAuthenticator {
|
||||||
return (app.get(VARIABLES_KEY) as ServerVariables).ldapAuthenticator;
|
return (app.get(VARIABLES_KEY) as ServerVariables).ldapAuthenticator;
|
||||||
}
|
}
|
||||||
|
|
||||||
static getLdapPasswordUpdater(app: express.Application): PasswordUpdater {
|
static getLdapPasswordUpdater(app: express.Application): IPasswordUpdater {
|
||||||
return (app.get(VARIABLES_KEY) as ServerVariables).ldapPasswordUpdater;
|
return (app.get(VARIABLES_KEY) as ServerVariables).ldapPasswordUpdater;
|
||||||
}
|
}
|
||||||
|
|
||||||
static getLdapEmailsRetriever(app: express.Application): EmailsRetriever {
|
static getLdapEmailsRetriever(app: express.Application): IEmailsRetriever {
|
||||||
return (app.get(VARIABLES_KEY) as ServerVariables).ldapEmailsRetriever;
|
return (app.get(VARIABLES_KEY) as ServerVariables).ldapEmailsRetriever;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,32 @@
|
||||||
|
export interface UserLdapConfiguration {
|
||||||
|
url: string;
|
||||||
|
base_dn: string;
|
||||||
|
|
||||||
|
additional_users_dn?: string;
|
||||||
|
users_filter?: string;
|
||||||
|
|
||||||
|
additional_groups_dn?: string;
|
||||||
|
groups_filter?: string;
|
||||||
|
|
||||||
|
group_name_attribute?: string;
|
||||||
|
mail_attribute?: string;
|
||||||
|
|
||||||
|
user: string; // admin username
|
||||||
|
password: string; // admin password
|
||||||
|
}
|
||||||
|
|
||||||
export interface LdapConfiguration {
|
export interface LdapConfiguration {
|
||||||
url: string;
|
url: string;
|
||||||
base_dn: string;
|
|
||||||
additional_user_dn?: string;
|
users_dn: string;
|
||||||
user_name_attribute?: string; // cn by default
|
users_filter: string;
|
||||||
additional_group_dn?: string;
|
|
||||||
group_name_attribute?: string; // cn by default
|
groups_dn: string;
|
||||||
|
groups_filter: string;
|
||||||
|
|
||||||
|
group_name_attribute: string;
|
||||||
|
mail_attribute: string;
|
||||||
|
|
||||||
user: string; // admin username
|
user: string; // admin username
|
||||||
password: string; // admin password
|
password: string; // admin password
|
||||||
}
|
}
|
||||||
|
@ -64,14 +85,21 @@ export interface StorageConfiguration {
|
||||||
mongo?: MongoStorageConfiguration;
|
mongo?: MongoStorageConfiguration;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RegulationConfiguration {
|
||||||
|
max_retries: number;
|
||||||
|
find_time: number;
|
||||||
|
ban_time: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface UserConfiguration {
|
export interface UserConfiguration {
|
||||||
port?: number;
|
port?: number;
|
||||||
logs_level?: string;
|
logs_level?: string;
|
||||||
ldap: LdapConfiguration;
|
ldap: UserLdapConfiguration;
|
||||||
session: SessionCookieConfiguration;
|
session: SessionCookieConfiguration;
|
||||||
storage: StorageConfiguration;
|
storage: StorageConfiguration;
|
||||||
notifier: NotifierConfiguration;
|
notifier: NotifierConfiguration;
|
||||||
access_control?: ACLConfiguration;
|
access_control?: ACLConfiguration;
|
||||||
|
regulation: RegulationConfiguration;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppConfiguration {
|
export interface AppConfiguration {
|
||||||
|
@ -82,4 +110,5 @@ export interface AppConfiguration {
|
||||||
storage: StorageConfiguration;
|
storage: StorageConfiguration;
|
||||||
notifier: NotifierConfiguration;
|
notifier: NotifierConfiguration;
|
||||||
access_control?: ACLConfiguration;
|
access_control?: ACLConfiguration;
|
||||||
|
regulation: RegulationConfiguration;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,8 @@ import * as ObjectPath from "object-path";
|
||||||
import {
|
import {
|
||||||
AppConfiguration, UserConfiguration, NotifierConfiguration,
|
AppConfiguration, UserConfiguration, NotifierConfiguration,
|
||||||
ACLConfiguration, LdapConfiguration, SessionRedisOptions,
|
ACLConfiguration, LdapConfiguration, SessionRedisOptions,
|
||||||
MongoStorageConfiguration, LocalStorageConfiguration
|
MongoStorageConfiguration, LocalStorageConfiguration,
|
||||||
|
UserLdapConfiguration
|
||||||
} from "./Configuration";
|
} from "./Configuration";
|
||||||
|
|
||||||
const LDAP_URL_ENV_VARIABLE = "LDAP_URL";
|
const LDAP_URL_ENV_VARIABLE = "LDAP_URL";
|
||||||
|
@ -23,15 +24,46 @@ function ensure_key_existence(config: object, path: string): void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function adaptLdapConfiguration(userConfig: UserLdapConfiguration): LdapConfiguration {
|
||||||
|
const DEFAULT_USERS_FILTER = "cn={0}";
|
||||||
|
const DEFAULT_GROUPS_FILTER = "member={0}";
|
||||||
|
const DEFAULT_GROUP_NAME_ATTRIBUTE = "cn";
|
||||||
|
const DEFAULT_MAIL_ATTRIBUTE = "mail";
|
||||||
|
|
||||||
|
let usersDN = userConfig.base_dn;
|
||||||
|
if (userConfig.additional_users_dn)
|
||||||
|
usersDN = userConfig.additional_users_dn + "," + usersDN;
|
||||||
|
|
||||||
|
let groupsDN = userConfig.base_dn;
|
||||||
|
if (userConfig.additional_groups_dn)
|
||||||
|
groupsDN = userConfig.additional_groups_dn + "," + groupsDN;
|
||||||
|
|
||||||
|
return {
|
||||||
|
url: userConfig.url,
|
||||||
|
users_dn: usersDN,
|
||||||
|
users_filter: userConfig.users_filter || DEFAULT_USERS_FILTER,
|
||||||
|
groups_dn: groupsDN,
|
||||||
|
groups_filter: userConfig.groups_filter || DEFAULT_GROUPS_FILTER,
|
||||||
|
group_name_attribute: userConfig.group_name_attribute || DEFAULT_GROUP_NAME_ATTRIBUTE,
|
||||||
|
mail_attribute: userConfig.mail_attribute || DEFAULT_MAIL_ATTRIBUTE,
|
||||||
|
password: userConfig.password,
|
||||||
|
user: userConfig.user
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function adaptFromUserConfiguration(userConfiguration: UserConfiguration): AppConfiguration {
|
function adaptFromUserConfiguration(userConfiguration: UserConfiguration): AppConfiguration {
|
||||||
ensure_key_existence(userConfiguration, "ldap");
|
ensure_key_existence(userConfiguration, "ldap");
|
||||||
|
// ensure_key_existence(userConfiguration, "ldap.url");
|
||||||
|
// ensure_key_existence(userConfiguration, "ldap.base_dn");
|
||||||
ensure_key_existence(userConfiguration, "session.secret");
|
ensure_key_existence(userConfiguration, "session.secret");
|
||||||
|
ensure_key_existence(userConfiguration, "regulation");
|
||||||
|
|
||||||
const port = ObjectPath.get(userConfiguration, "port", 8080);
|
const port = userConfiguration.port || 8080;
|
||||||
|
const ldapConfiguration = adaptLdapConfiguration(userConfiguration.ldap);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
port: port,
|
port: port,
|
||||||
ldap: ObjectPath.get<object, LdapConfiguration>(userConfiguration, "ldap"),
|
ldap: ldapConfiguration,
|
||||||
session: {
|
session: {
|
||||||
domain: ObjectPath.get<object, string>(userConfiguration, "session.domain"),
|
domain: ObjectPath.get<object, string>(userConfiguration, "session.domain"),
|
||||||
secret: ObjectPath.get<object, string>(userConfiguration, "session.secret"),
|
secret: ObjectPath.get<object, string>(userConfiguration, "session.secret"),
|
||||||
|
@ -44,7 +76,8 @@ function adaptFromUserConfiguration(userConfiguration: UserConfiguration): AppCo
|
||||||
},
|
},
|
||||||
logs_level: get_optional<string>(userConfiguration, "logs_level", "info"),
|
logs_level: get_optional<string>(userConfiguration, "logs_level", "info"),
|
||||||
notifier: ObjectPath.get<object, NotifierConfiguration>(userConfiguration, "notifier"),
|
notifier: ObjectPath.get<object, NotifierConfiguration>(userConfiguration, "notifier"),
|
||||||
access_control: ObjectPath.get<object, ACLConfiguration>(userConfiguration, "access_control")
|
access_control: ObjectPath.get<object, ACLConfiguration>(userConfiguration, "access_control"),
|
||||||
|
regulation: userConfiguration.regulation
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,36 +1,38 @@
|
||||||
import BluebirdPromise = require("bluebird");
|
import BluebirdPromise = require("bluebird");
|
||||||
import exceptions = require("../Exceptions");
|
import exceptions = require("../Exceptions");
|
||||||
import ldapjs = require("ldapjs");
|
import ldapjs = require("ldapjs");
|
||||||
import { Client, Attributes } from "./Client";
|
import { IClient } from "./IClient";
|
||||||
import { buildUserDN } from "./common";
|
import { IClientFactory } from "./IClientFactory";
|
||||||
|
import { GroupsAndEmails } from "./IClient";
|
||||||
|
|
||||||
|
import { IAuthenticator } from "./IAuthenticator";
|
||||||
import { LdapConfiguration } from "../configuration/Configuration";
|
import { LdapConfiguration } from "../configuration/Configuration";
|
||||||
import { Winston, Ldapjs, Dovehash } from "../../../types/Dependencies";
|
import { Winston, Ldapjs, Dovehash } from "../../../types/Dependencies";
|
||||||
|
|
||||||
|
|
||||||
export class Authenticator {
|
export class Authenticator implements IAuthenticator {
|
||||||
private options: LdapConfiguration;
|
private options: LdapConfiguration;
|
||||||
private ldapjs: Ldapjs;
|
private clientFactory: IClientFactory;
|
||||||
private logger: Winston;
|
|
||||||
|
|
||||||
constructor(options: LdapConfiguration, ldapjs: Ldapjs, logger: Winston) {
|
constructor(options: LdapConfiguration, clientFactory: IClientFactory) {
|
||||||
this.options = options;
|
this.options = options;
|
||||||
this.ldapjs = ldapjs;
|
this.clientFactory = clientFactory;
|
||||||
this.logger = logger;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private createClient(userDN: string, password: string): Client {
|
authenticate(username: string, password: string): BluebirdPromise<GroupsAndEmails> {
|
||||||
return new Client(userDN, password, this.options, this.ldapjs, undefined, this.logger);
|
const that = this;
|
||||||
}
|
let userClient: IClient;
|
||||||
|
const adminClient = this.clientFactory.create(this.options.user, this.options.password);
|
||||||
|
let groupsAndEmails: GroupsAndEmails;
|
||||||
|
|
||||||
authenticate(username: string, password: string): BluebirdPromise<Attributes> {
|
return adminClient.open()
|
||||||
const self = this;
|
.then(function () {
|
||||||
const userDN = buildUserDN(username, this.options);
|
return adminClient.searchUserDn(username);
|
||||||
const userClient = this.createClient(userDN, password);
|
})
|
||||||
const adminClient = this.createClient(this.options.user, this.options.password);
|
.then(function (userDN: string) {
|
||||||
let attributes: Attributes;
|
userClient = that.clientFactory.create(userDN, password);
|
||||||
|
return userClient.open();
|
||||||
return userClient.open()
|
})
|
||||||
.then(function () {
|
.then(function () {
|
||||||
return userClient.close();
|
return userClient.close();
|
||||||
})
|
})
|
||||||
|
@ -40,12 +42,12 @@ export class Authenticator {
|
||||||
.then(function () {
|
.then(function () {
|
||||||
return adminClient.searchEmailsAndGroups(username);
|
return adminClient.searchEmailsAndGroups(username);
|
||||||
})
|
})
|
||||||
.then(function (attr: Attributes) {
|
.then(function (gae: GroupsAndEmails) {
|
||||||
attributes = attr;
|
groupsAndEmails = gae;
|
||||||
return adminClient.close();
|
return adminClient.close();
|
||||||
})
|
})
|
||||||
.then(function () {
|
.then(function () {
|
||||||
return BluebirdPromise.resolve(attributes);
|
return BluebirdPromise.resolve(groupsAndEmails);
|
||||||
})
|
})
|
||||||
.error(function (err: Error) {
|
.error(function (err: Error) {
|
||||||
return BluebirdPromise.reject(new exceptions.LdapError(err.message));
|
return BluebirdPromise.reject(new exceptions.LdapError(err.message));
|
||||||
|
|
|
@ -2,33 +2,30 @@
|
||||||
import util = require("util");
|
import util = require("util");
|
||||||
import BluebirdPromise = require("bluebird");
|
import BluebirdPromise = require("bluebird");
|
||||||
import exceptions = require("../Exceptions");
|
import exceptions = require("../Exceptions");
|
||||||
import ldapjs = require("ldapjs");
|
import Ldapjs = require("ldapjs");
|
||||||
import { buildUserDN } from "./common";
|
import Dovehash = require("dovehash");
|
||||||
|
|
||||||
import { EventEmitter } from "events";
|
import { EventEmitter } from "events";
|
||||||
|
import { IClient, GroupsAndEmails } from "./IClient";
|
||||||
import { LdapConfiguration } from "../configuration/Configuration";
|
import { LdapConfiguration } from "../configuration/Configuration";
|
||||||
import { Winston, Ldapjs, Dovehash } from "../../../types/Dependencies";
|
import { Winston } from "../../../types/Dependencies";
|
||||||
|
|
||||||
interface SearchEntry {
|
interface SearchEntry {
|
||||||
object: any;
|
object: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Attributes {
|
export class Client implements IClient {
|
||||||
groups: string[];
|
|
||||||
emails: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Client {
|
|
||||||
private userDN: string;
|
private userDN: string;
|
||||||
private password: string;
|
private password: string;
|
||||||
private client: ldapjs.ClientAsync;
|
private client: Ldapjs.ClientAsync;
|
||||||
|
|
||||||
private ldapjs: Ldapjs;
|
private ldapjs: typeof Ldapjs;
|
||||||
private logger: Winston;
|
private logger: Winston;
|
||||||
private dovehash: Dovehash;
|
private dovehash: typeof Dovehash;
|
||||||
private options: LdapConfiguration;
|
private options: LdapConfiguration;
|
||||||
|
|
||||||
constructor(userDN: string, password: string, options: LdapConfiguration, ldapjs: Ldapjs, dovehash: Dovehash, logger: Winston) {
|
constructor(userDN: string, password: string, options: LdapConfiguration,
|
||||||
|
ldapjs: typeof Ldapjs, dovehash: typeof Dovehash, logger: Winston) {
|
||||||
this.options = options;
|
this.options = options;
|
||||||
this.ldapjs = ldapjs;
|
this.ldapjs = ldapjs;
|
||||||
this.dovehash = dovehash;
|
this.dovehash = dovehash;
|
||||||
|
@ -46,7 +43,7 @@ export class Client {
|
||||||
clientLogger.level("trace");
|
clientLogger.level("trace");
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
this.client = BluebirdPromise.promisifyAll(ldapClient) as ldapjs.ClientAsync;
|
this.client = BluebirdPromise.promisifyAll(ldapClient) as Ldapjs.ClientAsync;
|
||||||
}
|
}
|
||||||
|
|
||||||
open(): BluebirdPromise<void> {
|
open(): BluebirdPromise<void> {
|
||||||
|
@ -65,7 +62,7 @@ export class Client {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private search(base: string, query: ldapjs.SearchOptions): BluebirdPromise<any> {
|
private search(base: string, query: Ldapjs.SearchOptions): BluebirdPromise<any> {
|
||||||
const that = this;
|
const that = this;
|
||||||
|
|
||||||
that.logger.debug("LDAP: Search for '%s' in '%s'", JSON.stringify(query), base);
|
that.logger.debug("LDAP: Search for '%s' in '%s'", JSON.stringify(query), base);
|
||||||
|
@ -95,27 +92,18 @@ export class Client {
|
||||||
|
|
||||||
private searchGroups(username: string): BluebirdPromise<string[]> {
|
private searchGroups(username: string): BluebirdPromise<string[]> {
|
||||||
const that = this;
|
const that = this;
|
||||||
const userDN = buildUserDN(username, this.options);
|
|
||||||
const password = this.options.password;
|
|
||||||
|
|
||||||
let groupNameAttribute = this.options.group_name_attribute;
|
|
||||||
if (!groupNameAttribute) groupNameAttribute = "cn";
|
|
||||||
|
|
||||||
const additionalGroupDN = this.options.additional_group_dn;
|
|
||||||
const base_dn = this.options.base_dn;
|
|
||||||
|
|
||||||
let groupDN = base_dn;
|
|
||||||
if (additionalGroupDN)
|
|
||||||
groupDN = util.format("%s,", additionalGroupDN) + groupDN;
|
|
||||||
|
|
||||||
const query = {
|
|
||||||
scope: "sub",
|
|
||||||
attributes: [groupNameAttribute],
|
|
||||||
filter: "member=" + userDN
|
|
||||||
};
|
|
||||||
|
|
||||||
const groups: string[] = [];
|
const groups: string[] = [];
|
||||||
return that.search(groupDN, query)
|
return that.searchUserDn(username)
|
||||||
|
.then(function (userDN: string) {
|
||||||
|
const filter = that.options.groups_filter.replace("{0}", userDN);
|
||||||
|
const query = {
|
||||||
|
scope: "sub",
|
||||||
|
attributes: [that.options.group_name_attribute],
|
||||||
|
filter: filter
|
||||||
|
};
|
||||||
|
return that.search(that.options.groups_dn, query);
|
||||||
|
})
|
||||||
.then(function (docs) {
|
.then(function (docs) {
|
||||||
for (let i = 0; i < docs.length; ++i) {
|
for (let i = 0; i < docs.length; ++i) {
|
||||||
groups.push(docs[i].cn);
|
groups.push(docs[i].cn);
|
||||||
|
@ -127,32 +115,49 @@ export class Client {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
searchUserDn(username: string): BluebirdPromise<string> {
|
||||||
|
const that = this;
|
||||||
|
const filter = this.options.users_filter.replace("{0}", username);
|
||||||
|
const query = {
|
||||||
|
scope: "sub",
|
||||||
|
sizeLimit: 1,
|
||||||
|
attributes: ["dn"],
|
||||||
|
filter: filter
|
||||||
|
};
|
||||||
|
|
||||||
|
that.logger.debug("LDAP: searching for user dn of %s", username);
|
||||||
|
return that.search(this.options.users_dn, query)
|
||||||
|
.then(function (users: { dn: string }[]) {
|
||||||
|
that.logger.debug("LDAP: retrieved user dn is %s", users[0].dn);
|
||||||
|
return BluebirdPromise.resolve(users[0].dn);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
searchEmails(username: string): BluebirdPromise<string[]> {
|
searchEmails(username: string): BluebirdPromise<string[]> {
|
||||||
const that = this;
|
const that = this;
|
||||||
const userDN = buildUserDN(username, this.options);
|
|
||||||
|
|
||||||
const query = {
|
const query = {
|
||||||
scope: "base",
|
scope: "base",
|
||||||
sizeLimit: 1,
|
sizeLimit: 1,
|
||||||
attributes: ["mail"]
|
attributes: [this.options.mail_attribute]
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.search(userDN, query)
|
return this.searchUserDn(username)
|
||||||
.then(function (docs) {
|
.then(function (userDN) {
|
||||||
const emails = [];
|
return that.search(userDN, query);
|
||||||
for (let i = 0; i < docs.length; ++i) {
|
})
|
||||||
if (typeof docs[i].mail === "string")
|
.then(function (docs: { mail: string }[]) {
|
||||||
emails.push(docs[i].mail);
|
const emails: string[] = [];
|
||||||
else {
|
if (typeof docs[0].mail === "string")
|
||||||
emails.concat(docs[i].mail);
|
emails.push(docs[0].mail);
|
||||||
}
|
else {
|
||||||
|
emails.concat(docs[0].mail);
|
||||||
}
|
}
|
||||||
that.logger.debug("LDAP: emails of user '%s' are %s", username, emails);
|
that.logger.debug("LDAP: emails of user '%s' are %s", username, emails);
|
||||||
return BluebirdPromise.resolve(emails);
|
return BluebirdPromise.resolve(emails);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
searchEmailsAndGroups(username: string): BluebirdPromise<Attributes> {
|
searchEmailsAndGroups(username: string): BluebirdPromise<GroupsAndEmails> {
|
||||||
const that = this;
|
const that = this;
|
||||||
let retrievedEmails: string[], retrievedGroups: string[];
|
let retrievedEmails: string[], retrievedGroups: string[];
|
||||||
|
|
||||||
|
@ -172,8 +177,6 @@ export class Client {
|
||||||
|
|
||||||
modifyPassword(username: string, newPassword: string): BluebirdPromise<void> {
|
modifyPassword(username: string, newPassword: string): BluebirdPromise<void> {
|
||||||
const that = this;
|
const that = this;
|
||||||
const userDN = buildUserDN(username, this.options);
|
|
||||||
|
|
||||||
const encodedPassword = this.dovehash.encode("SSHA", newPassword);
|
const encodedPassword = this.dovehash.encode("SSHA", newPassword);
|
||||||
const change = {
|
const change = {
|
||||||
operation: "replace",
|
operation: "replace",
|
||||||
|
@ -183,7 +186,10 @@ export class Client {
|
||||||
};
|
};
|
||||||
|
|
||||||
this.logger.debug("LDAP: update password of user '%s'", username);
|
this.logger.debug("LDAP: update password of user '%s'", username);
|
||||||
return this.client.modifyAsync(userDN, change)
|
return this.searchUserDn(username)
|
||||||
|
.then(function (userDN: string) {
|
||||||
|
this.client.modifyAsync(userDN, change);
|
||||||
|
})
|
||||||
.then(function () {
|
.then(function () {
|
||||||
return that.client.unbindAsync();
|
return that.client.unbindAsync();
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { IClientFactory } from "./IClientFactory";
|
||||||
|
import { IClient } from "./IClient";
|
||||||
|
import { Client } from "./Client";
|
||||||
|
import { LdapConfiguration } from "../configuration/Configuration";
|
||||||
|
|
||||||
|
import Ldapjs = require("ldapjs");
|
||||||
|
import Dovehash = require("dovehash");
|
||||||
|
import Winston = require("winston");
|
||||||
|
|
||||||
|
export class ClientFactory implements IClientFactory {
|
||||||
|
private config: LdapConfiguration;
|
||||||
|
private ldapjs: typeof Ldapjs;
|
||||||
|
private dovehash: typeof Dovehash;
|
||||||
|
private logger: typeof Winston;
|
||||||
|
|
||||||
|
constructor(ldapConfiguration: LdapConfiguration, ldapjs: typeof Ldapjs,
|
||||||
|
dovehash: typeof Dovehash, logger: typeof Winston) {
|
||||||
|
this.config = ldapConfiguration;
|
||||||
|
this.ldapjs = ldapjs;
|
||||||
|
this.dovehash = dovehash;
|
||||||
|
this.logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
create(userDN: string, password: string): IClient {
|
||||||
|
return new Client(userDN, password, this.config, this.ldapjs, this.dovehash, this.logger);
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,30 +2,23 @@ import BluebirdPromise = require("bluebird");
|
||||||
import exceptions = require("../Exceptions");
|
import exceptions = require("../Exceptions");
|
||||||
import ldapjs = require("ldapjs");
|
import ldapjs = require("ldapjs");
|
||||||
import { Client } from "./Client";
|
import { Client } from "./Client";
|
||||||
import { buildUserDN } from "./common";
|
|
||||||
|
|
||||||
|
import { IClientFactory } from "./IClientFactory";
|
||||||
|
import { IEmailsRetriever } from "./IEmailsRetriever";
|
||||||
import { LdapConfiguration } from "../configuration/Configuration";
|
import { LdapConfiguration } from "../configuration/Configuration";
|
||||||
import { Winston, Ldapjs, Dovehash } from "../../../types/Dependencies";
|
|
||||||
|
|
||||||
|
|
||||||
export class EmailsRetriever {
|
export class EmailsRetriever implements IEmailsRetriever {
|
||||||
private options: LdapConfiguration;
|
private options: LdapConfiguration;
|
||||||
private ldapjs: Ldapjs;
|
private clientFactory: IClientFactory;
|
||||||
private logger: Winston;
|
|
||||||
|
|
||||||
constructor(options: LdapConfiguration, ldapjs: Ldapjs, logger: Winston) {
|
constructor(options: LdapConfiguration, clientFactory: IClientFactory) {
|
||||||
this.options = options;
|
this.options = options;
|
||||||
this.ldapjs = ldapjs;
|
this.clientFactory = clientFactory;
|
||||||
this.logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
private createClient(userDN: string, password: string): Client {
|
|
||||||
return new Client(userDN, password, this.options, this.ldapjs, undefined, this.logger);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
retrieve(username: string): BluebirdPromise<string[]> {
|
retrieve(username: string): BluebirdPromise<string[]> {
|
||||||
const userDN = buildUserDN(username, this.options);
|
const adminClient = this.clientFactory.create(this.options.user, this.options.password);
|
||||||
const adminClient = this.createClient(this.options.user, this.options.password);
|
|
||||||
let emails: string[];
|
let emails: string[];
|
||||||
|
|
||||||
return adminClient.open()
|
return adminClient.open()
|
||||||
|
@ -36,7 +29,7 @@ export class EmailsRetriever {
|
||||||
emails = emails_;
|
emails = emails_;
|
||||||
return adminClient.close();
|
return adminClient.close();
|
||||||
})
|
})
|
||||||
.then(function() {
|
.then(function () {
|
||||||
return BluebirdPromise.resolve(emails);
|
return BluebirdPromise.resolve(emails);
|
||||||
})
|
})
|
||||||
.error(function (err: Error) {
|
.error(function (err: Error) {
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
import BluebirdPromise = require("bluebird");
|
||||||
|
import { GroupsAndEmails } from "./IClient";
|
||||||
|
|
||||||
|
export interface IAuthenticator {
|
||||||
|
authenticate(username: string, password: string): BluebirdPromise<GroupsAndEmails>;
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
|
||||||
|
import BluebirdPromise = require("bluebird");
|
||||||
|
|
||||||
|
export interface GroupsAndEmails {
|
||||||
|
groups: string[];
|
||||||
|
emails: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IClient {
|
||||||
|
open(): BluebirdPromise<void>;
|
||||||
|
close(): BluebirdPromise<void>;
|
||||||
|
searchUserDn(username: string): BluebirdPromise<string>;
|
||||||
|
searchEmails(username: string): BluebirdPromise<string[]>;
|
||||||
|
searchEmailsAndGroups(username: string): BluebirdPromise<GroupsAndEmails>;
|
||||||
|
modifyPassword(username: string, newPassword: string): BluebirdPromise<void>;
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
|
||||||
|
import { IClient } from "./IClient";
|
||||||
|
|
||||||
|
export interface IClientFactory {
|
||||||
|
create(userDN: string, password: string): IClient;
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
import BluebirdPromise = require("bluebird");
|
||||||
|
|
||||||
|
export interface IEmailsRetriever {
|
||||||
|
retrieve(username: string): BluebirdPromise<string[]>;
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
import BluebirdPromise = require("bluebird");
|
||||||
|
|
||||||
|
export interface IPasswordUpdater {
|
||||||
|
updatePassword(username: string, newPassword: string): BluebirdPromise<void>;
|
||||||
|
}
|
|
@ -2,32 +2,23 @@ import BluebirdPromise = require("bluebird");
|
||||||
import exceptions = require("../Exceptions");
|
import exceptions = require("../Exceptions");
|
||||||
import ldapjs = require("ldapjs");
|
import ldapjs = require("ldapjs");
|
||||||
import { Client } from "./Client";
|
import { Client } from "./Client";
|
||||||
import { buildUserDN } from "./common";
|
|
||||||
|
|
||||||
|
import { IPasswordUpdater } from "./IPasswordUpdater";
|
||||||
import { LdapConfiguration } from "../configuration/Configuration";
|
import { LdapConfiguration } from "../configuration/Configuration";
|
||||||
import { Winston, Ldapjs, Dovehash } from "../../../types/Dependencies";
|
import { IClientFactory } from "./IClientFactory";
|
||||||
|
|
||||||
|
|
||||||
export class PasswordUpdater {
|
export class PasswordUpdater implements IPasswordUpdater {
|
||||||
private options: LdapConfiguration;
|
private options: LdapConfiguration;
|
||||||
private ldapjs: Ldapjs;
|
private clientFactory: IClientFactory;
|
||||||
private logger: Winston;
|
|
||||||
private dovehash: Dovehash;
|
|
||||||
|
|
||||||
constructor(options: LdapConfiguration, ldapjs: Ldapjs, dovehash: Dovehash, logger: Winston) {
|
constructor(options: LdapConfiguration, clientFactory: IClientFactory) {
|
||||||
this.options = options;
|
this.options = options;
|
||||||
this.ldapjs = ldapjs;
|
this.clientFactory = clientFactory;
|
||||||
this.logger = logger;
|
|
||||||
this.dovehash = dovehash;
|
|
||||||
}
|
|
||||||
|
|
||||||
private createClient(userDN: string, password: string): Client {
|
|
||||||
return new Client(userDN, password, this.options, this.ldapjs, this.dovehash, this.logger);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updatePassword(username: string, newPassword: string): BluebirdPromise<void> {
|
updatePassword(username: string, newPassword: string): BluebirdPromise<void> {
|
||||||
const userDN = buildUserDN(username, this.options);
|
const adminClient = this.clientFactory.create(this.options.user, this.options.password);
|
||||||
const adminClient = this.createClient(this.options.user, this.options.password);
|
|
||||||
|
|
||||||
return adminClient.open()
|
return adminClient.open()
|
||||||
.then(function () {
|
.then(function () {
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
import util = require("util");
|
|
||||||
|
|
||||||
import { LdapConfiguration } from "../configuration/Configuration";
|
|
||||||
|
|
||||||
|
|
||||||
export function buildUserDN(username: string, options: LdapConfiguration): string {
|
|
||||||
let userNameAttribute = options.user_name_attribute;
|
|
||||||
// if not provided, default to cn
|
|
||||||
if (!userNameAttribute) userNameAttribute = "cn";
|
|
||||||
|
|
||||||
const additionalUserDN = options.additional_user_dn;
|
|
||||||
const base_dn = options.base_dn;
|
|
||||||
|
|
||||||
let userDN = util.format("%s=%s", userNameAttribute, username);
|
|
||||||
if (additionalUserDN) userDN += util.format(",%s", additionalUserDN);
|
|
||||||
userDN += util.format(",%s", base_dn);
|
|
||||||
return userDN;
|
|
||||||
}
|
|
|
@ -5,7 +5,7 @@ import BluebirdPromise = require("bluebird");
|
||||||
import express = require("express");
|
import express = require("express");
|
||||||
import { AccessController } from "../../access_control/AccessController";
|
import { AccessController } from "../../access_control/AccessController";
|
||||||
import { AuthenticationRegulator } from "../../AuthenticationRegulator";
|
import { AuthenticationRegulator } from "../../AuthenticationRegulator";
|
||||||
import { Client, Attributes } from "../../ldap/Client";
|
import { GroupsAndEmails } from "../../ldap/IClient";
|
||||||
import Endpoint = require("../../../endpoints");
|
import Endpoint = require("../../../endpoints");
|
||||||
import ErrorReplies = require("../../ErrorReplies");
|
import ErrorReplies = require("../../ErrorReplies");
|
||||||
import { ServerVariablesHandler } from "../../ServerVariablesHandler";
|
import { ServerVariablesHandler } from "../../ServerVariablesHandler";
|
||||||
|
@ -38,13 +38,14 @@ export default function (req: express.Request, res: express.Response): BluebirdP
|
||||||
logger.info("1st factor: No regulation applied.");
|
logger.info("1st factor: No regulation applied.");
|
||||||
return ldap.authenticate(username, password);
|
return ldap.authenticate(username, password);
|
||||||
})
|
})
|
||||||
.then(function (attributes: Attributes) {
|
.then(function (groupsAndEmails: GroupsAndEmails) {
|
||||||
logger.info("1st factor: LDAP binding successful. Retrieved information about user are %s", JSON.stringify(attributes));
|
logger.info("1st factor: LDAP binding successful. Retrieved information about user are %s",
|
||||||
|
JSON.stringify(groupsAndEmails));
|
||||||
authSession.userid = username;
|
authSession.userid = username;
|
||||||
authSession.first_factor = true;
|
authSession.first_factor = true;
|
||||||
|
|
||||||
const emails: string[] = attributes.emails;
|
const emails: string[] = groupsAndEmails.emails;
|
||||||
const groups: string[] = attributes.groups;
|
const groups: string[] = groupsAndEmails.groups;
|
||||||
|
|
||||||
if (!emails || emails.length <= 0) {
|
if (!emails || emails.length <= 0) {
|
||||||
const errMessage = "No emails found. The user should have at least one email address to reset password.";
|
const errMessage = "No emails found. The user should have at least one email address to reset password.";
|
||||||
|
|
|
@ -11,7 +11,7 @@ export interface IUserDataStore {
|
||||||
retrieveU2FRegistration(userId: string, appId: string): BluebirdPromise<U2FRegistrationDocument>;
|
retrieveU2FRegistration(userId: string, appId: string): BluebirdPromise<U2FRegistrationDocument>;
|
||||||
|
|
||||||
saveAuthenticationTrace(userId: string, isAuthenticationSuccessful: boolean): BluebirdPromise<void>;
|
saveAuthenticationTrace(userId: string, isAuthenticationSuccessful: boolean): BluebirdPromise<void>;
|
||||||
retrieveLatestAuthenticationTraces(userId: string, isAuthenticationSuccessful: boolean, count: number): BluebirdPromise<AuthenticationTraceDocument[]>;
|
retrieveLatestAuthenticationTraces(userId: string, count: number): BluebirdPromise<AuthenticationTraceDocument[]>;
|
||||||
|
|
||||||
produceIdentityValidationToken(userId: string, token: string, challenge: string, maxAge: number): BluebirdPromise<any>;
|
produceIdentityValidationToken(userId: string, token: string, challenge: string, maxAge: number): BluebirdPromise<any>;
|
||||||
consumeIdentityValidationToken(token: string, challenge: string): BluebirdPromise<IdentityValidationDocument>;
|
consumeIdentityValidationToken(token: string, challenge: string): BluebirdPromise<IdentityValidationDocument>;
|
||||||
|
|
|
@ -76,10 +76,9 @@ export class UserDataStore implements IUserDataStore {
|
||||||
return this.authenticationTracesCollection.insert(newDocument);
|
return this.authenticationTracesCollection.insert(newDocument);
|
||||||
}
|
}
|
||||||
|
|
||||||
retrieveLatestAuthenticationTraces(userId: string, isAuthenticationSuccessful: boolean, count: number): BluebirdPromise<AuthenticationTraceDocument[]> {
|
retrieveLatestAuthenticationTraces(userId: string, count: number): BluebirdPromise<AuthenticationTraceDocument[]> {
|
||||||
const q = {
|
const q = {
|
||||||
userId: userId,
|
userId: userId
|
||||||
isAuthenticationSuccessful: isAuthenticationSuccessful
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.authenticationTracesCollection.find(q, { date: -1 }, count);
|
return this.authenticationTracesCollection.find(q, { date: -1 }, count);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
Feature: User is correctly redirected correctly
|
Feature: User is correctly redirected
|
||||||
|
|
||||||
Scenario: User is redirected to authelia when he is not authenticated
|
Scenario: User is redirected to authelia when he is not authenticated
|
||||||
Given I'm on https://home.test.local:8080
|
Given I'm on https://home.test.local:8080
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
Feature: Authelia regulates authentication to avoid brute force
|
||||||
|
|
||||||
|
@needs-test-config
|
||||||
|
Scenario: Attacker tries too many authentication in a short period of time and get banned
|
||||||
|
Given I visit "https://auth.test.local:8080/"
|
||||||
|
And I login with user "blackhat" and password "password"
|
||||||
|
And I register a TOTP secret called "Sec0"
|
||||||
|
And I visit "https://auth.test.local:8080/"
|
||||||
|
And I login with user "blackhat" and password "password" and I use TOTP token handle "Sec0"
|
||||||
|
And I visit "https://auth.test.local:8080/logout?redirect=https://auth.test.local:8080/"
|
||||||
|
And I visit "https://auth.test.local:8080/"
|
||||||
|
And I set field "username" to "blackhat"
|
||||||
|
And I set field "password" to "bad-password"
|
||||||
|
And I click on "Sign in"
|
||||||
|
And I get a notification of type "error" with message "Authentication failed. Please double check your credentials."
|
||||||
|
And I set field "password" to "bad-password"
|
||||||
|
And I click on "Sign in"
|
||||||
|
And I get a notification of type "error" with message "Authentication failed. Please double check your credentials."
|
||||||
|
And I set field "password" to "bad-password"
|
||||||
|
And I click on "Sign in"
|
||||||
|
And I get a notification of type "error" with message "Authentication failed. Please double check your credentials."
|
||||||
|
When I set field "password" to "password"
|
||||||
|
And I click on "Sign in"
|
||||||
|
Then I get a notification of type "error" with message "Authentication failed. Please double check your credentials."
|
||||||
|
|
||||||
|
@needs-test-config
|
||||||
|
Scenario: User is unbanned after a configured amount of time
|
||||||
|
Given I visit "https://auth.test.local:8080/"
|
||||||
|
And I login with user "blackhat" and password "password"
|
||||||
|
And I register a TOTP secret called "Sec0"
|
||||||
|
And I visit "https://auth.test.local:8080/"
|
||||||
|
And I login with user "blackhat" and password "password" and I use TOTP token handle "Sec0"
|
||||||
|
And I visit "https://auth.test.local:8080/logout?redirect=https://auth.test.local:8080/"
|
||||||
|
And I visit "https://auth.test.local:8080/"
|
||||||
|
And I set field "username" to "blackhat"
|
||||||
|
And I set field "password" to "bad-password"
|
||||||
|
And I click on "Sign in"
|
||||||
|
And I get a notification of type "error" with message "Authentication failed. Please double check your credentials."
|
||||||
|
And I set field "password" to "bad-password"
|
||||||
|
And I click on "Sign in"
|
||||||
|
And I get a notification of type "error" with message "Authentication failed. Please double check your credentials."
|
||||||
|
And I set field "password" to "bad-password"
|
||||||
|
And I click on "Sign in"
|
||||||
|
And I get a notification of type "error" with message "Authentication failed. Please double check your credentials."
|
||||||
|
When I wait 6 seconds
|
||||||
|
And I set field "password" to "password"
|
||||||
|
And I click on "Sign in"
|
||||||
|
And I use "Sec0" as TOTP token handle
|
||||||
|
And I click on "TOTP"
|
||||||
|
Then I have access to:
|
||||||
|
| url |
|
||||||
|
| https://public.test.local:8080/secret.html |
|
|
@ -5,9 +5,17 @@ Feature: Authelia keeps user sessions despite the application restart
|
||||||
And the application restarts
|
And the application restarts
|
||||||
Then I have access to:
|
Then I have access to:
|
||||||
| url |
|
| url |
|
||||||
| https://public.test.local:8080/secret.html |
|
|
||||||
| https://secret.test.local:8080/secret.html |
|
| https://secret.test.local:8080/secret.html |
|
||||||
| https://secret1.test.local:8080/secret.html |
|
|
||||||
| https://secret2.test.local:8080/secret.html |
|
Scenario: Secrets are stored even when Authelia restarts
|
||||||
| https://mx1.mail.test.local:8080/secret.html |
|
Given I visit "https://auth.test.local:8080/"
|
||||||
| https://mx2.mail.test.local:8080/secret.html |
|
And I login with user "john" and password "password"
|
||||||
|
And I register a TOTP secret called "Sec0"
|
||||||
|
When the application restarts
|
||||||
|
And I visit "https://secret.test.local:8080/secret.html" and get redirected "https://auth.test.local:8080/"
|
||||||
|
And I login with user "john" and password "password"
|
||||||
|
And I use "Sec0" as TOTP token handle
|
||||||
|
And I click on "TOTP"
|
||||||
|
Then I have access to:
|
||||||
|
| url |
|
||||||
|
| https://secret.test.local:8080/secret.html |
|
|
@ -1,7 +0,0 @@
|
||||||
import Cucumber = require("cucumber");
|
|
||||||
|
|
||||||
Cucumber.defineSupportCode(function({After}) {
|
|
||||||
After(function() {
|
|
||||||
return this.driver.quit();
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -22,9 +22,20 @@ Cucumber.defineSupportCode(function ({ Given, When, Then }) {
|
||||||
return this.clickOnButton(text);
|
return this.clickOnButton(text);
|
||||||
});
|
});
|
||||||
|
|
||||||
Given("I login with user {stringInDoubleQuotes} and password {stringInDoubleQuotes}", function (username: string, password: string) {
|
Given("I login with user {stringInDoubleQuotes} and password {stringInDoubleQuotes}",
|
||||||
return this.loginWithUserPassword(username, password);
|
function (username: string, password: string) {
|
||||||
});
|
return this.loginWithUserPassword(username, password);
|
||||||
|
});
|
||||||
|
|
||||||
|
Given("I login with user {stringInDoubleQuotes} and password {stringInDoubleQuotes} \
|
||||||
|
and I use TOTP token handle {stringInDoubleQuotes}",
|
||||||
|
function (username: string, password: string, totpTokenHandle: string) {
|
||||||
|
const that = this;
|
||||||
|
return this.loginWithUserPassword(username, password)
|
||||||
|
.then(function () {
|
||||||
|
return that.useTotpTokenHandle(totpTokenHandle);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
Given("I register a TOTP secret called {stringInDoubleQuotes}", function (handle: string) {
|
Given("I register a TOTP secret called {stringInDoubleQuotes}", function (handle: string) {
|
||||||
return this.registerTotpSecret(handle);
|
return this.registerTotpSecret(handle);
|
||||||
|
@ -38,17 +49,19 @@ Cucumber.defineSupportCode(function ({ Given, When, Then }) {
|
||||||
return this.useTotpTokenHandle(handle);
|
return this.useTotpTokenHandle(handle);
|
||||||
});
|
});
|
||||||
|
|
||||||
When("I visit {stringInDoubleQuotes} and get redirected {stringInDoubleQuotes}", function (url: string, redirectUrl: string) {
|
When("I visit {stringInDoubleQuotes} and get redirected {stringInDoubleQuotes}",
|
||||||
const that = this;
|
function (url: string, redirectUrl: string) {
|
||||||
return this.driver.get(url)
|
const that = this;
|
||||||
.then(function () {
|
return this.driver.get(url)
|
||||||
return that.driver.wait(seleniumWebdriver.until.urlIs(redirectUrl), 2000);
|
.then(function () {
|
||||||
});
|
return that.driver.wait(seleniumWebdriver.until.urlIs(redirectUrl), 2000);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
Given("I register TOTP and login with user {stringInDoubleQuotes} and password {stringInDoubleQuotes}", function (username: string, password: string) {
|
Given("I register TOTP and login with user {stringInDoubleQuotes} and password {stringInDoubleQuotes}",
|
||||||
return this.registerTotpAndSignin(username, password);
|
function (username: string, password: string) {
|
||||||
});
|
return this.registerTotpAndSignin(username, password);
|
||||||
|
});
|
||||||
|
|
||||||
function hasAccessToSecret(link: string, that: any) {
|
function hasAccessToSecret(link: string, that: any) {
|
||||||
return that.driver.get(link)
|
return that.driver.get(link)
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
import Cucumber = require("cucumber");
|
||||||
|
import fs = require("fs");
|
||||||
|
import BluebirdPromise = require("bluebird");
|
||||||
|
import ChildProcess = require("child_process");
|
||||||
|
|
||||||
|
Cucumber.defineSupportCode(function({ After, Before }) {
|
||||||
|
const exec = BluebirdPromise.promisify(ChildProcess.exec);
|
||||||
|
|
||||||
|
After(function() {
|
||||||
|
return this.driver.quit();
|
||||||
|
});
|
||||||
|
|
||||||
|
Before({tags: "@needs-test-config", timeout: 15 * 1000}, function () {
|
||||||
|
return exec("./scripts/example/dc-example.sh -f docker-compose.test.yml up -d authelia && sleep 2");
|
||||||
|
});
|
||||||
|
|
||||||
|
After({tags: "@needs-test-config", timeout: 15 * 1000}, function () {
|
||||||
|
return exec("./scripts/example/dc-example.sh up -d authelia && sleep 2");
|
||||||
|
});
|
||||||
|
});
|
|
@ -6,6 +6,7 @@ import CustomWorld = require("../support/world");
|
||||||
|
|
||||||
Cucumber.defineSupportCode(function ({ Given, When, Then }) {
|
Cucumber.defineSupportCode(function ({ Given, When, Then }) {
|
||||||
Then("I get a notification of type {stringInDoubleQuotes} with message {stringInDoubleQuotes}",
|
Then("I get a notification of type {stringInDoubleQuotes} with message {stringInDoubleQuotes}",
|
||||||
|
{ timeout: 10 * 1000 },
|
||||||
function (notificationType: string, notificationMessage: string) {
|
function (notificationType: string, notificationMessage: string) {
|
||||||
const that = this;
|
const that = this;
|
||||||
const notificationEl = this.driver.findElement(seleniumWebdriver.By.className("notification"));
|
const notificationEl = this.driver.findElement(seleniumWebdriver.By.className("notification"));
|
||||||
|
@ -17,8 +18,9 @@ Cucumber.defineSupportCode(function ({ Given, When, Then }) {
|
||||||
Assert.equal(notificationMessage, txt);
|
Assert.equal(notificationMessage, txt);
|
||||||
return notificationEl.getAttribute("class");
|
return notificationEl.getAttribute("class");
|
||||||
})
|
})
|
||||||
.then(function(classes: string) {
|
.then(function (classes: string) {
|
||||||
Assert(classes.indexOf(notificationType) > -1, "Class '" + notificationType + "' not found in notification element.");
|
Assert(classes.indexOf(notificationType) > -1, "Class '" + notificationType + "' not found in notification element.");
|
||||||
|
return that.driver.sleep(500);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
import Cucumber = require("cucumber");
|
||||||
|
import seleniumWebdriver = require("selenium-webdriver");
|
||||||
|
import Assert = require("assert");
|
||||||
|
import Fs = require("fs");
|
||||||
|
import CustomWorld = require("../support/world");
|
||||||
|
|
||||||
|
Cucumber.defineSupportCode(function ({ Given, When, Then }) {
|
||||||
|
When("I wait {number} seconds", { timeout: 10 * 1000 }, function (seconds: number) {
|
||||||
|
return this.driver.sleep(seconds * 1000);
|
||||||
|
});
|
||||||
|
});
|
|
@ -9,7 +9,7 @@ Cucumber.defineSupportCode(function ({ Given, When, Then }) {
|
||||||
});
|
});
|
||||||
|
|
||||||
When("I click on the link of the email", function () {
|
When("I click on the link of the email", function () {
|
||||||
const notif = Fs.readFileSync("./notifications/notification.txt").toString();
|
const notif = Fs.readFileSync("/tmp/notifications/notification.txt").toString();
|
||||||
const regexp = new RegExp(/Link: (.+)/);
|
const regexp = new RegExp(/Link: (.+)/);
|
||||||
const match = regexp.exec(notif);
|
const match = regexp.exec(notif);
|
||||||
const link = match[1];
|
const link = match[1];
|
||||||
|
|
|
@ -12,6 +12,7 @@ function CustomWorld() {
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
this.totpSecrets = {};
|
this.totpSecrets = {};
|
||||||
|
this.configuration = {};
|
||||||
|
|
||||||
this.visit = function (link: string) {
|
this.visit = function (link: string) {
|
||||||
return this.driver.get(link);
|
return this.driver.get(link);
|
||||||
|
@ -71,7 +72,7 @@ function CustomWorld() {
|
||||||
return that.driver.findElement(seleniumWebdriver.By.className("register-totp")).click();
|
return that.driver.findElement(seleniumWebdriver.By.className("register-totp")).click();
|
||||||
})
|
})
|
||||||
.then(function () {
|
.then(function () {
|
||||||
const notif = Fs.readFileSync("./notifications/notification.txt").toString();
|
const notif = Fs.readFileSync("/tmp/notifications/notification.txt").toString();
|
||||||
const regexp = new RegExp(/Link: (.+)/);
|
const regexp = new RegExp(/Link: (.+)/);
|
||||||
const match = regexp.exec(notif);
|
const match = regexp.exec(notif);
|
||||||
const link = match[1];
|
const link = match[1];
|
||||||
|
@ -98,7 +99,7 @@ function CustomWorld() {
|
||||||
};
|
};
|
||||||
|
|
||||||
this.useTotpToken = function (totpSecret: string) {
|
this.useTotpToken = function (totpSecret: string) {
|
||||||
return that.driver.wait(seleniumWebdriver.until.elementLocated(seleniumWebdriver.By.className("register-totp")), 4000)
|
return that.driver.wait(seleniumWebdriver.until.elementLocated(seleniumWebdriver.By.className("register-totp")), 5000)
|
||||||
.then(function () {
|
.then(function () {
|
||||||
return that.driver.findElement(seleniumWebdriver.By.id("token"))
|
return that.driver.findElement(seleniumWebdriver.By.id("token"))
|
||||||
.sendKeys(totpSecret);
|
.sendKeys(totpSecret);
|
||||||
|
|
|
@ -9,34 +9,34 @@ describe("test notifier", function() {
|
||||||
const SELECTOR = "dummy-selector";
|
const SELECTOR = "dummy-selector";
|
||||||
const MESSAGE = "This is a message";
|
const MESSAGE = "This is a message";
|
||||||
let jqueryMock: { jquery: JQueryMock.JQueryMock, element: JQueryMock.JQueryElementsMock };
|
let jqueryMock: { jquery: JQueryMock.JQueryMock, element: JQueryMock.JQueryElementsMock };
|
||||||
|
let clock: any;
|
||||||
|
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
jqueryMock = JQueryMock.JQueryMock();
|
jqueryMock = JQueryMock.JQueryMock();
|
||||||
|
clock = Sinon.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(function() {
|
||||||
|
clock.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
function should_fade_in_and_out_on_notification(notificationType: string): void {
|
function should_fade_in_and_out_on_notification(notificationType: string): void {
|
||||||
const fadeInReturn = {
|
|
||||||
delay: Sinon.stub()
|
|
||||||
};
|
|
||||||
|
|
||||||
const delayReturn = {
|
const delayReturn = {
|
||||||
fadeOut: Sinon.stub()
|
fadeOut: Sinon.stub()
|
||||||
};
|
};
|
||||||
|
|
||||||
jqueryMock.element.fadeIn.returns(fadeInReturn);
|
|
||||||
jqueryMock.element.fadeIn.yields();
|
jqueryMock.element.fadeIn.yields();
|
||||||
delayReturn.fadeOut.yields();
|
|
||||||
|
|
||||||
fadeInReturn.delay.returns(delayReturn);
|
|
||||||
|
|
||||||
function onFadedInCallback() {
|
function onFadedInCallback() {
|
||||||
Assert(jqueryMock.element.fadeIn.calledOnce);
|
Assert(jqueryMock.element.fadeIn.calledOnce);
|
||||||
Assert(jqueryMock.element.addClass.calledWith(notificationType));
|
Assert(jqueryMock.element.addClass.calledWith(notificationType));
|
||||||
Assert(!jqueryMock.element.removeClass.calledWith(notificationType));
|
Assert(!jqueryMock.element.removeClass.calledWith(notificationType));
|
||||||
|
clock.tick(10 * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onFadedOutCallback() {
|
function onFadedOutCallback() {
|
||||||
Assert(jqueryMock.element.removeClass.calledWith(notificationType));
|
Assert(jqueryMock.element.removeClass.calledWith(notificationType));
|
||||||
|
Assert(jqueryMock.element.fadeOut.calledOnce);
|
||||||
}
|
}
|
||||||
|
|
||||||
const notifier = new Notifier(SELECTOR, jqueryMock.jquery as any);
|
const notifier = new Notifier(SELECTOR, jqueryMock.jquery as any);
|
||||||
|
@ -47,9 +47,9 @@ describe("test notifier", function() {
|
||||||
onFadedOut: onFadedOutCallback
|
onFadedOut: onFadedOutCallback
|
||||||
});
|
});
|
||||||
|
|
||||||
|
clock.tick(510);
|
||||||
|
|
||||||
Assert(jqueryMock.element.fadeIn.calledOnce);
|
Assert(jqueryMock.element.fadeIn.calledOnce);
|
||||||
Assert(fadeInReturn.delay.calledOnce);
|
|
||||||
Assert(delayReturn.fadeOut.calledOnce);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@ export interface JQueryElementsMock {
|
||||||
addClass: sinon.SinonStub;
|
addClass: sinon.SinonStub;
|
||||||
removeClass: sinon.SinonStub;
|
removeClass: sinon.SinonStub;
|
||||||
fadeIn: sinon.SinonStub;
|
fadeIn: sinon.SinonStub;
|
||||||
|
fadeOut: sinon.SinonStub;
|
||||||
on: sinon.SinonStub;
|
on: sinon.SinonStub;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,6 +37,7 @@ export function JQueryMock(): { jquery: JQueryMock, element: JQueryElementsMock
|
||||||
addClass: sinon.stub(),
|
addClass: sinon.stub(),
|
||||||
removeClass: sinon.stub(),
|
removeClass: sinon.stub(),
|
||||||
fadeIn: sinon.stub(),
|
fadeIn: sinon.stub(),
|
||||||
|
fadeOut: sinon.stub(),
|
||||||
on: sinon.stub()
|
on: sinon.stub()
|
||||||
};
|
};
|
||||||
jquery.ajax = sinon.stub();
|
jquery.ajax = sinon.stub();
|
||||||
|
|
|
@ -1,120 +1,186 @@
|
||||||
|
|
||||||
import Sinon = require("sinon");
|
import Sinon = require("sinon");
|
||||||
import BluebirdPromise = require("bluebird");
|
import BluebirdPromise = require("bluebird");
|
||||||
|
import Assert = require("assert");
|
||||||
|
|
||||||
import { AuthenticationRegulator } from "../../../src/server/lib/AuthenticationRegulator";
|
import { AuthenticationRegulator } from "../../../src/server/lib/AuthenticationRegulator";
|
||||||
import { UserDataStore } from "../../../src/server/lib/storage/UserDataStore";
|
|
||||||
import MockDate = require("mockdate");
|
import MockDate = require("mockdate");
|
||||||
import exceptions = require("../../../src/server/lib/Exceptions");
|
import exceptions = require("../../../src/server/lib/Exceptions");
|
||||||
import { CollectionStub } from "./mocks/storage/CollectionStub";
|
import { UserDataStoreStub } from "./mocks/storage/UserDataStoreStub";
|
||||||
import { CollectionFactoryStub } from "./mocks/storage/CollectionFactoryStub";
|
|
||||||
|
|
||||||
describe("test authentication regulator", function () {
|
describe("test authentication regulator", function () {
|
||||||
let collectionFactory: CollectionFactoryStub;
|
const USER1 = "USER1";
|
||||||
let collection: CollectionStub;
|
const USER2 = "USER2";
|
||||||
|
let userDataStoreStub: UserDataStoreStub;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
collectionFactory = new CollectionFactoryStub();
|
userDataStoreStub = new UserDataStoreStub();
|
||||||
collection = new CollectionStub();
|
const dataStore: { [userId: string]: { userId: string, date: Date, isAuthenticationSuccessful: boolean }[] } = {
|
||||||
|
[USER1]: [],
|
||||||
|
[USER2]: []
|
||||||
|
};
|
||||||
|
|
||||||
collectionFactory.buildStub.returns(collection);
|
userDataStoreStub.saveAuthenticationTraceStub.callsFake(function (userId, isAuthenticationSuccessful) {
|
||||||
|
dataStore[userId].unshift({
|
||||||
|
userId: userId,
|
||||||
|
date: new Date(),
|
||||||
|
isAuthenticationSuccessful: isAuthenticationSuccessful,
|
||||||
|
});
|
||||||
|
return BluebirdPromise.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
userDataStoreStub.retrieveLatestAuthenticationTracesStub.callsFake(function (userId, count) {
|
||||||
|
const ret = (dataStore[userId].length <= count) ? dataStore[userId] : dataStore[userId].slice(0, 3);
|
||||||
|
return BluebirdPromise.resolve(ret);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should mark 2 authentication and regulate", function () {
|
afterEach(function () {
|
||||||
const user = "USER";
|
MockDate.reset();
|
||||||
|
});
|
||||||
|
|
||||||
collection.insertStub.returns(BluebirdPromise.resolve());
|
function markAuthenticationAt(regulator: AuthenticationRegulator, user: string, time: string, success: boolean) {
|
||||||
collection.findStub.returns(BluebirdPromise.resolve([{
|
MockDate.set(time);
|
||||||
userId: user,
|
return regulator.mark(user, success);
|
||||||
date: new Date(),
|
}
|
||||||
isAuthenticationSuccessful: false
|
|
||||||
}, {
|
|
||||||
userId: user,
|
|
||||||
date: new Date(),
|
|
||||||
isAuthenticationSuccessful: true
|
|
||||||
}]));
|
|
||||||
|
|
||||||
const dataStore = new UserDataStore(collectionFactory);
|
it("should mark 2 authentication and regulate (accept)", function () {
|
||||||
const regulator = new AuthenticationRegulator(dataStore, 10);
|
const regulator = new AuthenticationRegulator(userDataStoreStub, 3, 10, 10);
|
||||||
|
|
||||||
return regulator.mark(user, false)
|
return regulator.mark(USER1, false)
|
||||||
.then(function () {
|
.then(function () {
|
||||||
return regulator.mark(user, true);
|
return regulator.mark(USER1, true);
|
||||||
})
|
})
|
||||||
.then(function () {
|
.then(function () {
|
||||||
return regulator.regulate(user);
|
return regulator.regulate(USER1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should mark 3 authentications and regulate (reject)", function (done) {
|
it("should mark 3 authentications and regulate (reject)", function () {
|
||||||
const user = "USER";
|
const regulator = new AuthenticationRegulator(userDataStoreStub, 3, 10, 10);
|
||||||
collection.insertStub.returns(BluebirdPromise.resolve());
|
|
||||||
collection.findStub.returns(BluebirdPromise.resolve([{
|
|
||||||
userId: user,
|
|
||||||
date: new Date(),
|
|
||||||
isAuthenticationSuccessful: false
|
|
||||||
}, {
|
|
||||||
userId: user,
|
|
||||||
date: new Date(),
|
|
||||||
isAuthenticationSuccessful: false
|
|
||||||
}, {
|
|
||||||
userId: user,
|
|
||||||
date: new Date(),
|
|
||||||
isAuthenticationSuccessful: false
|
|
||||||
}]));
|
|
||||||
|
|
||||||
const dataStore = new UserDataStore(collectionFactory);
|
return regulator.mark(USER1, false)
|
||||||
const regulator = new AuthenticationRegulator(dataStore, 10);
|
|
||||||
|
|
||||||
regulator.mark(user, false)
|
|
||||||
.then(function () {
|
.then(function () {
|
||||||
return regulator.mark(user, false);
|
return regulator.mark(USER1, false);
|
||||||
})
|
})
|
||||||
.then(function () {
|
.then(function () {
|
||||||
return regulator.mark(user, false);
|
return regulator.mark(USER1, false);
|
||||||
})
|
})
|
||||||
.then(function () {
|
.then(function () {
|
||||||
return regulator.regulate(user);
|
return regulator.regulate(USER1);
|
||||||
})
|
})
|
||||||
|
.then(function () { return BluebirdPromise.reject(new Error("should not be here!")); })
|
||||||
.catch(exceptions.AuthenticationRegulationError, function () {
|
.catch(exceptions.AuthenticationRegulationError, function () {
|
||||||
done();
|
return BluebirdPromise.resolve();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should mark 3 authentications separated by a lot of time and allow access to user", function (done) {
|
it("should mark 1 failed, 1 successful and 1 failed authentications within minimum time and regulate (accept)", function () {
|
||||||
const user = "USER";
|
const regulator = new AuthenticationRegulator(userDataStoreStub, 3, 60, 30);
|
||||||
collection.insertStub.returns(BluebirdPromise.resolve());
|
|
||||||
collection.findStub.returns(BluebirdPromise.resolve([{
|
|
||||||
userId: user,
|
|
||||||
date: new Date("1/2/2000 06:00:15"),
|
|
||||||
isAuthenticationSuccessful: false
|
|
||||||
}, {
|
|
||||||
userId: user,
|
|
||||||
date: new Date("1/2/2000 00:00:15"),
|
|
||||||
isAuthenticationSuccessful: false
|
|
||||||
}, {
|
|
||||||
userId: user,
|
|
||||||
date: new Date("1/2/2000 00:00:00"),
|
|
||||||
isAuthenticationSuccessful: false
|
|
||||||
}]));
|
|
||||||
const data_store = new UserDataStore(collectionFactory);
|
|
||||||
const regulator = new AuthenticationRegulator(data_store, 10);
|
|
||||||
|
|
||||||
MockDate.set("1/2/2000 00:00:00");
|
return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:00", false)
|
||||||
regulator.mark(user, false)
|
|
||||||
.then(function () {
|
.then(function () {
|
||||||
MockDate.set("1/2/2000 00:00:15");
|
return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:10", true);
|
||||||
return regulator.mark(user, false);
|
|
||||||
})
|
})
|
||||||
.then(function () {
|
.then(function () {
|
||||||
MockDate.set("1/2/2000 06:00:15");
|
return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:20", false);
|
||||||
return regulator.mark(user, false);
|
|
||||||
})
|
})
|
||||||
.then(function () {
|
.then(function () {
|
||||||
return regulator.regulate(user);
|
return regulator.regulate(USER1);
|
||||||
})
|
})
|
||||||
.then(function () {
|
.then(function () {
|
||||||
done();
|
return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:30", false);
|
||||||
|
})
|
||||||
|
.then(function () {
|
||||||
|
return regulator.regulate(USER1);
|
||||||
|
})
|
||||||
|
.then(function () {
|
||||||
|
return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:39", false);
|
||||||
|
})
|
||||||
|
.then(function () {
|
||||||
|
return regulator.regulate(USER1);
|
||||||
|
})
|
||||||
|
.then(function () {
|
||||||
|
return BluebirdPromise.reject(new Error("should not be here!"));
|
||||||
|
},
|
||||||
|
function () {
|
||||||
|
return BluebirdPromise.resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should regulate user if number of failures is greater than 3 in allowed time lapse", function () {
|
||||||
|
function markAuthentications(regulator: AuthenticationRegulator, user: string) {
|
||||||
|
return markAuthenticationAt(regulator, user, "1/2/2000 00:00:00", false)
|
||||||
|
.then(function () {
|
||||||
|
return markAuthenticationAt(regulator, user, "1/2/2000 00:00:45", false);
|
||||||
|
})
|
||||||
|
.then(function () {
|
||||||
|
return markAuthenticationAt(regulator, user, "1/2/2000 00:01:05", false);
|
||||||
|
})
|
||||||
|
.then(function () {
|
||||||
|
return regulator.regulate(user);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const regulator1 = new AuthenticationRegulator(userDataStoreStub, 3, 60, 60);
|
||||||
|
const regulator2 = new AuthenticationRegulator(userDataStoreStub, 3, 2 * 60, 60);
|
||||||
|
|
||||||
|
const p1 = markAuthentications(regulator1, USER1);
|
||||||
|
const p2 = markAuthentications(regulator2, USER2);
|
||||||
|
|
||||||
|
return BluebirdPromise.join(p1, p2)
|
||||||
|
.then(function () {
|
||||||
|
return BluebirdPromise.reject(new Error("should not be here..."));
|
||||||
|
}, function () {
|
||||||
|
Assert(p1.isFulfilled());
|
||||||
|
Assert(p2.isRejected());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should user wait after regulation to authenticate again", function () {
|
||||||
|
function markAuthentications(regulator: AuthenticationRegulator, user: string) {
|
||||||
|
return markAuthenticationAt(regulator, user, "1/2/2000 00:00:00", false)
|
||||||
|
.then(function () {
|
||||||
|
return markAuthenticationAt(regulator, user, "1/2/2000 00:00:10", false);
|
||||||
|
})
|
||||||
|
.then(function () {
|
||||||
|
return markAuthenticationAt(regulator, user, "1/2/2000 00:00:15", false);
|
||||||
|
})
|
||||||
|
.then(function () {
|
||||||
|
return markAuthenticationAt(regulator, user, "1/2/2000 00:00:25", false);
|
||||||
|
})
|
||||||
|
.then(function () {
|
||||||
|
MockDate.set("1/2/2000 00:00:54");
|
||||||
|
return regulator.regulate(user);
|
||||||
|
})
|
||||||
|
.then(function () {
|
||||||
|
return BluebirdPromise.reject(new Error("should fail at this time"));
|
||||||
|
}, function () {
|
||||||
|
MockDate.set("1/2/2000 00:00:56");
|
||||||
|
return regulator.regulate(user);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const regulator = new AuthenticationRegulator(userDataStoreStub, 4, 30, 30);
|
||||||
|
return markAuthentications(regulator, USER1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should disable regulation when max_retries is set to 0", function () {
|
||||||
|
const maxRetries = 0;
|
||||||
|
const regulator = new AuthenticationRegulator(userDataStoreStub, maxRetries, 60, 30);
|
||||||
|
return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:00", false)
|
||||||
|
.then(function () {
|
||||||
|
return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:10", false);
|
||||||
|
})
|
||||||
|
.then(function () {
|
||||||
|
return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:15", false);
|
||||||
|
})
|
||||||
|
.then(function () {
|
||||||
|
return markAuthenticationAt(regulator, USER1, "1/2/2000 00:00:25", false);
|
||||||
|
})
|
||||||
|
.then(function () {
|
||||||
|
MockDate.set("1/2/2000 00:00:26");
|
||||||
|
return regulator.regulate(USER1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
|
@ -1,179 +0,0 @@
|
||||||
|
|
||||||
import * as BluebirdPromise from "bluebird";
|
|
||||||
import * as request from "request";
|
|
||||||
|
|
||||||
import Server from "../../../src/server/lib/Server";
|
|
||||||
import { UserConfiguration } from "../../../src/server/lib/configuration/Configuration";
|
|
||||||
import { GlobalDependencies } from "../../../src/types/Dependencies";
|
|
||||||
import * as tmp from "tmp";
|
|
||||||
import U2FMock = require("./mocks/u2f");
|
|
||||||
import { LdapjsClientMock } from "./mocks/ldapjs";
|
|
||||||
|
|
||||||
|
|
||||||
const requestp = BluebirdPromise.promisifyAll(request) as request.Request;
|
|
||||||
const assert = require("assert");
|
|
||||||
const speakeasy = require("speakeasy");
|
|
||||||
const sinon = require("sinon");
|
|
||||||
const nedb = require("nedb");
|
|
||||||
const session = require("express-session");
|
|
||||||
const winston = require("winston");
|
|
||||||
|
|
||||||
const PORT = 8050;
|
|
||||||
const requests = require("./requests")(PORT);
|
|
||||||
|
|
||||||
describe("test data persistence", function () {
|
|
||||||
let u2f: U2FMock.U2FMock;
|
|
||||||
let tmpDir: tmp.SynchrounousResult;
|
|
||||||
const ldapClient = LdapjsClientMock();
|
|
||||||
const ldap = {
|
|
||||||
createClient: sinon.spy(function () {
|
|
||||||
return ldapClient;
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
let config: UserConfiguration;
|
|
||||||
|
|
||||||
before(function () {
|
|
||||||
u2f = U2FMock.U2FMock();
|
|
||||||
|
|
||||||
const search_doc = {
|
|
||||||
object: {
|
|
||||||
mail: "test_ok@example.com"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const search_res = {
|
|
||||||
on: sinon.spy(function (event: string, fn: (s: object) => void) {
|
|
||||||
if (event != "error") fn(search_doc);
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
ldapClient.bind.withArgs("cn=admin,dc=example,dc=com",
|
|
||||||
"password").yields();
|
|
||||||
ldapClient.bind.withArgs("cn=test_ok,ou=users,dc=example,dc=com",
|
|
||||||
"password").yields();
|
|
||||||
ldapClient.bind.withArgs("cn=test_nok,ou=users,dc=example,dc=com",
|
|
||||||
"password").yields("error");
|
|
||||||
ldapClient.search.yields(undefined, search_res);
|
|
||||||
ldapClient.unbind.yields();
|
|
||||||
|
|
||||||
tmpDir = tmp.dirSync({ unsafeCleanup: true });
|
|
||||||
config = {
|
|
||||||
port: PORT,
|
|
||||||
ldap: {
|
|
||||||
url: "ldap://127.0.0.1:389",
|
|
||||||
base_dn: "ou=users,dc=example,dc=com",
|
|
||||||
user: "cn=admin,dc=example,dc=com",
|
|
||||||
password: "password"
|
|
||||||
},
|
|
||||||
session: {
|
|
||||||
secret: "session_secret",
|
|
||||||
expiration: 50000,
|
|
||||||
},
|
|
||||||
storage: {
|
|
||||||
local: {
|
|
||||||
path: tmpDir.name
|
|
||||||
}
|
|
||||||
},
|
|
||||||
notifier: {
|
|
||||||
gmail: {
|
|
||||||
username: "user@example.com",
|
|
||||||
password: "password"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
after(function () {
|
|
||||||
tmpDir.removeCallback();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should save a u2f meta and reload it after a restart of the server", function () {
|
|
||||||
let server: Server;
|
|
||||||
const sign_request = {};
|
|
||||||
const sign_status = {};
|
|
||||||
const registration_status = {};
|
|
||||||
u2f.request.returns(sign_request);
|
|
||||||
u2f.checkRegistration.returns(sign_status);
|
|
||||||
u2f.checkSignature.returns(registration_status);
|
|
||||||
|
|
||||||
const nodemailer = {
|
|
||||||
createTransport: sinon.spy(function () {
|
|
||||||
return transporter;
|
|
||||||
})
|
|
||||||
};
|
|
||||||
const transporter = {
|
|
||||||
sendMail: sinon.stub().yields()
|
|
||||||
};
|
|
||||||
|
|
||||||
const deps: GlobalDependencies = {
|
|
||||||
u2f: u2f,
|
|
||||||
nedb: nedb,
|
|
||||||
nodemailer: nodemailer,
|
|
||||||
session: session,
|
|
||||||
winston: winston,
|
|
||||||
ldapjs: ldap,
|
|
||||||
speakeasy: speakeasy,
|
|
||||||
ConnectRedis: sinon.spy(),
|
|
||||||
dovehash: sinon.spy()
|
|
||||||
};
|
|
||||||
|
|
||||||
const j1 = request.jar();
|
|
||||||
const j2 = request.jar();
|
|
||||||
|
|
||||||
return start_server(config, deps)
|
|
||||||
.then(function (s) {
|
|
||||||
server = s;
|
|
||||||
return requests.login(j1);
|
|
||||||
})
|
|
||||||
.then(function (res) {
|
|
||||||
return requests.first_factor(j1);
|
|
||||||
})
|
|
||||||
.then(function () {
|
|
||||||
return requests.u2f_registration(j1, transporter);
|
|
||||||
})
|
|
||||||
.then(function () {
|
|
||||||
return requests.u2f_authentication(j1);
|
|
||||||
})
|
|
||||||
.then(function () {
|
|
||||||
return stop_server(server);
|
|
||||||
})
|
|
||||||
.then(function () {
|
|
||||||
return start_server(config, deps);
|
|
||||||
})
|
|
||||||
.then(function (s) {
|
|
||||||
server = s;
|
|
||||||
return requests.login(j2);
|
|
||||||
})
|
|
||||||
.then(function () {
|
|
||||||
return requests.first_factor(j2);
|
|
||||||
})
|
|
||||||
.then(function () {
|
|
||||||
return requests.u2f_authentication(j2);
|
|
||||||
})
|
|
||||||
.then(function (res) {
|
|
||||||
assert.equal(200, res.statusCode);
|
|
||||||
server.stop();
|
|
||||||
return BluebirdPromise.resolve();
|
|
||||||
})
|
|
||||||
.catch(function (err) {
|
|
||||||
console.error(err);
|
|
||||||
return BluebirdPromise.reject(err);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function start_server(config: UserConfiguration, deps: GlobalDependencies): BluebirdPromise<Server> {
|
|
||||||
return new BluebirdPromise<Server>(function (resolve, reject) {
|
|
||||||
const s = new Server();
|
|
||||||
s.start(config, deps);
|
|
||||||
resolve(s);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function stop_server(s: Server) {
|
|
||||||
return new BluebirdPromise(function (resolve, reject) {
|
|
||||||
s.stop();
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -67,6 +67,11 @@ describe("test server configuration", function () {
|
||||||
password: "password"
|
password: "password"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
regulation: {
|
||||||
|
max_retries: 3,
|
||||||
|
ban_time: 5 * 60,
|
||||||
|
find_time: 5 * 60
|
||||||
|
},
|
||||||
storage: {
|
storage: {
|
||||||
local: {
|
local: {
|
||||||
in_memory: true
|
in_memory: true
|
||||||
|
|
|
@ -17,9 +17,14 @@ describe("test session configuration builder", function () {
|
||||||
},
|
},
|
||||||
ldap: {
|
ldap: {
|
||||||
url: "ldap://ldap",
|
url: "ldap://ldap",
|
||||||
base_dn: "dc=example,dc=com",
|
|
||||||
user: "user",
|
user: "user",
|
||||||
password: "password"
|
password: "password",
|
||||||
|
groups_dn: "ou=groups,dc=example,dc=com",
|
||||||
|
users_dn: "ou=users,dc=example,dc=com",
|
||||||
|
group_name_attribute: "",
|
||||||
|
groups_filter: "",
|
||||||
|
mail_attribute: "",
|
||||||
|
users_filter: ""
|
||||||
},
|
},
|
||||||
logs_level: "debug",
|
logs_level: "debug",
|
||||||
notifier: {
|
notifier: {
|
||||||
|
@ -33,6 +38,11 @@ describe("test session configuration builder", function () {
|
||||||
expiration: 3600,
|
expiration: 3600,
|
||||||
secret: "secret"
|
secret: "secret"
|
||||||
},
|
},
|
||||||
|
regulation: {
|
||||||
|
max_retries: 3,
|
||||||
|
ban_time: 5 * 60,
|
||||||
|
find_time: 5 * 60
|
||||||
|
},
|
||||||
storage: {
|
storage: {
|
||||||
local: {
|
local: {
|
||||||
in_memory: true
|
in_memory: true
|
||||||
|
@ -77,9 +87,14 @@ describe("test session configuration builder", function () {
|
||||||
},
|
},
|
||||||
ldap: {
|
ldap: {
|
||||||
url: "ldap://ldap",
|
url: "ldap://ldap",
|
||||||
base_dn: "dc=example,dc=com",
|
|
||||||
user: "user",
|
user: "user",
|
||||||
password: "password"
|
password: "password",
|
||||||
|
groups_dn: "ou=groups,dc=example,dc=com",
|
||||||
|
users_dn: "ou=users,dc=example,dc=com",
|
||||||
|
group_name_attribute: "",
|
||||||
|
groups_filter: "",
|
||||||
|
mail_attribute: "",
|
||||||
|
users_filter: ""
|
||||||
},
|
},
|
||||||
logs_level: "debug",
|
logs_level: "debug",
|
||||||
notifier: {
|
notifier: {
|
||||||
|
@ -97,6 +112,11 @@ describe("test session configuration builder", function () {
|
||||||
port: 6379
|
port: 6379
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
regulation: {
|
||||||
|
max_retries: 3,
|
||||||
|
ban_time: 5 * 60,
|
||||||
|
find_time: 5 * 60
|
||||||
|
},
|
||||||
storage: {
|
storage: {
|
||||||
local: {
|
local: {
|
||||||
in_memory: true
|
in_memory: true
|
||||||
|
|
|
@ -1,27 +1,34 @@
|
||||||
import * as Assert from "assert";
|
import * as Assert from "assert";
|
||||||
import { UserConfiguration } from "../../../src/server/lib/configuration/Configuration";
|
import { UserConfiguration, LdapConfiguration } from "../../../../src/server/lib/configuration/Configuration";
|
||||||
import { ConfigurationAdapter } from "../../../src/server/lib/configuration/ConfigurationAdapter";
|
import { ConfigurationAdapter } from "../../../../src/server/lib/configuration/ConfigurationAdapter";
|
||||||
|
|
||||||
describe("test config adapter", function() {
|
describe("test config adapter", function () {
|
||||||
function build_yaml_config(): UserConfiguration {
|
function build_yaml_config(): UserConfiguration {
|
||||||
const yaml_config = {
|
const yaml_config: UserConfiguration = {
|
||||||
port: 8080,
|
port: 8080,
|
||||||
ldap: {
|
ldap: {
|
||||||
url: "http://ldap",
|
url: "http://ldap",
|
||||||
base_dn: "cn=test,dc=example,dc=com",
|
base_dn: "dc=example,dc=com",
|
||||||
|
additional_users_dn: "ou=users",
|
||||||
|
additional_groups_dn: "ou=groups",
|
||||||
user: "user",
|
user: "user",
|
||||||
password: "pass"
|
password: "pass"
|
||||||
},
|
},
|
||||||
session: {
|
session: {
|
||||||
domain: "example.com",
|
domain: "example.com",
|
||||||
secret: "secret",
|
secret: "secret",
|
||||||
max_age: 40000
|
expiration: 40000
|
||||||
},
|
},
|
||||||
storage: {
|
storage: {
|
||||||
local: {
|
local: {
|
||||||
path: "/mydirectory"
|
path: "/mydirectory"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
regulation: {
|
||||||
|
max_retries: 3,
|
||||||
|
find_time: 5 * 60,
|
||||||
|
ban_time: 5 * 60
|
||||||
|
},
|
||||||
logs_level: "debug",
|
logs_level: "debug",
|
||||||
notifier: {
|
notifier: {
|
||||||
gmail: {
|
gmail: {
|
||||||
|
@ -33,41 +40,21 @@ describe("test config adapter", function() {
|
||||||
return yaml_config;
|
return yaml_config;
|
||||||
}
|
}
|
||||||
|
|
||||||
it("should read the port from the yaml file", function() {
|
it("should read the port from the yaml file", function () {
|
||||||
const yaml_config = build_yaml_config();
|
const yaml_config = build_yaml_config();
|
||||||
yaml_config.port = 7070;
|
yaml_config.port = 7070;
|
||||||
const config = ConfigurationAdapter.adapt(yaml_config);
|
const config = ConfigurationAdapter.adapt(yaml_config);
|
||||||
Assert.equal(config.port, 7070);
|
Assert.equal(config.port, 7070);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should default the port to 8080 if not provided", function() {
|
it("should default the port to 8080 if not provided", function () {
|
||||||
const yaml_config = build_yaml_config();
|
const yaml_config = build_yaml_config();
|
||||||
delete yaml_config.port;
|
delete yaml_config.port;
|
||||||
const config = ConfigurationAdapter.adapt(yaml_config);
|
const config = ConfigurationAdapter.adapt(yaml_config);
|
||||||
Assert.equal(config.port, 8080);
|
Assert.equal(config.port, 8080);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should get the ldap attributes", function() {
|
it("should get the session attributes", function () {
|
||||||
const yaml_config = build_yaml_config();
|
|
||||||
yaml_config.ldap = {
|
|
||||||
url: "http://ldap",
|
|
||||||
base_dn: "cn=test,dc=example,dc=com",
|
|
||||||
additional_user_dn: "ou=users",
|
|
||||||
user_name_attribute: "uid",
|
|
||||||
user: "admin",
|
|
||||||
password: "pass"
|
|
||||||
};
|
|
||||||
|
|
||||||
const config = ConfigurationAdapter.adapt(yaml_config);
|
|
||||||
|
|
||||||
Assert.equal(config.ldap.url, "http://ldap");
|
|
||||||
Assert.equal(config.ldap.additional_user_dn, "ou=users");
|
|
||||||
Assert.equal(config.ldap.user_name_attribute, "uid");
|
|
||||||
Assert.equal(config.ldap.user, "admin");
|
|
||||||
Assert.equal(config.ldap.password, "pass");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should get the session attributes", function() {
|
|
||||||
const yaml_config = build_yaml_config();
|
const yaml_config = build_yaml_config();
|
||||||
yaml_config.session = {
|
yaml_config.session = {
|
||||||
domain: "example.com",
|
domain: "example.com",
|
||||||
|
@ -80,14 +67,14 @@ describe("test config adapter", function() {
|
||||||
Assert.equal(config.session.expiration, 3600);
|
Assert.equal(config.session.expiration, 3600);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should get the log level", function() {
|
it("should get the log level", function () {
|
||||||
const yaml_config = build_yaml_config();
|
const yaml_config = build_yaml_config();
|
||||||
yaml_config.logs_level = "debug";
|
yaml_config.logs_level = "debug";
|
||||||
const config = ConfigurationAdapter.adapt(yaml_config);
|
const config = ConfigurationAdapter.adapt(yaml_config);
|
||||||
Assert.equal(config.logs_level, "debug");
|
Assert.equal(config.logs_level, "debug");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should get the notifier config", function() {
|
it("should get the notifier config", function () {
|
||||||
const yaml_config = build_yaml_config();
|
const yaml_config = build_yaml_config();
|
||||||
yaml_config.notifier = {
|
yaml_config.notifier = {
|
||||||
gmail: {
|
gmail: {
|
||||||
|
@ -104,7 +91,7 @@ describe("test config adapter", function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should get the access_control config", function() {
|
it("should get the access_control config", function () {
|
||||||
const yaml_config = build_yaml_config();
|
const yaml_config = build_yaml_config();
|
||||||
yaml_config.access_control = {
|
yaml_config.access_control = {
|
||||||
default: [],
|
default: [],
|
|
@ -0,0 +1,98 @@
|
||||||
|
import * as Assert from "assert";
|
||||||
|
import { UserConfiguration, LdapConfiguration } from "../../../../src/server/lib/configuration/Configuration";
|
||||||
|
import { ConfigurationAdapter } from "../../../../src/server/lib/configuration/ConfigurationAdapter";
|
||||||
|
|
||||||
|
describe("test ldap configuration adaptation", function () {
|
||||||
|
function build_yaml_config(): UserConfiguration {
|
||||||
|
const yaml_config: UserConfiguration = {
|
||||||
|
port: 8080,
|
||||||
|
ldap: {
|
||||||
|
url: "http://ldap",
|
||||||
|
base_dn: "dc=example,dc=com",
|
||||||
|
additional_users_dn: "ou=users",
|
||||||
|
additional_groups_dn: "ou=groups",
|
||||||
|
user: "user",
|
||||||
|
password: "pass"
|
||||||
|
},
|
||||||
|
session: {
|
||||||
|
domain: "example.com",
|
||||||
|
secret: "secret",
|
||||||
|
expiration: 40000
|
||||||
|
},
|
||||||
|
storage: {
|
||||||
|
local: {
|
||||||
|
path: "/mydirectory"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
regulation: {
|
||||||
|
max_retries: 3,
|
||||||
|
ban_time: 5 * 60,
|
||||||
|
find_time: 5 * 60,
|
||||||
|
},
|
||||||
|
logs_level: "debug",
|
||||||
|
notifier: {
|
||||||
|
gmail: {
|
||||||
|
username: "user",
|
||||||
|
password: "password"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return yaml_config;
|
||||||
|
}
|
||||||
|
|
||||||
|
it("should adapt correctly while user only specify mandatory fields", function () {
|
||||||
|
const yaml_config = build_yaml_config();
|
||||||
|
yaml_config.ldap = {
|
||||||
|
url: "http://ldap",
|
||||||
|
base_dn: "dc=example,dc=com",
|
||||||
|
user: "admin",
|
||||||
|
password: "password"
|
||||||
|
};
|
||||||
|
|
||||||
|
const config = ConfigurationAdapter.adapt(yaml_config);
|
||||||
|
const expectedConfig: LdapConfiguration = {
|
||||||
|
url: "http://ldap",
|
||||||
|
users_dn: "dc=example,dc=com",
|
||||||
|
users_filter: "cn={0}",
|
||||||
|
groups_dn: "dc=example,dc=com",
|
||||||
|
groups_filter: "member={0}",
|
||||||
|
group_name_attribute: "cn",
|
||||||
|
mail_attribute: "mail",
|
||||||
|
user: "admin",
|
||||||
|
password: "password"
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.deepEqual(config.ldap, expectedConfig);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should adapt correctly while user specify every fields", function () {
|
||||||
|
const yaml_config = build_yaml_config();
|
||||||
|
yaml_config.ldap = {
|
||||||
|
url: "http://ldap-server",
|
||||||
|
base_dn: "dc=example,dc=com",
|
||||||
|
additional_users_dn: "ou=users",
|
||||||
|
users_filter: "uid={0}",
|
||||||
|
additional_groups_dn: "ou=groups",
|
||||||
|
groups_filter: "uniqueMember={0}",
|
||||||
|
mail_attribute: "email",
|
||||||
|
group_name_attribute: "groupName",
|
||||||
|
user: "admin2",
|
||||||
|
password: "password2"
|
||||||
|
};
|
||||||
|
|
||||||
|
const config = ConfigurationAdapter.adapt(yaml_config);
|
||||||
|
const expectedConfig: LdapConfiguration = {
|
||||||
|
url: "http://ldap-server",
|
||||||
|
users_dn: "ou=users,dc=example,dc=com",
|
||||||
|
users_filter: "uid={0}",
|
||||||
|
groups_dn: "ou=groups,dc=example,dc=com",
|
||||||
|
groups_filter: "uniqueMember={0}",
|
||||||
|
mail_attribute: "email",
|
||||||
|
group_name_attribute: "groupName",
|
||||||
|
user: "admin2",
|
||||||
|
password: "password2"
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.deepEqual(config.ldap, expectedConfig);
|
||||||
|
});
|
||||||
|
});
|
|
@ -2,121 +2,127 @@
|
||||||
import { Authenticator } from "../../../../src/server/lib/ldap/Authenticator";
|
import { Authenticator } from "../../../../src/server/lib/ldap/Authenticator";
|
||||||
import { LdapConfiguration } from "../../../../src/server/lib/configuration/Configuration";
|
import { LdapConfiguration } from "../../../../src/server/lib/configuration/Configuration";
|
||||||
|
|
||||||
import sinon = require("sinon");
|
import Sinon = require("sinon");
|
||||||
import BluebirdPromise = require("bluebird");
|
import BluebirdPromise = require("bluebird");
|
||||||
import assert = require("assert");
|
import Assert = require("assert");
|
||||||
import ldapjs = require("ldapjs");
|
|
||||||
import winston = require("winston");
|
|
||||||
import { EventEmitter } from "events";
|
|
||||||
|
|
||||||
import { LdapjsMock, LdapjsClientMock } from "../mocks/ldapjs";
|
import { ClientFactoryStub } from "../mocks/ldap/ClientFactoryStub";
|
||||||
|
import { ClientStub } from "../mocks/ldap/ClientStub";
|
||||||
|
|
||||||
|
|
||||||
describe("test ldap authentication", function () {
|
describe("test ldap authentication", function () {
|
||||||
|
const USERNAME = "username";
|
||||||
|
const PASSWORD = "password";
|
||||||
|
|
||||||
|
const ADMIN_USER_DN = "cn=admin,dc=example,dc=com";
|
||||||
|
const ADMIN_PASSWORD = "admin_password";
|
||||||
|
|
||||||
|
let clientFactoryStub: ClientFactoryStub;
|
||||||
|
let adminClientStub: ClientStub;
|
||||||
|
let userClientStub: ClientStub;
|
||||||
|
|
||||||
let authenticator: Authenticator;
|
let authenticator: Authenticator;
|
||||||
let ldapClient: LdapjsClientMock;
|
|
||||||
let ldapjs: LdapjsMock;
|
|
||||||
let ldapConfig: LdapConfiguration;
|
let ldapConfig: LdapConfiguration;
|
||||||
let adminUserDN: string;
|
|
||||||
let adminPassword: string;
|
|
||||||
|
|
||||||
function retrieveEmailsAndGroups(ldapClient: LdapjsClientMock) {
|
|
||||||
const email0 = {
|
|
||||||
object: {
|
|
||||||
mail: "user@example.com"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const email1 = {
|
|
||||||
object: {
|
|
||||||
mail: "user@example1.com"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const group0 = {
|
|
||||||
object: {
|
|
||||||
group: "group0"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const emailsEmitter = {
|
|
||||||
on: sinon.spy(function (event: string, fn: (doc: any) => void) {
|
|
||||||
if (event != "error") fn(email0);
|
|
||||||
if (event != "error") fn(email1);
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
const groupsEmitter = {
|
|
||||||
on: sinon.spy(function (event: string, fn: (doc: any) => void) {
|
|
||||||
if (event != "error") fn(group0);
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
ldapClient.search.onCall(0).yields(undefined, emailsEmitter);
|
|
||||||
ldapClient.search.onCall(1).yields(undefined, groupsEmitter);
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
ldapClient = LdapjsClientMock();
|
clientFactoryStub = new ClientFactoryStub();
|
||||||
ldapjs = LdapjsMock();
|
adminClientStub = new ClientStub();
|
||||||
ldapjs.createClient.returns(ldapClient);
|
userClientStub = new ClientStub();
|
||||||
|
|
||||||
// winston.level = "debug";
|
|
||||||
|
|
||||||
adminUserDN = "cn=admin,dc=example,dc=com";
|
|
||||||
adminPassword = "password";
|
|
||||||
|
|
||||||
ldapConfig = {
|
ldapConfig = {
|
||||||
url: "http://localhost:324",
|
url: "http://localhost:324",
|
||||||
user: adminUserDN,
|
users_dn: "ou=users,dc=example,dc=com",
|
||||||
password: adminPassword,
|
users_filter: "cn={0}",
|
||||||
base_dn: "dc=example,dc=com",
|
groups_dn: "ou=groups,dc=example,dc=com",
|
||||||
additional_user_dn: "ou=users"
|
groups_filter: "member={0}",
|
||||||
|
mail_attribute: "mail",
|
||||||
|
group_name_attribute: "cn",
|
||||||
|
user: ADMIN_USER_DN,
|
||||||
|
password: ADMIN_PASSWORD
|
||||||
};
|
};
|
||||||
|
|
||||||
authenticator = new Authenticator(ldapConfig, ldapjs, winston);
|
authenticator = new Authenticator(ldapConfig, clientFactoryStub);
|
||||||
});
|
});
|
||||||
|
|
||||||
function test_check_password_internal() {
|
|
||||||
const username = "username";
|
|
||||||
const password = "password";
|
|
||||||
return authenticator.authenticate(username, password);
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("success", function () {
|
describe("success", function () {
|
||||||
beforeEach(function () {
|
|
||||||
retrieveEmailsAndGroups(ldapClient);
|
|
||||||
ldapClient.bind.withArgs(adminUserDN, adminPassword).yields();
|
|
||||||
ldapClient.unbind.yields();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should bind the user if good credentials provided", function () {
|
it("should bind the user if good credentials provided", function () {
|
||||||
ldapClient.bind.withArgs("cn=username,ou=users,dc=example,dc=com", "password").yields();
|
clientFactoryStub.createStub.withArgs(ADMIN_USER_DN, ADMIN_PASSWORD)
|
||||||
return test_check_password_internal();
|
.returns(adminClientStub);
|
||||||
});
|
clientFactoryStub.createStub.withArgs("cn=" + USERNAME + ",ou=users,dc=example,dc=com", PASSWORD)
|
||||||
|
.returns(userClientStub);
|
||||||
|
|
||||||
it("should bind the user with correct DN", function () {
|
// admin connects successfully
|
||||||
ldapConfig.user_name_attribute = "uid";
|
adminClientStub.openStub.returns(BluebirdPromise.resolve());
|
||||||
ldapClient.bind.withArgs("uid=username,ou=users,dc=example,dc=com", "password").yields();
|
adminClientStub.closeStub.returns(BluebirdPromise.resolve());
|
||||||
return test_check_password_internal();
|
|
||||||
|
// admin search for user dn of user
|
||||||
|
adminClientStub.searchUserDnStub.withArgs(USERNAME)
|
||||||
|
.returns(BluebirdPromise.resolve("cn=" + USERNAME + ",ou=users,dc=example,dc=com"));
|
||||||
|
|
||||||
|
// user connects successfully
|
||||||
|
userClientStub.openStub.returns(BluebirdPromise.resolve());
|
||||||
|
userClientStub.closeStub.returns(BluebirdPromise.resolve());
|
||||||
|
|
||||||
|
// admin retrieves emails and groups of user
|
||||||
|
adminClientStub.searchEmailsAndGroupsStub.returns(BluebirdPromise.resolve({
|
||||||
|
groups: ["group1"],
|
||||||
|
emails: ["user@example.com"]
|
||||||
|
}));
|
||||||
|
|
||||||
|
return authenticator.authenticate(USERNAME, PASSWORD);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("failure", function () {
|
describe("failure", function () {
|
||||||
it("should not bind the user if wrong credentials provided", function () {
|
it("should not bind the user if wrong credentials provided", function () {
|
||||||
ldapClient.bind.yields("wrong credentials");
|
clientFactoryStub.createStub.withArgs(ADMIN_USER_DN, ADMIN_PASSWORD)
|
||||||
return test_check_password_internal()
|
.returns(adminClientStub);
|
||||||
|
clientFactoryStub.createStub.withArgs("cn=" + USERNAME + ",ou=users,dc=example,dc=com", PASSWORD)
|
||||||
|
.returns(userClientStub);
|
||||||
|
|
||||||
|
// admin connects successfully
|
||||||
|
adminClientStub.openStub.returns(BluebirdPromise.resolve());
|
||||||
|
adminClientStub.closeStub.returns(BluebirdPromise.resolve());
|
||||||
|
|
||||||
|
// admin search for user dn of user
|
||||||
|
adminClientStub.searchUserDnStub.withArgs(USERNAME)
|
||||||
|
.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.closeStub.returns(BluebirdPromise.resolve());
|
||||||
|
|
||||||
|
return authenticator.authenticate(USERNAME, PASSWORD)
|
||||||
|
.then(function () { return BluebirdPromise.reject("Should not be here!"); })
|
||||||
.catch(function () {
|
.catch(function () {
|
||||||
return BluebirdPromise.resolve();
|
return BluebirdPromise.resolve();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not bind the user if search of emails or group fails", function () {
|
it("should not bind the user if search of emails or group fails", function () {
|
||||||
ldapClient.bind.withArgs("cn=username,ou=users,dc=example,dc=com", "password").yields();
|
clientFactoryStub.createStub.withArgs(ADMIN_USER_DN, ADMIN_PASSWORD)
|
||||||
ldapClient.bind.withArgs(adminUserDN, adminPassword).yields();
|
.returns(adminClientStub);
|
||||||
ldapClient.unbind.yields();
|
clientFactoryStub.createStub.withArgs("cn=" + USERNAME + ",ou=users,dc=example,dc=com", PASSWORD)
|
||||||
ldapClient.search.yields("wrong credentials");
|
.returns(userClientStub);
|
||||||
return test_check_password_internal()
|
|
||||||
|
// admin connects successfully
|
||||||
|
adminClientStub.openStub.returns(BluebirdPromise.resolve());
|
||||||
|
adminClientStub.closeStub.returns(BluebirdPromise.resolve());
|
||||||
|
|
||||||
|
// admin search for user dn of user
|
||||||
|
adminClientStub.searchUserDnStub.withArgs(USERNAME)
|
||||||
|
.returns(BluebirdPromise.resolve("cn=" + USERNAME + ",ou=users,dc=example,dc=com"));
|
||||||
|
|
||||||
|
// user connects successfully
|
||||||
|
userClientStub.openStub.returns(BluebirdPromise.resolve());
|
||||||
|
userClientStub.closeStub.returns(BluebirdPromise.resolve());
|
||||||
|
|
||||||
|
// admin retrieves emails and groups of user
|
||||||
|
adminClientStub.searchEmailsAndGroupsStub
|
||||||
|
.returns(BluebirdPromise.reject(new Error("Error while retrieving emails and groups")));
|
||||||
|
|
||||||
|
return authenticator.authenticate(USERNAME, PASSWORD)
|
||||||
|
.then(function () { return BluebirdPromise.reject("Should not be here!"); })
|
||||||
.catch(function () {
|
.catch(function () {
|
||||||
return BluebirdPromise.resolve();
|
return BluebirdPromise.resolve();
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,93 +2,74 @@
|
||||||
import { EmailsRetriever } from "../../../../src/server/lib/ldap/EmailsRetriever";
|
import { EmailsRetriever } from "../../../../src/server/lib/ldap/EmailsRetriever";
|
||||||
import { LdapConfiguration } from "../../../../src/server/lib/configuration/Configuration";
|
import { LdapConfiguration } from "../../../../src/server/lib/configuration/Configuration";
|
||||||
|
|
||||||
import sinon = require("sinon");
|
import Sinon = require("sinon");
|
||||||
import BluebirdPromise = require("bluebird");
|
import BluebirdPromise = require("bluebird");
|
||||||
import assert = require("assert");
|
import Assert = require("assert");
|
||||||
import ldapjs = require("ldapjs");
|
|
||||||
import winston = require("winston");
|
|
||||||
import { EventEmitter } from "events";
|
|
||||||
|
|
||||||
import { LdapjsMock, LdapjsClientMock } from "../mocks/ldapjs";
|
|
||||||
|
|
||||||
|
import { ClientFactoryStub } from "../mocks/ldap/ClientFactoryStub";
|
||||||
|
import { ClientStub } from "../mocks/ldap/ClientStub";
|
||||||
|
|
||||||
describe("test emails retriever", function () {
|
describe("test emails 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 emailsRetriever: EmailsRetriever;
|
let emailsRetriever: EmailsRetriever;
|
||||||
let ldapClient: LdapjsClientMock;
|
|
||||||
let ldapjs: LdapjsMock;
|
|
||||||
let ldapConfig: LdapConfiguration;
|
let ldapConfig: LdapConfiguration;
|
||||||
let adminUserDN: string;
|
|
||||||
let adminPassword: string;
|
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
ldapClient = LdapjsClientMock();
|
clientFactoryStub = new ClientFactoryStub();
|
||||||
ldapjs = LdapjsMock();
|
adminClientStub = new ClientStub();
|
||||||
ldapjs.createClient.returns(ldapClient);
|
|
||||||
|
|
||||||
// winston.level = "debug";
|
|
||||||
|
|
||||||
adminUserDN = "cn=admin,dc=example,dc=com";
|
|
||||||
adminPassword = "password";
|
|
||||||
|
|
||||||
ldapConfig = {
|
ldapConfig = {
|
||||||
url: "http://localhost:324",
|
url: "http://ldap",
|
||||||
user: adminUserDN,
|
user: ADMIN_USER_DN,
|
||||||
password: adminPassword,
|
password: ADMIN_PASSWORD,
|
||||||
base_dn: "dc=example,dc=com",
|
users_dn: "ou=users,dc=example,dc=com",
|
||||||
additional_user_dn: "ou=users"
|
groups_dn: "ou=groups,dc=example,dc=com",
|
||||||
|
group_name_attribute: "cn",
|
||||||
|
groups_filter: "cn={0}",
|
||||||
|
mail_attribute: "mail",
|
||||||
|
users_filter: "cn={0}"
|
||||||
};
|
};
|
||||||
|
|
||||||
emailsRetriever = new EmailsRetriever(ldapConfig, ldapjs, winston);
|
emailsRetriever = new EmailsRetriever(ldapConfig, clientFactoryStub);
|
||||||
});
|
});
|
||||||
|
|
||||||
function retrieveEmails(ldapClient: LdapjsClientMock) {
|
|
||||||
const email0 = {
|
|
||||||
object: {
|
|
||||||
mail: "user@example.com"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const email1 = {
|
|
||||||
object: {
|
|
||||||
mail: "user@example1.com"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const emailsEmitter = {
|
|
||||||
on: sinon.spy(function (event: string, fn: (doc: any) => void) {
|
|
||||||
if (event != "error") fn(email0);
|
|
||||||
if (event != "error") fn(email1);
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
ldapClient.search.onCall(0).yields(undefined, emailsEmitter);
|
|
||||||
}
|
|
||||||
|
|
||||||
function test_emails_retrieval() {
|
|
||||||
const username = "username";
|
|
||||||
return emailsRetriever.retrieve(username);
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("success", function () {
|
describe("success", function () {
|
||||||
beforeEach(function () {
|
it("should retrieve emails successfully", function () {
|
||||||
ldapClient.bind.withArgs(adminUserDN, adminPassword).yields();
|
clientFactoryStub.createStub.withArgs(ADMIN_USER_DN, ADMIN_PASSWORD)
|
||||||
ldapClient.unbind.yields();
|
.returns(adminClientStub);
|
||||||
});
|
|
||||||
|
|
||||||
it("should update the password successfully", function () {
|
// admin connects successfully
|
||||||
retrieveEmails(ldapClient);
|
adminClientStub.openStub.returns(BluebirdPromise.resolve());
|
||||||
return test_emails_retrieval();
|
adminClientStub.closeStub.returns(BluebirdPromise.resolve());
|
||||||
|
|
||||||
|
adminClientStub.searchEmailsStub.withArgs(USERNAME)
|
||||||
|
.returns(BluebirdPromise.resolve(["user@example.com"]));
|
||||||
|
|
||||||
|
return emailsRetriever.retrieve(USERNAME);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("failure", function () {
|
describe("failure", function () {
|
||||||
it("should fail retrieving emails when search operation fails", function () {
|
it("should fail retrieving emails when search operation fails", function () {
|
||||||
ldapClient.bind.withArgs(adminUserDN, adminPassword).yields();
|
clientFactoryStub.createStub.withArgs(ADMIN_USER_DN, ADMIN_PASSWORD)
|
||||||
ldapClient.search.yields("wrong credentials");
|
.returns(adminClientStub);
|
||||||
return test_emails_retrieval()
|
|
||||||
.catch(function () {
|
// admin connects successfully
|
||||||
return BluebirdPromise.resolve();
|
adminClientStub.openStub.returns(BluebirdPromise.resolve());
|
||||||
});
|
adminClientStub.closeStub.returns(BluebirdPromise.resolve());
|
||||||
|
|
||||||
|
adminClientStub.searchEmailsStub.withArgs(USERNAME)
|
||||||
|
.returns(BluebirdPromise.reject(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(); });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
|
@ -2,82 +2,78 @@
|
||||||
import { PasswordUpdater } from "../../../../src/server/lib/ldap/PasswordUpdater";
|
import { PasswordUpdater } from "../../../../src/server/lib/ldap/PasswordUpdater";
|
||||||
import { LdapConfiguration } from "../../../../src/server/lib/configuration/Configuration";
|
import { LdapConfiguration } from "../../../../src/server/lib/configuration/Configuration";
|
||||||
|
|
||||||
import sinon = require("sinon");
|
import Sinon = require("sinon");
|
||||||
import BluebirdPromise = require("bluebird");
|
import BluebirdPromise = require("bluebird");
|
||||||
import assert = require("assert");
|
import Assert = require("assert");
|
||||||
import ldapjs = require("ldapjs");
|
|
||||||
import winston = require("winston");
|
|
||||||
import { EventEmitter } from "events";
|
|
||||||
|
|
||||||
import { LdapjsMock, LdapjsClientMock } from "../mocks/ldapjs";
|
|
||||||
|
|
||||||
|
import { ClientFactoryStub } from "../mocks/ldap/ClientFactoryStub";
|
||||||
|
import { ClientStub } from "../mocks/ldap/ClientStub";
|
||||||
|
|
||||||
describe("test password update", function () {
|
describe("test password update", function () {
|
||||||
|
const USERNAME = "username";
|
||||||
|
const NEW_PASSWORD = "new-password";
|
||||||
|
|
||||||
|
const ADMIN_USER_DN = "cn=admin,dc=example,dc=com";
|
||||||
|
const ADMIN_PASSWORD = "password";
|
||||||
|
|
||||||
|
let clientFactoryStub: ClientFactoryStub;
|
||||||
|
let adminClientStub: ClientStub;
|
||||||
|
|
||||||
let passwordUpdater: PasswordUpdater;
|
let passwordUpdater: PasswordUpdater;
|
||||||
let ldapClient: LdapjsClientMock;
|
|
||||||
let ldapjs: LdapjsMock;
|
|
||||||
let ldapConfig: LdapConfiguration;
|
let ldapConfig: LdapConfiguration;
|
||||||
let adminUserDN: string;
|
|
||||||
let adminPassword: string;
|
|
||||||
let dovehash: any;
|
let dovehash: any;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
ldapClient = LdapjsClientMock();
|
clientFactoryStub = new ClientFactoryStub();
|
||||||
ldapjs = LdapjsMock();
|
adminClientStub = new ClientStub();
|
||||||
ldapjs.createClient.returns(ldapClient);
|
|
||||||
|
|
||||||
// winston.level = "debug";
|
|
||||||
|
|
||||||
adminUserDN = "cn=admin,dc=example,dc=com";
|
|
||||||
adminPassword = "password";
|
|
||||||
|
|
||||||
ldapConfig = {
|
ldapConfig = {
|
||||||
url: "http://localhost:324",
|
url: "http://ldap",
|
||||||
user: adminUserDN,
|
user: ADMIN_USER_DN,
|
||||||
password: adminPassword,
|
password: ADMIN_PASSWORD,
|
||||||
base_dn: "dc=example,dc=com",
|
users_dn: "ou=users,dc=example,dc=com",
|
||||||
additional_user_dn: "ou=users"
|
groups_dn: "ou=groups,dc=example,dc=com",
|
||||||
|
group_name_attribute: "cn",
|
||||||
|
groups_filter: "cn={0}",
|
||||||
|
mail_attribute: "mail",
|
||||||
|
users_filter: "cn={0}"
|
||||||
};
|
};
|
||||||
|
|
||||||
dovehash = {
|
dovehash = {
|
||||||
encode: sinon.stub()
|
encode: Sinon.stub()
|
||||||
};
|
};
|
||||||
|
|
||||||
passwordUpdater = new PasswordUpdater(ldapConfig, ldapjs, dovehash, winston);
|
passwordUpdater = new PasswordUpdater(ldapConfig, clientFactoryStub);
|
||||||
});
|
});
|
||||||
|
|
||||||
function test_update_password() {
|
|
||||||
const username = "username";
|
|
||||||
const newpassword = "newpassword";
|
|
||||||
return passwordUpdater.updatePassword(username, newpassword);
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("success", function () {
|
describe("success", function () {
|
||||||
beforeEach(function () {
|
|
||||||
ldapClient.bind.withArgs(adminUserDN, adminPassword).yields();
|
|
||||||
ldapClient.unbind.yields();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should update the password successfully", function () {
|
it("should update the password successfully", function () {
|
||||||
|
clientFactoryStub.createStub.withArgs(ADMIN_USER_DN, ADMIN_PASSWORD)
|
||||||
|
.returns(adminClientStub);
|
||||||
|
|
||||||
dovehash.encode.returns("{SSHA}AQmxaKfobGY9HSQa6aDYkAWOgPGNhGYn");
|
dovehash.encode.returns("{SSHA}AQmxaKfobGY9HSQa6aDYkAWOgPGNhGYn");
|
||||||
ldapClient.modify.withArgs("cn=username,ou=users,dc=example,dc=com", {
|
adminClientStub.modifyPasswordStub.withArgs(USERNAME, NEW_PASSWORD).returns(BluebirdPromise.resolve());
|
||||||
operation: "replace",
|
adminClientStub.openStub.returns(BluebirdPromise.resolve());
|
||||||
modification: {
|
adminClientStub.closeStub.returns(BluebirdPromise.resolve());
|
||||||
userPassword: "{SSHA}AQmxaKfobGY9HSQa6aDYkAWOgPGNhGYn"
|
|
||||||
}
|
return passwordUpdater.updatePassword(USERNAME, NEW_PASSWORD);
|
||||||
}).yields();
|
|
||||||
return test_update_password();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("failure", function () {
|
describe("failure", function () {
|
||||||
it("should fail updating password when modify operation fails", function () {
|
it("should fail updating password when modify operation fails", function () {
|
||||||
ldapClient.bind.withArgs(adminUserDN, adminPassword).yields();
|
clientFactoryStub.createStub.withArgs(ADMIN_USER_DN, ADMIN_PASSWORD)
|
||||||
ldapClient.modify.yields("wrong credentials");
|
.returns(adminClientStub);
|
||||||
return test_update_password()
|
|
||||||
.catch(function () {
|
dovehash.encode.returns("{SSHA}AQmxaKfobGY9HSQa6aDYkAWOgPGNhGYn");
|
||||||
return BluebirdPromise.resolve();
|
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(); });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
|
@ -2,7 +2,7 @@ import sinon = require("sinon");
|
||||||
import express = require("express");
|
import express = require("express");
|
||||||
import winston = require("winston");
|
import winston = require("winston");
|
||||||
import { UserDataStoreStub } from "./storage/UserDataStoreStub";
|
import { UserDataStoreStub } from "./storage/UserDataStoreStub";
|
||||||
import { ServerVariables, VARIABLES_KEY } from "../../../../src/server/lib/ServerVariablesHandler";
|
import { VARIABLES_KEY } from "../../../../src/server/lib/ServerVariablesHandler";
|
||||||
|
|
||||||
export interface ServerVariablesMock {
|
export interface ServerVariablesMock {
|
||||||
logger: any;
|
logger: any;
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
|
||||||
|
import { IClient } from "../../../../../src/server/lib/ldap/IClient";
|
||||||
|
import { IClientFactory } from "../../../../../src/server/lib/ldap/IClientFactory";
|
||||||
|
import Sinon = require("sinon");
|
||||||
|
|
||||||
|
export class ClientFactoryStub implements IClientFactory {
|
||||||
|
createStub: Sinon.SinonStub;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.createStub = Sinon.stub();
|
||||||
|
}
|
||||||
|
|
||||||
|
create(userDN: string, password: string): IClient {
|
||||||
|
return this.createStub(userDN, password);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
|
||||||
|
import BluebirdPromise = require("bluebird");
|
||||||
|
import { IClient, GroupsAndEmails } from "../../../../../src/server/lib/ldap/IClient";
|
||||||
|
import Sinon = require("sinon");
|
||||||
|
|
||||||
|
export class ClientStub implements IClient {
|
||||||
|
openStub: Sinon.SinonStub;
|
||||||
|
closeStub: Sinon.SinonStub;
|
||||||
|
searchUserDnStub: Sinon.SinonStub;
|
||||||
|
searchEmailsStub: Sinon.SinonStub;
|
||||||
|
searchEmailsAndGroupsStub: Sinon.SinonStub;
|
||||||
|
modifyPasswordStub: Sinon.SinonStub;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.openStub = Sinon.stub();
|
||||||
|
this.closeStub = Sinon.stub();
|
||||||
|
this.searchUserDnStub = Sinon.stub();
|
||||||
|
this.searchEmailsStub = Sinon.stub();
|
||||||
|
this.searchEmailsAndGroupsStub = Sinon.stub();
|
||||||
|
this.modifyPasswordStub = Sinon.stub();
|
||||||
|
}
|
||||||
|
|
||||||
|
open(): BluebirdPromise<void> {
|
||||||
|
return this.openStub();
|
||||||
|
}
|
||||||
|
|
||||||
|
close(): BluebirdPromise<void> {
|
||||||
|
return this.closeStub();
|
||||||
|
}
|
||||||
|
|
||||||
|
searchUserDn(username: string): BluebirdPromise<string> {
|
||||||
|
return this.searchUserDnStub(username);
|
||||||
|
}
|
||||||
|
|
||||||
|
searchEmails(username: string): BluebirdPromise<string[]> {
|
||||||
|
return this.searchEmailsStub(username);
|
||||||
|
}
|
||||||
|
|
||||||
|
searchEmailsAndGroups(username: string): BluebirdPromise<GroupsAndEmails> {
|
||||||
|
return this.searchEmailsAndGroupsStub(username);
|
||||||
|
}
|
||||||
|
|
||||||
|
modifyPassword(username: string, newPassword: string): BluebirdPromise<void> {
|
||||||
|
return this.modifyPasswordStub(username, newPassword);
|
||||||
|
}
|
||||||
|
}
|
|
@ -43,8 +43,8 @@ export class UserDataStoreStub implements IUserDataStore {
|
||||||
return this.saveAuthenticationTraceStub(userId, isAuthenticationSuccessful);
|
return this.saveAuthenticationTraceStub(userId, isAuthenticationSuccessful);
|
||||||
}
|
}
|
||||||
|
|
||||||
retrieveLatestAuthenticationTraces(userId: string, isAuthenticationSuccessful: boolean, count: number): BluebirdPromise<AuthenticationTraceDocument[]> {
|
retrieveLatestAuthenticationTraces(userId: string, count: number): BluebirdPromise<AuthenticationTraceDocument[]> {
|
||||||
return this.retrieveLatestAuthenticationTracesStub(userId, isAuthenticationSuccessful, count);
|
return this.retrieveLatestAuthenticationTracesStub(userId, count);
|
||||||
}
|
}
|
||||||
|
|
||||||
produceIdentityValidationToken(userId: string, token: string, challenge: string, maxAge: number): BluebirdPromise<any> {
|
produceIdentityValidationToken(userId: string, token: string, challenge: string, maxAge: number): BluebirdPromise<any> {
|
||||||
|
|
|
@ -5,6 +5,7 @@ import speakeasy = require("speakeasy");
|
||||||
import request = require("request");
|
import request = require("request");
|
||||||
import nedb = require("nedb");
|
import nedb = require("nedb");
|
||||||
import { GlobalDependencies } from "../../../../src/types/Dependencies";
|
import { GlobalDependencies } from "../../../../src/types/Dependencies";
|
||||||
|
import { UserConfiguration } from "../../../../src/server/lib/configuration/Configuration";
|
||||||
import { TOTPSecret } from "../../../../src/types/TOTPSecret";
|
import { TOTPSecret } from "../../../../src/types/TOTPSecret";
|
||||||
import U2FMock = require("./../mocks/u2f");
|
import U2FMock = require("./../mocks/u2f");
|
||||||
import Endpoints = require("../../../../src/server/endpoints");
|
import Endpoints = require("../../../../src/server/endpoints");
|
||||||
|
@ -28,12 +29,11 @@ describe("Private pages of the server must not be accessible without session", f
|
||||||
let u2f: U2FMock.U2FMock;
|
let u2f: U2FMock.U2FMock;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
const config = {
|
const config: UserConfiguration = {
|
||||||
port: PORT,
|
port: PORT,
|
||||||
ldap: {
|
ldap: {
|
||||||
url: "ldap://127.0.0.1:389",
|
url: "ldap://127.0.0.1:389",
|
||||||
base_dn: "ou=users,dc=example,dc=com",
|
base_dn: "ou=users,dc=example,dc=com",
|
||||||
user_name_attribute: "cn",
|
|
||||||
user: "cn=admin,dc=example,dc=com",
|
user: "cn=admin,dc=example,dc=com",
|
||||||
password: "password",
|
password: "password",
|
||||||
},
|
},
|
||||||
|
@ -41,6 +41,11 @@ describe("Private pages of the server must not be accessible without session", f
|
||||||
secret: "session_secret",
|
secret: "session_secret",
|
||||||
expiration: 50000,
|
expiration: 50000,
|
||||||
},
|
},
|
||||||
|
regulation: {
|
||||||
|
max_retries: 3,
|
||||||
|
ban_time: 5 * 60,
|
||||||
|
find_time: 5 * 60
|
||||||
|
},
|
||||||
storage: {
|
storage: {
|
||||||
local: {
|
local: {
|
||||||
in_memory: true
|
in_memory: true
|
||||||
|
|
|
@ -5,6 +5,7 @@ import speakeasy = require("speakeasy");
|
||||||
import Request = require("request");
|
import Request = require("request");
|
||||||
import nedb = require("nedb");
|
import nedb = require("nedb");
|
||||||
import { GlobalDependencies } from "../../../../src/types/Dependencies";
|
import { GlobalDependencies } from "../../../../src/types/Dependencies";
|
||||||
|
import { UserConfiguration } from "../../../../src/server/lib/configuration/Configuration";
|
||||||
import { TOTPSecret } from "../../../../src/types/TOTPSecret";
|
import { TOTPSecret } from "../../../../src/types/TOTPSecret";
|
||||||
import U2FMock = require("./../mocks/u2f");
|
import U2FMock = require("./../mocks/u2f");
|
||||||
import Endpoints = require("../../../../src/server/endpoints");
|
import Endpoints = require("../../../../src/server/endpoints");
|
||||||
|
@ -28,12 +29,11 @@ describe("Public pages of the server must be accessible without session", functi
|
||||||
let u2f: U2FMock.U2FMock;
|
let u2f: U2FMock.U2FMock;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
const config = {
|
const config: UserConfiguration = {
|
||||||
port: PORT,
|
port: PORT,
|
||||||
ldap: {
|
ldap: {
|
||||||
url: "ldap://127.0.0.1:389",
|
url: "ldap://127.0.0.1:389",
|
||||||
base_dn: "ou=users,dc=example,dc=com",
|
base_dn: "ou=users,dc=example,dc=com",
|
||||||
user_name_attribute: "cn",
|
|
||||||
user: "cn=admin,dc=example,dc=com",
|
user: "cn=admin,dc=example,dc=com",
|
||||||
password: "password",
|
password: "password",
|
||||||
},
|
},
|
||||||
|
@ -46,6 +46,11 @@ describe("Public pages of the server must be accessible without session", functi
|
||||||
in_memory: true
|
in_memory: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
regulation: {
|
||||||
|
max_retries: 3,
|
||||||
|
ban_time: 5 * 60,
|
||||||
|
find_time: 5 * 60
|
||||||
|
},
|
||||||
notifier: {
|
notifier: {
|
||||||
gmail: {
|
gmail: {
|
||||||
username: "user@example.com",
|
username: "user@example.com",
|
||||||
|
|
|
@ -1,292 +0,0 @@
|
||||||
|
|
||||||
|
|
||||||
import Server from "../../../../src/server/lib/Server";
|
|
||||||
import { LdapjsClientMock } from "./../mocks/ldapjs";
|
|
||||||
|
|
||||||
import BluebirdPromise = require("bluebird");
|
|
||||||
import speakeasy = require("speakeasy");
|
|
||||||
import request = require("request");
|
|
||||||
import nedb = require("nedb");
|
|
||||||
import { GlobalDependencies } from "../../../../src/types/Dependencies";
|
|
||||||
import { TOTPSecret } from "../../../../src/types/TOTPSecret";
|
|
||||||
import U2FMock = require("./../mocks/u2f");
|
|
||||||
import Endpoints = require("../../../../src/server/endpoints");
|
|
||||||
import Requests = require("../requests");
|
|
||||||
import Assert = require("assert");
|
|
||||||
import Sinon = require("sinon");
|
|
||||||
import Winston = require("winston");
|
|
||||||
import MockDate = require("mockdate");
|
|
||||||
import ExpressSession = require("express-session");
|
|
||||||
import ldapjs = require("ldapjs");
|
|
||||||
|
|
||||||
const requestp = BluebirdPromise.promisifyAll(request) as typeof request;
|
|
||||||
|
|
||||||
const PORT = 8090;
|
|
||||||
const BASE_URL = "http://localhost:" + PORT;
|
|
||||||
const requests = Requests(PORT);
|
|
||||||
|
|
||||||
describe("test the server", function () {
|
|
||||||
let server: Server;
|
|
||||||
let transporter: any;
|
|
||||||
let u2f: U2FMock.U2FMock;
|
|
||||||
|
|
||||||
beforeEach(function () {
|
|
||||||
const config = {
|
|
||||||
port: PORT,
|
|
||||||
ldap: {
|
|
||||||
url: "ldap://127.0.0.1:389",
|
|
||||||
base_dn: "ou=users,dc=example,dc=com",
|
|
||||||
user_name_attribute: "cn",
|
|
||||||
user: "cn=admin,dc=example,dc=com",
|
|
||||||
password: "password",
|
|
||||||
},
|
|
||||||
session: {
|
|
||||||
secret: "session_secret",
|
|
||||||
expiration: 50000,
|
|
||||||
},
|
|
||||||
storage: {
|
|
||||||
local: {
|
|
||||||
in_memory: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
notifier: {
|
|
||||||
gmail: {
|
|
||||||
username: "user@example.com",
|
|
||||||
password: "password"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const ldapClient = LdapjsClientMock();
|
|
||||||
const ldap = {
|
|
||||||
Change: Sinon.spy(),
|
|
||||||
createClient: Sinon.spy(function () {
|
|
||||||
return ldapClient;
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
u2f = U2FMock.U2FMock();
|
|
||||||
|
|
||||||
transporter = {
|
|
||||||
sendMail: Sinon.stub().yields()
|
|
||||||
};
|
|
||||||
|
|
||||||
const nodemailer = {
|
|
||||||
createTransport: Sinon.spy(function () {
|
|
||||||
return transporter;
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
const ldapDocument = {
|
|
||||||
object: {
|
|
||||||
mail: "test_ok@example.com",
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const search_res = {
|
|
||||||
on: Sinon.spy(function (event: string, fn: (s: any) => void) {
|
|
||||||
if (event != "error") fn(ldapDocument);
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
ldapClient.bind.withArgs("cn=test_ok,ou=users,dc=example,dc=com",
|
|
||||||
"password").yields();
|
|
||||||
ldapClient.bind.withArgs("cn=admin,dc=example,dc=com",
|
|
||||||
"password").yields();
|
|
||||||
|
|
||||||
ldapClient.bind.withArgs("cn=test_nok,ou=users,dc=example,dc=com",
|
|
||||||
"password").yields("Bad credentials");
|
|
||||||
|
|
||||||
ldapClient.unbind.yields();
|
|
||||||
ldapClient.modify.yields();
|
|
||||||
ldapClient.search.yields(undefined, search_res);
|
|
||||||
|
|
||||||
const deps: GlobalDependencies = {
|
|
||||||
u2f: u2f,
|
|
||||||
nedb: nedb,
|
|
||||||
nodemailer: nodemailer,
|
|
||||||
ldapjs: ldap,
|
|
||||||
session: ExpressSession,
|
|
||||||
winston: Winston,
|
|
||||||
speakeasy: speakeasy,
|
|
||||||
ConnectRedis: Sinon.spy(),
|
|
||||||
dovehash: {
|
|
||||||
encode: Sinon.stub().returns("abc")
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
server = new Server();
|
|
||||||
return server.start(config, deps);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(function () {
|
|
||||||
server.stop();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("test authentication and verification", function () {
|
|
||||||
test_authentication();
|
|
||||||
test_reset_password();
|
|
||||||
test_regulation();
|
|
||||||
});
|
|
||||||
|
|
||||||
function test_authentication() {
|
|
||||||
it("should return status code 401 when user is not authenticated", function () {
|
|
||||||
return requestp.getAsync({ url: BASE_URL + Endpoints.VERIFY_GET })
|
|
||||||
.then(function (response: request.RequestResponse) {
|
|
||||||
Assert.equal(response.statusCode, 401);
|
|
||||||
return BluebirdPromise.resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return status code 204 when user is authenticated using totp", function () {
|
|
||||||
const j = requestp.jar();
|
|
||||||
return requests.login(j)
|
|
||||||
.then(function (res: request.RequestResponse) {
|
|
||||||
Assert.equal(res.statusCode, 200, "get login page failed");
|
|
||||||
return requests.first_factor(j);
|
|
||||||
})
|
|
||||||
.then(function (res: request.RequestResponse) {
|
|
||||||
Assert.equal(res.statusCode, 302, "first factor failed");
|
|
||||||
return requests.register_totp(j, transporter);
|
|
||||||
})
|
|
||||||
.then(function (base32_secret: string) {
|
|
||||||
const realToken = speakeasy.totp({
|
|
||||||
secret: base32_secret,
|
|
||||||
encoding: "base32"
|
|
||||||
});
|
|
||||||
return requests.totp(j, realToken);
|
|
||||||
})
|
|
||||||
.then(function (res: request.RequestResponse) {
|
|
||||||
Assert.equal(res.statusCode, 200, "second factor failed");
|
|
||||||
return requests.verify(j);
|
|
||||||
})
|
|
||||||
.then(function (res: request.RequestResponse) {
|
|
||||||
Assert.equal(res.statusCode, 204, "verify failed");
|
|
||||||
return BluebirdPromise.resolve();
|
|
||||||
})
|
|
||||||
.catch(function (err: Error) { return BluebirdPromise.reject(err); });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should keep session variables when login page is reloaded", function () {
|
|
||||||
const j = requestp.jar();
|
|
||||||
return requests.login(j)
|
|
||||||
.then(function (res: request.RequestResponse) {
|
|
||||||
Assert.equal(res.statusCode, 200, "get login page failed");
|
|
||||||
return requests.first_factor(j);
|
|
||||||
})
|
|
||||||
.then(function (res: request.RequestResponse) {
|
|
||||||
Assert.equal(res.statusCode, 302, "first factor failed");
|
|
||||||
return requests.register_totp(j, transporter);
|
|
||||||
})
|
|
||||||
.then(function (base32_secret: string) {
|
|
||||||
const realToken = speakeasy.totp({
|
|
||||||
secret: base32_secret,
|
|
||||||
encoding: "base32"
|
|
||||||
});
|
|
||||||
return requests.totp(j, realToken);
|
|
||||||
})
|
|
||||||
.then(function (res: request.RequestResponse) {
|
|
||||||
Assert.equal(res.statusCode, 200, "second factor failed");
|
|
||||||
return requests.login(j);
|
|
||||||
})
|
|
||||||
.then(function (res: request.RequestResponse) {
|
|
||||||
Assert.equal(res.statusCode, 200, "login page loading failed");
|
|
||||||
return requests.verify(j);
|
|
||||||
})
|
|
||||||
.then(function (res: request.RequestResponse) {
|
|
||||||
Assert.equal(res.statusCode, 204, "verify failed");
|
|
||||||
return BluebirdPromise.resolve();
|
|
||||||
})
|
|
||||||
.catch(function (err: Error) { return BluebirdPromise.reject(err); });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return status code 204 when user is authenticated using u2f", function () {
|
|
||||||
const sign_request = {};
|
|
||||||
const sign_status = {};
|
|
||||||
const registration_request = {};
|
|
||||||
const registration_status = {};
|
|
||||||
u2f.request.returns(BluebirdPromise.resolve(sign_request));
|
|
||||||
u2f.checkRegistration.returns(BluebirdPromise.resolve(sign_status));
|
|
||||||
u2f.checkSignature.returns(BluebirdPromise.resolve(registration_status));
|
|
||||||
|
|
||||||
const j = requestp.jar();
|
|
||||||
return requests.login(j)
|
|
||||||
.then(function (res: request.RequestResponse) {
|
|
||||||
Assert.equal(res.statusCode, 200, "get login page failed");
|
|
||||||
return requests.first_factor(j);
|
|
||||||
})
|
|
||||||
.then(function (res: request.RequestResponse) {
|
|
||||||
// console.log(res);
|
|
||||||
Assert.equal(res.headers.location, Endpoints.SECOND_FACTOR_GET);
|
|
||||||
Assert.equal(res.statusCode, 302, "first factor failed");
|
|
||||||
return requests.u2f_registration(j, transporter);
|
|
||||||
})
|
|
||||||
.then(function (res: request.RequestResponse) {
|
|
||||||
Assert.equal(res.statusCode, 200, "second factor, finish register failed");
|
|
||||||
return requests.u2f_authentication(j);
|
|
||||||
})
|
|
||||||
.then(function (res: request.RequestResponse) {
|
|
||||||
Assert.equal(res.statusCode, 200, "second factor, finish sign failed");
|
|
||||||
return requests.verify(j);
|
|
||||||
})
|
|
||||||
.then(function (res: request.RequestResponse) {
|
|
||||||
Assert.equal(res.statusCode, 204, "verify failed");
|
|
||||||
return BluebirdPromise.resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function test_reset_password() {
|
|
||||||
it("should reset the password", function () {
|
|
||||||
const j = requestp.jar();
|
|
||||||
return requests.login(j)
|
|
||||||
.then(function (res: request.RequestResponse) {
|
|
||||||
Assert.equal(res.statusCode, 200, "get login page failed");
|
|
||||||
return requests.first_factor(j);
|
|
||||||
})
|
|
||||||
.then(function (res: request.RequestResponse) {
|
|
||||||
Assert.equal(res.headers.location, Endpoints.SECOND_FACTOR_GET);
|
|
||||||
Assert.equal(res.statusCode, 302, "first factor failed");
|
|
||||||
return requests.reset_password(j, transporter, "user", "new-password");
|
|
||||||
})
|
|
||||||
.then(function (res: request.RequestResponse) {
|
|
||||||
Assert.equal(res.statusCode, 204, "second factor, finish register failed");
|
|
||||||
return BluebirdPromise.resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function test_regulation() {
|
|
||||||
it("should regulate authentication", function () {
|
|
||||||
const j = requestp.jar();
|
|
||||||
MockDate.set("1/2/2017 00:00:00");
|
|
||||||
return requests.login(j)
|
|
||||||
.then(function (res: request.RequestResponse) {
|
|
||||||
Assert.equal(res.statusCode, 200, "get login page failed");
|
|
||||||
return requests.failing_first_factor(j);
|
|
||||||
})
|
|
||||||
.then(function (res: request.RequestResponse) {
|
|
||||||
Assert.equal(res.statusCode, 401, "first factor failed");
|
|
||||||
return requests.failing_first_factor(j);
|
|
||||||
})
|
|
||||||
.then(function (res: request.RequestResponse) {
|
|
||||||
Assert.equal(res.statusCode, 401, "first factor failed");
|
|
||||||
return requests.failing_first_factor(j);
|
|
||||||
})
|
|
||||||
.then(function (res: request.RequestResponse) {
|
|
||||||
Assert.equal(res.statusCode, 401, "first factor failed");
|
|
||||||
return requests.failing_first_factor(j);
|
|
||||||
})
|
|
||||||
.then(function (res: request.RequestResponse) {
|
|
||||||
Assert.equal(res.statusCode, 403, "first factor failed");
|
|
||||||
MockDate.set("1/2/2017 00:30:00");
|
|
||||||
return requests.failing_first_factor(j);
|
|
||||||
})
|
|
||||||
.then(function (res: request.RequestResponse) {
|
|
||||||
Assert.equal(res.statusCode, 401, "first factor failed");
|
|
||||||
return BluebirdPromise.resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
|
@ -141,29 +141,24 @@ describe("test user data store", function () {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function should_retrieve_latest_authentication_traces(count: number, status: boolean) {
|
function should_retrieve_latest_authentication_traces(count: number) {
|
||||||
factory.buildStub.returns(collection);
|
factory.buildStub.returns(collection);
|
||||||
collection.findStub.withArgs().returns(BluebirdPromise.resolve());
|
collection.findStub.withArgs().returns(BluebirdPromise.resolve());
|
||||||
|
|
||||||
const dataStore = new UserDataStore(factory);
|
const dataStore = new UserDataStore(factory);
|
||||||
|
|
||||||
return dataStore.retrieveLatestAuthenticationTraces(userId, status, count)
|
return dataStore.retrieveLatestAuthenticationTraces(userId, count)
|
||||||
.then(function (doc: AuthenticationTraceDocument[]) {
|
.then(function (doc: AuthenticationTraceDocument[]) {
|
||||||
Assert(collection.findStub.calledOnce);
|
Assert(collection.findStub.calledOnce);
|
||||||
Assert(collection.findStub.calledWith({
|
Assert(collection.findStub.calledWith({
|
||||||
userId: userId,
|
userId: userId,
|
||||||
isAuthenticationSuccessful: status,
|
|
||||||
}, { date: -1 }, count));
|
}, { date: -1 }, count));
|
||||||
return BluebirdPromise.resolve();
|
return BluebirdPromise.resolve();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
it("should retrieve 3 latest failed authentication traces", function () {
|
it("should retrieve 3 latest failed authentication traces", function () {
|
||||||
should_retrieve_latest_authentication_traces(3, false);
|
should_retrieve_latest_authentication_traces(3);
|
||||||
});
|
|
||||||
|
|
||||||
it("should retrieve 4 latest successful authentication traces", function () {
|
|
||||||
should_retrieve_latest_authentication_traces(4, true);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue