Almost full authentication workflow with U2F and TOTP.

This commit is contained in:
Clement Michaud 2019-01-13 21:35:46 +01:00
parent fe14bde29b
commit 9d7155a969
68 changed files with 1190 additions and 505 deletions

View File

@ -1,6 +1,6 @@
FROM node:10.15.0-jessie
WORKDIR /usr/app
WORKDIR /usr/app/client
ADD package.json package.json

View File

@ -5,9 +5,10 @@ services:
context: client-react
restart: always
volumes:
- ./client-react/tsconfig.json:/usr/app/tsconfig.json
- ./client-react/public:/usr/app/public
- ./client-react/src:/usr/app/src
- ./client-react/tsconfig.json:/usr/app/client/tsconfig.json
- ./client-react/public:/usr/app/client/public
- ./client-react/src:/usr/app/client/src
- ./shared:/usr/app/shared
networks:
example-network:
aliases:

View File

@ -965,6 +965,19 @@
"resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.1.tgz",
"integrity": "sha512-eqz8c/0kwNi/OEHQfvIuJVLTst3in0e7uTKeuY+WL/zfKn0xVujOTp42bS/vUUokhK5P2BppLd9JXMOMHcgbjA=="
},
"@types/qrcode.react": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@types/qrcode.react/-/qrcode.react-0.8.1.tgz",
"integrity": "sha512-OpMOBjWIMTnC1sdLcFgif/cXZYiPQGUN2yDaxC2EmZaAmElxehE+toMzPZvUJVXNyFXFdmYSDRWsjKtTTnqqAQ==",
"requires": {
"@types/react": "*"
}
},
"@types/query-string": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/@types/query-string/-/query-string-6.2.0.tgz",
"integrity": "sha512-dnYqKg7eZ+t7ZhCuBtwLxjqON8yXr27hiu3zXfPqxfJSbWUZNwwISE0BJUxghlcKsk4lZSp7bdFSJBJVNWBfmA=="
},
"@types/react": {
"version": "16.7.18",
"resolved": "https://registry.npmjs.org/@types/react/-/react-16.7.18.tgz",
@ -982,6 +995,15 @@
"@types/react": "*"
}
},
"@types/react-redux": {
"version": "6.0.12",
"resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-6.0.12.tgz",
"integrity": "sha512-fvcpm7cfW/JMflRdZgegCVbSGYt/hyEWQKriesaLZDRDjBGKQsAiui08VCQg5lBpocPmulVGKFhICtOAcMUPOQ==",
"requires": {
"@types/react": "*",
"redux": "^4.0.0"
}
},
"@types/react-router": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-4.4.3.tgz",
@ -1009,6 +1031,14 @@
"@types/react": "*"
}
},
"@types/redux-thunk": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@types/redux-thunk/-/redux-thunk-2.1.0.tgz",
"integrity": "sha1-vCtulylhgxr7gqm/TwZybjUflBY=",
"requires": {
"redux-thunk": "*"
}
},
"@types/tapable": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.2.tgz",
@ -1819,6 +1849,11 @@
}
}
},
"await-to-js": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/await-to-js/-/await-to-js-2.1.1.tgz",
"integrity": "sha512-CHBC6gQGCIzjZ09tJ+XmpQoZOn4GdWePB4qUweCaKNJ0D3f115YdhmYVTZ4rMVpiJ3cFzZcTYK1VMYEICV4YXw=="
},
"aws-sign2": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
@ -13362,11 +13397,34 @@
"resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
"integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc="
},
"qr.js": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/qr.js/-/qr.js-0.0.0.tgz",
"integrity": "sha1-ys6GOG9ZoNuAUPqQ2baw6IoeNk8="
},
"qrcode.react": {
"version": "0.9.2",
"resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-0.9.2.tgz",
"integrity": "sha512-opV0IA4w84qMaZg3hhgmktDs1xjfx3K7RAOzdvmKgkLdhmtv95AYGZmlG0s3NIAZ1qXCK4AyPJayLd3sa6p/RA==",
"requires": {
"prop-types": "^15.6.0",
"qr.js": "0.0.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=="
},
"query-string": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/query-string/-/query-string-6.2.0.tgz",
"integrity": "sha512-5wupExkIt8RYL4h/FE+WTg3JHk62e6fFPWtAZA9J5IWK1PfTfKkMS93HBUHcFpeYi9KsY5pFbh+ldvEyaz5MyA==",
"requires": {
"decode-uri-component": "^0.2.0",
"strict-uri-encode": "^2.0.0"
}
},
"querystring": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
@ -13653,14 +13711,6 @@
"warning": "^4.0.1"
}
},
"react-form-validator-core": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/react-form-validator-core/-/react-form-validator-core-0.5.1.tgz",
"integrity": "sha512-E+PO/F66q4LtvebzJo3R8zoW0il8yjS1cZGjRuV9WBNuTgBa00Ii+qH1EQcIZowhV9T/nkGT3XTp58TangDr3w==",
"requires": {
"react-lifecycles-compat": "^3.0.2"
}
},
"react-is": {
"version": "16.7.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.7.0.tgz",
@ -13671,12 +13721,17 @@
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
},
"react-material-ui-form-validator": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/react-material-ui-form-validator/-/react-material-ui-form-validator-2.0.3.tgz",
"integrity": "sha512-xXiY1wdlpu5UhDrJGaLwrDCNHl5jTykVJ4oEvc1daDh8hrEzt+ehaZqW6x9JZLH3DS86dHZg7cSZQJYiP7ZPZg==",
"react-redux": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-6.0.0.tgz",
"integrity": "sha512-EmbC3uLl60pw2VqSSkj6HpZ6jTk12RMrwXMBdYtM6niq0MdEaRq9KYCwpJflkOZj349BLGQm1MI/JO1W96kLWQ==",
"requires": {
"react-form-validator-core": "0.5.1"
"@babel/runtime": "^7.2.0",
"hoist-non-react-statics": "^3.2.1",
"invariant": "^2.2.4",
"loose-envify": "^1.4.0",
"prop-types": "^15.6.2",
"react-is": "^16.6.3"
}
},
"react-router": {
@ -14142,6 +14197,20 @@
"minimatch": "3.0.4"
}
},
"redux": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-4.0.1.tgz",
"integrity": "sha512-R7bAtSkk7nY6O/OYMVR9RiBI+XghjF9rlbl5806HJbQph0LJVHZrU5oaO4q70eUKiqMRqm4y07KLTlMZ2BlVmg==",
"requires": {
"loose-envify": "^1.4.0",
"symbol-observable": "^1.2.0"
}
},
"redux-thunk": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz",
"integrity": "sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw=="
},
"regenerate": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz",
@ -15494,6 +15563,11 @@
"resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz",
"integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI="
},
"strict-uri-encode": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz",
"integrity": "sha1-ucczDHBChi9rFC3CdLvMWGbONUY="
},
"string-length": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/string-length/-/string-length-2.0.0.tgz",
@ -16041,11 +16115,21 @@
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
},
"typesafe-actions": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/typesafe-actions/-/typesafe-actions-3.0.0.tgz",
"integrity": "sha512-NLpRc/FY+jPfWL0aUXQzjxPyF0Xug2om6akaoRLQ18KGwP2yYNBJu9vkv2q1q+Cx/+edy2Qf6O8xXnYY/xwz1A=="
},
"typescript": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.2.2.tgz",
"integrity": "sha512-VCj5UiSyHBjwfYacmDuc/NOk4QQixbE+Wn7MFJuS0nRuPQbof132Pw4u53dm264O8LPc2MVsc7RJNml5szurkg=="
},
"u2f-api": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/u2f-api/-/u2f-api-1.0.10.tgz",
"integrity": "sha512-0Zh+IqM2l6xaA/IUJDT9WwoEM6nwPx6ive4flVVYEfzzgXIrKFRaenieItsEkbXIgOZEw13nO3o3oLtSDOyesA=="
},
"ua-parser-js": {
"version": "0.7.19",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.19.tgz",

View File

@ -8,16 +8,27 @@
"@types/classnames": "^2.2.7",
"@types/jss": "^9.5.7",
"@types/node": "^10.12.2",
"@types/qrcode.react": "^0.8.1",
"@types/query-string": "^6.2.0",
"@types/react": "^16.4.18",
"@types/react-dom": "^16.0.9",
"@types/react-redux": "^6.0.12",
"@types/react-router-dom": "^4.3.1",
"@types/redux-thunk": "^2.1.0",
"await-to-js": "^2.1.1",
"classnames": "^2.2.6",
"jss": "^9.8.7",
"qrcode.react": "^0.9.2",
"query-string": "^6.2.0",
"react": "^16.6.0",
"react-dom": "^16.6.0",
"react-redux": "^6.0.0",
"react-router-dom": "^4.3.1",
"react-scripts": "^2.1.3",
"typescript": "^3.1.6"
"redux-thunk": "^2.3.0",
"typesafe-actions": "^3.0.0",
"typescript": "^3.1.6",
"u2f-api": "^1.0.10"
},
"scripts": {
"start": "react-scripts start",

View File

@ -4,21 +4,31 @@ import './App.css';
import { Router, Route, Switch } from "react-router-dom";
import { routes } from './routes/index';
import { createBrowserHistory } from 'history';
import { createStore, applyMiddleware } from 'redux';
import reducer from './reducers';
import { Provider } from 'react-redux';
import thunk from 'redux-thunk';
const history = createBrowserHistory();
const store = createStore(
reducer,
applyMiddleware(thunk)
);
class App extends Component {
render() {
return (
<Router history={history}>
<div className="App">
<Switch>
{routes.map((r, key) => {
return <Route path={r.path} component={r.component} key={key}/>
})}
</Switch>
</div>
</Router>
<Provider store={store}>
<Router history={history}>
<div className="App">
<Switch>
{routes.map((r, key) => {
return <Route path={r.path} component={r.component} key={key}/>
})}
</Switch>
</div>
</Router>
</Provider>
);
}
}

View File

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 286.054 286.054" style="enable-background:new 0 0 286.054 286.054;" xml:space="preserve">
<g>
<path style="fill:#E2574C;" d="M168.352,142.924l25.28-25.28c3.495-3.504,3.495-9.154,0-12.64l-12.64-12.649
c-3.495-3.486-9.145-3.495-12.64,0l-25.289,25.289l-25.271-25.271c-3.504-3.504-9.163-3.504-12.658-0.018l-12.64,12.649
c-3.495,3.486-3.486,9.154,0.018,12.649l25.271,25.271L92.556,168.15c-3.495,3.495-3.495,9.145,0,12.64l12.64,12.649
c3.495,3.486,9.145,3.495,12.64,0l25.226-25.226l25.405,25.414c3.504,3.504,9.163,3.504,12.658,0.009l12.64-12.64
c3.495-3.495,3.486-9.154-0.009-12.658L168.352,142.924z M143.027,0.004C64.031,0.004,0,64.036,0,143.022
c0,78.996,64.031,143.027,143.027,143.027s143.027-64.031,143.027-143.027C286.054,64.045,222.022,0.004,143.027,0.004z
M143.027,259.232c-64.183,0-116.209-52.026-116.209-116.209s52.026-116.21,116.209-116.21s116.209,52.026,116.209,116.209
S207.21,259.232,143.027,259.232z"/>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,22 @@
import { createStyles, Theme } from "@material-ui/core";
const styles = createStyles((theme: Theme) => ({
messageOuter: {
position: 'relative',
paddingTop: theme.spacing.unit * 2,
paddingBottom: theme.spacing.unit,
},
messageInner: {
width: '100%',
},
messageContainer: {
color: 'white',
fontSize: theme.typography.fontSize * 0.9,
padding: theme.spacing.unit * 2,
border: '1px solid red',
borderRadius: '5px',
backgroundColor: '#ff8d8d',
},
}));
export default styles;

View File

@ -0,0 +1,41 @@
import { createStyles, Theme } from "@material-ui/core";
const styles = createStyles((theme: Theme) => ({
mainContent: {
width: '440px',
margin: '0 auto',
padding: '50px 0px',
},
frame: {
boxShadow: 'rgba(0,0,0,0.14902) 0px 1px 1px 0px,rgba(0,0,0,0.09804) 0px 1px 2px 0px',
backgroundColor: 'white',
borderRadius: '5px',
padding: '30px 40px',
},
innerFrame: {
width: '100%',
},
title: {
fontSize: '1.4em',
fontWeight: 'bold',
borderBottom: '1px solid #c7c7c7',
display: 'inline-block',
paddingRight: '10px',
paddingBottom: '5px',
},
content: {
paddingTop: theme.spacing.unit * 2,
paddingBottom: theme.spacing.unit,
},
footer: {
marginTop: '10px',
textAlign: 'center',
fontSize: '0.65em',
color: 'grey',
'& a': {
color: 'grey',
}
},
}));
export default styles;

View File

@ -2,7 +2,7 @@ import { createStyles, Theme } from "@material-ui/core";
const styles = createStyles((theme: Theme) => ({
fields: {
marginTop: theme.spacing.unit * 3,
marginTop: theme.spacing.unit * 2,
marginBottom: theme.spacing.unit,
},
field: {
@ -23,6 +23,7 @@ const styles = createStyles((theme: Theme) => ({
},
rememberMe: {
float: 'left',
fontSize: theme.typography.fontSize * 0.8,
},
resetPassword: {
padding: '12px 0px',
@ -30,7 +31,7 @@ const styles = createStyles((theme: Theme) => ({
'& a': {
color: 'black',
},
},
},
}));
export default styles;

View File

@ -0,0 +1,15 @@
import { createStyles, Theme } from "@material-ui/core";
const styles = createStyles((theme: Theme) => ({
secretContainer: {
width: '100%',
},
qrcodeContainer: {
textAlign: 'center',
},
textField: {
width: '100%',
},
}));
export default styles;

View File

@ -1,15 +1,35 @@
import { createStyles, Theme } from "@material-ui/core";
import { isAbsolute } from "path";
const styles = createStyles((theme: Theme) => ({
container: {
position: 'relative',
paddingLeft: theme.spacing.unit,
paddingRight: theme.spacing.unit,
},
hello: {},
logout: {},
header: {
fontSize: theme.typography.fontSize * 1.5,
marginBottom: theme.spacing.unit,
position: 'relative',
'& $hello': {
display: 'inline-block',
},
'& $logout': {
position: 'absolute',
bottom: '0px',
right: '0px',
fontSize: theme.typography.fontSize * 0.9,
},
},
body: {
paddingTop: theme.spacing.unit * 2,
paddingBottom: theme.spacing.unit * 2,
paddingLeft: theme.spacing.unit,
paddingRight: theme.spacing.unit,
border: '1px solid #e0e0e0',
borderRadius: '2px',
textAlign: 'justify',
},
image: {
width: '120px',
@ -20,6 +40,7 @@ const styles = createStyles((theme: Theme) => ({
marginBottom: theme.spacing.unit * 2,
},
footer: {
paddingTop: theme.spacing.unit,
fontSize: theme.typography.fontSize * 0.9,
},
registerDevice: {

View File

@ -0,0 +1,15 @@
import { createStyles, Theme } from "@material-ui/core";
const styles = createStyles((theme: Theme) => ({
infoContainer: {
marginBottom: theme.spacing.unit * 2,
},
imageContainer: {
textAlign: 'center',
'& img': {
width: '120px',
},
},
}));
export default styles;

View File

@ -0,0 +1,27 @@
import React, { Component } from "react";
import { withStyles, WithStyles, Collapse } from "@material-ui/core";
import styles from '../../assets/jss/components/FormNotification/FormNotification';
interface Props extends WithStyles {
show: boolean;
}
class FormNotification extends Component<Props> {
render() {
const { classes } = this.props;
return (
<Collapse in={this.props.show}>
<div className={classes.messageOuter}>
<div className={classes.messageInner}>
<div className={classes.messageContainer}>
{this.props.children}
</div>
</div>
</div>
</Collapse>
)
}
}
export default withStyles(styles)(FormNotification);

View File

@ -0,0 +1,28 @@
import React, { Component } from "react";
import RemoteState from "../../reducers/Portal/RemoteState";
import { WithState } from "./WithState";
export type OnLoaded = (state: RemoteState) => void;
export type OnError = (err: Error) => void;
export interface Props extends WithState {
fetch: (onloaded: OnLoaded, onerror: OnError) => void;
onLoaded: OnLoaded;
onError?: OnError;
}
class StateSynchronizer extends Component<Props> {
componentWillMount() {
this.props.fetch(
(state) => this.props.onLoaded(state),
(err: Error) => {
if (this.props.onError) this.props.onError(err);
});
}
render() {
return null;
}
}
export default StateSynchronizer;

View File

@ -0,0 +1,7 @@
import RemoteState from '../../reducers/Portal/RemoteState';
export interface WithState {
state: RemoteState | null;
stateError: string | null;
stateLoading: boolean;
}

View File

@ -0,0 +1,31 @@
import { connect } from 'react-redux';
import StateSynchronizer, { OnLoaded, OnError } from '../../../components/StateSynchronizer/StateSynchronizer';
import { RootState } from '../../../reducers';
import { fetchStateSuccess, fetchState, fetchStateFailure } from '../../../reducers/Portal/actions';
import RemoteState from '../../../reducers/Portal/RemoteState';
import { Dispatch } from 'redux';
const mapStateToProps = (state: RootState) => ({
state: state.remoteState,
stateError: state.remoteStateError,
stateLoading: state.remoteStateLoading,
});
const mapDispatchToProps = (dispatch: Dispatch) => {
return {
fetch: (onloaded: OnLoaded, onerror: OnError) => {
dispatch(fetchState());
fetch('/api/state').then(async (res) => {
const body = await res.json() as RemoteState;
await dispatch(fetchStateSuccess(body));
await onloaded(body);
})
.catch(async (err) => {
await dispatch(fetchStateFailure(err));
await onerror(err);
})
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(StateSynchronizer);

View File

@ -0,0 +1,9 @@
import { connect } from 'react-redux';
import PortalLayout from '../../../layouts/PortalLayout/PortalLayout';
import { RootState } from '../../../reducers';
const mapStateToProps = (state: RootState) => ({
authenticationLevel: (state.remoteState) ? state.remoteState.authentication_level : 0,
});
export default connect(mapStateToProps)(PortalLayout);

View File

@ -0,0 +1,41 @@
import { connect } from 'react-redux';
import FirstFactorView, { Props } from '../../../views/FirstFactorView/FirstFactorView';
import { Dispatch } from 'redux';
import { authenticateFailure, authenticateSuccess, authenticate } from '../../../reducers/Portal/actions';
import { RootState } from '../../../reducers';
const mapStateToProps = (state: RootState) => ({});
function onAuthenticationRequested(dispatch: Dispatch, ownProps: Props) {
return async (username: string, password: string) => {
dispatch(authenticate());
fetch('/api/firstfactor', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
username: username,
password: password,
})
}).then(async (res) => {
const json = await res.json();
if ('error' in json) {
dispatch(authenticateFailure(json['error']));
return;
}
dispatch(authenticateSuccess());
ownProps.history.push('/2fa');
});
}
}
const mapDispatchToProps = (dispatch: Dispatch, ownProps: Props) => {
return {
onAuthenticationRequested: onAuthenticationRequested(dispatch, ownProps),
}
}
export default connect(mapStateToProps, mapDispatchToProps)(FirstFactorView);

View File

@ -0,0 +1,44 @@
import { connect } from 'react-redux';
import OneTimePasswordRegistrationView, { OnSuccess, OnFailure } from '../../../views/OneTimePasswordRegistrationView/OneTimePasswordRegistrationView';
import { RootState } from '../../../reducers';
import { Dispatch } from 'redux';
import {to} from 'await-to-js';
const mapStateToProps = (state: RootState) => ({});
async function checkIdentity(token: string) {
return fetch(`/api/secondfactor/totp/identity/finish?token=${token}`, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
})
.then(async (res) => {
if (res.status !== 200) {
throw new Error('Status code ' + res.status);
}
const body = await res.json();
if ('error' in body) {
throw new Error(body['error']);
}
return body;
});
}
const mapDispatchToProps = (dispatch: Dispatch) => {
return {
componentDidMount: async (token: string, onSuccess: OnSuccess, onFailure: OnFailure) => {
let err, result;
[err, result] = await to(checkIdentity(token));
if (err) {
onFailure(err);
return;
}
onSuccess(result.otpauth_url);
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(OneTimePasswordRegistrationView);

View File

@ -0,0 +1,116 @@
import { connect } from 'react-redux';
import SecondFactorView, {Props} from '../../../views/SecondFactorView/SecondFactorView';
import { RootState } from '../../../reducers';
import { Dispatch } from 'redux';
import u2fApi, { SignResponse } from 'u2f-api';
import to from 'await-to-js';
import { logoutSuccess, logoutFailure, logout } from '../../../reducers/Portal/actions';
import AuthenticationLevel from '../../../types/AuthenticationLevel';
import RemoteState from '../../../reducers/Portal/RemoteState';
const mapStateToProps = (state: RootState) => ({
state: state.remoteState,
stateError: state.remoteStateError,
});
async function requestSigning() {
return fetch('/api/u2f/sign_request')
.then(async (res) => {
if (res.status !== 200) {
throw new Error('Status code ' + res.status);
}
return res.json();
});
}
async function completeSigning(response: u2fApi.SignResponse) {
return fetch('/api/u2f/sign', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(response),
})
.then(async (res) => {
if (res.status !== 200) {
throw new Error('Status code ' + res.status);
}
});
}
async function triggerSecurityKeySigning() {
let err, result;
[err, result] = await to(requestSigning());
if (err) {
console.error(err);
return;
}
[err, result] = await to(u2fApi.sign(result, 60));
if (err) {
console.error(err);
return;
}
[err, result] = await to(completeSigning(result as SignResponse));
if (err) {
console.error(err);
return;
}
}
const mapDispatchToProps = (dispatch: Dispatch, ownProps: Props) => {
return {
onLogoutClicked: () => {
dispatch(logout());
fetch('/api/logout', {
method: 'POST',
})
.then(async (res) => {
if (res.status != 200) {
throw new Error('Status code ' + res.status);
}
await dispatch(logoutSuccess());
ownProps.history.push('/');
})
.catch(async (err: string) => {
console.error(err);
await dispatch(logoutFailure(err));
});
},
onRegisterSecurityKeyClicked: () => {
fetch('/api/secondfactor/u2f/identity/start', {
method: 'POST',
})
.then(async (res) => {
if (res.status != 200) {
throw new Error('Status code ' + res.status);
}
ownProps.history.push('/confirmation-sent');
})
.catch((err) => console.error(err));
},
onRegisterOneTimePasswordClicked: () => {
fetch('/api/secondfactor/totp/identity/start', {
method: 'POST',
})
.then(async (res) => {
if (res.status != 200) {
throw new Error('Status code ' + res.status);
}
ownProps.history.push('/confirmation-sent');
})
.catch((err) => console.error(err));
},
onStateLoaded: async (state: RemoteState) => {
if (state.authentication_level < AuthenticationLevel.ONE_FACTOR) {
ownProps.history.push('/');
return;
}
await triggerSecurityKeySigning();
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(SecondFactorView);

View File

@ -0,0 +1,72 @@
import { connect } from 'react-redux';
import SecurityKeyRegistrationView from '../../../views/SecurityKeyRegistrationView/SecurityKeyRegistrationView';
import { RootState } from '../../../reducers';
import { Dispatch } from 'redux';
import {to} from 'await-to-js';
import * as U2fApi from "u2f-api";
const mapStateToProps = (state: RootState) => ({});
async function checkIdentity(token: string) {
return fetch(`/api/secondfactor/u2f/identity/finish?token=${token}`, {
method: 'POST',
});
}
async function requestRegistration() {
return fetch('/api/u2f/register_request')
.then(async (res) => {
if (res.status !== 200) {
throw new Error('Status code ' + res.status);
}
return res.json();
});
}
async function completeRegistration(response: U2fApi.RegisterResponse) {
return fetch('/api/u2f/register', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(response),
})
.then(async (res) => {
if (res.status !== 200) {
throw new Error('Status code ' + res.status);
}
});
}
const mapDispatchToProps = (dispatch: Dispatch) => {
return {
componentDidMount: async (token: string) => {
let err, result;
[err, result] = await to(checkIdentity(token));
if (err) {
console.error(err);
return;
}
[err, result] = await to(requestRegistration());
if (err) {
console.error(err);
return;
}
[err, result] = await to(U2fApi.register(result, [], 60));
if (err) {
console.error(err);
return;
}
[err, result] = await to(completeRegistration(result as U2fApi.RegisterResponse));
if (err) {
console.error(err);
return;
}
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(SecurityKeyRegistrationView);

View File

@ -1,41 +0,0 @@
.mainContent {
width: 440px;
margin: 0 auto;
padding: 50px 0px;
}
/* FRAME */
.frame {
box-shadow: rgba(0,0,0,0.14902) 0px 1px 1px 0px,rgba(0,0,0,0.09804) 0px 1px 2px 0px;
background-color: white;
border-radius: 5px;
padding: 30px 40px;
}
.innerFrame {
width: 100%;
}
.title {
font-size: 1.4em;
font-weight: bold;
border-bottom: 1px solid #c7c7c7;
display: inline-block;
padding-right: 10px;
padding-bottom: 5px;
}
/* FOOTER */
.footer {
margin-top: 10px;
text-align: center;
font-size: 0.65em;
color: grey;
}
.footer a {
color: grey;
}

View File

@ -1,15 +1,19 @@
import React, { Component } from "react";
import styles from "./PortalLayout.module.css"
import { Route, Switch, Redirect, RouterProps, RouteProps } from "react-router";
import { routes } from '../../routes/routes';
import { AUTHELIA_GITHUB_URL } from "../../constants";
import { WithStyles, withStyles } from "@material-ui/core";
interface Props extends RouterProps, RouteProps {}
import styles from '../../assets/jss/layouts/PortalLayout/PortalLayout';
import AuthenticationLevel from "../../types/AuthenticationLevel";
export default class PortalLayout extends Component<Props> {
interface Props extends RouterProps, RouteProps, WithStyles {
authenticationLevel: AuthenticationLevel;
}
class PortalLayout extends Component<Props> {
private renderTitle() {
if (!this.props.location) return;
@ -24,14 +28,15 @@ export default class PortalLayout extends Component<Props> {
render() {
const { classes } = this.props;
return (
<div className={styles.mainContent}>
<div className={styles.frame}>
<div className={styles.innerFrame}>
<div className={styles.title}>
<div className={classes.mainContent}>
<div className={classes.frame}>
<div className={classes.innerFrame}>
<div className={classes.title}>
{this.renderTitle()}
</div>
<div className={styles.content}>
<div className={classes.content}>
<Switch>
{routes.map((r, key) => {
return <Route path={r.path} component={r.component} exact={true} key={key} />
@ -41,10 +46,12 @@ export default class PortalLayout extends Component<Props> {
</div>
</div>
</div>
<div className={styles.footer}>
<div className={classes.footer}>
<div>Powered by <a href={AUTHELIA_GITHUB_URL}>Authelia</a></div>
</div>
</div>
)
}
}
}
export default withStyles(styles)(PortalLayout);

View File

@ -0,0 +1,8 @@
import AuthenticationLevel from '../../types/AuthenticationLevel';
interface RemoteState {
username: string;
authentication_level: AuthenticationLevel;
}
export default RemoteState;

View File

@ -0,0 +1,41 @@
import { createAction } from 'typesafe-actions';
import {
AUTHENTICATE_REQUEST,
AUTHENTICATE_SUCCESS,
AUTHENTICATE_FAILURE,
FETCH_STATE_REQUEST,
FETCH_STATE_SUCCESS,
FETCH_STATE_FAILURE,
LOGOUT_REQUEST,
LOGOUT_SUCCESS,
LOGOUT_FAILURE
} from "../constants";
import RemoteState from './RemoteState';
/* FETCH_STATE */
export const fetchState = createAction(FETCH_STATE_REQUEST);
export const fetchStateSuccess = createAction(FETCH_STATE_SUCCESS, resolve => {
return (state: RemoteState) => {
return resolve(state);
}
});
export const fetchStateFailure = createAction(FETCH_STATE_FAILURE, resolve => {
return (err: string) => {
return resolve(err);
}
})
/* AUTHENTICATE_REQUEST */
export const authenticate = createAction(AUTHENTICATE_REQUEST);
export const authenticateSuccess = createAction(AUTHENTICATE_SUCCESS);
export const authenticateFailure = createAction(AUTHENTICATE_FAILURE, resolve => {
return (error: string) => resolve(error);
});
/* AUTHENTICATE_REQUEST */
export const logout = createAction(LOGOUT_REQUEST);
export const logoutSuccess = createAction(LOGOUT_SUCCESS);
export const logoutFailure = createAction(LOGOUT_FAILURE, resolve => {
return (error: string) => resolve(error);
});

View File

@ -0,0 +1,106 @@
import * as Actions from './actions';
import { ActionType, getType, StateType } from 'typesafe-actions';
import RemoteState from './RemoteState';
export type FirstFactorAction = ActionType<typeof Actions>;
enum Result {
NONE,
SUCCESS,
FAILURE,
}
interface State {
lastResult: Result;
loading: boolean;
error: string | null;
remoteState: RemoteState | null;
remoteStateLoading: boolean;
remoteStateError: string | null;
logoutLoading: boolean;
logoutSuccess: boolean | null;
logoutError: string | null;
}
const initialState: State = {
lastResult: Result.NONE,
loading: false,
error: null,
remoteState: null,
remoteStateLoading: false,
remoteStateError: null,
logoutLoading: false,
logoutError: null,
logoutSuccess: null,
}
export type PortalState = StateType<State>;
export default (state = initialState, action: FirstFactorAction) => {
switch(action.type) {
case getType(Actions.authenticate):
return {
...state,
loading: true,
error: null
};
case getType(Actions.authenticateSuccess):
return {
...state,
lastResult: Result.SUCCESS,
loading: false,
error: null,
};
case getType(Actions.authenticateFailure):
return {
...state,
lastResult: Result.FAILURE,
loading: false,
error: action.payload,
};
case getType(Actions.fetchState):
return {
...state,
remoteState: null,
remoteStateError: null,
remoteStateLoading: true,
};
case getType(Actions.fetchStateSuccess):
return {
...state,
remoteState: action.payload,
remoteStateLoading: false,
};
case getType(Actions.fetchStateFailure):
return {
...state,
remoteStateError: action.payload,
remoteStateLoading: false,
};
case getType(Actions.logout):
return {
...state,
logoutLoading: true,
logoutSuccess: null,
logoutError: null,
};
case getType(Actions.logoutSuccess):
return {
...state,
logoutLoading: false,
logoutSuccess: true,
};
case getType(Actions.logoutFailure):
return {
...state,
logoutLoading: false,
logoutError: action.payload,
}
}
return state;
}

View File

@ -0,0 +1,12 @@
export const FETCH_STATE_REQUEST = '@portal/fetch_state_request';
export const FETCH_STATE_SUCCESS = '@portal/fetch_state_success';
export const FETCH_STATE_FAILURE = '@portal/fetch_state_failure';
export const AUTHENTICATE_REQUEST = '@portal/authenticate_request';
export const AUTHENTICATE_SUCCESS = '@portal/authenticate_success';
export const AUTHENTICATE_FAILURE = '@portal/authenticate_failure';
export const LOGOUT_REQUEST = '@portal/logout_request';
export const LOGOUT_SUCCESS = '@portal/logout_success';
export const LOGOUT_FAILURE = '@portal/logout_failure';

View File

@ -0,0 +1,5 @@
import PortalReducer, { PortalState } from './Portal/reducer';
export type RootState = PortalState;
export default PortalReducer;

View File

@ -1,4 +1,4 @@
import PortalLayout from "../layouts/PortalLayout/PortalLayout";
import PortalLayout from "../containers/layouts/PortalLayout/PortalLayout";
export const routes = [{
path: '/',

View File

@ -1,8 +1,8 @@
import FirstFactorView from "../views/FirstFactorView/FirstFactorView";
import SecondFactorView from "../views/SecondFactorView/SecondFactorView";
import ConfirmationSent from "../views/ConfirmationSentView/ConfirmationSentView";
import OneTimePasswordRegistrationView from "../views/OneTimePasswordRegistrationView/OneTimePasswordRegistrationView";
import SecurityKeyRegistrationView from "../views/SecurityKeyRegistrationView/SecurityKeyRegistrationView";
import FirstFactorView from "../containers/views/FirstFactorView/FirstFactorView";
import SecondFactorView from "../containers/views/SecondFactorView/SecondFactorView";
import ConfirmationSentView from "../views/ConfirmationSentView/ConfirmationSentView";
import OneTimePasswordRegistrationView from "../containers/views/OneTimePasswordRegistrationView/OneTimePasswordRegistrationView";
import SecurityKeyRegistrationView from "../containers/views/SecurityKeyRegistrationView/SecurityKeyRegistrationView";
import ForgotPasswordView from "../views/ForgotPasswordView/ForgotPasswordView";
import ResetPasswordView from "../views/ResetPasswordView/ResetPasswordView";
@ -15,9 +15,9 @@ export const routes = [{
title: '2-factor',
component: SecondFactorView,
}, {
path: '/confirm',
path: '/confirmation-sent',
title: 'e-mail sent',
component: ConfirmationSent
component: ConfirmationSentView
}, {
path: '/one-time-password-registration',
title: 'One-time password registration',

View File

@ -0,0 +1,7 @@
enum AuthenticationLevel {
NOT_AUTHENTICATED = 0,
ONE_FACTOR = 1,
TWO_FACTOR = 2
};
export default AuthenticationLevel;

View File

@ -9,7 +9,7 @@ import { RouterProps } from "react-router";
interface Props extends RouterProps {}
export default class ConfirmationSent extends Component<Props> {
class ConfirmationSentView extends Component<Props> {
render() {
return (
<div className={styles.main}>
@ -20,15 +20,17 @@ export default class ConfirmationSent extends Component<Props> {
Please check your e-mails and follow the instructions to confirm the operation.
<div className={styles.buttonContainer}>
<Button
onClick={() => this.props.history.push('/')}
onClick={() => this.props.history.goBack()}
className={styles.button}
variant="contained"
color="primary">
Back to login
Back
</Button>
</div>
</div>
</div>
)
}
}
}
export default ConfirmationSentView;

View File

@ -11,14 +11,24 @@ import { RouterProps } from "react-router";
import { WithStyles, withStyles } from "@material-ui/core";
import firstFactorViewStyles from '../../assets/jss/views/FirstFactorView/FirstFactorView';
import FormNotification from "../../components/FormNotification/FormNotification";
interface Props extends RouterProps, WithStyles {}
import CheckBoxOutlineBlankIcon from '@material-ui/icons/CheckBoxOutlineBlank'
import CheckBoxIcon from '@material-ui/icons/CheckBox';
import StateSynchronizer from "../../containers/components/StateSynchronizer/StateSynchronizer";
import RemoteState from "../../reducers/Portal/RemoteState";
export interface Props extends RouterProps, WithStyles {
onAuthenticationRequested(username: string, password: string): void;
}
interface State {
rememberMe: boolean;
username: string;
password: string;
loginButtonDisabled: boolean;
errorMessage: string | null;
remoteState: RemoteState | null;
}
class FirstFactorView extends Component<Props, State> {
@ -29,6 +39,8 @@ class FirstFactorView extends Component<Props, State> {
username: '',
password: '',
loginButtonDisabled: false,
errorMessage: null,
remoteState: null,
}
}
@ -56,10 +68,14 @@ class FirstFactorView extends Component<Props, State> {
}
}
render() {
private renderWithState(state: RemoteState) {
const { classes } = this.props;
return (
<div>
<FormNotification
show={this.state.errorMessage != null}>
{this.state.errorMessage || ''}
</FormNotification>
<div className={classes.fields}>
<div className={classes.field}>
<TextField
@ -97,6 +113,8 @@ class FirstFactorView extends Component<Props, State> {
<FormControlLabel
control={
<Checkbox
icon={<CheckBoxOutlineBlankIcon fontSize="small" />}
checkedIcon={<CheckBoxIcon fontSize="small" />}
checked={this.state.rememberMe}
onChange={this.toggleRememberMe}
color="primary"
@ -114,28 +132,34 @@ class FirstFactorView extends Component<Props, State> {
)
}
render() {
return (
<div>
<StateSynchronizer
onLoaded={(remoteState) => this.setState({remoteState})}/>
{this.state.remoteState ? this.renderWithState(this.state.remoteState) : null}
</div>
)
}
private authenticate() {
this.setState({loginButtonDisabled: true})
fetch('/api/firstfactor', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
username: this.state.username,
password: this.state.password,
})
}).then(async (res) => {
const json = await res.json();
if ('error' in json) {
console.log('ERROR!');
this.setState({loginButtonDisabled: false});
return;
}
this.props.history.push('/2fa');
this.setState({loginButtonDisabled: true});
this.props.onAuthenticationRequested(
this.state.username,
this.state.password);
this.setState({errorMessage: null});
}
onFailure = (error: string) => {
this.setState({
loginButtonDisabled: false,
errorMessage: 'An error occured. Your username/password are probably wrong.'
});
}
onSuccess = () => {
this.props.history.push('/2fa');
}
}
export default withStyles(firstFactorViewStyles)(FirstFactorView);

View File

@ -1,8 +1,88 @@
import React, { Component } from "react";
import { WithStyles, withStyles, TextField } from "@material-ui/core";
export default class OneTimePasswordRegistrationView extends Component {
render() {
return (<div></div>)
import styles from '../../assets/jss/views/OneTimePasswordRegistrationView/OneTimePasswordRegistrationView';
import { RouteProps } from "react-router";
import QueryString from 'query-string';
import QRCode from 'qrcode.react';
export type OnSuccess = (secret: Secret) => void;
export type OnFailure = (err: Error) => void;
export interface Props extends WithStyles, RouteProps {
componentDidMount: (token: string, onSuccess: OnSuccess, onFailure: OnFailure) => void;
}
export interface Secret {
otp_url: string;
base32_secret: string;
}
interface State {
secret: Secret | null;
}
class OneTimePasswordRegistrationView extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
secret: {
otp_url: 'https://coucou',
base32_secret: 'coucou',
},
}
}
}
componentDidMount() {
if (!this.props.location) {
console.error('There is no location to retrieve query params from...');
return;
}
const params = QueryString.parse(this.props.location.search);
if (!('token' in params)) {
console.error('Token parameter is expected and not provided');
return;
}
this.props.componentDidMount(
params['token'] as string,
this.onSuccess,
this.onFailure);
}
onSuccess = (secret: Secret) => {
this.setState({secret});
}
onFailure = (err: Error) => {}
private renderWithSecret(secret: Secret) {
const { classes } = this.props;
return (
<div>
<div className={classes.secretContainer}>
<TextField
id="totp-secret"
label="Key"
defaultValue={secret.base32_secret}
className={classes.textField}
margin="normal"
InputProps={{
readOnly: true,
}}
variant="outlined"
/>
</div>
<div className={classes.qrcodeContainer}>
{this.state.secret ? <QRCode value={this.state.secret.otp_url}></QRCode> : null}
</div>
</div>
)
}
render() {
return this.state.secret ? this.renderWithSecret(this.state.secret) : null;
}
}
export default withStyles(styles)(OneTimePasswordRegistrationView);

View File

@ -4,13 +4,24 @@ import { WithStyles, withStyles, Button, TextField } from '@material-ui/core';
import styles from '../../assets/jss/views/SecondFactorView/SecondFactorView';
import securityKeyImage from '../../assets/images/security-key-hand.png';
import StateSynchronizer from '../../containers/components/StateSynchronizer/StateSynchronizer';
import { RouterProps, Redirect } from 'react-router';
import RemoteState from '../../reducers/Portal/RemoteState';
import AuthenticationLevel from '../../types/AuthenticationLevel';
import { WithState } from '../../components/StateSynchronizer/WithState';
type Mode = 'u2f' | 'totp';
interface Props extends WithStyles {};
export interface Props extends WithStyles, RouterProps, WithState {
onLogoutClicked: () => void;
onRegisterSecurityKeyClicked: () => void;
onRegisterOneTimePasswordClicked: () => void;
onStateLoaded: (state: RemoteState) => void;
};
interface State {
mode: Mode;
remoteState: RemoteState | null;
}
class SecondFactorView extends Component<Props, State> {
@ -18,6 +29,7 @@ class SecondFactorView extends Component<Props, State> {
super(props);
this.state = {
mode: 'u2f',
remoteState: null,
}
}
@ -71,14 +83,32 @@ class SecondFactorView extends Component<Props, State> {
}
}
render() {
private onRegisterClicked = () => {
const mode = this.state.mode;
if (mode === 'u2f') {
this.props.onRegisterSecurityKeyClicked();
} else {
this.props.onRegisterOneTimePasswordClicked();
}
}
private renderWithState(state: RemoteState) {
if (state.authentication_level < AuthenticationLevel.ONE_FACTOR) {
return <Redirect to='/' key='redirect' />;
}
const { classes } = this.props;
return (
<div className={classes.container}>
<div className={classes.header}>
<div className={classes.hello}>Hello <b>{state.username}</b></div>
<div className={classes.logout}>
<a onClick={this.props.onLogoutClicked} href="#">Logout</a>
</div>
</div>
<div className={classes.body}>
{this.renderMode()}
</div>
<hr />
<div className={classes.footer}>
<a
className={classes.otherMethod}
@ -86,11 +116,28 @@ class SecondFactorView extends Component<Props, State> {
onClick={this.toggleMode}>
{this.state.mode === 'u2f' ? 'Use one-time password' : 'Use security key'}
</a>
<a className={classes.registerDevice} href="/security-key-registration">Register device</a>
<a className={classes.registerDevice} href="#" onClick={this.onRegisterClicked}>
Register device
</a>
</div>
</div>
)
}
onStateLoaded = (remoteState: RemoteState) => {
this.setState({remoteState});
this.props.onStateLoaded(remoteState);
}
render() {
return (
<div>
<StateSynchronizer
onLoaded={this.onStateLoaded}/>
{this.state.remoteState ? this.renderWithState(this.state.remoteState) : null}
</div>
)
}
}
export default withStyles(styles)(SecondFactorView);

View File

@ -1,8 +1,41 @@
import React, { Component } from "react";
import securityKeyImage from '../../assets/images/security-key-hand.png';
import { WithStyles, withStyles } from "@material-ui/core";
export default class SecurityKeyRegistrationView extends Component {
render() {
return (<div></div>)
import styles from '../../assets/jss/views/SecurityKeyRegistrationView/SecurityKeyRegistrationView';
import { RouteProps } from "react-router";
import QueryString from 'query-string';
interface Props extends WithStyles, RouteProps {
componentDidMount: (token: string) => void;
}
class SecurityKeyRegistrationView extends Component<Props> {
componentDidMount() {
if (!this.props.location) {
console.error('There is no location to retrieve query params from...');
return;
}
const params = QueryString.parse(this.props.location.search);
if (!('token' in params)) {
console.error('Token parameter is expected and not provided');
return;
}
this.props.componentDidMount(params['token'] as string);
}
}
render() {
const {classes} = this.props;
return (
<div>
<div className={classes.infoContainer}>Press the gold disk to register your security key</div>
<div className={classes.imageContainer}>
<img src={securityKeyImage} alt="security key" />
</div>
</div>
)
}
}
export default withStyles(styles)(SecurityKeyRegistrationView);

View File

@ -28,21 +28,6 @@ http {
proxy_pass $frontend_endpoint;
}
# Serves the portal application.
location /sockjs-node {
# Allow websockets for webpack to auto-reload.
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_pass $frontend_endpoint;
}
# Serves the portal application.
location /static {
proxy_pass $frontend_endpoint;
}
# Serve the backend API for the portal.
location /api {
@ -53,12 +38,6 @@ http {
proxy_intercept_errors on;
proxy_pass $backend_endpoint;
if ($request_method !~ ^(POST)$){
error_page 401 = /error/401;
error_page 403 = /error/403;
error_page 404 = /error/404;
}
}
}

111
package-lock.json generated
View File

@ -115,21 +115,6 @@
"integrity": "sha512-dWP6LJm9nKT6ALaa+bnL247GHHMWir3vSlZ2+IHgHgktZQx0L3Uvq2uAWcuzIe+fujRsYWBW2q622C5UvGK9iQ==",
"dev": true
},
"@babel/runtime": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.2.0.tgz",
"integrity": "sha512-oouEibCbHMVdZSDlJBO6bZmID/zA/G/Qx3H1d3rSNPTD+L8UNKvCat7aKWSJ74zYbm5zWGh0GQN0hKj8zYFTCg==",
"requires": {
"regenerator-runtime": "^0.12.0"
},
"dependencies": {
"regenerator-runtime": {
"version": "0.12.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz",
"integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg=="
}
}
},
"@babel/template": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.1.2.tgz",
@ -512,11 +497,6 @@
"integrity": "sha512-txsii9cwD2OUOPukfPu3Jpoi3CnznBAwRX3JF26EC4p5T6IA8AaL6PBilACyY2fJkk+ydDNo4BJrJOo/OmNaZw==",
"dev": true
},
"@types/prop-types": {
"version": "15.5.8",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.5.8.tgz",
"integrity": "sha512-3AQoUxQcQtLHsK25wtTWIoIpgYjH3vSDroZOUr7PpCHw/jLY1RB9z9E8dBT/OSmwStVgkRNvdh+ZHNiomRieaw=="
},
"@types/proxyquire": {
"version": "1.3.28",
"resolved": "https://registry.npmjs.org/@types/proxyquire/-/proxyquire-1.3.28.tgz",
@ -535,24 +515,6 @@
"integrity": "sha512-XRIZIMTxjcUukqQcYBdpFWGbcRDyNBXrvTEtTYgFMIbBNUVt+9mCKsU+jUUDLeFO/RXopUgR5OLiBqbY18vSHQ==",
"dev": true
},
"@types/react": {
"version": "16.7.18",
"resolved": "https://registry.npmjs.org/@types/react/-/react-16.7.18.tgz",
"integrity": "sha512-Tx4uu3ppK53/iHk6VpamMP3f3ahfDLEVt3ZQc8TFm30a1H3v9lMsCntBREswZIW/SKrvJjkb3Hq8UwO6GREBng==",
"requires": {
"@types/prop-types": "*",
"csstype": "^2.2.0"
}
},
"@types/react-redux": {
"version": "6.0.12",
"resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-6.0.12.tgz",
"integrity": "sha512-fvcpm7cfW/JMflRdZgegCVbSGYt/hyEWQKriesaLZDRDjBGKQsAiui08VCQg5lBpocPmulVGKFhICtOAcMUPOQ==",
"requires": {
"@types/react": "*",
"redux": "^4.0.0"
}
},
"@types/redis": {
"version": "2.8.6",
"resolved": "https://registry.npmjs.org/@types/redis/-/redis-2.8.6.tgz",
@ -2281,11 +2243,6 @@
"cssom": "0.3.x"
}
},
"csstype": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.0.tgz",
"integrity": "sha512-by8hi8BlLbowQq0qtkx54d9aN73R9oUW20HISpka5kmgsR9F7nnxgfsemuR2sdCKZh+CDNf5egW9UZMm4mgJRg=="
},
"cucumber": {
"version": "4.2.1",
"resolved": "http://registry.npmjs.org/cucumber/-/cucumber-4.2.1.tgz",
@ -4748,14 +4705,6 @@
"minimalistic-crypto-utils": "^1.0.1"
}
},
"hoist-non-react-statics": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.2.1.tgz",
"integrity": "sha512-TFsu3TV3YLY+zFTZDrN8L2DTFanObwmBLpWvJs1qfUuEQ5bTAdFcwfx2T/bsCXfM9QHSLvjfP+nihEl0yvozxw==",
"requires": {
"react-is": "^16.3.2"
}
},
"hooker": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz",
@ -4945,14 +4894,6 @@
"xtend": "^4.0.0"
}
},
"invariant": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
"integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
"requires": {
"loose-envify": "^1.0.0"
}
},
"invert-kv": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz",
@ -5394,7 +5335,8 @@
"js-tokens": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz",
"integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls="
"integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=",
"dev": true
},
"js-yaml": {
"version": "3.5.5",
@ -5888,14 +5830,6 @@
"resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz",
"integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc="
},
"loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"requires": {
"js-tokens": "^3.0.0 || ^4.0.0"
}
},
"loud-rejection": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz",
@ -8769,15 +8703,6 @@
"asap": "~2.0.3"
}
},
"prop-types": {
"version": "15.6.2",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.2.tgz",
"integrity": "sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ==",
"requires": {
"loose-envify": "^1.3.1",
"object-assign": "^4.1.1"
}
},
"proxy-addr": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.3.tgz",
@ -9105,24 +9030,6 @@
}
}
},
"react-is": {
"version": "16.7.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.7.0.tgz",
"integrity": "sha512-Z0VRQdF4NPDoI0tsXVMLkJLiwEBa+RP66g0xDHxgxysxSoCUccSten4RTF/UFvZF1dZvZ9Zu1sx+MDXwcOR34g=="
},
"react-redux": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-6.0.0.tgz",
"integrity": "sha512-EmbC3uLl60pw2VqSSkj6HpZ6jTk12RMrwXMBdYtM6niq0MdEaRq9KYCwpJflkOZj349BLGQm1MI/JO1W96kLWQ==",
"requires": {
"@babel/runtime": "^7.2.0",
"hoist-non-react-statics": "^3.2.1",
"invariant": "^2.2.4",
"loose-envify": "^1.4.0",
"prop-types": "^15.6.2",
"react-is": "^16.6.3"
}
},
"read-only-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/read-only-stream/-/read-only-stream-2.0.0.tgz",
@ -9221,15 +9128,6 @@
"resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-2.6.0.tgz",
"integrity": "sha1-Uu0J2srBCPGmMcB+m2mUHnoZUEs="
},
"redux": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-4.0.1.tgz",
"integrity": "sha512-R7bAtSkk7nY6O/OYMVR9RiBI+XghjF9rlbl5806HJbQph0LJVHZrU5oaO4q70eUKiqMRqm4y07KLTlMZ2BlVmg==",
"requires": {
"loose-envify": "^1.4.0",
"symbol-observable": "^1.2.0"
}
},
"referrer-policy": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/referrer-policy/-/referrer-policy-1.1.0.tgz",
@ -10304,11 +10202,6 @@
"integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
"dev": true
},
"symbol-observable": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz",
"integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ=="
},
"symbol-tree": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.2.tgz",

View File

@ -24,7 +24,6 @@
"title": "Authelia API documentation"
},
"dependencies": {
"@types/react-redux": "^6.0.12",
"ajv": "^6.3.0",
"bluebird": "^3.5.0",
"body-parser": "^1.15.2",
@ -45,7 +44,6 @@
"pug": "^2.0.0-rc.2",
"randomatic": "^3.1.0",
"randomstring": "^1.1.5",
"react-redux": "^6.0.0",
"redis": "^2.8.0",
"speakeasy": "^2.0.0",
"u2f": "^0.1.2",

View File

@ -1,8 +1,6 @@
import express = require("express");
import U2f = require("u2f");
import BluebirdPromise = require("bluebird");
import { AuthenticationSession } from "../../types/AuthenticationSession";
import { IRequestLogger } from "./logging/IRequestLogger";
import { Level } from "./authentication/Level";
@ -36,7 +34,8 @@ export class AuthenticationSessionHandler {
}
if (!req.session.auth) {
logger.debug(req, "Authentication session %s was undefined. Resetting.", req.sessionID);
logger.debug(req, "Authentication session %s was undefined. Resetting..." +
" If it's unexpected, make sure you are visiting the expected domain.", req.sessionID);
AuthenticationSessionHandler.reset(req);
}

View File

@ -1,5 +1,4 @@
import express = require("express");
import BluebirdPromise = require("bluebird");
import { IRequestLogger } from "./logging/IRequestLogger";
function replyWithError(req: express.Request, res: express.Response,

View File

@ -1,7 +1,6 @@
import BluebirdPromise = require("bluebird");
import express = require("express");
import objectPath = require("object-path");
import Exceptions = require("./Exceptions");
import { AuthenticationSessionHandler } from "./AuthenticationSessionHandler";
import { IRequestLogger } from "./logging/IRequestLogger";

View File

@ -1,24 +1,15 @@
import sinon = require("sinon");
import IdentityValidator = require("./IdentityCheckMiddleware");
import { AuthenticationSessionHandler }
from "./AuthenticationSessionHandler";
import { AuthenticationSession } from "../../types/AuthenticationSession";
import { UserDataStore } from "./storage/UserDataStore";
import exceptions = require("./Exceptions");
import { ServerVariables } from "./ServerVariables";
import Assert = require("assert");
import express = require("express");
import BluebirdPromise = require("bluebird");
import ExpressMock = require("./stubs/express.spec");
import NotifierMock = require("./notifiers/NotifierStub.spec");
import { IdentityValidableStub } from "./IdentityValidableStub.spec";
import { RequestLoggerStub } from "./logging/RequestLoggerStub.spec";
import { ServerVariablesMock, ServerVariablesMockBuilder }
from "./ServerVariablesMockBuilder.spec";
import { PRE_VALIDATION_TEMPLATE }
from "./IdentityCheckPreValidationTemplate";
describe("IdentityCheckMiddleware", function () {
let req: ExpressMock.RequestMock;

View File

@ -3,8 +3,6 @@ import randomstring = require("randomstring");
import BluebirdPromise = require("bluebird");
import util = require("util");
import Exceptions = require("./Exceptions");
import fs = require("fs");
import ejs = require("ejs");
import { IUserDataStore } from "./storage/IUserDataStore";
import Express = require("express");
import ErrorReplies = require("./ErrorReplies");
@ -16,16 +14,13 @@ import { IdentityValidable } from "./IdentityValidable";
import Identity = require("../../types/Identity");
import { IdentityValidationDocument }
from "./storage/IdentityValidationDocument";
const filePath = __dirname + "/../resources/email-template.ejs";
const email_template = fs.readFileSync(filePath, "utf8");
import { OPERATION_FAILED } from "../../../shared/UserMessages";
function createAndSaveToken(userid: string, challenge: string,
userDataStore: IUserDataStore): BluebirdPromise<string> {
const five_minutes = 4 * 60 * 1000;
const token = randomstring.generate({ length: 64 });
const that = this;
return userDataStore.produceIdentityValidationToken(userid, token, challenge,
five_minutes)
@ -46,9 +41,9 @@ export function register(app: Express.Application,
handler: IdentityValidable,
vars: ServerVariables) {
app.get(pre_validation_endpoint,
app.post(pre_validation_endpoint,
get_start_validation(handler, post_validation_endpoint, vars));
app.get(post_validation_endpoint,
app.post(post_validation_endpoint,
get_finish_validation(handler, vars));
}
@ -69,7 +64,7 @@ export function get_finish_validation(handler: IdentityValidable,
let authSession: AuthenticationSession;
const identityToken = objectPath.get<Express.Request, string>(
req, "query.identity_token");
req, "query.token");
vars.logger.debug(req, "Identity token provided is %s", identityToken);
return checkIdentityToken(req, identityToken)
@ -89,7 +84,7 @@ export function get_finish_validation(handler: IdentityValidable,
handler.postValidationResponse(req, res);
return BluebirdPromise.resolve();
})
.catch(ErrorReplies.replyWithError401(req, res, vars.logger));
.catch(ErrorReplies.replyWithError200(req, res, vars.logger, OPERATION_FAILED));
};
}
@ -118,8 +113,8 @@ export function get_start_validation(handler: IdentityValidable,
})
.then((token) => {
const host = req.get("Host");
const link_url = util.format("https://%s%s?identity_token=%s", host,
postValidationEndpoint, token);
const link_url = util.format("https://%s%s?token=%s", host,
handler.destinationPath(), token);
vars.logger.info(req, "Notification sent to user \"%s\"",
identity.userid);
return vars.notifier.notify(identity.email, handler.mailSubject(),

View File

@ -16,4 +16,5 @@ export interface IdentityValidable {
// Serves the page if identity validated
postValidationResponse(req: Express.Request, res: Express.Response): void;
mailSubject(): string;
destinationPath(): string;
}

View File

@ -1,7 +1,6 @@
import Sinon = require("sinon");
import { IdentityValidable } from "./IdentityValidable";
import express = require("express");
import Bluebird = require("bluebird");
import { Identity } from "../../types/Identity";
@ -13,6 +12,7 @@ export class IdentityValidableStub implements IdentityValidable {
preValidationResponseStub: Sinon.SinonStub;
postValidationResponseStub: Sinon.SinonStub;
mailSubjectStub: Sinon.SinonStub;
destinationPathStub: Sinon.SinonStub;
constructor() {
this.challengeStub = Sinon.stub();
@ -49,4 +49,8 @@ export class IdentityValidableStub implements IdentityValidable {
mailSubject(): string {
return this.mailSubjectStub();
}
destinationPath(): string {
return this.destinationPathStub();
}
}

View File

@ -1,11 +1,8 @@
import BluebirdPromise = require("bluebird");
import ObjectPath = require("object-path");
import { Configuration } from "./configuration/schema/Configuration";
import { GlobalDependencies } from "../../types/Dependencies";
import { UserDataStore } from "./storage/UserDataStore";
import { ConfigurationParser } from "./configuration/ConfigurationParser";
import { SessionConfigurationBuilder } from "./configuration/SessionConfigurationBuilder";
import { GlobalLogger } from "./logging/GlobalLogger";
import { RequestLogger } from "./logging/RequestLogger";
import { ServerVariables } from "./ServerVariables";
@ -13,7 +10,6 @@ import { ServerVariablesInitializer } from "./ServerVariablesInitializer";
import { Configurator } from "./web_server/Configurator";
import * as Express from "express";
import * as Path from "path";
import * as http from "http";
function clone(obj: any) {

View File

@ -1,29 +1,19 @@
import winston = require("winston");
import BluebirdPromise = require("bluebird");
import U2F = require("u2f");
import Nodemailer = require("nodemailer");
import { IRequestLogger } from "./logging/IRequestLogger";
import { RequestLogger } from "./logging/RequestLogger";
import { TotpHandler } from "./authentication/totp/TotpHandler";
import { ITotpHandler } from "./authentication/totp/ITotpHandler";
import { NotifierFactory } from "./notifiers/NotifierFactory";
import { MailSenderBuilder } from "./notifiers/MailSenderBuilder";
import { LdapUsersDatabase } from "./authentication/backends/ldap/LdapUsersDatabase";
import { ConnectorFactory } from "./authentication/backends/ldap/connector/ConnectorFactory";
import { IUserDataStore } from "./storage/IUserDataStore";
import { UserDataStore } from "./storage/UserDataStore";
import { INotifier } from "./notifiers/INotifier";
import { Regulator } from "./regulation/Regulator";
import { IRegulator } from "./regulation/IRegulator";
import Configuration = require("./configuration/schema/Configuration");
import { CollectionFactoryFactory } from "./storage/CollectionFactoryFactory";
import { ICollectionFactory } from "./storage/ICollectionFactory";
import { MongoCollectionFactory } from "./storage/mongo/MongoCollectionFactory";
import { IMongoClient } from "./connectors/mongo/IMongoClient";
import { GlobalDependencies } from "../../types/Dependencies";
import { ServerVariables } from "./ServerVariables";

View File

@ -1,5 +1,3 @@
export enum Level {
NOT_AUTHENTICATED = 0,
ONE_FACTOR = 1,
TWO_FACTOR = 2
}
import { default as Level } from '../../../../shared/AuthenticationLevel';
export {Level};

View File

@ -1,6 +1,5 @@
import { INotifier } from "../notifiers/INotifier";
import { Identity } from "../../../types/Identity";
import Fs = require("fs");
import Path = require("path");

View File

@ -1,6 +1,3 @@
import * as BluebirdPromise from "bluebird";
import { AbstractEmailNotifier } from "../notifiers/AbstractEmailNotifier";
import { EmailNotifierConfiguration } from "../configuration/schema/NotifierConfiguration";
import { IMailSender } from "./IMailSender";

View File

@ -1,72 +0,0 @@
import express = require("express");
import Endpoints = require("../../../../../shared/api");
import BluebirdPromise = require("bluebird");
import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler";
import Constants = require("../../../../../shared/constants");
import Util = require("util");
import { ServerVariables } from "../../ServerVariables";
import { SafeRedirector } from "../../utils/SafeRedirection";
import { Level } from "../../authentication/Level";
function getRedirectParam(
req: express.Request) {
return req.query[Constants.REDIRECT_QUERY_PARAM] != "undefined"
? req.query[Constants.REDIRECT_QUERY_PARAM]
: undefined;
}
function redirectToSecondFactorPage(
req: express.Request,
res: express.Response) {
const redirectUrl = getRedirectParam(req);
if (!redirectUrl)
res.redirect(Endpoints.SECOND_FACTOR_GET);
else
res.redirect(
Util.format("%s?%s=%s",
Endpoints.SECOND_FACTOR_GET,
Constants.REDIRECT_QUERY_PARAM,
redirectUrl));
}
function redirectToService(
req: express.Request,
res: express.Response,
redirector: SafeRedirector) {
const redirectUrl = getRedirectParam(req);
if (!redirectUrl) {
res.redirect(Endpoints.LOGGED_IN);
} else {
redirector.redirectOrElse(res, redirectUrl, Endpoints.LOGGED_IN);
}
}
function renderFirstFactor(
res: express.Response) {
res.render("firstfactor", {
first_factor_post_endpoint: Endpoints.FIRST_FACTOR_POST,
reset_password_request_endpoint: Endpoints.RESET_PASSWORD_REQUEST_GET
});
}
export default function (
vars: ServerVariables) {
const redirector = new SafeRedirector(vars.config.session.domain);
return function (req: express.Request, res: express.Response): BluebirdPromise<void> {
return new BluebirdPromise(function (resolve, reject) {
const authSession = AuthenticationSessionHandler.get(req, vars.logger);
if (authSession.authentication_level == Level.ONE_FACTOR) {
redirectToSecondFactorPage(req, res);
} else if (authSession.authentication_level == Level.TWO_FACTOR) {
redirectToService(req, res, redirector);
} else {
renderFirstFactor(res);
}
resolve();
});
};
}

View File

@ -10,7 +10,7 @@ import UserMessages = require("../../../../../shared/UserMessages");
import { ServerVariables } from "../../ServerVariables";
import { AuthenticationSession } from "../../../../types/AuthenticationSession";
import { GroupsAndEmails } from "../../authentication/backends/GroupsAndEmails";
import { Level as AuthenticationLevel } from "../../authentication/Level";
import {Level} from "../../authentication/Level";
import { Level as AuthorizationLevel } from "../../authorization/Level";
import { URLDecomposer } from "../../utils/URLDecomposer";
@ -48,7 +48,7 @@ export default function (vars: ServerVariables) {
JSON.stringify(groupsAndEmails));
authSession.userid = username;
authSession.keep_me_logged_in = keepMeLoggedIn;
authSession.authentication_level = AuthenticationLevel.ONE_FACTOR;
authSession.authentication_level = Level.ONE_FACTOR;
const redirectUrl: string = req.query[Constants.REDIRECT_QUERY_PARAM] !== "undefined"
// Fuck, don't know why it is a string!
? req.query[Constants.REDIRECT_QUERY_PARAM]
@ -80,7 +80,7 @@ export default function (vars: ServerVariables) {
vars.logger.debug(req, "Redirect to '%s'", redirectUrl);
}
else {
let newRedirectUrl = Endpoint.SECOND_FACTOR_GET;
let newRedirectUrl = '/2fa';
if (redirectUrl) {
newRedirectUrl += "?" + Constants.REDIRECT_QUERY_PARAM + "="
+ redirectUrl;

View File

@ -10,7 +10,7 @@ export default function (vars: ServerVariables) {
return new BluebirdPromise<void>(function (resolve, reject) {
const authSession = AuthenticationSessionHandler.get(req, vars.logger);
res.render("already-logged-in", {
logout_endpoint: Endpoints.LOGOUT_GET,
logout_endpoint: Endpoints.LOGOUT_POST,
username: authSession.userid,
redirection_url: vars.config.default_redirection_url
});

View File

@ -66,4 +66,8 @@ export default class PasswordResetHandler implements IdentityValidable {
mailSubject(): string {
return "Reset your password";
}
destinationPath(): string {
return "/reset-password";
}
}

View File

@ -1,44 +0,0 @@
import SecondFactorGet from "./get";
import { ServerVariablesMockBuilder, ServerVariablesMock }
from "../../ServerVariablesMockBuilder.spec";
import { ServerVariables } from "../../ServerVariables";
import Sinon = require("sinon");
import ExpressMock = require("../../stubs/express.spec");
import Assert = require("assert");
import Endpoints = require("../../../../../shared/api");
import BluebirdPromise = require("bluebird");
describe("routes/secondfactor/get", function () {
let mocks: ServerVariablesMock;
let vars: ServerVariables;
let req: ExpressMock.RequestMock;
let res: ExpressMock.ResponseMock;
beforeEach(function () {
const s = ServerVariablesMockBuilder.build();
mocks = s.mocks;
vars = s.variables;
req = ExpressMock.RequestMock();
res = ExpressMock.ResponseMock();
req.session = {
auth: {
userid: "user",
first_factor: true,
second_factor: false
}
};
});
describe("test rendering", function () {
it("should render second factor page", function () {
req.session.auth.second_factor = false;
return SecondFactorGet(vars)(req as any, res as any)
.then(function () {
Assert(res.render.calledWith("secondfactor"));
return BluebirdPromise.resolve();
});
});
});
});

View File

@ -1,28 +0,0 @@
import Express = require("express");
import Endpoints = require("../../../../../shared/api");
import BluebirdPromise = require("bluebird");
import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler";
import { ServerVariables } from "../../ServerVariables";
const TEMPLATE_NAME = "secondfactor";
export default function (vars: ServerVariables) {
function handler(req: Express.Request, res: Express.Response)
: BluebirdPromise<void> {
return new BluebirdPromise(function (resolve, reject) {
const authSession = AuthenticationSessionHandler.get(req, vars.logger);
res.render(TEMPLATE_NAME, {
username: authSession.userid,
totp_identity_start_endpoint:
Endpoints.SECOND_FACTOR_TOTP_IDENTITY_START_GET,
u2f_identity_start_endpoint:
Endpoints.SECOND_FACTOR_U2F_IDENTITY_START_GET
});
resolve();
});
}
return handler;
}

View File

@ -1,13 +1,10 @@
import express = require("express");
import objectPath = require("object-path");
import Endpoints = require("../../../../../shared/api");
import { ServerVariables } from "../../ServerVariables";
import BluebirdPromise = require("bluebird");
import ErrorReplies = require("../../ErrorReplies");
import UserMessages = require("../../../../../shared/UserMessages");
import { RedirectionMessage } from "../../../../../shared/RedirectionMessage";
import Constants = require("../../../../../shared/constants");
export default function (vars: ServerVariables) {
return function (req: express.Request, res: express.Response)

View File

@ -97,7 +97,7 @@ export default class RegistrationHandler implements IdentityValidable {
.then(function () {
AuthenticationSessionHandler.reset(req);
res.render(Constants.TEMPLATE_NAME, {
res.json({
base32_secret: secret.base32,
otpauth_url: secret.otpauth_url,
login_endpoint: Endpoints.FIRST_FACTOR_GET
@ -109,4 +109,8 @@ export default class RegistrationHandler implements IdentityValidable {
mailSubject(): string {
return "Set up Authelia's one-time password";
}
destinationPath(): string {
return "/one-time-password-registration";
}
}

View File

@ -2,7 +2,6 @@ import Bluebird = require("bluebird");
import Express = require("express");
import { TOTPSecretDocument } from "../../../../storage/TOTPSecretDocument";
import Endpoints = require("../../../../../../../shared/api");
import Redirect from "../../redirect";
import ErrorReplies = require("../../../../ErrorReplies");
import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler";
@ -11,8 +10,6 @@ import UserMessages = require("../../../../../../../shared/UserMessages");
import { ServerVariables } from "../../../../ServerVariables";
import { Level } from "../../../../authentication/Level";
const UNAUTHORIZED_MESSAGE = "Unauthorized access";
export default function (vars: ServerVariables) {
function handler(req: Express.Request, res: Express.Response): Bluebird<void> {
let authSession: AuthenticationSession;
@ -27,8 +24,9 @@ export default function (vars: ServerVariables) {
return vars.userDataStore.retrieveTOTPSecret(authSession.userid);
})
.then(function (doc: TOTPSecretDocument) {
if (!vars.totpHandler.validate(token, doc.secret.base32))
if (!vars.totpHandler.validate(token, doc.secret.base32)) {
return Bluebird.reject(new Error("Invalid TOTP token."));
}
vars.logger.debug(req, "TOTP validation succeeded.");
authSession.authentication_level = Level.TWO_FACTOR;

View File

@ -69,5 +69,9 @@ export default class RegistrationHandler implements IdentityValidable {
mailSubject(): string {
return MAIL_SUBJECT;
}
destinationPath(): string {
return "/security-key-registration";
}
}

View File

@ -1,7 +1,4 @@
import { UserDataStore } from "../../../../storage/UserDataStore";
import objectPath = require("object-path");
import u2f_common = require("../U2FCommon");
import BluebirdPromise = require("bluebird");
import express = require("express");
@ -21,8 +18,6 @@ export default function (vars: ServerVariables) {
authSession = AuthenticationSessionHandler.get(req, vars.logger);
if (!authSession.identity_check
|| authSession.identity_check.challenge != "u2f-register") {
res.status(403);
res.send();
return reject(new Error("Bad challenge."));
}

View File

@ -0,0 +1,17 @@
import * as Express from 'express';
import * as Bluebird from 'bluebird';
import { ServerVariables } from "../../ServerVariables";
import { AuthenticationSessionHandler } from '../../AuthenticationSessionHandler';
export default function (vars: ServerVariables) {
return function (req: Express.Request, res: Express.Response): Bluebird<void> {
return new Bluebird(function (resolve, reject) {
const authSession = AuthenticationSessionHandler.get(req, vars.logger);
res.json({
username: authSession.userid,
authentication_level: authSession.authentication_level
})
resolve();
});
};
}

View File

@ -6,7 +6,6 @@ import Exceptions = require("../../Exceptions");
import { Level as AuthorizationLevel } from "../../authorization/Level";
import { Level as AuthenticationLevel } from "../../authentication/Level";
import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler";
import { ServerVariables } from "../../ServerVariables";
function isAuthorized(

View File

@ -1,10 +1,8 @@
import Express = require("express");
import FirstFactorGet = require("../routes/firstfactor/get");
import SecondFactorGet = require("../routes/secondfactor/get");
import FirstFactorPost = require("../routes/firstfactor/post");
import LogoutGet = require("../routes/logout/get");
import LogoutPost from "../routes/logout/post";
import StateGet from "../routes/state/get";
import VerifyGet = require("../routes/verify/get");
import TOTPSignGet = require("../routes/secondfactor/totp/sign/post");
@ -69,15 +67,15 @@ function setupU2f(app: Express.Application, vars: ServerVariables) {
RequireValidatedFirstFactor.middleware(vars.logger),
U2FRegisterPost.default(vars));
app.get(Endpoints.SECOND_FACTOR_U2F_IDENTITY_START_GET,
app.post(Endpoints.SECOND_FACTOR_U2F_IDENTITY_START_POST,
RequireValidatedFirstFactor.middleware(vars.logger));
app.get(Endpoints.SECOND_FACTOR_U2F_IDENTITY_FINISH_GET,
app.post(Endpoints.SECOND_FACTOR_U2F_IDENTITY_FINISH_POST,
RequireValidatedFirstFactor.middleware(vars.logger));
IdentityCheckMiddleware.register(app,
Endpoints.SECOND_FACTOR_U2F_IDENTITY_START_GET,
Endpoints.SECOND_FACTOR_U2F_IDENTITY_FINISH_GET,
Endpoints.SECOND_FACTOR_U2F_IDENTITY_START_POST,
Endpoints.SECOND_FACTOR_U2F_IDENTITY_FINISH_POST,
new U2FRegistrationIdentityHandler(vars.logger), vars);
}
@ -102,13 +100,9 @@ function setupErrors(app: Express.Application, vars: ServerVariables) {
export class RestApi {
static setup(app: Express.Application, vars: ServerVariables): void {
app.get(Endpoints.FIRST_FACTOR_GET, FirstFactorGet.default(vars));
app.get(Endpoints.STATE_GET, StateGet(vars));
app.get(Endpoints.SECOND_FACTOR_GET,
RequireValidatedFirstFactor.middleware(vars.logger),
SecondFactorGet.default(vars));
app.get(Endpoints.LOGOUT_GET, LogoutGet.default(vars));
app.post(Endpoints.LOGOUT_POST, LogoutPost(vars));
app.get(Endpoints.VERIFY_GET, VerifyGet.default(vars));
app.post(Endpoints.FIRST_FACTOR_POST, FirstFactorPost.default(vars));

View File

@ -0,0 +1,7 @@
enum Level {
NOT_AUTHENTICATED = 0,
ONE_FACTOR = 1,
TWO_FACTOR = 2
};
export default Level;

View File

@ -109,17 +109,17 @@ export const SECOND_FACTOR_TOTP_POST = "/api/totp";
/**
* @api {get} /secondfactor/u2f/identity/start Start U2F registration identity validation
* @api {get} /api/secondfactor/u2f/identity/start Start U2F registration identity validation
* @apiName RequestU2FRegistration
* @apiGroup U2F
* @apiVersion 1.0.0
* @apiUse UserSession
* @apiUse IdentityValidationStart
*/
export const SECOND_FACTOR_U2F_IDENTITY_START_GET = "/secondfactor/u2f/identity/start";
export const SECOND_FACTOR_U2F_IDENTITY_START_POST = "/api/secondfactor/u2f/identity/start";
/**
* @api {get} /secondfactor/u2f/identity/finish Finish U2F registration identity validation
* @api {get} /api/secondfactor/u2f/identity/finish Finish U2F registration identity validation
* @apiName ServeU2FRegistrationPage
* @apiGroup U2F
* @apiVersion 1.0.0
@ -129,12 +129,12 @@ export const SECOND_FACTOR_U2F_IDENTITY_START_GET = "/secondfactor/u2f/identity/
* @apiDescription Serves the U2F registration page that asks the user to
* touch the token of the U2F device.
*/
export const SECOND_FACTOR_U2F_IDENTITY_FINISH_GET = "/secondfactor/u2f/identity/finish";
export const SECOND_FACTOR_U2F_IDENTITY_FINISH_POST = "/api/secondfactor/u2f/identity/finish";
/**
* @api {get} /secondfactor/totp/identity/start Start TOTP registration identity validation
* @api {get} /api/secondfactor/totp/identity/start Start TOTP registration identity validation
* @apiName StartTOTPRegistration
* @apiGroup TOTP
* @apiVersion 1.0.0
@ -143,12 +143,12 @@ export const SECOND_FACTOR_U2F_IDENTITY_FINISH_GET = "/secondfactor/u2f/identity
*
* @apiDescription Initiates the identity validation
*/
export const SECOND_FACTOR_TOTP_IDENTITY_START_GET = "/secondfactor/totp/identity/start";
export const SECOND_FACTOR_TOTP_IDENTITY_START_GET = "/api/secondfactor/totp/identity/start";
/**
* @api {get} /secondfactor/totp/identity/finish Finish TOTP registration identity validation
* @api {get} /api/secondfactor/totp/identity/finish Finish TOTP registration identity validation
* @apiName FinishTOTPRegistration
* @apiGroup TOTP
* @apiVersion 1.0.0
@ -159,7 +159,7 @@ export const SECOND_FACTOR_TOTP_IDENTITY_START_GET = "/secondfactor/totp/identit
* @apiDescription Serves the TOTP registration page that displays the secret.
* The secret is a QRCode and a base32 secret.
*/
export const SECOND_FACTOR_TOTP_IDENTITY_FINISH_GET = "/secondfactor/totp/identity/finish";
export const SECOND_FACTOR_TOTP_IDENTITY_FINISH_GET = "/api/secondfactor/totp/identity/finish";
@ -252,16 +252,17 @@ export const FIRST_FACTOR_POST = "/api/firstfactor";
export const FIRST_FACTOR_GET = "/";
/**
* @api {get} /secondfactor Second factor page
* @apiName SecondFactor
* @api {get} /state Authentication state
* @apiName State
* @apiGroup Authentication
* @apiVersion 1.0.0
*
* @apiSuccess (Success 200) {String} Content The content of second factor page.
*
* @apiDescription Serves the second factor page
*
* @apiSuccess (Success 200) A dict containing the username and the authentication
* level
*
* @apiDescription Get the authentication state of the user based on the cookie.
*/
export const SECOND_FACTOR_GET = "/secondfactor";
export const STATE_GET = "/api/state";
/**
* @api {get} /api/verify Verify user authentication
@ -287,17 +288,16 @@ export const SECOND_FACTOR_GET = "/secondfactor";
export const VERIFY_GET = "/api/verify";
/**
* @api {get} /logout Serves logout page
* @api {post} /api/logout Logout procedure
* @apiName Logout
* @apiGroup Authentication
* @apiVersion 1.0.0
*
* @apiParam {String} redirect Redirect to this URL when user is deauthenticated.
* @apiSuccess (Success 302) redirect Redirect to the URL.
* @apiSuccess (Success 200)
*
* @apiDescription Log out the user and redirect to the URL.
* @apiDescription Resets the session to logout the user.
*/
export const LOGOUT_GET = "/logout";
export const LOGOUT_POST = "/api/logout";
export const ERROR_401_GET = "/error/401";
export const ERROR_403_GET = "/error/403";