From 0414d28e2b09215c98f5c0757461ee315c182104 Mon Sep 17 00:00:00 2001 From: Clement Michaud Date: Thu, 15 Jun 2017 00:22:16 +0200 Subject: [PATCH 1/3] Fix LDAP binding non working on servers with restricted ACL rules and add unit tests --- docker-compose.dev.yml | 7 - docker-compose.yml | 40 +++--- example/ldap/access.rules | 4 + src/server/lib/ErrorReplies.ts | 5 +- src/server/lib/LdapClient.ts | 131 ++++++++++-------- src/server/lib/routes/firstfactor/post.ts | 17 ++- .../lib/routes/password-reset/form/post.ts | 2 +- .../identity/PasswordResetHandler.ts | 2 +- src/types/ldapjs-async.d.ts | 1 + ...stence.test.ts => DataPersistence.test.ts} | 18 ++- test/server/LdapClient.test.ts | 119 ++++++++-------- test/server/Server.test.ts | 57 ++++---- ...er_config.test.ts => ServerConfig.test.ts} | 7 +- test/server/mocks/LdapClient.ts | 20 +-- test/server/mocks/ldapjs.ts | 2 + test/server/routes/firstfactor/post.test.ts | 73 +++++----- .../identity/PasswordResetHandler.test.ts | 8 +- .../server/routes/password-reset/post.test.ts | 14 +- 18 files changed, 283 insertions(+), 244 deletions(-) create mode 100644 example/ldap/access.rules rename test/server/{data_persistence.test.ts => DataPersistence.test.ts} (91%) rename test/server/{server_config.test.ts => ServerConfig.test.ts} (93%) diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 79b5208b..765f5014 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -8,10 +8,3 @@ services: - ./node_modules:/usr/src/node_modules - ./config.yml:/etc/auth-server/config.yml:ro - ldap-admin: - image: osixia/phpldapadmin:0.6.11 - ports: - - 9090:80 - environment: - - PHPLDAPADMIN_LDAP_HOSTS=ldap - - PHPLDAPADMIN_HTTPS=false diff --git a/docker-compose.yml b/docker-compose.yml index bfaaeb33..7245e3d6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,29 +1,12 @@ - version: '2' services: auth: build: . - depends_on: - - ldap restart: always volumes: - ./config.template.yml:/etc/auth-server/config.yml:ro - ./notifications:/var/lib/auth-server/notifications - ldap: - image: dinkel/openldap - environment: - - SLAPD_ORGANISATION=MyCompany - - SLAPD_DOMAIN=example.com - - SLAPD_PASSWORD=password - - SLAPD_ADDITIONAL_MODULES=memberof - - SLAPD_ADDITIONAL_SCHEMAS=openldap - - SLAPD_FORCE_RECONFIGURE=true - expose: - - "389" - volumes: - - ./example/ldap:/etc/ldap.dist/prepopulate - nginx: image: nginx:alpine volumes: @@ -35,3 +18,26 @@ services: - auth ports: - "8080:443" + + openldap: + image: clems4ever/openldap + ports: + - "389:389" + environment: + - SLAPD_ORGANISATION=MyCompany + - SLAPD_DOMAIN=example.com + - SLAPD_PASSWORD=password + - SLAPD_CONFIG_PASSWORD=password + - SLAPD_ADDITIONAL_MODULES=memberof + - SLAPD_ADDITIONAL_SCHEMAS=openldap + - SLAPD_FORCE_RECONFIGURE=true + volumes: + - ./example/ldap:/etc/ldap.dist/prepopulate + + openldap-admin: + image: osixia/phpldapadmin:0.6.11 + ports: + - 9090:80 + environment: + - PHPLDAPADMIN_LDAP_HOSTS=openldap + - PHPLDAPADMIN_HTTPS=false diff --git a/example/ldap/access.rules b/example/ldap/access.rules new file mode 100644 index 00000000..125b4ce9 --- /dev/null +++ b/example/ldap/access.rules @@ -0,0 +1,4 @@ +olcAccess: {0}to attrs=userPassword,shadowLastChange by self write by anonymou + s auth by * none +# olcAccess: {1}to dn.base="" by * read +# olcAccess: {2}to * by * read diff --git a/src/server/lib/ErrorReplies.ts b/src/server/lib/ErrorReplies.ts index 98e0b575..24a40031 100644 --- a/src/server/lib/ErrorReplies.ts +++ b/src/server/lib/ErrorReplies.ts @@ -1,8 +1,9 @@ import express = require("express"); import { Winston } from "winston"; +import BluebirdPromise = require("bluebird"); -function replyWithError(res: express.Response, code: number, logger: Winston) { - return function (err: Error) { +function replyWithError(res: express.Response, code: number, logger: Winston): (err: Error) => void { + return function (err: Error): void { logger.error("Reply with error %d: %s", code, err); res.status(code); res.send(); diff --git a/src/server/lib/LdapClient.ts b/src/server/lib/LdapClient.ts index 5ef54541..bf16a6d1 100644 --- a/src/server/lib/LdapClient.ts +++ b/src/server/lib/LdapClient.ts @@ -18,7 +18,7 @@ export class LdapClient { private options: LdapConfiguration; private ldapjs: Ldapjs; private logger: Winston; - private client: ldapjs.ClientAsync; + private adminClient: ldapjs.ClientAsync; constructor(options: LdapConfiguration, ldapjs: Ldapjs, logger: Winston) { this.options = options; @@ -28,100 +28,122 @@ export class LdapClient { this.connect(); } - connect(): void { - const ldap_client = this.ldapjs.createClient({ + private createClient(): ldapjs.ClientAsync { + const ldapClient = this.ldapjs.createClient({ url: this.options.url, reconnect: true }); - ldap_client.on("error", function (err: Error) { + ldapClient.on("error", function (err: Error) { console.error("LDAP Error:", err.message); }); - this.client = BluebirdPromise.promisifyAll(ldap_client) as ldapjs.ClientAsync; + return BluebirdPromise.promisifyAll(ldapClient) as ldapjs.ClientAsync; } - private build_user_dn(username: string): string { - let user_name_attr = this.options.user_name_attribute; - // if not provided, default to cn - if (!user_name_attr) user_name_attr = "cn"; + connect(): BluebirdPromise { + const userDN = this.options.user; + const password = this.options.password; - const additional_user_dn = this.options.additional_user_dn; + this.adminClient = this.createClient(); + return this.adminClient.bindAsync(userDN, password); + } + + private buildUserDN(username: string): string { + let userNameAttribute = this.options.user_name_attribute; + // if not provided, default to cn + if (!userNameAttribute) userNameAttribute = "cn"; + + const additionalUserDN = this.options.additional_user_dn; const base_dn = this.options.base_dn; - let user_dn = util.format("%s=%s", user_name_attr, username); - if (additional_user_dn) user_dn += util.format(",%s", additional_user_dn); - user_dn += util.format(",%s", base_dn); - return user_dn; + let userDN = util.format("%s=%s", userNameAttribute, username); + if (additionalUserDN) userDN += util.format(",%s", additionalUserDN); + userDN += util.format(",%s", base_dn); + return userDN; } - bind(username: string, password: string): BluebirdPromise { - const user_dn = this.build_user_dn(username); + checkPassword(username: string, password: string): BluebirdPromise { + const userDN = this.buildUserDN(username); + const that = this; + const ldapClient = this.createClient(); - this.logger.debug("LDAP: Bind user %s", user_dn); - return this.client.bindAsync(user_dn, password) + this.logger.debug("LDAP: Check password for user '%s'", userDN); + return ldapClient.bindAsync(userDN, password) + .then(function () { + return ldapClient.unbindAsync(); + }) .error(function (err: Error) { - throw new exceptions.LdapBindError(err.message); + return BluebirdPromise.reject(new exceptions.LdapBindError(err.message)); }); } - private search_in_ldap(base: string, query: ldapjs.SearchOptions): BluebirdPromise { - this.logger.debug("LDAP: Search for %s in %s", JSON.stringify(query), base); - return new BluebirdPromise((resolve, reject) => { - this.client.searchAsync(base, query) - .then(function (res: EventEmitter) { - const doc: SearchEntry[] = []; + private search(base: string, query: ldapjs.SearchOptions): BluebirdPromise { + const that = this; + + that.logger.debug("LDAP: Search for '%s' in '%s'", JSON.stringify(query), base); + return that.adminClient.searchAsync(base, query) + .then(function (res: EventEmitter) { + const doc: SearchEntry[] = []; + + return new BluebirdPromise((resolve, reject) => { res.on("searchEntry", function (entry: SearchEntry) { + that.logger.debug("Entry retrieved from LDAP is '%s'", JSON.stringify(entry.object)); doc.push(entry.object); }); res.on("error", function (err: Error) { + that.logger.error("LDAP: Error received during search '%s'.", JSON.stringify(err)); reject(new exceptions.LdapSearchError(err.message)); }); res.on("end", function () { + that.logger.debug("LDAP: Result of search is '%s'.", JSON.stringify(doc)); resolve(doc); }); - }) - .catch(function (err: Error) { - reject(new exceptions.LdapSearchError(err.message)); }); - }); + }) + .catch(function (err: Error) { + return BluebirdPromise.reject(new exceptions.LdapSearchError(err.message)); + }); } - get_groups(username: string): BluebirdPromise { - const user_dn = this.build_user_dn(username); + retrieveGroups(username: string): BluebirdPromise { + const userDN = this.buildUserDN(username); + const password = this.options.password; - let group_name_attr = this.options.group_name_attribute; - if (!group_name_attr) group_name_attr = "cn"; + let groupNameAttribute = this.options.group_name_attribute; + if (!groupNameAttribute) groupNameAttribute = "cn"; - const additional_group_dn = this.options.additional_group_dn; + const additionalGroupDN = this.options.additional_group_dn; const base_dn = this.options.base_dn; - let group_dn = base_dn; - if (additional_group_dn) - group_dn = util.format("%s,", additional_group_dn) + group_dn; + let groupDN = base_dn; + if (additionalGroupDN) + groupDN = util.format("%s,", additionalGroupDN) + groupDN; const query = { scope: "sub", - attributes: [group_name_attr], - filter: "member=" + user_dn + attributes: [groupNameAttribute], + filter: "member=" + userDN }; const that = this; this.logger.debug("LDAP: get groups of user %s", username); - return this.search_in_ldap(group_dn, query) + const groups: string[] = []; + return that.search(groupDN, query) .then(function (docs) { - const groups = []; for (let i = 0; i < docs.length; ++i) { groups.push(docs[i].cn); } - that.logger.debug("LDAP: got groups %s", groups); + that.logger.debug("LDAP: got groups '%s'", groups); + }) + .then(function () { return BluebirdPromise.resolve(groups); }); } - get_emails(username: string): BluebirdPromise { + retrieveEmails(username: string): BluebirdPromise { const that = this; - const user_dn = this.build_user_dn(username); + const user_dn = this.buildUserDN(username); const query = { scope: "base", @@ -129,8 +151,8 @@ export class LdapClient { attributes: ["mail"] }; - this.logger.debug("LDAP: get emails of user %s", username); - return this.search_in_ldap(user_dn, query) + this.logger.debug("LDAP: get emails of user '%s'", username); + return this.search(user_dn, query) .then(function (docs) { const emails = []; for (let i = 0; i < docs.length; ++i) { @@ -140,15 +162,15 @@ export class LdapClient { emails.concat(docs[i].mail); } } - that.logger.debug("LDAP: got emails %s", emails); + that.logger.debug("LDAP: got emails '%s'", emails); return BluebirdPromise.resolve(emails); }); } - update_password(username: string, new_password: string): BluebirdPromise { - const user_dn = this.build_user_dn(username); + updatePassword(username: string, newPassword: string): BluebirdPromise { + const user_dn = this.buildUserDN(username); - const encoded_password = Dovehash.encode("SSHA", new_password); + const encoded_password = Dovehash.encode("SSHA", newPassword); const change = { operation: "replace", modification: { @@ -157,13 +179,12 @@ export class LdapClient { }; const that = this; - this.logger.debug("LDAP: update password of user %s", username); + this.logger.debug("LDAP: update password of user '%s'", username); - this.logger.debug("LDAP: bind admin"); - return this.client.bindAsync(this.options.user, this.options.password) + that.logger.debug("LDAP: modify password"); + return that.adminClient.modifyAsync(user_dn, change) .then(function () { - that.logger.debug("LDAP: modify password"); - return that.client.modifyAsync(user_dn, change); + return that.adminClient.unbindAsync(); }); } } diff --git a/src/server/lib/routes/firstfactor/post.ts b/src/server/lib/routes/firstfactor/post.ts index 033f40dd..aef3ff0f 100644 --- a/src/server/lib/routes/firstfactor/post.ts +++ b/src/server/lib/routes/firstfactor/post.ts @@ -35,21 +35,28 @@ export default function (req: express.Request, res: express.Response): BluebirdP return regulator.regulate(username) .then(function () { - return ldap.bind(username, password); + logger.info("1st factor: No regulation applied."); + return ldap.checkPassword(username, password); }) .then(function () { + logger.info("1st factor: LDAP binding successful"); authSession.userid = username; authSession.first_factor = true; - logger.info("1st factor: LDAP binding successful"); logger.debug("1st factor: Retrieve email from LDAP"); - return BluebirdPromise.join(ldap.get_emails(username), ldap.get_groups(username)); + return BluebirdPromise.join(ldap.retrieveEmails(username), ldap.retrieveGroups(username)); }) .then(function (data: [string[], string[]]) { const emails: string[] = data[0]; const groups: string[] = data[1]; - if (!emails && emails.length <= 0) throw new Error("No email found"); + if (!emails || emails.length <= 0) { + const errMessage = "No emails found. The user should have at least one email address to reset password."; + logger.error("1s factor: %s", errMessage); + return BluebirdPromise.reject(new Error(errMessage)); + } + logger.debug("1st factor: Retrieved email are %s", emails); + logger.debug("1st factor: Retrieved groups are %s", groups); authSession.email = emails[0]; authSession.groups = groups; @@ -61,7 +68,7 @@ export default function (req: express.Request, res: express.Response): BluebirdP .catch(exceptions.LdapSearchError, ErrorReplies.replyWithError500(res, logger)) .catch(exceptions.LdapBindError, function (err: Error) { regulator.mark(username, false); - ErrorReplies.replyWithError401(res, logger)(err); + return ErrorReplies.replyWithError401(res, logger)(err); }) .catch(exceptions.AuthenticationRegulationError, ErrorReplies.replyWithError403(res, logger)) .catch(exceptions.DomainAccessDenied, ErrorReplies.replyWithError401(res, logger)) diff --git a/src/server/lib/routes/password-reset/form/post.ts b/src/server/lib/routes/password-reset/form/post.ts index 9160b68c..5a2cd45d 100644 --- a/src/server/lib/routes/password-reset/form/post.ts +++ b/src/server/lib/routes/password-reset/form/post.ts @@ -26,7 +26,7 @@ export default function (req: express.Request, res: express.Response): BluebirdP logger.info("POST reset-password: User %s wants to reset his/her password", userid); - return ldap.update_password(userid, new_password) + return ldap.updatePassword(userid, new_password) .then(function () { logger.info("POST reset-password: Password reset for user '%s'", userid); AuthenticationSession.reset(req); diff --git a/src/server/lib/routes/password-reset/identity/PasswordResetHandler.ts b/src/server/lib/routes/password-reset/identity/PasswordResetHandler.ts index e41a1e0d..898d5532 100644 --- a/src/server/lib/routes/password-reset/identity/PasswordResetHandler.ts +++ b/src/server/lib/routes/password-reset/identity/PasswordResetHandler.ts @@ -26,7 +26,7 @@ export default class PasswordResetHandler implements IdentityValidable { return BluebirdPromise.reject(new exceptions.AccessDeniedError("No user id provided")); const ldap = ServerVariables.getLdapClient(req.app); - return ldap.get_emails(userid) + return ldap.retrieveEmails(userid) .then(function (emails: string[]) { if (!emails && emails.length <= 0) throw new Error("No email found"); diff --git a/src/types/ldapjs-async.d.ts b/src/types/ldapjs-async.d.ts index e5fad359..669c15e6 100644 --- a/src/types/ldapjs-async.d.ts +++ b/src/types/ldapjs-async.d.ts @@ -5,6 +5,7 @@ import { EventEmitter } from "events"; declare module "ldapjs" { export interface ClientAsync { bindAsync(username: string, password: string): BluebirdPromise; + unbindAsync(): BluebirdPromise; searchAsync(base: string, query: ldapjs.SearchOptions): BluebirdPromise; modifyAsync(userdn: string, change: ldapjs.Change): BluebirdPromise; } diff --git a/test/server/data_persistence.test.ts b/test/server/DataPersistence.test.ts similarity index 91% rename from test/server/data_persistence.test.ts rename to test/server/DataPersistence.test.ts index 1beac313..e6c33f7d 100644 --- a/test/server/data_persistence.test.ts +++ b/test/server/DataPersistence.test.ts @@ -7,6 +7,7 @@ import { UserConfiguration } from "../../src/types/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; @@ -23,14 +24,10 @@ const requests = require("./requests")(PORT); describe("test data persistence", function () { let u2f: U2FMock.U2FMock; let tmpDir: tmp.SynchrounousResult; - const ldap_client = { - bind: sinon.stub(), - search: sinon.stub(), - on: sinon.spy() - }; + const ldapClient = LdapjsClientMock(); const ldap = { createClient: sinon.spy(function () { - return ldap_client; + return ldapClient; }) }; @@ -51,11 +48,12 @@ describe("test data persistence", function () { }) }; - ldap_client.bind.withArgs("cn=test_ok,ou=users,dc=example,dc=com", - "password").yields(undefined); - ldap_client.bind.withArgs("cn=test_nok,ou=users,dc=example,dc=com", + 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"); - ldap_client.search.yields(undefined, search_res); + ldapClient.search.yields(undefined, search_res); + ldapClient.unbind.yields(); tmpDir = tmp.dirSync({ unsafeCleanup: true }); config = { diff --git a/test/server/LdapClient.test.ts b/test/server/LdapClient.test.ts index 994c7943..8d1abe94 100644 --- a/test/server/LdapClient.test.ts +++ b/test/server/LdapClient.test.ts @@ -14,22 +14,16 @@ import { LdapjsMock, LdapjsClientMock } from "./mocks/ldapjs"; describe("test ldap validation", function () { let ldap: LdapClient.LdapClient; - let ldap_client: LdapjsClientMock; + let ldapClient: LdapjsClientMock; let ldapjs: LdapjsMock; - let ldap_config: LdapConfiguration; + let ldapConfig: LdapConfiguration; beforeEach(function () { - ldap_client = { - bind: sinon.stub(), - search: sinon.stub(), - modify: sinon.stub(), - on: sinon.stub() - } as any; - + ldapClient = LdapjsClientMock(); ldapjs = LdapjsMock(); - ldapjs.createClient.returns(ldap_client); + ldapjs.createClient.returns(ldapClient); - ldap_config = { + ldapConfig = { url: "http://localhost:324", user: "admin", password: "password", @@ -37,45 +31,47 @@ describe("test ldap validation", function () { additional_user_dn: "ou=users" }; - ldap = new LdapClient.LdapClient(ldap_config, ldapjs, winston); - return ldap.connect(); + ldap = new LdapClient.LdapClient(ldapConfig, ldapjs, winston); }); - describe("test binding", test_binding); + describe("test checking password", test_checking_password); describe("test get emails from username", test_get_emails); describe("test get groups from username", test_get_groups); describe("test update password", test_update_password); - function test_binding() { - function test_bind() { + function test_checking_password() { + function test_check_password_internal() { const username = "username"; const password = "password"; - return ldap.bind(username, password); + return ldap.checkPassword(username, password); } it("should bind the user if good credentials provided", function () { - ldap_client.bind.yields(); - return test_bind(); + ldapClient.bind.yields(); + ldapClient.unbind.yields(); + return test_check_password_internal(); }); it("should bind the user with correct DN", function () { - ldap_config.user_name_attribute = "uid"; + ldapConfig.user_name_attribute = "uid"; const username = "user"; const password = "password"; - ldap_client.bind.withArgs("uid=user,ou=users,dc=example,dc=com").yields(); - return ldap.bind(username, password); + ldapClient.bind.withArgs("uid=user,ou=users,dc=example,dc=com").yields(); + ldapClient.unbind.yields(); + return ldap.checkPassword(username, password); }); it("should default to cn user search filter if no filter provided", function () { const username = "user"; const password = "password"; - ldap_client.bind.withArgs("cn=user,ou=users,dc=example,dc=com").yields(); - return ldap.bind(username, password); + ldapClient.bind.withArgs("cn=user,ou=users,dc=example,dc=com").yields(); + ldapClient.unbind.yields(); + return ldap.checkPassword(username, password); }); it("should not bind the user if wrong credentials provided", function () { - ldap_client.bind.yields("wrong credentials"); - const promise = test_bind(); + ldapClient.bind.yields("wrong credentials"); + const promise = test_check_password_internal(); return promise.catch(function () { return BluebirdPromise.resolve(); }); @@ -101,9 +97,9 @@ describe("test ldap validation", function () { }); it("should retrieve the email of an existing user", function () { - ldap_client.search.yields(undefined, res_emitter); + ldapClient.search.yields(undefined, res_emitter); - return ldap.get_emails("user") + return ldap.retrieveEmails("user") .then(function (emails) { assert.deepEqual(emails, [expected_doc.object.mail]); return BluebirdPromise.resolve(); @@ -111,9 +107,9 @@ describe("test ldap validation", function () { }); it("should retrieve email for user with uid name attribute", function () { - ldap_config.user_name_attribute = "uid"; - ldap_client.search.withArgs("uid=username,ou=users,dc=example,dc=com").yields(undefined, res_emitter); - return ldap.get_emails("username") + ldapConfig.user_name_attribute = "uid"; + ldapClient.search.withArgs("uid=username,ou=users,dc=example,dc=com").yields(undefined, res_emitter); + return ldap.retrieveEmails("username") .then(function (emails) { assert.deepEqual(emails, ["user@example.com"]); return BluebirdPromise.resolve(); @@ -124,9 +120,9 @@ describe("test ldap validation", function () { const expected_doc = { mail: ["user@example.com"] }; - ldap_client.search.yields("Error while searching mails"); + ldapClient.search.yields("Error while searching mails"); - return ldap.get_emails("user") + return ldap.retrieveEmails("user") .catch(function () { return BluebirdPromise.resolve(); }); @@ -159,8 +155,8 @@ describe("test ldap validation", function () { }); it("should retrieve the groups of an existing user", function () { - ldap_client.search.yields(undefined, res_emitter); - return ldap.get_groups("user") + ldapClient.search.yields(undefined, res_emitter); + return ldap.retrieveGroups("user") .then(function (groups) { assert.deepEqual(groups, ["group1", "group2"]); return BluebirdPromise.resolve(); @@ -168,29 +164,29 @@ describe("test ldap validation", function () { }); it("should reduce the scope to additional_group_dn", function (done) { - ldap_config.additional_group_dn = "ou=groups"; - ldap_client.search.yields(undefined, res_emitter); - ldap.get_groups("user") + ldapConfig.additional_group_dn = "ou=groups"; + ldapClient.search.yields(undefined, res_emitter); + ldap.retrieveGroups("user") .then(function() { - assert.equal(ldap_client.search.getCall(0).args[0], "ou=groups,dc=example,dc=com"); + assert.equal(ldapClient.search.getCall(0).args[0], "ou=groups,dc=example,dc=com"); done(); }); }); it("should use default group_name_attr if not provided", function (done) { - ldap_client.search.yields(undefined, res_emitter); - ldap.get_groups("user") + ldapClient.search.yields(undefined, res_emitter); + ldap.retrieveGroups("user") .then(function() { - assert.equal(ldap_client.search.getCall(0).args[0], "dc=example,dc=com"); - assert.equal(ldap_client.search.getCall(0).args[1].filter, "member=cn=user,ou=users,dc=example,dc=com"); - assert.deepEqual(ldap_client.search.getCall(0).args[1].attributes, ["cn"]); + assert.equal(ldapClient.search.getCall(0).args[0], "dc=example,dc=com"); + assert.equal(ldapClient.search.getCall(0).args[1].filter, "member=cn=user,ou=users,dc=example,dc=com"); + assert.deepEqual(ldapClient.search.getCall(0).args[1].attributes, ["cn"]); done(); }); }); it("should fail on error with search method", function () { - ldap_client.search.yields("error"); - return ldap.get_groups("user") + ldapClient.search.yields("error"); + return ldap.retrieveGroups("user") .catch(function () { return BluebirdPromise.resolve(); }); @@ -207,36 +203,39 @@ describe("test ldap validation", function () { }; const userdn = "cn=user,ou=users,dc=example,dc=com"; - ldap_client.bind.yields(undefined); - ldap_client.modify.yields(undefined); + ldapClient.bind.yields(); + ldapClient.unbind.yields(); + ldapClient.modify.yields(); - return ldap.update_password("user", "new-password") + return ldap.updatePassword("user", "new-password") .then(function () { - assert.deepEqual(ldap_client.modify.getCall(0).args[0], userdn); - assert.deepEqual(ldap_client.modify.getCall(0).args[1].operation, change.operation); + assert.deepEqual(ldapClient.modify.getCall(0).args[0], userdn); + assert.deepEqual(ldapClient.modify.getCall(0).args[1].operation, change.operation); - const userPassword = ldap_client.modify.getCall(0).args[1].modification.userPassword; + const userPassword = ldapClient.modify.getCall(0).args[1].modification.userPassword; assert(/{SSHA}/.test(userPassword)); return BluebirdPromise.resolve(); - }); + }) + .catch(function(err) { return BluebirdPromise.reject(new Error("It should fail")); }); }); it("should fail when ldap throws an error", function () { - ldap_client.bind.yields(undefined); - ldap_client.modify.yields("Error"); + ldapClient.bind.yields(undefined); + ldapClient.modify.yields("Error"); - return ldap.update_password("user", "new-password") + return ldap.updatePassword("user", "new-password") .catch(function () { return BluebirdPromise.resolve(); }); }); it("should update password of user using particular user name attribute", function () { - ldap_config.user_name_attribute = "uid"; + ldapConfig.user_name_attribute = "uid"; - ldap_client.bind.yields(undefined); - ldap_client.modify.withArgs("uid=username,ou=users,dc=example,dc=com").yields(); - return ldap.update_password("username", "newpass"); + ldapClient.bind.yields(); + ldapClient.unbind.yields(); + ldapClient.modify.withArgs("uid=username,ou=users,dc=example,dc=com").yields(); + return ldap.updatePassword("username", "newpass"); }); } }); diff --git a/test/server/Server.test.ts b/test/server/Server.test.ts index 0fa64f03..5b4673fe 100644 --- a/test/server/Server.test.ts +++ b/test/server/Server.test.ts @@ -1,6 +1,7 @@ import Server from "../../src/server/lib/Server"; import LdapClient = require("../../src/server/lib/LdapClient"); +import { LdapjsClientMock } from "./mocks/ldapjs"; import BluebirdPromise = require("bluebird"); import speakeasy = require("speakeasy"); @@ -51,16 +52,11 @@ describe("test the server", function () { } }; - const ldap_client = { - bind: sinon.stub(), - search: sinon.stub(), - modify: sinon.stub(), - on: sinon.spy() - }; + const ldapClient = LdapjsClientMock(); const ldap = { Change: sinon.spy(), createClient: sinon.spy(function () { - return ldap_client; + return ldapClient; }) }; @@ -76,7 +72,7 @@ describe("test the server", function () { }) }; - const ldap_document = { + const ldapDocument = { object: { mail: "test_ok@example.com", } @@ -84,20 +80,21 @@ describe("test the server", function () { const search_res = { on: sinon.spy(function (event: string, fn: (s: any) => void) { - if (event != "error") fn(ldap_document); + if (event != "error") fn(ldapDocument); }) }; - ldap_client.bind.withArgs("cn=test_ok,ou=users,dc=example,dc=com", - "password").yields(undefined); - ldap_client.bind.withArgs("cn=admin,dc=example,dc=com", - "password").yields(undefined); + 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(); - ldap_client.bind.withArgs("cn=test_nok,ou=users,dc=example,dc=com", + ldapClient.bind.withArgs("cn=test_nok,ou=users,dc=example,dc=com", "password").yields("error"); - ldap_client.modify.yields(undefined); - ldap_client.search.yields(undefined, search_res); + ldapClient.unbind.yields(); + ldapClient.modify.yields(); + ldapClient.search.yields(undefined, search_res); const deps = { u2f: u2f, @@ -241,11 +238,11 @@ describe("test the server", function () { return requests.register_totp(j, transporter); }) .then(function (base32_secret: string) { - const real_token = speakeasy.totp({ + const realToken = speakeasy.totp({ secret: base32_secret, encoding: "base32" }); - return requests.totp(j, real_token); + return requests.totp(j, realToken); }) .then(function (res: request.RequestResponse) { assert.equal(res.statusCode, 200, "second factor failed"); @@ -254,14 +251,11 @@ describe("test the server", function () { .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 real_token = speakeasy.totp({ - secret: "totp_secret", - encoding: "base32" - }); const j = requestp.jar(); return requests.login(j) .then(function (res: request.RequestResponse) { @@ -269,11 +263,18 @@ describe("test the server", function () { return requests.first_factor(j); }) .then(function (res: request.RequestResponse) { - assert.equal(res.statusCode, 204, "first factor failed"); - return requests.totp(j, real_token); + 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, 204, "second factor failed"); + assert.equal(res.statusCode, 200, "second factor failed"); return requests.login(j); }) .then(function (res: request.RequestResponse) { @@ -284,9 +285,7 @@ describe("test the server", function () { assert.equal(res.statusCode, 204, "verify failed"); return BluebirdPromise.resolve(); }) - .catch(function (err: Error) { - console.error(err); - }); + .catch(function (err: Error) { return BluebirdPromise.reject(err); }); }); it("should return status code 204 when user is authenticated using u2f", function () { diff --git a/test/server/server_config.test.ts b/test/server/ServerConfig.test.ts similarity index 93% rename from test/server/server_config.test.ts rename to test/server/ServerConfig.test.ts index 83570f02..4773539a 100644 --- a/test/server/server_config.test.ts +++ b/test/server/ServerConfig.test.ts @@ -1,6 +1,6 @@ import assert = require("assert"); -import sinon = require ("sinon"); +import sinon = require("sinon"); import nedb = require("nedb"); import express = require("express"); import winston = require("winston"); @@ -36,7 +36,10 @@ describe("test server configuration", function () { winston: winston, ldapjs: { createClient: sinon.spy(function () { - return { on: sinon.spy() }; + return { + on: sinon.spy(), + bind: sinon.spy() + }; }) }, session: sessionMock as any diff --git a/test/server/mocks/LdapClient.ts b/test/server/mocks/LdapClient.ts index 17495c69..416071cb 100644 --- a/test/server/mocks/LdapClient.ts +++ b/test/server/mocks/LdapClient.ts @@ -2,19 +2,19 @@ import sinon = require("sinon"); export interface LdapClientMock { - bind: sinon.SinonStub; - get_emails: sinon.SinonStub; - get_groups: sinon.SinonStub; - search_in_ldap: sinon.SinonStub; - update_password: sinon.SinonStub; + checkPassword: sinon.SinonStub; + retrieveEmails: sinon.SinonStub; + retrieveGroups: sinon.SinonStub; + search: sinon.SinonStub; + updatePassword: sinon.SinonStub; } export function LdapClientMock(): LdapClientMock { return { - bind: sinon.stub(), - get_emails: sinon.stub(), - get_groups: sinon.stub(), - search_in_ldap: sinon.stub(), - update_password: sinon.stub() + checkPassword: sinon.stub(), + retrieveEmails: sinon.stub(), + retrieveGroups: sinon.stub(), + search: sinon.stub(), + updatePassword: sinon.stub() }; } diff --git a/test/server/mocks/ldapjs.ts b/test/server/mocks/ldapjs.ts index 957f4a9e..dcd1bacd 100644 --- a/test/server/mocks/ldapjs.ts +++ b/test/server/mocks/ldapjs.ts @@ -7,6 +7,7 @@ export interface LdapjsMock { export interface LdapjsClientMock { bind: sinon.SinonStub; + unbind: sinon.SinonStub; search: sinon.SinonStub; modify: sinon.SinonStub; on: sinon.SinonStub; @@ -21,6 +22,7 @@ export function LdapjsMock(): LdapjsMock { export function LdapjsClientMock(): LdapjsClientMock { return { bind: sinon.stub(), + unbind: sinon.stub(), search: sinon.stub(), modify: sinon.stub(), on: sinon.stub() diff --git a/test/server/routes/firstfactor/post.test.ts b/test/server/routes/firstfactor/post.test.ts index e38bbec0..25baab59 100644 --- a/test/server/routes/firstfactor/post.test.ts +++ b/test/server/routes/firstfactor/post.test.ts @@ -72,8 +72,8 @@ describe("test the first factor validation route", function () { }); it("should redirect client to second factor page", function () { - ldapMock.bind.withArgs("username").returns(BluebirdPromise.resolve()); - ldapMock.get_emails.returns(BluebirdPromise.resolve(emails)); + ldapMock.checkPassword.withArgs("username").returns(BluebirdPromise.resolve()); + ldapMock.retrieveEmails.returns(BluebirdPromise.resolve(emails)); const authSession = AuthenticationSession.get(req as any); return FirstFactorPost.default(req as any, res as any) .then(function () { @@ -82,55 +82,60 @@ describe("test the first factor validation route", function () { }); }); - it("should retrieve email from LDAP", function (done) { - res.redirect = sinon.spy(function () { done(); }); - ldapMock.bind.returns(BluebirdPromise.resolve()); - ldapMock.get_emails = sinon.stub().withArgs("username").returns(BluebirdPromise.resolve([{ mail: ["test@example.com"] }])); - FirstFactorPost.default(req as any, res as any); + it("should retrieve email from LDAP", function () { + ldapMock.checkPassword.returns(BluebirdPromise.resolve()); + ldapMock.retrieveEmails = sinon.stub().withArgs("username").returns(BluebirdPromise.resolve([{ mail: ["test@example.com"] }])); + return FirstFactorPost.default(req as any, res as any); }); - it("should set email as session variables", function () { + it("should set first email address as user session variable", function () { const emails = ["test_ok@example.com"]; const authSession = AuthenticationSession.get(req as any); - ldapMock.bind.returns(BluebirdPromise.resolve()); - ldapMock.get_emails.returns(BluebirdPromise.resolve(emails)); + ldapMock.checkPassword.returns(BluebirdPromise.resolve()); + ldapMock.retrieveEmails.returns(BluebirdPromise.resolve(emails)); return FirstFactorPost.default(req as any, res as any) .then(function () { assert.equal("test_ok@example.com", authSession.email); }); }); - it("should return status code 401 when LDAP binding throws", function (done) { - res.send = sinon.spy(function () { - assert.equal(401, res.status.getCall(0).args[0]); - assert.equal(regulator.mark.getCall(0).args[0], "username"); - done(); - }); - ldapMock.bind.returns(BluebirdPromise.reject(new exceptions.LdapBindError("Bad credentials"))); - FirstFactorPost.default(req as any, res as any); + it("should return status code 401 when LDAP binding throws", function () { + ldapMock.checkPassword.returns(BluebirdPromise.reject(new exceptions.LdapBindError("Bad credentials"))); + return FirstFactorPost.default(req as any, res as any) + .then(function () { + assert.equal(401, res.status.getCall(0).args[0]); + assert.equal(regulator.mark.getCall(0).args[0], "username"); + }); }); - it("should return status code 500 when LDAP search throws", function (done) { - res.send = sinon.spy(function () { - assert.equal(500, res.status.getCall(0).args[0]); - done(); - }); - ldapMock.bind.returns(BluebirdPromise.resolve()); - ldapMock.get_emails.returns(BluebirdPromise.reject(new exceptions.LdapSearchError("error while retrieving emails"))); - FirstFactorPost.default(req as any, res as any); + it("should return status code 500 when LDAP search throws", function () { + ldapMock.checkPassword.returns(BluebirdPromise.resolve()); + ldapMock.retrieveEmails.returns(BluebirdPromise.reject(new exceptions.LdapSearchError("error while retrieving emails"))); + return FirstFactorPost.default(req as any, res as any) + .then(function () { + assert.equal(500, res.status.getCall(0).args[0]); + }); }); - it("should return status code 403 when regulator rejects authentication", function (done) { + it("should return status code 403 when regulator rejects authentication", function () { const err = new exceptions.AuthenticationRegulationError("Authentication regulation..."); regulator.regulate.returns(BluebirdPromise.reject(err)); + return FirstFactorPost.default(req as any, res as any) + .then(function () { + assert.equal(403, res.status.getCall(0).args[0]); + assert.equal(1, res.send.callCount); + }); + }); - res.send = sinon.spy(function () { - assert.equal(403, res.status.getCall(0).args[0]); - done(); - }); - ldapMock.bind.returns(BluebirdPromise.resolve()); - ldapMock.get_emails.returns(BluebirdPromise.resolve()); - FirstFactorPost.default(req as any, res as any); + it("should fail when admin user does not have rights to retrieve attribute mail", function () { + ldapMock.checkPassword.returns(BluebirdPromise.resolve()); + ldapMock.retrieveEmails = sinon.stub().withArgs("username").returns(BluebirdPromise.resolve([])); + ldapMock.retrieveGroups = sinon.stub().withArgs("username").returns(BluebirdPromise.resolve(["group1"])); + return FirstFactorPost.default(req as any, res as any) + .then(function () { + assert.equal(500, res.status.getCall(0).args[0]); + assert.equal(1, res.send.callCount); + }); }); }); diff --git a/test/server/routes/password-reset/identity/PasswordResetHandler.test.ts b/test/server/routes/password-reset/identity/PasswordResetHandler.test.ts index 3b90b893..f69edf5b 100644 --- a/test/server/routes/password-reset/identity/PasswordResetHandler.test.ts +++ b/test/server/routes/password-reset/identity/PasswordResetHandler.test.ts @@ -82,7 +82,7 @@ describe("test reset password identity check", function () { }); it("should fail if ldap fail", function (done) { - ldap_client.get_emails.returns(BluebirdPromise.reject("Internal error")); + ldap_client.retrieveEmails.returns(BluebirdPromise.reject("Internal error")); new PasswordResetHandler().preValidationInit(req as any) .catch(function (err: Error) { done(); @@ -91,16 +91,16 @@ describe("test reset password identity check", function () { it("should perform a search in ldap to find email address", function (done) { configuration.ldap.user_name_attribute = "uid"; - ldap_client.get_emails.returns(BluebirdPromise.resolve([])); + ldap_client.retrieveEmails.returns(BluebirdPromise.resolve([])); new PasswordResetHandler().preValidationInit(req as any) .then(function () { - assert.equal("user", ldap_client.get_emails.getCall(0).args[0]); + assert.equal("user", ldap_client.retrieveEmails.getCall(0).args[0]); done(); }); }); it("should returns identity when ldap replies", function (done) { - ldap_client.get_emails.returns(BluebirdPromise.resolve(["test@example.com"])); + ldap_client.retrieveEmails.returns(BluebirdPromise.resolve(["test@example.com"])); new PasswordResetHandler().preValidationInit(req as any) .then(function () { done(); diff --git a/test/server/routes/password-reset/post.test.ts b/test/server/routes/password-reset/post.test.ts index 9548998d..9362de8a 100644 --- a/test/server/routes/password-reset/post.test.ts +++ b/test/server/routes/password-reset/post.test.ts @@ -16,7 +16,7 @@ describe("test reset password route", function () { let req: ExpressMock.RequestMock; let res: ExpressMock.ResponseMock; let user_data_store: UserDataStore; - let ldap_client: LdapClientMock; + let ldapClient: LdapClientMock; let configuration: any; let authSession: AuthenticationSession.AuthenticationSession; @@ -64,8 +64,8 @@ describe("test reset password route", function () { mocks.logger = winston; mocks.config = configuration; - ldap_client = LdapClientMock(); - mocks.ldap = ldap_client; + ldapClient = LdapClientMock(); + mocks.ldap = ldapClient; res = ExpressMock.ResponseMock(); }); @@ -79,8 +79,8 @@ describe("test reset password route", function () { req.body = {}; req.body.password = "new-password"; - ldap_client.update_password.returns(BluebirdPromise.resolve()); - ldap_client.bind.returns(BluebirdPromise.resolve()); + ldapClient.updatePassword.returns(BluebirdPromise.resolve()); + ldapClient.checkPassword.returns(BluebirdPromise.resolve()); return PasswordResetFormPost.default(req as any, res as any) .then(function () { const authSession = AuthenticationSession.get(req as any); @@ -111,8 +111,8 @@ describe("test reset password route", function () { req.body = {}; req.body.password = "new-password"; - ldap_client.bind.yields(undefined); - ldap_client.update_password.returns(BluebirdPromise.reject("Internal error with LDAP")); + ldapClient.checkPassword.yields(undefined); + ldapClient.updatePassword.returns(BluebirdPromise.reject("Internal error with LDAP")); res.send = sinon.spy(function () { assert.equal(res.status.getCall(0).args[0], 500); done(); From e56c2492ed048c0a214bd1e2422a31a3bec0a85c Mon Sep 17 00:00:00 2001 From: Clement Michaud Date: Wed, 28 Jun 2017 15:57:58 +0200 Subject: [PATCH 2/3] Fix integration test and package Travis scripts --- .gitignore | 4 +- .travis.yml | 9 +- Dockerfile | 6 +- Gruntfile.js | 8 +- config.template.yml | 4 +- docker-compose.base.yml | 5 + docker-compose.dev.yml | 8 +- docker-compose.yml | 42 +---- example/ldap/Dockerfile | 9 + example/ldap/base.ldif | 17 +- example/ldap/docker-compose.admin.yml | 11 ++ example/ldap/docker-compose.yml | 10 ++ example/nginx/docker-compose.yml | 24 +++ example/{nginx_conf => nginx}/index.html | 0 example/{nginx_conf => nginx}/nginx.conf | 4 +- example/{nginx_conf => nginx}/secret.html | 2 +- example/{nginx_conf => nginx}/ssl/server.crt | 0 example/{nginx_conf => nginx}/ssl/server.csr | 0 example/{nginx_conf => nginx}/ssl/server.key | 0 package.json | 5 +- scripts/dc-example.sh | 5 + scripts/dc-test.sh | 5 + scripts/deploy-example.sh | 4 + scripts/docker-publish.sh | 1 + scripts/npm-deployment-test.sh | 2 + scripts/run-int-test.sh | 22 +++ scripts/run-staging.sh | 15 ++ scripts/travis.sh | 21 +++ scripts/undeploy-example.sh | 3 + src/server/lib/ConfigurationAdapter.ts | 46 ++++-- src/server/lib/LdapClient.ts | 3 +- src/server/lib/Server.ts | 1 + src/server/lib/notifiers/GMailNotifier.ts | 2 +- test/integration/Dockerfile | 5 + test/integration/Server.test.ts | 164 +++++++++++++++++++ test/integration/config.yml | 94 +++++++++++ test/integration/docker-compose.yml | 41 +++++ test/integration/nginx.conf | 86 ++++++++++ test/integration/test_server.ts | 157 ------------------ 39 files changed, 585 insertions(+), 260 deletions(-) create mode 100644 docker-compose.base.yml create mode 100644 example/ldap/Dockerfile create mode 100644 example/ldap/docker-compose.admin.yml create mode 100644 example/ldap/docker-compose.yml create mode 100644 example/nginx/docker-compose.yml rename example/{nginx_conf => nginx}/index.html (100%) rename example/{nginx_conf => nginx}/nginx.conf (95%) rename example/{nginx_conf => nginx}/secret.html (63%) rename example/{nginx_conf => nginx}/ssl/server.crt (100%) rename example/{nginx_conf => nginx}/ssl/server.csr (100%) rename example/{nginx_conf => nginx}/ssl/server.key (100%) create mode 100755 scripts/dc-example.sh create mode 100755 scripts/dc-test.sh create mode 100755 scripts/deploy-example.sh create mode 100755 scripts/run-int-test.sh create mode 100755 scripts/run-staging.sh create mode 100755 scripts/travis.sh create mode 100755 scripts/undeploy-example.sh create mode 100644 test/integration/Dockerfile create mode 100644 test/integration/Server.test.ts create mode 100644 test/integration/config.yml create mode 100644 test/integration/docker-compose.yml create mode 100644 test/integration/nginx.conf delete mode 100644 test/integration/test_server.ts diff --git a/.gitignore b/.gitignore index e9fe0ab2..43c1931f 100644 --- a/.gitignore +++ b/.gitignore @@ -13,9 +13,7 @@ src/.baseDir.ts *.swp -*.sh - -config.yml +/config.yml npm-debug.log diff --git a/.travis.yml b/.travis.yml index a0ec104f..b5052c4f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,14 +19,7 @@ addons: before_install: npm install -g npm@'>=2.13.5' script: -- grunt build-dist -- grunt docker-build -- docker-compose build -- docker-compose up -d -- sleep 5 -- ./scripts/check-services.sh -- npm run int-test -- ./scripts/npm-deployment-test.sh + - ./scripts/travis.sh after_success: - ./scripts/docker-publish.sh diff --git a/Dockerfile b/Dockerfile index aec7ddcf..63b6d3eb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,7 +10,7 @@ COPY dist/src/server /usr/src ENV PORT=80 EXPOSE 80 -VOLUME /etc/auth-server -VOLUME /var/lib/auth-server +VOLUME /etc/authelia +VOLUME /var/lib/authelia -CMD ["node", "index.js", "/etc/auth-server/config.yml"] +CMD ["node", "index.js", "/etc/authelia/config.yml"] diff --git a/Gruntfile.js b/Gruntfile.js index 654f5f7c..775c85d3 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -5,12 +5,12 @@ module.exports = function (grunt) { run: { options: {}, "build": { - cmd: "npm", - args: ['run', 'build'] + cmd: "./node_modules/.bin/tsc", + args: ['-p', 'tsconfig.json'] }, "tslint": { - cmd: "npm", - args: ['run', 'tslint'] + cmd: "./node_modules/.bin/tslint", + args: ['-c', 'tslint.json', '-p', 'tsconfig.json'] }, "test": { cmd: "npm", diff --git a/config.template.yml b/config.template.yml index 2b234c11..acff1362 100644 --- a/config.template.yml +++ b/config.template.yml @@ -12,7 +12,7 @@ logs_level: info # 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://ldap + url: ldap://openldap-restriction # The base dn for every entries base_dn: dc=example,dc=com @@ -85,7 +85,7 @@ store_directory: /var/lib/authelia/store notifier: # For testing purpose, notifications can be sent in a file filesystem: - filename: /var/lib/auth-server/notifications/notification.txt + filename: /var/lib/authelia/notifications/notification.txt # Use your gmail account to send the notifications. You can use an app password. # gmail: diff --git a/docker-compose.base.yml b/docker-compose.base.yml new file mode 100644 index 00000000..3432395a --- /dev/null +++ b/docker-compose.base.yml @@ -0,0 +1,5 @@ +version: '2' + +networks: + example-network: + driver: bridge diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 765f5014..6f48df1f 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -1,10 +1,10 @@ - version: '2' services: - auth: + authelia: volumes: - ./test:/usr/src/test - ./dist/src/server:/usr/src - ./node_modules:/usr/src/node_modules - - ./config.yml:/etc/auth-server/config.yml:ro - + - ./config.yml:/etc/authelia/config.yml:ro + networks: + - example-network diff --git a/docker-compose.yml b/docker-compose.yml index 7245e3d6..deee95c9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,43 +1,11 @@ version: '2' services: - auth: + authelia: build: . restart: always volumes: - - ./config.template.yml:/etc/auth-server/config.yml:ro - - ./notifications:/var/lib/auth-server/notifications + - ./config.template.yml:/etc/authelia/config.yml:ro + - ./notifications:/var/lib/authelia/notifications + networks: + - example-network - nginx: - image: nginx:alpine - volumes: - - ./example/nginx_conf/nginx.conf:/etc/nginx/nginx.conf - - ./example/nginx_conf/index.html:/usr/share/nginx/html/index.html - - ./example/nginx_conf/secret.html:/usr/share/nginx/html/secret.html - - ./example/nginx_conf/ssl:/etc/ssl - depends_on: - - auth - ports: - - "8080:443" - - openldap: - image: clems4ever/openldap - ports: - - "389:389" - environment: - - SLAPD_ORGANISATION=MyCompany - - SLAPD_DOMAIN=example.com - - SLAPD_PASSWORD=password - - SLAPD_CONFIG_PASSWORD=password - - SLAPD_ADDITIONAL_MODULES=memberof - - SLAPD_ADDITIONAL_SCHEMAS=openldap - - SLAPD_FORCE_RECONFIGURE=true - volumes: - - ./example/ldap:/etc/ldap.dist/prepopulate - - openldap-admin: - image: osixia/phpldapadmin:0.6.11 - ports: - - 9090:80 - environment: - - PHPLDAPADMIN_LDAP_HOSTS=openldap - - PHPLDAPADMIN_HTTPS=false diff --git a/example/ldap/Dockerfile b/example/ldap/Dockerfile new file mode 100644 index 00000000..fbb515eb --- /dev/null +++ b/example/ldap/Dockerfile @@ -0,0 +1,9 @@ +FROM clems4ever/openldap + +ENV SLAPD_ORGANISATION=MyCompany +ENV SLAPD_DOMAIN=example.com +ENV SLAPD_PASSWORD=password +ENV SLAPD_CONFIG_PASSWORD=password +ENV SLAPD_ADDITIONAL_MODULES=memberof +ENV SLAPD_ADDITIONAL_SCHEMAS=openldap +ENV SLAPD_FORCE_RECONFIGURE=true diff --git a/example/ldap/base.ldif b/example/ldap/base.ldif index 97ca0356..f1fbdb88 100644 --- a/example/ldap/base.ldif +++ b/example/ldap/base.ldif @@ -25,7 +25,7 @@ dn: cn=john,ou=users,dc=example,dc=com cn: john objectclass: inetOrgPerson objectclass: top -mail: clement.michaud34@gmail.com +mail: john.doe@example.com sn: John Doe userpassword: {SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g= @@ -45,18 +45,3 @@ mail: bob.dylan@example.com sn: Bob Dylan userpassword: {SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g= -# dn: uid=jack,ou=users,dc=example,dc=com -# cn: jack -# gidnumber: 501 -# givenname: Jack -# homedirectory: /home/jack -# loginshell: /bin/sh -# objectclass: inetOrgPerson -# objectclass: posixAccount -# objectclass: top -# mail: jack.daniels@example.com -# sn: Jack Daniels -# uid: jack -# uidnumber: 1001 -# userpassword: {SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g= -# diff --git a/example/ldap/docker-compose.admin.yml b/example/ldap/docker-compose.admin.yml new file mode 100644 index 00000000..c7307578 --- /dev/null +++ b/example/ldap/docker-compose.admin.yml @@ -0,0 +1,11 @@ +version: '2' +services: + openldap-admin: + image: osixia/phpldapadmin:0.6.11 + ports: + - 9090:80 + environment: + - PHPLDAPADMIN_LDAP_HOSTS=openldap + - PHPLDAPADMIN_HTTPS=false + networks: + - example-network diff --git a/example/ldap/docker-compose.yml b/example/ldap/docker-compose.yml new file mode 100644 index 00000000..0f505a0a --- /dev/null +++ b/example/ldap/docker-compose.yml @@ -0,0 +1,10 @@ +version: '2' +services: + openldap: + build: ./example/ldap + volumes: + - ./example/ldap/base.ldif:/etc/ldap.dist/prepopulate/base.ldif + - ./example/ldap/access.rules:/etc/ldap.dist/prepopulate/access.rules + networks: + - example-network + diff --git a/example/nginx/docker-compose.yml b/example/nginx/docker-compose.yml new file mode 100644 index 00000000..f4127377 --- /dev/null +++ b/example/nginx/docker-compose.yml @@ -0,0 +1,24 @@ +version: '2' +services: + nginx: + image: nginx:alpine + volumes: + - ./example/nginx/index.html:/usr/share/nginx/html/index.html + - ./example/nginx/secret.html:/usr/share/nginx/html/secret.html + - ./example/nginx/ssl:/etc/ssl + - ./example/nginx/nginx.conf:/etc/nginx/nginx.conf + ports: + - "8080:443" + depends_on: + - authelia + networks: + example-network: + aliases: + - home.test.local + - secret.test.local + - secret1.test.local + - secret2.test.local + - mx1.mail.test.local + - mx2.mail.test.local + - auth.test.local + diff --git a/example/nginx_conf/index.html b/example/nginx/index.html similarity index 100% rename from example/nginx_conf/index.html rename to example/nginx/index.html diff --git a/example/nginx_conf/nginx.conf b/example/nginx/nginx.conf similarity index 95% rename from example/nginx_conf/nginx.conf rename to example/nginx/nginx.conf index 400eb115..bb0749f3 100644 --- a/example/nginx_conf/nginx.conf +++ b/example/nginx/nginx.conf @@ -36,7 +36,7 @@ http { proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; - proxy_pass http://auth/; + proxy_pass http://authelia/; proxy_intercept_errors on; @@ -68,7 +68,7 @@ http { proxy_set_header X-Real-IP $remote_addr; proxy_set_header Host $http_host; - proxy_pass http://auth/verify; + proxy_pass http://authelia/verify; } location = /secret.html { diff --git a/example/nginx_conf/secret.html b/example/nginx/secret.html similarity index 63% rename from example/nginx_conf/secret.html rename to example/nginx/secret.html index 8b44155a..d1693678 100644 --- a/example/nginx_conf/secret.html +++ b/example/nginx/secret.html @@ -4,6 +4,6 @@ This is a very important secret!
- Go back to home page. + Go back to home page. diff --git a/example/nginx_conf/ssl/server.crt b/example/nginx/ssl/server.crt similarity index 100% rename from example/nginx_conf/ssl/server.crt rename to example/nginx/ssl/server.crt diff --git a/example/nginx_conf/ssl/server.csr b/example/nginx/ssl/server.csr similarity index 100% rename from example/nginx_conf/ssl/server.csr rename to example/nginx/ssl/server.csr diff --git a/example/nginx_conf/ssl/server.key b/example/nginx/ssl/server.key similarity index 100% rename from example/nginx_conf/ssl/server.key rename to example/nginx/ssl/server.key diff --git a/package.json b/package.json index 601b07e5..eb40da9f 100644 --- a/package.json +++ b/package.json @@ -8,10 +8,7 @@ }, "scripts": { "test": "./node_modules/.bin/mocha --compilers ts:ts-node/register --recursive test/client test/server", - "int-test": "./node_modules/.bin/mocha --compilers ts:ts-node/register --recursive test/integration", "cover": "NODE_ENV=test nyc npm t", - "build": "tsc", - "tslint": "tslint -c tslint.json -p tsconfig.json", "serve": "node dist/server/index.js" }, "repository": { @@ -63,7 +60,7 @@ "@types/proxyquire": "^1.3.27", "@types/query-string": "^4.3.1", "@types/randomstring": "^1.1.5", - "@types/request": "0.0.43", + "@types/request": "0.0.45", "@types/sinon": "^2.2.1", "@types/speakeasy": "^2.0.1", "@types/tmp": "0.0.33", diff --git a/scripts/dc-example.sh b/scripts/dc-example.sh new file mode 100755 index 00000000..1abd427b --- /dev/null +++ b/scripts/dc-example.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +set -e + +docker-compose -f docker-compose.base.yml -f docker-compose.yml -f example/nginx/docker-compose.yml -f example/ldap/docker-compose.yml $* diff --git a/scripts/dc-test.sh b/scripts/dc-test.sh new file mode 100755 index 00000000..df285b0c --- /dev/null +++ b/scripts/dc-test.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +set -e + +docker-compose -f docker-compose.base.yml -f example/ldap/docker-compose.yml -f test/integration/docker-compose.yml $* diff --git a/scripts/deploy-example.sh b/scripts/deploy-example.sh new file mode 100755 index 00000000..006747a4 --- /dev/null +++ b/scripts/deploy-example.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +./scripts/dc-example.sh build +./scripts/dc-example.sh up -d diff --git a/scripts/docker-publish.sh b/scripts/docker-publish.sh index bcf80f80..2d7b4568 100755 --- a/scripts/docker-publish.sh +++ b/scripts/docker-publish.sh @@ -16,6 +16,7 @@ function deploy_on_dockerhub { docker login -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD"; echo "Docker image $IMAGE_WITH_TAG will be deployed on Dockerhub." + docker build -t $IMAGE_NAME . docker tag $IMAGE_NAME $IMAGE_WITH_TAG; docker push $IMAGE_WITH_TAG; echo "Docker image deployed successfully." diff --git a/scripts/npm-deployment-test.sh b/scripts/npm-deployment-test.sh index 92dad4a8..c2950303 100755 --- a/scripts/npm-deployment-test.sh +++ b/scripts/npm-deployment-test.sh @@ -1,5 +1,7 @@ #!/bin/bash +set -e + NPM_UNPACK_DIR=/tmp/npm-unpack echo "--- Packing npm package into a tarball" diff --git a/scripts/run-int-test.sh b/scripts/run-int-test.sh new file mode 100755 index 00000000..efa09a77 --- /dev/null +++ b/scripts/run-int-test.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +set -e + +echo "Build services images..." +./scripts/dc-test.sh build + +echo "Start services..." +./scripts/dc-test.sh up -d authelia nginx openldap +sleep 3 +docker ps -a + +echo "Display services logs..." +./scripts/dc-test.sh logs authelia +./scripts/dc-test.sh logs nginx +./scripts/dc-test.sh logs openldap + +echo "Run integration tests..." +./scripts/dc-test.sh run --rm --name int-test int-test + +echo "Shutdown services..." +./scripts/dc-test.sh down diff --git a/scripts/run-staging.sh b/scripts/run-staging.sh new file mode 100755 index 00000000..7b03e236 --- /dev/null +++ b/scripts/run-staging.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +set -e + +# Build production environment and set it up +./scripts/dc-example.sh build +./scripts/dc-example.sh up -d + +# Wait for services to be running +sleep 5 + +# Check if services are correctly running +./scripts/check-services.sh + +./scripts/dc-example.sh down diff --git a/scripts/travis.sh b/scripts/travis.sh new file mode 100755 index 00000000..ef0d7e15 --- /dev/null +++ b/scripts/travis.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +set -e + +docker --version +docker-compose --version + +# Run unit tests +grunt test + +# Build the app from Typescript and package +grunt build-dist + +# Run integration tests +./scripts/run-int-test.sh + +# Test staging environment +./scripts/run-staging.sh + +# Test npm deployment before actual deployment +./scripts/npm-deployment-test.sh diff --git a/scripts/undeploy-example.sh b/scripts/undeploy-example.sh new file mode 100755 index 00000000..5040b84f --- /dev/null +++ b/scripts/undeploy-example.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +./scripts/dc-example.sh down diff --git a/src/server/lib/ConfigurationAdapter.ts b/src/server/lib/ConfigurationAdapter.ts index 9fd64d39..4779fa08 100644 --- a/src/server/lib/ConfigurationAdapter.ts +++ b/src/server/lib/ConfigurationAdapter.ts @@ -2,6 +2,8 @@ import * as ObjectPath from "object-path"; import { AppConfiguration, UserConfiguration, NotifierConfiguration, ACLConfiguration, LdapConfiguration } from "./../../types/Configuration"; +const LDAP_URL_ENV_VARIABLE = "LDAP_URL"; + function get_optional(config: object, path: string, default_value: T): T { let entry = default_value; @@ -17,26 +19,36 @@ function ensure_key_existence(config: object, path: string): void { } } +function adaptFromUserConfiguration(userConfiguration: UserConfiguration): AppConfiguration { + ensure_key_existence(userConfiguration, "ldap"); + ensure_key_existence(userConfiguration, "session.secret"); + + const port = ObjectPath.get(userConfiguration, "port", 8080); + + return { + port: port, + ldap: ObjectPath.get(userConfiguration, "ldap"), + session: { + domain: ObjectPath.get(userConfiguration, "session.domain"), + secret: ObjectPath.get(userConfiguration, "session.secret"), + expiration: get_optional(userConfiguration, "session.expiration", 3600000), // in ms + }, + store_directory: get_optional(userConfiguration, "store_directory", undefined), + logs_level: get_optional(userConfiguration, "logs_level", "info"), + notifier: ObjectPath.get(userConfiguration, "notifier"), + access_control: ObjectPath.get(userConfiguration, "access_control") + }; +} + export default class ConfigurationAdapter { - static adapt(yaml_config: UserConfiguration): AppConfiguration { - ensure_key_existence(yaml_config, "ldap"); - ensure_key_existence(yaml_config, "session.secret"); + static adapt(userConfiguration: UserConfiguration): AppConfiguration { + const appConfiguration = adaptFromUserConfiguration(userConfiguration); - const port = ObjectPath.get(yaml_config, "port", 8080); + const ldapUrl = process.env[LDAP_URL_ENV_VARIABLE]; + if (ldapUrl) + appConfiguration.ldap.url = ldapUrl; - return { - port: port, - ldap: ObjectPath.get(yaml_config, "ldap"), - session: { - domain: ObjectPath.get(yaml_config, "session.domain"), - secret: ObjectPath.get(yaml_config, "session.secret"), - expiration: get_optional(yaml_config, "session.expiration", 3600000), // in ms - }, - store_directory: get_optional(yaml_config, "store_directory", undefined), - logs_level: get_optional(yaml_config, "logs_level", "info"), - notifier: ObjectPath.get(yaml_config, "notifier"), - access_control: ObjectPath.get(yaml_config, "access_control") - }; + return appConfiguration; } } diff --git a/src/server/lib/LdapClient.ts b/src/server/lib/LdapClient.ts index bf16a6d1..8504a38d 100644 --- a/src/server/lib/LdapClient.ts +++ b/src/server/lib/LdapClient.ts @@ -68,9 +68,10 @@ export class LdapClient { const that = this; const ldapClient = this.createClient(); - this.logger.debug("LDAP: Check password for user '%s'", userDN); + this.logger.debug("LDAP: Check password by binding user '%s'", userDN); return ldapClient.bindAsync(userDN, password) .then(function () { + that.logger.debug("LDAP: Unbind user '%s'", userDN); return ldapClient.unbindAsync(); }) .error(function (err: Error) { diff --git a/src/server/lib/Server.ts b/src/server/lib/Server.ts index 00b54731..1b1525ea 100644 --- a/src/server/lib/Server.ts +++ b/src/server/lib/Server.ts @@ -50,6 +50,7 @@ export default class Server { // by default the level of logs is info deps.winston.level = config.logs_level; console.log("Log level = ", deps.winston.level); + deps.winston.debug("Authelia configuration is %s", JSON.stringify(config, undefined, 2)); ServerVariables.fill(app, config, deps); diff --git a/src/server/lib/notifiers/GMailNotifier.ts b/src/server/lib/notifiers/GMailNotifier.ts index 474ff1f0..384dc7d9 100644 --- a/src/server/lib/notifiers/GMailNotifier.ts +++ b/src/server/lib/notifiers/GMailNotifier.ts @@ -35,7 +35,7 @@ export class GMailNotifier extends INotifier { }; const mailOptions = { - from: "auth-server@open-intent.io", + from: "authelia@authelia.com", to: identity.email, subject: subject, html: ejs.render(email_template, d) diff --git a/test/integration/Dockerfile b/test/integration/Dockerfile new file mode 100644 index 00000000..fff70158 --- /dev/null +++ b/test/integration/Dockerfile @@ -0,0 +1,5 @@ +FROM node:7-alpine + +WORKDIR /usr/src + +CMD ["./node_modules/.bin/mocha", "--compilers", "ts:ts-node/register", "--recursive", "test/integration"] diff --git a/test/integration/Server.test.ts b/test/integration/Server.test.ts new file mode 100644 index 00000000..dd1aa6d6 --- /dev/null +++ b/test/integration/Server.test.ts @@ -0,0 +1,164 @@ + +import Request = require("request"); +import Assert = require("assert"); +import Speakeasy = require("speakeasy"); +import BluebirdPromise = require("bluebird"); +import Util = require("util"); +import Sinon = require("sinon"); +import Endpoints = require("../../src/server/endpoints"); + +const EXEC_PATH = "./dist/src/server/index.js"; +const CONFIG_PATH = "./test/integration/config.yml"; +const j = Request.jar(); +const request: typeof Request = BluebirdPromise.promisifyAll(Request.defaults({ jar: j })); + +process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; + +const DOMAIN = "test.local"; +const PORT = 8080; + +const HOME_URL = Util.format("https://%s.%s:%d", "home", DOMAIN, PORT); +const SECRET_URL = Util.format("https://%s.%s:%d", "secret", DOMAIN, PORT); +const SECRET1_URL = Util.format("https://%s.%s:%d", "secret1", DOMAIN, PORT); +const SECRET2_URL = Util.format("https://%s.%s:%d", "secret2", DOMAIN, PORT); +const MX1_URL = Util.format("https://%s.%s:%d", "mx1.mail", DOMAIN, PORT); +const MX2_URL = Util.format("https://%s.%s:%d", "mx2.mail", DOMAIN, PORT); +const BASE_AUTH_URL = Util.format("https://%s.%s:%d", "auth", DOMAIN, PORT); + +function waitFor(ms: number): BluebirdPromise<{}> { + return new BluebirdPromise(function (resolve, reject) { + setTimeout(function () { + resolve(); + }, ms); + }); +} + +describe("test the server", function () { + let home_page: string; + let login_page: string; + + before(function () { + const home_page_promise = getHomePage() + .then(function (data) { + home_page = data.body; + }); + const login_page_promise = getLoginPage() + .then(function (data) { + login_page = data.body; + }); + + return BluebirdPromise.all([home_page_promise, + login_page_promise]); + }); + + after(function () { + }); + + function str_contains(str: string, pattern: string) { + return str.indexOf(pattern) != -1; + } + + function home_page_contains(pattern: string) { + return str_contains(home_page, pattern); + } + + it("should serve a correct home page", function () { + Assert(home_page_contains(BASE_AUTH_URL + Endpoints.LOGOUT_GET + "?redirect=" + HOME_URL + "/")); + Assert(home_page_contains(HOME_URL + "/secret.html")); + Assert(home_page_contains(SECRET_URL + "/secret.html")); + Assert(home_page_contains(SECRET1_URL + "/secret.html")); + Assert(home_page_contains(SECRET2_URL + "/secret.html")); + Assert(home_page_contains(MX1_URL + "/secret.html")); + Assert(home_page_contains(MX2_URL + "/secret.html")); + }); + + it("should serve the login page", function () { + return getPromised(BASE_AUTH_URL + Endpoints.FIRST_FACTOR_GET) + .then(function (data: Request.RequestResponse) { + Assert.equal(data.statusCode, 200); + }); + }); + + it("should serve the homepage", function () { + return getPromised(HOME_URL + "/") + .then(function (data: Request.RequestResponse) { + Assert.equal(data.statusCode, 200); + }); + }); + + it("should redirect when logout", function () { + return getPromised(BASE_AUTH_URL + Endpoints.LOGOUT_GET + "?redirect=" + HOME_URL) + .then(function (data: Request.RequestResponse) { + Assert.equal(data.statusCode, 200); + Assert.equal(data.body, home_page); + }); + }); + + it("should be redirected to the login page when accessing secret while not authenticated", function () { + return getPromised(HOME_URL + "/secret.html") + .then(function (data: Request.RequestResponse) { + Assert.equal(data.statusCode, 200); + Assert.equal(data.body, login_page); + }); + }); + + it.skip("should fail the first factor", function () { + return postPromised(BASE_AUTH_URL + Endpoints.FIRST_FACTOR_POST, { + form: { + username: "admin", + password: "password", + } + }) + .then(function (data: Request.RequestResponse) { + Assert.equal(data.body, "Bad credentials"); + }); + }); + + function login_as(username: string, password: string) { + return postPromised(BASE_AUTH_URL + Endpoints.FIRST_FACTOR_POST, { + form: { + username: "john", + password: "password", + } + }) + .then(function (data: Request.RequestResponse) { + Assert.equal(data.statusCode, 302); + return BluebirdPromise.resolve(); + }); + } + + it("should succeed the first factor", function () { + return login_as("john", "password"); + }); + + describe("test ldap connection", function () { + it("should not fail after inactivity", function () { + const clock = Sinon.useFakeTimers(); + return login_as("john", "password") + .then(function () { + clock.tick(3600000 * 24); // 24 hour + return login_as("john", "password"); + }) + .then(function () { + clock.restore(); + return BluebirdPromise.resolve(); + }); + }); + }); +}); + +function getPromised(url: string) { + return request.getAsync(url); +} + +function postPromised(url: string, body: Object) { + return request.postAsync(url, body); +} + +function getHomePage(): BluebirdPromise { + return getPromised(HOME_URL + "/"); +} + +function getLoginPage(): BluebirdPromise { + return getPromised(BASE_AUTH_URL + Endpoints.FIRST_FACTOR_GET); +} diff --git a/test/integration/config.yml b/test/integration/config.yml new file mode 100644 index 00000000..5ec8a6d7 --- /dev/null +++ b/test/integration/config.yml @@ -0,0 +1,94 @@ + +# 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_user_dn: ou=users + + # The user name attribute of users. Might uid for FreeIPA. 'cn' by default. + user_name_attribute: cn + + # An additional dn to define the scope of groups + additional_group_dn: ou=groups + + # The group name attribute of group. 'cn' by default. + group_name_attribute: cn + + # 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 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. +# +# '*' means 'any' subdomains and matches any string. It must stand at the +# beginning of the pattern. +access_control: + default: + - home.test.local + groups: + admin: + - '*.test.local' + dev: + - secret.test.local + - secret2.test.local + users: + harry: + - secret1.test.local + bob: + - '*.mail.test.local' + + +# Configuration of session cookies +# +# _secret_ the secret to encrypt session cookies +# _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: + secret: unsecure_secret + expiration: 3600000 + domain: test.local + + +# The directory where the DB files will be saved +store_directory: /var/lib/authelia/store + + +# Notifications are sent to users when they require a password reset, a u2f +# registration or a TOTP registration. +# Use only one 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 + # password: password + diff --git a/test/integration/docker-compose.yml b/test/integration/docker-compose.yml new file mode 100644 index 00000000..2920725a --- /dev/null +++ b/test/integration/docker-compose.yml @@ -0,0 +1,41 @@ +version: '2' +services: + authelia: + image: node:7-alpine + command: node /usr/src/dist/src/server/index.js /etc/authelia/config.yml + volumes: + - ./:/usr/src + - ./test/integration/config.yml:/etc/authelia/config.yml:ro + networks: + - example-network + + int-test: + build: ./test/integration + volumes: + - ./:/usr/src + networks: + - example-network + + + nginx: + image: nginx:alpine + volumes: + - ./example/nginx/index.html:/usr/share/nginx/html/index.html + - ./example/nginx/secret.html:/usr/share/nginx/html/secret.html + - ./example/nginx/ssl:/etc/ssl + - ./test/integration/nginx.conf:/etc/nginx/nginx.conf + expose: + - "8080" + depends_on: + - authelia + networks: + example-network: + aliases: + - home.test.local + - secret.test.local + - secret1.test.local + - secret2.test.local + - mx1.mail.test.local + - mx2.mail.test.local + - auth.test.local + diff --git a/test/integration/nginx.conf b/test/integration/nginx.conf new file mode 100644 index 00000000..39f7baa0 --- /dev/null +++ b/test/integration/nginx.conf @@ -0,0 +1,86 @@ +# nginx-sso - example nginx config +# +# (c) 2015 by Johannes Gilger +# +# This is an example config for using nginx with the nginx-sso cookie system. +# For simplicity, this config sets up two fictional vhosts that you can use to +# test against both components of the nginx-sso system: ssoauth & ssologin. +# In a real deployment, these vhosts would be separate hosts. + +#user nobody; +worker_processes 1; + +#error_log logs/error.log; +#error_log logs/error.log notice; +#error_log logs/error.log info; + +#pid logs/nginx.pid; + +events { + worker_connections 1024; +} + + +http { + server { + listen 8080 ssl; + server_name auth.test.local localhost; + + ssl on; + ssl_certificate /etc/ssl/server.crt; + ssl_certificate_key /etc/ssl/server.key; + + + location / { + proxy_set_header X-Original-URI $request_uri; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + + proxy_pass http://authelia/; + + proxy_intercept_errors on; + + error_page 401 = /error/401; + error_page 403 = /error/403; + error_page 404 = /error/404; + } + } + + server { + listen 8080 ssl; + root /usr/share/nginx/html; + + server_name secret1.test.local secret2.test.local secret.test.local + home.test.local mx1.mail.test.local mx2.mail.test.local; + + ssl on; + ssl_certificate /etc/ssl/server.crt; + ssl_certificate_key /etc/ssl/server.key; + + error_page 401 = @error401; + location @error401 { + return 302 https://auth.test.local:8080; + } + + location /auth_verify { + internal; + proxy_set_header X-Original-URI $request_uri; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header Host $http_host; + + proxy_pass http://authelia/verify; + } + + location = /secret.html { + auth_request /auth_verify; + + auth_request_set $user $upstream_http_x_remote_user; + proxy_set_header X-Forwarded-User $user; + auth_request_set $groups $upstream_http_remote_groups; + proxy_set_header Remote-Groups $groups; + auth_request_set $expiry $upstream_http_remote_expiry; + proxy_set_header Remote-Expiry $expiry; + } + } +} + diff --git a/test/integration/test_server.ts b/test/integration/test_server.ts deleted file mode 100644 index 5a0541ed..00000000 --- a/test/integration/test_server.ts +++ /dev/null @@ -1,157 +0,0 @@ - -import request_ = require("request"); -import assert = require("assert"); -import speakeasy = require("speakeasy"); -import BluebirdPromise = require("bluebird"); -import util = require("util"); -import sinon = require("sinon"); -import Endpoints = require("../../src/server/endpoints"); - -const j = request_.jar(); -const request: typeof request_ = BluebirdPromise.promisifyAll(request_.defaults({ jar: j })); - -process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; - -const AUTHELIA_HOST = "nginx"; -const DOMAIN = "test.local"; -const PORT = 8080; - -const HOME_URL = util.format("https://%s.%s:%d", "home", DOMAIN, PORT); -const SECRET_URL = util.format("https://%s.%s:%d", "secret", DOMAIN, PORT); -const SECRET1_URL = util.format("https://%s.%s:%d", "secret1", DOMAIN, PORT); -const SECRET2_URL = util.format("https://%s.%s:%d", "secret2", DOMAIN, PORT); -const MX1_URL = util.format("https://%s.%s:%d", "mx1.mail", DOMAIN, PORT); -const MX2_URL = util.format("https://%s.%s:%d", "mx2.mail", DOMAIN, PORT); -const BASE_AUTH_URL = util.format("https://%s.%s:%d", "auth", DOMAIN, PORT); - -describe("test the server", function () { - let home_page: string; - let login_page: string; - - before(function () { - const home_page_promise = getHomePage() - .then(function (data) { - home_page = data.body; - }); - const login_page_promise = getLoginPage() - .then(function (data) { - login_page = data.body; - }); - return BluebirdPromise.all([home_page_promise, - login_page_promise]); - }); - - function str_contains(str: string, pattern: string) { - return str.indexOf(pattern) != -1; - } - - function home_page_contains(pattern: string) { - return str_contains(home_page, pattern); - } - - it("should serve a correct home page", function () { - assert(home_page_contains(BASE_AUTH_URL + Endpoints.LOGOUT_GET + "?redirect=" + HOME_URL + "/")); - assert(home_page_contains(HOME_URL + "/secret.html")); - assert(home_page_contains(SECRET_URL + "/secret.html")); - assert(home_page_contains(SECRET1_URL + "/secret.html")); - assert(home_page_contains(SECRET2_URL + "/secret.html")); - assert(home_page_contains(MX1_URL + "/secret.html")); - assert(home_page_contains(MX2_URL + "/secret.html")); - }); - - it("should serve the login page", function (done) { - getPromised(BASE_AUTH_URL + Endpoints.FIRST_FACTOR_GET + "?redirect=/") - .then(function (data: request_.RequestResponse) { - assert.equal(data.statusCode, 200); - done(); - }); - }); - - it("should serve the homepage", function (done) { - getPromised(HOME_URL + "/") - .then(function (data: request_.RequestResponse) { - assert.equal(data.statusCode, 200); - done(); - }); - }); - - it("should redirect when logout", function (done) { - getPromised(BASE_AUTH_URL + Endpoints.LOGOUT_GET + "?redirect=" + HOME_URL) - .then(function (data: request_.RequestResponse) { - assert.equal(data.statusCode, 200); - assert.equal(data.body, home_page); - done(); - }); - }); - - it("should be redirected to the login page when accessing secret while not authenticated", function (done) { - const url = HOME_URL + "/secret.html"; - getPromised(url) - .then(function (data: request_.RequestResponse) { - assert.equal(data.statusCode, 200); - assert.equal(data.body, login_page); - done(); - }); - }); - - it.skip("should fail the first factor", function (done) { - postPromised(BASE_AUTH_URL + Endpoints.FIRST_FACTOR_POST, { - form: { - username: "admin", - password: "password", - } - }) - .then(function (data: request_.RequestResponse) { - assert.equal(data.body, "Bad credentials"); - done(); - }); - }); - - function login_as(username: string, password: string) { - return postPromised(BASE_AUTH_URL + Endpoints.FIRST_FACTOR_POST, { - form: { - username: "john", - password: "password", - } - }) - .then(function (data: request_.RequestResponse) { - assert.equal(data.statusCode, 302); - return BluebirdPromise.resolve(); - }); - } - - it("should succeed the first factor", function () { - return login_as("john", "password"); - }); - - describe("test ldap connection", function () { - it("should not fail after inactivity", function () { - const clock = sinon.useFakeTimers(); - return login_as("john", "password") - .then(function () { - clock.tick(3600000 * 24); // 24 hour - return login_as("john", "password"); - }) - .then(function () { - clock.restore(); - return BluebirdPromise.resolve(); - }); - }); - }); -}); - -function getPromised(url: string) { - return request.getAsync(url); -} - -function postPromised(url: string, body: Object) { - return request.postAsync(url, body); -} - -function getHomePage(): BluebirdPromise { - return getPromised(HOME_URL + "/"); -} - -function getLoginPage(): BluebirdPromise { - return getPromised(BASE_AUTH_URL + Endpoints.FIRST_FACTOR_GET); -} From 03c1088a9242b002d62fb6010fca284103611974 Mon Sep 17 00:00:00 2001 From: Clement Michaud Date: Thu, 29 Jun 2017 11:51:52 +0200 Subject: [PATCH 3/3] Update the README to take example environment changes and new deployment command into account --- README.md | 98 +++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 67 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 13f9969c..450c7106 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![Build](https://travis-ci.org/clems4ever/authelia.svg?branch=master)](https://travis-ci.org/clems4ever/authelia) **Authelia** is a complete HTTP 2-factor authentication server for proxies like -nginx. It has been made to work with NGINX auth_request module and is currently +nginx. It has been made to work with nginx [auth_request] module and is currently used in production to secure internal services in a small docker swarm cluster. ## Features @@ -17,25 +17,53 @@ address. ## Deployment -If you don't have any LDAP and nginx setup yet, I advise you to follow the -Getting Started. That way, you will not require anything to start. +If you don't have any LDAP and/or nginx setup yet, I advise you to follow the +[Getting Started](#Getting-started) section. That way, you can test it right away +without even configure anything. -Otherwise here are the available steps to deploy on your machine. +Otherwise here are the available steps to deploy **Authelia** on your machine given +your configuration file is **/path/to/your/config.yml**. ### With NPM npm install -g authelia + authelia /path/to/your/config.yml ### With Docker docker pull clems4ever/authelia + docker run -v /path/to/your/config.yml:/etc/authelia/config.yml -v /path/to/data/dir:/var/lib/authelia clems4ever/authelia + +where **/path/to/data/dir** is the directory where all user data will be stored. ## Getting started The provided example is docker-based so that you can deploy and test it very -quickly. First clone the repo make sure you don't have anything listening on -port 8080 before starting. -Add the following lines to your /etc/hosts to simulate multiple subdomains +quickly. + +### Pre-requisites + +#### npm +Make sure you have npm and node installed on your computer. + +#### Docker +Make sure you have **docker** and **docker-compose** installed on your machine. +For your information, here are the versions that have been used for testing: + + docker --version + +gave *Docker version 17.03.1-ce, build c6d412e*. + + docker-compose --version + +gave *docker-compose version 1.14.0, build c7bdf9e*. + +#### Available port +Make sure you don't have anything listening on port 8080. + +#### Subdomain aliases + +Add the following lines to your **/etc/hosts** to alias multiple subdomains so that nginx can redirect request to the correct virtual host. 127.0.0.1 secret.test.local 127.0.0.1 secret1.test.local @@ -44,23 +72,28 @@ Add the following lines to your /etc/hosts to simulate multiple subdomains 127.0.0.1 mx1.mail.test.local 127.0.0.1 mx2.mail.test.local 127.0.0.1 auth.test.local + +### Deployment -Then, type the following command to build and deploy the services: +Deploy **Authelia** example with the following command: npm install --only=dev - grunt build-dist - docker-compose build - docker-compose up -d + ./node_modules/.bin/grunt build-dist + ./scripts/deploy-example.sh After few seconds the services should be running and you should be able to visit -[https://home.test.local:8080/](https://home.test.local:8080/). +[https://home.test.local:8080/](https://home.test.local:8080/). -Normally, a self-signed certificate exception should appear, it has to be -accepted before getting to the login page: +When accessing the login page, a self-signed certificate exception should appear, +it has to be trusted before you can get to the target page. The certificate +must be trusted for each subdomain, therefore it is normal to see the exception + several times. + +Below is what the login page looks like: -### 1st factor: LDAP and ACL +### First factor: LDAP and ACL An LDAP server has been deployed for you with the following credentials and access control list: @@ -76,54 +109,55 @@ any subdomain. - [secret1.test.local](https://secret1.test.local:8080/secret.html) - [home.test.local](https://home.test.local:8080/secret.html) -Type them in the login page and validate. Then, the second factor page should -have appeared as shown below. +You can use them in the login page. If everything is ok, the second factor +page should appear as shown below. Otherwise you'll get an error message notifying +your credentials are wrong. -### 2nd factor: TOTP (Time-Base One Time Password) +### Second factor: TOTP (Time-Base One Time Password) In **Authelia**, you need to register a per user TOTP secret before 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 be sent, the link is rather delivered in the file -./notifications/notification.txt. Paste the link in your browser and you'll get +**./notifications/notification.txt**. Paste the link in your browser and you'll get your secret in QRCode and Base32 formats. You can use -[Google Authenticator](https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en) -to store them and get the generated tokens required during authentication. +[Google Authenticator] +to store them and get the generated tokens with the app. ### 2nd factor: U2F (Universal 2-Factor) with security keys **Authelia** also offers authentication using U2F devices like [Yubikey](Yubikey) USB security keys. U2F is one of the most secure authentication protocol and is -already available for accounts on Google, Facebook, Github and more. +already available for Google, Facebook, Github accounts and more. -Like TOTP, U2F requires you register your security key before authenticating -with it. To do so, click on the register button. This will send a link to the +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 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 **./notifications/notification.txt**. Paste the link in your browser and you'll be asking to touch the token of your device -to register it. You can now authenticate using your U2F device by simply -touching the token. +to register. Upon successful registration, you can authenticate using your U2F +device by simply touching the token. Easy, right?! ### Password reset With **Authelia**, you can also reset your password in no time. Click on the -according button 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 email address. For the sake of the example, the email is delivered in the file -./notifications/notification.txt. +**./notifications/notification.txt**. Paste the link in your browser and you should be able to reset the password. ### Access Control With **Authelia**, you can define your own access control rules for restricting -the access to certain subdomains to your users. Those rules are defined in the -configuration file and can be either default, per-user or per-group policies. +the user access to some subdomains. Those rules are defined in the +configuration file and can be set either for everyone, per-user or per-group policies. Check out the *config.template.yml* to see how they are defined. ## Documentation @@ -172,4 +206,6 @@ Follow [contributing](CONTRIBUTORS.md) file. [TOTP]: https://en.wikipedia.org/wiki/Time-based_One-time_Password_Algorithm [U2F]: https://www.yubico.com/about/background/fido/ [Yubikey]: https://www.yubico.com/products/yubikey-hardware/yubikey4/ +[auth_request]: http://nginx.org/en/docs/http/ngx_http_auth_request_module.html +[Google Authenticator]: https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en