#389: allow switch accounts, when refreshToken is invalid. Fix oauth in case, when refreshToken is invalid

This commit is contained in:
SleepWalker 2018-02-17 21:59:35 +02:00
parent f64fb3b96b
commit bf2976c009
11 changed files with 187 additions and 34 deletions

View File

@ -1,9 +1,8 @@
// @flow // @flow
import { getJwtPayload } from 'functions'; import { getJwtPayload } from 'functions';
import { browserHistory } from 'services/history';
import { sessionStorage } from 'services/localStorage'; import { sessionStorage } from 'services/localStorage';
import authentication from 'services/api/authentication'; import authentication from 'services/api/authentication';
import { setLogin } from 'components/auth/actions'; import { relogin as navigateToLogin } from 'components/auth/actions';
import { updateUser, setGuest } from 'components/user/actions'; import { updateUser, setGuest } from 'components/user/actions';
import { setLocale } from 'components/i18n/actions'; import { setLocale } from 'components/i18n/actions';
import { setAccountSwitcher } from 'components/auth/actions'; import { setAccountSwitcher } from 'components/auth/actions';
@ -238,8 +237,7 @@ export function relogin(email?: string) {
email = activeAccount.email; email = activeAccount.email;
} }
email && dispatch(setLogin(email)); dispatch(navigateToLogin(email || null));
browserHistory.push('/login');
}; };
} }

View File

@ -16,6 +16,7 @@ import ContactForm from 'components/contact/ContactForm';
export { updateUser } from 'components/user/actions'; export { updateUser } from 'components/user/actions';
export { authenticate, logoutAll as logout } from 'components/accounts/actions'; export { authenticate, logoutAll as logout } from 'components/accounts/actions';
import { getCredentials } from './reducer';
/** /**
* Reoutes user to the previous page if it is possible * Reoutes user to the previous page if it is possible
@ -224,19 +225,40 @@ export function setLogin(login: ?string) {
}; };
} }
export function relogin(login: ?string) {
return (dispatch: (Function | Object) => void) => {
dispatch({
type: SET_CREDENTIALS,
payload: {
login,
returnUrl: location.pathname + location.search,
isRelogin: true,
},
});
browserHistory.push('/login');
};
}
function requestTotp({login, password, rememberMe}: { function requestTotp({login, password, rememberMe}: {
login: string, login: string,
password: string, password: string,
rememberMe: bool rememberMe: bool
}) { }) {
return { return (dispatch: (Function | Object) => void, getState: () => Object) => {
type: SET_CREDENTIALS, // merging with current credentials to propogate returnUrl
payload: { const credentials = getCredentials(getState());
login,
password, dispatch({
rememberMe, type: SET_CREDENTIALS,
isTotpRequired: true payload: {
} ...credentials,
login,
password,
rememberMe,
isTotpRequired: true
}
});
}; };
} }

View File

@ -19,11 +19,13 @@ export default class ChooseAccountBody extends BaseAuthBody {
<div> <div>
{this.renderErrors()} {this.renderErrors()}
<div className={styles.description}> {client && (
<Message {...messages.description} values={{ <div className={styles.description}>
appName: <span className={styles.appName}>{client.name}</span> <Message {...messages.description} values={{
}} /> appName: <span className={styles.appName}>{client.name}</span>
</div> }} />
</div>
)}
<div className={styles.accountSwitcherContainer}> <div className={styles.accountSwitcherContainer}>
<AccountSwitcher <AccountSwitcher

View File

@ -13,6 +13,15 @@ import {
SET_SWITCHER SET_SWITCHER
} from './actions'; } from './actions';
type Credentials = {
login?: string,
password?: string,
rememberMe?: bool,
returnUrl?: string,
isRelogin?: bool,
isTotpRequired?: bool,
};
export default combineReducers({ export default combineReducers({
credentials, credentials,
error, error,
@ -44,12 +53,7 @@ function credentials(
state = {}, state = {},
{type, payload}: { {type, payload}: {
type: string, type: string,
payload: ?{ payload: ?Credentials
login?: string,
password?: string,
rememberMe?: bool,
isTotpRequired?: bool
}
} }
) { ) {
if (type === SET_CREDENTIALS) { if (type === SET_CREDENTIALS) {
@ -166,11 +170,6 @@ export function getLogin(state: Object): ?string {
return state.auth.credentials.login || null; return state.auth.credentials.login || null;
} }
export function getCredentials(state: Object): { export function getCredentials(state: Object): Credentials {
login?: string,
password?: string,
rememberMe?: bool,
isTotpRequired?: bool
} {
return state.auth.credentials; return state.auth.credentials;
} }

View File

@ -59,6 +59,7 @@ class AuthPage extends Component<{
<Route path="/activation/:key?" render={renderPanelTransition(Activation)} /> <Route path="/activation/:key?" render={renderPanelTransition(Activation)} />
<Route path="/resend-activation" render={renderPanelTransition(ResendActivation)} /> <Route path="/resend-activation" render={renderPanelTransition(ResendActivation)} />
<Route path="/oauth/permissions" render={renderPanelTransition(Permissions)} /> <Route path="/oauth/permissions" render={renderPanelTransition(Permissions)} />
<Route path="/choose-account" render={renderPanelTransition(ChooseAccount)} />
<Route path="/oauth/choose-account" render={renderPanelTransition(ChooseAccount)} /> <Route path="/oauth/choose-account" render={renderPanelTransition(ChooseAccount)} />
<Route path="/oauth/finish" component={Finish} /> <Route path="/oauth/finish" component={Finish} />
<Route path="/accept-rules" render={renderPanelTransition(AcceptRules)} /> <Route path="/accept-rules" render={renderPanelTransition(AcceptRules)} />

View File

@ -11,6 +11,7 @@ import ForgotPasswordState from './ForgotPasswordState';
import RecoverPasswordState from './RecoverPasswordState'; import RecoverPasswordState from './RecoverPasswordState';
import ActivationState from './ActivationState'; import ActivationState from './ActivationState';
import CompleteState from './CompleteState'; import CompleteState from './CompleteState';
import ChooseAccountState from './ChooseAccountState';
import ResendActivationState from './ResendActivationState'; import ResendActivationState from './ResendActivationState';
import type AbstractState from './AbstractState'; import type AbstractState from './AbstractState';
@ -219,6 +220,10 @@ export default class AuthFlow implements AuthContext {
this.setState(new ResendActivationState()); this.setState(new ResendActivationState());
break; break;
case '/choose-account':
this.setState(new ChooseAccountState());
break;
case '/': case '/':
case '/login': case '/login':
case '/password': case '/password':

View File

@ -13,6 +13,7 @@ import ActivationState from 'services/authFlow/ActivationState';
import ResendActivationState from 'services/authFlow/ResendActivationState'; import ResendActivationState from 'services/authFlow/ResendActivationState';
import LoginState from 'services/authFlow/LoginState'; import LoginState from 'services/authFlow/LoginState';
import CompleteState from 'services/authFlow/CompleteState'; import CompleteState from 'services/authFlow/CompleteState';
import ChooseAccountState from 'services/authFlow/ChooseAccountState';
describe('AuthFlow', () => { describe('AuthFlow', () => {
let flow; let flow;
@ -275,6 +276,7 @@ describe('AuthFlow', () => {
'/oauth2/v1': OAuthState, '/oauth2/v1': OAuthState,
'/oauth2': OAuthState, '/oauth2': OAuthState,
'/register': RegisterState, '/register': RegisterState,
'/choose-account': ChooseAccountState,
'/recover-password': RecoverPasswordState, '/recover-password': RecoverPasswordState,
'/recover-password/key123': RecoverPasswordState, '/recover-password/key123': RecoverPasswordState,
'/forgot-password': ForgotPasswordState, '/forgot-password': ForgotPasswordState,

View File

@ -4,7 +4,13 @@ import CompleteState from './CompleteState';
export default class ChooseAccountState extends AbstractState { export default class ChooseAccountState extends AbstractState {
enter(context) { enter(context) {
context.navigate('/oauth/choose-account'); const { auth } = context.getState();
if (auth.oauth) {
context.navigate('/oauth/choose-account');
} else {
context.navigate('/choose-account');
}
} }
resolve(context, payload) { resolve(context, payload) {
@ -12,6 +18,7 @@ export default class ChooseAccountState extends AbstractState {
context.setState(new CompleteState()); context.setState(new CompleteState());
} else { } else {
context.navigate('/login'); context.navigate('/login');
context.run('setLogin', null);
context.setState(new LoginState()); context.setState(new LoginState());
} }
} }

View File

@ -23,10 +23,28 @@ describe('ChooseAccountState', () => {
describe('#enter', () => { describe('#enter', () => {
it('should navigate to /oauth/choose-account', () => { it('should navigate to /oauth/choose-account', () => {
context.getState.returns({
auth: {
oauth: {},
},
});
expectNavigate(mock, '/oauth/choose-account'); expectNavigate(mock, '/oauth/choose-account');
state.enter(context); state.enter(context);
}); });
it('should navigate to /choose-account if not oauth', () => {
context.getState.returns({
auth: {
oauth: null,
},
});
expectNavigate(mock, '/choose-account');
state.enter(context);
});
}); });
describe('#resolve', () => { describe('#resolve', () => {
@ -38,6 +56,7 @@ describe('ChooseAccountState', () => {
it('should transition to login if user wants to add new account', () => { it('should transition to login if user wants to add new account', () => {
expectNavigate(mock, '/login'); expectNavigate(mock, '/login');
expectRun(mock, 'setLogin', null);
expectState(mock, LoginState); expectState(mock, LoginState);
state.resolve(context, {}); state.resolve(context, {});

View File

@ -3,6 +3,7 @@ import logger from 'services/logger';
import { getCredentials } from 'components/auth/reducer'; import { getCredentials } from 'components/auth/reducer';
import AbstractState from './AbstractState'; import AbstractState from './AbstractState';
import ChooseAccountState from './ChooseAccountState';
import CompleteState from './CompleteState'; import CompleteState from './CompleteState';
import ForgotPasswordState from './ForgotPasswordState'; import ForgotPasswordState from './ForgotPasswordState';
import LoginState from './LoginState'; import LoginState from './LoginState';
@ -31,7 +32,7 @@ export default class PasswordState extends AbstractState {
rememberMe: bool rememberMe: bool
} }
) { ) {
const {login} = getCredentials(context.getState()); const { login, returnUrl } = getCredentials(context.getState());
return context.run('login', { return context.run('login', {
password, password,
@ -39,12 +40,17 @@ export default class PasswordState extends AbstractState {
login login
}) })
.then(() => { .then(() => {
const {isTotpRequired} = getCredentials(context.getState()); const { isTotpRequired } = getCredentials(context.getState());
if (isTotpRequired) { if (isTotpRequired) {
return context.setState(new MfaState()); return context.setState(new MfaState());
} }
if (returnUrl) {
context.navigate(returnUrl);
return;
}
return context.setState(new CompleteState()); return context.setState(new CompleteState());
}) })
.catch((err = {}) => .catch((err = {}) =>
@ -57,7 +63,13 @@ export default class PasswordState extends AbstractState {
} }
goBack(context: AuthContext) { goBack(context: AuthContext) {
context.run('setLogin', null); const { isRelogin } = getCredentials(context.getState());
context.setState(new LoginState());
if (isRelogin) {
context.setState(new ChooseAccountState());
} else {
context.run('setLogin', null);
context.setState(new LoginState());
}
} }
} }

View File

@ -3,8 +3,10 @@ import sinon from 'sinon';
import PasswordState from 'services/authFlow/PasswordState'; import PasswordState from 'services/authFlow/PasswordState';
import CompleteState from 'services/authFlow/CompleteState'; import CompleteState from 'services/authFlow/CompleteState';
import MfaState from 'services/authFlow/MfaState';
import LoginState from 'services/authFlow/LoginState'; import LoginState from 'services/authFlow/LoginState';
import ForgotPasswordState from 'services/authFlow/ForgotPasswordState'; import ForgotPasswordState from 'services/authFlow/ForgotPasswordState';
import ChooseAccountState from 'services/authFlow/ChooseAccountState';
import { bootstrap, expectState, expectNavigate, expectRun } from './helpers'; import { bootstrap, expectState, expectNavigate, expectRun } from './helpers';
@ -82,6 +84,67 @@ describe('PasswordState', () => {
return expect(state.resolve(context, payload), 'to be fulfilled'); return expect(state.resolve(context, payload), 'to be fulfilled');
}); });
it('should go to MfaState if totp required', () => {
const expectedLogin = 'foo';
const expectedPassword = 'bar';
const expectedRememberMe = true;
context.getState.returns({
auth: {
credentials: {
login: expectedLogin,
isTotpRequired: true,
}
}
});
expectRun(
mock,
'login',
sinon.match({
login: expectedLogin,
password: expectedPassword,
rememberMe: expectedRememberMe,
})
).returns(Promise.resolve());
expectState(mock, MfaState);
const payload = {password: expectedPassword, rememberMe: expectedRememberMe};
return expect(state.resolve(context, payload), 'to be fulfilled');
});
it('should navigate to returnUrl if any', () => {
const expectedLogin = 'foo';
const expectedPassword = 'bar';
const expectedReturnUrl = '/returnUrl';
const expectedRememberMe = true;
context.getState.returns({
auth: {
credentials: {
login: expectedLogin,
returnUrl: expectedReturnUrl,
}
}
});
expectRun(
mock,
'login',
sinon.match({
login: expectedLogin,
password: expectedPassword,
rememberMe: expectedRememberMe,
})
).returns(Promise.resolve());
expectNavigate(mock, expectedReturnUrl);
const payload = {password: expectedPassword, rememberMe: expectedRememberMe};
return expect(state.resolve(context, payload), 'to be fulfilled');
});
}); });
describe('#reject', () => { describe('#reject', () => {
@ -94,10 +157,33 @@ describe('PasswordState', () => {
describe('#goBack', () => { describe('#goBack', () => {
it('should transition to login state', () => { it('should transition to login state', () => {
context.getState.returns({
auth: {
credentials: {
login: 'foo'
}
}
});
expectRun(mock, 'setLogin', null); expectRun(mock, 'setLogin', null);
expectState(mock, LoginState); expectState(mock, LoginState);
state.goBack(context); state.goBack(context);
}); });
it('should transition to ChooseAccountState if this is relogin', () => {
context.getState.returns({
auth: {
credentials: {
login: 'foo',
isRelogin: true,
}
}
});
expectState(mock, ChooseAccountState);
state.goBack(context);
});
}); });
}); });