diff --git a/client-react/Dockerfile b/client-react/Dockerfile index 88d5d11f..d7a7e1a0 100644 --- a/client-react/Dockerfile +++ b/client-react/Dockerfile @@ -1,6 +1,6 @@ FROM node:10.15.0-jessie -WORKDIR /usr/app +WORKDIR /usr/app/client ADD package.json package.json diff --git a/client-react/docker-compose.yml b/client-react/docker-compose.yml index 0fccaef1..3e8ecb1e 100644 --- a/client-react/docker-compose.yml +++ b/client-react/docker-compose.yml @@ -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: diff --git a/client-react/package-lock.json b/client-react/package-lock.json index 141c27ed..2c93ee2e 100644 --- a/client-react/package-lock.json +++ b/client-react/package-lock.json @@ -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", diff --git a/client-react/package.json b/client-react/package.json index 316a63c8..8f5933aa 100644 --- a/client-react/package.json +++ b/client-react/package.json @@ -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", diff --git a/client-react/src/App.tsx b/client-react/src/App.tsx index 8bb44bdb..f38561c7 100644 --- a/client-react/src/App.tsx +++ b/client-react/src/App.tsx @@ -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 ( - -
- - {routes.map((r, key) => { - return - })} - -
-
+ + +
+ + {routes.map((r, key) => { + return + })} + +
+
+
); } } diff --git a/client-react/src/assets/images/error.svg b/client-react/src/assets/images/error.svg new file mode 100644 index 00000000..b0e544af --- /dev/null +++ b/client-react/src/assets/images/error.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/client-react/src/assets/jss/components/FormNotification/FormNotification.ts b/client-react/src/assets/jss/components/FormNotification/FormNotification.ts new file mode 100644 index 00000000..3a715e37 --- /dev/null +++ b/client-react/src/assets/jss/components/FormNotification/FormNotification.ts @@ -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; \ No newline at end of file diff --git a/client-react/src/assets/jss/layouts/PortalLayout/PortalLayout.ts b/client-react/src/assets/jss/layouts/PortalLayout/PortalLayout.ts new file mode 100644 index 00000000..dd22d98b --- /dev/null +++ b/client-react/src/assets/jss/layouts/PortalLayout/PortalLayout.ts @@ -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; \ No newline at end of file diff --git a/client-react/src/assets/jss/views/FirstFactorView/FirstFactorView.ts b/client-react/src/assets/jss/views/FirstFactorView/FirstFactorView.ts index b981fccd..0404c260 100644 --- a/client-react/src/assets/jss/views/FirstFactorView/FirstFactorView.ts +++ b/client-react/src/assets/jss/views/FirstFactorView/FirstFactorView.ts @@ -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; \ No newline at end of file diff --git a/client-react/src/assets/jss/views/OneTimePasswordRegistrationView/OneTimePasswordRegistrationView.ts b/client-react/src/assets/jss/views/OneTimePasswordRegistrationView/OneTimePasswordRegistrationView.ts new file mode 100644 index 00000000..d955e688 --- /dev/null +++ b/client-react/src/assets/jss/views/OneTimePasswordRegistrationView/OneTimePasswordRegistrationView.ts @@ -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; \ No newline at end of file diff --git a/client-react/src/assets/jss/views/SecondFactorView/SecondFactorView.ts b/client-react/src/assets/jss/views/SecondFactorView/SecondFactorView.ts index 68fc2c79..088735f8 100644 --- a/client-react/src/assets/jss/views/SecondFactorView/SecondFactorView.ts +++ b/client-react/src/assets/jss/views/SecondFactorView/SecondFactorView.ts @@ -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: { diff --git a/client-react/src/assets/jss/views/SecurityKeyRegistrationView/SecurityKeyRegistrationView.ts b/client-react/src/assets/jss/views/SecurityKeyRegistrationView/SecurityKeyRegistrationView.ts new file mode 100644 index 00000000..c7d17ebb --- /dev/null +++ b/client-react/src/assets/jss/views/SecurityKeyRegistrationView/SecurityKeyRegistrationView.ts @@ -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; \ No newline at end of file diff --git a/client-react/src/components/FormNotification/FormNotification.tsx b/client-react/src/components/FormNotification/FormNotification.tsx new file mode 100644 index 00000000..87caaeb8 --- /dev/null +++ b/client-react/src/components/FormNotification/FormNotification.tsx @@ -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 { + render() { + const { classes } = this.props; + return ( + +
+
+
+ {this.props.children} +
+
+
+
+ ) + } +} + +export default withStyles(styles)(FormNotification); \ No newline at end of file diff --git a/client-react/src/components/StateSynchronizer/StateSynchronizer.tsx b/client-react/src/components/StateSynchronizer/StateSynchronizer.tsx new file mode 100644 index 00000000..46b7399b --- /dev/null +++ b/client-react/src/components/StateSynchronizer/StateSynchronizer.tsx @@ -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 { + 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; \ No newline at end of file diff --git a/client-react/src/components/StateSynchronizer/WithState.ts b/client-react/src/components/StateSynchronizer/WithState.ts new file mode 100644 index 00000000..4f3e6ec7 --- /dev/null +++ b/client-react/src/components/StateSynchronizer/WithState.ts @@ -0,0 +1,7 @@ +import RemoteState from '../../reducers/Portal/RemoteState'; + +export interface WithState { + state: RemoteState | null; + stateError: string | null; + stateLoading: boolean; +} \ No newline at end of file diff --git a/client-react/src/containers/components/StateSynchronizer/StateSynchronizer.ts b/client-react/src/containers/components/StateSynchronizer/StateSynchronizer.ts new file mode 100644 index 00000000..7dbd7756 --- /dev/null +++ b/client-react/src/containers/components/StateSynchronizer/StateSynchronizer.ts @@ -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); \ No newline at end of file diff --git a/client-react/src/containers/layouts/PortalLayout/PortalLayout.ts b/client-react/src/containers/layouts/PortalLayout/PortalLayout.ts new file mode 100644 index 00000000..602a6c62 --- /dev/null +++ b/client-react/src/containers/layouts/PortalLayout/PortalLayout.ts @@ -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); \ No newline at end of file diff --git a/client-react/src/containers/views/FirstFactorView/FirstFactorView.ts b/client-react/src/containers/views/FirstFactorView/FirstFactorView.ts new file mode 100644 index 00000000..d13e6685 --- /dev/null +++ b/client-react/src/containers/views/FirstFactorView/FirstFactorView.ts @@ -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); \ No newline at end of file diff --git a/client-react/src/containers/views/OneTimePasswordRegistrationView/OneTimePasswordRegistrationView.ts b/client-react/src/containers/views/OneTimePasswordRegistrationView/OneTimePasswordRegistrationView.ts new file mode 100644 index 00000000..bfdb5ad3 --- /dev/null +++ b/client-react/src/containers/views/OneTimePasswordRegistrationView/OneTimePasswordRegistrationView.ts @@ -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); \ No newline at end of file diff --git a/client-react/src/containers/views/SecondFactorView/SecondFactorView.ts b/client-react/src/containers/views/SecondFactorView/SecondFactorView.ts new file mode 100644 index 00000000..404f7eb0 --- /dev/null +++ b/client-react/src/containers/views/SecondFactorView/SecondFactorView.ts @@ -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); \ No newline at end of file diff --git a/client-react/src/containers/views/SecurityKeyRegistrationView/SecurityKeyRegistrationView.ts b/client-react/src/containers/views/SecurityKeyRegistrationView/SecurityKeyRegistrationView.ts new file mode 100644 index 00000000..6f139cec --- /dev/null +++ b/client-react/src/containers/views/SecurityKeyRegistrationView/SecurityKeyRegistrationView.ts @@ -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); \ No newline at end of file diff --git a/client-react/src/layouts/PortalLayout/PortalLayout.module.css b/client-react/src/layouts/PortalLayout/PortalLayout.module.css deleted file mode 100644 index 3c898522..00000000 --- a/client-react/src/layouts/PortalLayout/PortalLayout.module.css +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/client-react/src/layouts/PortalLayout/PortalLayout.tsx b/client-react/src/layouts/PortalLayout/PortalLayout.tsx index dd369905..02922362 100644 --- a/client-react/src/layouts/PortalLayout/PortalLayout.tsx +++ b/client-react/src/layouts/PortalLayout/PortalLayout.tsx @@ -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 { +interface Props extends RouterProps, RouteProps, WithStyles { + authenticationLevel: AuthenticationLevel; +} +class PortalLayout extends Component { private renderTitle() { if (!this.props.location) return; @@ -24,14 +28,15 @@ export default class PortalLayout extends Component { render() { + const { classes } = this.props; return ( -
-
-
-
+
+
+
+
{this.renderTitle()}
-
+
{routes.map((r, key) => { return @@ -41,10 +46,12 @@ export default class PortalLayout extends Component {
-
+
Powered by Authelia
) } -} \ No newline at end of file +} + +export default withStyles(styles)(PortalLayout); \ No newline at end of file diff --git a/client-react/src/reducers/Portal/RemoteState.ts b/client-react/src/reducers/Portal/RemoteState.ts new file mode 100644 index 00000000..aa10b406 --- /dev/null +++ b/client-react/src/reducers/Portal/RemoteState.ts @@ -0,0 +1,8 @@ +import AuthenticationLevel from '../../types/AuthenticationLevel'; + +interface RemoteState { + username: string; + authentication_level: AuthenticationLevel; +} + +export default RemoteState; \ No newline at end of file diff --git a/client-react/src/reducers/Portal/actions.ts b/client-react/src/reducers/Portal/actions.ts new file mode 100644 index 00000000..5548dcd5 --- /dev/null +++ b/client-react/src/reducers/Portal/actions.ts @@ -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); +}); diff --git a/client-react/src/reducers/Portal/reducer.ts b/client-react/src/reducers/Portal/reducer.ts new file mode 100644 index 00000000..203fbcbf --- /dev/null +++ b/client-react/src/reducers/Portal/reducer.ts @@ -0,0 +1,106 @@ + +import * as Actions from './actions'; +import { ActionType, getType, StateType } from 'typesafe-actions'; +import RemoteState from './RemoteState'; + +export type FirstFactorAction = ActionType; + +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; + +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; +} \ No newline at end of file diff --git a/client-react/src/reducers/constants.ts b/client-react/src/reducers/constants.ts new file mode 100644 index 00000000..a4af97ad --- /dev/null +++ b/client-react/src/reducers/constants.ts @@ -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'; diff --git a/client-react/src/reducers/index.ts b/client-react/src/reducers/index.ts new file mode 100644 index 00000000..09dcf83f --- /dev/null +++ b/client-react/src/reducers/index.ts @@ -0,0 +1,5 @@ +import PortalReducer, { PortalState } from './Portal/reducer'; + +export type RootState = PortalState; + +export default PortalReducer; \ No newline at end of file diff --git a/client-react/src/routes/index.ts b/client-react/src/routes/index.ts index 990500dc..731c0a05 100644 --- a/client-react/src/routes/index.ts +++ b/client-react/src/routes/index.ts @@ -1,4 +1,4 @@ -import PortalLayout from "../layouts/PortalLayout/PortalLayout"; +import PortalLayout from "../containers/layouts/PortalLayout/PortalLayout"; export const routes = [{ path: '/', diff --git a/client-react/src/routes/routes.ts b/client-react/src/routes/routes.ts index eb8950be..f7d54929 100644 --- a/client-react/src/routes/routes.ts +++ b/client-react/src/routes/routes.ts @@ -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', diff --git a/client-react/src/types/AuthenticationLevel.ts b/client-react/src/types/AuthenticationLevel.ts new file mode 100644 index 00000000..550cc947 --- /dev/null +++ b/client-react/src/types/AuthenticationLevel.ts @@ -0,0 +1,7 @@ +enum AuthenticationLevel { + NOT_AUTHENTICATED = 0, + ONE_FACTOR = 1, + TWO_FACTOR = 2 +}; + +export default AuthenticationLevel; \ No newline at end of file diff --git a/client-react/src/views/ConfirmationSentView/ConfirmationSentView.tsx b/client-react/src/views/ConfirmationSentView/ConfirmationSentView.tsx index ae253834..113fb7cd 100644 --- a/client-react/src/views/ConfirmationSentView/ConfirmationSentView.tsx +++ b/client-react/src/views/ConfirmationSentView/ConfirmationSentView.tsx @@ -9,7 +9,7 @@ import { RouterProps } from "react-router"; interface Props extends RouterProps {} -export default class ConfirmationSent extends Component { +class ConfirmationSentView extends Component { render() { return (
@@ -20,15 +20,17 @@ export default class ConfirmationSent extends Component { Please check your e-mails and follow the instructions to confirm the operation.
) } -} \ No newline at end of file +} + +export default ConfirmationSentView; \ No newline at end of file diff --git a/client-react/src/views/FirstFactorView/FirstFactorView.tsx b/client-react/src/views/FirstFactorView/FirstFactorView.tsx index 179a0820..b232468d 100644 --- a/client-react/src/views/FirstFactorView/FirstFactorView.tsx +++ b/client-react/src/views/FirstFactorView/FirstFactorView.tsx @@ -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 { @@ -29,6 +39,8 @@ class FirstFactorView extends Component { username: '', password: '', loginButtonDisabled: false, + errorMessage: null, + remoteState: null, } } @@ -56,10 +68,14 @@ class FirstFactorView extends Component { } } - render() { + private renderWithState(state: RemoteState) { const { classes } = this.props; return (
+ + {this.state.errorMessage || ''} +
{ } + checkedIcon={} checked={this.state.rememberMe} onChange={this.toggleRememberMe} color="primary" @@ -114,28 +132,34 @@ class FirstFactorView extends Component { ) } + render() { + return ( +
+ this.setState({remoteState})}/> + {this.state.remoteState ? this.renderWithState(this.state.remoteState) : null} +
+ ) + } + 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); \ No newline at end of file diff --git a/client-react/src/views/OneTimePasswordRegistrationView/OneTimePasswordRegistrationView.tsx b/client-react/src/views/OneTimePasswordRegistrationView/OneTimePasswordRegistrationView.tsx index 77711515..90564a74 100644 --- a/client-react/src/views/OneTimePasswordRegistrationView/OneTimePasswordRegistrationView.tsx +++ b/client-react/src/views/OneTimePasswordRegistrationView/OneTimePasswordRegistrationView.tsx @@ -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 (
) +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 { + constructor(props: Props) { + super(props); + this.state = { + secret: { + otp_url: 'https://coucou', + base32_secret: 'coucou', + }, + } } -} \ No newline at end of file + 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 ( +
+
+ +
+
+ {this.state.secret ? : null} +
+
+ ) + } + + render() { + return this.state.secret ? this.renderWithSecret(this.state.secret) : null; + } +} + +export default withStyles(styles)(OneTimePasswordRegistrationView); \ No newline at end of file diff --git a/client-react/src/views/SecondFactorView/SecondFactorView.tsx b/client-react/src/views/SecondFactorView/SecondFactorView.tsx index 97afb3fd..6e9e78ec 100644 --- a/client-react/src/views/SecondFactorView/SecondFactorView.tsx +++ b/client-react/src/views/SecondFactorView/SecondFactorView.tsx @@ -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 { @@ -18,6 +29,7 @@ class SecondFactorView extends Component { super(props); this.state = { mode: 'u2f', + remoteState: null, } } @@ -71,14 +83,32 @@ class SecondFactorView extends Component { } } - 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 ; + } + const { classes } = this.props; return ( ) } + + onStateLoaded = (remoteState: RemoteState) => { + this.setState({remoteState}); + this.props.onStateLoaded(remoteState); + } + + render() { + return ( +
+ + {this.state.remoteState ? this.renderWithState(this.state.remoteState) : null} +
+ ) + } } export default withStyles(styles)(SecondFactorView); \ No newline at end of file diff --git a/client-react/src/views/SecurityKeyRegistrationView/SecurityKeyRegistrationView.tsx b/client-react/src/views/SecurityKeyRegistrationView/SecurityKeyRegistrationView.tsx index 6bd8540d..b5aaa5aa 100644 --- a/client-react/src/views/SecurityKeyRegistrationView/SecurityKeyRegistrationView.tsx +++ b/client-react/src/views/SecurityKeyRegistrationView/SecurityKeyRegistrationView.tsx @@ -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 (
) +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 { + 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); } -} \ No newline at end of file + + render() { + const {classes} = this.props; + return ( +
+
Press the gold disk to register your security key
+
+ security key +
+
+ ) + } +} + +export default withStyles(styles)(SecurityKeyRegistrationView); \ No newline at end of file diff --git a/example/compose/nginx/portal/nginx.conf b/example/compose/nginx/portal/nginx.conf index 646456af..2ac6d384 100644 --- a/example/compose/nginx/portal/nginx.conf +++ b/example/compose/nginx/portal/nginx.conf @@ -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; - } } } diff --git a/package-lock.json b/package-lock.json index e7a1e627..6a80683c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index e7a2235d..01580c89 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/server/src/lib/AuthenticationSessionHandler.ts b/server/src/lib/AuthenticationSessionHandler.ts index 57361bf8..1e54054f 100644 --- a/server/src/lib/AuthenticationSessionHandler.ts +++ b/server/src/lib/AuthenticationSessionHandler.ts @@ -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); } diff --git a/server/src/lib/ErrorReplies.ts b/server/src/lib/ErrorReplies.ts index f1c5f4fd..93bc8984 100644 --- a/server/src/lib/ErrorReplies.ts +++ b/server/src/lib/ErrorReplies.ts @@ -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, diff --git a/server/src/lib/FirstFactorValidator.ts b/server/src/lib/FirstFactorValidator.ts index 23106000..e098b7e9 100644 --- a/server/src/lib/FirstFactorValidator.ts +++ b/server/src/lib/FirstFactorValidator.ts @@ -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"; diff --git a/server/src/lib/IdentityCheckMiddleware.spec.ts b/server/src/lib/IdentityCheckMiddleware.spec.ts index 842ed6bc..b2fc9df7 100644 --- a/server/src/lib/IdentityCheckMiddleware.spec.ts +++ b/server/src/lib/IdentityCheckMiddleware.spec.ts @@ -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; diff --git a/server/src/lib/IdentityCheckMiddleware.ts b/server/src/lib/IdentityCheckMiddleware.ts index e72ea4db..15605493 100644 --- a/server/src/lib/IdentityCheckMiddleware.ts +++ b/server/src/lib/IdentityCheckMiddleware.ts @@ -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 { 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( - 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(), diff --git a/server/src/lib/IdentityValidable.ts b/server/src/lib/IdentityValidable.ts index 075580c9..85a9a45c 100644 --- a/server/src/lib/IdentityValidable.ts +++ b/server/src/lib/IdentityValidable.ts @@ -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; } \ No newline at end of file diff --git a/server/src/lib/IdentityValidableStub.spec.ts b/server/src/lib/IdentityValidableStub.spec.ts index 20a97714..af9f1a79 100644 --- a/server/src/lib/IdentityValidableStub.spec.ts +++ b/server/src/lib/IdentityValidableStub.spec.ts @@ -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(); + } } \ No newline at end of file diff --git a/server/src/lib/Server.ts b/server/src/lib/Server.ts index 4090f629..1a510578 100644 --- a/server/src/lib/Server.ts +++ b/server/src/lib/Server.ts @@ -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) { diff --git a/server/src/lib/ServerVariablesInitializer.ts b/server/src/lib/ServerVariablesInitializer.ts index df79238c..fcf146d0 100644 --- a/server/src/lib/ServerVariablesInitializer.ts +++ b/server/src/lib/ServerVariablesInitializer.ts @@ -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"; diff --git a/server/src/lib/authentication/Level.ts b/server/src/lib/authentication/Level.ts index 57b6a234..dad04dd0 100644 --- a/server/src/lib/authentication/Level.ts +++ b/server/src/lib/authentication/Level.ts @@ -1,5 +1,3 @@ -export enum Level { - NOT_AUTHENTICATED = 0, - ONE_FACTOR = 1, - TWO_FACTOR = 2 -} \ No newline at end of file +import { default as Level } from '../../../../shared/AuthenticationLevel'; + +export {Level}; \ No newline at end of file diff --git a/server/src/lib/notifiers/AbstractEmailNotifier.ts b/server/src/lib/notifiers/AbstractEmailNotifier.ts index 198e4e5d..626f2b6a 100644 --- a/server/src/lib/notifiers/AbstractEmailNotifier.ts +++ b/server/src/lib/notifiers/AbstractEmailNotifier.ts @@ -1,6 +1,5 @@ import { INotifier } from "../notifiers/INotifier"; -import { Identity } from "../../../types/Identity"; import Fs = require("fs"); import Path = require("path"); diff --git a/server/src/lib/notifiers/EmailNotifier.ts b/server/src/lib/notifiers/EmailNotifier.ts index 4df7c861..4ffd385d 100644 --- a/server/src/lib/notifiers/EmailNotifier.ts +++ b/server/src/lib/notifiers/EmailNotifier.ts @@ -1,6 +1,3 @@ - -import * as BluebirdPromise from "bluebird"; - import { AbstractEmailNotifier } from "../notifiers/AbstractEmailNotifier"; import { EmailNotifierConfiguration } from "../configuration/schema/NotifierConfiguration"; import { IMailSender } from "./IMailSender"; diff --git a/server/src/lib/routes/firstfactor/get.ts b/server/src/lib/routes/firstfactor/get.ts deleted file mode 100644 index d94f656c..00000000 --- a/server/src/lib/routes/firstfactor/get.ts +++ /dev/null @@ -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 { - 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(); - }); - }; -} diff --git a/server/src/lib/routes/firstfactor/post.ts b/server/src/lib/routes/firstfactor/post.ts index 565681d6..e1332071 100644 --- a/server/src/lib/routes/firstfactor/post.ts +++ b/server/src/lib/routes/firstfactor/post.ts @@ -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; diff --git a/server/src/lib/routes/loggedin/get.ts b/server/src/lib/routes/loggedin/get.ts index 283a041b..3a6f183d 100644 --- a/server/src/lib/routes/loggedin/get.ts +++ b/server/src/lib/routes/loggedin/get.ts @@ -10,7 +10,7 @@ export default function (vars: ServerVariables) { return new BluebirdPromise(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 }); diff --git a/server/src/lib/routes/logout/get.ts b/server/src/lib/routes/logout/post.ts similarity index 100% rename from server/src/lib/routes/logout/get.ts rename to server/src/lib/routes/logout/post.ts diff --git a/server/src/lib/routes/password-reset/identity/PasswordResetHandler.ts b/server/src/lib/routes/password-reset/identity/PasswordResetHandler.ts index 42ae92cd..a5116a8b 100644 --- a/server/src/lib/routes/password-reset/identity/PasswordResetHandler.ts +++ b/server/src/lib/routes/password-reset/identity/PasswordResetHandler.ts @@ -66,4 +66,8 @@ export default class PasswordResetHandler implements IdentityValidable { mailSubject(): string { return "Reset your password"; } + + destinationPath(): string { + return "/reset-password"; + } } \ No newline at end of file diff --git a/server/src/lib/routes/secondfactor/get.spec.ts b/server/src/lib/routes/secondfactor/get.spec.ts deleted file mode 100644 index 6c77e1f6..00000000 --- a/server/src/lib/routes/secondfactor/get.spec.ts +++ /dev/null @@ -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(); - }); - }); - }); -}); \ No newline at end of file diff --git a/server/src/lib/routes/secondfactor/get.ts b/server/src/lib/routes/secondfactor/get.ts deleted file mode 100644 index 9f6deb4c..00000000 --- a/server/src/lib/routes/secondfactor/get.ts +++ /dev/null @@ -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 { - - 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; -} diff --git a/server/src/lib/routes/secondfactor/redirect.ts b/server/src/lib/routes/secondfactor/redirect.ts index 5d84d9eb..75132f96 100644 --- a/server/src/lib/routes/secondfactor/redirect.ts +++ b/server/src/lib/routes/secondfactor/redirect.ts @@ -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) diff --git a/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts b/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts index b39b6d04..91407676 100644 --- a/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts +++ b/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts @@ -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"; + } } \ No newline at end of file diff --git a/server/src/lib/routes/secondfactor/totp/sign/post.ts b/server/src/lib/routes/secondfactor/totp/sign/post.ts index 34a276d1..2da0cfad 100644 --- a/server/src/lib/routes/secondfactor/totp/sign/post.ts +++ b/server/src/lib/routes/secondfactor/totp/sign/post.ts @@ -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 { 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; diff --git a/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.ts b/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.ts index bc4713c7..25bfc9a8 100644 --- a/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.ts +++ b/server/src/lib/routes/secondfactor/u2f/identity/RegistrationHandler.ts @@ -69,5 +69,9 @@ export default class RegistrationHandler implements IdentityValidable { mailSubject(): string { return MAIL_SUBJECT; } + + destinationPath(): string { + return "/security-key-registration"; + } } diff --git a/server/src/lib/routes/secondfactor/u2f/register_request/get.ts b/server/src/lib/routes/secondfactor/u2f/register_request/get.ts index f611af93..ecac24f4 100644 --- a/server/src/lib/routes/secondfactor/u2f/register_request/get.ts +++ b/server/src/lib/routes/secondfactor/u2f/register_request/get.ts @@ -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.")); } diff --git a/server/src/lib/routes/state/get.ts b/server/src/lib/routes/state/get.ts new file mode 100644 index 00000000..be3e5507 --- /dev/null +++ b/server/src/lib/routes/state/get.ts @@ -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 { + return new Bluebird(function (resolve, reject) { + const authSession = AuthenticationSessionHandler.get(req, vars.logger); + res.json({ + username: authSession.userid, + authentication_level: authSession.authentication_level + }) + resolve(); + }); + }; +} diff --git a/server/src/lib/routes/verify/access_control.ts b/server/src/lib/routes/verify/access_control.ts index 136239ae..cbf97ab3 100644 --- a/server/src/lib/routes/verify/access_control.ts +++ b/server/src/lib/routes/verify/access_control.ts @@ -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( diff --git a/server/src/lib/web_server/RestApi.ts b/server/src/lib/web_server/RestApi.ts index 9144a15b..12d66117 100644 --- a/server/src/lib/web_server/RestApi.ts +++ b/server/src/lib/web_server/RestApi.ts @@ -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)); diff --git a/shared/AuthenticationLevel.ts b/shared/AuthenticationLevel.ts new file mode 100644 index 00000000..b92d6a0a --- /dev/null +++ b/shared/AuthenticationLevel.ts @@ -0,0 +1,7 @@ +enum Level { + NOT_AUTHENTICATED = 0, + ONE_FACTOR = 1, + TWO_FACTOR = 2 +}; + +export default Level; \ No newline at end of file diff --git a/shared/api.ts b/shared/api.ts index 2ebe5d3d..65be01e4 100644 --- a/shared/api.ts +++ b/shared/api.ts @@ -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";