feat: Improve authentication adapter interface to support multi-factor authentication (MFA), authentication challenges, and provide a more powerful interface for writing custom authentication adapters (#8156)
This commit is contained in:
@@ -1,20 +1,96 @@
|
||||
/*eslint no-unused-vars: "off"*/
|
||||
|
||||
/**
|
||||
* @interface ParseAuthResponse
|
||||
* @property {Boolean} [doNotSave] If true, Parse Server will not save provided authData.
|
||||
* @property {Object} [response] If set, Parse Server will send the provided response to the client under authDataResponse
|
||||
* @property {Object} [save] If set, Parse Server will save the object provided into this key, instead of client provided authData
|
||||
*/
|
||||
|
||||
/**
|
||||
* AuthPolicy
|
||||
* default: can be combined with ONE additional auth provider if additional configured on user
|
||||
* additional: could be only used with a default policy auth provider
|
||||
* solo: Will ignore ALL additional providers if additional configured on user
|
||||
* @typedef {"default" | "additional" | "solo"} AuthPolicy
|
||||
*/
|
||||
|
||||
export class AuthAdapter {
|
||||
/*
|
||||
@param appIds: the specified app ids in the configuration
|
||||
@param authData: the client provided authData
|
||||
@param options: additional options
|
||||
@returns a promise that resolves if the applicationId is valid
|
||||
constructor() {
|
||||
/**
|
||||
* Usage policy
|
||||
* @type {AuthPolicy}
|
||||
*/
|
||||
this.policy = 'default';
|
||||
}
|
||||
/**
|
||||
* @param appIds The specified app IDs in the configuration
|
||||
* @param {Object} authData The client provided authData
|
||||
* @param {Object} options additional adapter options
|
||||
* @param {Parse.Cloud.TriggerRequest} request
|
||||
* @returns {(Promise<undefined|void>|void|undefined)} resolves or returns if the applicationId is valid
|
||||
*/
|
||||
validateAppId(appIds, authData, options) {
|
||||
validateAppId(appIds, authData, options, request) {
|
||||
return Promise.resolve({});
|
||||
}
|
||||
|
||||
/*
|
||||
@param authData: the client provided authData
|
||||
@param options: additional options
|
||||
/**
|
||||
* 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
|
||||
* @returns {Promise<ParseAuthResponse|void|undefined>}
|
||||
*/
|
||||
validateAuthData(authData, options) {
|
||||
validateAuthData(authData, request, options) {
|
||||
return Promise.resolve({});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @returns {Promise<ParseAuthResponse|void|undefined>}
|
||||
*/
|
||||
validateSetUp(authData, req, options) {
|
||||
return Promise.resolve({});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @returns {Promise<ParseAuthResponse|void|undefined>}
|
||||
*/
|
||||
validateLogin(authData, req, options) {
|
||||
return Promise.resolve({});
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggered when user provide authData related to this provider
|
||||
* the user is logged in and has already set this provider before
|
||||
* @param {Object} authData The client provided authData
|
||||
* @param {Object} options additional adapter options
|
||||
* @param {Parse.Cloud.TriggerRequest} request
|
||||
* @returns {Promise<ParseAuthResponse|void|undefined>}
|
||||
*/
|
||||
validateUpdate(authData, req, options) {
|
||||
return Promise.resolve({});
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggered in pre authentication process if needed (like webauthn, SMS OTP)
|
||||
* @param {Object} challengeData Data provided by the client
|
||||
* @param {(Object|undefined)} authData Auth data provided by the client, can be used for validation
|
||||
* @param {Object} options additional adapter options
|
||||
* @param {Parse.Cloud.TriggerRequest} request
|
||||
* @returns {Promise<Object>} A promise that resolves, resolved value will be added to challenge response under challenge key
|
||||
*/
|
||||
challenge(challengeData, authData, options, request) {
|
||||
return Promise.resolve({});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import loadAdapter from '../AdapterLoader';
|
||||
import Parse from 'parse/node';
|
||||
|
||||
const apple = require('./apple');
|
||||
const gcenter = require('./gcenter');
|
||||
@@ -61,19 +62,83 @@ const providers = {
|
||||
ldap,
|
||||
};
|
||||
|
||||
function authDataValidator(adapter, appIds, options) {
|
||||
return function (authData) {
|
||||
return adapter.validateAuthData(authData, options).then(() => {
|
||||
if (appIds) {
|
||||
return adapter.validateAppId(appIds, authData, options);
|
||||
// Indexed auth policies
|
||||
const authAdapterPolicies = {
|
||||
default: true,
|
||||
solo: true,
|
||||
additional: true,
|
||||
};
|
||||
|
||||
function authDataValidator(provider, adapter, appIds, options) {
|
||||
return async function (authData, req, user, requestObject) {
|
||||
if (appIds && typeof adapter.validateAppId === 'function') {
|
||||
await Promise.resolve(adapter.validateAppId(appIds, authData, options, requestObject));
|
||||
}
|
||||
if (adapter.policy && !authAdapterPolicies[adapter.policy]) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OTHER_CAUSE,
|
||||
'AuthAdapter policy is not configured correctly. The value must be either "solo", "additional", "default" or undefined (will be handled as "default")'
|
||||
);
|
||||
}
|
||||
if (typeof adapter.validateAuthData === 'function') {
|
||||
return adapter.validateAuthData(authData, options, requestObject);
|
||||
}
|
||||
if (
|
||||
typeof adapter.validateSetUp !== 'function' ||
|
||||
typeof adapter.validateLogin !== 'function' ||
|
||||
typeof adapter.validateUpdate !== 'function'
|
||||
) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OTHER_CAUSE,
|
||||
'Adapter is not configured. Implement either validateAuthData or all of the following: validateSetUp, validateLogin and validateUpdate'
|
||||
);
|
||||
}
|
||||
// When masterKey is detected, we should trigger a logged in user
|
||||
const isLoggedIn =
|
||||
(req.auth.user && user && req.auth.user.id === user.id) || (user && req.auth.isMaster);
|
||||
let hasAuthDataConfigured = false;
|
||||
|
||||
if (user && user.get('authData') && user.get('authData')[provider]) {
|
||||
hasAuthDataConfigured = true;
|
||||
}
|
||||
|
||||
if (isLoggedIn) {
|
||||
// User is updating their authData
|
||||
if (hasAuthDataConfigured) {
|
||||
return {
|
||||
method: 'validateUpdate',
|
||||
validator: () => adapter.validateUpdate(authData, options, requestObject),
|
||||
};
|
||||
}
|
||||
return Promise.resolve();
|
||||
});
|
||||
// Set up if the user does not have the provider configured
|
||||
return {
|
||||
method: 'validateSetUp',
|
||||
validator: () => adapter.validateSetUp(authData, options, requestObject),
|
||||
};
|
||||
}
|
||||
|
||||
// Not logged in and authData is configured on the user
|
||||
if (hasAuthDataConfigured) {
|
||||
return {
|
||||
method: 'validateLogin',
|
||||
validator: () => adapter.validateLogin(authData, options, requestObject),
|
||||
};
|
||||
}
|
||||
|
||||
// User not logged in and the provider is not set up, for example when a new user
|
||||
// signs up or an existing user uses a new auth provider
|
||||
return {
|
||||
method: 'validateSetUp',
|
||||
validator: () => adapter.validateSetUp(authData, options, requestObject),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
function loadAuthAdapter(provider, authOptions) {
|
||||
// providers are auth providers implemented by default
|
||||
let defaultAdapter = providers[provider];
|
||||
// authOptions can contain complete custom auth adapters or
|
||||
// a default auth adapter like Facebook
|
||||
const providerOptions = authOptions[provider];
|
||||
if (
|
||||
providerOptions &&
|
||||
@@ -83,6 +148,7 @@ function loadAuthAdapter(provider, authOptions) {
|
||||
defaultAdapter = oauth2;
|
||||
}
|
||||
|
||||
// Default provider not found and a custom auth provider was not provided
|
||||
if (!defaultAdapter && !providerOptions) {
|
||||
return;
|
||||
}
|
||||
@@ -94,7 +160,15 @@ function loadAuthAdapter(provider, authOptions) {
|
||||
if (providerOptions) {
|
||||
const optionalAdapter = loadAdapter(providerOptions, undefined, providerOptions);
|
||||
if (optionalAdapter) {
|
||||
['validateAuthData', 'validateAppId'].forEach(key => {
|
||||
[
|
||||
'validateAuthData',
|
||||
'validateAppId',
|
||||
'validateSetUp',
|
||||
'validateLogin',
|
||||
'validateUpdate',
|
||||
'challenge',
|
||||
'policy',
|
||||
].forEach(key => {
|
||||
if (optionalAdapter[key]) {
|
||||
adapter[key] = optionalAdapter[key];
|
||||
}
|
||||
@@ -102,14 +176,6 @@ function loadAuthAdapter(provider, authOptions) {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: create a new module from validateAdapter() in
|
||||
// src/Controllers/AdaptableController.js so we can use it here for adapter
|
||||
// validation based on the src/Adapters/Auth/AuthAdapter.js expected class
|
||||
// signature.
|
||||
if (!adapter.validateAuthData || !adapter.validateAppId) {
|
||||
return;
|
||||
}
|
||||
|
||||
return { adapter, appIds, providerOptions };
|
||||
}
|
||||
|
||||
@@ -121,12 +187,12 @@ module.exports = function (authOptions = {}, enableAnonymousUsers = true) {
|
||||
// To handle the test cases on configuration
|
||||
const getValidatorForProvider = function (provider) {
|
||||
if (provider === 'anonymous' && !_enableAnonymousUsers) {
|
||||
return;
|
||||
return { validator: undefined };
|
||||
}
|
||||
|
||||
const { adapter, appIds, providerOptions } = loadAuthAdapter(provider, authOptions);
|
||||
|
||||
return authDataValidator(adapter, appIds, providerOptions);
|
||||
const authAdapter = loadAuthAdapter(provider, authOptions);
|
||||
if (!authAdapter) return;
|
||||
const { adapter, appIds, providerOptions } = authAdapter;
|
||||
return { validator: authDataValidator(provider, adapter, appIds, providerOptions), adapter };
|
||||
};
|
||||
|
||||
return Object.freeze({
|
||||
|
||||
@@ -289,7 +289,6 @@ const buildWhereClause = ({ schema, query, index, caseInsensitive }): WhereClaus
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const authDataMatch = fieldName.match(/^_auth_data_([a-zA-Z0-9_]+)$/);
|
||||
if (authDataMatch) {
|
||||
// TODO: Handle querying by _auth_data_provider, authData is stored in authData field
|
||||
@@ -1322,12 +1321,17 @@ export class PostgresStorageAdapter implements StorageAdapter {
|
||||
return;
|
||||
}
|
||||
var authDataMatch = fieldName.match(/^_auth_data_([a-zA-Z0-9_]+)$/);
|
||||
const authDataAlreadyExists = !!object.authData;
|
||||
if (authDataMatch) {
|
||||
var provider = authDataMatch[1];
|
||||
object['authData'] = object['authData'] || {};
|
||||
object['authData'][provider] = object[fieldName];
|
||||
delete object[fieldName];
|
||||
fieldName = 'authData';
|
||||
// Avoid adding authData multiple times to the query
|
||||
if (authDataAlreadyExists) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
columnsArray.push(fieldName);
|
||||
@@ -1807,7 +1811,6 @@ export class PostgresStorageAdapter implements StorageAdapter {
|
||||
caseInsensitive,
|
||||
});
|
||||
values.push(...where.values);
|
||||
|
||||
const wherePattern = where.pattern.length > 0 ? `WHERE ${where.pattern}` : '';
|
||||
const limitPattern = hasLimit ? `LIMIT $${values.length + 1}` : '';
|
||||
if (hasLimit) {
|
||||
|
||||
Reference in New Issue
Block a user