mirror of
https://github.com/0rangebananaspy/authelia.git
synced 2024-09-14 22:47:21 +07:00
Add Duo Push Notification option as 2FA.
This commit is contained in:
parent
090a74299f
commit
8ef402511c
|
@ -25,6 +25,7 @@ addons:
|
|||
- authelia.example.com
|
||||
- admin.example.com
|
||||
- mail.example.com
|
||||
- duo.example.com
|
||||
|
||||
before_script:
|
||||
- export DISPLAY=:99.0
|
||||
|
|
|
@ -38,7 +38,8 @@ then
|
|||
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"
|
||||
127.0.0.1 login.example.com
|
||||
192.168.240.100 duo.example.com"
|
||||
return;
|
||||
fi
|
||||
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
@import '../../variables.scss';
|
||||
|
||||
.image {
|
||||
width: '120px';
|
||||
}
|
||||
|
||||
.imageContainer {
|
||||
text-align: center;
|
||||
margin-top: ($theme-spacing) * 2;
|
||||
margin-bottom: ($theme-spacing) * 2;
|
||||
}
|
||||
|
||||
.retryContainer {
|
||||
text-align: center;
|
||||
}
|
18
client/src/behaviors/TriggerDuoPushAuth.ts
Normal file
18
client/src/behaviors/TriggerDuoPushAuth.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { Dispatch } from "redux";
|
||||
import AutheliaService from "../services/AutheliaService";
|
||||
import { triggerDuoPushAuth, triggerDuoPushAuthSuccess, triggerDuoPushAuthFailure } from "../reducers/Portal/SecondFactor/actions";
|
||||
|
||||
export default async function(dispatch: Dispatch, redirectionUrl: string | null) {
|
||||
dispatch(triggerDuoPushAuth());
|
||||
try {
|
||||
const res = await AutheliaService.triggerDuoPush(redirectionUrl);
|
||||
const body = await res.json();
|
||||
if ('error' in body) {
|
||||
throw new Error(body['error']);
|
||||
}
|
||||
dispatch(triggerDuoPushAuthSuccess());
|
||||
return body;
|
||||
} catch (err) {
|
||||
dispatch(triggerDuoPushAuthFailure(err.message))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
import React from 'react';
|
||||
|
||||
import classnames from 'classnames';
|
||||
import CircleLoader, { Status } from '../../components/CircleLoader/CircleLoader';
|
||||
import styles from '../../assets/scss/components/SecondFactorDuoPush/SecondFactorDuoPush.module.scss';
|
||||
import { Button } from '@material/react-button';
|
||||
|
||||
export interface OwnProps {
|
||||
redirectionUrl: string | null;
|
||||
}
|
||||
|
||||
export interface StateProps {
|
||||
duoPushVerified: boolean | null;
|
||||
duoPushError: string | null;
|
||||
}
|
||||
|
||||
export interface DispatchProps {
|
||||
onInit: () => void;
|
||||
onRetryClicked: () => void;
|
||||
}
|
||||
|
||||
export type Props = OwnProps & StateProps & DispatchProps;
|
||||
|
||||
export default class SecondFactorDuoPush extends React.Component<Props> {
|
||||
componentWillMount() {
|
||||
this.props.onInit();
|
||||
}
|
||||
|
||||
render() {
|
||||
let u2fStatus = Status.LOADING;
|
||||
if (this.props.duoPushVerified === true) {
|
||||
u2fStatus = Status.SUCCESSFUL;
|
||||
} else if (this.props.duoPushError) {
|
||||
u2fStatus = Status.FAILURE;
|
||||
}
|
||||
return (
|
||||
<div className={classnames('duo-push-view')}>
|
||||
<div>You will soon receive a push notification on your phone.</div>
|
||||
<div className={styles.imageContainer}>
|
||||
<CircleLoader status={u2fStatus}></CircleLoader>
|
||||
</div>
|
||||
{(u2fStatus == Status.FAILURE)
|
||||
? <div className={styles.retryContainer}>
|
||||
<Button raised onClick={this.props.onRetryClicked}>Retry</Button>
|
||||
</div>
|
||||
: null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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<Props> {
|
|||
if (method == 'u2f') {
|
||||
title = "Security Key";
|
||||
methodComponent = (<SecondFactorU2F redirectionUrl={this.props.redirectionUrl}></SecondFactorU2F>);
|
||||
} else if (method == "duo_push") {
|
||||
title = "Duo Push Notification";
|
||||
methodComponent = (<SecondFactorDuoPush redirectionUrl={this.props.redirectionUrl}></SecondFactorDuoPush>);
|
||||
} else {
|
||||
title = "One-Time Password"
|
||||
methodComponent = (<SecondFactorTOTP redirectionUrl={this.props.redirectionUrl}></SecondFactorTOTP>);
|
||||
|
@ -57,6 +62,7 @@ class SecondFactorForm extends Component<Props> {
|
|||
<div className={styles.buttonsContainer}>
|
||||
<Button raised onClick={this.props.onOneTimePasswordMethodClicked}>One-Time Password</Button>
|
||||
<Button raised onClick={this.props.onSecurityKeyMethodClicked}>Security Key (U2F)</Button>
|
||||
<Button raised onClick={this.props.onDuoPushMethodClicked}>Duo Push Notification</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -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);
|
|
@ -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)),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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<SecondFactorState>;
|
||||
|
@ -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;
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -113,6 +113,21 @@ class AutheliaService {
|
|||
})
|
||||
}
|
||||
|
||||
static async triggerDuoPush(redirectionUrl: string | null): Promise<any> {
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'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',
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
|
||||
type Method2FA = "u2f" | "totp";
|
||||
type Method2FA = "u2f" | "totp" | "duo_push";
|
||||
|
||||
export default Method2FA;
|
|
@ -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.
|
||||
|
|
12
example/compose/duo-api/Dockerfile
Normal file
12
example/compose/duo-api/Dockerfile
Normal file
|
@ -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"]
|
6
example/compose/duo-api/docker-compose.yml
Normal file
6
example/compose/duo-api/docker-compose.yml
Normal file
|
@ -0,0 +1,6 @@
|
|||
version: '2'
|
||||
services:
|
||||
duo-api:
|
||||
image: authelia-duo-api
|
||||
networks:
|
||||
- authelianet
|
66
example/compose/duo-api/duo_api.js
Normal file
66
example/compose/duo-api/duo_api.js
Normal file
|
@ -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]);
|
||||
});
|
||||
});
|
10
example/compose/duo-api/duo_client.js
Normal file
10
example/compose/duo-api/duo_client.js
Normal file
|
@ -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);
|
358
example/compose/duo-api/package-lock.json
generated
Normal file
358
example/compose/duo-api/package-lock.json
generated
Normal file
|
@ -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="
|
||||
}
|
||||
}
|
||||
}
|
14
example/compose/duo-api/package.json
Normal file
14
example/compose/duo-api/package.json
Normal file
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
62
package-lock.json
generated
62
package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
|
||||
export interface DuoPushConfiguration {
|
||||
hostname: string;
|
||||
integration_key: string;
|
||||
secret_key: string;
|
||||
}
|
109
server/src/lib/routes/secondfactor/duo-push/Post.spec.ts
Normal file
109
server/src/lib/routes/secondfactor/duo-push/Post.spec.ts
Normal file
|
@ -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;
|
||||
});
|
||||
});
|
51
server/src/lib/routes/secondfactor/duo-push/Post.ts
Normal file
51
server/src/lib/routes/secondfactor/duo-push/Post.ts
Normal file
|
@ -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<DuoResponse> {
|
||||
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);
|
||||
}
|
||||
};
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
5
test/helpers/assertions/VerifyHasAppeared.ts
Normal file
5
test/helpers/assertions/VerifyHasAppeared.ts
Normal file
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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:
|
||||
|
|
12
test/suites/duo-push/README.md
Normal file
12
test/suites/duo-push/README.md
Normal file
|
@ -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.
|
116
test/suites/duo-push/config.yml
Normal file
116
test/suites/duo-push/config.yml
Normal file
|
@ -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
|
||||
|
36
test/suites/duo-push/environment.ts
Normal file
36
test/suites/duo-push/environment.ts
Normal file
|
@ -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
|
||||
};
|
64
test/suites/duo-push/scenarii/DuoPushNotification.ts
Normal file
64
test/suites/duo-push/scenarii/DuoPushNotification.ts
Normal file
|
@ -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"));
|
||||
});
|
||||
});
|
||||
}
|
16
test/suites/duo-push/test.ts
Normal file
16
test/suites/duo-push/test.ts
Normal file
|
@ -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);
|
||||
});
|
29
test/suites/duo-push/users_database.yml
Normal file
29
test/suites/duo-push/users_database.yml
Normal file
|
@ -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
|
Loading…
Reference in New Issue
Block a user