fix: Authentication provider credentials are usable across Parse Server apps; fixes security vulnerability [GHSA-837q-jhwx-cmpv](https://github.com/parse-community/parse-server/security/advisories/GHSA-837q-jhwx-cmpv) (#9667)
This commit is contained in:
@@ -1,51 +1,244 @@
|
||||
// Helper functions for accessing the twitter API.
|
||||
var OAuth = require('./OAuth1Client');
|
||||
var Parse = require('parse/node').Parse;
|
||||
/**
|
||||
* Parse Server authentication adapter for Twitter.
|
||||
*
|
||||
* @class TwitterAdapter
|
||||
* @param {Object} options - The adapter configuration options.
|
||||
* @param {string} options.consumerKey - The Twitter App Consumer Key. Required for secure authentication.
|
||||
* @param {string} options.consumerSecret - The Twitter App Consumer Secret. Required for secure authentication.
|
||||
* @param {boolean} [options.enableInsecureAuth=false] - **[DEPRECATED]** Enable insecure authentication (not recommended).
|
||||
*
|
||||
* @description
|
||||
* ## Parse Server Configuration
|
||||
* To configure Parse Server for Twitter authentication, use the following structure:
|
||||
* ### Secure Configuration
|
||||
* ```json
|
||||
* {
|
||||
* "auth": {
|
||||
* "twitter": {
|
||||
* "consumerKey": "your-consumer-key",
|
||||
* "consumerSecret": "your-consumer-secret"
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
* ### Insecure Configuration (Not Recommended)
|
||||
* ```json
|
||||
* {
|
||||
* "auth": {
|
||||
* "twitter": {
|
||||
* "enableInsecureAuth": true
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* The adapter requires the following `authData` fields:
|
||||
* - **Secure Authentication**: `oauth_token`, `oauth_verifier`.
|
||||
* - **Insecure Authentication (Not Recommended)**: `id`, `oauth_token`, `oauth_token_secret`.
|
||||
*
|
||||
* ## Auth Payloads
|
||||
* ### Secure Authentication Payload
|
||||
* ```json
|
||||
* {
|
||||
* "twitter": {
|
||||
* "oauth_token": "1234567890-abc123def456",
|
||||
* "oauth_verifier": "abc123def456"
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* ### Insecure Authentication Payload (Not Recommended)
|
||||
* ```json
|
||||
* {
|
||||
* "twitter": {
|
||||
* "id": "1234567890",
|
||||
* "oauth_token": "1234567890-abc123def456",
|
||||
* "oauth_token_secret": "1234567890-abc123def456"
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* ## Notes
|
||||
* - **Deprecation Notice**: `enableInsecureAuth` and insecure fields (`id`, `oauth_token_secret`) are **deprecated** and may be removed in future versions. Use secure authentication with `consumerKey` and `consumerSecret`.
|
||||
* - Secure authentication exchanges the `oauth_token` and `oauth_verifier` provided by the client for an access token using Twitter's OAuth API.
|
||||
*
|
||||
* @see {@link https://developer.twitter.com/en/docs/authentication/oauth-1-0a Twitter OAuth Documentation}
|
||||
*/
|
||||
|
||||
// Returns a promise that fulfills iff this user id is valid.
|
||||
function validateAuthData(authData, options) {
|
||||
if (!options) {
|
||||
throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'Twitter auth configuration missing');
|
||||
import Config from '../../Config';
|
||||
import querystring from 'querystring';
|
||||
import AuthAdapter from './AuthAdapter';
|
||||
|
||||
class TwitterAuthAdapter extends AuthAdapter {
|
||||
validateOptions(options) {
|
||||
if (!options) {
|
||||
throw new Error('Twitter auth options are required.');
|
||||
}
|
||||
|
||||
this.enableInsecureAuth = options.enableInsecureAuth;
|
||||
|
||||
if (!this.enableInsecureAuth && (!options.consumer_key || !options.consumer_secret)) {
|
||||
throw new Error('Consumer key and secret are required for secure Twitter auth.');
|
||||
}
|
||||
}
|
||||
options = handleMultipleConfigurations(authData, options);
|
||||
var client = new OAuth(options);
|
||||
client.host = 'api.twitter.com';
|
||||
client.auth_token = authData.auth_token;
|
||||
client.auth_token_secret = authData.auth_token_secret;
|
||||
|
||||
return client.get('/1.1/account/verify_credentials.json').then(data => {
|
||||
if (data && data.id_str == '' + authData.id) {
|
||||
async validateAuthData(authData, options) {
|
||||
const config = Config.get(Parse.applicationId);
|
||||
const twitterConfig = config.auth.twitter;
|
||||
|
||||
if (this.enableInsecureAuth && twitterConfig && config.enableInsecureAuthAdapters) {
|
||||
return this.validateInsecureAuth(authData, options);
|
||||
}
|
||||
|
||||
if (!options.consumer_key || !options.consumer_secret) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Twitter auth configuration missing consumer_key and/or consumer_secret.'
|
||||
);
|
||||
}
|
||||
|
||||
const accessTokenData = await this.exchangeAccessToken(authData);
|
||||
|
||||
if (accessTokenData?.oauth_token && accessTokenData?.user_id) {
|
||||
authData.id = accessTokenData.user_id;
|
||||
authData.auth_token = accessTokenData.oauth_token;
|
||||
return;
|
||||
}
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Twitter auth is invalid for this user.');
|
||||
});
|
||||
}
|
||||
|
||||
// Returns a promise that fulfills iff this app id is valid.
|
||||
function validateAppId() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Twitter auth is invalid for this user.'
|
||||
);
|
||||
}
|
||||
|
||||
function handleMultipleConfigurations(authData, options) {
|
||||
if (Array.isArray(options)) {
|
||||
const consumer_key = authData.consumer_key;
|
||||
if (!consumer_key) {
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Twitter auth is invalid for this user.');
|
||||
async validateInsecureAuth(authData, options) {
|
||||
if (!authData.oauth_token || !authData.oauth_token_secret) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Twitter insecure auth requires oauth_token and oauth_token_secret.'
|
||||
);
|
||||
}
|
||||
options = options.filter(option => {
|
||||
return option.consumer_key == consumer_key;
|
||||
|
||||
options = this.handleMultipleConfigurations(authData, options);
|
||||
|
||||
const data = await this.request(authData, options);
|
||||
const parsedData = await data.json();
|
||||
|
||||
if (parsedData?.id === authData.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Twitter auth is invalid for this user.'
|
||||
);
|
||||
}
|
||||
|
||||
async exchangeAccessToken(authData) {
|
||||
const accessTokenRequestOptions = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: querystring.stringify({
|
||||
oauth_token: authData.oauth_token,
|
||||
oauth_verifier: authData.oauth_verifier,
|
||||
}),
|
||||
};
|
||||
|
||||
const response = await fetch('https://api.twitter.com/oauth/access_token', accessTokenRequestOptions);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to exchange access token.');
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
handleMultipleConfigurations(authData, options) {
|
||||
if (Array.isArray(options)) {
|
||||
const consumer_key = authData.consumer_key;
|
||||
|
||||
if (!consumer_key) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Twitter auth is invalid for this user.'
|
||||
);
|
||||
}
|
||||
|
||||
options = options.filter(option => option.consumer_key === consumer_key);
|
||||
|
||||
if (options.length === 0) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Twitter auth is invalid for this user.'
|
||||
);
|
||||
}
|
||||
|
||||
return options[0];
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
async request(authData, options) {
|
||||
const { consumer_key, consumer_secret } = options;
|
||||
|
||||
const oauth = {
|
||||
consumer_key,
|
||||
consumer_secret,
|
||||
auth_token: authData.oauth_token,
|
||||
auth_token_secret: authData.oauth_token_secret,
|
||||
};
|
||||
|
||||
const url = new URL('https://api.twitter.com/2/users/me');
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + oauth.auth_token,
|
||||
},
|
||||
body: JSON.stringify(oauth),
|
||||
});
|
||||
|
||||
if (options.length == 0) {
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Twitter auth is invalid for this user.');
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch user data.');
|
||||
}
|
||||
options = options[0];
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async beforeFind(authData) {
|
||||
if (this.enableInsecureAuth && !authData?.code) {
|
||||
if (!authData?.access_token) {
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Twitter auth is invalid for this user.');
|
||||
}
|
||||
|
||||
const user = await this.getUserFromAccessToken(authData.access_token, authData);
|
||||
|
||||
if (user.id !== authData.id) {
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Twitter auth is invalid for this user.');
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!authData?.code) {
|
||||
throw new Parse.Error(Parse.Error.VALIDATION_ERROR, 'Twitter code is required.');
|
||||
}
|
||||
|
||||
const access_token = await this.exchangeAccessToken(authData);
|
||||
const user = await this.getUserFromAccessToken(access_token, authData);
|
||||
|
||||
|
||||
authData.access_token = access_token;
|
||||
authData.id = user.id;
|
||||
|
||||
delete authData.code;
|
||||
delete authData.redirect_uri;
|
||||
}
|
||||
|
||||
validateAppId() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
validateAppId,
|
||||
validateAuthData,
|
||||
handleMultipleConfigurations,
|
||||
};
|
||||
export default new TwitterAuthAdapter();
|
||||
|
||||
Reference in New Issue
Block a user