From 4cd78f3f831f08087dcbdbacb47f35903e55a0e6 Mon Sep 17 00:00:00 2001 From: Clement Michaud Date: Sun, 24 Sep 2017 14:49:03 +0200 Subject: [PATCH] Add SMTP notifier as an available option in configuration One can now plug its own SMTP server to send notifications for identity validation and password reset requests. Filesystem has been removed from the template configuration file since even tests now use mail catcher (the fake webmail) to retrieve the email and the confirmation link. --- .gitignore | 2 + README.md | 25 +++++++-- config.template.yml | 11 ++-- config.test.yml | 12 +++-- docker-compose.yml | 1 - example/ldap/base.ldif | 10 ++-- example/ldap/docker-compose.yml | 1 + example/smtp/docker-compose.yml | 8 +++ package.json | 2 + scripts/dc-dev.sh | 1 + scripts/example-commit/dc-example.sh | 1 + scripts/example-commit/deploy-example.sh | 2 +- scripts/example-dockerhub/dc-example.sh | 1 + scripts/example-dockerhub/deploy-example.sh | 2 +- scripts/integration-tests.sh | 10 ++-- .../lib/configuration/Configuration.d.ts | 9 ++++ .../lib/notifiers/AbstractEmailNotifier.ts | 24 +++++++++ src/server/lib/notifiers/GMailNotifier.ts | 23 +++----- src/server/lib/notifiers/NotifierFactory.ts | 7 +++ src/server/lib/notifiers/SmtpNotifier.ts | 53 +++++++++++++++++++ .../step_definitions/reset-password.ts | 10 ++-- test/features/support/world.ts | 37 ++++++++++--- 22 files changed, 195 insertions(+), 57 deletions(-) create mode 100644 example/smtp/docker-compose.yml create mode 100644 src/server/lib/notifiers/AbstractEmailNotifier.ts create mode 100644 src/server/lib/notifiers/SmtpNotifier.ts diff --git a/.gitignore b/.gitignore index 8e7376122..f80f27d26 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,5 @@ dist/ # Specific files /config.yml + +example/ldap/private.ldif diff --git a/README.md b/README.md index 778d2929c..9da56de7e 100644 --- a/README.md +++ b/README.md @@ -39,14 +39,15 @@ as 2nd factor. address. * Access restriction after too many authentication attempts. * Session management using Redis key/value store. +* User-defined access control per subdomain and resource. ## Deployment 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. +without even configuring anything. -Otherwise here are the available steps to deploy **Authelia** on your machine given +Otherwise, here are the available steps to deploy **Authelia** on your machine given your configuration file is **/path/to/your/config.yml**. Note that you can create your own the configuration file from [config.template.yml] at the root of the repo. @@ -85,7 +86,7 @@ gave *Docker version 17.03.1-ce, build c6d412e*. gave *docker-compose version 1.14.0, build c7bdf9e*. #### Available port -Make sure you don't have anything listening on port 8080. +Make sure you don't have anything listening on port 8080 (webserver) and 8085 (webmail). #### Subdomain aliases @@ -141,10 +142,16 @@ You can find an example of the configuration of the LDAP backend in [config.temp ### Second factor with TOTP In **Authelia**, you can register a per user TOTP (Time-Based One Time Password) secret before authenticating. To do that, you need to click on the register button. It will +<<<<<<< 7a2b45a66fba8ad1862f25cfa727df03d218ba83 send a link to the user email address defined in the LDAP. Since this is an example, no email will be sent, the link is rather delivered in the file **/tmp/notifications/notification.txt**. Paste the link in your browser and you'll get your secret in QRCode and Base32 format. You can use +======= +send a link to the user email address stored in LDAP. Since this is an example, the email is sent +to a fake email address you can access from the webmail at [http://localhost:8085](http://localhost:8085). +Click on **Continue** and you'll get your secret in QRCode and Base32 formats. You can use +>>>>>>> Add SMTP notifier as an available option in configuration [Google Authenticator] to store them and get the generated tokens with the app. @@ -155,11 +162,19 @@ to store them and get the generated tokens with the app. USB security keys. U2F is one of the most secure authentication protocol and is already available for Google, Facebook, Github accounts and more. +<<<<<<< 7a2b45a66fba8ad1862f25cfa727df03d218ba83 Like TOTP, U2F requires you register a security key before authenticating. To do so, click on the register link. 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 **/tmp/notifications/notification.txt**. Paste the link in your browser and you'll be asking to touch the token of your device +======= +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, the email is sent +to a fake email address you can access from the webmail at [http://localhost:8085](http://localhost:8085). +Click on **Continue** and you'll be asking to touch the token of your device +>>>>>>> Add SMTP notifier as an available option in configuration to register. Upon successful registration, you can authenticate using your U2F device by simply touching the token. Easy, right?! @@ -169,8 +184,8 @@ device by simply touching the token. Easy, right?! 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 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 -**/tmp/notifications/notification.txt**. +email address. For the sake of the example, the email is delivered in a fake webmail deployed +for you and accessible at [http://localhost:8085](http://localhost:8085). Paste the link in your browser and you should be able to reset the password. diff --git a/config.template.yml b/config.template.yml index 09404d09b..85920e8fa 100644 --- a/config.template.yml +++ b/config.template.yml @@ -182,12 +182,15 @@ storage: # 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 + # Use a SMTP server for sending notifications + smtp: + username: test + password: test + secure: false + host: 'smtp' + port: 1025 diff --git a/config.test.yml b/config.test.yml index f297b0252..351a1262f 100644 --- a/config.test.yml +++ b/config.test.yml @@ -162,12 +162,16 @@ storage: # 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 + # Use a SMTP server for sending notifications + smtp: + username: test + password: test + secure: false + host: 'smtp' + port: 1025 + diff --git a/docker-compose.yml b/docker-compose.yml index 82e4e0a27..3c8717131 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,7 +5,6 @@ services: restart: always volumes: - ./config.template.yml:/etc/authelia/config.yml:ro - - /tmp/notifications:/var/lib/authelia/notifications depends_on: - redis networks: diff --git a/example/ldap/base.ldif b/example/ldap/base.ldif index 4c6c33c76..50f65ee98 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: john.doe@example.com +mail: john.doe@authelia.com sn: John Doe userpassword: {SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g= @@ -33,7 +33,7 @@ dn: cn=harry,ou=users,dc=example,dc=com cn: harry objectclass: inetOrgPerson objectclass: top -mail: harry.potter@example.com +mail: harry.potter@authelia.com sn: Harry Potter userpassword: {SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g= @@ -41,7 +41,7 @@ dn: cn=bob,ou=users,dc=example,dc=com cn: bob objectclass: inetOrgPerson objectclass: top -mail: bob.dylan@example.com +mail: bob.dylan@authelia.com sn: Bob Dylan userpassword: {SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g= @@ -49,7 +49,7 @@ dn: cn=james,ou=users,dc=example,dc=com cn: james objectclass: inetOrgPerson objectclass: top -mail: james.dean@example.com +mail: james.dean@authelia.com sn: James Dean userpassword: {SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g= @@ -57,6 +57,6 @@ dn: cn=blackhat,ou=users,dc=example,dc=com cn: blackhat objectclass: inetOrgPerson objectclass: top -mail: billy.blackhat@example.com +mail: billy.blackhat@authelia.com sn: Billy BlackHat userpassword: {SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g= diff --git a/example/ldap/docker-compose.yml b/example/ldap/docker-compose.yml index e991df60b..4d3c6d4c4 100644 --- a/example/ldap/docker-compose.yml +++ b/example/ldap/docker-compose.yml @@ -12,6 +12,7 @@ services: - SLAPD_FORCE_RECONFIGURE=true volumes: - ./example/ldap/base.ldif:/etc/ldap.dist/prepopulate/base.ldif + - ./example/ldap/private.ldif:/etc/ldap.dist/prepopulate/private.ldif - ./example/ldap/access.rules:/etc/ldap.dist/prepopulate/access.rules networks: - example-network diff --git a/example/smtp/docker-compose.yml b/example/smtp/docker-compose.yml new file mode 100644 index 000000000..6fe76e9f3 --- /dev/null +++ b/example/smtp/docker-compose.yml @@ -0,0 +1,8 @@ +version: '2' +services: + smtp: + image: schickling/mailcatcher + ports: + - "8085:1080" + networks: + - example-network diff --git a/package.json b/package.json index 12b899158..2d72df9ef 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "@types/randomstring": "^1.1.5", "@types/redis": "^2.6.0", "@types/request": "0.0.46", + "@types/request-promise": "^4.1.37", "@types/selenium-webdriver": "^3.0.4", "@types/sinon": "^2.2.1", "@types/speakeasy": "^2.0.1", @@ -94,6 +95,7 @@ "proxyquire": "^1.8.0", "query-string": "^4.3.4", "request": "^2.81.0", + "request-promise": "^4.2.2", "selenium-webdriver": "^3.5.0", "should": "^11.1.1", "sinon": "^2.3.8", diff --git a/scripts/dc-dev.sh b/scripts/dc-dev.sh index 416a3ba2c..3d83b914b 100755 --- a/scripts/dc-dev.sh +++ b/scripts/dc-dev.sh @@ -9,5 +9,6 @@ docker-compose \ -f example/mongo/docker-compose.yml \ -f example/redis/docker-compose.yml \ -f example/nginx/docker-compose.yml \ + -f example/smtp/docker-compose.yml \ -f example/ldap/docker-compose.admin.yml \ -f example/ldap/docker-compose.yml $* diff --git a/scripts/example-commit/dc-example.sh b/scripts/example-commit/dc-example.sh index 640976d62..9ede68d99 100755 --- a/scripts/example-commit/dc-example.sh +++ b/scripts/example-commit/dc-example.sh @@ -8,4 +8,5 @@ docker-compose \ -f example/mongo/docker-compose.yml \ -f example/redis/docker-compose.yml \ -f example/nginx/docker-compose.yml \ + -f example/smtp/docker-compose.yml \ -f example/ldap/docker-compose.yml $* diff --git a/scripts/example-commit/deploy-example.sh b/scripts/example-commit/deploy-example.sh index d0cfd5dd9..e5855f1f2 100755 --- a/scripts/example-commit/deploy-example.sh +++ b/scripts/example-commit/deploy-example.sh @@ -3,4 +3,4 @@ DC_SCRIPT=./scripts/example-commit/dc-example.sh $DC_SCRIPT build -$DC_SCRIPT up -d mongo redis openldap authelia nginx +$DC_SCRIPT up -d mongo redis openldap authelia nginx smtp diff --git a/scripts/example-dockerhub/dc-example.sh b/scripts/example-dockerhub/dc-example.sh index b72c6a37e..e73486744 100755 --- a/scripts/example-dockerhub/dc-example.sh +++ b/scripts/example-dockerhub/dc-example.sh @@ -8,4 +8,5 @@ docker-compose \ -f example/mongo/docker-compose.yml \ -f example/redis/docker-compose.yml \ -f example/nginx/docker-compose.yml \ + -f example/smtp/docker-compose.yml \ -f example/ldap/docker-compose.yml $* diff --git a/scripts/example-dockerhub/deploy-example.sh b/scripts/example-dockerhub/deploy-example.sh index 3b71fd1b1..81eddfdbc 100755 --- a/scripts/example-dockerhub/deploy-example.sh +++ b/scripts/example-dockerhub/deploy-example.sh @@ -3,4 +3,4 @@ DC_SCRIPT=./scripts/example-dockerhub/dc-example.sh #$DC_SCRIPT build -$DC_SCRIPT up -d mongo redis openldap authelia nginx +$DC_SCRIPT up -d mongo redis openldap authelia nginx smtp diff --git a/scripts/integration-tests.sh b/scripts/integration-tests.sh index 12ef19ed9..59c9bf15d 100755 --- a/scripts/integration-tests.sh +++ b/scripts/integration-tests.sh @@ -1,10 +1,10 @@ #!/bin/bash DC_SCRIPT=./scripts/example-commit/dc-example.sh -EXPECTED_SERVICES_COUNT=5 +EXPECTED_SERVICES_COUNT=6 start_services() { - $DC_SCRIPT up -d mongo redis openldap authelia nginx + $DC_SCRIPT up -d mongo redis openldap authelia nginx smtp sleep 3 } @@ -29,7 +29,7 @@ expect_services_count() { run_integration_tests() { echo "Start services..." start_services - expect_services_count $EXPECTED_SERVICES_COUNT + expect_services_count $EXPECTED_SERVICES_COUNT sleep 5 ./node_modules/.bin/grunt run:integration-tests @@ -41,13 +41,13 @@ run_other_tests() { npm install --only=dev ./node_modules/.bin/grunt build-dist ./scripts/example-commit/deploy-example.sh - expect_services_count 5 + expect_services_count $EXPECTED_SERVICES_COUNT } run_other_tests_docker() { echo "Test dev docker deployment (commands in README)" ./scripts/example-dockerhub/deploy-example.sh - expect_services_count 5 + expect_services_count $EXPECTED_SERVICES_COUNT } diff --git a/src/server/lib/configuration/Configuration.d.ts b/src/server/lib/configuration/Configuration.d.ts index cdfb029de..7df8c3c50 100644 --- a/src/server/lib/configuration/Configuration.d.ts +++ b/src/server/lib/configuration/Configuration.d.ts @@ -71,12 +71,21 @@ export interface GmailNotifierConfiguration { password: string; } +export interface SmtpNotifierConfiguration { + username: string; + password: string; + host: string; + port: number; + secure: boolean; +} + export interface FileSystemNotifierConfiguration { filename: string; } export interface NotifierConfiguration { gmail?: GmailNotifierConfiguration; + smtp?: SmtpNotifierConfiguration; filesystem?: FileSystemNotifierConfiguration; } diff --git a/src/server/lib/notifiers/AbstractEmailNotifier.ts b/src/server/lib/notifiers/AbstractEmailNotifier.ts new file mode 100644 index 000000000..2a925fd88 --- /dev/null +++ b/src/server/lib/notifiers/AbstractEmailNotifier.ts @@ -0,0 +1,24 @@ + +import { INotifier } from "../notifiers/INotifier"; +import { Identity } from "../../../types/Identity"; + +import Fs = require("fs"); +import Path = require("path"); +import Ejs = require("ejs"); +import BluebirdPromise = require("bluebird"); + +const email_template = Fs.readFileSync(Path.join(__dirname, "../../resources/email-template.ejs"), "UTF-8"); + +export abstract class AbstractEmailNotifier implements INotifier { + + notify(identity: Identity, subject: string, link: string): BluebirdPromise { + const d = { + url: link, + button_title: "Continue", + title: subject + }; + return this.sendEmail(identity.email, subject, Ejs.render(email_template, d)); + } + + abstract sendEmail(email: string, subject: string, content: string): BluebirdPromise; +} \ No newline at end of file diff --git a/src/server/lib/notifiers/GMailNotifier.ts b/src/server/lib/notifiers/GMailNotifier.ts index a601e258e..93b015738 100644 --- a/src/server/lib/notifiers/GMailNotifier.ts +++ b/src/server/lib/notifiers/GMailNotifier.ts @@ -1,21 +1,16 @@ import * as BluebirdPromise from "bluebird"; -import * as fs from "fs"; -import * as ejs from "ejs"; import nodemailer = require("nodemailer"); import { Nodemailer } from "../../../types/Dependencies"; -import { Identity } from "../../../types/Identity"; -import { INotifier } from "../notifiers/INotifier"; +import { AbstractEmailNotifier } from "../notifiers/AbstractEmailNotifier"; import { GmailNotifierConfiguration } from "../configuration/Configuration"; -import path = require("path"); -const email_template = fs.readFileSync(path.join(__dirname, "../../resources/email-template.ejs"), "UTF-8"); - -export class GMailNotifier implements INotifier { +export class GMailNotifier extends AbstractEmailNotifier { private transporter: any; constructor(options: GmailNotifierConfiguration, nodemailer: Nodemailer) { + super(); const transporter = nodemailer.createTransport({ service: "gmail", auth: { @@ -26,18 +21,12 @@ export class GMailNotifier implements INotifier { this.transporter = BluebirdPromise.promisifyAll(transporter); } - notify(identity: Identity, subject: string, link: string): BluebirdPromise { - const d = { - url: link, - button_title: "Continue", - title: subject - }; - + sendEmail(email: string, subject: string, content: string) { const mailOptions = { from: "authelia@authelia.com", - to: identity.email, + to: email, subject: subject, - html: ejs.render(email_template, d) + html: content }; return this.transporter.sendMailAsync(mailOptions); } diff --git a/src/server/lib/notifiers/NotifierFactory.ts b/src/server/lib/notifiers/NotifierFactory.ts index 99d5852d3..b1f202ae8 100644 --- a/src/server/lib/notifiers/NotifierFactory.ts +++ b/src/server/lib/notifiers/NotifierFactory.ts @@ -4,6 +4,7 @@ import { Nodemailer } from "../../../types/Dependencies"; import { INotifier } from "./INotifier"; import { GMailNotifier } from "./GMailNotifier"; +import { SmtpNotifier } from "./SmtpNotifier"; import { FileSystemNotifier } from "./FileSystemNotifier"; export class NotifierFactory { @@ -14,6 +15,12 @@ export class NotifierFactory { else if ("filesystem" in options) { return new FileSystemNotifier(options.filesystem); } + else if ("smtp" in options) { + return new SmtpNotifier(options.smtp, nodemailer); + } + else { + throw new Error("No available notifier option detected."); + } } } diff --git a/src/server/lib/notifiers/SmtpNotifier.ts b/src/server/lib/notifiers/SmtpNotifier.ts new file mode 100644 index 000000000..17612e25f --- /dev/null +++ b/src/server/lib/notifiers/SmtpNotifier.ts @@ -0,0 +1,53 @@ + + +import * as BluebirdPromise from "bluebird"; +import Nodemailer = require("nodemailer"); + +import { AbstractEmailNotifier } from "../notifiers/AbstractEmailNotifier"; +import { SmtpNotifierConfiguration } from "../configuration/Configuration"; + +export class SmtpNotifier extends AbstractEmailNotifier { + private transporter: any; + + constructor(options: SmtpNotifierConfiguration, nodemailer: typeof Nodemailer) { + super(); + const smtpOptions = { + host: options.host, + port: options.port, + secure: options.secure, // upgrade later with STARTTLS + auth: { + user: options.username, + pass: options.password + } + }; + console.log(smtpOptions); + const transporter = nodemailer.createTransport(smtpOptions); + this.transporter = BluebirdPromise.promisifyAll(transporter); + + // verify connection configuration + console.log("Checking SMTP server connection."); + transporter.verify(function (error, success) { + if (error) { + throw new Error("Unable to connect to SMTP server. \ +Please check the service is running and your credentials are correct."); + } else { + console.log("SMTP Server is ready to take our messages"); + } + }); + } + + sendEmail(email: string, subject: string, content: string) { + const mailOptions = { + from: "authelia@authelia.com", + to: email, + subject: subject, + html: content + }; + return this.transporter.sendMail(mailOptions, (error: Error, data: string) => { + if (error) { + return console.log(error); + } + console.log("Message sent: %s", JSON.stringify(data)); + }); + } +} diff --git a/test/features/step_definitions/reset-password.ts b/test/features/step_definitions/reset-password.ts index 45c0a40c1..c27f751af 100644 --- a/test/features/step_definitions/reset-password.ts +++ b/test/features/step_definitions/reset-password.ts @@ -9,12 +9,10 @@ Cucumber.defineSupportCode(function ({ Given, When, Then }) { }); When("I click on the link of the email", function () { - const notif = Fs.readFileSync("/tmp/notifications/notification.txt").toString(); - const regexp = new RegExp(/Link: (.+)/); - const match = regexp.exec(notif); - const link = match[1]; const that = this; - - return this.driver.get(link); + return this.retrieveLatestMail() + .then(function (link: string) { + return that.driver.get(link); + }); }); }); \ No newline at end of file diff --git a/test/features/support/world.ts b/test/features/support/world.ts index 0335b78e0..707317163 100644 --- a/test/features/support/world.ts +++ b/test/features/support/world.ts @@ -4,6 +4,8 @@ import Cucumber = require("cucumber"); import Fs = require("fs"); import Speakeasy = require("speakeasy"); import Assert = require("assert"); +import Request = require("request-promise"); +import BluebirdPromise = require("bluebird"); function CustomWorld() { const that = this; @@ -49,7 +51,7 @@ function CustomWorld() { .click(); }) .then(function () { - return that.driver.sleep(500); + return that.driver.sleep(1000); }); }; @@ -69,18 +71,37 @@ function CustomWorld() { }); }; + this.retrieveLatestMail = function () { + return Request({ + method: "GET", + uri: "http://localhost:8085/messages", + json: true + }) + .then(function (data: any) { + const messageId = data[data.length - 1].id; + return Request({ + method: "GET", + uri: `http://localhost:8085/messages/${messageId}.html` + }); + }) + .then(function (data: any) { + const regexp = new RegExp(/Continue<\/a>/); + const match = regexp.exec(data); + const link = match[1]; + return BluebirdPromise.resolve(link); + }); + }; + this.registerTotpSecret = function (totpSecretHandle: string) { return that.driver.wait(seleniumWebdriver.until.elementLocated(seleniumWebdriver.By.className("register-totp")), 4000) .then(function () { return that.driver.findElement(seleniumWebdriver.By.className("register-totp")).click(); }) .then(function () { - const notif = Fs.readFileSync("/tmp/notifications/notification.txt").toString(); - const regexp = new RegExp(/Link: (.+)/); - const match = regexp.exec(notif); - const link = match[1]; - console.log("Link: " + link); - return that.driver.get(link); + return that.retrieveLatestMail(); + }) + .then(function (url: string) { + return that.driver.get(url); }) .then(function () { return that.driver.wait(seleniumWebdriver.until.elementLocated(seleniumWebdriver.By.id("secret")), 5000); @@ -140,4 +161,4 @@ function CustomWorld() { Cucumber.defineSupportCode(function ({ setWorldConstructor }) { setWorldConstructor(CustomWorld); -}); \ No newline at end of file +});