2017-12-31 00:34:31 +05:30
|
|
|
// @flow
|
2019-01-28 00:42:58 +05:30
|
|
|
import type { Account, State as AccountsState } from './reducer';
|
2019-05-20 19:02:07 +05:30
|
|
|
import { getJwtPayloads } from 'functions';
|
2017-04-12 00:55:27 +05:30
|
|
|
import { sessionStorage } from 'services/localStorage';
|
2019-11-27 14:33:32 +05:30
|
|
|
import {
|
|
|
|
validateToken,
|
|
|
|
requestToken,
|
|
|
|
logout,
|
|
|
|
} from 'services/api/authentication';
|
2018-02-18 01:29:35 +05:30
|
|
|
import { relogin as navigateToLogin } from 'components/auth/actions';
|
2017-01-04 11:22:46 +05:30
|
|
|
import { updateUser, setGuest } from 'components/user/actions';
|
2016-11-05 15:41:41 +05:30
|
|
|
import { setLocale } from 'components/i18n/actions';
|
2017-01-31 11:35:36 +05:30
|
|
|
import { setAccountSwitcher } from 'components/auth/actions';
|
2017-12-31 00:34:31 +05:30
|
|
|
import { getActiveAccount } from 'components/accounts/reducer';
|
2016-12-07 02:36:45 +05:30
|
|
|
import logger from 'services/logger';
|
2016-10-30 17:42:49 +05:30
|
|
|
|
2017-01-27 11:59:20 +05:30
|
|
|
import {
|
2019-11-27 14:33:32 +05:30
|
|
|
add,
|
|
|
|
remove,
|
|
|
|
activate,
|
|
|
|
reset,
|
|
|
|
updateToken,
|
2017-12-31 00:34:31 +05:30
|
|
|
} from './actions/pure-actions';
|
2017-01-27 11:59:20 +05:30
|
|
|
|
2017-12-31 00:34:31 +05:30
|
|
|
type Dispatch = (action: Object) => Promise<*>;
|
2017-01-27 11:59:20 +05:30
|
|
|
|
2017-12-31 00:34:31 +05:30
|
|
|
type State = {
|
2019-11-27 14:33:32 +05:30
|
|
|
accounts: AccountsState,
|
|
|
|
auth: {
|
|
|
|
oauth?: {
|
|
|
|
clientId?: string,
|
2017-12-31 00:34:31 +05:30
|
|
|
},
|
2019-11-27 14:33:32 +05:30
|
|
|
},
|
2017-12-31 00:34:31 +05:30
|
|
|
};
|
|
|
|
|
2018-03-14 02:21:37 +05:30
|
|
|
export { updateToken, activate, remove };
|
2016-10-30 17:42:49 +05:30
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {Account|object} account
|
|
|
|
* @param {string} account.token
|
|
|
|
* @param {string} account.refreshToken
|
2016-11-08 12:00:53 +05:30
|
|
|
*
|
2019-11-27 14:33:32 +05:30
|
|
|
* @returns {Function}
|
2016-10-30 17:42:49 +05:30
|
|
|
*/
|
2019-11-27 14:33:32 +05:30
|
|
|
export function authenticate(
|
|
|
|
account:
|
|
|
|
| Account
|
|
|
|
| {
|
|
|
|
token: string,
|
|
|
|
refreshToken: ?string,
|
|
|
|
},
|
|
|
|
) {
|
|
|
|
const { token, refreshToken } = account;
|
|
|
|
const email = account.email || null;
|
|
|
|
|
|
|
|
return async (
|
|
|
|
dispatch: Dispatch,
|
|
|
|
getState: () => State,
|
|
|
|
): Promise<Account> => {
|
|
|
|
let accountId: number;
|
|
|
|
|
|
|
|
if (typeof account.id === 'number') {
|
|
|
|
accountId = account.id;
|
|
|
|
} else {
|
|
|
|
accountId = findAccountIdFromToken(token);
|
|
|
|
}
|
2018-02-28 02:47:31 +05:30
|
|
|
|
2019-11-27 14:33:32 +05:30
|
|
|
const knownAccount = getState().accounts.available.find(
|
|
|
|
item => item.id === accountId,
|
|
|
|
);
|
2018-02-28 02:47:31 +05:30
|
|
|
|
2019-11-27 14:33:32 +05:30
|
|
|
if (knownAccount) {
|
|
|
|
// this account is already available
|
|
|
|
// activate it before validation
|
|
|
|
dispatch(activate(knownAccount));
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
const {
|
|
|
|
token: newToken,
|
|
|
|
refreshToken: newRefreshToken,
|
|
|
|
user,
|
|
|
|
// $FlowFixMe have no idea why it's causes error about missing properties
|
|
|
|
} = await validateToken(accountId, token, refreshToken);
|
|
|
|
const { auth } = getState();
|
|
|
|
const account: Account = {
|
|
|
|
id: user.id,
|
|
|
|
username: user.username,
|
|
|
|
email: user.email,
|
|
|
|
token: newToken,
|
|
|
|
refreshToken: newRefreshToken,
|
|
|
|
};
|
|
|
|
dispatch(add(account));
|
|
|
|
dispatch(activate(account));
|
|
|
|
dispatch(
|
|
|
|
updateUser({
|
|
|
|
isGuest: false,
|
|
|
|
...user,
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
|
|
|
|
// TODO: probably should be moved from here, because it is a side effect
|
|
|
|
logger.setUser(user);
|
|
|
|
|
|
|
|
if (!newRefreshToken) {
|
|
|
|
// mark user as stranger (user does not want us to remember his account)
|
|
|
|
sessionStorage.setItem(`stranger${account.id}`, 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (auth && auth.oauth && auth.oauth.clientId) {
|
|
|
|
// if we authenticating during oauth, we disable account chooser
|
|
|
|
// because user probably has made his choise now
|
|
|
|
// this may happen, when user registers, logs in or uses account
|
|
|
|
// chooser panel during oauth
|
|
|
|
dispatch(setAccountSwitcher(false));
|
|
|
|
}
|
|
|
|
|
|
|
|
await dispatch(setLocale(user.lang));
|
|
|
|
|
|
|
|
return account;
|
|
|
|
} catch (resp) {
|
|
|
|
// all the logic to get the valid token was failed,
|
|
|
|
// looks like we have some problems with token
|
|
|
|
// lets redirect to login page
|
|
|
|
if (typeof email === 'string') {
|
|
|
|
// TODO: we should somehow try to find email by token
|
|
|
|
dispatch(relogin(email));
|
|
|
|
}
|
|
|
|
|
|
|
|
throw resp;
|
|
|
|
}
|
|
|
|
};
|
2019-01-28 00:42:58 +05:30
|
|
|
}
|
2016-12-06 00:44:38 +05:30
|
|
|
|
2019-01-28 00:42:58 +05:30
|
|
|
function findAccountIdFromToken(token: string): number {
|
2019-11-27 14:33:32 +05:30
|
|
|
const { sub, jti } = getJwtPayloads(token);
|
2017-01-31 11:35:36 +05:30
|
|
|
|
2019-11-27 14:33:32 +05:30
|
|
|
// sub has the format "ely|{accountId}", so we must trim "ely|" part
|
|
|
|
if (sub) {
|
|
|
|
return parseInt(sub.substr(4), 10);
|
|
|
|
}
|
|
|
|
|
|
|
|
// In older backend versions identity was stored in jti claim. Some users still have such tokens
|
|
|
|
if (jti) {
|
|
|
|
return jti;
|
|
|
|
}
|
2019-01-28 00:42:58 +05:30
|
|
|
|
2019-11-27 14:33:32 +05:30
|
|
|
throw new Error('payloads is not contains any identity claim');
|
2016-10-30 17:42:49 +05:30
|
|
|
}
|
|
|
|
|
2018-02-13 02:24:31 +05:30
|
|
|
/**
|
|
|
|
* Checks the current user's token exp time. Supposed to be used before performing
|
|
|
|
* any api request
|
|
|
|
*
|
|
|
|
* @see components/user/middlewares/refreshTokenMiddleware
|
|
|
|
*
|
2019-11-27 14:33:32 +05:30
|
|
|
* @returns {Function}
|
2018-02-13 02:24:31 +05:30
|
|
|
*/
|
2017-12-31 00:34:31 +05:30
|
|
|
export function ensureToken() {
|
2019-11-27 14:33:32 +05:30
|
|
|
return (dispatch: Dispatch, getState: () => State): Promise<void> => {
|
|
|
|
const { token } = getActiveAccount(getState()) || {};
|
2017-12-31 00:34:31 +05:30
|
|
|
|
2019-11-27 14:33:32 +05:30
|
|
|
try {
|
|
|
|
const SAFETY_FACTOR = 300; // ask new token earlier to overcome time desynchronization problem
|
|
|
|
const { exp } = getJwtPayloads(token);
|
2017-12-31 00:34:31 +05:30
|
|
|
|
2019-11-27 14:33:32 +05:30
|
|
|
if (exp - SAFETY_FACTOR < Date.now() / 1000) {
|
|
|
|
return dispatch(requestNewToken());
|
|
|
|
}
|
|
|
|
} catch (err) {
|
|
|
|
logger.warn('Refresh token error: bad token', {
|
|
|
|
token,
|
|
|
|
});
|
2017-12-31 00:34:31 +05:30
|
|
|
|
2019-11-27 14:33:32 +05:30
|
|
|
dispatch(relogin());
|
2017-12-31 00:34:31 +05:30
|
|
|
|
2019-11-27 14:33:32 +05:30
|
|
|
return Promise.reject(new Error('Invalid token'));
|
|
|
|
}
|
2017-12-31 00:34:31 +05:30
|
|
|
|
2019-11-27 14:33:32 +05:30
|
|
|
return Promise.resolve();
|
|
|
|
};
|
2017-12-31 00:34:31 +05:30
|
|
|
}
|
|
|
|
|
2018-02-13 02:24:31 +05:30
|
|
|
/**
|
|
|
|
* Checks whether request `error` is an auth error and tries recover from it by
|
|
|
|
* requesting a new auth token
|
|
|
|
*
|
|
|
|
* @see components/user/middlewares/refreshTokenMiddleware
|
|
|
|
*
|
|
|
|
* @param {object} error
|
|
|
|
*
|
2019-11-27 14:33:32 +05:30
|
|
|
* @returns {Function}
|
2018-02-13 02:24:31 +05:30
|
|
|
*/
|
2019-11-27 14:33:32 +05:30
|
|
|
export function recoverFromTokenError(
|
|
|
|
error: ?{
|
2017-12-31 00:34:31 +05:30
|
|
|
status: number,
|
|
|
|
message: string,
|
2019-11-27 14:33:32 +05:30
|
|
|
},
|
|
|
|
) {
|
|
|
|
return (dispatch: Dispatch, getState: () => State): Promise<void> => {
|
|
|
|
if (error && error.status === 401) {
|
|
|
|
const activeAccount = getActiveAccount(getState());
|
|
|
|
|
|
|
|
if (activeAccount && activeAccount.refreshToken) {
|
|
|
|
if (
|
|
|
|
[
|
|
|
|
'Token expired',
|
|
|
|
'Incorrect token',
|
|
|
|
'You are requesting with an invalid credential.',
|
|
|
|
].includes(error.message)
|
|
|
|
) {
|
|
|
|
// request token and retry
|
|
|
|
return dispatch(requestNewToken());
|
2017-12-31 00:34:31 +05:30
|
|
|
}
|
|
|
|
|
2019-11-27 14:33:32 +05:30
|
|
|
logger.error('Unknown unauthorized response', {
|
|
|
|
error,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// user's access token is outdated and we have no refreshToken
|
|
|
|
// or something unexpected happend
|
|
|
|
// in both cases we resetting all the user's state
|
|
|
|
dispatch(relogin());
|
|
|
|
}
|
|
|
|
|
|
|
|
return Promise.reject(error);
|
|
|
|
};
|
2017-12-31 00:34:31 +05:30
|
|
|
}
|
|
|
|
|
2018-02-13 02:24:31 +05:30
|
|
|
/**
|
|
|
|
* Requests new token and updates state. In case, when token can not be updated,
|
|
|
|
* it will redirect user to login page
|
|
|
|
*
|
2019-11-27 14:33:32 +05:30
|
|
|
* @returns {Function}
|
2018-02-13 02:24:31 +05:30
|
|
|
*/
|
2017-12-31 00:34:31 +05:30
|
|
|
export function requestNewToken() {
|
2019-11-27 14:33:32 +05:30
|
|
|
return (dispatch: Dispatch, getState: () => State): Promise<void> => {
|
|
|
|
const { refreshToken } = getActiveAccount(getState()) || {};
|
2017-12-31 00:34:31 +05:30
|
|
|
|
2019-11-27 14:33:32 +05:30
|
|
|
if (!refreshToken) {
|
|
|
|
dispatch(relogin());
|
2017-12-31 00:34:31 +05:30
|
|
|
|
2019-11-27 14:33:32 +05:30
|
|
|
return Promise.resolve();
|
|
|
|
}
|
2017-12-31 00:34:31 +05:30
|
|
|
|
2019-11-27 14:33:32 +05:30
|
|
|
return requestToken(refreshToken)
|
|
|
|
.then(token => {
|
|
|
|
dispatch(updateToken(token));
|
|
|
|
})
|
|
|
|
.catch(resp => {
|
|
|
|
// all the logic to get the valid token was failed,
|
|
|
|
// looks like we have some problems with token
|
|
|
|
// lets redirect to login page
|
|
|
|
dispatch(relogin());
|
|
|
|
|
|
|
|
return Promise.reject(resp);
|
|
|
|
});
|
|
|
|
};
|
2017-12-31 00:34:31 +05:30
|
|
|
}
|
|
|
|
|
2016-10-30 17:42:49 +05:30
|
|
|
/**
|
2017-01-27 11:59:20 +05:30
|
|
|
* Remove one account from current user's account list
|
|
|
|
*
|
2016-10-30 17:42:49 +05:30
|
|
|
* @param {Account} account
|
2016-11-08 12:00:53 +05:30
|
|
|
*
|
2019-11-27 14:33:32 +05:30
|
|
|
* @returns {Function}
|
2016-10-30 17:42:49 +05:30
|
|
|
*/
|
2017-12-31 00:34:31 +05:30
|
|
|
export function revoke(account: Account) {
|
2019-11-27 14:33:32 +05:30
|
|
|
return (dispatch: Dispatch, getState: () => State): Promise<void> => {
|
|
|
|
const accountToReplace: ?Account = getState().accounts.available.find(
|
|
|
|
({ id }) => id !== account.id,
|
|
|
|
);
|
|
|
|
|
|
|
|
if (accountToReplace) {
|
|
|
|
return dispatch(authenticate(accountToReplace))
|
|
|
|
.finally(() => {
|
|
|
|
// we need to logout user, even in case, when we can
|
|
|
|
// not authenticate him with new account
|
|
|
|
logout(account.token).catch(() => {
|
|
|
|
// we don't care
|
|
|
|
});
|
|
|
|
dispatch(remove(account));
|
|
|
|
})
|
|
|
|
.catch(() => {
|
|
|
|
// we don't care
|
|
|
|
});
|
|
|
|
}
|
2016-11-08 12:00:53 +05:30
|
|
|
|
2019-11-27 14:33:32 +05:30
|
|
|
return dispatch(logoutAll());
|
|
|
|
};
|
2016-10-30 17:42:49 +05:30
|
|
|
}
|
|
|
|
|
2017-12-31 00:34:31 +05:30
|
|
|
export function relogin(email?: string) {
|
2019-11-27 14:33:32 +05:30
|
|
|
return (dispatch: Dispatch, getState: () => State) => {
|
|
|
|
const activeAccount = getActiveAccount(getState());
|
2017-12-31 00:34:31 +05:30
|
|
|
|
2019-11-27 14:33:32 +05:30
|
|
|
if (!email && activeAccount) {
|
|
|
|
email = activeAccount.email;
|
|
|
|
}
|
2017-12-31 00:34:31 +05:30
|
|
|
|
2019-11-27 14:33:32 +05:30
|
|
|
dispatch(navigateToLogin(email || null));
|
|
|
|
};
|
2017-12-31 00:34:31 +05:30
|
|
|
}
|
|
|
|
|
2016-12-06 00:44:38 +05:30
|
|
|
export function logoutAll() {
|
2019-11-27 14:33:32 +05:30
|
|
|
return (dispatch: Dispatch, getState: () => State): Promise<void> => {
|
|
|
|
dispatch(setGuest());
|
2017-01-04 11:22:46 +05:30
|
|
|
|
2019-11-27 14:33:32 +05:30
|
|
|
const {
|
|
|
|
accounts: { available },
|
|
|
|
} = getState();
|
2016-12-06 00:44:38 +05:30
|
|
|
|
2019-11-27 14:33:32 +05:30
|
|
|
available.forEach(account =>
|
|
|
|
logout(account.token).catch(() => {
|
|
|
|
// we don't care
|
|
|
|
}),
|
|
|
|
);
|
2016-12-06 00:44:38 +05:30
|
|
|
|
2019-11-27 14:33:32 +05:30
|
|
|
dispatch(reset());
|
|
|
|
dispatch(relogin());
|
2017-01-04 11:22:46 +05:30
|
|
|
|
2019-11-27 14:33:32 +05:30
|
|
|
return Promise.resolve();
|
|
|
|
};
|
2016-12-06 00:44:38 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Logouts accounts, that was marked as "do not remember me"
|
|
|
|
*
|
|
|
|
* We detecting foreign accounts by the absence of refreshToken. The account
|
|
|
|
* won't be removed, until key `stranger${account.id}` is present in sessionStorage
|
|
|
|
*
|
2019-11-27 14:33:32 +05:30
|
|
|
* @returns {Function}
|
2016-12-06 00:44:38 +05:30
|
|
|
*/
|
|
|
|
export function logoutStrangers() {
|
2019-11-27 14:33:32 +05:30
|
|
|
return (dispatch: Dispatch, getState: () => State): Promise<void> => {
|
|
|
|
const {
|
|
|
|
accounts: { available },
|
|
|
|
} = getState();
|
|
|
|
const activeAccount = getActiveAccount(getState());
|
|
|
|
|
|
|
|
const isStranger = ({ refreshToken, id }: Account) =>
|
|
|
|
!refreshToken && !sessionStorage.getItem(`stranger${id}`);
|
|
|
|
|
|
|
|
if (available.some(isStranger)) {
|
|
|
|
const accountToReplace = available.filter(
|
|
|
|
account => !isStranger(account),
|
|
|
|
)[0];
|
|
|
|
|
|
|
|
if (accountToReplace) {
|
|
|
|
available.filter(isStranger).forEach(account => {
|
|
|
|
dispatch(remove(account));
|
|
|
|
logout(account.token);
|
|
|
|
});
|
|
|
|
|
|
|
|
if (activeAccount && isStranger(activeAccount)) {
|
|
|
|
return dispatch(authenticate(accountToReplace));
|
2016-12-06 00:44:38 +05:30
|
|
|
}
|
2019-11-27 14:33:32 +05:30
|
|
|
} else {
|
|
|
|
return dispatch(logoutAll());
|
|
|
|
}
|
|
|
|
}
|
2016-12-06 00:44:38 +05:30
|
|
|
|
2019-11-27 14:33:32 +05:30
|
|
|
return Promise.resolve();
|
|
|
|
};
|
2016-12-06 00:44:38 +05:30
|
|
|
}
|