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:
@@ -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({});
|
||||
}
|
||||
|
||||
|
||||
112
src/Adapters/Auth/BaseCodeAuthAdapter.js
Normal file
112
src/Adapters/Auth/BaseCodeAuthAdapter.js
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
47
src/Auth.js
47
src/Auth.js
@@ -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(
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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' },
|
||||
];
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
},
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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('');
|
||||
|
||||
Reference in New Issue
Block a user