2021-08-22 15:52:02 +00:00
|
|
|
/**
|
|
|
|
* Auth: login & registration workflow.
|
|
|
|
* This file contains abstractions over auth concepts.
|
|
|
|
* @module soapbox/actions/auth
|
|
|
|
* @see module:soapbox/actions/apps
|
|
|
|
* @see module:soapbox/actions/oauth
|
|
|
|
* @see module:soapbox/actions/security
|
|
|
|
*/
|
|
|
|
|
2021-06-26 22:04:27 +00:00
|
|
|
import { defineMessages } from 'react-intl';
|
2022-01-10 22:25:06 +00:00
|
|
|
|
2021-03-26 20:29:15 +00:00
|
|
|
import { createAccount } from 'soapbox/actions/accounts';
|
2021-08-22 00:05:59 +00:00
|
|
|
import { createApp } from 'soapbox/actions/apps';
|
2022-01-10 22:17:52 +00:00
|
|
|
import { fetchMeSuccess, fetchMeFail } from 'soapbox/actions/me';
|
2021-08-22 00:37:28 +00:00
|
|
|
import { obtainOAuthToken, revokeOAuthToken } from 'soapbox/actions/oauth';
|
2022-05-02 21:24:19 +00:00
|
|
|
import { startOnboarding } from 'soapbox/actions/onboarding';
|
2022-04-12 20:23:18 +00:00
|
|
|
import { custom } from 'soapbox/custom';
|
2022-11-17 18:13:09 +00:00
|
|
|
import { queryClient } from 'soapbox/queries/client';
|
2022-11-15 20:42:22 +00:00
|
|
|
import KVStore from 'soapbox/storage/kv-store';
|
2022-12-20 15:47:46 +00:00
|
|
|
import toast from 'soapbox/toast';
|
2022-01-10 22:17:52 +00:00
|
|
|
import { getLoggedInAccount, parseBaseURL } from 'soapbox/utils/auth';
|
2021-08-22 03:46:33 +00:00
|
|
|
import sourceCode from 'soapbox/utils/code';
|
2021-08-23 00:13:09 +00:00
|
|
|
import { getFeatures } from 'soapbox/utils/features';
|
2022-09-16 15:42:05 +00:00
|
|
|
import { normalizeUsername } from 'soapbox/utils/input';
|
2021-08-30 23:41:05 +00:00
|
|
|
import { isStandalone } from 'soapbox/utils/state';
|
2022-01-10 22:25:06 +00:00
|
|
|
|
2022-01-10 22:01:24 +00:00
|
|
|
import api, { baseClient } from '../api';
|
2022-01-10 22:25:06 +00:00
|
|
|
|
2022-01-10 22:01:24 +00:00
|
|
|
import { importFetchedAccount } from './importer';
|
2020-04-04 20:28:57 +00:00
|
|
|
|
2022-06-10 17:56:22 +00:00
|
|
|
import type { AxiosError } from 'axios';
|
|
|
|
import type { AppDispatch, RootState } from 'soapbox/store';
|
|
|
|
|
2021-03-24 00:06:55 +00:00
|
|
|
export const SWITCH_ACCOUNT = 'SWITCH_ACCOUNT';
|
|
|
|
|
2020-04-05 23:39:22 +00:00
|
|
|
export const AUTH_APP_CREATED = 'AUTH_APP_CREATED';
|
|
|
|
export const AUTH_APP_AUTHORIZED = 'AUTH_APP_AUTHORIZED';
|
|
|
|
export const AUTH_LOGGED_IN = 'AUTH_LOGGED_IN';
|
2020-04-11 19:41:13 +00:00
|
|
|
export const AUTH_LOGGED_OUT = 'AUTH_LOGGED_OUT';
|
2020-04-05 21:54:51 +00:00
|
|
|
|
2021-03-24 00:06:55 +00:00
|
|
|
export const VERIFY_CREDENTIALS_REQUEST = 'VERIFY_CREDENTIALS_REQUEST';
|
|
|
|
export const VERIFY_CREDENTIALS_SUCCESS = 'VERIFY_CREDENTIALS_SUCCESS';
|
|
|
|
export const VERIFY_CREDENTIALS_FAIL = 'VERIFY_CREDENTIALS_FAIL';
|
|
|
|
|
2021-10-20 21:27:36 +00:00
|
|
|
export const AUTH_ACCOUNT_REMEMBER_REQUEST = 'AUTH_ACCOUNT_REMEMBER_REQUEST';
|
|
|
|
export const AUTH_ACCOUNT_REMEMBER_SUCCESS = 'AUTH_ACCOUNT_REMEMBER_SUCCESS';
|
|
|
|
export const AUTH_ACCOUNT_REMEMBER_FAIL = 'AUTH_ACCOUNT_REMEMBER_FAIL';
|
|
|
|
|
2022-04-12 20:23:18 +00:00
|
|
|
const customApp = custom('app');
|
|
|
|
|
2021-08-22 15:52:02 +00:00
|
|
|
export const messages = defineMessages({
|
2021-06-26 22:04:27 +00:00
|
|
|
loggedOut: { id: 'auth.logged_out', defaultMessage: 'Logged out.' },
|
|
|
|
invalidCredentials: { id: 'auth.invalid_credentials', defaultMessage: 'Wrong username or password' },
|
|
|
|
});
|
|
|
|
|
2022-06-10 17:56:22 +00:00
|
|
|
const noOp = () => new Promise(f => f(undefined));
|
2020-04-29 19:07:22 +00:00
|
|
|
|
2022-06-10 17:56:22 +00:00
|
|
|
const getScopes = (state: RootState) => {
|
|
|
|
const instance = state.instance;
|
2021-08-23 00:13:09 +00:00
|
|
|
const { scopes } = getFeatures(instance);
|
|
|
|
return scopes;
|
|
|
|
};
|
|
|
|
|
2022-06-10 17:56:22 +00:00
|
|
|
const createAppAndToken = () =>
|
|
|
|
(dispatch: AppDispatch) =>
|
|
|
|
dispatch(getAuthApp()).then(() =>
|
|
|
|
dispatch(createAppToken()),
|
|
|
|
);
|
2020-04-29 19:07:22 +00:00
|
|
|
|
2022-04-12 20:23:18 +00:00
|
|
|
/** Create an auth app, or use it from build config */
|
2022-06-10 17:56:22 +00:00
|
|
|
const getAuthApp = () =>
|
|
|
|
(dispatch: AppDispatch) => {
|
2022-04-12 20:23:18 +00:00
|
|
|
if (customApp?.client_secret) {
|
|
|
|
return noOp().then(() => dispatch({ type: AUTH_APP_CREATED, app: customApp }));
|
|
|
|
} else {
|
|
|
|
return dispatch(createAuthApp());
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2022-06-10 17:56:22 +00:00
|
|
|
const createAuthApp = () =>
|
2022-06-18 09:51:02 +00:00
|
|
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
2021-08-22 00:05:59 +00:00
|
|
|
const params = {
|
2021-08-22 03:46:33 +00:00
|
|
|
client_name: sourceCode.displayName,
|
2020-04-04 20:28:57 +00:00
|
|
|
redirect_uris: 'urn:ietf:wg:oauth:2.0:oob',
|
2021-08-23 00:13:09 +00:00
|
|
|
scopes: getScopes(getState()),
|
2021-08-22 19:34:58 +00:00
|
|
|
website: sourceCode.homepage,
|
2021-08-22 00:05:59 +00:00
|
|
|
};
|
|
|
|
|
2022-06-10 17:56:22 +00:00
|
|
|
return dispatch(createApp(params)).then((app: Record<string, string>) =>
|
|
|
|
dispatch({ type: AUTH_APP_CREATED, app }),
|
|
|
|
);
|
2020-04-29 19:07:22 +00:00
|
|
|
};
|
|
|
|
|
2022-06-10 17:56:22 +00:00
|
|
|
const createAppToken = () =>
|
2022-06-18 09:51:02 +00:00
|
|
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
2022-12-25 23:31:07 +00:00
|
|
|
const app = getState().auth.app;
|
2020-04-29 19:07:22 +00:00
|
|
|
|
2021-08-22 00:37:28 +00:00
|
|
|
const params = {
|
2022-12-26 13:09:09 +00:00
|
|
|
client_id: app.client_id!,
|
|
|
|
client_secret: app.client_secret!,
|
2020-04-29 21:53:10 +00:00
|
|
|
redirect_uri: 'urn:ietf:wg:oauth:2.0:oob',
|
|
|
|
grant_type: 'client_credentials',
|
2021-08-23 00:13:09 +00:00
|
|
|
scope: getScopes(getState()),
|
2021-08-22 00:37:28 +00:00
|
|
|
};
|
|
|
|
|
2022-06-10 17:56:22 +00:00
|
|
|
return dispatch(obtainOAuthToken(params)).then((token: Record<string, string | number>) =>
|
|
|
|
dispatch({ type: AUTH_APP_AUTHORIZED, app, token }),
|
|
|
|
);
|
2020-04-14 18:44:40 +00:00
|
|
|
};
|
2020-04-04 20:28:57 +00:00
|
|
|
|
2022-06-10 17:56:22 +00:00
|
|
|
const createUserToken = (username: string, password: string) =>
|
2022-06-18 09:51:02 +00:00
|
|
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
2022-12-25 23:31:07 +00:00
|
|
|
const app = getState().auth.app;
|
2021-08-22 00:37:28 +00:00
|
|
|
|
|
|
|
const params = {
|
2022-12-26 13:09:09 +00:00
|
|
|
client_id: app.client_id!,
|
|
|
|
client_secret: app.client_secret!,
|
2020-04-29 21:53:10 +00:00
|
|
|
redirect_uri: 'urn:ietf:wg:oauth:2.0:oob',
|
|
|
|
grant_type: 'password',
|
|
|
|
username: username,
|
|
|
|
password: password,
|
2021-08-23 00:13:09 +00:00
|
|
|
scope: getScopes(getState()),
|
2021-08-22 00:37:28 +00:00
|
|
|
};
|
|
|
|
|
2021-08-22 19:34:58 +00:00
|
|
|
return dispatch(obtainOAuthToken(params))
|
2022-06-10 17:56:22 +00:00
|
|
|
.then((token: Record<string, string | number>) => dispatch(authLoggedIn(token)));
|
2020-04-30 00:38:24 +00:00
|
|
|
};
|
|
|
|
|
2022-06-10 17:56:22 +00:00
|
|
|
export const otpVerify = (code: string, mfa_token: string) =>
|
2022-06-18 09:51:02 +00:00
|
|
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
2022-12-25 23:31:07 +00:00
|
|
|
const app = getState().auth.app;
|
2020-08-07 20:17:13 +00:00
|
|
|
return api(getState, 'app').post('/oauth/mfa/challenge', {
|
2022-12-25 23:31:07 +00:00
|
|
|
client_id: app.client_id,
|
|
|
|
client_secret: app.client_secret,
|
2020-08-07 20:17:13 +00:00
|
|
|
mfa_token: mfa_token,
|
|
|
|
code: code,
|
|
|
|
challenge_type: 'totp',
|
|
|
|
redirect_uri: 'urn:ietf:wg:oauth:2.0:oob',
|
2022-01-12 18:40:40 +00:00
|
|
|
scope: getScopes(getState()),
|
2021-08-22 19:34:58 +00:00
|
|
|
}).then(({ data: token }) => dispatch(authLoggedIn(token)));
|
2020-08-07 20:17:13 +00:00
|
|
|
};
|
|
|
|
|
2022-06-10 17:56:22 +00:00
|
|
|
export const verifyCredentials = (token: string, accountUrl?: string) => {
|
2021-08-22 01:41:29 +00:00
|
|
|
const baseURL = parseBaseURL(accountUrl);
|
|
|
|
|
2022-06-18 09:51:02 +00:00
|
|
|
return (dispatch: AppDispatch, getState: () => RootState) => {
|
2021-09-24 23:47:22 +00:00
|
|
|
dispatch({ type: VERIFY_CREDENTIALS_REQUEST, token });
|
2021-03-24 00:06:55 +00:00
|
|
|
|
2021-08-22 01:41:29 +00:00
|
|
|
return baseClient(token, baseURL).get('/api/v1/accounts/verify_credentials').then(({ data: account }) => {
|
2021-03-24 02:01:50 +00:00
|
|
|
dispatch(importFetchedAccount(account));
|
2021-03-24 00:06:55 +00:00
|
|
|
dispatch({ type: VERIFY_CREDENTIALS_SUCCESS, token, account });
|
2022-06-10 17:56:22 +00:00
|
|
|
if (account.id === getState().me) dispatch(fetchMeSuccess(account));
|
2021-03-24 00:06:55 +00:00
|
|
|
return account;
|
|
|
|
}).catch(error => {
|
2022-03-21 18:09:01 +00:00
|
|
|
if (error?.response?.status === 403 && error?.response?.data?.id) {
|
|
|
|
// The user is waitlisted
|
|
|
|
const account = error.response.data;
|
|
|
|
dispatch(importFetchedAccount(account));
|
|
|
|
dispatch({ type: VERIFY_CREDENTIALS_SUCCESS, token, account });
|
2022-06-10 17:56:22 +00:00
|
|
|
if (account.id === getState().me) dispatch(fetchMeSuccess(account));
|
2022-03-21 18:09:01 +00:00
|
|
|
return account;
|
|
|
|
} else {
|
2022-06-10 17:56:22 +00:00
|
|
|
if (getState().me === null) dispatch(fetchMeFail(error));
|
2022-07-08 19:48:06 +00:00
|
|
|
dispatch({ type: VERIFY_CREDENTIALS_FAIL, token, error });
|
2022-07-08 19:38:36 +00:00
|
|
|
throw error;
|
2022-03-21 18:09:01 +00:00
|
|
|
}
|
2021-03-24 00:06:55 +00:00
|
|
|
});
|
|
|
|
};
|
2022-06-10 17:56:22 +00:00
|
|
|
};
|
2021-03-24 00:06:55 +00:00
|
|
|
|
2022-06-10 17:56:22 +00:00
|
|
|
export const rememberAuthAccount = (accountUrl: string) =>
|
2022-06-18 09:51:02 +00:00
|
|
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
2021-10-20 21:27:36 +00:00
|
|
|
dispatch({ type: AUTH_ACCOUNT_REMEMBER_REQUEST, accountUrl });
|
|
|
|
return KVStore.getItemOrError(`authAccount:${accountUrl}`).then(account => {
|
|
|
|
dispatch(importFetchedAccount(account));
|
|
|
|
dispatch({ type: AUTH_ACCOUNT_REMEMBER_SUCCESS, account, accountUrl });
|
2022-06-10 17:56:22 +00:00
|
|
|
if (account.id === getState().me) dispatch(fetchMeSuccess(account));
|
2021-10-20 21:27:36 +00:00
|
|
|
return account;
|
|
|
|
}).catch(error => {
|
|
|
|
dispatch({ type: AUTH_ACCOUNT_REMEMBER_FAIL, error, accountUrl, skipAlert: true });
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2022-06-10 17:56:22 +00:00
|
|
|
export const loadCredentials = (token: string, accountUrl: string) =>
|
|
|
|
(dispatch: AppDispatch) => dispatch(rememberAuthAccount(accountUrl))
|
2022-08-02 13:20:07 +00:00
|
|
|
.then(() => dispatch(verifyCredentials(token, accountUrl)))
|
2022-06-10 17:56:22 +00:00
|
|
|
.catch(() => dispatch(verifyCredentials(token, accountUrl)));
|
|
|
|
|
|
|
|
export const logIn = (username: string, password: string) =>
|
|
|
|
(dispatch: AppDispatch) => dispatch(getAuthApp()).then(() => {
|
2022-07-19 02:50:02 +00:00
|
|
|
return dispatch(createUserToken(normalizeUsername(username), password));
|
2022-06-10 17:56:22 +00:00
|
|
|
}).catch((error: AxiosError) => {
|
2022-12-26 13:09:09 +00:00
|
|
|
if ((error.response?.data as any)?.error === 'mfa_required') {
|
2022-06-10 17:56:22 +00:00
|
|
|
// If MFA is required, throw the error and handle it in the component.
|
2020-04-11 19:41:13 +00:00
|
|
|
throw error;
|
2022-06-10 17:56:22 +00:00
|
|
|
} else {
|
|
|
|
// Return "wrong password" message.
|
2022-12-20 16:34:53 +00:00
|
|
|
toast.error(messages.invalidCredentials);
|
2022-06-10 17:56:22 +00:00
|
|
|
}
|
|
|
|
throw error;
|
|
|
|
});
|
2020-04-05 21:54:51 +00:00
|
|
|
|
2022-06-10 17:56:22 +00:00
|
|
|
export const deleteSession = () =>
|
2022-06-18 09:51:02 +00:00
|
|
|
(dispatch: AppDispatch, getState: () => RootState) => api(getState).delete('/api/sign_out');
|
2022-03-21 18:09:01 +00:00
|
|
|
|
2022-06-10 17:56:22 +00:00
|
|
|
export const logOut = () =>
|
|
|
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
2020-09-28 18:05:20 +00:00
|
|
|
const state = getState();
|
2021-08-21 21:54:53 +00:00
|
|
|
const account = getLoggedInAccount(state);
|
2021-08-30 23:41:05 +00:00
|
|
|
const standalone = isStandalone(state);
|
2020-09-28 18:05:20 +00:00
|
|
|
|
2022-04-19 19:37:48 +00:00
|
|
|
if (!account) return dispatch(noOp);
|
|
|
|
|
2021-08-22 00:37:28 +00:00
|
|
|
const params = {
|
2022-12-26 13:09:09 +00:00
|
|
|
client_id: state.auth.app.client_id!,
|
|
|
|
client_secret: state.auth.app.client_secret!,
|
2022-12-25 23:31:07 +00:00
|
|
|
token: state.auth.users.get(account.url)?.access_token!,
|
2021-08-22 00:37:28 +00:00
|
|
|
};
|
|
|
|
|
2022-11-17 18:13:09 +00:00
|
|
|
return dispatch(revokeOAuthToken(params))
|
|
|
|
.finally(() => {
|
|
|
|
// Clear all stored cache from React Query
|
|
|
|
queryClient.invalidateQueries();
|
|
|
|
queryClient.clear();
|
|
|
|
|
|
|
|
dispatch({ type: AUTH_LOGGED_OUT, account, standalone });
|
|
|
|
|
2022-12-20 15:47:46 +00:00
|
|
|
toast.success(messages.loggedOut);
|
2022-11-17 18:13:09 +00:00
|
|
|
});
|
2020-04-11 19:41:13 +00:00
|
|
|
};
|
|
|
|
|
2022-06-10 17:56:22 +00:00
|
|
|
export const switchAccount = (accountId: string, background = false) =>
|
2022-06-18 09:51:02 +00:00
|
|
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
2022-06-10 17:56:22 +00:00
|
|
|
const account = getState().accounts.get(accountId);
|
2022-11-18 15:25:24 +00:00
|
|
|
// Clear all stored cache from React Query
|
|
|
|
queryClient.invalidateQueries();
|
|
|
|
queryClient.clear();
|
|
|
|
|
2022-06-10 17:56:22 +00:00
|
|
|
return dispatch({ type: SWITCH_ACCOUNT, account, background });
|
2021-08-21 21:54:53 +00:00
|
|
|
};
|
2021-03-24 00:06:55 +00:00
|
|
|
|
2022-06-10 17:56:22 +00:00
|
|
|
export const fetchOwnAccounts = () =>
|
|
|
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
2021-03-24 02:01:50 +00:00
|
|
|
const state = getState();
|
2022-12-25 23:31:07 +00:00
|
|
|
return state.auth.users.forEach((user) => {
|
|
|
|
const account = state.accounts.get(user.id);
|
2021-03-24 02:01:50 +00:00
|
|
|
if (!account) {
|
2022-12-26 13:09:09 +00:00
|
|
|
dispatch(verifyCredentials(user.access_token!, user.url!));
|
2021-03-24 02:01:50 +00:00
|
|
|
}
|
|
|
|
});
|
2021-03-25 18:47:01 +00:00
|
|
|
};
|
2021-03-24 02:01:50 +00:00
|
|
|
|
2022-06-10 17:56:22 +00:00
|
|
|
export const register = (params: Record<string, any>) =>
|
|
|
|
(dispatch: AppDispatch) => {
|
2020-08-30 23:48:00 +00:00
|
|
|
params.fullname = params.username;
|
2021-03-26 20:29:15 +00:00
|
|
|
|
2021-08-22 19:34:58 +00:00
|
|
|
return dispatch(createAppAndToken())
|
|
|
|
.then(() => dispatch(createAccount(params)))
|
2022-06-10 17:56:22 +00:00
|
|
|
.then(({ token }: { token: Record<string, string | number> }) => {
|
2022-05-02 21:24:19 +00:00
|
|
|
dispatch(startOnboarding());
|
|
|
|
return dispatch(authLoggedIn(token));
|
|
|
|
});
|
2020-04-23 23:41:20 +00:00
|
|
|
};
|
|
|
|
|
2022-06-10 17:56:22 +00:00
|
|
|
export const fetchCaptcha = () =>
|
2022-06-18 09:51:02 +00:00
|
|
|
(_dispatch: AppDispatch, getState: () => RootState) => {
|
2020-04-24 01:48:25 +00:00
|
|
|
return api(getState).get('/api/pleroma/captcha');
|
|
|
|
};
|
|
|
|
|
2022-06-10 17:56:22 +00:00
|
|
|
export const authLoggedIn = (token: Record<string, string | number>) =>
|
|
|
|
(dispatch: AppDispatch) => {
|
2021-08-22 19:34:58 +00:00
|
|
|
dispatch({ type: AUTH_LOGGED_IN, token });
|
|
|
|
return token;
|
2020-04-05 21:54:51 +00:00
|
|
|
};
|