diff --git a/.gitignore b/.gitignore index 6026d1d63..f9cbd72b5 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,5 @@ dist/ example/ldap/private.ldif package-lock.json + +Configuration.schema.json diff --git a/.npmignore b/.npmignore index f0a6765d2..3aaca70d2 100644 --- a/.npmignore +++ b/.npmignore @@ -1,10 +1,10 @@ -src/ +client/ +server/ test/ doc/ scripts/ images/ example/ -dist/test/ .travis.yml config.test.yml diff --git a/README.md b/README.md index 7049052dd..2554719fd 100644 --- a/README.md +++ b/README.md @@ -24,11 +24,12 @@ used in production to secure internal services in a small docker swarm cluster. 5. [Access control](#access-control) 6. [Basic authentication](#basic-authentication) 7. [Session management with Redis](#session-management-with-redis) -4. [Documentation](#documentation) +4. [Security](#security) +5. [Documentation](#documentation) 1. [Authelia configuration](#authelia-configuration) - 1. [API documentation](#api-documentation) -5. [Contributing to Authelia](#contributing-to-authelia) -6. [License](#license) + 2. [API documentation](#api-documentation) +6. [Contributing to Authelia](#contributing-to-authelia) +7. [License](#license) --- @@ -197,6 +198,29 @@ Please see [config.template.yml] to see an example of configuration. ### Session management with Redis When your users authenticate against Authelia, sessions are stored in a Redis key/value store. You can specify your own Redis instance in [config.template.yml]. +## Security + +### Protection against cookie theft + +Authelia uses two mechanism to protect against cookie theft: +1. session attribute `httpOnly` set to true make client-side code unable to +read the cookie. +2. session attribute `secure` ensure the cookie will never be sent over an +unsecure HTTP connections. + +### Protection against multi-domain cookie attacks + +Since Authelia uses multi-domain cookies to perform single sign-on, an +attacker who poisonned a user's DNS cache can easily retrieve the user's +cookies by making the user send a request to one of the attacker's IPs. + +To mitigate this risk, it's advisable to only use HTTPS connections with valid +certificates and enforce it with HTTP Strict Transport Security ([HSTS]) so +that the attacker must also require the certificate to retrieve the cookies. + +Note that using [HSTS] has consequences. That's why you should read the blog +post nginx has written on [HSTS]. + ## Documentation ### Authelia configuration The configuration of the server is defined in the file @@ -246,4 +270,4 @@ Follow [contributing](CONTRIBUTORS.md) file. [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 [config.template.yml]: https://github.com/clems4ever/authelia/blob/master/config.template.yml - +[HSTS]: https://www.nginx.com/blog/http-strict-transport-security-hsts-and-nginx/ diff --git a/config.template.yml b/config.template.yml index 846f7a8c8..83f1fed72 100644 --- a/config.template.yml +++ b/config.template.yml @@ -55,8 +55,10 @@ ldap: # # Note: by default a domain uses "two_factor" method. # -# Note: 'overriden_methods' is a dictionary where keys must be subdomains and +# Note: 'per_subdomain_methods' is a dictionary where keys must be subdomains and # values must be one of the two possible methods. +# +# Note: 'per_subdomain_methods' is optional. authentication_methods: default_method: two_factor per_subdomain_methods: diff --git a/config.test.yml b/config.test.yml index d2a9b7a46..ca70f5275 100644 --- a/config.test.yml +++ b/config.test.yml @@ -47,6 +47,21 @@ ldap: user: cn=admin,dc=example,dc=com password: password +# Authentication methods +# +# Authentication methods can be defined per subdomain. +# There are currently two available methods: "basic_auth" and "two_factor" +# +# Note: by default a domain uses "two_factor" method. +# +# Note: 'per_subdomain_methods' is a dictionary where keys must be subdomains and +# values must be one of the two possible methods. +# +# Note: 'per_subdomain_methods' is optional. +authentication_methods: + default_method: two_factor + per_subdomain_methods: + basicauth.test.local: basic_auth # Access Control # diff --git a/docker-compose.yml b/docker-compose.yml index 3c8717131..a76ee46ea 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,6 +5,8 @@ services: restart: always volumes: - ./config.template.yml:/etc/authelia/config.yml:ro + environment: + - NODE_TLS_REJECT_UNAUTHORIZED=0 depends_on: - redis networks: diff --git a/example/authelia/docker-compose.yml b/example/authelia/docker-compose.yml index 9f1f1cb5d..7c68dec9a 100644 --- a/example/authelia/docker-compose.yml +++ b/example/authelia/docker-compose.yml @@ -6,6 +6,8 @@ services: volumes: - ./config.template.yml:/etc/authelia/config.yml:ro - ./notifications:/var/lib/authelia/notifications + environment: + - NODE_TLS_REJECT_UNAUTHORIZED=0 depends_on: - redis networks: diff --git a/example/nginx/nginx.conf b/example/nginx/nginx.conf index a1ec3dbc2..0db8f9d52 100644 --- a/example/nginx/nginx.conf +++ b/example/nginx/nginx.conf @@ -30,11 +30,14 @@ http { ssl_certificate /etc/ssl/server.crt; ssl_certificate_key /etc/ssl/server.key; + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + add_header X-Frame-Options "SAMEORIGIN"; location / { proxy_set_header X-Original-URI $request_uri; proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; proxy_pass http://authelia/; @@ -57,6 +60,9 @@ http { ssl on; ssl_certificate /etc/ssl/server.crt; ssl_certificate_key /etc/ssl/server.key; + + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + add_header X-Frame-Options "SAMEORIGIN"; } server { @@ -69,10 +75,14 @@ http { ssl_certificate /etc/ssl/server.crt; ssl_certificate_key /etc/ssl/server.key; + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + add_header X-Frame-Options "SAMEORIGIN"; + location /auth_verify { internal; proxy_set_header X-Original-URI $request_uri; proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Host $http_host; proxy_set_header Content-Length ""; @@ -122,10 +132,14 @@ http { ssl_certificate /etc/ssl/server.crt; ssl_certificate_key /etc/ssl/server.key; + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + add_header X-Frame-Options "SAMEORIGIN"; + location /auth_verify { internal; proxy_set_header X-Original-URI $request_uri; proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Host $http_host; proxy_set_header Content-Length ""; @@ -158,10 +172,14 @@ http { ssl_certificate /etc/ssl/server.crt; ssl_certificate_key /etc/ssl/server.key; + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + add_header X-Frame-Options "SAMEORIGIN"; + location /auth_verify { internal; proxy_set_header X-Original-URI $request_uri; proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Host $http_host; proxy_set_header Content-Length ""; @@ -194,10 +212,14 @@ http { ssl_certificate /etc/ssl/server.crt; ssl_certificate_key /etc/ssl/server.key; + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + add_header X-Frame-Options "SAMEORIGIN"; + location /auth_verify { internal; proxy_set_header X-Original-URI $request_uri; proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Host $http_host; proxy_set_header Content-Length ""; @@ -230,10 +252,14 @@ http { ssl_certificate /etc/ssl/server.crt; ssl_certificate_key /etc/ssl/server.key; + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + add_header X-Frame-Options "SAMEORIGIN"; + location /auth_verify { internal; proxy_set_header X-Original-URI $request_uri; proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Host $http_host; proxy_set_header Content-Length ""; diff --git a/scripts/docker-publish.sh b/scripts/docker-publish.sh index 3d9d84e0e..0e6bb76b6 100755 --- a/scripts/docker-publish.sh +++ b/scripts/docker-publish.sh @@ -31,6 +31,9 @@ function deploy_on_dockerhub { if [ "$TRAVIS_BRANCH" == "master" ]; then login_to_dockerhub deploy_on_dockerhub master +elif [ "$TRAVIS_BRANCH" == "develop" ]; then + login_to_dockerhub + deploy_on_dockerhub develop elif [ ! -z "$TRAVIS_TAG" ]; then login_to_dockerhub deploy_on_dockerhub $TRAVIS_TAG diff --git a/scripts/travis.sh b/scripts/travis.sh index 97e2c5454..332b24501 100755 --- a/scripts/travis.sh +++ b/scripts/travis.sh @@ -5,6 +5,8 @@ set -e docker --version docker-compose --version +grunt run:generate-config-schema + # Run unit tests grunt test-unit diff --git a/server/src/index.ts b/server/src/index.ts index b5a20ac52..429cc8578 100755 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -1,7 +1,5 @@ #! /usr/bin/env node -process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; - import Server from "./lib/Server"; import { GlobalDependencies } from "../types/Dependencies"; import YAML = require("yamljs"); diff --git a/server/src/lib/Server.ts b/server/src/lib/Server.ts index 5d80e3322..f043d4f33 100644 --- a/server/src/lib/Server.ts +++ b/server/src/lib/Server.ts @@ -23,8 +23,8 @@ import * as http from "http"; const addRequestId = require("express-request-id")(); // Constants - const TRUST_PROXY = "trust proxy"; +const X_POWERED_BY = "x-powered-by"; const VIEWS = "views"; const VIEW_ENGINE = "view engine"; const PUG = "pug"; @@ -54,9 +54,9 @@ export default class Server { app.use(BodyParser.json()); app.use(deps.session(expressSessionOptions)); app.use(addRequestId); - app.disable("x-powered-by"); + app.disable(X_POWERED_BY); + app.enable(TRUST_PROXY); - app.set(TRUST_PROXY, 1); app.set(VIEWS, viewsDirectory); app.set(VIEW_ENGINE, PUG); diff --git a/server/src/lib/configuration/Configuration.d.ts b/server/src/lib/configuration/Configuration.d.ts index 802572b2f..199a84bc0 100644 --- a/server/src/lib/configuration/Configuration.d.ts +++ b/server/src/lib/configuration/Configuration.d.ts @@ -73,8 +73,8 @@ export interface GmailNotifierConfiguration { } export interface SmtpNotifierConfiguration { - username: string; - password: string; + username?: string; + password?: string; host: string; port: number; secure: boolean; @@ -116,7 +116,7 @@ declare type AuthenticationMethodPerSubdomain = { [subdomain: string]: Authentic export interface AuthenticationMethodsConfiguration { default_method: AuthenticationMethod; - per_subdomain_methods: AuthenticationMethodPerSubdomain; + per_subdomain_methods?: AuthenticationMethodPerSubdomain; } export interface UserConfiguration { diff --git a/server/src/lib/configuration/Configuration.schema.json b/server/src/lib/configuration/Configuration.schema.json deleted file mode 100644 index 9fd4e9238..000000000 --- a/server/src/lib/configuration/Configuration.schema.json +++ /dev/null @@ -1,368 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - "definitions": { - "ACLConfiguration": { - "properties": { - "any": { - "items": { - "properties": { - "domain": { - "type": "string" - }, - "policy": { - "$ref": "#/definitions/ACLPolicy" - }, - "resources": { - "items": { - "type": "string" - }, - "type": "array" - } - }, - "required": [ - "domain", - "policy" - ], - "type": "object" - }, - "type": "array" - }, - "default_policy": { - "$ref": "#/definitions/ACLPolicy" - }, - "groups": { - "additionalProperties": { - "items": { - "properties": { - "domain": { - "type": "string" - }, - "policy": { - "$ref": "#/definitions/ACLPolicy" - }, - "resources": { - "items": { - "type": "string" - }, - "type": "array" - } - }, - "required": [ - "domain", - "policy" - ], - "type": "object" - }, - "type": "array" - }, - "type": "object" - }, - "users": { - "additionalProperties": { - "items": { - "properties": { - "domain": { - "type": "string" - }, - "policy": { - "$ref": "#/definitions/ACLPolicy" - }, - "resources": { - "items": { - "type": "string" - }, - "type": "array" - } - }, - "required": [ - "domain", - "policy" - ], - "type": "object" - }, - "type": "array" - }, - "type": "object" - } - }, - "type": "object" - }, - "ACLPolicy": { - "enum": [ - "allow", - "deny" - ], - "type": "string" - }, - "AuthenticationMethod": { - "enum": [ - "basic_auth", - "two_factor" - ], - "type": "string" - }, - "AuthenticationMethodsConfiguration": { - "properties": { - "default_method": { - "$ref": "#/definitions/AuthenticationMethod" - }, - "per_subdomain_methods": { - "additionalProperties": { - "enum": [ - "basic_auth", - "two_factor" - ], - "type": "string" - }, - "type": "object" - } - }, - "required": [ - "default_method", - "per_subdomain_methods" - ], - "type": "object" - }, - "FileSystemNotifierConfiguration": { - "properties": { - "filename": { - "type": "string" - } - }, - "required": [ - "filename" - ], - "type": "object" - }, - "GmailNotifierConfiguration": { - "properties": { - "password": { - "type": "string" - }, - "sender": { - "type": "string" - }, - "username": { - "type": "string" - } - }, - "required": [ - "password", - "sender", - "username" - ], - "type": "object" - }, - "LocalStorageConfiguration": { - "properties": { - "in_memory": { - "type": "boolean" - }, - "path": { - "type": "string" - } - }, - "type": "object" - }, - "MongoStorageConfiguration": { - "properties": { - "url": { - "type": "string" - } - }, - "required": [ - "url" - ], - "type": "object" - }, - "NotifierConfiguration": { - "properties": { - "filesystem": { - "$ref": "#/definitions/FileSystemNotifierConfiguration" - }, - "gmail": { - "$ref": "#/definitions/GmailNotifierConfiguration" - }, - "smtp": { - "$ref": "#/definitions/SmtpNotifierConfiguration" - } - }, - "type": "object" - }, - "RegulationConfiguration": { - "properties": { - "ban_time": { - "type": "number" - }, - "find_time": { - "type": "number" - }, - "max_retries": { - "type": "number" - } - }, - "required": [ - "ban_time", - "find_time", - "max_retries" - ], - "type": "object" - }, - "SessionCookieConfiguration": { - "properties": { - "domain": { - "type": "string" - }, - "expiration": { - "type": "number" - }, - "redis": { - "$ref": "#/definitions/SessionRedisOptions" - }, - "secret": { - "type": "string" - } - }, - "required": [ - "secret" - ], - "type": "object" - }, - "SessionRedisOptions": { - "properties": { - "host": { - "type": "string" - }, - "port": { - "type": "number" - } - }, - "required": [ - "host", - "port" - ], - "type": "object" - }, - "SmtpNotifierConfiguration": { - "properties": { - "host": { - "type": "string" - }, - "password": { - "type": "string" - }, - "port": { - "type": "number" - }, - "secure": { - "type": "boolean" - }, - "sender": { - "type": "string" - }, - "username": { - "type": "string" - } - }, - "required": [ - "host", - "password", - "port", - "secure", - "sender", - "username" - ], - "type": "object" - }, - "StorageConfiguration": { - "properties": { - "local": { - "$ref": "#/definitions/LocalStorageConfiguration" - }, - "mongo": { - "$ref": "#/definitions/MongoStorageConfiguration" - } - }, - "type": "object" - }, - "UserLdapConfiguration": { - "properties": { - "additional_groups_dn": { - "type": "string" - }, - "additional_users_dn": { - "type": "string" - }, - "base_dn": { - "type": "string" - }, - "group_name_attribute": { - "type": "string" - }, - "groups_filter": { - "type": "string" - }, - "mail_attribute": { - "type": "string" - }, - "password": { - "type": "string" - }, - "url": { - "type": "string" - }, - "user": { - "type": "string" - }, - "users_filter": { - "type": "string" - } - }, - "required": [ - "base_dn", - "password", - "url", - "user" - ], - "type": "object" - } - }, - "properties": { - "access_control": { - "$ref": "#/definitions/ACLConfiguration" - }, - "authentication_methods": { - "$ref": "#/definitions/AuthenticationMethodsConfiguration" - }, - "ldap": { - "$ref": "#/definitions/UserLdapConfiguration" - }, - "logs_level": { - "type": "string" - }, - "notifier": { - "$ref": "#/definitions/NotifierConfiguration" - }, - "port": { - "type": "number" - }, - "regulation": { - "$ref": "#/definitions/RegulationConfiguration" - }, - "session": { - "$ref": "#/definitions/SessionCookieConfiguration" - }, - "storage": { - "$ref": "#/definitions/StorageConfiguration" - } - }, - "required": [ - "ldap", - "notifier", - "regulation", - "session", - "storage" - ], - "type": "object" -} - diff --git a/server/src/lib/configuration/SessionConfigurationBuilder.ts b/server/src/lib/configuration/SessionConfigurationBuilder.ts index 3560cbb2b..bee21c764 100644 --- a/server/src/lib/configuration/SessionConfigurationBuilder.ts +++ b/server/src/lib/configuration/SessionConfigurationBuilder.ts @@ -12,7 +12,8 @@ export class SessionConfigurationBuilder { resave: false, saveUninitialized: true, cookie: { - secure: false, + secure: true, + httpOnly: true, maxAge: configuration.session.expiration, domain: configuration.session.domain }, diff --git a/server/src/lib/notifiers/MailSenderBuilder.ts b/server/src/lib/notifiers/MailSenderBuilder.ts index 3b2a1ba90..1b44f7b4c 100644 --- a/server/src/lib/notifiers/MailSenderBuilder.ts +++ b/server/src/lib/notifiers/MailSenderBuilder.ts @@ -28,11 +28,15 @@ export class MailSenderBuilder implements IMailSenderBuilder { host: options.host, port: options.port, secure: options.secure, // upgrade later with STARTTLS - auth: { + }; + + if (options.username && options.password) { + smtpOptions.auth = { user: options.username, pass: options.password - } - }; + }; + } + return new MailSender(smtpOptions, this.nodemailer); } } \ No newline at end of file diff --git a/server/src/lib/routes/loggedin/get.ts b/server/src/lib/routes/loggedin/get.ts index 0a9910a92..0585d6604 100644 --- a/server/src/lib/routes/loggedin/get.ts +++ b/server/src/lib/routes/loggedin/get.ts @@ -1,8 +1,17 @@ import Express = require("express"); import Endpoints = require("../../../../../shared/api"); +import FirstFactorBlocker from "../FirstFactorBlocker"; +import BluebirdPromise = require("bluebird"); +import AuthenticationSession = require("../../AuthenticationSession"); -export default function(req: Express.Request, res: Express.Response) { - res.render("already-logged-in", { - logout_endpoint: Endpoints.LOGOUT_GET - }); -} \ No newline at end of file +export default FirstFactorBlocker(handler); + +function handler(req: Express.Request, res: Express.Response): BluebirdPromise { + return AuthenticationSession.get(req) + .then(function (authSession) { + res.render("already-logged-in", { + logout_endpoint: Endpoints.LOGOUT_GET, + username: authSession.userid + }); + }); +} diff --git a/server/src/views/already-logged-in.pug b/server/src/views/already-logged-in.pug index b6795e0bc..40920e08b 100644 --- a/server/src/views/already-logged-in.pug +++ b/server/src/views/already-logged-in.pug @@ -5,5 +5,5 @@ block form-header block content -

You are already logged in.
+

You are already logged in as #{ username }.
| Click here to log off.

diff --git a/server/test/SessionConfigurationBuilder.test.ts b/server/test/SessionConfigurationBuilder.test.ts index bae332347..c5a8cd91d 100644 --- a/server/test/SessionConfigurationBuilder.test.ts +++ b/server/test/SessionConfigurationBuilder.test.ts @@ -73,7 +73,8 @@ describe("test session configuration builder", function () { resave: false, saveUninitialized: true, cookie: { - secure: false, + secure: true, + httpOnly: true, maxAge: 3600, domain: "example.com" } @@ -153,7 +154,8 @@ describe("test session configuration builder", function () { resave: false, saveUninitialized: true, cookie: { - secure: false, + secure: true, + httpOnly: true, maxAge: 3600, domain: "example.com" }, diff --git a/server/test/notifiers/MailSenderBuilder.test.ts b/server/test/notifiers/MailSenderBuilder.test.ts index f0d769116..6bdc0f5fc 100644 --- a/server/test/notifiers/MailSenderBuilder.test.ts +++ b/server/test/notifiers/MailSenderBuilder.test.ts @@ -25,24 +25,41 @@ describe("test MailSenderBuilder", function() { Assert.equal(createTransportStub.getCall(0).args[0].auth.pass, "pass_gmail"); }); - it("should create a smtp mail sender", function() { - const mailSenderBuilder = new MailSenderBuilder(Nodemailer); - mailSenderBuilder.buildSmtp({ - host: "mail.example.com", - password: "password", - port: 25, - secure: true, - username: "user", - sender: "admin@example.com" + describe("build smtp mail sender", function() { + it("should create a smtp mail sender with authenticated user", function() { + const mailSenderBuilder = new MailSenderBuilder(Nodemailer); + mailSenderBuilder.buildSmtp({ + host: "mail.example.com", + password: "password", + port: 25, + secure: true, + username: "user", + sender: "admin@example.com" + }); + Assert.deepStrictEqual(createTransportStub.getCall(0).args[0], { + host: "mail.example.com", + auth: { + pass: "password", + user: "user" + }, + port: 25, + secure: true, + }); }); - Assert.deepStrictEqual(createTransportStub.getCall(0).args[0], { - host: "mail.example.com", - auth: { - pass: "password", - user: "user" - }, - port: 25, - secure: true, + + it("should create a smtp mail sender with anonymous user", function() { + const mailSenderBuilder = new MailSenderBuilder(Nodemailer); + mailSenderBuilder.buildSmtp({ + host: "mail.example.com", + port: 25, + secure: true, + sender: "admin@example.com" + }); + Assert.deepStrictEqual(createTransportStub.getCall(0).args[0], { + host: "mail.example.com", + port: 25, + secure: true, + }); }); }); }); \ No newline at end of file diff --git a/server/test/server/PrivatePages.ts b/server/test/server/PrivatePages.ts index 35cf758c6..8fd9f698a 100644 --- a/server/test/server/PrivatePages.ts +++ b/server/test/server/PrivatePages.ts @@ -173,6 +173,10 @@ describe("Private pages of the server must not be accessible without session", f it("should block " + Endpoints.SECOND_FACTOR_TOTP_POST, function () { return should_post_and_reply_with_401(BASE_URL + Endpoints.SECOND_FACTOR_TOTP_POST); }); + + it("should block " + Endpoints.LOGGED_IN, function () { + return should_get_and_reply_with_401(BASE_URL + Endpoints.LOGGED_IN); + }); }); });