-
Hello {state.username}
+
Hello {this.props.username}
@@ -123,21 +155,6 @@ class SecondFactorView extends Component
{
)
}
-
- 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/components/StateSynchronizer/StateSynchronizer.tsx b/client-react/src/components/StateSynchronizer/StateSynchronizer.tsx
deleted file mode 100644
index 46b7399bf..000000000
--- a/client-react/src/components/StateSynchronizer/StateSynchronizer.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-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
deleted file mode 100644
index 4f3e6ec71..000000000
--- a/client-react/src/components/StateSynchronizer/WithState.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-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/AlreadyAuthenticated/AlreadyAuthenticated.ts b/client-react/src/containers/components/AlreadyAuthenticated/AlreadyAuthenticated.ts
new file mode 100644
index 000000000..5029b5f4e
--- /dev/null
+++ b/client-react/src/containers/components/AlreadyAuthenticated/AlreadyAuthenticated.ts
@@ -0,0 +1,17 @@
+import { connect } from 'react-redux';
+import { Dispatch } from 'redux';
+import { RootState } from '../../../reducers';
+import AlreadyAuthenticated, { DispatchProps } from '../../../components/AlreadyAuthenticated/AlreadyAuthenticated';
+import LogoutBehavior from '../../../behaviors/LogoutBehavior';
+
+const mapStateToProps = (state: RootState) => {
+ return {};
+}
+
+const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => {
+ return {
+ onLogoutClicked: () => LogoutBehavior(dispatch),
+ }
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(AlreadyAuthenticated);
\ No newline at end of file
diff --git a/client-react/src/containers/components/FirstFactorForm/FirstFactorForm.ts b/client-react/src/containers/components/FirstFactorForm/FirstFactorForm.ts
new file mode 100644
index 000000000..40e30e52e
--- /dev/null
+++ b/client-react/src/containers/components/FirstFactorForm/FirstFactorForm.ts
@@ -0,0 +1,53 @@
+import { connect } from 'react-redux';
+import { Dispatch } from 'redux';
+import { authenticateFailure, authenticateSuccess, authenticate } from '../../../reducers/Portal/FirstFactor/actions';
+import FirstFactorForm, { StateProps } from '../../../components/FirstFactorForm/FirstFactorForm';
+import { RootState } from '../../../reducers';
+import * as AutheliaService from '../../../services/AutheliaService';
+import to from 'await-to-js';
+import FetchStateBehavior from '../../../behaviors/FetchStateBehavior';
+
+const mapStateToProps = (state: RootState): StateProps => {
+ return {
+ error: state.firstFactor.error,
+ formDisabled: state.firstFactor.loading,
+ };
+}
+
+function onAuthenticationRequested(dispatch: Dispatch) {
+ return async (username: string, password: string) => {
+ let err, res;
+
+ // Validate first factor
+ dispatch(authenticate());
+ [err, res] = await to(AutheliaService.postFirstFactorAuth(username, password));
+
+ if (err) {
+ await dispatch(authenticateFailure(err.message));
+ return;
+ }
+
+ if (!res) {
+ await dispatch(authenticateFailure('No response'));
+ return;
+ }
+
+ const json = await res.json();
+ if ('error' in json) {
+ await dispatch(authenticateFailure(json['error']));
+ return;
+ }
+ dispatch(authenticateSuccess());
+
+ // fetch state
+ FetchStateBehavior(dispatch);
+ }
+}
+
+const mapDispatchToProps = (dispatch: Dispatch) => {
+ return {
+ onAuthenticationRequested: onAuthenticationRequested(dispatch),
+ }
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(FirstFactorForm);
\ No newline at end of file
diff --git a/client-react/src/containers/components/SecondFactorForm/SecondFactorForm.ts b/client-react/src/containers/components/SecondFactorForm/SecondFactorForm.ts
new file mode 100644
index 000000000..98f2e0606
--- /dev/null
+++ b/client-react/src/containers/components/SecondFactorForm/SecondFactorForm.ts
@@ -0,0 +1,114 @@
+import { connect } from 'react-redux';
+import { RootState } from '../../../reducers';
+import { Dispatch } from 'redux';
+import u2fApi from 'u2f-api';
+import to from 'await-to-js';
+import { securityKeySignSuccess, securityKeySign, securityKeySignFailure, setSecurityKeySupported, oneTimePasswordVerification, oneTimePasswordVerificationFailure, oneTimePasswordVerificationSuccess } from '../../../reducers/Portal/SecondFactor/actions';
+import SecondFactorForm, { OwnProps, StateProps } from '../../../components/SecondFactorForm/SecondFactorForm';
+import * as AutheliaService from '../../../services/AutheliaService';
+import { push } from 'connected-react-router';
+import fetchState from '../../../behaviors/FetchStateBehavior';
+import LogoutBehavior from '../../../behaviors/LogoutBehavior';
+
+const mapStateToProps = (state: RootState): StateProps => ({
+ securityKeySupported: state.secondFactor.securityKeySupported,
+ securityKeyVerified: state.secondFactor.securityKeySignSuccess || false,
+ securityKeyError: state.secondFactor.error,
+
+ oneTimePasswordVerificationInProgress: state.secondFactor.oneTimePasswordVerificationLoading,
+ oneTimePasswordVerificationError: state.secondFactor.oneTimePasswordVerificationError,
+});
+
+async function triggerSecurityKeySigning(dispatch: Dispatch) {
+ let err, result;
+ dispatch(securityKeySign());
+ [err, result] = await to(AutheliaService.requestSigning());
+ if (err) {
+ await dispatch(securityKeySignFailure(err.message));
+ throw err;
+ }
+
+ if (!result) {
+ await dispatch(securityKeySignFailure('No response'));
+ throw 'No response';
+ }
+
+ [err, result] = await to(u2fApi.sign(result, 60));
+ if (err) {
+ await dispatch(securityKeySignFailure(err.message));
+ throw err;
+ }
+
+ if (!result) {
+ await dispatch(securityKeySignFailure('No response'));
+ throw 'No response';
+ }
+
+ [err, result] = await to(AutheliaService.completeSecurityKeySigning(result));
+ if (err) {
+ await dispatch(securityKeySignFailure(err.message));
+ throw err;
+ }
+ await dispatch(securityKeySignSuccess());
+}
+
+function redirectOnSuccess(dispatch: Dispatch, ownProps: OwnProps, duration?: number) {
+ function redirect() {
+ if (ownProps.redirection) {
+ window.location.href = ownProps.redirection;
+ } else {
+ fetchState(dispatch);
+ }
+ }
+
+ if (duration) {
+ setTimeout(redirect, duration);
+ } else {
+ redirect();
+ }
+}
+
+const mapDispatchToProps = (dispatch: Dispatch, ownProps: OwnProps) => {
+ return {
+ onLogoutClicked: () => LogoutBehavior(dispatch),
+ onRegisterSecurityKeyClicked: async () => {
+ await AutheliaService.startU2FRegistrationIdentityProcess();
+ await dispatch(push('/confirmation-sent'));
+ },
+ onRegisterOneTimePasswordClicked: async () => {
+ await AutheliaService.startTOTPRegistrationIdentityProcess();
+ await dispatch(push('/confirmation-sent'));
+ },
+ onInit: async () => {
+ const isU2FSupported = await u2fApi.isSupported();
+ if (isU2FSupported) {
+ await dispatch(setSecurityKeySupported(true));
+ await triggerSecurityKeySigning(dispatch);
+ redirectOnSuccess(dispatch, ownProps, 1000);
+ }
+ },
+ onOneTimePasswordValidationRequested: async (token: string) => {
+ let err, res;
+ dispatch(oneTimePasswordVerification());
+ [err, res] = await to(AutheliaService.verifyTotpToken(token));
+ if (err) {
+ dispatch(oneTimePasswordVerificationFailure(err.message));
+ throw err;
+ }
+ if (!res) {
+ dispatch(oneTimePasswordVerificationFailure('No response'));
+ throw 'No response';
+ }
+
+ const body = await res.json();
+ if ('error' in body) {
+ dispatch(oneTimePasswordVerificationFailure(body['error']));
+ throw body['error'];
+ }
+ dispatch(oneTimePasswordVerificationSuccess());
+ redirectOnSuccess(dispatch, ownProps);
+ },
+ }
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(SecondFactorForm);
\ 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
deleted file mode 100644
index c525cfa74..000000000
--- a/client-react/src/containers/components/StateSynchronizer/StateSynchronizer.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { connect } from 'react-redux';
-import StateSynchronizer, { OnLoaded, OnError } from '../../../components/StateSynchronizer/StateSynchronizer';
-import { RootState } from '../../../reducers';
-import { fetchStateSuccess, fetchState, fetchStateFailure } from '../../../reducers/Portal/FirstFactor/actions';
-import RemoteState from '../../../reducers/Portal/RemoteState';
-import { Dispatch } from 'redux';
-
-const mapStateToProps = (state: RootState) => ({
- state: state.firstFactor.remoteState,
- stateError: state.firstFactor.remoteStateError,
- stateLoading: state.firstFactor.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
index b426550da..f3c52147b 100644
--- a/client-react/src/containers/layouts/PortalLayout/PortalLayout.ts
+++ b/client-react/src/containers/layouts/PortalLayout/PortalLayout.ts
@@ -2,8 +2,6 @@ import { connect } from 'react-redux';
import PortalLayout from '../../../layouts/PortalLayout/PortalLayout';
import { RootState } from '../../../reducers';
-const mapStateToProps = (state: RootState) => ({
- authenticationLevel: (state.firstFactor.remoteState) ? state.firstFactor.remoteState.authentication_level : 0,
-});
+const mapStateToProps = (state: RootState) => ({});
export default connect(mapStateToProps)(PortalLayout);
\ No newline at end of file
diff --git a/client-react/src/containers/views/AuthenticationView/AuthenticationView.ts b/client-react/src/containers/views/AuthenticationView/AuthenticationView.ts
new file mode 100644
index 000000000..fe9fd47d4
--- /dev/null
+++ b/client-react/src/containers/views/AuthenticationView/AuthenticationView.ts
@@ -0,0 +1,42 @@
+import { connect } from 'react-redux';
+import AuthenticationView, {StateProps, Stage, DispatchProps} from '../../../views/AuthenticationView/AuthenticationView';
+import { RootState } from '../../../reducers';
+import { Dispatch } from 'redux';
+import AuthenticationLevel from '../../../types/AuthenticationLevel';
+import FetchStateBehavior from '../../../behaviors/FetchStateBehavior';
+import { setRedirectionUrl } from '../../../reducers/Portal/Authentication/actions';
+
+function authenticationLevelToStage(level: AuthenticationLevel): Stage {
+ switch (level) {
+ case AuthenticationLevel.NOT_AUTHENTICATED:
+ return Stage.FIRST_FACTOR;
+ case AuthenticationLevel.ONE_FACTOR:
+ return Stage.SECOND_FACTOR;
+ case AuthenticationLevel.TWO_FACTOR:
+ return Stage.ALREADY_AUTHENTICATED;
+ }
+}
+
+const mapStateToProps = (state: RootState): StateProps => {
+ const stage = (state.authentication.remoteState)
+ ? authenticationLevelToStage(state.authentication.remoteState.authentication_level)
+ : Stage.FIRST_FACTOR;
+ return {
+ redirectionUrl: state.authentication.redirectionUrl,
+ remoteState: state.authentication.remoteState,
+ stage: stage,
+ };
+}
+
+const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => {
+ return {
+ onInit: async (redirectionUrl?: string) => {
+ await FetchStateBehavior(dispatch);
+ if (redirectionUrl) {
+ await dispatch(setRedirectionUrl(redirectionUrl));
+ }
+ }
+ }
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(AuthenticationView);
\ 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
deleted file mode 100644
index 1ab101d42..000000000
--- a/client-react/src/containers/views/FirstFactorView/FirstFactorView.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-import { connect } from 'react-redux';
-import QueryString from 'query-string';
-import FirstFactorView, { Props } from '../../../views/FirstFactorView/FirstFactorView';
-import { Dispatch } from 'redux';
-import { authenticateFailure, authenticateSuccess, authenticate } from '../../../reducers/Portal/FirstFactor/actions';
-import { RootState } from '../../../reducers';
-
-
-const mapStateToProps = (state: RootState) => ({});
-
-function redirect2FA(props: Props) {
- if (!props.location) {
- props.history.push('/2fa');
- return;
- }
- const params = QueryString.parse(props.location.search);
-
- if ('rd' in params) {
- const rd = params['rd'] as string;
- props.history.push(`/2fa?rd=${rd}`);
- return;
- }
- props.history.push('/2fa');
-}
-
-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());
- redirect2FA(ownProps);
- });
- }
-}
-
-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/ForgotPasswordView/ForgotPasswordView.ts b/client-react/src/containers/views/ForgotPasswordView/ForgotPasswordView.ts
new file mode 100644
index 000000000..53fc7721a
--- /dev/null
+++ b/client-react/src/containers/views/ForgotPasswordView/ForgotPasswordView.ts
@@ -0,0 +1,31 @@
+import { connect } from 'react-redux';
+import { RootState } from '../../../reducers';
+import { Dispatch } from 'redux';
+import { push } from 'connected-react-router';
+import * as AutheliaService from '../../../services/AutheliaService';
+import ForgotPasswordView from '../../../views/ForgotPasswordView/ForgotPasswordView';
+import { forgotPasswordRequest, forgotPasswordSuccess, forgotPasswordFailure } from '../../../reducers/Portal/ForgotPassword/actions';
+
+const mapStateToProps = (state: RootState) => ({
+ disabled: state.forgotPassword.loading,
+});
+
+const mapDispatchToProps = (dispatch: Dispatch) => {
+ return {
+ onPasswordResetRequested: async (username: string) => {
+ try {
+ dispatch(forgotPasswordRequest());
+ await AutheliaService.initiatePasswordResetIdentityValidation(username);
+ dispatch(forgotPasswordSuccess());
+ await dispatch(push('/confirmation-sent'));
+ } catch (err) {
+ dispatch(forgotPasswordFailure(err.message));
+ }
+ },
+ onCancelClicked: async () => {
+ dispatch(push('/'));
+ }
+ }
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(ForgotPasswordView);
\ 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
index dbc356a6a..f724b3458 100644
--- a/client-react/src/containers/views/OneTimePasswordRegistrationView/OneTimePasswordRegistrationView.ts
+++ b/client-react/src/containers/views/OneTimePasswordRegistrationView/OneTimePasswordRegistrationView.ts
@@ -4,7 +4,7 @@ import { RootState } from '../../../reducers';
import { Dispatch } from 'redux';
import {to} from 'await-to-js';
import { generateTotpSecret, generateTotpSecretSuccess, generateTotpSecretFailure } from '../../../reducers/Portal/OneTimePasswordRegistration/actions';
-import { Props } from '../../../views/OneTimePasswordRegistrationView/OneTimePasswordRegistrationView';
+import { push } from 'connected-react-router';
const mapStateToProps = (state: RootState) => ({
error: state.oneTimePasswordRegistration.error,
@@ -46,7 +46,7 @@ async function tryGenerateTotpSecret(dispatch: Dispatch, token: string) {
dispatch(generateTotpSecretSuccess(result));
}
-const mapDispatchToProps = (dispatch: Dispatch, ownProps: Props) => {
+const mapDispatchToProps = (dispatch: Dispatch) => {
let internalToken: string;
return {
onInit: async (token: string) => {
@@ -57,10 +57,10 @@ const mapDispatchToProps = (dispatch: Dispatch, ownProps: Props) => {
await tryGenerateTotpSecret(dispatch, internalToken);
},
onCancelClicked: () => {
- ownProps.history.push('/2fa');
+ dispatch(push('/'));
},
onLoginClicked: () => {
- ownProps.history.push('/2fa');
+ dispatch(push('/'));
}
}
}
diff --git a/client-react/src/containers/views/ResetPasswordView/ResetPasswordView.ts b/client-react/src/containers/views/ResetPasswordView/ResetPasswordView.ts
new file mode 100644
index 000000000..60840c171
--- /dev/null
+++ b/client-react/src/containers/views/ResetPasswordView/ResetPasswordView.ts
@@ -0,0 +1,27 @@
+import { connect } from 'react-redux';
+import { RootState } from '../../../reducers';
+import { Dispatch } from 'redux';
+import { push } from 'connected-react-router';
+import * as AutheliaService from '../../../services/AutheliaService';
+import ResetPasswordView, { StateProps } from '../../../views/ResetPasswordView/ResetPasswordView';
+
+const mapStateToProps = (state: RootState): StateProps => ({
+ disabled: state.resetPassword.loading,
+});
+
+const mapDispatchToProps = (dispatch: Dispatch) => {
+ return {
+ onInit: async (token: string) => {
+ await AutheliaService.completePasswordResetIdentityValidation(token);
+ },
+ onPasswordResetRequested: async (newPassword: string) => {
+ await AutheliaService.resetPassword(newPassword);
+ await dispatch(push('/'));
+ },
+ onCancelClicked: async () => {
+ await dispatch(push('/'));
+ }
+ }
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(ResetPasswordView);
\ 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
deleted file mode 100644
index 2364c99d3..000000000
--- a/client-react/src/containers/views/SecondFactorView/SecondFactorView.ts
+++ /dev/null
@@ -1,136 +0,0 @@
-import { connect } from 'react-redux';
-import QueryString from 'query-string';
-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, securityKeySignSuccess, securityKeySign, securityKeySignFailure, setSecurityKeySupported } from '../../../reducers/Portal/SecondFactor/actions';
-import AuthenticationLevel from '../../../types/AuthenticationLevel';
-import RemoteState from '../../../reducers/Portal/RemoteState';
-
-const mapStateToProps = (state: RootState) => ({
- state: state.firstFactor.remoteState,
- stateError: state.firstFactor.remoteStateError,
- securityKeySupported: state.secondFactor.securityKeySupported,
- securityKeyVerified: state.secondFactor.securityKeySignSuccess || false,
- securityKeyError: state.secondFactor.error,
-});
-
-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 completeSecurityKeySigning(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(dispatch: Dispatch, props: Props) {
- let err, result;
- dispatch(securityKeySign());
- [err, result] = await to(requestSigning());
- if (err) {
- dispatch(securityKeySignFailure(err.message));
- return;
- }
-
- [err, result] = await to(u2fApi.sign(result, 60));
- if (err) {
- dispatch(securityKeySignFailure(err.message));
- return;
- }
-
- [err, result] = await to(completeSecurityKeySigning(result as SignResponse));
- if (err) {
- dispatch(securityKeySignFailure(err.message));
- return;
- }
- dispatch(securityKeySignSuccess());
- await redirectUponAuthentication(props);
-}
-
-async function redirectUponAuthentication(props: Props) {
- const params = QueryString.parse(props.history.location.search);
- if ('rd' in params) {
- setTimeout(() => {
- window.location.replace(params['rd'] as string);
- }, 1500);
- }
-}
-
-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;
- }
- const isU2FSupported = await u2fApi.isSupported();
- if (isU2FSupported) {
- await dispatch(setSecurityKeySupported(true));
- await triggerSecurityKeySigning(dispatch, ownProps);
- }
- }
- }
-}
-
-export default connect(mapStateToProps, mapDispatchToProps)(SecondFactorView);
\ 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 029223623..03aa3275f 100644
--- a/client-react/src/layouts/PortalLayout/PortalLayout.tsx
+++ b/client-react/src/layouts/PortalLayout/PortalLayout.tsx
@@ -7,11 +7,8 @@ import { AUTHELIA_GITHUB_URL } from "../../constants";
import { WithStyles, withStyles } from "@material-ui/core";
import styles from '../../assets/jss/layouts/PortalLayout/PortalLayout';
-import AuthenticationLevel from "../../types/AuthenticationLevel";
-interface Props extends RouterProps, RouteProps, WithStyles {
- authenticationLevel: AuthenticationLevel;
-}
+interface Props extends RouterProps, RouteProps, WithStyles {}
class PortalLayout extends Component {
private renderTitle() {
diff --git a/client-react/src/reducers/Portal/Authentication/actions.ts b/client-react/src/reducers/Portal/Authentication/actions.ts
new file mode 100644
index 000000000..6402f0989
--- /dev/null
+++ b/client-react/src/reducers/Portal/Authentication/actions.ts
@@ -0,0 +1,25 @@
+import { createAction } from 'typesafe-actions';
+import {
+ FETCH_STATE_REQUEST,
+ FETCH_STATE_SUCCESS,
+ FETCH_STATE_FAILURE,
+ SET_REDIRECTION_URL,
+} from "../../constants";
+import RemoteState from '../../../views/AuthenticationView/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);
+ }
+});
+
+export const setRedirectionUrl = createAction(SET_REDIRECTION_URL, resolve => {
+ return (url: string) => resolve(url);
+})
\ No newline at end of file
diff --git a/client-react/src/reducers/Portal/Authentication/reducer.ts b/client-react/src/reducers/Portal/Authentication/reducer.ts
new file mode 100644
index 000000000..c5ef18c08
--- /dev/null
+++ b/client-react/src/reducers/Portal/Authentication/reducer.ts
@@ -0,0 +1,51 @@
+
+import * as Actions from './actions';
+import { ActionType, getType } from 'typesafe-actions';
+import RemoteState from '../../../views/AuthenticationView/RemoteState';
+
+export type Action = ActionType;
+
+interface State {
+ redirectionUrl : string | null;
+ remoteState: RemoteState | null;
+ remoteStateLoading: boolean;
+ remoteStateError: string | null;
+}
+
+const initialState: State = {
+ redirectionUrl: null,
+
+ remoteState: null,
+ remoteStateLoading: false,
+ remoteStateError: null,
+}
+
+export default (state = initialState, action: Action): State => {
+ switch(action.type) {
+ 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.setRedirectionUrl):
+ return {
+ ...state,
+ redirectionUrl: action.payload,
+ }
+ }
+ return state;
+}
\ No newline at end of file
diff --git a/client-react/src/reducers/Portal/FirstFactor/actions.ts b/client-react/src/reducers/Portal/FirstFactor/actions.ts
index 005389ba5..cbbf2cdd2 100644
--- a/client-react/src/reducers/Portal/FirstFactor/actions.ts
+++ b/client-react/src/reducers/Portal/FirstFactor/actions.ts
@@ -2,25 +2,8 @@ import { createAction } from 'typesafe-actions';
import {
AUTHENTICATE_REQUEST,
AUTHENTICATE_SUCCESS,
- AUTHENTICATE_FAILURE,
- FETCH_STATE_REQUEST,
- FETCH_STATE_SUCCESS,
- FETCH_STATE_FAILURE,
+ AUTHENTICATE_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);
diff --git a/client-react/src/reducers/Portal/FirstFactor/reducer.ts b/client-react/src/reducers/Portal/FirstFactor/reducer.ts
index 07ac6d601..f7aa685db 100644
--- a/client-react/src/reducers/Portal/FirstFactor/reducer.ts
+++ b/client-react/src/reducers/Portal/FirstFactor/reducer.ts
@@ -1,7 +1,6 @@
import * as Actions from './actions';
-import { ActionType, getType, StateType } from 'typesafe-actions';
-import RemoteState from '../RemoteState';
+import { ActionType, getType } from 'typesafe-actions';
export type FirstFactorAction = ActionType;
@@ -11,28 +10,19 @@ enum Result {
FAILURE,
}
-interface State {
+interface FirstFactorState {
lastResult: Result;
loading: boolean;
error: string | null;
-
- remoteState: RemoteState | null;
- remoteStateLoading: boolean;
- remoteStateError: string | null;
}
-const initialState: State = {
+const firstFactorInitialState: FirstFactorState = {
lastResult: Result.NONE,
loading: false,
error: null,
- remoteState: null,
- remoteStateLoading: false,
- remoteStateError: null,
}
-export type PortalState = StateType;
-
-export default (state = initialState, action: FirstFactorAction) => {
+export default (state = firstFactorInitialState, action: FirstFactorAction): FirstFactorState => {
switch(action.type) {
case getType(Actions.authenticate):
return {
@@ -54,26 +44,6 @@ export default (state = initialState, action: FirstFactorAction) => {
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,
- };
}
return state;
}
\ No newline at end of file
diff --git a/client-react/src/reducers/Portal/ForgotPassword/actions.ts b/client-react/src/reducers/Portal/ForgotPassword/actions.ts
new file mode 100644
index 000000000..9182dfa93
--- /dev/null
+++ b/client-react/src/reducers/Portal/ForgotPassword/actions.ts
@@ -0,0 +1,13 @@
+import { createAction } from 'typesafe-actions';
+import {
+ FORGOT_PASSWORD_REQUEST,
+ FORGOT_PASSWORD_SUCCESS,
+ FORGOT_PASSWORD_FAILURE
+} from "../../constants";
+
+/* AUTHENTICATE_REQUEST */
+export const forgotPasswordRequest = createAction(FORGOT_PASSWORD_REQUEST);
+export const forgotPasswordSuccess = createAction(FORGOT_PASSWORD_SUCCESS);
+export const forgotPasswordFailure = createAction(FORGOT_PASSWORD_FAILURE, resolve => {
+ return (error: string) => resolve(error);
+});
diff --git a/client-react/src/reducers/Portal/ForgotPassword/reducer.ts b/client-react/src/reducers/Portal/ForgotPassword/reducer.ts
new file mode 100644
index 000000000..d210eb63a
--- /dev/null
+++ b/client-react/src/reducers/Portal/ForgotPassword/reducer.ts
@@ -0,0 +1,44 @@
+
+import * as Actions from './actions';
+import { ActionType, getType } from 'typesafe-actions';
+
+export type Action = ActionType;
+
+
+interface State {
+ loading: boolean;
+ success: boolean | null;
+ error: string | null;
+}
+
+const initialState: State = {
+ loading: false,
+ success: null,
+ error: null,
+}
+
+export default (state = initialState, action: Action): State => {
+ switch(action.type) {
+ case getType(Actions.forgotPasswordRequest):
+ return {
+ ...state,
+ loading: true,
+ error: null
+ };
+ case getType(Actions.forgotPasswordSuccess):
+ return {
+ ...state,
+ success: true,
+ loading: false,
+ error: null,
+ };
+ case getType(Actions.forgotPasswordFailure):
+ return {
+ ...state,
+ success: false,
+ loading: false,
+ error: action.payload,
+ };
+ }
+ return state;
+}
\ No newline at end of file
diff --git a/client-react/src/reducers/Portal/OneTimePasswordRegistration/reducer.ts b/client-react/src/reducers/Portal/OneTimePasswordRegistration/reducer.ts
index 8d3d8a718..0ac990418 100644
--- a/client-react/src/reducers/Portal/OneTimePasswordRegistration/reducer.ts
+++ b/client-react/src/reducers/Portal/OneTimePasswordRegistration/reducer.ts
@@ -4,28 +4,19 @@ import { Secret } from "../../../views/OneTimePasswordRegistrationView/Secret";
type OneTimePasswordRegistrationAction = ActionType
-export interface State {
+export interface OneTimePasswordRegistrationState {
loading: boolean;
error: string | null;
secret: Secret | null;
}
-let initialState: State = {
+let oneTimePasswordRegistrationInitialState: OneTimePasswordRegistrationState = {
loading: true,
error: null,
secret: null,
}
-initialState = {
- secret: {
- base32_secret: 'PBSFWU2RM42HG3TNIRHUQMKSKVUW6NCNOBNFOLCFJZATS6CTI47A',
- otpauth_url: 'PBSFWU2RM42HG3TNIRHUQMKSKVUW6NCNOBNFOLCFJZATS6CTI47A',
- },
- error: null,
- loading: false,
-}
-
-export default (state = initialState, action: OneTimePasswordRegistrationAction) => {
+export default (state = oneTimePasswordRegistrationInitialState, action: OneTimePasswordRegistrationAction): OneTimePasswordRegistrationState => {
switch(action.type) {
case getType(Actions.generateTotpSecret):
return {
diff --git a/client-react/src/reducers/Portal/ResetPassword/actions.ts b/client-react/src/reducers/Portal/ResetPassword/actions.ts
new file mode 100644
index 000000000..9220ec435
--- /dev/null
+++ b/client-react/src/reducers/Portal/ResetPassword/actions.ts
@@ -0,0 +1,9 @@
+import { createAction } from 'typesafe-actions';
+import { RESET_PASSWORD_REQUEST, RESET_PASSWORD_SUCCESS, RESET_PASSWORD_FAILURE } from "../../constants";
+
+/* AUTHENTICATE_REQUEST */
+export const resetPasswordRequest = createAction(RESET_PASSWORD_REQUEST);
+export const resetPasswordSuccess = createAction(RESET_PASSWORD_SUCCESS);
+export const resetPasswordFailure = createAction(RESET_PASSWORD_FAILURE, resolve => {
+ return (error: string) => resolve(error);
+});
diff --git a/client-react/src/reducers/Portal/ResetPassword/reducer.ts b/client-react/src/reducers/Portal/ResetPassword/reducer.ts
new file mode 100644
index 000000000..8d415ac9e
--- /dev/null
+++ b/client-react/src/reducers/Portal/ResetPassword/reducer.ts
@@ -0,0 +1,44 @@
+
+import * as Actions from './actions';
+import { ActionType, getType } from 'typesafe-actions';
+
+export type Action = ActionType;
+
+
+interface State {
+ loading: boolean;
+ success: boolean | null;
+ error: string | null;
+}
+
+const initialState: State = {
+ loading: false,
+ success: null,
+ error: null,
+}
+
+export default (state = initialState, action: Action): State => {
+ switch(action.type) {
+ case getType(Actions.resetPasswordRequest):
+ return {
+ ...state,
+ loading: true,
+ error: null
+ };
+ case getType(Actions.resetPasswordSuccess):
+ return {
+ ...state,
+ success: true,
+ loading: false,
+ error: null,
+ };
+ case getType(Actions.resetPasswordFailure):
+ return {
+ ...state,
+ success: false,
+ loading: false,
+ error: action.payload,
+ };
+ }
+ return state;
+}
\ No newline at end of file
diff --git a/client-react/src/reducers/Portal/SecondFactor/actions.ts b/client-react/src/reducers/Portal/SecondFactor/actions.ts
index befe92c55..c934824ef 100644
--- a/client-react/src/reducers/Portal/SecondFactor/actions.ts
+++ b/client-react/src/reducers/Portal/SecondFactor/actions.ts
@@ -6,7 +6,10 @@ import {
SECURITY_KEY_SIGN,
SECURITY_KEY_SIGN_SUCCESS,
SECURITY_KEY_SIGN_FAILURE,
- SET_SECURITY_KEY_SUPPORTED
+ SET_SECURITY_KEY_SUPPORTED,
+ ONE_TIME_PASSWORD_VERIFICATION_REQUEST,
+ ONE_TIME_PASSWORD_VERIFICATION_SUCCESS,
+ ONE_TIME_PASSWORD_VERIFICATION_FAILURE
} from "../../constants";
export const setSecurityKeySupported = createAction(SET_SECURITY_KEY_SUPPORTED, resolve => {
@@ -17,7 +20,14 @@ export const securityKeySign = createAction(SECURITY_KEY_SIGN);
export const securityKeySignSuccess = createAction(SECURITY_KEY_SIGN_SUCCESS);
export const securityKeySignFailure = createAction(SECURITY_KEY_SIGN_FAILURE, resolve => {
return (error: string) => resolve(error);
-})
+});
+
+export const oneTimePasswordVerification = createAction(ONE_TIME_PASSWORD_VERIFICATION_REQUEST);
+export const oneTimePasswordVerificationSuccess = createAction(ONE_TIME_PASSWORD_VERIFICATION_SUCCESS);
+export const oneTimePasswordVerificationFailure = createAction(ONE_TIME_PASSWORD_VERIFICATION_FAILURE, resolve => {
+ return (err: string) => resolve(err);
+});
+
export const logout = createAction(LOGOUT_REQUEST);
export const logoutSuccess = createAction(LOGOUT_SUCCESS);
diff --git a/client-react/src/reducers/Portal/SecondFactor/reducer.ts b/client-react/src/reducers/Portal/SecondFactor/reducer.ts
index e3d6f9377..86a446122 100644
--- a/client-react/src/reducers/Portal/SecondFactor/reducer.ts
+++ b/client-react/src/reducers/Portal/SecondFactor/reducer.ts
@@ -4,7 +4,7 @@ import { ActionType, getType, StateType } from 'typesafe-actions';
export type SecondFactorAction = ActionType;
-interface State {
+interface SecondFactorState {
logoutLoading: boolean;
logoutSuccess: boolean | null;
error: string | null;
@@ -12,9 +12,13 @@ interface State {
securityKeySupported: boolean;
securityKeySignLoading: boolean;
securityKeySignSuccess: boolean | null;
+
+ oneTimePasswordVerificationLoading: boolean,
+ oneTimePasswordVerificationSuccess: boolean | null,
+ oneTimePasswordVerificationError: string | null,
}
-const initialState: State = {
+const secondFactorInitialState: SecondFactorState = {
logoutLoading: false,
logoutSuccess: null,
error: null,
@@ -22,11 +26,15 @@ const initialState: State = {
securityKeySupported: false,
securityKeySignLoading: false,
securityKeySignSuccess: null,
+
+ oneTimePasswordVerificationLoading: false,
+ oneTimePasswordVerificationError: null,
+ oneTimePasswordVerificationSuccess: null,
}
-export type PortalState = StateType;
+export type PortalState = StateType;
-export default (state = initialState, action: SecondFactorAction): State => {
+export default (state = secondFactorInitialState, action: SecondFactorAction): SecondFactorState => {
switch(action.type) {
case getType(Actions.logout):
return {
@@ -47,6 +55,7 @@ export default (state = initialState, action: SecondFactorAction): State => {
logoutLoading: false,
error: action.payload,
}
+
case getType(Actions.securityKeySign):
return {
...state,
@@ -65,11 +74,31 @@ export default (state = initialState, action: SecondFactorAction): State => {
securityKeySignLoading: false,
securityKeySignSuccess: false,
};
+
case getType(Actions.setSecurityKeySupported):
return {
...state,
securityKeySupported: action.payload,
};
+
+ case getType(Actions.oneTimePasswordVerification):
+ return {
+ ...state,
+ oneTimePasswordVerificationLoading: true,
+ oneTimePasswordVerificationError: null,
+ }
+ case getType(Actions.oneTimePasswordVerificationSuccess):
+ return {
+ ...state,
+ oneTimePasswordVerificationLoading: false,
+ oneTimePasswordVerificationSuccess: true,
+ }
+ case getType(Actions.oneTimePasswordVerificationFailure):
+ return {
+ ...state,
+ oneTimePasswordVerificationLoading: false,
+ oneTimePasswordVerificationError: action.payload,
+ }
}
return state;
}
\ No newline at end of file
diff --git a/client-react/src/reducers/Portal/SecurityKeyRegistration/reducer.ts b/client-react/src/reducers/Portal/SecurityKeyRegistration/reducer.ts
index 41d80bdf8..00ff3c530 100644
--- a/client-react/src/reducers/Portal/SecurityKeyRegistration/reducer.ts
+++ b/client-react/src/reducers/Portal/SecurityKeyRegistration/reducer.ts
@@ -3,17 +3,17 @@ import * as Actions from './actions';
type SecurityKeyRegistrationAction = ActionType
-export interface State {
+export interface SecurityKeyRegistrationState {
error: string | null;
success: boolean | null;
}
-let initialState: State = {
+let securityKeyRegistrationInitialState: SecurityKeyRegistrationState = {
error: null,
success: null,
}
-export default (state = initialState, action: SecurityKeyRegistrationAction): State => {
+export default (state = securityKeyRegistrationInitialState, action: SecurityKeyRegistrationAction): SecurityKeyRegistrationState => {
switch(action.type) {
case getType(Actions.registerSecurityKey):
return {
diff --git a/client-react/src/reducers/Portal/index.ts b/client-react/src/reducers/Portal/index.ts
index d8537b204..65265caa9 100644
--- a/client-react/src/reducers/Portal/index.ts
+++ b/client-react/src/reducers/Portal/index.ts
@@ -4,10 +4,25 @@ import FirstFactorReducer from './FirstFactor/reducer';
import SecondFactorReducer from './SecondFactor/reducer';
import OneTimePasswordRegistrationReducer from './OneTimePasswordRegistration/reducer';
import SecurityKeyRegistrationReducer from './SecurityKeyRegistration/reducer';
+import AuthenticationReducer from './Authentication/reducer';
+import ForgotPasswordReducer from './ForgotPassword/reducer';
+import ResetPasswordReducer from './ResetPassword/reducer';
-export default combineReducers({
- firstFactor: FirstFactorReducer,
- secondFactor: SecondFactorReducer,
- oneTimePasswordRegistration: OneTimePasswordRegistrationReducer,
- securityKeyRegistration: SecurityKeyRegistrationReducer,
-});
\ No newline at end of file
+import { connectRouter } from 'connected-react-router'
+import { History } from 'history';
+
+function reducer(history: History) {
+ return combineReducers({
+ router: connectRouter(history),
+ authentication: AuthenticationReducer,
+ firstFactor: FirstFactorReducer,
+ secondFactor: SecondFactorReducer,
+ oneTimePasswordRegistration: OneTimePasswordRegistrationReducer,
+ securityKeyRegistration: SecurityKeyRegistrationReducer,
+ forgotPassword: ForgotPasswordReducer,
+ resetPassword: ResetPasswordReducer,
+ });
+}
+
+
+export default reducer;
\ No newline at end of file
diff --git a/client-react/src/reducers/constants.ts b/client-react/src/reducers/constants.ts
index 23f8a9319..4b9784d9f 100644
--- a/client-react/src/reducers/constants.ts
+++ b/client-react/src/reducers/constants.ts
@@ -3,6 +3,10 @@ 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';
+// AUTHENTICATION PROCESS
+
+export const SET_REDIRECTION_URL = '@portal/authenticate/set_redirection_url';
+
export const AUTHENTICATE_REQUEST = '@portal/authenticate_request';
export const AUTHENTICATE_SUCCESS = '@portal/authenticate_success';
export const AUTHENTICATE_FAILURE = '@portal/authenticate_failure';
@@ -14,6 +18,10 @@ export const SECURITY_KEY_SIGN = '@portal/second_factor/security_key_sign';
export const SECURITY_KEY_SIGN_SUCCESS = '@portal/second_factor/security_key_sign_success';
export const SECURITY_KEY_SIGN_FAILURE = '@portal/second_factor/security_key_sign_failure';
+export const ONE_TIME_PASSWORD_VERIFICATION_REQUEST = '@portal/second_factor/one_time_password_verification_request';
+export const ONE_TIME_PASSWORD_VERIFICATION_SUCCESS = '@portal/second_factor/one_time_password_verification_success';
+export const ONE_TIME_PASSWORD_VERIFICATION_FAILURE = '@portal/second_factor/one_time_password_verification_failure';
+
export const LOGOUT_REQUEST = '@portal/logout_request';
export const LOGOUT_SUCCESS = '@portal/logout_success';
export const LOGOUT_FAILURE = '@portal/logout_failure';
@@ -26,4 +34,14 @@ export const GENERATE_TOTP_SECRET_FAILURE = '@portal/generate_totp_secret_failur
// U2F REGISTRATION
export const REGISTER_SECURITY_KEY_REQUEST = '@portal/security_key_registration/register_request';
export const REGISTER_SECURITY_KEY_SUCCESS = '@portal/security_key_registration/register_success';
-export const REGISTER_SECURITY_KEY_FAILURE = '@portal/security_key_registration/register_failed';
\ No newline at end of file
+export const REGISTER_SECURITY_KEY_FAILURE = '@portal/security_key_registration/register_failed';
+
+// FORGOT PASSWORD
+export const FORGOT_PASSWORD_REQUEST = '@portal/forgot_password/forgot_password_request';
+export const FORGOT_PASSWORD_SUCCESS = '@portal/forgot_password/forgot_password_success';
+export const FORGOT_PASSWORD_FAILURE = '@portal/forgot_password/forgot_password_failure';
+
+// FORGOT PASSWORD
+export const RESET_PASSWORD_REQUEST = '@portal/forgot_password/reset_password_request';
+export const RESET_PASSWORD_SUCCESS = '@portal/forgot_password/reset_password_success';
+export const RESET_PASSWORD_FAILURE = '@portal/forgot_password/reset_password_failure';
\ No newline at end of file
diff --git a/client-react/src/reducers/index.ts b/client-react/src/reducers/index.ts
index 41da5ac60..bdaa1bb58 100644
--- a/client-react/src/reducers/index.ts
+++ b/client-react/src/reducers/index.ts
@@ -1,6 +1,11 @@
import PortalReducer from './Portal';
import { StateType } from 'typesafe-actions';
-export type RootState = StateType;
+function getReturnType (f: (...args: any[]) => R): R {
+ return null!;
+}
+
+const t = getReturnType(PortalReducer)
+export type RootState = StateType;
export default PortalReducer;
\ No newline at end of file
diff --git a/client-react/src/routes/routes.ts b/client-react/src/routes/routes.ts
index f7d54929c..e0375e1a6 100644
--- a/client-react/src/routes/routes.ts
+++ b/client-react/src/routes/routes.ts
@@ -1,19 +1,14 @@
-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";
+import ForgotPasswordView from "../containers/views/ForgotPasswordView/ForgotPasswordView";
+import ResetPasswordView from "../containers/views/ResetPasswordView/ResetPasswordView";
+import AuthenticationView from "../containers/views/AuthenticationView/AuthenticationView";
export const routes = [{
path: '/',
title: 'Login',
- component: FirstFactorView,
-}, {
- path: '/2fa',
- title: '2-factor',
- component: SecondFactorView,
+ component: AuthenticationView,
}, {
path: '/confirmation-sent',
title: 'e-mail sent',
diff --git a/client-react/src/services/AutheliaService.ts b/client-react/src/services/AutheliaService.ts
new file mode 100644
index 000000000..5c4a962bc
--- /dev/null
+++ b/client-react/src/services/AutheliaService.ts
@@ -0,0 +1,117 @@
+import RemoteState from "../views/AuthenticationView/RemoteState";
+import u2fApi, { SignRequest } from "u2f-api";
+
+async function fetchSafe(url: string, options?: RequestInit) {
+ return fetch(url, options)
+ .then(async (res) => {
+ if (res.status !== 200 && res.status !== 204) {
+ throw new Error('Status code ' + res.status);
+ }
+ return res;
+ });
+}
+
+/**
+ * Fetch current authentication state.
+ */
+export async function fetchState() {
+ return fetchSafe('/api/state')
+ .then(async (res) => {
+ const body = await res.json() as RemoteState;
+ return body;
+ });
+}
+
+export async function postFirstFactorAuth(username: string, password: string) {
+ return fetchSafe('/api/firstfactor', {
+ method: 'POST',
+ headers: {
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ username: username,
+ password: password,
+ })
+ });
+}
+
+export async function postLogout() {
+ return fetchSafe('/api/logout', {
+ method: 'POST',
+ headers: {
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json',
+ },
+ })
+}
+
+export async function startU2FRegistrationIdentityProcess() {
+ return fetchSafe('/api/secondfactor/u2f/identity/start', {
+ method: 'POST',
+ });
+}
+
+export async function startTOTPRegistrationIdentityProcess() {
+ return fetchSafe('/api/secondfactor/totp/identity/start', {
+ method: 'POST',
+ });
+}
+
+export async function requestSigning() {
+ return fetchSafe('/api/u2f/sign_request')
+ .then(async (res) => {
+ const body = await res.json();
+ return body as SignRequest;
+ });
+}
+
+export async function completeSecurityKeySigning(response: u2fApi.SignResponse) {
+ return fetchSafe('/api/u2f/sign', {
+ method: 'POST',
+ headers: {
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(response),
+ });
+}
+
+export async function verifyTotpToken(token: string) {
+ return fetchSafe('/api/totp', {
+ method: 'POST',
+ headers: {
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({token}),
+ })
+}
+
+export async function initiatePasswordResetIdentityValidation(username: string) {
+ return fetchSafe('/api/password-reset/identity/start', {
+ method: 'POST',
+ headers: {
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({username})
+ });
+}
+
+export async function completePasswordResetIdentityValidation(token: string) {
+ return fetch(`/api/password-reset/identity/finish?token=${token}`, {
+ method: 'POST',
+ });
+}
+
+export async function resetPassword(newPassword: string) {
+ return fetchSafe('/api/password-reset', {
+ method: 'POST',
+ headers: {
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({password: newPassword})
+ });
+}
\ No newline at end of file
diff --git a/client-react/src/views/AuthenticationView/AuthenticationView.tsx b/client-react/src/views/AuthenticationView/AuthenticationView.tsx
new file mode 100644
index 000000000..90f8480cb
--- /dev/null
+++ b/client-react/src/views/AuthenticationView/AuthenticationView.tsx
@@ -0,0 +1,53 @@
+import React, { Component } from "react";
+import AlreadyAuthenticated from "../../containers/components/AlreadyAuthenticated/AlreadyAuthenticated";
+import FirstFactorForm from "../../containers/components/FirstFactorForm/FirstFactorForm";
+import SecondFactorForm from "../../containers/components/SecondFactorForm/SecondFactorForm";
+import RemoteState from "./RemoteState";
+import { RouterProps, Redirect } from "react-router";
+import queryString from 'query-string';
+
+export enum Stage {
+ FIRST_FACTOR,
+ SECOND_FACTOR,
+ ALREADY_AUTHENTICATED,
+}
+
+export interface StateProps {
+ stage: Stage;
+ remoteState: RemoteState | null;
+ redirectionUrl: string | null;
+}
+
+export interface DispatchProps {
+ onInit: (redirectionUrl?: string) => void;
+}
+
+export type Props = StateProps & DispatchProps & RouterProps;
+
+class AuthenticationView extends Component {
+ componentDidMount() {
+ if (this.props.history.location) {
+ const params = queryString.parse(this.props.history.location.search);
+ if ('rd' in params) {
+ this.props.onInit(params['rd'] as string);
+ }
+ }
+ this.props.onInit();
+ }
+
+ render() {
+ if (!this.props.remoteState) return null;
+
+ if (this.props.stage === Stage.SECOND_FACTOR) {
+ return ;
+ } else if (this.props.stage === Stage.ALREADY_AUTHENTICATED) {
+ return ;
+ }
+ return ;
+ }
+}
+
+export default AuthenticationView;
\ No newline at end of file
diff --git a/client-react/src/reducers/Portal/RemoteState.ts b/client-react/src/views/AuthenticationView/RemoteState.ts
similarity index 100%
rename from client-react/src/reducers/Portal/RemoteState.ts
rename to client-react/src/views/AuthenticationView/RemoteState.ts
diff --git a/client-react/src/views/ForgotPasswordView/ForgotPasswordView.tsx b/client-react/src/views/ForgotPasswordView/ForgotPasswordView.tsx
index b7c00c30a..77df0862f 100644
--- a/client-react/src/views/ForgotPasswordView/ForgotPasswordView.tsx
+++ b/client-react/src/views/ForgotPasswordView/ForgotPasswordView.tsx
@@ -1,31 +1,84 @@
-import React, { Component } from "react";
+import React, { Component, ChangeEvent, KeyboardEvent } from "react";
import { TextField, WithStyles, withStyles, Button } from "@material-ui/core";
+import classnames from 'classnames';
import styles from '../../assets/jss/views/ForgotPasswordView/ForgotPasswordView';
-import { RouterProps } from "react-router";
-interface Props extends WithStyles, RouterProps {}
+export interface StateProps {
+ disabled: boolean;
+}
+
+export interface DispatchProps {
+ onPasswordResetRequested: (username: string) => void;
+ onCancelClicked: () => void;
+}
+
+export type Props = StateProps & DispatchProps & WithStyles;
+
+interface State {
+ username: string;
+}
+
+class ForgotPasswordView extends Component {
+ constructor(props: Props) {
+ super(props);
+ this.state = {
+ username: '',
+ }
+ }
+
+ private onUsernameChanged = (e: ChangeEvent) => {
+ this.setState({username: e.target.value});
+ }
+
+ private onKeyPressed = (e: KeyboardEvent) => {
+ if (e.key == 'Enter') {
+ this.onPasswordResetRequested();
+ }
+ }
+
+ private onPasswordResetRequested = () => {
+ if (this.state.username.length == 0) return;
+ this.props.onPasswordResetRequested(this.state.username);
+ }
-class ForgotPasswordView extends Component {
render() {
const { classes } = this.props;
return (
-
What's you e-mail address?
+
What's your username?
+ id="username"
+ label="Username"
+ onChange={this.onUsernameChanged}
+ onKeyPress={this.onKeyPressed}
+ value={this.state.username}
+ disabled={this.props.disabled}>
-
+
+
+
+
+
+
+
+
);
diff --git a/client-react/src/views/ResetPasswordView/ResetPasswordView.tsx b/client-react/src/views/ResetPasswordView/ResetPasswordView.tsx
index b1c08d366..148f83aea 100644
--- a/client-react/src/views/ResetPasswordView/ResetPasswordView.tsx
+++ b/client-react/src/views/ResetPasswordView/ResetPasswordView.tsx
@@ -1,16 +1,86 @@
-import React, { Component } from "react";
+import React, { Component, KeyboardEvent, ChangeEvent } from "react";
import { TextField, Button, WithStyles, withStyles } from "@material-ui/core";
import { RouterProps } from "react-router";
+import classnames from 'classnames';
+import QueryString from 'query-string';
import styles from '../../assets/jss/views/ResetPasswordView/ResetPasswordView';
+import FormNotification from "../../components/FormNotification/FormNotification";
-interface Props extends RouterProps, WithStyles {};
+export interface StateProps {
+ disabled: boolean;
+}
+
+export interface DispatchProps {
+ onInit: (token: string) => void;
+ onPasswordResetRequested: (password: string) => void;
+ onCancelClicked: () => void;
+}
+
+export type Props = StateProps & DispatchProps & RouterProps & WithStyles;
+
+interface State {
+ password1: string;
+ password2: string;
+ error: string | null,
+}
+
+class ResetPasswordView extends Component {
+ constructor(props: Props) {
+ super(props);
+ this.state = {
+ password1: '',
+ password2: '',
+ error: null,
+ }
+ }
+
+ componentWillMount() {
+ if (!this.props.history.location) {
+ console.error('There is no location to retrieve query params from...');
+ return;
+ }
+ const params = QueryString.parse(this.props.history.location.search);
+ if (!('token' in params)) {
+ console.error('Token parameter is expected and not provided');
+ return;
+ }
+ this.props.onInit(params['token'] as string);
+ }
+
+ private onPasswordResetRequested() {
+ if (this.state.password1 && this.state.password1 === this.state.password2) {
+ this.props.onPasswordResetRequested(this.state.password1);
+ } else {
+ this.setState({error: 'The passwords are different.'});
+ }
+ }
+
+ private onKeyPressed = (e: KeyboardEvent) => {
+ if (e.key == 'Enter') {
+ this.onPasswordResetRequested();
+ }
+ }
+
+ private onResetClicked = () => {
+ this.onPasswordResetRequested();
+ }
+
+ private onPassword1Changed = (e: ChangeEvent) => {
+ this.setState({password1: e.target.value});
+ }
+
+ private onPassword2Changed = (e: ChangeEvent) => {
+ this.setState({password2: e.target.value});
+ }
-class ResetPasswordView extends Component {
render() {
const { classes } = this.props;
return (
+
+ {this.state.error}
+
Enter your new password
{
variant="outlined"
type="password"
id="password1"
+ value={this.state.password1}
+ onChange={this.onPassword1Changed}
+ disabled={this.props.disabled}
label="New password">
{
variant="outlined"
type="password"
id="password2"
+ value={this.state.password2}
+ onKeyPress={this.onKeyPressed}
+ onChange={this.onPassword2Changed}
+ disabled={this.props.disabled}
label="Confirm password">
-
+
+
+
+
+
+
+
+
)
diff --git a/server/src/lib/routes/firstfactor/post.ts b/server/src/lib/routes/firstfactor/post.ts
index e13320713..1e5b9e6a9 100644
--- a/server/src/lib/routes/firstfactor/post.ts
+++ b/server/src/lib/routes/firstfactor/post.ts
@@ -94,8 +94,8 @@ export default function (vars: ServerVariables) {
})
.catch(Exceptions.LdapBindError, function (err: Error) {
vars.regulator.mark(username, false);
- return ErrorReplies.replyWithError200(req, res, vars.logger, UserMessages.OPERATION_FAILED)(err);
+ return ErrorReplies.replyWithError200(req, res, vars.logger, UserMessages.AUTHENTICATION_FAILED)(err);
})
- .catch(ErrorReplies.replyWithError200(req, res, vars.logger, UserMessages.OPERATION_FAILED));
+ .catch(ErrorReplies.replyWithError200(req, res, vars.logger, UserMessages.AUTHENTICATION_FAILED));
};
}
\ No newline at end of file
diff --git a/server/src/lib/routes/password-reset/identity/PasswordResetHandler.ts b/server/src/lib/routes/password-reset/identity/PasswordResetHandler.ts
index a5116a8b9..55dc233fd 100644
--- a/server/src/lib/routes/password-reset/identity/PasswordResetHandler.ts
+++ b/server/src/lib/routes/password-reset/identity/PasswordResetHandler.ts
@@ -28,7 +28,7 @@ export default class PasswordResetHandler implements IdentityValidable {
preValidationInit(req: express.Request): BluebirdPromise {
const that = this;
const userid: string =
- objectPath.get(req, "query.userid");
+ objectPath.get(req, "body.username");
return BluebirdPromise.resolve()
.then(function () {
that.logger.debug(req, "User '%s' requested a password reset", userid);
diff --git a/server/src/lib/routes/password-reset/request/get.ts b/server/src/lib/routes/password-reset/request/get.ts
index 8f3ae2b4b..d77a000f3 100644
--- a/server/src/lib/routes/password-reset/request/get.ts
+++ b/server/src/lib/routes/password-reset/request/get.ts
@@ -1,10 +1,5 @@
import express = require("express");
-import BluebirdPromise = require("bluebird");
-import objectPath = require("object-path");
-import exceptions = require("../../../Exceptions");
-
-import Constants = require("./../constants");
const TEMPLATE_NAME = "password-reset-request";
diff --git a/server/src/lib/routes/secondfactor/totp/sign/post.ts b/server/src/lib/routes/secondfactor/totp/sign/post.ts
index 2da0cfad6..0f3797405 100644
--- a/server/src/lib/routes/secondfactor/totp/sign/post.ts
+++ b/server/src/lib/routes/secondfactor/totp/sign/post.ts
@@ -34,7 +34,7 @@ export default function (vars: ServerVariables) {
return Bluebird.resolve();
})
.catch(ErrorReplies.replyWithError200(req, res, vars.logger,
- UserMessages.OPERATION_FAILED));
+ UserMessages.AUTHENTICATION_TOTP_FAILED));
}
return handler;
}
diff --git a/server/src/lib/routes/secondfactor/u2f/sign/post.ts b/server/src/lib/routes/secondfactor/u2f/sign/post.ts
index 7ee711c2c..2f333c85d 100644
--- a/server/src/lib/routes/secondfactor/u2f/sign/post.ts
+++ b/server/src/lib/routes/secondfactor/u2f/sign/post.ts
@@ -49,7 +49,7 @@ export default function (vars: ServerVariables) {
return BluebirdPromise.resolve();
})
.catch(ErrorReplies.replyWithError200(req, res, vars.logger,
- UserMessages.OPERATION_FAILED));
+ UserMessages.AUTHENTICATION_U2F_FAILED));
}
return handler;
diff --git a/shared/api.ts b/shared/api.ts
index 65be01e4a..2844cdcb7 100644
--- a/shared/api.ts
+++ b/shared/api.ts
@@ -187,7 +187,7 @@ export const RESET_PASSWORD_FORM_POST = "/api/password-reset";
*
* @apiDescription Serve a page that requires the username.
*/
-export const RESET_PASSWORD_REQUEST_GET = "/password-reset/request";
+export const RESET_PASSWORD_REQUEST_GET = "/api/password-reset/request";
@@ -201,7 +201,7 @@ export const RESET_PASSWORD_REQUEST_GET = "/password-reset/request";
*
* @apiDescription Start password reset request.
*/
-export const RESET_PASSWORD_IDENTITY_START_GET = "/password-reset/identity/start";
+export const RESET_PASSWORD_IDENTITY_START_GET = "/api/password-reset/identity/start";
@@ -215,7 +215,7 @@ export const RESET_PASSWORD_IDENTITY_START_GET = "/password-reset/identity/start
*
* @apiDescription Start password reset request.
*/
-export const RESET_PASSWORD_IDENTITY_FINISH_GET = "/password-reset/identity/finish";
+export const RESET_PASSWORD_IDENTITY_FINISH_GET = "/api/password-reset/identity/finish";