Merge branch 'custom-auth' into 'next'

Next: Let a custom auth app be embedded in the build

See merge request soapbox-pub/soapbox-fe!1208
This commit is contained in:
Alex Gleason 2022-04-12 20:43:02 +00:00
commit 5f8c7c8db6
3 changed files with 50 additions and 7 deletions

View file

@ -14,6 +14,7 @@ import { createApp } from 'soapbox/actions/apps';
import { fetchMeSuccess, fetchMeFail } from 'soapbox/actions/me'; import { fetchMeSuccess, fetchMeFail } from 'soapbox/actions/me';
import { obtainOAuthToken, revokeOAuthToken } from 'soapbox/actions/oauth'; import { obtainOAuthToken, revokeOAuthToken } from 'soapbox/actions/oauth';
import snackbar from 'soapbox/actions/snackbar'; import snackbar from 'soapbox/actions/snackbar';
import { custom } from 'soapbox/custom';
import KVStore from 'soapbox/storage/kv_store'; import KVStore from 'soapbox/storage/kv_store';
import { getLoggedInAccount, parseBaseURL } from 'soapbox/utils/auth'; import { getLoggedInAccount, parseBaseURL } from 'soapbox/utils/auth';
import sourceCode from 'soapbox/utils/code'; 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_SUCCESS = 'AUTH_ACCOUNT_REMEMBER_SUCCESS';
export const AUTH_ACCOUNT_REMEMBER_FAIL = 'AUTH_ACCOUNT_REMEMBER_FAIL'; export const AUTH_ACCOUNT_REMEMBER_FAIL = 'AUTH_ACCOUNT_REMEMBER_FAIL';
const customApp = custom('app');
export const messages = defineMessages({ export const messages = defineMessages({
loggedOut: { id: 'auth.logged_out', defaultMessage: 'Logged out.' }, loggedOut: { id: 'auth.logged_out', defaultMessage: 'Logged out.' },
invalidCredentials: { id: 'auth.invalid_credentials', defaultMessage: 'Wrong username or password' }, 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 getScopes = state => {
const instance = state.get('instance'); const instance = state.get('instance');
@ -54,12 +57,23 @@ const getScopes = state => {
function createAppAndToken() { function createAppAndToken() {
return (dispatch, getState) => { return (dispatch, getState) => {
return dispatch(createAuthApp()).then(() => { return dispatch(getAuthApp()).then(() => {
return dispatch(createAppToken()); 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() { function createAuthApp() {
return (dispatch, getState) => { return (dispatch, getState) => {
const params = { const params = {
@ -117,7 +131,7 @@ export function refreshUserToken() {
const refreshToken = getState().getIn(['auth', 'user', 'refresh_token']); const refreshToken = getState().getIn(['auth', 'user', 'refresh_token']);
const app = getState().getIn(['auth', 'app']); const app = getState().getIn(['auth', 'app']);
if (!refreshToken) return dispatch(noOp()); if (!refreshToken) return dispatch(noOp);
const params = { const params = {
client_id: app.get('client_id'), client_id: app.get('client_id'),
@ -200,7 +214,7 @@ export function loadCredentials(token, accountUrl) {
export function logIn(intl, username, password) { export function logIn(intl, username, password) {
return (dispatch, getState) => { return (dispatch, getState) => {
return dispatch(createAuthApp()).then(() => { return dispatch(getAuthApp()).then(() => {
return dispatch(createUserToken(username, password)); return dispatch(createUserToken(username, password));
}).catch(error => { }).catch(error => {
if (error.response.data.error === 'mfa_required') { if (error.response.data.error === 'mfa_required') {

View file

@ -1,12 +1,13 @@
/** /**
* Functions for dealing with custom build configuration. * 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 */ /** Require a custom JSON file if it exists */
export const custom = (filename, fallback = {}) => { export const custom = (filename: string, fallback: any = {}): any => {
if (NODE_ENV === 'test') return fallback; if (BuildConfig.NODE_ENV === 'test') return fallback;
// @ts-ignore: yes it does
const context = require.context('custom', false, /\.json$/); const context = require.context('custom', false, /\.json$/);
const path = `./${filename}.json`; const path = `./${filename}.json`;

View file

@ -38,6 +38,34 @@ For example:
See `app/soapbox/utils/features.js` for the full list of features. 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/*`) ### Custom files (`custom/instance/*`)
You can place arbitrary files of any type in the `custom/instance/` directory. You can place arbitrary files of any type in the `custom/instance/` directory.