mirror of
https://github.com/0rangebananaspy/authelia.git
synced 2024-09-14 22:47:21 +07:00
Almost full authentication workflow with U2F and TOTP.
This commit is contained in:
parent
fe14bde29b
commit
9d7155a969
|
@ -1,6 +1,6 @@
|
|||
FROM node:10.15.0-jessie
|
||||
|
||||
WORKDIR /usr/app
|
||||
WORKDIR /usr/app/client
|
||||
|
||||
ADD package.json package.json
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
110
client-react/package-lock.json
generated
110
client-react/package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
45
client-react/src/assets/images/error.svg
Normal file
45
client-react/src/assets/images/error.svg
Normal 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 |
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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: {
|
||||
|
|
|
@ -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;
|
|
@ -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);
|
|
@ -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;
|
|
@ -0,0 +1,7 @@
|
|||
import RemoteState from '../../reducers/Portal/RemoteState';
|
||||
|
||||
export interface WithState {
|
||||
state: RemoteState | null;
|
||||
stateError: string | null;
|
||||
stateLoading: boolean;
|
||||
}
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
8
client-react/src/reducers/Portal/RemoteState.ts
Normal file
8
client-react/src/reducers/Portal/RemoteState.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
import AuthenticationLevel from '../../types/AuthenticationLevel';
|
||||
|
||||
interface RemoteState {
|
||||
username: string;
|
||||
authentication_level: AuthenticationLevel;
|
||||
}
|
||||
|
||||
export default RemoteState;
|
41
client-react/src/reducers/Portal/actions.ts
Normal file
41
client-react/src/reducers/Portal/actions.ts
Normal 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);
|
||||
});
|
106
client-react/src/reducers/Portal/reducer.ts
Normal file
106
client-react/src/reducers/Portal/reducer.ts
Normal 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;
|
||||
}
|
12
client-react/src/reducers/constants.ts
Normal file
12
client-react/src/reducers/constants.ts
Normal 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';
|
5
client-react/src/reducers/index.ts
Normal file
5
client-react/src/reducers/index.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import PortalReducer, { PortalState } from './Portal/reducer';
|
||||
|
||||
export type RootState = PortalState;
|
||||
|
||||
export default PortalReducer;
|
|
@ -1,4 +1,4 @@
|
|||
import PortalLayout from "../layouts/PortalLayout/PortalLayout";
|
||||
import PortalLayout from "../containers/layouts/PortalLayout/PortalLayout";
|
||||
|
||||
export const routes = [{
|
||||
path: '/',
|
||||
|
|
|
@ -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',
|
||||
|
|
7
client-react/src/types/AuthenticationLevel.ts
Normal file
7
client-react/src/types/AuthenticationLevel.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
enum AuthenticationLevel {
|
||||
NOT_AUTHENTICATED = 0,
|
||||
ONE_FACTOR = 1,
|
||||
TWO_FACTOR = 2
|
||||
};
|
||||
|
||||
export default AuthenticationLevel;
|
|
@ -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;
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -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
111
package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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};
|
|
@ -1,6 +1,5 @@
|
|||
|
||||
import { INotifier } from "../notifiers/INotifier";
|
||||
import { Identity } from "../../../types/Identity";
|
||||
|
||||
import Fs = require("fs");
|
||||
import Path = require("path");
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
|
||||
import * as BluebirdPromise from "bluebird";
|
||||
|
||||
import { AbstractEmailNotifier } from "../notifiers/AbstractEmailNotifier";
|
||||
import { EmailNotifierConfiguration } from "../configuration/schema/NotifierConfiguration";
|
||||
import { IMailSender } from "./IMailSender";
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
};
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
});
|
||||
|
|
|
@ -66,4 +66,8 @@ export default class PasswordResetHandler implements IdentityValidable {
|
|||
mailSubject(): string {
|
||||
return "Reset your password";
|
||||
}
|
||||
|
||||
destinationPath(): string {
|
||||
return "/reset-password";
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -69,5 +69,9 @@ export default class RegistrationHandler implements IdentityValidable {
|
|||
mailSubject(): string {
|
||||
return MAIL_SUBJECT;
|
||||
}
|
||||
|
||||
destinationPath(): string {
|
||||
return "/security-key-registration";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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."));
|
||||
}
|
||||
|
||||
|
|
17
server/src/lib/routes/state/get.ts
Normal file
17
server/src/lib/routes/state/get.ts
Normal 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();
|
||||
});
|
||||
};
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -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));
|
||||
|
|
7
shared/AuthenticationLevel.ts
Normal file
7
shared/AuthenticationLevel.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
enum Level {
|
||||
NOT_AUTHENTICATED = 0,
|
||||
ONE_FACTOR = 1,
|
||||
TWO_FACTOR = 2
|
||||
};
|
||||
|
||||
export default Level;
|
|
@ -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";
|
||||
|
|
Loading…
Reference in New Issue
Block a user