You will soon receive a push notification on your phone.
+
+
+
+ {(u2fStatus == Status.FAILURE)
+ ?
+
+
+ : null}
+
+ )
+ }
+}
\ No newline at end of file
diff --git a/client/src/components/SecondFactorForm/SecondFactorForm.tsx b/client/src/components/SecondFactorForm/SecondFactorForm.tsx
index fef245ce..111d6021 100644
--- a/client/src/components/SecondFactorForm/SecondFactorForm.tsx
+++ b/client/src/components/SecondFactorForm/SecondFactorForm.tsx
@@ -5,6 +5,7 @@ import SecondFactorTOTP from '../../containers/components/SecondFactorTOTP/Secon
import SecondFactorU2F from '../../containers/components/SecondFactorU2F/SecondFactorU2F';
import { Button } from '@material/react-button';
import classnames from 'classnames';
+import SecondFactorDuoPush from '../../containers/components/SecondFactorDuoPush/SecondFactorDuoPush';
export interface OwnProps {
username: string;
@@ -21,6 +22,7 @@ export interface DispatchProps {
onLogoutClicked: () => void;
onOneTimePasswordMethodClicked: () => void;
onSecurityKeyMethodClicked: () => void;
+ onDuoPushMethodClicked: () => void;
onUseAnotherMethodClicked: () => void;
}
@@ -37,6 +39,9 @@ class SecondFactorForm extends Component {
if (method == 'u2f') {
title = "Security Key";
methodComponent = ();
+ } else if (method == "duo_push") {
+ title = "Duo Push Notification";
+ methodComponent = ();
} else {
title = "One-Time Password"
methodComponent = ();
@@ -57,6 +62,7 @@ class SecondFactorForm extends Component {
+
);
diff --git a/client/src/containers/components/SecondFactorDuoPush/SecondFactorDuoPush.ts b/client/src/containers/components/SecondFactorDuoPush/SecondFactorDuoPush.ts
new file mode 100644
index 00000000..2f23d23e
--- /dev/null
+++ b/client/src/containers/components/SecondFactorDuoPush/SecondFactorDuoPush.ts
@@ -0,0 +1,55 @@
+import { connect } from 'react-redux';
+import { RootState } from '../../../reducers';
+import { Dispatch } from 'redux';
+import SecondFactorDuoPush, { StateProps, OwnProps, DispatchProps } from '../../../components/SecondFactorDuoPush/SecondFactorDuoPush';
+import FetchStateBehavior from '../../../behaviors/FetchStateBehavior';
+import TriggerDuoPushAuth from '../../../behaviors/TriggerDuoPushAuth';
+
+
+const mapStateToProps = (state: RootState): StateProps => ({
+ duoPushVerified: state.secondFactor.duoPushVerificationSuccess,
+ duoPushError: state.secondFactor.duoPushVerificationError,
+});
+
+async function redirectIfPossible(body: any) {
+ if ('redirect' in body) {
+ window.location.href = body['redirect'];
+ return true;
+ }
+ return false;
+}
+
+async function handleSuccess(dispatch: Dispatch, res: Response, duration?: number) {
+ async function handle() {
+ const redirected = await redirectIfPossible(res);
+ if (!redirected) {
+ await FetchStateBehavior(dispatch);
+ }
+ }
+
+ if (duration) {
+ setTimeout(handle, duration);
+ } else {
+ await handle();
+ }
+}
+
+async function triggerDuoPushAuth(dispatch: Dispatch, redirectionUrl: string | null) {
+ const res = await TriggerDuoPushAuth(dispatch, redirectionUrl);
+ if (!res) return;
+ await handleSuccess(dispatch, res, 2000);
+}
+
+const mapDispatchToProps = (dispatch: Dispatch, ownProps: OwnProps): DispatchProps => {
+ return {
+ onInit: async () => {
+ await triggerDuoPushAuth(dispatch, ownProps.redirectionUrl);
+ },
+ onRetryClicked: async () => {
+ await triggerDuoPushAuth(dispatch, ownProps.redirectionUrl);
+ }
+ }
+}
+
+
+export default connect(mapStateToProps, mapDispatchToProps)(SecondFactorDuoPush);
\ No newline at end of file
diff --git a/client/src/containers/components/SecondFactorForm/SecondFactorForm.ts b/client/src/containers/components/SecondFactorForm/SecondFactorForm.ts
index 66b1e5ea..01dde789 100644
--- a/client/src/containers/components/SecondFactorForm/SecondFactorForm.ts
+++ b/client/src/containers/components/SecondFactorForm/SecondFactorForm.ts
@@ -31,6 +31,7 @@ const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => {
onLogoutClicked: () => LogoutBehavior(dispatch),
onOneTimePasswordMethodClicked: () => storeMethod(dispatch, 'totp'),
onSecurityKeyMethodClicked: () => storeMethod(dispatch, 'u2f'),
+ onDuoPushMethodClicked: () => storeMethod(dispatch, "duo_push"),
onUseAnotherMethodClicked: () => dispatch(setUseAnotherMethod(true)),
}
}
diff --git a/client/src/reducers/Portal/SecondFactor/actions.ts b/client/src/reducers/Portal/SecondFactor/actions.ts
index eaeae9cf..d7c23576 100644
--- a/client/src/reducers/Portal/SecondFactor/actions.ts
+++ b/client/src/reducers/Portal/SecondFactor/actions.ts
@@ -16,7 +16,10 @@ import {
SET_PREFERED_METHOD,
SET_PREFERED_METHOD_FAILURE,
SET_PREFERED_METHOD_SUCCESS,
- SET_USE_ANOTHER_METHOD
+ SET_USE_ANOTHER_METHOD,
+ TRIGGER_DUO_PUSH_AUTH,
+ TRIGGER_DUO_PUSH_AUTH_SUCCESS,
+ TRIGGER_DUO_PUSH_AUTH_FAILURE
} from "../../constants";
import Method2FA from "../../../types/Method2FA";
@@ -54,6 +57,11 @@ export const oneTimePasswordVerificationFailure = createAction(ONE_TIME_PASSWORD
return (err: string) => resolve(err);
});
+export const triggerDuoPushAuth = createAction(TRIGGER_DUO_PUSH_AUTH);
+export const triggerDuoPushAuthSuccess = createAction(TRIGGER_DUO_PUSH_AUTH_SUCCESS);
+export const triggerDuoPushAuthFailure = createAction(TRIGGER_DUO_PUSH_AUTH_FAILURE, resolve => {
+ return (err: string) => resolve(err);
+});
export const logout = createAction(LOGOUT_REQUEST);
export const logoutSuccess = createAction(LOGOUT_SUCCESS);
diff --git a/client/src/reducers/Portal/SecondFactor/reducer.ts b/client/src/reducers/Portal/SecondFactor/reducer.ts
index e12608e4..cf6605e2 100644
--- a/client/src/reducers/Portal/SecondFactor/reducer.ts
+++ b/client/src/reducers/Portal/SecondFactor/reducer.ts
@@ -27,6 +27,10 @@ interface SecondFactorState {
oneTimePasswordVerificationLoading: boolean,
oneTimePasswordVerificationSuccess: boolean | null,
oneTimePasswordVerificationError: string | null,
+
+ duoPushVerificationLoading: boolean;
+ duoPushVerificationSuccess: boolean | null;
+ duoPushVerificationError: string | null;
}
const secondFactorInitialState: SecondFactorState = {
@@ -51,6 +55,10 @@ const secondFactorInitialState: SecondFactorState = {
oneTimePasswordVerificationLoading: false,
oneTimePasswordVerificationError: null,
oneTimePasswordVerificationSuccess: null,
+
+ duoPushVerificationLoading: false,
+ duoPushVerificationSuccess: null,
+ duoPushVerificationError: null,
}
export type PortalState = StateType;
@@ -163,6 +171,25 @@ export default (state = secondFactorInitialState, action: SecondFactorAction): S
...state,
userAnotherMethod: action.payload,
}
+ case getType(Actions.triggerDuoPushAuth):
+ return {
+ ...state,
+ duoPushVerificationLoading: true,
+ duoPushVerificationError: null,
+ duoPushVerificationSuccess: null,
+ }
+ case getType(Actions.triggerDuoPushAuthSuccess):
+ return {
+ ...state,
+ duoPushVerificationLoading: false,
+ duoPushVerificationSuccess: true,
+ }
+ case getType(Actions.triggerDuoPushAuthFailure):
+ return {
+ ...state,
+ duoPushVerificationLoading: false,
+ duoPushVerificationError: action.payload,
+ }
}
return state;
}
\ No newline at end of file
diff --git a/client/src/reducers/constants.ts b/client/src/reducers/constants.ts
index abbd5f05..9332d4ee 100644
--- a/client/src/reducers/constants.ts
+++ b/client/src/reducers/constants.ts
@@ -28,6 +28,10 @@ export const ONE_TIME_PASSWORD_VERIFICATION_REQUEST = '@portal/second_factor/one
export const ONE_TIME_PASSWORD_VERIFICATION_SUCCESS = '@portal/second_factor/one_time_password_verification_success';
export const ONE_TIME_PASSWORD_VERIFICATION_FAILURE = '@portal/second_factor/one_time_password_verification_failure';
+export const TRIGGER_DUO_PUSH_AUTH = '@portal/second_factor/trigger_duo_push_auth_request';
+export const TRIGGER_DUO_PUSH_AUTH_SUCCESS = '@portal/second_factor/trigger_duo_push_auth_request_success';
+export const TRIGGER_DUO_PUSH_AUTH_FAILURE = '@portal/second_factor/trigger_duo_push_auth_request_failure';
+
export const LOGOUT_REQUEST = '@portal/logout_request';
export const LOGOUT_SUCCESS = '@portal/logout_success';
export const LOGOUT_FAILURE = '@portal/logout_failure';
diff --git a/client/src/services/AutheliaService.ts b/client/src/services/AutheliaService.ts
index a0c5d1f5..cf5b9b07 100644
--- a/client/src/services/AutheliaService.ts
+++ b/client/src/services/AutheliaService.ts
@@ -113,6 +113,21 @@ class AutheliaService {
})
}
+ static async triggerDuoPush(redirectionUrl: string | null): Promise {
+
+ const headers: Record = {
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json',
+ }
+ if (redirectionUrl) {
+ headers['X-Target-Url'] = redirectionUrl;
+ }
+ return this.fetchSafe('/api/duo-push', {
+ method: 'POST',
+ headers: headers,
+ })
+ }
+
static async initiatePasswordResetIdentityValidation(username: string) {
return this.fetchSafe('/api/password-reset/identity/start', {
method: 'POST',
diff --git a/client/src/types/Method2FA.ts b/client/src/types/Method2FA.ts
index 09e0b3c4..c46b4b27 100644
--- a/client/src/types/Method2FA.ts
+++ b/client/src/types/Method2FA.ts
@@ -1,4 +1,4 @@
-type Method2FA = "u2f" | "totp";
+type Method2FA = "u2f" | "totp" | "duo_push";
export default Method2FA;
\ No newline at end of file
diff --git a/config.template.yml b/config.template.yml
index a29682b8..0587fa53 100644
--- a/config.template.yml
+++ b/config.template.yml
@@ -29,6 +29,15 @@ default_redirection_url: https://home.example.com:8080/
totp:
issuer: authelia.com
+# Duo Push API
+#
+# Parameters used to contact the Duo API. Those are generated when you protect an application
+# of type "Partner Auth API" in the management panel.
+duo_api:
+ hostname: api-123456789.duosecurity.com
+ integration_key: ABCDEF
+ secret_key: 1234567890abcdefghifjkl
+
# The authentication backend to use for verifying user passwords
# and retrieve information such as email address and groups
# users belong to.
diff --git a/example/compose/duo-api/Dockerfile b/example/compose/duo-api/Dockerfile
new file mode 100644
index 00000000..fc70d55d
--- /dev/null
+++ b/example/compose/duo-api/Dockerfile
@@ -0,0 +1,12 @@
+FROM node:8.7.0-alpine
+
+WORKDIR /usr/app/src
+
+ADD package.json package.json
+RUN npm install --production --quiet
+
+ADD duo_api.js duo_api.js
+
+EXPOSE 3000
+
+CMD ["node", "duo_api.js"]
\ No newline at end of file
diff --git a/example/compose/duo-api/docker-compose.yml b/example/compose/duo-api/docker-compose.yml
new file mode 100644
index 00000000..0932084a
--- /dev/null
+++ b/example/compose/duo-api/docker-compose.yml
@@ -0,0 +1,6 @@
+version: '2'
+services:
+ duo-api:
+ image: authelia-duo-api
+ networks:
+ - authelianet
diff --git a/example/compose/duo-api/duo_api.js b/example/compose/duo-api/duo_api.js
new file mode 100644
index 00000000..5181f5c7
--- /dev/null
+++ b/example/compose/duo-api/duo_api.js
@@ -0,0 +1,66 @@
+/*
+ * This is a script to fake the Duo API for push notifications.
+ *
+ * Access is allowed by default but one can change the behavior at runtime
+ * by POSTing to /allow or /deny. Then the /auth/v2/auth endpoint will act
+ * accordingly.
+ */
+
+const express = require("express");
+const app = express();
+const port = 3000;
+
+app.set('trust proxy', true);
+
+let permission = 'allow';
+
+app.post('/allow', (req, res) => {
+ permission = 'allow';
+ res.send('ALLOWED');
+});
+
+app.post('/deny', (req, res) => {
+ permission = 'deny';
+ res.send('DENIED');
+});
+
+app.post('/auth/v2/auth', (req, res) => {
+ let response;
+ if (permission == 'allow') {
+ response = {
+ response: {
+ result: 'allow',
+ status: 'allow',
+ status_msg: 'The user allowed access.',
+ },
+ stat: 'OK',
+ };
+ } else {
+ response = {
+ response: {
+ result: 'deny',
+ status: 'deny',
+ status_msg: 'The user denied access.',
+ },
+ stat: 'OK',
+ };
+ }
+ setTimeout(() => res.json(response), 2000);
+});
+
+app.listen(port, () => console.log(`Duo API listening on port ${port}!`));
+
+// The signals we want to handle
+// NOTE: although it is tempting, the SIGKILL signal (9) cannot be intercepted and handled
+var signals = {
+ 'SIGHUP': 1,
+ 'SIGINT': 2,
+ 'SIGTERM': 15
+};
+// Create a listener for each of the signals that we want to handle
+Object.keys(signals).forEach((signal) => {
+ process.on(signal, () => {
+ console.log(`process received a ${signal} signal`);
+ process.exit(128 + signals[signal]);
+ });
+});
\ No newline at end of file
diff --git a/example/compose/duo-api/duo_client.js b/example/compose/duo-api/duo_client.js
new file mode 100644
index 00000000..ee6b2a11
--- /dev/null
+++ b/example/compose/duo-api/duo_client.js
@@ -0,0 +1,10 @@
+/*
+ * This is just client script to test the fake API.
+ */
+
+const DuoApi = require("@duosecurity/duo_api");
+
+process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = 0;
+
+const client = new DuoApi.Client("ABCDEFG", "SECRET", "duo.example.com");
+client.jsonApiCall("POST", "/auth/v2/auth", { username: 'john', factor: "push", device: "auto" }, console.log);
\ No newline at end of file
diff --git a/example/compose/duo-api/package-lock.json b/example/compose/duo-api/package-lock.json
new file mode 100644
index 00000000..06cb0628
--- /dev/null
+++ b/example/compose/duo-api/package-lock.json
@@ -0,0 +1,358 @@
+{
+ "name": "duo-api",
+ "version": "1.0.0",
+ "lockfileVersion": 1,
+ "requires": true,
+ "dependencies": {
+ "accepts": {
+ "version": "1.3.5",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz",
+ "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=",
+ "requires": {
+ "mime-types": "2.1.22",
+ "negotiator": "0.6.1"
+ }
+ },
+ "array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
+ },
+ "body-parser": {
+ "version": "1.18.3",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz",
+ "integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=",
+ "requires": {
+ "bytes": "3.0.0",
+ "content-type": "1.0.4",
+ "debug": "2.6.9",
+ "depd": "1.1.2",
+ "http-errors": "1.6.3",
+ "iconv-lite": "0.4.23",
+ "on-finished": "2.3.0",
+ "qs": "6.5.2",
+ "raw-body": "2.3.3",
+ "type-is": "1.6.16"
+ }
+ },
+ "bytes": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
+ "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg="
+ },
+ "content-disposition": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz",
+ "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ="
+ },
+ "content-type": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
+ "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="
+ },
+ "cookie": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
+ "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s="
+ },
+ "cookie-signature": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+ "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
+ },
+ "debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "requires": {
+ "ms": "2.0.0"
+ }
+ },
+ "depd": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
+ "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
+ },
+ "destroy": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
+ "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
+ },
+ "ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
+ },
+ "encodeurl": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+ "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
+ },
+ "escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
+ },
+ "etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
+ },
+ "express": {
+ "version": "4.16.4",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz",
+ "integrity": "sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg==",
+ "requires": {
+ "accepts": "1.3.5",
+ "array-flatten": "1.1.1",
+ "body-parser": "1.18.3",
+ "content-disposition": "0.5.2",
+ "content-type": "1.0.4",
+ "cookie": "0.3.1",
+ "cookie-signature": "1.0.6",
+ "debug": "2.6.9",
+ "depd": "1.1.2",
+ "encodeurl": "1.0.2",
+ "escape-html": "1.0.3",
+ "etag": "1.8.1",
+ "finalhandler": "1.1.1",
+ "fresh": "0.5.2",
+ "merge-descriptors": "1.0.1",
+ "methods": "1.1.2",
+ "on-finished": "2.3.0",
+ "parseurl": "1.3.2",
+ "path-to-regexp": "0.1.7",
+ "proxy-addr": "2.0.4",
+ "qs": "6.5.2",
+ "range-parser": "1.2.0",
+ "safe-buffer": "5.1.2",
+ "send": "0.16.2",
+ "serve-static": "1.13.2",
+ "setprototypeof": "1.1.0",
+ "statuses": "1.4.0",
+ "type-is": "1.6.16",
+ "utils-merge": "1.0.1",
+ "vary": "1.1.2"
+ }
+ },
+ "finalhandler": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz",
+ "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==",
+ "requires": {
+ "debug": "2.6.9",
+ "encodeurl": "1.0.2",
+ "escape-html": "1.0.3",
+ "on-finished": "2.3.0",
+ "parseurl": "1.3.2",
+ "statuses": "1.4.0",
+ "unpipe": "1.0.0"
+ }
+ },
+ "forwarded": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
+ "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ="
+ },
+ "fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
+ },
+ "http-errors": {
+ "version": "1.6.3",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
+ "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=",
+ "requires": {
+ "depd": "1.1.2",
+ "inherits": "2.0.3",
+ "setprototypeof": "1.1.0",
+ "statuses": "1.4.0"
+ }
+ },
+ "iconv-lite": {
+ "version": "0.4.23",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz",
+ "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==",
+ "requires": {
+ "safer-buffer": "2.1.2"
+ }
+ },
+ "inherits": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+ "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
+ },
+ "ipaddr.js": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.0.tgz",
+ "integrity": "sha1-6qM9bd16zo9/b+DJygRA5wZzix4="
+ },
+ "media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
+ },
+ "merge-descriptors": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
+ "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
+ },
+ "methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
+ },
+ "mime": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz",
+ "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ=="
+ },
+ "mime-db": {
+ "version": "1.38.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.38.0.tgz",
+ "integrity": "sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg=="
+ },
+ "mime-types": {
+ "version": "2.1.22",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.22.tgz",
+ "integrity": "sha512-aGl6TZGnhm/li6F7yx82bJiBZwgiEa4Hf6CNr8YO+r5UHr53tSTYZb102zyU50DOWWKeOv0uQLRL0/9EiKWCog==",
+ "requires": {
+ "mime-db": "1.38.0"
+ }
+ },
+ "ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
+ },
+ "negotiator": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz",
+ "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk="
+ },
+ "on-finished": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
+ "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
+ "requires": {
+ "ee-first": "1.1.1"
+ }
+ },
+ "parseurl": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz",
+ "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M="
+ },
+ "path-to-regexp": {
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
+ "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
+ },
+ "proxy-addr": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz",
+ "integrity": "sha512-5erio2h9jp5CHGwcybmxmVqHmnCBZeewlfJ0pex+UW7Qny7OOZXTtH56TGNyBizkgiOwhJtMKrVzDTeKcySZwA==",
+ "requires": {
+ "forwarded": "0.1.2",
+ "ipaddr.js": "1.8.0"
+ }
+ },
+ "qs": {
+ "version": "6.5.2",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
+ "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA=="
+ },
+ "range-parser": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz",
+ "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4="
+ },
+ "raw-body": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz",
+ "integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==",
+ "requires": {
+ "bytes": "3.0.0",
+ "http-errors": "1.6.3",
+ "iconv-lite": "0.4.23",
+ "unpipe": "1.0.0"
+ }
+ },
+ "safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
+ },
+ "safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
+ },
+ "send": {
+ "version": "0.16.2",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz",
+ "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==",
+ "requires": {
+ "debug": "2.6.9",
+ "depd": "1.1.2",
+ "destroy": "1.0.4",
+ "encodeurl": "1.0.2",
+ "escape-html": "1.0.3",
+ "etag": "1.8.1",
+ "fresh": "0.5.2",
+ "http-errors": "1.6.3",
+ "mime": "1.4.1",
+ "ms": "2.0.0",
+ "on-finished": "2.3.0",
+ "range-parser": "1.2.0",
+ "statuses": "1.4.0"
+ }
+ },
+ "serve-static": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz",
+ "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==",
+ "requires": {
+ "encodeurl": "1.0.2",
+ "escape-html": "1.0.3",
+ "parseurl": "1.3.2",
+ "send": "0.16.2"
+ }
+ },
+ "setprototypeof": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz",
+ "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ=="
+ },
+ "statuses": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",
+ "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew=="
+ },
+ "type-is": {
+ "version": "1.6.16",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz",
+ "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==",
+ "requires": {
+ "media-typer": "0.3.0",
+ "mime-types": "2.1.22"
+ }
+ },
+ "unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
+ },
+ "utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
+ },
+ "vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
+ }
+ }
+}
diff --git a/example/compose/duo-api/package.json b/example/compose/duo-api/package.json
new file mode 100644
index 00000000..4d8267ba
--- /dev/null
+++ b/example/compose/duo-api/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "duo-api",
+ "version": "1.0.0",
+ "description": "",
+ "main": "duo_api.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "author": "",
+ "license": "ISC",
+ "dependencies": {
+ "express": "^4.16.4"
+ }
+}
diff --git a/example/compose/nginx/portal/docker-compose.yml b/example/compose/nginx/portal/docker-compose.yml
index d5fa8b3e..44622085 100644
--- a/example/compose/nginx/portal/docker-compose.yml
+++ b/example/compose/nginx/portal/docker-compose.yml
@@ -8,4 +8,6 @@ services:
ports:
- "8080:443"
networks:
- - authelianet
+ authelianet:
+ # Set the IP to be able to query on port 443
+ ipv4_address: 192.168.240.100
diff --git a/example/compose/nginx/portal/nginx.conf.ejs b/example/compose/nginx/portal/nginx.conf.ejs
index 3306e45a..2f7c14c9 100644
--- a/example/compose/nginx/portal/nginx.conf.ejs
+++ b/example/compose/nginx/portal/nginx.conf.ejs
@@ -431,5 +431,24 @@ http {
proxy_pass $upstream_endpoint;
}
}
+
+ server {
+ listen 443 ssl;
+ server_name duo.example.com;
+
+ resolver 127.0.0.11 ipv6=off;
+ set $upstream_endpoint http://duo-api:3000;
+
+ 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 Host $http_host;
+ proxy_pass $upstream_endpoint;
+ }
+ }
}
diff --git a/package-lock.json b/package-lock.json
index ba64b8be..e29d9f0b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -171,6 +171,14 @@
"to-fast-properties": "2.0.0"
}
},
+ "@duosecurity/duo_api": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@duosecurity/duo_api/-/duo_api-1.2.0.tgz",
+ "integrity": "sha512-Jxmeo5VZtaut9hELnBNZyvA7kojwRBAHl0uOk0dZSfBbphjr3QJ+92dnm/I++GPUEhEKjALLeQ9fCABwo5HsPQ==",
+ "requires": {
+ "nopt": "3.0.6"
+ }
+ },
"@sinonjs/commons": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.3.0.tgz",
@@ -482,12 +490,6 @@
"integrity": "sha512-txsii9cwD2OUOPukfPu3Jpoi3CnznBAwRX3JF26EC4p5T6IA8AaL6PBilACyY2fJkk+ydDNo4BJrJOo/OmNaZw==",
"dev": true
},
- "@types/proxyquire": {
- "version": "1.3.28",
- "resolved": "https://registry.npmjs.org/@types/proxyquire/-/proxyquire-1.3.28.tgz",
- "integrity": "sha512-SQaNzWQ2YZSr7FqAyPPiA3FYpux2Lqh3HWMZQk47x3xbMCqgC/w0dY3dw9rGqlweDDkrySQBcaScXWeR+Yb11Q==",
- "dev": true
- },
"@types/query-string": {
"version": "5.1.0",
"resolved": "http://registry.npmjs.org/@types/query-string/-/query-string-5.1.0.tgz",
@@ -595,8 +597,7 @@
"abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
- "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
- "dev": true
+ "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
},
"accepts": {
"version": "1.3.5",
@@ -2530,16 +2531,6 @@
"integrity": "sha1-peeo/7+kk7Q7kju9TKiaU7Y7YSs=",
"dev": true
},
- "fill-keys": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/fill-keys/-/fill-keys-1.0.2.tgz",
- "integrity": "sha1-mo+jb06K1jTjv2tPPIiCVRRS6yA=",
- "dev": true,
- "requires": {
- "is-object": "1.0.1",
- "merge-descriptors": "1.0.1"
- }
- },
"fill-range": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
@@ -4201,12 +4192,6 @@
"integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=",
"dev": true
},
- "is-object": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.1.tgz",
- "integrity": "sha1-iVJojF7C/9awPsyF52ngKQMINHA=",
- "dev": true
- },
"is-path-cwd": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz",
@@ -5163,12 +5148,6 @@
"integrity": "sha1-WuDA6vj+I+AJzQH5iJtCxPY0rxI=",
"dev": true
},
- "module-not-found-error": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/module-not-found-error/-/module-not-found-error-1.0.1.tgz",
- "integrity": "sha1-z4tP9PKWQGdNbN0CsOO8UjwrvcA=",
- "dev": true
- },
"moment": {
"version": "2.22.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.22.1.tgz",
@@ -5812,7 +5791,6 @@
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz",
"integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=",
- "dev": true,
"requires": {
"abbrev": "1.1.1"
}
@@ -7288,28 +7266,6 @@
"ipaddr.js": "1.6.0"
}
},
- "proxyquire": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/proxyquire/-/proxyquire-2.1.0.tgz",
- "integrity": "sha512-kptdFArCfGRtQFv3Qwjr10lwbEV0TBJYvfqzhwucyfEXqVgmnAkyEw/S3FYzR5HI9i5QOq4rcqQjZ6AlknlCDQ==",
- "dev": true,
- "requires": {
- "fill-keys": "1.0.2",
- "module-not-found-error": "1.0.1",
- "resolve": "1.8.1"
- },
- "dependencies": {
- "resolve": {
- "version": "1.8.1",
- "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.8.1.tgz",
- "integrity": "sha512-AicPrAC7Qu1JxPCZ9ZgCZlY35QgFnNqc+0LtbRNxnVw4TXvjQ72wnuL9JQcEBgXkI9JM8MsT9kaQoHcpCRJOYA==",
- "dev": true,
- "requires": {
- "path-parse": "1.0.5"
- }
- }
- }
- },
"pseudomap": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
diff --git a/package.json b/package.json
index 589edd3d..727ede86 100644
--- a/package.json
+++ b/package.json
@@ -31,6 +31,7 @@
"title": "Authelia API documentation"
},
"dependencies": {
+ "@duosecurity/duo_api": "^1.2.0",
"ajv": "^6.3.0",
"bluebird": "^3.5.0",
"body-parser": "^1.15.2",
diff --git a/scripts/authelia-scripts-bootstrap b/scripts/authelia-scripts-bootstrap
index d2832c21..b4ec915f 100755
--- a/scripts/authelia-scripts-bootstrap
+++ b/scripts/authelia-scripts-bootstrap
@@ -7,6 +7,9 @@ async function main() {
console.log('Build authelia-example-backend docker image.')
await exec('docker build -t authelia-example-backend example/compose/nginx/backend');
+ console.log('Build authelia-duo-api docker image.')
+ await exec('docker build -t authelia-duo-api example/compose/duo-api');
+
if (!fs.existsSync('/tmp/kind')) {
console.log('Install Kind for spawning a Kubernetes cluster.');
await exec('wget https://github.com/clems4ever/kind/releases/download/0.1.0-cmic1/kind-linux-amd64 -O /tmp/kind && chmod +x /tmp/kind');
diff --git a/server/src/lib/Server.ts b/server/src/lib/Server.ts
index c36854d1..346965c7 100644
--- a/server/src/lib/Server.ts
+++ b/server/src/lib/Server.ts
@@ -40,6 +40,9 @@ export default class Server {
displayableConfiguration.notifier.email.password = STARS;
if (displayableConfiguration.notifier && displayableConfiguration.notifier.smtp)
displayableConfiguration.notifier.smtp.password = STARS;
+ if (displayableConfiguration.duo_api) {
+ displayableConfiguration.duo_api.secret_key = STARS;
+ }
this.globalLogger.debug("User configuration is %s",
JSON.stringify(displayableConfiguration, undefined, 2));
diff --git a/server/src/lib/configuration/schema/Configuration.ts b/server/src/lib/configuration/schema/Configuration.ts
index 8d16a5fb..d0584732 100644
--- a/server/src/lib/configuration/schema/Configuration.ts
+++ b/server/src/lib/configuration/schema/Configuration.ts
@@ -5,6 +5,7 @@ import { RegulationConfiguration, complete as RegulationConfigurationComplete }
import { SessionConfiguration, complete as SessionConfigurationComplete } from "./SessionConfiguration";
import { StorageConfiguration, complete as StorageConfigurationComplete } from "./StorageConfiguration";
import { TotpConfiguration, complete as TotpConfigurationComplete } from "./TotpConfiguration";
+import { DuoPushConfiguration } from "./DuoPushConfiguration";
export interface Configuration {
access_control?: ACLConfiguration;
@@ -17,6 +18,7 @@ export interface Configuration {
session?: SessionConfiguration;
storage?: StorageConfiguration;
totp?: TotpConfiguration;
+ duo_api?: DuoPushConfiguration;
}
export function complete(
diff --git a/server/src/lib/configuration/schema/DuoPushConfiguration.ts b/server/src/lib/configuration/schema/DuoPushConfiguration.ts
new file mode 100644
index 00000000..bede3767
--- /dev/null
+++ b/server/src/lib/configuration/schema/DuoPushConfiguration.ts
@@ -0,0 +1,6 @@
+
+export interface DuoPushConfiguration {
+ hostname: string;
+ integration_key: string;
+ secret_key: string;
+}
\ No newline at end of file
diff --git a/server/src/lib/routes/secondfactor/duo-push/Post.spec.ts b/server/src/lib/routes/secondfactor/duo-push/Post.spec.ts
new file mode 100644
index 00000000..cd3fa1e4
--- /dev/null
+++ b/server/src/lib/routes/secondfactor/duo-push/Post.spec.ts
@@ -0,0 +1,109 @@
+import * as Express from "express";
+import { ServerVariables } from "../../../ServerVariables";
+import { ServerVariablesMockBuilder, ServerVariablesMock } from "../../../ServerVariablesMockBuilder.spec";
+import * as ExpressMock from "../../../stubs/express.spec";
+import Post from "./Post";
+import * as Sinon from "sinon";
+import * as Assert from "assert";
+import { Level } from "../../../authentication/Level";
+const DuoApi = require("@duosecurity/duo_api");
+
+
+describe("routes/secondfactor/duo-push/Post", function() {
+ let vars: ServerVariables;
+ let mocks: ServerVariablesMock;
+ let req: Express.Request;
+ let res: ExpressMock.ResponseMock;
+
+ beforeEach(function() {
+ const sv = ServerVariablesMockBuilder.build();
+ vars = sv.variables;
+ mocks = sv.mocks;
+
+ vars.config.duo_api = {
+ hostname: 'abc',
+ integration_key: 'xyz',
+ secret_key: 'secret',
+ }
+
+ req = ExpressMock.RequestMock();
+ res = ExpressMock.ResponseMock();
+ })
+
+ it("should raise authentication level of user", async function() {
+ const mock = Sinon.stub(DuoApi, "Client");
+ mock.returns({
+ jsonApiCall: Sinon.stub().yields({response: {result: 'allow'}})
+ });
+ req.session.auth = {
+ userid: 'john'
+ }
+
+ Assert.equal(req.session.auth.authentication_level, undefined);
+ await Post(vars)(req, res as any);
+ Assert(res.status.calledWith(204));
+ Assert(res.send.calledWith());
+ Assert.equal(req.session.auth.authentication_level, Level.TWO_FACTOR);
+ mock.restore();
+ });
+
+ it("should block if no duo API is configured", async function() {
+ const mock = Sinon.stub(DuoApi, "Client");
+ mock.returns({
+ jsonApiCall: Sinon.stub().yields({response: {result: 'allow'}})
+ });
+ req.session.auth = {
+ userid: 'john'
+ }
+ vars.config.duo_api = undefined;
+
+ Assert.equal(req.session.auth.authentication_level, undefined);
+ await Post(vars)(req, res as any);
+ Assert(res.status.calledWith(200));
+ Assert(res.send.calledWith({error: 'Operation failed.'}));
+ Assert.equal(req.session.auth.authentication_level, undefined);
+ mock.restore();
+ });
+
+ it("should block if user denied notification", async function() {
+ const mock = Sinon.stub(DuoApi, "Client");
+ mock.returns({
+ jsonApiCall: Sinon.stub().yields({response: {result: 'deny'}})
+ });
+ req.session.auth = {
+ userid: 'john'
+ }
+
+ Assert.equal(req.session.auth.authentication_level, undefined);
+ await Post(vars)(req, res as any);
+ Assert(res.status.calledWith(200));
+ Assert(res.send.calledWith({error: 'Operation failed.'}));
+ Assert.equal(req.session.auth.authentication_level, undefined);
+ mock.restore();
+ });
+
+ it("should block if duo push service is down", function() {
+ const mock = Sinon.stub(DuoApi, "Client");
+ const timerMock = Sinon.useFakeTimers();
+ mock.returns({
+ jsonApiCall: Sinon.stub()
+ });
+ req.session.auth = {
+ userid: 'john'
+ }
+
+ Assert.equal(req.session.auth.authentication_level, undefined);
+ const promise = Post(vars)(req, res as any)
+ .then(() => {
+ Assert(res.status.calledWith(200));
+ Assert(res.send.calledWith({error: 'Operation failed.'}));
+ Assert.equal(req.session.auth.authentication_level, undefined);
+
+ mock.restore();
+ timerMock.restore();
+ });
+ // Move forward in time to timeout.
+ timerMock.tick(62000);
+ return promise;
+ });
+});
\ No newline at end of file
diff --git a/server/src/lib/routes/secondfactor/duo-push/Post.ts b/server/src/lib/routes/secondfactor/duo-push/Post.ts
new file mode 100644
index 00000000..f3891a8e
--- /dev/null
+++ b/server/src/lib/routes/secondfactor/duo-push/Post.ts
@@ -0,0 +1,51 @@
+import * as Express from "express";
+import { ServerVariables } from "../../../ServerVariables";
+import { AuthenticationSessionHandler } from "../../../AuthenticationSessionHandler";
+import * as ErrorReplies from "../../../ErrorReplies";
+import * as UserMessage from "../../../../../../shared/UserMessages";
+import redirect from "../redirect";
+import { Level } from "../../../authentication/Level";
+import { DuoPushConfiguration } from "../../../configuration/schema/DuoPushConfiguration";
+const DuoApi = require("@duosecurity/duo_api");
+
+interface DuoResponse {
+ response: {
+ result: "allow" | "deny";
+ status: "allow" | "deny" | "fraud";
+ status_msg: string;
+ };
+ stat: "OK" | "FAIL";
+}
+
+function triggerAuth(username: string, config: DuoPushConfiguration): Promise {
+ return new Promise((resolve, reject) => {
+ const client = new DuoApi.Client(config.integration_key, config.secret_key, config.hostname);
+ const timer = setTimeout(() => reject(new Error("Call to duo push API timed out.")), 60000);
+ client.jsonApiCall("POST", "/auth/v2/auth", { username, factor: "push", device: "auto" }, (data: DuoResponse) => {
+ clearTimeout(timer);
+ resolve(data);
+ });
+ });
+}
+
+
+export default function(vars: ServerVariables) {
+ return async function(req: Express.Request, res: Express.Response) {
+ try {
+ if (!vars.config.duo_api) {
+ throw new Error("Duo Push Notification is not configured.");
+ }
+
+ const authSession = AuthenticationSessionHandler.get(req, vars.logger);
+ const authRes = await triggerAuth(authSession.userid, vars.config.duo_api);
+ if (authRes.response.result !== "allow") {
+ throw new Error("User denied access.");
+ }
+ vars.logger.debug(req, "Access allowed by user via Duo Push.");
+ authSession.authentication_level = Level.TWO_FACTOR;
+ await redirect(vars)(req, res);
+ } catch (err) {
+ ErrorReplies.replyWithError200(req, res, vars.logger, UserMessage.OPERATION_FAILED)(err);
+ }
+ };
+}
\ No newline at end of file
diff --git a/server/src/lib/routes/secondfactor/preferences/Get.spec.ts b/server/src/lib/routes/secondfactor/preferences/Get.spec.ts
index 08900347..7767672c 100644
--- a/server/src/lib/routes/secondfactor/preferences/Get.spec.ts
+++ b/server/src/lib/routes/secondfactor/preferences/Get.spec.ts
@@ -6,7 +6,7 @@ import * as ExpressMock from "../../../stubs/express.spec";
import Get from "./Get";
import * as Assert from "assert";
-describe("routes/secondfactor/Get", function() {
+describe("routes/secondfactor/preferences/Get", function() {
let vars: ServerVariables;
let mocks: ServerVariablesMock;
let req: Express.Request;
diff --git a/server/src/lib/routes/secondfactor/preferences/Post.spec.ts b/server/src/lib/routes/secondfactor/preferences/Post.spec.ts
index 2d55e5d3..da2b71e8 100644
--- a/server/src/lib/routes/secondfactor/preferences/Post.spec.ts
+++ b/server/src/lib/routes/secondfactor/preferences/Post.spec.ts
@@ -6,7 +6,7 @@ import * as ExpressMock from "../../../stubs/express.spec";
import Post from "./Post";
import * as Assert from "assert";
-describe("routes/secondfactor/Post", function() {
+describe("routes/secondfactor/preferences/Post", function() {
let vars: ServerVariables;
let mocks: ServerVariablesMock;
let req: Express.Request;
diff --git a/server/src/lib/routes/verify/CheckAuthorizations.ts b/server/src/lib/routes/verify/CheckAuthorizations.ts
index 0374a1f4..95984225 100644
--- a/server/src/lib/routes/verify/CheckAuthorizations.ts
+++ b/server/src/lib/routes/verify/CheckAuthorizations.ts
@@ -36,11 +36,11 @@ export default function (
}
else if (user && authorizationLevel == AuthorizationLevel.DENY) {
throw new Exceptions.NotAuthorizedError(
- Util.format("User %s is not authorized to access %s%s", user, domain, resource));
+ Util.format("User %s is not authorized to access %s%s", (user) ? user : "unknown", domain, resource));
}
else if (!isAuthorized(authorizationLevel, authenticationLevel)) {
throw new Exceptions.NotAuthenticatedError(Util.format(
- "User '%s' is not sufficiently authorized to access %s%s.", user, domain, resource));
+ "User '%s' is not sufficiently authorized to access %s%s.", (user) ? user : "unknown", domain, resource));
}
return authorizationLevel;
}
\ No newline at end of file
diff --git a/server/src/lib/web_server/RestApi.ts b/server/src/lib/web_server/RestApi.ts
index 9d3d9f75..04a15b5b 100644
--- a/server/src/lib/web_server/RestApi.ts
+++ b/server/src/lib/web_server/RestApi.ts
@@ -1,6 +1,7 @@
import * as Express from "express";
import SecondFactorPreferencesGet from "../routes/secondfactor/preferences/Get";
import SecondFactorPreferencesPost from "../routes/secondfactor/preferences/Post";
+import SecondFactorDuoPushPost from "../routes/secondfactor/duo-push/Post";
import FirstFactorPost = require("../routes/firstfactor/post");
import LogoutPost from "../routes/logout/post";
@@ -102,6 +103,12 @@ export class RestApi {
RequireValidatedFirstFactor.middleware(vars.logger),
SecondFactorPreferencesPost(vars));
+ if (vars.config.duo_api) {
+ app.post(Endpoints.SECOND_FACTOR_DUO_PUSH_POST,
+ RequireValidatedFirstFactor.middleware(vars.logger),
+ SecondFactorDuoPushPost(vars));
+ }
+
setupTotp(app, vars);
setupU2f(app, vars);
setupResetPassword(app, vars);
diff --git a/shared/api.ts b/shared/api.ts
index 055def7a..89023277 100644
--- a/shared/api.ts
+++ b/shared/api.ts
@@ -107,6 +107,21 @@ export const SECOND_FACTOR_U2F_SIGN_REQUEST_GET = "/api/u2f/sign_request";
*/
export const SECOND_FACTOR_TOTP_POST = "/api/totp";
+/**
+ * @api {post} /api/duo-push Complete Duo Push Factor
+ * @apiName ValidateDuoPushSecondFactor
+ * @apiGroup DuoPush
+ * @apiVersion 1.0.0
+ * @apiUse UserSession
+ * @apiUse InternalError
+ *
+ * @apiSuccess (Success 302) Redirect to the URL that has been stored during last call to /api/verify.
+ * @apiError (Error 401) {none} error TOTP token is invalid.
+ *
+ * @apiDescription Verify TOTP token. The user is authenticated upon success.
+ */
+export const SECOND_FACTOR_DUO_PUSH_POST = "/api/duo-push";
+
/**
* @api {get} /api/secondfactor/u2f/identity/start Start U2F registration identity validation
diff --git a/test/helpers/assertions/VerifyHasAppeared.ts b/test/helpers/assertions/VerifyHasAppeared.ts
new file mode 100644
index 00000000..fea58cc8
--- /dev/null
+++ b/test/helpers/assertions/VerifyHasAppeared.ts
@@ -0,0 +1,5 @@
+import SeleniumWebDriver, { WebDriver } from "selenium-webdriver";
+
+export default async function(driver: WebDriver, locator: SeleniumWebDriver.Locator, timeout: number = 5000) {
+ await driver.wait(SeleniumWebDriver.until.elementLocated(locator), timeout);
+}
\ No newline at end of file
diff --git a/test/helpers/context/AutheliaServerWithHotReload.ts b/test/helpers/context/AutheliaServerWithHotReload.ts
index edcb5b1c..2a8ad315 100644
--- a/test/helpers/context/AutheliaServerWithHotReload.ts
+++ b/test/helpers/context/AutheliaServerWithHotReload.ts
@@ -17,8 +17,10 @@ class AutheliaServerWithHotReload implements AutheliaServerInterface {
constructor(configPath: string, watchedPaths: string[]) {
this.configPath = configPath;
- this.watcher = Chokidar.watch(['server', 'shared/**/*.ts', 'node_modules',
- this.AUTHELIA_INTERRUPT_FILENAME, configPath].concat(watchedPaths), {
+ const pathsToReload = ['server', 'shared/**/*.ts', 'node_modules',
+ this.AUTHELIA_INTERRUPT_FILENAME, configPath].concat(watchedPaths);
+ console.log("Authelia will reload on changes of files or directories in " + pathsToReload.join(', '));
+ this.watcher = Chokidar.watch(pathsToReload, {
persistent: true,
ignoreInitial: true,
});
@@ -29,7 +31,10 @@ class AutheliaServerWithHotReload implements AutheliaServerInterface {
await exec('./node_modules/.bin/tslint -c server/tslint.json -p server/tsconfig.json')
this.serverProcess = ChildProcess.spawn('./node_modules/.bin/ts-node',
['-P', './server/tsconfig.json', './server/src/index.ts', this.configPath], {
- env: {...process.env},
+ env: {
+ ...process.env,
+ NODE_TLS_REJECT_UNAUTHORIZED: 0,
+ },
});
this.serverProcess.stdout.pipe(process.stdout);
this.serverProcess.stderr.pipe(process.stderr);
diff --git a/test/suites/basic/config.yml b/test/suites/basic/config.yml
index 172aa3ef..534f2568 100644
--- a/test/suites/basic/config.yml
+++ b/test/suites/basic/config.yml
@@ -86,12 +86,6 @@ regulation:
# The length of time before a banned user can login again.
ban_time: 900
-# Default redirection URL
-#
-# Note: this parameter is optional. If not provided, user won't
-# be redirected upon successful authentication.
-#default_redirection_url: https://authelia.example.domain
-
notifier:
# For testing purpose, notifications can be sent in a file
# filesystem:
diff --git a/test/suites/duo-push/README.md b/test/suites/duo-push/README.md
new file mode 100644
index 00000000..d8d83dd3
--- /dev/null
+++ b/test/suites/duo-push/README.md
@@ -0,0 +1,12 @@
+# Duo Push Notification suite
+
+This suite has been created to test Authelia against the Duo API for push notifications.
+It allows a user to validate second factor with a mobile phone.
+
+## Components
+
+Authelia, nginx, Duo fake API
+
+## Tests
+
+Test allowed and denied access via push notifications.
\ No newline at end of file
diff --git a/test/suites/duo-push/config.yml b/test/suites/duo-push/config.yml
new file mode 100644
index 00000000..c3320d65
--- /dev/null
+++ b/test/suites/duo-push/config.yml
@@ -0,0 +1,116 @@
+###############################################################
+# Authelia minimal configuration #
+###############################################################
+
+port: 9091
+
+logs_level: debug
+
+default_redirection_url: https://home.example.com:8080/
+
+authentication_backend:
+ file:
+ path: ./test/suites/basic/users_database.test.yml
+
+session:
+ secret: unsecure_session_secret
+ domain: example.com
+ expiration: 3600000 # 1 hour
+ inactivity: 300000 # 5 minutes
+
+# Configuration of the storage backend used to store data and secrets. i.e. totp data
+storage:
+ local:
+ path: /tmp/authelia/db
+
+# TOTP Issuer Name
+#
+# This will be the issuer name displayed in Google Authenticator
+# See: https://github.com/google/google-authenticator/wiki/Key-Uri-Format for more info on issuer names
+totp:
+ issuer: example.com
+
+# The Duo Push Notification API configuration
+duo_api:
+ hostname: duo.example.com
+ integration_key: ABCDEFGHIJKL
+ secret_key: abcdefghijklmnopqrstuvwxyz123456789
+
+# Access Control
+#
+# Access control is a set of rules you can use to restrict user access to certain
+# resources.
+access_control:
+ # Default policy can either be `bypass`, `one_factor`, `two_factor` or `deny`.
+ default_policy: deny
+
+ rules:
+ - domain: singlefactor.example.com
+ policy: one_factor
+
+ - domain: public.example.com
+ policy: bypass
+
+ - domain: secure.example.com
+ policy: two_factor
+
+ - domain: '*.example.com'
+ subject: "group:admins"
+ policy: two_factor
+
+ - domain: dev.example.com
+ resources:
+ - '^/users/john/.*$'
+ subject: "user:john"
+ policy: two_factor
+
+ - domain: dev.example.com
+ resources:
+ - '^/users/harry/.*$'
+ subject: "user:harry"
+ policy: two_factor
+
+ - domain: '*.mail.example.com'
+ subject: "user:bob"
+ policy: two_factor
+
+ - domain: dev.example.com
+ resources:
+ - '^/users/bob/.*$'
+ subject: "user:bob"
+ policy: two_factor
+
+
+# Configuration of the authentication regulation mechanism.
+regulation:
+ # Set it to 0 to disable max_retries.
+ max_retries: 3
+
+ # The user is banned if the authenticaction failed `max_retries` times in a `find_time` seconds window.
+ find_time: 300
+
+ # The length of time before a banned user can login again.
+ ban_time: 900
+
+notifier:
+ # For testing purpose, notifications can be sent in a file
+ # filesystem:
+ # filename: /tmp/authelia/notification.txt
+
+ # Use your email account to send the notifications. You can use an app password.
+ # List of valid services can be found here: https://nodemailer.com/smtp/well-known/
+ ## email:
+ ## username: user@example.com
+ ## password: yourpassword
+ ## sender: admin@example.com
+ ## service: gmail
+
+ # Use a SMTP server for sending notifications
+ smtp:
+ username: test
+ password: password
+ secure: false
+ host: 127.0.0.1
+ port: 1025
+ sender: admin@example.com
+
diff --git a/test/suites/duo-push/environment.ts b/test/suites/duo-push/environment.ts
new file mode 100644
index 00000000..e52b040a
--- /dev/null
+++ b/test/suites/duo-push/environment.ts
@@ -0,0 +1,36 @@
+import fs from 'fs';
+import { exec } from "../../helpers/utils/exec";
+import AutheliaServer from "../../helpers/context/AutheliaServer";
+import DockerEnvironment from "../../helpers/context/DockerEnvironment";
+
+const autheliaServer = new AutheliaServer(__dirname + '/config.yml', [__dirname + '/users_database.yml']);
+const dockerEnv = new DockerEnvironment([
+ 'docker-compose.yml',
+ 'example/compose/nginx/backend/docker-compose.yml',
+ 'example/compose/nginx/portal/docker-compose.yml',
+ 'example/compose/duo-api/docker-compose.yml',
+])
+
+async function setup() {
+ await exec(`cp ${__dirname}/users_database.yml ${__dirname}/users_database.test.yml`);
+ await exec('mkdir -p /tmp/authelia/db');
+ await exec('./example/compose/nginx/portal/render.js ' + (fs.existsSync('.suite') ? '': '--production'));
+ await dockerEnv.start();
+ await autheliaServer.start();
+}
+
+async function teardown() {
+ await autheliaServer.stop();
+ await dockerEnv.stop();
+ await exec('rm -rf /tmp/authelia/db');
+}
+
+const setup_timeout = 30000;
+const teardown_timeout = 30000;
+
+export {
+ setup,
+ setup_timeout,
+ teardown,
+ teardown_timeout
+};
\ No newline at end of file
diff --git a/test/suites/duo-push/scenarii/DuoPushNotification.ts b/test/suites/duo-push/scenarii/DuoPushNotification.ts
new file mode 100644
index 00000000..ab8875d2
--- /dev/null
+++ b/test/suites/duo-push/scenarii/DuoPushNotification.ts
@@ -0,0 +1,64 @@
+import { StartDriver, StopDriver } from "../../../helpers/context/WithDriver";
+import LoginAs from "../../../helpers/LoginAs";
+import VerifyIsSecondFactorStage from "../../../helpers/assertions/VerifyIsSecondFactorStage";
+import ClickOnLink from "../../../helpers/ClickOnLink";
+import VerifyIsUseAnotherMethodView from "../../../helpers/assertions/VerifyIsUseAnotherMethodView";
+import ClickOnButton from "../../../helpers/behaviors/ClickOnButton";
+import VerifySecretObserved from "../../../helpers/assertions/VerifySecretObserved";
+import Request from 'request-promise';
+import VerifyUrlIs from "../../../helpers/assertions/VerifyUrlIs";
+import VerifyHasAppeared from "../../../helpers/assertions/VerifyHasAppeared";
+import SeleniumWebDriver from "selenium-webdriver";
+import VisitPage from "../../../helpers/VisitPage";
+
+
+export default function() {
+ before(async function() {
+ this.driver = await StartDriver();
+ });
+
+ after(async function () {
+ await StopDriver(this.driver);
+ });
+
+ describe('Allow access', function() {
+ before(async function() {
+ // Configure the fake API to return allowing response.
+ await Request('https://duo.example.com/allow', {method: 'POST'});
+ });
+
+ it('should grant access with Duo API', async function() {
+ await LoginAs(this.driver, "john", "password", "https://secure.example.com:8080/secret.html");
+ await VerifyIsSecondFactorStage(this.driver);
+
+ await ClickOnLink(this.driver, 'Use another method');
+ await VerifyIsUseAnotherMethodView(this.driver);
+ await ClickOnButton(this.driver, 'Duo Push Notification');
+
+ await VerifyUrlIs(this.driver, "https://secure.example.com:8080/secret.html");
+ await VerifySecretObserved(this.driver);
+
+ await VisitPage(this.driver, "https://login.example.com:8080/#/");
+ await ClickOnButton(this.driver, "Logout");
+ });
+ });
+
+ describe('Deny access', function() {
+ before(async function() {
+ // Configure the fake API to return denying response.
+ await Request('https://duo.example.com/deny', {method: 'POST'});
+ });
+
+ it('should grant access with Duo API', async function() {
+ await LoginAs(this.driver, "john", "password", "https://secure.example.com:8080/secret.html");
+ await VerifyIsSecondFactorStage(this.driver);
+
+ await ClickOnLink(this.driver, 'Use another method');
+ await VerifyIsUseAnotherMethodView(this.driver);
+ await ClickOnButton(this.driver, 'Duo Push Notification');
+
+ // The retry button appeared.
+ await VerifyHasAppeared(this.driver, SeleniumWebDriver.By.tagName("button"));
+ });
+ });
+}
\ No newline at end of file
diff --git a/test/suites/duo-push/test.ts b/test/suites/duo-push/test.ts
new file mode 100644
index 00000000..1dab3d4d
--- /dev/null
+++ b/test/suites/duo-push/test.ts
@@ -0,0 +1,16 @@
+import AutheliaSuite from "../../helpers/context/AutheliaSuite";
+import { exec } from '../../helpers/utils/exec';
+import DuoPushNotification from "./scenarii/DuoPushNotification";
+
+// required to query duo-api over https
+process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = 0 as any;
+
+AutheliaSuite(__dirname, function() {
+ this.timeout(10000);
+
+ beforeEach(async function() {
+ await exec(`cp ${__dirname}/users_database.yml ${__dirname}/users_database.test.yml`);
+ });
+
+ describe("Duo Push Notication", DuoPushNotification);
+});
\ No newline at end of file
diff --git a/test/suites/duo-push/users_database.yml b/test/suites/duo-push/users_database.yml
new file mode 100644
index 00000000..6fe7a384
--- /dev/null
+++ b/test/suites/duo-push/users_database.yml
@@ -0,0 +1,29 @@
+###############################################################
+# Users Database #
+###############################################################
+
+# This file can be used if you do not have an LDAP set up.
+
+# List of users
+users:
+ john:
+ password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
+ email: john.doe@authelia.com
+ groups:
+ - admins
+ - dev
+
+ harry:
+ password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
+ email: harry.potter@authelia.com
+ groups: []
+
+ bob:
+ password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
+ email: bob.dylan@authelia.com
+ groups:
+ - dev
+
+ james:
+ password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
+ email: james.dean@authelia.com
\ No newline at end of file
From ff88ad354f334e255236a87f45473d93baaf97ee Mon Sep 17 00:00:00 2001
From: Clement Michaud
Date: Sun, 24 Mar 2019 16:37:31 +0100
Subject: [PATCH 2/7] Install /etc/hosts entries from bootstrap script.
This allows to add an entry which is not pointing to localhost but
to a docker container in the Travis virtual env.
---
.travis.yml | 15 +-------
bootstrap.sh | 19 -----------
scripts/authelia-scripts-bootstrap | 55 +++++++++++++++++++++++++++++-
3 files changed, 55 insertions(+), 34 deletions(-)
diff --git a/.travis.yml b/.travis.yml
index 681936a5..4d922109 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,4 +1,5 @@
language: node_js
+required: sudo
node_js:
- '9'
services:
@@ -12,20 +13,6 @@ addons:
packages:
- libgif-dev
- google-chrome-stable
- hosts:
- - admin.example.com
- - login.example.com
- - singlefactor.example.com
- - dev.example.com
- - home.example.com
- - mx1.mail.example.com
- - mx2.mail.example.com
- - public.example.com
- - secure.example.com
- - authelia.example.com
- - admin.example.com
- - mail.example.com
- - duo.example.com
before_script:
- export DISPLAY=:99.0
diff --git a/bootstrap.sh b/bootstrap.sh
index c3eee60e..8d5fc04f 100644
--- a/bootstrap.sh
+++ b/bootstrap.sh
@@ -24,25 +24,6 @@ then
return;
fi
-echo "[BOOTSTRAP] Checking if example.com domain is forwarded to your machine..."
-cat /etc/hosts | grep "login.example.com" > /dev/null
-if [ $? -ne 0 ];
-then
- echo "[ERROR] Please add those lines to /etc/hosts:
-
-127.0.0.1 home.example.com
-127.0.0.1 public.example.com
-127.0.0.1 secure.example.com
-127.0.0.1 dev.example.com
-127.0.0.1 admin.example.com
-127.0.0.1 mx1.mail.example.com
-127.0.0.1 mx2.mail.example.com
-127.0.0.1 singlefactor.example.com
-127.0.0.1 login.example.com
-192.168.240.100 duo.example.com"
- return;
-fi
-
echo "[BOOTSTRAP] Running additional bootstrap steps..."
authelia-scripts bootstrap
diff --git a/scripts/authelia-scripts-bootstrap b/scripts/authelia-scripts-bootstrap
index b4ec915f..2e58fb58 100755
--- a/scripts/authelia-scripts-bootstrap
+++ b/scripts/authelia-scripts-bootstrap
@@ -3,12 +3,59 @@
var { exec } = require('./utils/exec');
var fs = require('fs');
-async function main() {
+async function buildDockerImages() {
+ console.log("[BOOTSTRAP] Building required Docker images...");
+
console.log('Build authelia-example-backend docker image.')
await exec('docker build -t authelia-example-backend example/compose/nginx/backend');
console.log('Build authelia-duo-api docker image.')
await exec('docker build -t authelia-duo-api example/compose/duo-api');
+}
+
+async function checkHostsFile() {
+ async function checkAndFixEntry(entries, domain, ip) {
+ const foundEntry = entries.filter(l => l[1] == domain);
+ if (foundEntry.length > 0) {
+ if (foundEntry[0][0] == ip) {
+ // The entry exists and is correct.
+ return;
+ }
+ else {
+ // We need to remove the entry and replace it.
+ console.log(`Update entry for ${domain}.`);
+ await exec(`cat /etc/hosts | grep -v "${domain}" | /usr/bin/sudo tee /etc/hosts > /dev/null`);
+ await exec(`echo "${ip} ${domain}" | /usr/bin/sudo tee -a /etc/hosts > /dev/null`);
+ }
+ }
+ else {
+ // We need to add the new entry.
+ console.log(`Add entry for ${domain}.`);
+ await exec(`echo "${ip} ${domain}" | /usr/bin/sudo tee -a /etc/hosts > /dev/null`);
+ }
+ }
+
+ console.log("[BOOTSTRAP] Checking if example.com domain is forwarded to your machine...");
+ const actualEntries = fs.readFileSync("/etc/hosts").toString("utf-8")
+ .split("\n").filter(l => l !== '').map(l => l.split(" ").filter(w => w !== ''));
+
+ await checkAndFixEntry(actualEntries, 'login.example.com', '127.0.0.1');
+ await checkAndFixEntry(actualEntries, 'admin.example.com', '127.0.0.1');
+ await checkAndFixEntry(actualEntries, 'singlefactor.example.com', '127.0.0.1');
+ await checkAndFixEntry(actualEntries, 'dev.example.com', '127.0.0.1');
+ await checkAndFixEntry(actualEntries, 'home.example.com', '127.0.0.1');
+ await checkAndFixEntry(actualEntries, 'mx1.mail.example.com', '127.0.0.1');
+ await checkAndFixEntry(actualEntries, 'mx2.mail.example.com', '127.0.0.1');
+ await checkAndFixEntry(actualEntries, 'public.example.com', '127.0.0.1');
+ await checkAndFixEntry(actualEntries, 'secure.example.com', '127.0.0.1');
+ await checkAndFixEntry(actualEntries, 'authelia.example.com', '127.0.0.1');
+ await checkAndFixEntry(actualEntries, 'mail.example.com', '127.0.0.1');
+
+ await checkAndFixEntry(actualEntries, 'duo.example.com', '192.168.240.100');
+}
+
+async function checkKubernetesDependencies() {
+ console.log("[BOOTSTRAP] Checking Kubernetes tools in /tmp to allow testing a Kube cluster... (no junk installed on host)");
if (!fs.existsSync('/tmp/kind')) {
console.log('Install Kind for spawning a Kubernetes cluster.');
@@ -21,6 +68,12 @@ async function main() {
}
}
+async function main() {
+ await checkHostsFile();
+ await buildDockerImages();
+ await checkKubernetesDependencies();
+}
+
main().catch((err) => {
console.error(err);
process.exit(1);
From 4eaafb711561e8774ff8b8f0f7d6e89d42afbfe0 Mon Sep 17 00:00:00 2001
From: Clement Michaud
Date: Sun, 24 Mar 2019 18:45:32 +0100
Subject: [PATCH 3/7] Update the documentation to include information on Duo.
---
README.md | 18 ++++---
config.template.yml | 2 +-
docs/2factor/duo-push-notifications.md | 47 +++++++++++++++++++
docs/2factor/security-key.md | 40 ++++++++++++++++
docs/2factor/time-based-one-time-password.md | 29 ++++++++++++
docs/features.md | 37 +++------------
images/2factor_duo.png | Bin 0 -> 19030 bytes
images/2factor_totp.png | Bin 0 -> 18316 bytes
images/2factor_u2f.png | Bin 0 -> 20848 bytes
images/duo-push-1.jpg | Bin 0 -> 51976 bytes
images/duo-push-2.png | Bin 0 -> 53608 bytes
images/first_factor.png | Bin 19471 -> 16479 bytes
images/use-another-method.png | Bin 0 -> 26999 bytes
test/suites/.gitignore | 4 +-
14 files changed, 138 insertions(+), 39 deletions(-)
create mode 100644 docs/2factor/duo-push-notifications.md
create mode 100644 docs/2factor/security-key.md
create mode 100644 docs/2factor/time-based-one-time-password.md
create mode 100644 images/2factor_duo.png
create mode 100644 images/2factor_totp.png
create mode 100644 images/2factor_u2f.png
create mode 100644 images/duo-push-1.jpg
create mode 100644 images/duo-push-2.png
create mode 100644 images/use-another-method.png
diff --git a/README.md b/README.md
index d98936e3..ad4e1861 100644
--- a/README.md
+++ b/README.md
@@ -8,8 +8,9 @@
[![Gitter](https://img.shields.io/gitter/room/badges/shields.svg)](https://gitter.im/authelia/general?utm_source=share-link&utm_medium=link&utm_campaign=share-link)
[![Donate](https://img.shields.io/badge/Donate-PayPal-orange.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=clement%2emichaud34%40gmail%2ecom&lc=FR&item_name=Authelia¤cy_code=EUR&bn=PP%2dDonationsBF%3abtn_donate_SM%2egif%3aNonHosted)
-**Authelia** is an open-source authentication and authorization providing
- 2-factor authentication and single sign-on (SSO) for your applications.
+**Authelia** is an open-source authentication and authorization server
+providing 2-factor authentication and single sign-on (SSO) for your
+applications.
It acts as a companion of reverse proxies by handling authentication and
authorization requests.
@@ -20,15 +21,17 @@ for specific services in only few seconds.
-
+
## Features summary
Here is the list of the main available features:
-* **[U2F] - Universal 2-Factor -** support with [Yubikey].
-* **[TOTP] - Time-Base One Time password -** support with [Google Authenticator].
+* Several kind of second factor:
+ * **[Security Key (U2F)](./docs/2factor/security-key.md)** support with [Yubikey].
+ * **[Time-based One-Time password](./docs/2factor/time-based-one-time-password.md)** support with [Google Authenticator].
+ * **[Mobile Push Notifications](./docs/2factor/duo-push-notifications.md)** with [Duo](https://duo.com/).
* Password reset with identity verification using email.
* Single-factor only authentication method available.
* Access restriction after too many authentication attempts.
@@ -43,6 +46,7 @@ For more details about the features, follow [Features](./docs/features.md).
You can start off with
+ git clone https://github.com/clems4ever/authelia.git
source bootstrap.sh
If you want to go further, please read [Getting Started](./docs/getting-started.md).
@@ -113,8 +117,8 @@ Wanna see more features? Then fuel us with a few beers!
[MIT License]: https://opensource.org/licenses/MIT
[TOTP]: https://en.wikipedia.org/wiki/Time-based_One-time_Password_Algorithm
-[U2F]: https://www.yubico.com/about/background/fido/
+[Security Key]: 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
-[config.template.yml]: https://github.com/clems4ever/authelia/blob/master/config.template.yml
+[config.template.yml]: https://github.com/clems4ever/authelia/blob/master/config.template.yml
\ No newline at end of file
diff --git a/config.template.yml b/config.template.yml
index 0587fa53..727858bf 100644
--- a/config.template.yml
+++ b/config.template.yml
@@ -34,7 +34,7 @@ totp:
# Parameters used to contact the Duo API. Those are generated when you protect an application
# of type "Partner Auth API" in the management panel.
duo_api:
- hostname: api-123456789.duosecurity.com
+ hostname: api-123456789.example.com
integration_key: ABCDEF
secret_key: 1234567890abcdefghifjkl
diff --git a/docs/2factor/duo-push-notifications.md b/docs/2factor/duo-push-notifications.md
new file mode 100644
index 00000000..7b2eaf59
--- /dev/null
+++ b/docs/2factor/duo-push-notifications.md
@@ -0,0 +1,47 @@
+# Duo Push Notification
+
+Using mobile push notifications is becoming the new trendy way to validate
+the second factor of a 2FA authentication process. [Duo](https://duo.com/) is offering an API
+to integrate this kind validation and **Authelia** leverages this mechanism
+so that you can simply push a button on your smartphone to be securely granted
+access to your services.
+
+
+
+
+
+In order to use this feature, you should first create a free account on Duo
+(up to 10 users), create a user account and attach it a mobile device. The name
+of the user must match the name of the user in your internal database.
+Then, click on *Applications* and *Protect an Application*. Then select the option
+called *Partner Auth API*. This will generate an integration key, a secret key and
+a hostname. You can set the name of the application to **Authelia** and then you
+must add the generated information to your configuration as:
+
+ duo_api:
+ hostname: api-123456789.example.com
+ integration_key: ABCDEF
+ secret_key: 1234567890abcdefghifjkl
+
+This can be seen in [config.template.yml](../../config.template.yml) file.
+
+When selecting *Duo Push Notification* at the second factor stage, you will
+automatically receive a push notification on your phone to grant or deny access.
+
+
+
+
+
+
+## Limitations
+
+Users must be enrolled via the Duo Admin panel, they cannot enroll a device from
+**Authelia** yet.
+
+
+## FAQ
+
+### Why don't I have access to the *Duo Push Notification* option?
+
+It's likely that you have not configured **Authelia** correctly. Please read this
+documentation again and be sure you had a look at [config.template.yml](../../config.template.yml).
\ No newline at end of file
diff --git a/docs/2factor/security-key.md b/docs/2factor/security-key.md
new file mode 100644
index 00000000..c1f748cf
--- /dev/null
+++ b/docs/2factor/security-key.md
@@ -0,0 +1,40 @@
+# Security Keys (U2F)
+
+**Authelia** also offers authentication using Security Keys supporting U2F
+like [Yubikey](Yubikey) USB devices. U2F is one of the most secure
+authentication protocol and is already available for Google, Facebook, Github
+accounts and more.
+
+The protocol requires your security key being enrolled before authenticating.
+
+
+
+
+
+To do so, select the *Security Key* method in the second factor page and click
+on the *register new device* link. This will send a link to the
+user email address. This e-mail will likely be sent to https://mail.example.com:8080/
+if you're testing Authelia and you've not configured anything.
+
+Confirm your identity by clicking on **Continue** and you'll be asked to
+touch the token of your security key to enroll.
+
+
+
+
+
+Upon successful registration, you can authenticate using your security key by simply
+touching the token again.
+
+Easy, right?!
+
+## FAQ
+
+### Why don't I have access to the *Security Key* option?
+
+U2F protocol is a new protocol that is only supported by recent browser
+and must even be enabled on some of them like Firefox. Please be sure
+your browser supports U2F and that the feature is enabled to make the
+option available in **Authelia**.
+
+[Yubikey]: https://www.yubico.com/products/yubikey-hardware/yubikey4/
diff --git a/docs/2factor/time-based-one-time-password.md b/docs/2factor/time-based-one-time-password.md
new file mode 100644
index 00000000..2ef9bbf7
--- /dev/null
+++ b/docs/2factor/time-based-one-time-password.md
@@ -0,0 +1,29 @@
+# One-Time Passwords
+
+In **Authelia**, your users can use [Google Authenticator] for generating unique
+tokens that they can use to pass the second factor.
+
+
+
+
+
+Select the *One-Time Password method* and click on the *register new device* link.
+Then, check the email sent by **Authelia** to your email address
+to validate your identity. If you're testing **Authelia**, it's likely
+that this e-mail has been sent to https://mail.example.com:8080/
+
+Confirm your identity by clicking on **Continue** and you'll get redirected
+on a page where your secret will be displayed as QRCode and in Base32 formats.
+
+
+
+
+
+You can use [Google Authenticator] to store it.
+
+From now on, you'll get generated
+tokens from your phone that you can use to validate the second factor in **Authelia**.
+
+
+
+[Google Authenticator]: https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en
\ No newline at end of file
diff --git a/docs/features.md b/docs/features.md
index b601cda9..f811973e 100644
--- a/docs/features.md
+++ b/docs/features.md
@@ -15,39 +15,16 @@ You can find an example of the configuration of the LDAP backend in
-## Second factor with TOTP
+## Second factor
-In **Authelia**, you can register a per user TOTP (Time-Based One Time
-Password) secret before being being able to authenticate. Click on the
-register button and check the email **Authelia** sent to your email address
-to validate your identity.
+**Authelia** comes with three kind of second factor.
-Confirm your identity by clicking on **Continue** and you'll get redirected
-on a page where your secret will be displayed in QRCode and Base32 formats.
-You can use [Google Authenticator] to store it and get the generated tokens.
+* Security keys like [Yubikey]. More info [here](./2factor/security-key.md).
+* One-Time Passwords generated by [Google Authenticator]. More info [here](./2factor/time-based-one-time-password.md).
+* Duo Push Notifications to use with [Duo mobile application](https://play.google.com/store/apps/details?id=com.duosecurity.duomobile&hl=en) available on Android, iOS and Windows. More info [here](./2factor/duo-push-notifications.md).
-
-
-
-## Second factor with U2F security keys
-
-**Authelia** also offers authentication using U2F (Universal 2-Factor) devices
-like [Yubikey](Yubikey) USB security keys. U2F is one of the most secure
-authentication protocol and is already available for Google, Facebook, Github
-accounts and more.
-
-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.
-Confirm your identity by clicking on **Continue** and you'll be asked to
-touch the token of your device to register. Upon successful registration,
-you can authenticate using your U2F device by simply touching the token.
-
-Easy, right?!
-
-
-
+
## Password reset
@@ -96,5 +73,5 @@ Redis key/value store. You can specify your own Redis instance in
[basic authentication]: https://en.wikipedia.org/wiki/Basic_access_authentication
[config.template.yml]: https://github.com/clems4ever/authelia/blob/master/config.template.yml
-[Google Authenticator]: https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en
[Yubikey]: https://www.yubico.com/products/yubikey-hardware/yubikey4/
+[Google Authenticator]: https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en
\ No newline at end of file
diff --git a/images/2factor_duo.png b/images/2factor_duo.png
new file mode 100644
index 0000000000000000000000000000000000000000..f68d1b48d127d198d50128fe53e2922550934eac
GIT binary patch
literal 19030
zcmd?Rbx<79_b)gkfh1@kxP}Q3+}$R?;1B}AJ-E9LmV^L<1q<#F+=4p;!94_bcMA@K
z%
z=Gi0Q2oriL4)E`}lbnt#2!z}9?-wne1(yN@dIge)N~n9K9W45}QCuW*{FSXcAk+KC
z=S-l-Aedoy27X2vo;dZW3Hq4=yu{Cp75OMnway%$WBb*>rOa2?upJ?~H5b~wMlARr
z{s9kKO#4t5Cg|rNMkYE;oYejxUKhzq%9qjhCW`L}M`x6w-`dn>;S3xY-ZRXP#-$kV-Uziq6Y-}q#w-%P)qXv^~3pQ
zJze5HgBN{KM=g0xVH_OWwHg9TF54>nZKqSR1zksYX>%}3Z+T$cM3%n{WaY>}?%+(1
z`nzmLYH0X+dka;R6}O#?G}Cx*MdH2dLW)mT$HvBElPB==AT&?8cyzE((=9xAn_Wy<0<@
zQdCRj!&xWI@m-ela&|#QOW$e1(#_^;6}$Te)jc0&n^H&nQpn9sFO5UM{$G^X`P$Vi
z8kT%!bEZ>;-)NoLFONWYq4PEmqGf+8(LqaPM;8rL)i*IfdQyRCL4_W^p$D5Vm$%;B
zYO%hrHYv?Mndt8fQAsFo-8Vryn`OCLIGgB=ghIOrwOHF+PS((|CJ1fqtVG>U|88Gk
zIT-m!w7NnY7Wm)xSWfw)x{nJAB6v3n&_Ma=Oz~Wmo&gs<@}clB
zN3Y9Ph-eL+7;cw#M6e@%m-yUuu6jEsk${eouXE=x7IItuV1kL98>$QO-?DUExa;+K
zBWNzFBI-HarX2W`0@Us%kk@1S&d;MN9uC)WnLkv-NK4SEg4lC9k~c?2dE=UkH+?KLINW%G%AG|MO5FKw&bqJj9l_2Or!=j6
zGdljE7eP2$oZra{P*z`D-um5gWIRYP;mgxN>YewCLUouxZ!eQ{$)pZqF9H@&OODjH
z9kq+{QqT!q)X8PRLlH6|k_Klyiazun6)>UK_6Pap0y7x?&SCj_FqB+^so8cse_kQ9
z@S<~@zbz(Aeu(w)CoFzmHY3{WvW|WW6gXIIh=**4eMiG~
z>`J?32E=VR$rL*Sc=v2OV@7x;4`o`pvP6>kE^lbW(*Fim4FhBmU6jftHiKRdsk
zLj5jB^k~wob~0bAS7Af%&o!*I~3AF=hzI>c#g;P&%rI$@?qOvyM-@R%_2x4);<8
z^0p}JsUQA2?Mp+%B^9Y*A3JNY@h1cBPN4K8p!V+qwdH51l}1^Aqyr0)mVTv~gtfFa
z*2UVRzDFNwv7xHMq5JL>3ecLbKx6BXdDa&g(MW$4_!MsIGz<*|1b
zB`&BB`E?nH+%mF5c09d0wo8K>?A4mxV^Mf4oep+db%ENkx#PKI?sn4E2eWGdzjNUx
zcrTW}7L0@NJ^^1P>N`z4E^8)=ox*R|Wxz!z8>@IK)DJ&V3Z)zlO@}!G9!$-CfHH_b
z>^8ql;Xa6;A}MuTJS`cG;RbP~_DPV-f2+
zr%u*A+>$I&TD-t=SURsWzBg{ZE%Ou?zPl;#^=nyc6#Y@u_|U!sPv$m
z#Q&scRTh%A@_iOi98Xlg9|6;25YN|~#zvuWJ`kw=oVT{;4h>{1{C{y*GiP4(Ooqc_
z`_1wdL7*fTLQLs=MbdDl?_B+6+(ewaC@c_XTwacJ_ItWG`5?&{cX=KSZU!Pw0|SEq
zpNf4kONYTC$|Al&TzXLZSJSY#Y7)0a6BT`b#dq)C;hO6OS}2G@fYYfC)0;Du{m)&*
zv(X-dqTflNt_#h4x!-U=1e$euK9~<7N=o)OjD#Vu=fL+)pXvP02|m2sIoR79D0luk
zWT{k)KR!E4b`8;q8v=piSZAegjWpdnJk&ICv@FF<;f%>j9&T=FLLFvJFF>D)_J^M5
z=jOf+e-xZA*YiSEOKW^_(>pg#XR=$`?_F<3igxMh
z4u3OG^sWsEgp}wp6G||j%|LB5L?k={aYl2GqqkrGWGL`_-1sC$VD_*{N$WA_Hy$^`
z{)tS%bkZ&u1i}bx;|77EvA8W3awMV_O}MT2D0}y@8A?Q6klhY|i{f{^zKvU5$W#w+
z;)Qpz<9kqO2T5kXcOpgw?N_Bl!pib}%ry%*cqhIG6|U5#;s|G~3mRH-&iiGb4+k5-
zG_&9Tt^33nd|zIJSw{UaZ1@Z46Vnqv^L22rsKq+cz+K5a-q3L+_hScYPpa8>X;>U#
zFly*~^T2{79GU;-iZ8mUN}*s}TB<`#1ov=u^~B-tO?vQHeEA7Xc;`gqVxgjP$%1i%P^AQq%
z;lm+_QLi38s!7pDjvWA0V)rHaoABLUPGFyj$2~|*9J|rPCy4LtJtqa5)LPV!?;AA_
zZa){6b)CcUrd$%vx2YB=i|MdoGenS%iPenmp9@Fat6WKiBh$b6ae;HHCiNaK9kqvD
zmtvfi@Da$%F5+e`TE2@atnaH{f$`d+>I0t{