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:
Manuel
2025-03-21 10:49:09 +01:00
committed by GitHub
parent c56b2c49b2
commit 5ef0440c8e
59 changed files with 5987 additions and 1680 deletions

View File

@@ -40,11 +40,11 @@ export class AuthAdapter {
* Legacy usage, if provided it will be triggered when authData related to this provider is touched (signup/update/login)
* otherwise you should implement validateSetup, validateLogin and validateUpdate
* @param {Object} authData The client provided authData
* @param {Parse.Cloud.TriggerRequest} request
* @param {Object} options additional adapter options
* @param {Parse.Cloud.TriggerRequest} request
* @returns {Promise<ParseAuthResponse|void|undefined>}
*/
validateAuthData(authData, request, options) {
validateAuthData(authData, options, request) {
return Promise.resolve({});
}
@@ -52,11 +52,11 @@ export class AuthAdapter {
* Triggered when user provide for the first time this auth provider
* could be a register or the user adding a new auth service
* @param {Object} authData The client provided authData
* @param {Parse.Cloud.TriggerRequest} request
* @param {Object} options additional adapter options
* @param {Parse.Cloud.TriggerRequest} request
* @returns {Promise<ParseAuthResponse|void|undefined>}
*/
validateSetUp(authData, req, options) {
validateSetUp(authData, options, req) {
return Promise.resolve({});
}
@@ -64,11 +64,11 @@ export class AuthAdapter {
* Triggered when user provide authData related to this provider
* The user is not logged in and has already set this provider before
* @param {Object} authData The client provided authData
* @param {Parse.Cloud.TriggerRequest} request
* @param {Object} options additional adapter options
* @param {Parse.Cloud.TriggerRequest} request
* @returns {Promise<ParseAuthResponse|void|undefined>}
*/
validateLogin(authData, req, options) {
validateLogin(authData, options, req) {
return Promise.resolve({});
}
@@ -80,10 +80,18 @@ export class AuthAdapter {
* @param {Parse.Cloud.TriggerRequest} request
* @returns {Promise<ParseAuthResponse|void|undefined>}
*/
validateUpdate(authData, req, options) {
validateUpdate(authData, options, req) {
return Promise.resolve({});
}
/**
* Triggered when user is looked up by authData with this provider. Override the `id` field if needed.
* @param {Object} authData The client provided authData
*/
beforeFind(authData) {
}
/**
* Triggered in pre authentication process if needed (like webauthn, SMS OTP)
* @param {Object} challengeData Data provided by the client
@@ -100,9 +108,10 @@ export class AuthAdapter {
* Triggered when auth data is fetched
* @param {Object} authData authData
* @param {Object} options additional adapter options
* @param {Parse.Cloud.TriggerRequest} request
* @returns {Promise<Object>} Any overrides required to authData
*/
afterFind(authData, options) {
afterFind(authData, options, request) {
return Promise.resolve({});
}

View File

@@ -0,0 +1,112 @@
// abstract class for auth code adapters
import AuthAdapter from './AuthAdapter';
export default class BaseAuthCodeAdapter extends AuthAdapter {
constructor(adapterName) {
super();
this.adapterName = adapterName;
}
validateOptions(options) {
if (!options) {
throw new Error(`${this.adapterName} options are required.`);
}
this.enableInsecureAuth = options.enableInsecureAuth;
if (this.enableInsecureAuth) {
return;
}
this.clientId = options.clientId;
this.clientSecret = options.clientSecret;
if (!this.clientId) {
throw new Error(`${this.adapterName} clientId is required.`);
}
if (!this.clientSecret) {
throw new Error(`${this.adapterName} clientSecret is required.`);
}
}
async beforeFind(authData) {
if (this.enableInsecureAuth && !authData?.code) {
if (!authData?.access_token) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `${this.adapterName} 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, `${this.adapterName} auth is invalid for this user.`);
}
return;
}
if (!authData?.code) {
throw new Parse.Error(Parse.Error.VALIDATION_ERROR, `${this.adapterName} code is required.`);
}
const access_token = await this.getAccessTokenFromCode(authData);
const user = await this.getUserFromAccessToken(access_token, authData);
if (authData.id && user.id !== authData.id) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `${this.adapterName} auth is invalid for this user.`);
}
authData.access_token = access_token;
authData.id = user.id;
delete authData.code;
delete authData.redirect_uri;
}
async getUserFromAccessToken() {
// abstract method
throw new Error('getUserFromAccessToken is not implemented');
}
async getAccessTokenFromCode() {
// abstract method
throw new Error('getAccessTokenFromCode is not implemented');
}
validateLogin(authData) {
// User validation is already done in beforeFind
return {
id: authData.id,
}
}
validateSetUp(authData) {
// User validation is already done in beforeFind
return {
id: authData.id,
}
}
afterFind(authData) {
return {
id: authData.id,
}
}
validateUpdate(authData) {
// User validation is already done in beforeFind
return {
id: authData.id,
}
}
parseResponseData(data) {
const startPos = data.indexOf('(');
const endPos = data.indexOf(')');
if (startPos === -1 || endPos === -1) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `${this.adapterName} auth is invalid for this user.`);
}
const jsonData = data.substring(startPos + 1, endPos);
return JSON.parse(jsonData);
}
}

View File

@@ -1,3 +1,47 @@
/**
* Parse Server authentication adapter for Apple.
*
* @class AppleAdapter
* @param {Object} options - Configuration options for the adapter.
* @param {string} options.clientId - Your Apple App ID.
*
* @param {Object} authData - The authentication data provided by the client.
* @param {string} authData.id - The user ID obtained from Apple.
* @param {string} authData.token - The token obtained from Apple.
*
* @description
* ## Parse Server Configuration
* To configure Parse Server for Apple authentication, use the following structure:
* ```json
* {
* "auth": {
* "apple": {
* "clientId": "12345"
* }
* }
* }
* ```
*
* ## Expected `authData` from the Client
* The adapter expects the client to provide the following `authData` payload:
* - `authData.id` (**string**, required): The user ID obtained from Apple.
* - `authData.token` (**string**, required): The token obtained from Apple.
*
* Parse Server stores the required authentication data in the database.
*
* ### Example AuthData from Apple
* ```json
* {
* "apple": {
* "id": "1234567",
* "token": "xxxxx.yyyyy.zzzzz"
* }
* }
* ```
*
* @see {@link https://developer.apple.com/documentation/signinwithapplerestapi Sign in with Apple REST API Documentation}
*/
// Apple SignIn Auth
// https://developer.apple.com/documentation/signinwithapplerestapi

View File

@@ -1,3 +1,63 @@
/**
* Parse Server authentication adapter for Facebook.
*
* @class FacebookAdapter
* @param {Object} options - The adapter configuration options.
* @param {string} options.appSecret - Your Facebook App Secret. Required for secure authentication.
* @param {string[]} options.appIds - An array of Facebook App IDs. Required for validating the app.
*
* @description
* ## Parse Server Configuration
* To configure Parse Server for Facebook authentication, use the following structure:
* ```json
* {
* "auth": {
* "facebook": {
* "appSecret": "your-app-secret",
* "appIds": ["your-app-id"]
* }
* }
* }
* ```
*
* The adapter supports the following authentication methods:
* - **Standard Login**: Requires `id` and `access_token`.
* - **Limited Login**: Requires `id` and `token`.
*
* ## Auth Payloads
* ### Standard Login Payload
* ```json
* {
* "facebook": {
* "id": "1234567",
* "access_token": "abc123def456ghi789"
* }
* }
* ```
*
* ### Limited Login Payload
* ```json
* {
* "facebook": {
* "id": "1234567",
* "token": "xxxxx.yyyyy.zzzzz"
* }
* }
* ```
*
* ## Notes
* - **Standard Login**: Use `id` and `access_token` for full functionality.
* - **Limited Login**: Use `id` and `token` (JWT) when tracking is opted out (e.g., via Apple's App Tracking Transparency).
* - Supported Parse Server versions:
* - `>= 6.5.6 < 7`
* - `>= 7.0.1`
*
* Secure authentication is recommended to ensure proper data protection and compliance with Facebook's guidelines.
*
* @see {@link https://developers.facebook.com/docs/facebook-login/limited-login/ Facebook Limited Login}
* @see {@link https://developers.facebook.com/docs/facebook-login/facebook-login-for-business/ Facebook Login for Business}
*/
// Helper functions for accessing the Facebook Graph API.
const Parse = require('parse/node').Parse;
const crypto = require('crypto');

View File

@@ -1,195 +1,239 @@
/* Apple Game Center Auth
https://developer.apple.com/documentation/gamekit/gklocalplayer/1515407-generateidentityverificationsign#discussion
/**
* Parse Server authentication adapter for Apple Game Center.
*
* @class AppleGameCenterAdapter
* @param {Object} options - Configuration options for the adapter.
* @param {string} options.bundleId - Your Apple Game Center bundle ID. Required for secure authentication.
* @param {boolean} [options.enableInsecureAuth=false] - **[DEPRECATED]** Enable insecure authentication (not recommended).
*
* @param {Object} authData - The authentication data provided by the client.
* @param {string} authData.id - The user ID obtained from Apple Game Center.
* @param {string} authData.publicKeyUrl - The public key URL obtained from Apple Game Center.
* @param {string} authData.timestamp - The timestamp obtained from Apple Game Center.
* @param {string} authData.signature - The signature obtained from Apple Game Center.
* @param {string} authData.salt - The salt obtained from Apple Game Center.
* @param {string} [authData.bundleId] - **[DEPRECATED]** The bundle ID obtained from Apple Game Center (required for insecure authentication).
*
* @description
* ## Parse Server Configuration
* The following `authData` fields are required:
* `id`, `publicKeyUrl`, `timestamp`, `signature`, and `salt`. These fields are validated against the configured `bundleId` for additional security.
*
* To configure Parse Server for Apple Game Center authentication, use the following structure:
* ```json
* {
* "auth": {
* "gcenter": {
* "bundleId": "com.valid.app"
* }
* }
* ```
*
* ## Insecure Authentication (Not Recommended)
* The following `authData` fields are required for insecure authentication:
* `id`, `publicKeyUrl`, `timestamp`, `signature`, `salt`, and `bundleId` (**[DEPRECATED]**). This flow is insecure and poses potential security risks.
*
* To configure Parse Server for insecure authentication, use the following structure:
* ```json
* {
* "auth": {
* "gcenter": {
* "enableInsecureAuth": true
* }
* }
* ```
*
* ### Deprecation Notice
* The `enableInsecureAuth` option and `authData.bundleId` parameter are deprecated and may be removed in future releases. Use secure authentication with the `bundleId` configured in the `options` object instead.
*
*
* @example <caption>Secure Authentication Example</caption>
* // Example authData for secure authentication:
* const authData = {
* gcenter: {
* id: "1234567",
* publicKeyUrl: "https://valid.apple.com/public/timeout.cer",
* timestamp: 1460981421303,
* salt: "saltST==",
* signature: "PoDwf39DCN464B49jJCU0d9Y0J"
* }
* };
*
* @example <caption>Insecure Authentication Example (Not Recommended)</caption>
* // Example authData for insecure authentication:
* const authData = {
* gcenter: {
* id: "1234567",
* publicKeyUrl: "https://valid.apple.com/public/timeout.cer",
* timestamp: 1460981421303,
* salt: "saltST==",
* signature: "PoDwf39DCN464B49jJCU0d9Y0J",
* bundleId: "com.valid.app" // Deprecated.
* }
* };
*
* @see {@link https://developer.apple.com/documentation/gamekit/gklocalplayer/3516283-fetchitems Apple Game Center Documentation}
*/
/* global BigInt */
const authData = {
publicKeyUrl: 'https://valid.apple.com/public/timeout.cer',
timestamp: 1460981421303,
signature: 'PoDwf39DCN464B49jJCU0d9Y0J',
salt: 'saltST==',
bundleId: 'com.valid.app'
id: 'playerId',
};
*/
import crypto from 'crypto';
import { asn1, pki } from 'node-forge';
import AuthAdapter from './AuthAdapter';
class GameCenterAuth extends AuthAdapter {
constructor() {
super();
this.ca = { cert: null, url: null };
this.cache = {};
this.bundleId = '';
}
const { Parse } = require('parse/node');
const crypto = require('crypto');
const https = require('https');
const { pki } = require('node-forge');
const ca = { cert: null, url: null };
const cache = {}; // (publicKey -> cert) cache
validateOptions(options) {
if (!options) {
throw new Error('Game center auth options are required.');
}
function verifyPublicKeyUrl(publicKeyUrl) {
try {
if (!this.loadingPromise) {
this.loadingPromise = this.loadCertificate(options);
}
this.enableInsecureAuth = options.enableInsecureAuth;
this.bundleId = options.bundleId;
if (!this.enableInsecureAuth && !this.bundleId) {
throw new Error('bundleId is required for secure auth.');
}
}
async loadCertificate(options) {
const rootCertificateUrl =
options.rootCertificateUrl ||
'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem';
if (this.ca.url === rootCertificateUrl) {
return rootCertificateUrl;
}
const { certificate, headers } = await this.fetchCertificate(rootCertificateUrl);
if (
headers.get('content-type') !== 'application/x-pem-file' ||
!headers.get('content-length') ||
parseInt(headers.get('content-length'), 10) > 10000
) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Invalid rootCertificateURL.');
}
this.ca.cert = pki.certificateFromPem(certificate);
this.ca.url = rootCertificateUrl;
return rootCertificateUrl;
}
verifyPublicKeyUrl(publicKeyUrl) {
const regex = /^https:\/\/(?:[-_A-Za-z0-9]+\.){0,}apple\.com\/.*\.cer$/;
return regex.test(publicKeyUrl);
} catch (error) {
return false;
}
}
function convertX509CertToPEM(X509Cert) {
const pemPreFix = '-----BEGIN CERTIFICATE-----\n';
const pemPostFix = '-----END CERTIFICATE-----';
const base64 = X509Cert;
const certBody = base64.match(new RegExp('.{0,64}', 'g')).join('\n');
return pemPreFix + certBody + pemPostFix;
}
async function getAppleCertificate(publicKeyUrl) {
if (!verifyPublicKeyUrl(publicKeyUrl)) {
throw new Parse.Error(
Parse.Error.OBJECT_NOT_FOUND,
`Apple Game Center - invalid publicKeyUrl: ${publicKeyUrl}`
);
}
if (cache[publicKeyUrl]) {
return cache[publicKeyUrl];
}
const url = new URL(publicKeyUrl);
const headOptions = {
hostname: url.hostname,
path: url.pathname,
method: 'HEAD',
};
const cert_headers = await new Promise((resolve, reject) =>
https.get(headOptions, res => resolve(res.headers)).on('error', reject)
);
const validContentTypes = ['application/x-x509-ca-cert', 'application/pkix-cert'];
if (
!validContentTypes.includes(cert_headers['content-type']) ||
cert_headers['content-length'] == null ||
cert_headers['content-length'] > 10000
) {
throw new Parse.Error(
Parse.Error.OBJECT_NOT_FOUND,
`Apple Game Center - invalid publicKeyUrl: ${publicKeyUrl}`
);
}
const { certificate, headers } = await getCertificate(publicKeyUrl);
if (headers['cache-control']) {
const expire = headers['cache-control'].match(/max-age=([0-9]+)/);
if (expire) {
cache[publicKeyUrl] = certificate;
// we'll expire the cache entry later, as per max-age
setTimeout(() => {
delete cache[publicKeyUrl];
}, parseInt(expire[1], 10) * 1000);
async fetchCertificate(url) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch certificate: ${url}`);
}
const contentType = response.headers.get('content-type');
const isPem = contentType?.includes('application/x-pem-file');
if (isPem) {
const certificate = await response.text();
return { certificate, headers: response.headers };
}
const data = await response.arrayBuffer();
const binaryData = Buffer.from(data);
const asn1Cert = asn1.fromDer(binaryData.toString('binary'));
const forgeCert = pki.certificateFromAsn1(asn1Cert);
const certificate = pki.certificateToPem(forgeCert);
return { certificate, headers: response.headers };
}
return verifyPublicKeyIssuer(certificate, publicKeyUrl);
}
function getCertificate(url, buffer) {
return new Promise((resolve, reject) => {
https
.get(url, res => {
const data = [];
res.on('data', chunk => {
data.push(chunk);
});
res.on('end', () => {
if (buffer) {
resolve({ certificate: Buffer.concat(data), headers: res.headers });
return;
}
let cert = '';
for (const chunk of data) {
cert += chunk.toString('base64');
}
const certificate = convertX509CertToPEM(cert);
resolve({ certificate, headers: res.headers });
});
})
.on('error', reject);
});
}
async getAppleCertificate(publicKeyUrl) {
if (!this.verifyPublicKeyUrl(publicKeyUrl)) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `Invalid publicKeyUrl: ${publicKeyUrl}`);
}
function convertTimestampToBigEndian(timestamp) {
const buffer = Buffer.alloc(8);
if (this.cache[publicKeyUrl]) {
return this.cache[publicKeyUrl];
}
const high = ~~(timestamp / 0xffffffff);
const low = timestamp % (0xffffffff + 0x1);
const { certificate, headers } = await this.fetchCertificate(publicKeyUrl);
const cacheControl = headers.get('cache-control');
const expire = cacheControl?.match(/max-age=([0-9]+)/);
buffer.writeUInt32BE(parseInt(high, 10), 0);
buffer.writeUInt32BE(parseInt(low, 10), 4);
this.verifyPublicKeyIssuer(certificate, publicKeyUrl);
return buffer;
}
if (expire) {
this.cache[publicKeyUrl] = certificate;
setTimeout(() => delete this.cache[publicKeyUrl], parseInt(expire[1], 10) * 1000);
}
function verifySignature(publicKey, authData) {
const verifier = crypto.createVerify('sha256');
verifier.update(authData.playerId, 'utf8');
verifier.update(authData.bundleId, 'utf8');
verifier.update(convertTimestampToBigEndian(authData.timestamp));
verifier.update(authData.salt, 'base64');
if (!verifier.verify(publicKey, authData.signature, 'base64')) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Apple Game Center - invalid signature');
return certificate;
}
}
function verifyPublicKeyIssuer(cert, publicKeyUrl) {
const publicKeyCert = pki.certificateFromPem(cert);
if (!ca.cert) {
throw new Parse.Error(
Parse.Error.OBJECT_NOT_FOUND,
'Apple Game Center auth adapter parameter `rootCertificateURL` is invalid.'
);
}
try {
if (!ca.cert.verify(publicKeyCert)) {
verifyPublicKeyIssuer(cert, publicKeyUrl) {
const publicKeyCert = pki.certificateFromPem(cert);
if (!this.ca.cert) {
throw new Parse.Error(
Parse.Error.OBJECT_NOT_FOUND,
`Apple Game Center - invalid publicKeyUrl: ${publicKeyUrl}`
'Root certificate is invalid or missing.'
);
}
} catch (e) {
throw new Parse.Error(
Parse.Error.OBJECT_NOT_FOUND,
`Apple Game Center - invalid publicKeyUrl: ${publicKeyUrl}`
);
if (!this.ca.cert.verify(publicKeyCert)) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `Invalid publicKeyUrl: ${publicKeyUrl}`);
}
}
verifySignature(publicKey, authData) {
const bundleId = this.bundleId || (this.enableInsecureAuth && authData.bundleId);
const verifier = crypto.createVerify('sha256');
verifier.update(Buffer.from(authData.id, 'utf8'));
verifier.update(Buffer.from(bundleId, 'utf8'));
verifier.update(this.convertTimestampToBigEndian(authData.timestamp));
verifier.update(Buffer.from(authData.salt, 'base64'));
if (!verifier.verify(publicKey, authData.signature, 'base64')) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Invalid signature.');
}
}
async validateAuthData(authData) {
const requiredKeys = ['id', 'publicKeyUrl', 'timestamp', 'signature', 'salt'];
if (this.enableInsecureAuth) {
requiredKeys.push('bundleId');
}
for (const key of requiredKeys) {
if (!authData[key]) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `AuthData ${key} is missing.`);
}
}
await this.loadingPromise;
const publicKey = await this.getAppleCertificate(authData.publicKeyUrl);
this.verifySignature(publicKey, authData);
}
convertTimestampToBigEndian(timestamp) {
const buffer = Buffer.alloc(8);
buffer.writeBigUInt64BE(BigInt(timestamp));
return buffer;
}
return cert;
}
// Returns a promise that fulfills if this user id is valid.
async function validateAuthData(authData) {
if (!authData.id) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Apple Game Center - authData id missing');
}
authData.playerId = authData.id;
const publicKey = await getAppleCertificate(authData.publicKeyUrl);
return verifySignature(publicKey, authData);
}
// Returns a promise that fulfills if this app id is valid.
async function validateAppId(appIds, authData, options = {}) {
if (!options.rootCertificateUrl) {
options.rootCertificateUrl =
'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem';
}
if (ca.url === options.rootCertificateUrl) {
return;
}
const { certificate, headers } = await getCertificate(options.rootCertificateUrl, true);
if (
headers['content-type'] !== 'application/x-pem-file' ||
headers['content-length'] == null ||
headers['content-length'] > 10000
) {
throw new Parse.Error(
Parse.Error.OBJECT_NOT_FOUND,
'Apple Game Center auth adapter parameter `rootCertificateURL` is invalid.'
);
}
ca.cert = pki.certificateFromPem(certificate);
ca.url = options.rootCertificateUrl;
}
module.exports = {
validateAppId,
validateAuthData,
cache,
};
export default new GameCenterAuth();

View File

@@ -1,35 +1,127 @@
// Helper functions for accessing the github API.
var Parse = require('parse/node').Parse;
const httpsRequest = require('./httpsRequest');
/**
* Parse Server authentication adapter for GitHub.
* @class GitHubAdapter
* @param {Object} options - The adapter configuration options.
* @param {string} options.clientId - The GitHub App Client ID. Required for secure authentication.
* @param {string} options.clientSecret - The GitHub App Client Secret. Required for secure authentication.
* @param {boolean} [options.enableInsecureAuth=false] - **[DEPRECATED]** Enable insecure authentication (not recommended).
*
* @param {Object} authData - The authentication data provided by the client.
* @param {string} authData.code - The authorization code from GitHub. Required for secure authentication.
* @param {string} [authData.id] - **[DEPRECATED]** The GitHub user ID (required for insecure authentication).
* @param {string} [authData.access_token] - **[DEPRECATED]** The GitHub access token (required for insecure authentication).
*
* @description
* ## Parse Server Configuration
* * To configure Parse Server for GitHub authentication, use the following structure:
* ```json
* {
* "auth": {
* "github": {
* "clientId": "12345",
* "clientSecret": "abcde"
* }
* }
* ```
*
* The GitHub adapter exchanges the `authData.code` provided by the client for an access token using GitHub's OAuth API. The following `authData` field is required:
* - `code`
*
* ## Insecure Authentication (Not Recommended)
* Insecure authentication uses the `authData.id` and `authData.access_token` provided by the client. This flow is insecure, deprecated, and poses potential security risks. The following `authData` fields are required:
* - `id` (**[DEPRECATED]**): The GitHub user ID.
* - `access_token` (**[DEPRECATED]**): The GitHub access token.
* To configure Parse Server for insecure authentication, use the following structure:
* ```json
* {
* "auth": {
* "github": {
* "enableInsecureAuth": true
* }
* }
* ```
*
* ### Deprecation Notice
* The `enableInsecureAuth` option and insecure `authData` fields (`id`, `access_token`) are deprecated and will be removed in future versions. Use secure authentication with `clientId` and `clientSecret`.
*
* @example <caption>Secure Authentication Example</caption>
* // Example authData for secure authentication:
* const authData = {
* github: {
* code: "abc123def456ghi789"
* }
* };
*
* @example <caption>Insecure Authentication Example (Not Recommended)</caption>
* // Example authData for insecure authentication:
* const authData = {
* github: {
* id: "1234567",
* access_token: "abc123def456ghi789" // Deprecated.
* }
* };
*
* @note `enableInsecureAuth` will be removed in future versions. Use secure authentication with `clientId` and `clientSecret`.
* @note Secure authentication exchanges the `code` provided by the client for an access token using GitHub's OAuth API.
*
* @see {@link https://docs.github.com/en/developers/apps/authorizing-oauth-apps GitHub OAuth Documentation}
*/
// Returns a promise that fulfills iff this user id is valid.
function validateAuthData(authData) {
return request('user', authData.access_token).then(data => {
if (data && data.id == authData.id) {
return;
import BaseCodeAuthAdapter from './BaseCodeAuthAdapter';
class GitHubAdapter extends BaseCodeAuthAdapter {
constructor() {
super('GitHub');
}
async getAccessTokenFromCode(authData) {
const tokenUrl = 'https://github.com/login/oauth/access_token';
const response = await fetch(tokenUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify({
client_id: this.clientId,
client_secret: this.clientSecret,
code: authData.code,
}),
});
if (!response.ok) {
throw new Parse.Error(Parse.Error.VALIDATION_ERROR, `Failed to exchange code for token: ${response.statusText}`);
}
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Github auth is invalid for this user.');
});
const data = await response.json();
if (data.error) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, data.error_description || data.error);
}
return data.access_token;
}
async getUserFromAccessToken(accessToken) {
const userApiUrl = 'https://api.github.com/user';
const response = await fetch(userApiUrl, {
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
},
});
if (!response.ok) {
throw new Parse.Error(Parse.Error.VALIDATION_ERROR, `Failed to fetch GitHub user: ${response.statusText}`);
}
const userData = await response.json();
if (!userData.id || !userData.login) {
throw new Parse.Error(Parse.Error.VALIDATION_ERROR, 'Invalid GitHub user data received.');
}
return userData;
}
}
// Returns a promise that fulfills iff this app id is valid.
function validateAppId() {
return Promise.resolve();
}
export default new GitHubAdapter();
// A promisey wrapper for api requests
function request(path, access_token) {
return httpsRequest.get({
host: 'api.github.com',
path: '/' + path,
headers: {
Authorization: 'bearer ' + access_token,
'User-Agent': 'parse-server',
},
});
}
module.exports = {
validateAppId: validateAppId,
validateAuthData: validateAuthData,
};

View File

@@ -1,3 +1,47 @@
/**
* Parse Server authentication adapter for Google.
*
* @class GoogleAdapter
* @param {Object} options - The adapter configuration options.
* @param {string} options.clientId - Your Google application Client ID. Required for authentication.
*
* @description
* ## Parse Server Configuration
* To configure Parse Server for Google authentication, use the following structure:
* ```json
* {
* "auth": {
* "google": {
* "clientId": "your-client-id"
* }
* }
* }
* ```
*
* The adapter requires the following `authData` fields:
* - **id**: The Google user ID.
* - **id_token**: The Google ID token.
* - **access_token**: The Google access token.
*
* ## Auth Payload
* ### Example Auth Data Payload
* ```json
* {
* "google": {
* "id": "1234567",
* "id_token": "xxxxx.yyyyy.zzzzz",
* "access_token": "abc123def456ghi789"
* }
* }
* ```
*
* ## Notes
* - Ensure your Google Client ID is configured properly in the Parse Server configuration.
* - The `id_token` and `access_token` are validated against Google's authentication services.
*
* @see {@link https://developers.google.com/identity/sign-in/web/backend-auth Google Authentication Documentation}
*/
'use strict';
// Helper functions for accessing the google API.

View File

@@ -1,33 +1,139 @@
/* Google Play Game Services
https://developers.google.com/games/services/web/api/players/get
/**
* Parse Server authentication adapter for Google Play Games Services.
*
* @class GooglePlayGamesServicesAdapter
* @param {Object} options - The adapter configuration options.
* @param {string} options.clientId - Your Google Play Games Services App Client ID. Required for secure authentication.
* @param {string} options.clientSecret - Your Google Play Games Services App Client 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 Google Play Games Services authentication, use the following structure:
* ```json
* {
* "auth": {
* "gpgames": {
* "clientId": "your-client-id",
* "clientSecret": "your-client-secret"
* }
* }
* }
* ```
* ### Insecure Configuration (Not Recommended)
* ```json
* {
* "auth": {
* "gpgames": {
* "enableInsecureAuth": true
* }
* }
* }
* ```
*
* The adapter requires the following `authData` fields:
* - **Secure Authentication**: `code`, `redirect_uri`.
* - **Insecure Authentication (Not Recommended)**: `id`, `access_token`.
*
* ## Auth Payloads
* ### Secure Authentication Payload
* ```json
* {
* "gpgames": {
* "code": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
* "redirect_uri": "https://example.com/callback"
* }
* }
* ```
*
* ### Insecure Authentication Payload (Not Recommended)
* ```json
* {
* "gpgames": {
* "id": "123456789",
* "access_token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
* }
* }
* ```
*
* ## Notes
* - `enableInsecureAuth` is **not recommended** and may be removed in future versions. Use secure authentication with `code` and `redirect_uri`.
* - Secure authentication exchanges the `code` provided by the client for an access token using Google Play Games Services' OAuth API.
*
* @see {@link https://developers.google.com/games/services/console/enabling Google Play Games Services Authentication Documentation}
*/
const authData = {
id: 'playerId',
access_token: 'token',
};
*/
const { Parse } = require('parse/node');
const httpsRequest = require('./httpsRequest');
// Returns a promise that fulfills if this user id is valid.
async function validateAuthData(authData) {
const response = await httpsRequest.get(
`https://www.googleapis.com/games/v1/players/${authData.id}?access_token=${authData.access_token}`
);
if (!(response && response.playerId === authData.id)) {
throw new Parse.Error(
Parse.Error.OBJECT_NOT_FOUND,
'Google Play Games Services - authData is invalid for this user.'
);
import BaseCodeAuthAdapter from './BaseCodeAuthAdapter';
class GooglePlayGamesServicesAdapter extends BaseCodeAuthAdapter {
constructor() {
super("gpgames");
}
async getAccessTokenFromCode(authData) {
const tokenUrl = 'https://oauth2.googleapis.com/token';
const response = await fetch(tokenUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify({
client_id: this.clientId,
client_secret: this.clientSecret,
code: authData.code,
redirect_uri: authData.redirectUri,
grant_type: 'authorization_code',
}),
});
if (!response.ok) {
throw new Parse.Error(
Parse.Error.VALIDATION_ERROR,
`Failed to exchange code for token: ${response.statusText}`
);
}
const data = await response.json();
if (data.error) {
throw new Parse.Error(
Parse.Error.OBJECT_NOT_FOUND,
data.error_description || data.error
);
}
return data.access_token;
}
async getUserFromAccessToken(accessToken, authData) {
const userApiUrl = `https://www.googleapis.com/games/v1/players/${authData.id}`;
const response = await fetch(userApiUrl, {
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
},
});
if (!response.ok) {
throw new Parse.Error(
Parse.Error.VALIDATION_ERROR,
`Failed to fetch Google Play Games Services user: ${response.statusText}`
);
}
const userData = await response.json();
if (!userData.playerId || userData.playerId !== authData.id) {
throw new Parse.Error(
Parse.Error.VALIDATION_ERROR,
'Invalid Google Play Games Services user data received.'
);
}
return {
id: userData.playerId
};
}
}
// Returns a promise that fulfills if this app id is valid.
function validateAppId() {
return Promise.resolve();
}
module.exports = {
validateAppId,
validateAuthData,
};
export default new GooglePlayGamesServicesAdapter();

View File

@@ -3,30 +3,31 @@ import Parse from 'parse/node';
import AuthAdapter from './AuthAdapter';
const apple = require('./apple');
const gcenter = require('./gcenter');
const gpgames = require('./gpgames');
const facebook = require('./facebook');
const instagram = require('./instagram');
const linkedin = require('./linkedin');
const meetup = require('./meetup');
import mfa from './mfa';
const google = require('./google');
const github = require('./github');
const twitter = require('./twitter');
const spotify = require('./spotify');
const digits = require('./twitter'); // digits tokens are validated by twitter
const janrainengage = require('./janrainengage');
const facebook = require('./facebook');
import gcenter from './gcenter';
import github from './github';
const google = require('./google');
import gpgames from './gpgames';
import instagram from './instagram';
const janraincapture = require('./janraincapture');
const line = require('./line');
const vkontakte = require('./vkontakte');
const qq = require('./qq');
const wechat = require('./wechat');
const weibo = require('./weibo');
const oauth2 = require('./oauth2');
const phantauth = require('./phantauth');
const microsoft = require('./microsoft');
const janrainengage = require('./janrainengage');
const keycloak = require('./keycloak');
const ldap = require('./ldap');
import line from './line';
import linkedin from './linkedin';
const meetup = require('./meetup');
import mfa from './mfa';
import microsoft from './microsoft';
import oauth2 from './oauth2';
const phantauth = require('./phantauth');
import qq from './qq';
import spotify from './spotify';
import twitter from './twitter';
const vkontakte = require('./vkontakte');
import wechat from './wechat';
import weibo from './weibo';
const anonymous = {
validateAuthData: () => {
@@ -241,9 +242,9 @@ module.exports = function (authOptions = {}, enableAnonymousUsers = true) {
};
const result = afterFind.call(
adapter,
requestObject,
authData[provider],
providerOptions
providerOptions,
requestObject,
);
if (result) {
authData[provider] = result;

View File

@@ -1,27 +1,121 @@
// Helper functions for accessing the instagram API.
var Parse = require('parse/node').Parse;
const httpsRequest = require('./httpsRequest');
const defaultURL = 'https://graph.instagram.com/';
/**
* Parse Server authentication adapter for Instagram.
*
* @class InstagramAdapter
* @param {Object} options - The adapter configuration options.
* @param {string} options.clientId - Your Instagram App Client ID. Required for secure authentication.
* @param {string} options.clientSecret - Your Instagram App Client 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 Instagram authentication, use the following structure:
* ```json
* {
* "auth": {
* "instagram": {
* "clientId": "your-client-id",
* "clientSecret": "your-client-secret"
* }
* }
* }
* ```
* ### Insecure Configuration (Not Recommended)
* ```json
* {
* "auth": {
* "instagram": {
* "enableInsecureAuth": true
* }
* }
* }
* ```
*
* The adapter requires the following `authData` fields:
* - **Secure Authentication**: `code`, `redirect_uri`.
* - **Insecure Authentication (Deprecated)**: `id`, `access_token`.
*
* ## Auth Payloads
* ### Secure Authentication Payload
* ```json
* {
* "instagram": {
* "code": "lmn789opq012rst345uvw",
* "redirect_uri": "https://example.com/callback"
* }
* }
* ```
*
* ### Insecure Authentication Payload (Deprecated)
* ```json
* {
* "instagram": {
* "id": "1234567",
* "access_token": "AQXNnd2hIT6z9bHFzZz2Kp1ghiMz_RtyuvwXYZ123abc"
* }
* }
* ```
*
* ## Notes
* - `enableInsecureAuth` is **deprecated** and will be removed in future versions. Use secure authentication with `code` and `redirect_uri`.
* - Secure authentication exchanges the `code` and `redirect_uri` provided by the client for an access token using Instagram's OAuth flow.
*
* @see {@link https://developers.facebook.com/docs/instagram-basic-display-api/getting-started Instagram Basic Display API - Getting Started}
*/
// Returns a promise that fulfills if this user id is valid.
function validateAuthData(authData) {
const apiURL = authData.apiURL || defaultURL;
const path = `${apiURL}me?fields=id&access_token=${authData.access_token}`;
return httpsRequest.get(path).then(response => {
const user = response.data ? response.data : response;
if (user && user.id == authData.id) {
return;
import BaseAuthCodeAdapter from './BaseCodeAuthAdapter';
class InstagramAdapter extends BaseAuthCodeAdapter {
constructor() {
super('Instagram');
}
async getAccessTokenFromCode(authData) {
const response = await fetch('https://api.instagram.com/oauth/access_token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
client_id: this.clientId,
client_secret: this.clientSecret,
grant_type: 'authorization_code',
redirect_uri: this.redirectUri,
code: authData.code
})
});
if (!response.ok) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Instagram API request failed.');
}
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Instagram auth is invalid for this user.');
});
const data = await response.json();
if (data.error) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, data.error_description || data.error);
}
return data.access_token;
}
async getUserFromAccessToken(accessToken, authData) {
const defaultURL = 'https://graph.instagram.com/';
const apiURL = authData.apiURL || defaultURL;
const path = `${apiURL}me?fields=id&access_token=${accessToken}`;
const response = await fetch(path);
if (!response.ok) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Instagram API request failed.');
}
const user = await response.json();
if (user?.id !== authData.id) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Instagram auth is invalid for this user.');
}
return {
id: user.id,
}
}
}
// Returns a promise that fulfills iff this app id is valid.
function validateAppId() {
return Promise.resolve();
}
module.exports = {
validateAppId,
validateAuthData,
};
export default new InstagramAdapter();

View File

@@ -1,3 +1,48 @@
/**
* Parse Server authentication adapter for Janrain Capture API.
*
* @class JanrainCapture
* @param {Object} options - The adapter configuration options.
* @param {String} options.janrain_capture_host - The Janrain Capture API host.
*
* @param {Object} authData - The authentication data provided by the client.
* @param {String} authData.id - The Janrain Capture user ID.
* @param {String} authData.access_token - The Janrain Capture access token.
*
* @description
* ## Parse Server Configuration
* To configure Parse Server for Janrain Capture authentication, use the following structure:
* ```json
* {
* "auth": {
* "janrain": {
* "janrain_capture_host": "your-janrain-capture-host"
* }
* }
* }
* ```
*
* The adapter requires the following `authData` fields:
* - `id`: The Janrain Capture user ID.
* - `access_token`: An authorized Janrain Capture access token for the user.
*
* ## Auth Payload Example
* ```json
* {
* "janrain": {
* "id": "user's Janrain Capture ID as a string",
* "access_token": "an authorized Janrain Capture access token for the user"
* }
* }
* ```
*
* ## Notes
* Parse Server validates the provided `authData` using the Janrain Capture API.
*
* @see {@link https://docs.janrain.com/api/registration/entity/#entity Janrain Capture API Documentation}
*/
// Helper functions for accessing the Janrain Capture API.
var Parse = require('parse/node').Parse;
var querystring = require('querystring');

View File

@@ -2,9 +2,18 @@
var httpsRequest = require('./httpsRequest');
var Parse = require('parse/node').Parse;
var querystring = require('querystring');
import Config from '../../Config';
import Deprecator from '../../Deprecator/Deprecator';
// Returns a promise that fulfills iff this user id is valid.
function validateAuthData(authData, options) {
const config = Config.get(Parse.applicationId);
Deprecator.logRuntimeDeprecation({ usage: 'janrainengage adapter' });
if (!config?.auth?.janrainengage?.enableInsecureAuth || !config.enableInsecureAuthAdapters) {
throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'janrainengage adapter only works with enableInsecureAuth: true');
}
return apiRequest(options.api_key, authData.auth_token).then(data => {
//successful response will have a "stat" (status) of 'ok' and a profile node with an identifier
//see: http://developers.janrain.com/overview/social-login/identity-providers/user-profile-data/#normalized-user-profile-data

View File

@@ -1,37 +1,70 @@
/*
# Parse Server Keycloak Authentication
## Keycloak `authData`
```
{
"keycloak": {
"access_token": "access token you got from keycloak JS client authentication",
"id": "the id retrieved from client authentication in Keycloak",
"roles": ["the roles retrieved from client authentication in Keycloak"],
"groups": ["the groups retrieved from client authentication in Keycloak"]
}
}
```
The authentication module will test if the authData is the same as the
userinfo oauth call, comparing the attributes
Copy the JSON config file generated on Keycloak (https://www.keycloak.org/docs/latest/securing_apps/index.html#_javascript_adapter)
and paste it inside of a folder (Ex.: `auth/keycloak.json`) in your server.
The options passed to Parse server:
```
{
auth: {
keycloak: {
config: require(`./auth/keycloak.json`)
}
}
}
```
*/
/**
* Parse Server authentication adapter for Keycloak.
*
* @class KeycloakAdapter
* @param {Object} options - The adapter configuration options.
* @param {Object} options.config - The Keycloak configuration object, typically loaded from a JSON file.
* @param {String} options.config.auth-server-url - The Keycloak authentication server URL.
* @param {String} options.config.realm - The Keycloak realm name.
* @param {String} options.config.client-id - The Keycloak client ID.
*
* @param {Object} authData - The authentication data provided by the client.
* @param {String} authData.access_token - The Keycloak access token retrieved during client authentication.
* @param {String} authData.id - The user ID retrieved from Keycloak during client authentication.
* @param {Array} [authData.roles] - The roles assigned to the user in Keycloak (optional).
* @param {Array} [authData.groups] - The groups assigned to the user in Keycloak (optional).
*
* @description
* ## Parse Server Configuration
* To configure Parse Server for Keycloak authentication, use the following structure:
* ```javascript
* {
* "auth": {
* "keycloak": {
* "config": require('./auth/keycloak.json')
* }
* }
* }
* ```
* Ensure the `keycloak.json` configuration file is generated from Keycloak's setup guide and includes:
* - `auth-server-url`: The Keycloak authentication server URL.
* - `realm`: The Keycloak realm name.
* - `client-id`: The Keycloak client ID.
*
* ## Auth Data
* The adapter requires the following `authData` fields:
* - `access_token`: The Keycloak access token retrieved during client authentication.
* - `id`: The user ID retrieved from Keycloak during client authentication.
* - `roles` (optional): The roles assigned to the user in Keycloak.
* - `groups` (optional): The groups assigned to the user in Keycloak.
*
* ## Auth Payload Example
* ### Example Auth Data
* ```json
* {
* "keycloak": {
* "access_token": "an authorized Keycloak access token for the user",
* "id": "user's Keycloak ID as a string",
* "roles": ["admin", "user"],
* "groups": ["group1", "group2"]
* }
* }
* ```
*
* ## Notes
* - Parse Server validates the provided `authData` by making a `userinfo` call to Keycloak and ensures the attributes match those returned by Keycloak.
*
* ## Keycloak Configuration
* To configure Keycloak, copy the JSON configuration file generated from Keycloak's setup guide:
* - [Keycloak Securing Apps Documentation](https://www.keycloak.org/docs/latest/securing_apps/index.html#_javascript_adapter)
*
* Place the configuration file on your server, for example:
* - `auth/keycloak.json`
*
* For more information on Keycloak authentication, see:
* - [Securing Apps Documentation](https://www.keycloak.org/docs/latest/securing_apps/)
* - [Server Administration Documentation](https://www.keycloak.org/docs/latest/server_admin/)
*/
const { Parse } = require('parse/node');
const httpsRequest = require('./httpsRequest');

View File

@@ -1,3 +1,78 @@
/**
* Parse Server authentication adapter for LDAP.
*
* @class LDAP
* @param {Object} options - The adapter configuration options.
* @param {String} options.url - The LDAP server URL. Must start with `ldap://` or `ldaps://`.
* @param {String} options.suffix - The LDAP suffix for user distinguished names (DN).
* @param {String} [options.dn] - The distinguished name (DN) template for user authentication. Replace `{{id}}` with the username.
* @param {Object} [options.tlsOptions] - Options for LDAPS TLS connections.
* @param {String} [options.groupCn] - The common name (CN) of the group to verify user membership.
* @param {String} [options.groupFilter] - The LDAP search filter for groups, with `{{id}}` replaced by the username.
*
* @param {Object} authData - The authentication data provided by the client.
* @param {String} authData.id - The user's LDAP username.
* @param {String} authData.password - The user's LDAP password.
*
* @description
* ## Parse Server Configuration
* To configure Parse Server for LDAP authentication, use the following structure:
* ```javascript
* {
* auth: {
* ldap: {
* url: 'ldaps://ldap.example.com',
* suffix: 'ou=users,dc=example,dc=com',
* groupCn: 'admins',
* groupFilter: '(memberUid={{id}})',
* tlsOptions: {
* rejectUnauthorized: false
* }
* }
* }
* }
* ```
*
* ## Authentication Process
* 1. Validates the provided `authData` using an LDAP bind operation.
* 2. Optionally, verifies that the user belongs to a specific group by performing an LDAP search using the provided `groupCn` or `groupFilter`.
*
* ## Auth Payload
* The adapter requires the following `authData` fields:
* - `id`: The user's LDAP username.
* - `password`: The user's LDAP password.
*
* ### Example Auth Payload
* ```json
* {
* "ldap": {
* "id": "jdoe",
* "password": "password123"
* }
* }
* ```
*
* @example <caption>Configuration Example</caption>
* // Example Parse Server configuration:
* const config = {
* auth: {
* ldap: {
* url: 'ldaps://ldap.example.com',
* suffix: 'ou=users,dc=example,dc=com',
* groupCn: 'admins',
* groupFilter: '(memberUid={{id}})',
* tlsOptions: {
* rejectUnauthorized: false
* }
* }
* }
* };
*
* @see {@link https://ldap.com/ LDAP Basics}
* @see {@link https://ldap.com/ldap-filters/ LDAP Filters}
*/
const ldapjs = require('ldapjs');
const Parse = require('parse/node').Parse;

View File

@@ -1,36 +1,143 @@
// Helper functions for accessing the line API.
var Parse = require('parse/node').Parse;
const httpsRequest = require('./httpsRequest');
/**
* Parse Server authentication adapter for Line.
*
* @class LineAdapter
* @param {Object} options - The adapter configuration options.
* @param {string} options.clientId - Your Line App Client ID. Required for secure authentication.
* @param {string} options.clientSecret - Your Line App Client 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 Line authentication, use the following structure:
* ### Secure Configuration
* ```json
* {
* "auth": {
* "line": {
* "clientId": "your-client-id",
* "clientSecret": "your-client-secret"
* }
* }
* }
* ```
* ### Insecure Configuration (Not Recommended)
* ```json
* {
* "auth": {
* "line": {
* "enableInsecureAuth": true
* }
* }
* }
* ```
*
* The adapter requires the following `authData` fields:
* - **Secure Authentication**: `code`, `redirect_uri`.
* - **Insecure Authentication (Not Recommended)**: `id`, `access_token`.
*
* ## Auth Payloads
* ### Secure Authentication Payload
* ```json
* {
* "line": {
* "code": "xxxxxxxxx",
* "redirect_uri": "https://example.com/callback"
* }
* }
* ```
*
* ### Insecure Authentication Payload (Not Recommended)
* ```json
* {
* "line": {
* "id": "1234567",
* "access_token": "xxxxxxxxx"
* }
* }
* ```
*
* ## Notes
* - `enableInsecureAuth` is **not recommended** and will be removed in future versions. Use secure authentication with `clientId` and `clientSecret`.
* - Secure authentication exchanges the `code` and `redirect_uri` provided by the client for an access token using Line's OAuth flow.
*
* @see {@link https://developers.line.biz/en/docs/line-login/integrate-line-login/ Line Login Documentation}
*/
// Returns a promise that fulfills if this user id is valid.
function validateAuthData(authData) {
return request('profile', authData.access_token).then(response => {
if (response && response.userId && response.userId === authData.id) {
return;
import BaseCodeAuthAdapter from './BaseCodeAuthAdapter';
class LineAdapter extends BaseCodeAuthAdapter {
constructor() {
super('Line');
}
async getAccessTokenFromCode(authData) {
if (!authData.code) {
throw new Parse.Error(
Parse.Error.OBJECT_NOT_FOUND,
'Line auth is invalid for this user.'
);
}
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Line auth is invalid for this user.');
});
const tokenUrl = 'https://api.line.me/oauth2/v2.1/token';
const response = await fetch(tokenUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
client_id: this.clientId,
client_secret: this.clientSecret,
grant_type: 'authorization_code',
redirect_uri: authData.redirect_uri,
code: authData.code,
}),
});
if (!response.ok) {
throw new Parse.Error(
Parse.Error.OBJECT_NOT_FOUND,
`Failed to exchange code for token: ${response.statusText}`
);
}
const data = await response.json();
if (data.error) {
throw new Parse.Error(
Parse.Error.OBJECT_NOT_FOUND,
data.error_description || data.error
);
}
return data.access_token;
}
async getUserFromAccessToken(accessToken) {
const userApiUrl = 'https://api.line.me/v2/profile';
const response = await fetch(userApiUrl, {
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
if (!response.ok) {
throw new Parse.Error(
Parse.Error.OBJECT_NOT_FOUND,
`Failed to fetch Line user: ${response.statusText}`
);
}
const userData = await response.json();
if (!userData?.userId) {
throw new Parse.Error(
Parse.Error.VALIDATION_ERROR,
'Invalid Line user data received.'
);
}
return userData;
}
}
// Returns a promise that fulfills iff this app id is valid.
function validateAppId() {
return Promise.resolve();
}
// A promisey wrapper for api requests
function request(path, access_token) {
var options = {
host: 'api.line.me',
path: '/v2/' + path,
method: 'GET',
headers: {
Authorization: 'Bearer ' + access_token,
},
};
return httpsRequest.get(options);
}
module.exports = {
validateAppId: validateAppId,
validateAuthData: validateAuthData,
};
export default new LineAdapter();

View File

@@ -1,40 +1,115 @@
// Helper functions for accessing the linkedin API.
var Parse = require('parse/node').Parse;
const httpsRequest = require('./httpsRequest');
/**
* Parse Server authentication adapter for LinkedIn.
*
* @class LinkedInAdapter
* @param {Object} options - The adapter configuration options.
* @param {string} options.clientId - Your LinkedIn App Client ID. Required for secure authentication.
* @param {string} options.clientSecret - Your LinkedIn App Client 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 LinkedIn authentication, use the following structure:
* ### Secure Configuration
* ```json
* {
* "auth": {
* "linkedin": {
* "clientId": "your-client-id",
* "clientSecret": "your-client-secret"
* }
* }
* }
* ```
* ### Insecure Configuration (Not Recommended)
* ```json
* {
* "auth": {
* "linkedin": {
* "enableInsecureAuth": true
* }
* }
* }
* ```
*
* The adapter requires the following `authData` fields:
* - **Secure Authentication**: `code`, `redirect_uri`, and optionally `is_mobile_sdk`.
* - **Insecure Authentication (Not Recommended)**: `id`, `access_token`, and optionally `is_mobile_sdk`.
*
* ## Auth Payloads
* ### Secure Authentication Payload
* ```json
* {
* "linkedin": {
* "code": "lmn789opq012rst345uvw",
* "redirect_uri": "https://your-redirect-uri.com/callback",
* "is_mobile_sdk": true
* }
* }
* ```
*
* ### Insecure Authentication Payload (Not Recommended)
* ```json
* {
* "linkedin": {
* "id": "7654321",
* "access_token": "AQXNnd2hIT6z9bHFzZz2Kp1ghiMz_RtyuvwXYZ123abc",
* "is_mobile_sdk": true
* }
* }
* ```
*
* ## Notes
* - Secure authentication exchanges the `code` and `redirect_uri` provided by the client for an access token using LinkedIn's OAuth API.
* - Insecure authentication validates the user ID and access token directly, bypassing OAuth flows. This method is **not recommended** and may introduce security vulnerabilities.
* - `enableInsecureAuth` is **deprecated** and may be removed in future versions.
*
* @see {@link https://learn.microsoft.com/en-us/linkedin/shared/authentication/authentication LinkedIn Authentication Documentation}
*/
// Returns a promise that fulfills iff this user id is valid.
function validateAuthData(authData) {
return request('me', authData.access_token, authData.is_mobile_sdk).then(data => {
if (data && data.id == authData.id) {
return;
}
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Linkedin auth is invalid for this user.');
});
}
// Returns a promise that fulfills iff this app id is valid.
function validateAppId() {
return Promise.resolve();
}
// A promisey wrapper for api requests
function request(path, access_token, is_mobile_sdk) {
var headers = {
Authorization: 'Bearer ' + access_token,
'x-li-format': 'json',
};
if (is_mobile_sdk) {
headers['x-li-src'] = 'msdk';
import BaseAuthCodeAdapter from './BaseCodeAuthAdapter';
class LinkedInAdapter extends BaseAuthCodeAdapter {
constructor() {
super('LinkedIn');
}
async getUserFromAccessToken(access_token, authData) {
const response = await fetch('https://api.linkedin.com/v2/me', {
headers: {
Authorization: `Bearer ${access_token}`,
'x-li-format': 'json',
'x-li-src': authData?.is_mobile_sdk ? 'msdk' : undefined,
},
});
if (!response.ok) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'LinkedIn API request failed.');
}
return response.json();
}
async getAccessTokenFromCode(authData) {
const response = await fetch('https://www.linkedin.com/oauth/v2/accessToken', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code: authData.code,
redirect_uri: authData.redirect_uri,
client_id: this.clientId,
client_secret: this.clientSecret,
}),
});
if (!response.ok) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'LinkedIn API request failed.');
}
const json = await response.json();
return json.access_token;
}
return httpsRequest.get({
host: 'api.linkedin.com',
path: '/v2/' + path,
headers: headers,
});
}
module.exports = {
validateAppId: validateAppId,
validateAuthData: validateAuthData,
};
export default new LinkedInAdapter();

View File

@@ -1,15 +1,24 @@
// Helper functions for accessing the meetup API.
var Parse = require('parse/node').Parse;
const httpsRequest = require('./httpsRequest');
import Config from '../../Config';
import Deprecator from '../../Deprecator/Deprecator';
// Returns a promise that fulfills iff this user id is valid.
function validateAuthData(authData) {
return request('member/self', authData.access_token).then(data => {
if (data && data.id == authData.id) {
return;
}
async function validateAuthData(authData) {
const config = Config.get(Parse.applicationId);
const meetupConfig = config.auth.meetup;
Deprecator.logRuntimeDeprecation({ usage: 'meetup adapter' });
if (!meetupConfig?.enableInsecureAuth) {
throw new Parse.Error('Meetup only works with enableInsecureAuth: true');
}
const data = await request('member/self', authData.access_token);
if (data?.id !== authData.id) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Meetup auth is invalid for this user.');
});
}
}
// Returns a promise that fulfills iff this app id is valid.

View File

@@ -1,3 +1,81 @@
/**
* Parse Server authentication adapter for Multi-Factor Authentication (MFA).
*
* @class MFAAdapter
* @param {Object} options - The adapter options.
* @param {Array<String>} options.options - Supported MFA methods. Must include `"SMS"` or `"TOTP"`.
* @param {Number} [options.digits=6] - The number of digits for the one-time password (OTP). Must be between 4 and 10.
* @param {Number} [options.period=30] - The validity period of the OTP in seconds. Must be greater than 10.
* @param {String} [options.algorithm="SHA1"] - The algorithm used for TOTP generation. Defaults to `"SHA1"`.
* @param {Function} [options.sendSMS] - A callback function for sending SMS OTPs. Required if `"SMS"` is included in `options`.
*
* @description
* ## Parse Server Configuration
* To configure Parse Server for MFA, use the following structure:
* ```javascript
* {
* auth: {
* mfa: {
* options: ["SMS", "TOTP"],
* digits: 6,
* period: 30,
* algorithm: "SHA1",
* sendSMS: (token, mobile) => {
* // Send the SMS using your preferred SMS provider.
* console.log(`Sending SMS to ${mobile} with token: ${token}`);
* }
* }
* }
* }
* ```
*
* ## MFA Methods
* - **SMS**:
* - Requires a valid mobile number.
* - Sends a one-time password (OTP) via SMS for login or verification.
* - Uses the `sendSMS` callback for sending the OTP.
*
* - **TOTP**:
* - Requires a secret key for setup.
* - Validates the user's OTP against a time-based one-time password (TOTP) generated using the secret key.
* - Supports configurable digits, period, and algorithm for TOTP generation.
*
* ## MFA Payload
* The adapter requires the following `authData` fields:
* - **For SMS-based MFA**:
* - `mobile`: The user's mobile number (required for setup).
* - `token`: The OTP provided by the user for login or verification.
* - **For TOTP-based MFA**:
* - `secret`: The TOTP secret key for the user (required for setup).
* - `token`: The OTP provided by the user for login or verification.
*
* ## Example Payloads
* ### SMS Setup Payload
* ```json
* {
* "mobile": "+1234567890"
* }
* ```
*
* ### TOTP Setup Payload
* ```json
* {
* "secret": "BASE32ENCODEDSECRET",
* "token": "123456"
* }
* ```
*
* ### Login Payload
* ```json
* {
* "token": "123456"
* }
* ```
*
* @see {@link https://en.wikipedia.org/wiki/Time-based_One-Time_Password_algorithm Time-based One-Time Password Algorithm (TOTP)}
* @see {@link https://tools.ietf.org/html/rfc6238 RFC 6238: TOTP: Time-Based One-Time Password Algorithm}
*/
import { TOTP, Secret } from 'otpauth';
import { randomString } from '../../cryptoUtils';
import AuthAdapter from './AuthAdapter';
@@ -113,7 +191,7 @@ class MFAAdapter extends AuthAdapter {
}
throw 'Invalid MFA data';
}
afterFind(req, authData) {
afterFind(authData, options, req) {
if (req.master) {
return;
}

View File

@@ -1,37 +1,109 @@
// Helper functions for accessing the microsoft graph API.
var Parse = require('parse/node').Parse;
const httpsRequest = require('./httpsRequest');
/**
* Parse Server authentication adapter for Microsoft.
*
* @class MicrosoftAdapter
* @param {Object} options - The adapter configuration options.
* @param {string} options.clientId - Your Microsoft App Client ID. Required for secure authentication.
* @param {string} options.clientSecret - Your Microsoft App Client 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 Microsoft authentication, use the following structure:
* ### Secure Configuration
* ```json
* {
* "auth": {
* "microsoft": {
* "clientId": "your-client-id",
* "clientSecret": "your-client-secret"
* }
* }
* }
* ```
* ### Insecure Configuration (Not Recommended)
* ```json
* {
* "auth": {
* "microsoft": {
* "enableInsecureAuth": true
* }
* }
* }
* ```
*
* The adapter requires the following `authData` fields:
* - **Secure Authentication**: `code`, `redirect_uri`.
* - **Insecure Authentication (Not Recommended)**: `id`, `access_token`.
*
* ## Auth Payloads
* ### Secure Authentication Payload
* ```json
* {
* "microsoft": {
* "code": "lmn789opq012rst345uvw",
* "redirect_uri": "https://your-redirect-uri.com/callback"
* }
* }
* ```
* ### Insecure Authentication Payload (Not Recommended)
* ```json
* {
* "microsoft": {
* "id": "7654321",
* "access_token": "AQXNnd2hIT6z9bHFzZz2Kp1ghiMz_RtyuvwXYZ123abc"
* }
* }
* ```
*
* ## Notes
* - Secure authentication exchanges the `code` and `redirect_uri` provided by the client for an access token using Microsoft's OAuth API.
* - **Insecure authentication** validates the user ID and access token directly, bypassing OAuth flows (not recommended). This method is deprecated and may be removed in future versions.
*
* @see {@link https://docs.microsoft.com/en-us/graph/auth/auth-concepts Microsoft Authentication Documentation}
*/
// Returns a promise that fulfills if this user mail is valid.
function validateAuthData(authData) {
return request('me', authData.access_token).then(response => {
if (response && response.id && response.id == authData.id) {
return;
import BaseAuthCodeAdapter from './BaseCodeAuthAdapter';
class MicrosoftAdapter extends BaseAuthCodeAdapter {
constructor() {
super('Microsoft');
}
async getUserFromAccessToken(access_token) {
const userResponse = await fetch('https://graph.microsoft.com/v1.0/me', {
headers: {
Authorization: 'Bearer ' + access_token,
},
});
if (!userResponse.ok) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Microsoft API request failed.');
}
throw new Parse.Error(
Parse.Error.OBJECT_NOT_FOUND,
'Microsoft Graph auth is invalid for this user.'
);
});
return userResponse.json();
}
async getAccessTokenFromCode(authData) {
const response = await fetch('https://login.microsoftonline.com/common/oauth2/v2.0/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
client_id: this.clientId,
client_secret: this.clientSecret,
grant_type: 'authorization_code',
redirect_uri: authData.redirect_uri,
code: authData.code,
}),
});
if (!response.ok) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Microsoft API request failed.');
}
const json = await response.json();
return json.access_token;
}
}
// Returns a promise that fulfills if this app id is valid.
function validateAppId() {
return Promise.resolve();
}
// A promisey wrapper for api requests
function request(path, access_token) {
return httpsRequest.get({
host: 'graph.microsoft.com',
path: '/v1.0/' + path,
headers: {
Authorization: 'Bearer ' + access_token,
},
});
}
module.exports = {
validateAppId: validateAppId,
validateAuthData: validateAuthData,
};
export default new MicrosoftAdapter();

View File

@@ -1,137 +1,121 @@
/*
* This auth adapter is based on the OAuth 2.0 Token Introspection specification.
* See RFC 7662 for details (https://tools.ietf.org/html/rfc7662).
* It's purpose is to validate OAuth2 access tokens using the OAuth2 provider's
* token introspection endpoint (if implemented by the provider).
/**
* Parse Server authentication adapter for OAuth2 Token Introspection.
*
* The adapter accepts the following config parameters:
*
* 1. "tokenIntrospectionEndpointUrl" (string, required)
* The URL of the token introspection endpoint of the OAuth2 provider that
* issued the access token to the client that is to be validated.
*
* 2. "useridField" (string, optional)
* The name of the field in the token introspection response that contains
* the userid. If specified, it will be used to verify the value of the "id"
* field in the "authData" JSON that is coming from the client.
* This can be the "aud" (i.e. audience), the "sub" (i.e. subject) or the
* "username" field in the introspection response, but since only the
* "active" field is required and all other reponse fields are optional
* in the RFC, it has to be optional in this adapter as well.
* Default: - (undefined)
*
* 3. "appidField" (string, optional)
* The name of the field in the token introspection response that contains
* the appId of the client. If specified, it will be used to verify it's
* value against the set of appIds in the adapter config. The concept of
* appIds comes from the two major social login providers
* (Google and Facebook). They have not yet implemented the token
* introspection endpoint, but the concept can be valid for any OAuth2
* provider.
* Default: - (undefined)
*
* 4. "appIds" (array of strings, required if appidField is defined)
* A set of appIds that are used to restrict accepted access tokens based
* on a specific field's value in the token introspection response.
* Default: - (undefined)
*
* 5. "authorizationHeader" (string, optional)
* The value of the "Authorization" HTTP header in requests sent to the
* introspection endpoint. It must contain the raw value.
* Thus if HTTP Basic authorization is to be used, it must contain the
* "Basic" string, followed by whitespace, then by the base64 encoded
* version of the concatenated <username> + ":" + <password> string.
* Eg. "Basic dXNlcm5hbWU6cGFzc3dvcmQ="
*
* The adapter expects requests with the following authData JSON:
* @class OAuth2Adapter
* @param {Object} options - The adapter configuration options.
* @param {string} options.tokenIntrospectionEndpointUrl - The URL of the token introspection endpoint. Required.
* @param {boolean} options.oauth2 - Indicates that the request should be handled by the OAuth2 adapter. Required.
* @param {string} [options.useridField] - The field in the introspection response that contains the user ID. Optional.
* @param {string} [options.appidField] - The field in the introspection response that contains the app ID. Optional.
* @param {string[]} [options.appIds] - List of allowed app IDs. Required if `appidField` is defined.
* @param {string} [options.authorizationHeader] - The Authorization header value for the introspection request. Optional.
*
* @description
* ## Parse Server Configuration
* To configure Parse Server for OAuth2 Token Introspection, use the following structure:
* ```json
* {
* "someadapter": {
* "id": "user's OAuth2 provider-specific id as a string",
* "access_token": "an authorized OAuth2 access token for the user",
* "auth": {
* "oauth2Provider": {
* "tokenIntrospectionEndpointUrl": "https://provider.com/introspect",
* "useridField": "sub",
* "appidField": "aud",
* "appIds": ["my-app-id"],
* "authorizationHeader": "Basic dXNlcm5hbWU6cGFzc3dvcmQ=",
* "oauth2": true
* }
* }
* }
* ```
*
* The adapter requires the following `authData` fields:
* - `id`: The user ID provided by the client.
* - `access_token`: The access token provided by the client.
*
* ## Auth Payload
* ### Example Auth Payload
* ```json
* {
* "oauth2": {
* "id": "user-id",
* "access_token": "access-token"
* }
* }
* ```
*
* ## Notes
* - `tokenIntrospectionEndpointUrl` is mandatory and should point to a valid OAuth2 provider's introspection endpoint.
* - If `appidField` is defined, `appIds` must also be specified to validate the app ID in the introspection response.
* - `authorizationHeader` can be used to authenticate requests to the token introspection endpoint.
*
* @see {@link https://datatracker.ietf.org/doc/html/rfc7662 OAuth 2.0 Token Introspection Specification}
*/
const Parse = require('parse/node').Parse;
const querystring = require('querystring');
const httpsRequest = require('./httpsRequest');
const INVALID_ACCESS = 'OAuth2 access token is invalid for this user.';
const INVALID_ACCESS_APPID =
"OAuth2: the access_token's appID is empty or is not in the list of permitted appIDs in the auth configuration.";
const MISSING_APPIDS =
'OAuth2 configuration is missing the client app IDs ("appIds" config parameter).';
const MISSING_URL = 'OAuth2 token introspection endpoint URL is missing from configuration!';
import AuthAdapter from './AuthAdapter';
// Returns a promise that fulfills if this user id is valid.
function validateAuthData(authData, options) {
return requestTokenInfo(options, authData.access_token).then(response => {
if (
!response ||
!response.active ||
(options.useridField && authData.id !== response[options.useridField])
) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, INVALID_ACCESS);
class OAuth2Adapter extends AuthAdapter {
validateOptions(options) {
super.validateOptions(options);
if (!options.tokenIntrospectionEndpointUrl) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'OAuth2 token introspection endpoint URL is missing.');
}
if (options.appidField && !options.appIds?.length) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'OAuth2 configuration is missing app IDs.');
}
});
}
function validateAppId(appIds, authData, options) {
if (!options || !options.appidField) {
return Promise.resolve();
this.tokenIntrospectionEndpointUrl = options.tokenIntrospectionEndpointUrl;
this.useridField = options.useridField;
this.appidField = options.appidField;
this.appIds = options.appIds;
this.authorizationHeader = options.authorizationHeader;
}
if (!appIds || appIds.length === 0) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, MISSING_APPIDS);
}
return requestTokenInfo(options, authData.access_token).then(response => {
if (!response || !response.active) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, INVALID_ACCESS);
}
const appidField = options.appidField;
if (!response[appidField]) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, INVALID_ACCESS_APPID);
}
const responseValue = response[appidField];
if (!Array.isArray(responseValue) && appIds.includes(responseValue)) {
async validateAppId(authData) {
if (!this.appidField) {
return;
} else if (
Array.isArray(responseValue) &&
responseValue.some(appId => appIds.includes(appId))
) {
return;
} else {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, INVALID_ACCESS_APPID);
}
});
const response = await this.requestTokenInfo(authData.access_token);
const appIdFieldValue = response[this.appidField];
const isValidAppId = Array.isArray(appIdFieldValue)
? appIdFieldValue.some(appId => this.appIds.includes(appId))
: this.appIds.includes(appIdFieldValue);
if (!isValidAppId) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'OAuth2: Invalid app ID.');
}
}
async validateAuthData(authData) {
const response = await this.requestTokenInfo(authData.access_token);
if (!response.active || (this.useridField && authData.id !== response[this.useridField])) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'OAuth2 access token is invalid for this user.');
}
return {};
}
async requestTokenInfo(accessToken) {
const response = await fetch(this.tokenIntrospectionEndpointUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
...(this.authorizationHeader && { Authorization: this.authorizationHeader })
},
body: new URLSearchParams({ token: accessToken })
});
if (!response.ok) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'OAuth2 token introspection request failed.');
}
return response.json();
}
}
// A promise wrapper for requests to the OAuth2 token introspection endpoint.
function requestTokenInfo(options, access_token) {
if (!options || !options.tokenIntrospectionEndpointUrl) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, MISSING_URL);
}
const parsedUrl = new URL(options.tokenIntrospectionEndpointUrl);
const postData = querystring.stringify({
token: access_token,
});
const headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': Buffer.byteLength(postData),
};
if (options.authorizationHeader) {
headers['Authorization'] = options.authorizationHeader;
}
const postOptions = {
hostname: parsedUrl.hostname,
path: parsedUrl.pathname,
method: 'POST',
headers: headers,
};
return httpsRequest.request(postOptions, postData);
}
export default new OAuth2Adapter();
module.exports = {
validateAppId: validateAppId,
validateAuthData: validateAuthData,
};

View File

@@ -7,15 +7,24 @@
const { Parse } = require('parse/node');
const httpsRequest = require('./httpsRequest');
import Config from '../../Config';
import Deprecator from '../../Deprecator/Deprecator';
// Returns a promise that fulfills if this user id is valid.
function validateAuthData(authData) {
return request('auth/userinfo', authData.access_token).then(data => {
if (data && data.sub == authData.id) {
return;
}
async function validateAuthData(authData) {
const config = Config.get(Parse.applicationId);
Deprecator.logRuntimeDeprecation({ usage: 'phantauth adapter' });
const phantauthConfig = config.auth.phantauth;
if (!phantauthConfig?.enableInsecureAuth) {
throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'PhantAuth only works with enableInsecureAuth: true');
}
const data = await request('auth/userinfo', authData.access_token);
if (data?.sub !== authData.id) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'PhantAuth auth is invalid for this user.');
});
}
}
// Returns a promise that fulfills if this app id is valid.

View File

@@ -1,41 +1,112 @@
// Helper functions for accessing the qq Graph API.
const httpsRequest = require('./httpsRequest');
var Parse = require('parse/node').Parse;
/**
* Parse Server authentication adapter for QQ.
*
* @class QqAdapter
* @param {Object} options - The adapter configuration options.
* @param {string} options.clientId - Your QQ App ID. Required for secure authentication.
* @param {string} options.clientSecret - Your QQ App 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 QQ authentication, use the following structure:
* ### Secure Configuration
* ```json
* {
* "auth": {
* "qq": {
* "clientId": "your-app-id",
* "clientSecret": "your-app-secret"
* }
* }
* }
* ```
* ### Insecure Configuration (Not Recommended)
* ```json
* {
* "auth": {
* "qq": {
* "enableInsecureAuth": true
* }
* }
* }
* ```
*
* The adapter requires the following `authData` fields:
* - **Secure Authentication**: `code`, `redirect_uri`.
* - **Insecure Authentication (Not Recommended)**: `id`, `access_token`.
*
* ## Auth Payloads
* ### Secure Authentication Payload
* ```json
* {
* "qq": {
* "code": "abcd1234",
* "redirect_uri": "https://your-redirect-uri.com/callback"
* }
* }
* ```
* ### Insecure Authentication Payload (Not Recommended)
* ```json
* {
* "qq": {
* "id": "1234567",
* "access_token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
* }
* }
* ```
*
* ## Notes
* - Secure authentication exchanges the `code` and `redirect_uri` provided by the client for an access token using QQ's OAuth API.
* - **Insecure authentication** validates the `id` and `access_token` directly, bypassing OAuth flows. This approach is not recommended and may be deprecated in future versions.
*
* @see {@link https://wiki.connect.qq.com/ QQ Authentication Documentation}
*/
// Returns a promise that fulfills iff this user id is valid.
function validateAuthData(authData) {
return graphRequest('me?access_token=' + authData.access_token).then(function (data) {
if (data && data.openid == authData.id) {
return;
}
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'qq auth is invalid for this user.');
});
}
// Returns a promise that fulfills if this app id is valid.
function validateAppId() {
return Promise.resolve();
}
// A promisey wrapper for qq graph requests.
function graphRequest(path) {
return httpsRequest.get('https://graph.qq.com/oauth2.0/' + path, true).then(data => {
return parseResponseData(data);
});
}
function parseResponseData(data) {
const starPos = data.indexOf('(');
const endPos = data.indexOf(')');
if (starPos == -1 || endPos == -1) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'qq auth is invalid for this user.');
import BaseAuthCodeAdapter from './BaseCodeAuthAdapter';
class QqAdapter extends BaseAuthCodeAdapter {
constructor() {
super('qq');
}
async getUserFromAccessToken(access_token) {
const response = await fetch('https://graph.qq.com/oauth2.0/me', {
headers: {
Authorization: `Bearer ${access_token}`,
},
});
if (!response.ok) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'qq API request failed.');
}
const data = await response.text();
return this.parseResponseData(data);
}
async getAccessTokenFromCode(authData) {
const response = await fetch('https://graph.qq.com/oauth2.0/token', {
method: 'GET',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: this.clientId,
client_secret: this.clientSecret,
redirect_uri: authData.redirect_uri,
code: authData.code,
}).toString(),
});
if (!response.ok) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'qq API request failed.');
}
const text = await response.text();
const data = this.parseResponseData(text);
return data.access_token;
}
data = data.substring(starPos + 1, endPos - 1);
return JSON.parse(data);
}
module.exports = {
validateAppId,
validateAuthData,
parseResponseData,
};
export default new QqAdapter();

View File

@@ -1,44 +1,118 @@
// Helper functions for accessing the Spotify API.
const httpsRequest = require('./httpsRequest');
var Parse = require('parse/node').Parse;
/**
* Parse Server authentication adapter for Spotify.
*
* @class SpotifyAdapter
* @param {Object} options - The adapter configuration options.
* @param {string} options.clientId - Your Spotify application's Client ID. Required for secure authentication.
* @param {boolean} [options.enableInsecureAuth=false] - **[DEPRECATED]** Enable insecure authentication (not recommended).
*
* @description
* ## Parse Server Configuration
* To configure Parse Server for Spotify authentication, use the following structure:
* ### Secure Configuration
* ```json
* {
* "auth": {
* "spotify": {
* "clientId": "your-client-id"
* }
* }
* }
* ```
* ### Insecure Configuration (Not Recommended)
* ```json
* {
* "auth": {
* "spotify": {
* "enableInsecureAuth": true
* }
* }
* }
* ```
*
* The adapter requires the following `authData` fields:
* - **Secure Authentication**: `code`, `redirect_uri`, and `code_verifier`.
* - **Insecure Authentication (Not Recommended)**: `id`, `access_token`.
*
* ## Auth Payloads
* ### Secure Authentication Payload
* ```json
* {
* "spotify": {
* "code": "abc123def456ghi789",
* "redirect_uri": "https://example.com/callback",
* "code_verifier": "secure-code-verifier"
* }
* }
* ```
* ### Insecure Authentication Payload (Not Recommended)
* ```json
* {
* "spotify": {
* "id": "1234567",
* "access_token": "abc123def456ghi789"
* }
* }
* ```
*
* ## Notes
* - `enableInsecureAuth` is **not recommended** and bypasses secure flows by validating the user ID and access token directly. This method is not suitable for production environments and may be removed in future versions.
* - Secure authentication exchanges the `code` provided by the client for an access token using Spotify's OAuth API. This method ensures greater security and is the recommended approach.
*
* @see {@link https://developer.spotify.com/documentation/web-api/tutorials/getting-started Spotify OAuth Documentation}
*/
// Returns a promise that fulfills iff this user id is valid.
function validateAuthData(authData) {
return request('me', authData.access_token).then(data => {
if (data && data.id == authData.id) {
return;
import BaseAuthCodeAdapter from './BaseCodeAuthAdapter';
class SpotifyAdapter extends BaseAuthCodeAdapter {
constructor() {
super('spotify');
}
async getUserFromAccessToken(access_token) {
const response = await fetch('https://api.spotify.com/v1/me', {
headers: {
Authorization: 'Bearer ' + access_token,
},
});
if (!response.ok) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Spotify API request failed.');
}
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Spotify auth is invalid for this user.');
});
}
// Returns a promise that fulfills if this app id is valid.
async function validateAppId(appIds, authData) {
const access_token = authData.access_token;
if (!Array.isArray(appIds)) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'appIds must be an array.');
const user = await response.json();
return {
id: user.id,
};
}
if (!appIds.length) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Spotify auth is not configured.');
}
const data = await request('me', access_token);
if (!data || !appIds.includes(data.id)) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Spotify auth is invalid for this user.');
async getAccessTokenFromCode(authData) {
if (!authData.code || !authData.redirect_uri || !authData.code_verifier) {
throw new Parse.Error(
Parse.Error.OBJECT_NOT_FOUND,
'Spotify auth configuration authData.code and/or authData.redirect_uri and/or authData.code_verifier.'
);
}
const response = await fetch('https://accounts.spotify.com/api/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code: authData.code,
redirect_uri: authData.redirect_uri,
code_verifier: authData.code_verifier,
client_id: this.clientId,
}),
});
if (!response.ok) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Spotify API request failed.');
}
return response.json();
}
}
// A promisey wrapper for Spotify API requests.
function request(path, access_token) {
return httpsRequest.get({
host: 'api.spotify.com',
path: '/v1/' + path,
headers: {
Authorization: 'Bearer ' + access_token,
},
});
}
module.exports = {
validateAppId: validateAppId,
validateAuthData: validateAuthData,
};
export default new SpotifyAdapter();

View File

@@ -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();

View File

@@ -4,28 +4,32 @@
const httpsRequest = require('./httpsRequest');
var Parse = require('parse/node').Parse;
import Config from '../../Config';
import Deprecator from '../../Deprecator/Deprecator';
// Returns a promise that fulfills iff this user id is valid.
function validateAuthData(authData, params) {
return vkOAuth2Request(params).then(function (response) {
if (response && response.access_token) {
return request(
'api.vk.com',
'method/users.get?access_token=' + authData.access_token + '&v=' + params.apiVersion
).then(function (response) {
if (
response &&
response.response &&
response.response.length &&
response.response[0].id == authData.id
) {
return;
}
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Vk auth is invalid for this user.');
});
}
async function validateAuthData(authData, params) {
const config = Config.get(Parse.applicationId);
Deprecator.logRuntimeDeprecation({ usage: 'vkontakte adapter' });
const vkConfig = config.auth.vkontakte;
if (!vkConfig?.enableInsecureAuth || !config.enableInsecureAuthAdapters) {
throw new Parse.Error('Vk only works with enableInsecureAuth: true');
}
const response = await vkOAuth2Request(params);
if (!response?.access_token) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Vk appIds or appSecret is incorrect.');
});
}
const vkUser = await request(
'api.vk.com',
`method/users.get?access_token=${authData.access_token}&v=${params.apiVersion}`
);
if (!vkUser?.response?.length || vkUser.response[0].id !== authData.id) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Vk auth is invalid for this user.');
}
}
function vkOAuth2Request(params) {

View File

@@ -1,30 +1,120 @@
// Helper functions for accessing the WeChat Graph API.
const httpsRequest = require('./httpsRequest');
var Parse = require('parse/node').Parse;
/**
* Parse Server authentication adapter for WeChat.
*
* @class WeChatAdapter
* @param {Object} options - The adapter options object.
* @param {boolean} [options.enableInsecureAuth=false] - **[DEPRECATED]** Enable insecure authentication (not recommended).
* @param {string} options.clientId - Your WeChat App ID.
* @param {string} options.clientSecret - Your WeChat App Secret.
*
* @description
* ## Parse Server Configuration
* To configure Parse Server for WeChat authentication, use the following structure:
* ### Secure Configuration (Recommended)
* ```json
* {
* "auth": {
* "wechat": {
* "clientId": "your-client-id",
* "clientSecret": "your-client-secret"
* }
* }
* }
* ```
* ### Insecure Configuration (Not Recommended)
* ```json
* {
* "auth": {
* "wechat": {
* "enableInsecureAuth": true
* }
* }
* }
* ```
*
* The adapter requires the following `authData` fields:
* - **With `enableInsecureAuth` (Not Recommended)**: `id`, `access_token`.
* - **Without `enableInsecureAuth`**: `code`.
*
* ## Auth Payloads
* ### Secure Authentication Payload (Recommended)
* ```json
* {
* "wechat": {
* "code": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
* }
* }
* ```
* ### Insecure Authentication Payload (Not Recommended)
* ```json
* {
* "wechat": {
* "id": "1234567",
* "access_token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
* }
* }
* ```
*
* ## Notes
* - With `enableInsecureAuth`, the adapter directly validates the `id` and `access_token` sent by the client.
* - Without `enableInsecureAuth`, the adapter uses the `code` provided by the client to exchange for an access token via WeChat's OAuth API.
* - The `enableInsecureAuth` flag is **deprecated** and may be removed in future versions. Use secure authentication with the `code` field instead.
*
* @example <caption>Auth Data Example</caption>
* // Example authData provided by the client:
* const authData = {
* wechat: {
* code: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
* }
* };
*
* @see {@link https://developers.weixin.qq.com/doc/offiaccount/en/OA_Web_Apps/Wechat_webpage_authorization.html WeChat Authentication Documentation}
*/
// Returns a promise that fulfills iff this user id is valid.
function validateAuthData(authData) {
return graphRequest('auth?access_token=' + authData.access_token + '&openid=' + authData.id).then(
function (data) {
if (data.errcode == 0) {
return;
}
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'wechat auth is invalid for this user.');
import BaseAuthCodeAdapter from './BaseCodeAuthAdapter';
class WeChatAdapter extends BaseAuthCodeAdapter {
constructor() {
super('WeChat');
}
async getUserFromAccessToken(access_token, authData) {
const response = await fetch(
`https://api.weixin.qq.com/sns/auth?access_token=${access_token}&openid=${authData.id}`
);
const data = await response.json();
if (!response.ok || data.errcode !== 0) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'WeChat auth is invalid for this user.');
}
);
return data;
}
async getAccessTokenFromCode(authData) {
if (!authData.code) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'WeChat auth requires a code to be sent.');
}
const appId = this.clientId;
const appSecret = this.clientSecret
const response = await fetch(
`https://api.weixin.qq.com/sns/oauth2/access_token?appid=${appId}&secret=${appSecret}&code=${authData.code}&grant_type=authorization_code`
);
const data = await response.json();
if (!response.ok || data.errcode) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'WeChat auth is invalid for this user.');
}
authData.id = data.openid;
return data.access_token;
}
}
// Returns a promise that fulfills if this app id is valid.
function validateAppId() {
return Promise.resolve();
}
// A promisey wrapper for WeChat graph requests.
function graphRequest(path) {
return httpsRequest.get('https://api.weixin.qq.com/sns/' + path);
}
module.exports = {
validateAppId,
validateAuthData,
};
export default new WeChatAdapter();

View File

@@ -1,41 +1,149 @@
// Helper functions for accessing the weibo Graph API.
var httpsRequest = require('./httpsRequest');
var Parse = require('parse/node').Parse;
var querystring = require('querystring');
/**
* Parse Server authentication adapter for Weibo.
*
* @class WeiboAdapter
* @param {Object} options - The adapter configuration options.
* @param {boolean} [options.enableInsecureAuth=false] - **[DEPRECATED]** Enable insecure authentication (not recommended).
* @param {string} options.clientId - Your Weibo client ID.
* @param {string} options.clientSecret - Your Weibo client secret.
*
* @description
* ## Parse Server Configuration
* To configure Parse Server for Weibo authentication, use the following structure:
* ### Secure Configuration
* ```json
* {
* "auth": {
* "weibo": {
* "clientId": "your-client-id",
* "clientSecret": "your-client-secret"
* }
* }
* }
* ```
* ### Insecure Configuration (Not Recommended)
* ```json
* {
* "auth": {
* "weibo": {
* "enableInsecureAuth": true
* }
* }
* }
* ```
*
* The adapter requires the following `authData` fields:
* - **Secure Authentication**: `code`, `redirect_uri`.
* - **Insecure Authentication (Not Recommended)**: `id`, `access_token`.
*
* ## Auth Payloads
* ### Secure Authentication Payload
* ```json
* {
* "weibo": {
* "code": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
* "redirect_uri": "https://example.com/callback"
* }
* }
* ```
* ### Insecure Authentication Payload (Not Recommended)
* ```json
* {
* "weibo": {
* "id": "1234567",
* "access_token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
* }
* }
* ```
*
* ## Notes
* - **Insecure Authentication**: When `enableInsecureAuth` is enabled, the adapter directly validates the `id` and `access_token` provided by the client.
* - **Secure Authentication**: When `enableInsecureAuth` is disabled, the adapter exchanges the `code` and `redirect_uri` for an access token using Weibo's OAuth API.
* - `enableInsecureAuth` is **deprecated** and may be removed in future versions. Use secure authentication with `code` and `redirect_uri`.
*
* @example <caption>Auth Data Example (Secure)</caption>
* const authData = {
* weibo: {
* code: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
* redirect_uri: "https://example.com/callback"
* }
* };
*
* @example <caption>Auth Data Example (Insecure - Not Recommended)</caption>
* const authData = {
* weibo: {
* id: "1234567",
* access_token: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
* }
* };
*
* @see {@link https://open.weibo.com/wiki/Oauth2/access_token Weibo Authentication Documentation}
*/
// Returns a promise that fulfills iff this user id is valid.
function validateAuthData(authData) {
return graphRequest(authData.access_token).then(function (data) {
if (data && data.uid == authData.id) {
return;
import BaseAuthCodeAdapter from './BaseCodeAuthAdapter';
import querystring from 'querystring';
class WeiboAdapter extends BaseAuthCodeAdapter {
constructor() {
super('Weibo');
}
async getUserFromAccessToken(access_token) {
const postData = querystring.stringify({
access_token: access_token,
});
const response = await fetch('https://api.weibo.com/oauth2/get_token_info', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: postData,
});
const data = await response.json();
if (!response.ok) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Weibo auth is invalid for this user.');
}
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'weibo auth is invalid for this user.');
});
return {
id: data.uid,
}
}
async getAccessTokenFromCode(authData) {
if (!authData?.code || !authData?.redirect_uri) {
throw new Parse.Error(
Parse.Error.OBJECT_NOT_FOUND,
'Weibo auth requires code and redirect_uri to be sent.'
);
}
const postData = querystring.stringify({
client_id: this.clientId,
client_secret: this.clientSecret,
grant_type: 'authorization_code',
code: authData.code,
redirect_uri: authData.redirect_uri,
});
const response = await fetch('https://api.weibo.com/oauth2/access_token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: postData,
});
const data = await response.json();
if (!response.ok || data.errcode) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Weibo auth is invalid for this user.');
}
return data.access_token;
}
}
// Returns a promise that fulfills if this app id is valid.
function validateAppId() {
return Promise.resolve();
}
// A promisey wrapper for weibo graph requests.
function graphRequest(access_token) {
var postData = querystring.stringify({
access_token: access_token,
});
var options = {
hostname: 'api.weibo.com',
path: '/oauth2/get_token_info',
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': Buffer.byteLength(postData),
},
};
return httpsRequest.request(options, postData);
}
module.exports = {
validateAppId,
validateAuthData,
};
export default new WeiboAdapter();

View File

@@ -417,26 +417,35 @@ Auth.prototype._getAllRolesNamesForRoleIds = function (roleIDs, names = [], quer
});
};
const findUsersWithAuthData = (config, authData) => {
const findUsersWithAuthData = async (config, authData, beforeFind) => {
const providers = Object.keys(authData);
const query = providers
.reduce((memo, provider) => {
if (!authData[provider] || (authData && !authData[provider].id)) {
return memo;
}
const queryKey = `authData.${provider}.id`;
const query = {};
query[queryKey] = authData[provider].id;
memo.push(query);
return memo;
}, [])
.filter(q => {
return typeof q !== 'undefined';
});
return query.length > 0
? config.database.find('_User', { $or: query }, { limit: 2 })
: Promise.resolve([]);
const queries = await Promise.all(
providers.map(async provider => {
const providerAuthData = authData[provider];
const adapter = config.authDataManager.getValidatorForProvider(provider)?.adapter;
if (beforeFind && typeof adapter?.beforeFind === 'function') {
await adapter.beforeFind(providerAuthData);
}
if (!providerAuthData?.id) {
return null;
}
return { [`authData.${provider}.id`]: providerAuthData.id };
})
);
// Filter out null queries
const validQueries = queries.filter(query => query !== null);
if (!validQueries.length) {
return [];
}
// Perform database query
return config.database.find('_User', { $or: validQueries }, { limit: 2 });
};
const hasMutatedAuthData = (authData, userAuthData) => {
@@ -539,7 +548,7 @@ const handleAuthDataValidation = async (authData, req, foundUser) => {
acc.authData[provider] = null;
continue;
}
const { validator } = req.config.authDataManager.getValidatorForProvider(provider);
const { validator } = req.config.authDataManager.getValidatorForProvider(provider) || {};
const authProvider = (req.config.auth || {})[provider] || {};
if (!validator || authProvider.enabled === false) {
throw new Parse.Error(

View File

@@ -20,6 +20,7 @@ import {
SecurityOptions,
} from './Options/Definitions';
import ParseServer from './cloud-code/Parse.Server';
import Deprecator from './Deprecator/Deprecator';
function removeTrailingSlash(str) {
if (!str) {
@@ -84,6 +85,7 @@ export class Config {
pages,
security,
enforcePrivateUsers,
enableInsecureAuthAdapters,
schema,
requestKeywordDenylist,
allowExpiredAuthDataToken,
@@ -129,6 +131,7 @@ export class Config {
this.validateSecurityOptions(security);
this.validateSchemaOptions(schema);
this.validateEnforcePrivateUsers(enforcePrivateUsers);
this.validateEnableInsecureAuthAdapters(enableInsecureAuthAdapters);
this.validateAllowExpiredAuthDataToken(allowExpiredAuthDataToken);
this.validateRequestKeywordDenylist(requestKeywordDenylist);
this.validateRateLimit(rateLimit);
@@ -504,6 +507,15 @@ export class Config {
}
}
static validateEnableInsecureAuthAdapters(enableInsecureAuthAdapters) {
if (enableInsecureAuthAdapters && typeof enableInsecureAuthAdapters !== 'boolean') {
throw 'Parse Server option enableInsecureAuthAdapters must be a boolean.';
}
if (enableInsecureAuthAdapters) {
Deprecator.logRuntimeDeprecation({ usage: 'insecure adapter' });
}
}
get mount() {
var mount = this._mount;
if (this.publicServerURL) {

View File

@@ -15,4 +15,7 @@
*
* If there are no deprecations, this must return an empty array.
*/
module.exports = [{ optionKey: 'encodeParseObjectInCloudFunction', changeNewKey: '' }]
module.exports = [
{ optionKey: 'encodeParseObjectInCloudFunction', changeNewDefault: 'true' },
{ optionKey: 'enableInsecureAuthAdapters', changeNewDefault: 'false' },
];

View File

@@ -233,6 +233,13 @@ module.exports.ParseServerOptions = {
action: parsers.booleanParser,
default: false,
},
enableInsecureAuthAdapters: {
env: 'PARSE_SERVER_ENABLE_INSECURE_AUTH_ADAPTERS',
help:
'Enable (or disable) insecure auth adapters, defaults to true. Insecure auth adapters are deprecated and it is recommended to disable them.',
action: parsers.booleanParser,
default: true,
},
encodeParseObjectInCloudFunction: {
env: 'PARSE_SERVER_ENCODE_PARSE_OBJECT_IN_CLOUD_FUNCTION',
help:

View File

@@ -43,6 +43,7 @@
* @property {Boolean} enableAnonymousUsers Enable (or disable) anonymous users, defaults to true
* @property {Boolean} enableCollationCaseComparison Optional. If set to `true`, the collation rule of case comparison for queries and indexes is enabled. Enable this option to run Parse Server with MongoDB Atlas Serverless or AWS Amazon DocumentDB. If `false`, the collation rule of case comparison is disabled. Default is `false`.
* @property {Boolean} enableExpressErrorHandler Enables the default express error handler for all errors
* @property {Boolean} enableInsecureAuthAdapters Enable (or disable) insecure auth adapters, defaults to true. Insecure auth adapters are deprecated and it is recommended to disable them.
* @property {Boolean} encodeParseObjectInCloudFunction If set to `true`, a `Parse.Object` that is in the payload when calling a Cloud Function will be converted to an instance of `Parse.Object`. If `false`, the object will not be converted and instead be a plain JavaScript object, which contains the raw data of a `Parse.Object` but is not an actual instance of `Parse.Object`. Default is `false`. <br><br> The expected behavior would be that the object is converted to an instance of `Parse.Object`, so you would normally set this option to `true`. The default is `false` because this is a temporary option that has been introduced to avoid a breaking change when fixing a bug where JavaScript objects are not converted to actual instances of `Parse.Object`.
* @property {String} encryptionKey Key for encrypting your files
* @property {Boolean} enforcePrivateUsers Set to true if new users should be created without public read and write access.

View File

@@ -161,6 +161,10 @@ export interface ParseServerOptions {
/* Configuration for your authentication providers, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication
:ENV: PARSE_SERVER_AUTH_PROVIDERS */
auth: ?{ [string]: AuthAdapter };
/* Enable (or disable) insecure auth adapters, defaults to true. Insecure auth adapters are deprecated and it is recommended to disable them.
:ENV: PARSE_SERVER_ENABLE_INSECURE_AUTH_ADAPTERS
:DEFAULT: true */
enableInsecureAuthAdapters: ?boolean;
/* Max file size for uploads, defaults to 20mb
:DEFAULT: 20mb */
maxUploadSize: ?string;

View File

@@ -458,9 +458,8 @@ RestWrite.prototype.validateAuthData = function () {
var providers = Object.keys(authData);
if (providers.length > 0) {
const canHandleAuthData = providers.some(provider => {
var providerAuthData = authData[provider];
var hasToken = providerAuthData && providerAuthData.id;
return hasToken || providerAuthData === null;
const providerAuthData = authData[provider] || {};
return !!Object.keys(providerAuthData).length;
});
if (canHandleAuthData || hasUsernameAndPassword || this.auth.isMaster || this.getUserId()) {
return this.handleAuthData(authData);
@@ -520,7 +519,7 @@ RestWrite.prototype.ensureUniqueAuthDataId = async function () {
};
RestWrite.prototype.handleAuthData = async function (authData) {
const r = await Auth.findUsersWithAuthData(this.config, authData);
const r = await Auth.findUsersWithAuthData(this.config, authData, true);
const results = this.filteredObjectsByACL(r);
const userId = this.getUserId();

View File

@@ -69,6 +69,17 @@ class CheckGroupServerConfig extends CheckGroup {
}
},
}),
new Check({
title: 'Insecure auth adapters disabled',
warning:
"Attackers may explore insecure auth adapters' vulnerabilities and log in on behalf of another user.",
solution: "Change Parse Server configuration to 'enableInsecureAuthAdapters: false'.",
check: () => {
if (config.enableInsecureAuthAdapters !== false) {
throw 1;
}
},
}),
];
}
}

View File

@@ -32,6 +32,7 @@ runner({
help,
usage: '[options] <path/to/configuration.json>',
start: function (program, options, logOptions) {
if (!options.appId || !options.masterKey) {
program.outputHelp();
console.error('');