From f051b70b102cf8d141c7ea3e9dbd056c17a4f073 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 10 Feb 2022 16:33:28 -0600 Subject: [PATCH] Support Ethereum external login --- app/soapbox/actions/auth.js | 21 +++-- app/soapbox/actions/external_auth.js | 82 +++++++++++++------ .../auth_login/components/login_page.js | 35 ++------ .../components/external_login_form.js | 4 +- app/soapbox/utils/quirks.js | 15 ++-- 5 files changed, 89 insertions(+), 68 deletions(-) diff --git a/app/soapbox/actions/auth.js b/app/soapbox/actions/auth.js index dde684f77..65c1dc0f5 100644 --- a/app/soapbox/actions/auth.js +++ b/app/soapbox/actions/auth.js @@ -18,6 +18,7 @@ import KVStore from 'soapbox/storage/kv_store'; import { getLoggedInAccount, parseBaseURL } from 'soapbox/utils/auth'; import sourceCode from 'soapbox/utils/code'; import { getFeatures } from 'soapbox/utils/features'; +import { getQuirks } from 'soapbox/utils/quirks'; import { isStandalone } from 'soapbox/utils/state'; import api, { baseClient } from '../api'; @@ -62,6 +63,10 @@ function createAppAndToken() { function createAuthApp() { return (dispatch, getState) => { + // Mitra: skip creating the app + const quirks = getQuirks(getState().get('instance')); + if (quirks.skipsAppCreation) return dispatch(noOp()); + const params = { client_name: sourceCode.displayName, redirect_uris: 'urn:ietf:wg:oauth:2.0:oob', @@ -213,10 +218,13 @@ export function logIn(intl, username, password) { }; } -export function ethereumLogin() { +export function ethereumLogin(instance, baseURL) { return (dispatch, getState) => { + instance = (instance || getState().get('instance')); + const { ethereum } = window; - const loginMessage = getState().getIn(['instance', 'login_message']); + const { scopes } = getFeatures(instance); + const loginMessage = instance.get('login_message'); return ethereum.request({ method: 'eth_requestAccounts' }).then(walletAddresses => { const [walletAddress] = walletAddresses; @@ -227,15 +235,14 @@ export function ethereumLogin() { wallet_address: walletAddress.toLowerCase(), password: signature, redirect_uri: 'urn:ietf:wg:oauth:2.0:oob', - scope: getScopes(getState()), + scope: scopes, }; // Note: skips app creation // TODO: add to quirks.js for Mitra - return dispatch(obtainOAuthToken(params)).then(token => { - dispatch(authLoggedIn(token)); - return dispatch(verifyCredentials(token.access_token)); - }); + return dispatch(obtainOAuthToken(params, baseURL)) + .then(token => dispatch(authLoggedIn(token))) + .then(({ access_token }) => dispatch(verifyCredentials(access_token, baseURL))); }); }); }; diff --git a/app/soapbox/actions/external_auth.js b/app/soapbox/actions/external_auth.js index b3258ab70..35af6699e 100644 --- a/app/soapbox/actions/external_auth.js +++ b/app/soapbox/actions/external_auth.js @@ -9,11 +9,12 @@ import { Map as ImmutableMap, fromJS } from 'immutable'; import { createApp } from 'soapbox/actions/apps'; -import { authLoggedIn, verifyCredentials, switchAccount } from 'soapbox/actions/auth'; +import { authLoggedIn, verifyCredentials, switchAccount, ethereumLogin } from 'soapbox/actions/auth'; import { obtainOAuthToken } from 'soapbox/actions/oauth'; import { parseBaseURL } from 'soapbox/utils/auth'; import sourceCode from 'soapbox/utils/code'; import { getFeatures } from 'soapbox/utils/features'; +import { getQuirks } from 'soapbox/utils/quirks'; import { baseClient } from '../api'; @@ -32,36 +33,65 @@ const fetchExternalInstance = baseURL => { }); }; -export function createAppAndRedirect(host) { +function createExternalApp(instance, baseURL) { + return (dispatch, getState) => { + const { scopes } = getFeatures(instance); + + const params = { + client_name: sourceCode.displayName, + redirect_uris: `${window.location.origin}/auth/external`, + website: sourceCode.homepage, + scopes, + }; + + return dispatch(createApp(params, baseURL)); + }; +} + +function externalAuthorize(instance, baseURL) { + return (dispatch, getState) => { + const { scopes } = getFeatures(instance); + + return dispatch(createExternalApp(instance, baseURL)).then(app => { + const { client_id, redirect_uri } = app; + + const query = new URLSearchParams({ + client_id, + redirect_uri, + response_type: 'code', + scope: scopes, + }); + + localStorage.setItem('soapbox:external:app', JSON.stringify(app)); + localStorage.setItem('soapbox:external:baseurl', baseURL); + localStorage.setItem('soapbox:external:scopes', scopes); + + window.location.href = `${baseURL}/oauth/authorize?${query.toString()}`; + }); + }; +} + +function externalEthereumLogin(instance, baseURL) { + return (dispatch, getState) => { + return dispatch(ethereumLogin(instance, baseURL)) + .then(account => dispatch(switchAccount(account.id))) + .then(() => window.location.href = '/'); + }; +} + +export function externalLogin(host) { return (dispatch, getState) => { const baseURL = parseBaseURL(host) || parseBaseURL(`https://${host}`); return fetchExternalInstance(baseURL).then(instance => { - const { scopes } = getFeatures(instance); + const features = getFeatures(instance); + const quirks = getQuirks(instance); - const params = { - client_name: sourceCode.displayName, - redirect_uris: `${window.location.origin}/auth/external`, - website: sourceCode.homepage, - scopes, - }; - - return dispatch(createApp(params, baseURL)).then(app => { - const { client_id, redirect_uri } = app; - - const query = new URLSearchParams({ - client_id, - redirect_uri, - response_type: 'code', - scope: scopes, - }); - - localStorage.setItem('soapbox:external:app', JSON.stringify(app)); - localStorage.setItem('soapbox:external:baseurl', baseURL); - localStorage.setItem('soapbox:external:scopes', scopes); - - window.location.href = `${baseURL}/oauth/authorize?${query.toString()}`; - }); + if (features.ethereumLogin && quirks.ethereumLoginOnly) { + return dispatch(externalEthereumLogin(instance, baseURL)); + } else { + return dispatch(externalAuthorize(instance, baseURL)); + } }); }; } diff --git a/app/soapbox/features/auth_login/components/login_page.js b/app/soapbox/features/auth_login/components/login_page.js index d6de2ca3d..ca16c2a81 100644 --- a/app/soapbox/features/auth_login/components/login_page.js +++ b/app/soapbox/features/auth_login/components/login_page.js @@ -4,25 +4,18 @@ import { injectIntl } from 'react-intl'; import { connect } from 'react-redux'; import { Redirect } from 'react-router-dom'; -import { ethereumLogin } from 'soapbox/actions/auth'; import { logIn, verifyCredentials, switchAccount } from 'soapbox/actions/auth'; import { fetchInstance } from 'soapbox/actions/instance'; -import { getFeatures } from 'soapbox/utils/features'; import { isStandalone } from 'soapbox/utils/state'; import LoginForm from './login_form'; import OtpAuthForm from './otp_auth_form'; -const mapStateToProps = state => { - const instance = state.get('instance'); - - return { - me: state.get('me'), - isLoading: false, - standalone: isStandalone(state), - features: getFeatures(instance), - }; -}; +const mapStateToProps = state => ({ + me: state.get('me'), + isLoading: false, + standalone: isStandalone(state), +}); export default @connect(mapStateToProps) @injectIntl @@ -69,18 +62,8 @@ class LoginPage extends ImmutablePureComponent { event.preventDefault(); } - handleEthereumLogin = e => { - const { dispatch } = this.props; - - dispatch(ethereumLogin()) - .then(() => this.setState({ shouldRedirect: true })) - .catch(console.error); - - e.preventDefault(); - }; - render() { - const { standalone, features } = this.props; + const { standalone } = this.props; const { isLoading, mfa_auth_needed, mfa_token, shouldRedirect } = this.state; if (standalone) return ; @@ -89,11 +72,7 @@ class LoginPage extends ImmutablePureComponent { if (mfa_auth_needed) return ; - if (features.ethereumLogin) { - return ; - } else { - return ; - } + return ; } } diff --git a/app/soapbox/features/external_login/components/external_login_form.js b/app/soapbox/features/external_login/components/external_login_form.js index c5da61efb..83a9ccf87 100644 --- a/app/soapbox/features/external_login/components/external_login_form.js +++ b/app/soapbox/features/external_login/components/external_login_form.js @@ -3,7 +3,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import { injectIntl, FormattedMessage, defineMessages } from 'react-intl'; import { connect } from 'react-redux'; -import { createAppAndRedirect, loginWithCode } from 'soapbox/actions/external_auth'; +import { externalLogin, loginWithCode } from 'soapbox/actions/external_auth'; import LoadingIndicator from 'soapbox/components/loading_indicator'; import { SimpleForm, FieldsGroup, TextInput } from 'soapbox/features/forms'; @@ -31,7 +31,7 @@ class ExternalLoginForm extends ImmutablePureComponent { this.setState({ isLoading: true }); - dispatch(createAppAndRedirect(host)) + dispatch(externalLogin(host)) .then(() => this.setState({ isLoading: false })) .catch(() => this.setState({ isLoading: false })); } diff --git a/app/soapbox/utils/quirks.js b/app/soapbox/utils/quirks.js index 764b10e26..e3ec5e651 100644 --- a/app/soapbox/utils/quirks.js +++ b/app/soapbox/utils/quirks.js @@ -1,12 +1,17 @@ -import { parseVersion } from './features'; +import { createSelector } from 'reselect'; + +import { parseVersion, PLEROMA, MITRA } from './features'; // For solving bugs between API implementations -export const getQuirks = instance => { - const v = parseVersion(instance.get('version')); +export const getQuirks = createSelector([ + instance => parseVersion(instance.get('version')), +], (v) => { return { - invertedPagination: v.software === 'Pleroma', + invertedPagination: v.software === PLEROMA, + skipsAppCreation: v.software === MITRA, + ethereumLoginOnly: v.software === MITRA, }; -}; +}); export const getNextLinkName = getState => getQuirks(getState().get('instance')).invertedPagination ? 'prev' : 'next';