diff --git a/app/soapbox/actions/auth.js b/app/soapbox/actions/auth.js index 1a700b9ec..5c0fc467a 100644 --- a/app/soapbox/actions/auth.js +++ b/app/soapbox/actions/auth.js @@ -14,6 +14,7 @@ import { createApp } from 'soapbox/actions/apps'; import { fetchMeSuccess, fetchMeFail } from 'soapbox/actions/me'; import { obtainOAuthToken, revokeOAuthToken } from 'soapbox/actions/oauth'; import snackbar from 'soapbox/actions/snackbar'; +import { custom } from 'soapbox/custom'; import KVStore from 'soapbox/storage/kv_store'; import { getLoggedInAccount, parseBaseURL } from 'soapbox/utils/auth'; import sourceCode from 'soapbox/utils/code'; @@ -39,12 +40,14 @@ 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'; +const customApp = custom('app'); + export const messages = defineMessages({ loggedOut: { id: 'auth.logged_out', defaultMessage: 'Logged out.' }, invalidCredentials: { id: 'auth.invalid_credentials', defaultMessage: 'Wrong username or password' }, }); -const noOp = () => () => new Promise(f => f()); +const noOp = () => new Promise(f => f()); const getScopes = state => { const instance = state.get('instance'); @@ -54,12 +57,23 @@ const getScopes = state => { function createAppAndToken() { return (dispatch, getState) => { - return dispatch(createAuthApp()).then(() => { + return dispatch(getAuthApp()).then(() => { return dispatch(createAppToken()); }); }; } +/** Create an auth app, or use it from build config */ +function getAuthApp() { + return (dispatch, getState) => { + if (customApp?.client_secret) { + return noOp().then(() => dispatch({ type: AUTH_APP_CREATED, app: customApp })); + } else { + return dispatch(createAuthApp()); + } + }; +} + function createAuthApp() { return (dispatch, getState) => { const params = { @@ -117,7 +131,7 @@ export function refreshUserToken() { const refreshToken = getState().getIn(['auth', 'user', 'refresh_token']); const app = getState().getIn(['auth', 'app']); - if (!refreshToken) return dispatch(noOp()); + if (!refreshToken) return dispatch(noOp); const params = { client_id: app.get('client_id'), @@ -200,7 +214,7 @@ export function loadCredentials(token, accountUrl) { export function logIn(intl, username, password) { return (dispatch, getState) => { - return dispatch(createAuthApp()).then(() => { + return dispatch(getAuthApp()).then(() => { return dispatch(createUserToken(username, password)); }).catch(error => { if (error.response.data.error === 'mfa_required') { diff --git a/app/soapbox/custom.js b/app/soapbox/custom.ts similarity index 57% rename from app/soapbox/custom.js rename to app/soapbox/custom.ts index 623cb22b3..4bccb386d 100644 --- a/app/soapbox/custom.js +++ b/app/soapbox/custom.ts @@ -1,12 +1,13 @@ /** * Functions for dealing with custom build configuration. */ -import { NODE_ENV } from 'soapbox/build_config'; +import * as BuildConfig from 'soapbox/build_config'; /** Require a custom JSON file if it exists */ -export const custom = (filename, fallback = {}) => { - if (NODE_ENV === 'test') return fallback; +export const custom = (filename: string, fallback: any = {}): any => { + if (BuildConfig.NODE_ENV === 'test') return fallback; + // @ts-ignore: yes it does const context = require.context('custom', false, /\.json$/); const path = `./${filename}.json`; diff --git a/docs/development/build-config.md b/docs/development/build-config.md index 0d7f44b99..4467dbfe0 100644 --- a/docs/development/build-config.md +++ b/docs/development/build-config.md @@ -38,6 +38,34 @@ For example: See `app/soapbox/utils/features.js` for the full list of features. +### Embedded app (`custom/app.json`) + +By default, Soapbox will create a new OAuth app every time a user tries to register or log in. +This is usually the desired behavior, as it works "out of the box" without any additional configuration, and it is resistant to tampering and subtle client bugs. +However, some larger servers may wish to skip this step for performance reasons. + +If an app is supplied in `custom/app.json`, it will be used for authorization. +The full app entity must be provided, for example: + +```json +{ + "client_id": "cf5yI6ffXH1UcDkEApEIrtHpwCi5Tv9xmju8IKdMAkE", + "client_secret": "vHmSDpm6BJGUvR4_qWzmqWjfHcSYlZumxpFfohRwNNQ", + "id": "7132", + "name": "Soapbox FE", + "redirect_uri": "urn:ietf:wg:oauth:2.0:oob", + "website": "https://soapbox.pub/", + "vapid_key": "BLElLQVJVmY_e4F5JoYxI5jXiVOYNsJ9p-amkykc9NcI-jwa9T1Y2GIbDqbY-HqC6ayPkfW4K4o9vgBFKYmkuS4" +} +``` + +It is crucial that the app has the expected scopes. +You can obtain one with the following curl command (replace `MY_DOMAIN`): + +```sh +curl -X POST -H "Content-Type: application/json" -d '{"client_name": "Soapbox FE", "redirect_uris": "urn:ietf:wg:oauth:2.0:oob", "scopes": "read write follow push admin", "website": "https://soapbox.pub/"}' "https://MY_DOMAIN.com/api/v1/apps" +``` + ### Custom files (`custom/instance/*`) You can place arbitrary files of any type in the `custom/instance/` directory.