added an RFC 7662 compliant OAuth2 auth adapter (#4910)
* added an RFC 7662 compliant OAuth2 auth adapter * forgot to add the actual auth adapter to the previous commit * fixed lint errors * * added test coverage * changed option names in auth adapter from snake case to camel case * added underscore prefix to helper function names * merged consecutive logger calls into one call and use JSON.stringify() to convert JSON objects to strings * changed error handling (ParseErrors are no longer thrown, but returned) * added description of the "debug" option and added this option to the tests too * added a check of the "debug" option to the unittests and replaced require() of the logger with an import (the former does not work correctly) * added AuthAdapter based auth adapter runtime validation to src/Adapters/Auth/index.js, added capability to define arbitrary providernames with an "adapter" property in auth config, replaced various "var" keywords with "const" in oauth2.js * incorporated changes requested by flovilmart (mainly that oauth2 is now not a standalone adapter, but can be selected by setting the "oauth2" property to true in auth config * modified oauth2 adapter as requested by flovilmart * bugfix: defaultAdapter can be null in loadAuthAdapter() of index.js (my change broke the tests) * added TODO on need for a validateAdapter() to validate auth adapters * test cases and cleanup
This commit is contained in:
committed by
Diamond Lewis
parent
a3746cab00
commit
019cf0a986
@@ -37,11 +37,14 @@ describe('AuthenticationProviders', function() {
|
||||
const provider = require('../lib/Adapters/Auth/' + providerName);
|
||||
jequal(typeof provider.validateAuthData, 'function');
|
||||
jequal(typeof provider.validateAppId, 'function');
|
||||
const authDataPromise = provider.validateAuthData({}, {});
|
||||
const validateAuthDataPromise = provider.validateAuthData({}, {});
|
||||
const validateAppIdPromise = provider.validateAppId('app', 'key', {});
|
||||
jequal(authDataPromise.constructor, Promise.prototype.constructor);
|
||||
jequal(
|
||||
validateAuthDataPromise.constructor,
|
||||
Promise.prototype.constructor
|
||||
);
|
||||
jequal(validateAppIdPromise.constructor, Promise.prototype.constructor);
|
||||
authDataPromise.then(() => {}, () => {});
|
||||
validateAuthDataPromise.then(() => {}, () => {});
|
||||
validateAppIdPromise.then(() => {}, () => {});
|
||||
done();
|
||||
});
|
||||
@@ -584,3 +587,449 @@ describe('google auth adapter', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('oauth2 auth adapter', () => {
|
||||
const oauth2 = require('../lib/Adapters/Auth/oauth2');
|
||||
const httpsRequest = require('../lib/Adapters/Auth/httpsRequest');
|
||||
|
||||
it('properly loads OAuth2 adapter via the "oauth2" option', () => {
|
||||
const options = {
|
||||
oauth2Authentication: {
|
||||
oauth2: true,
|
||||
},
|
||||
};
|
||||
const loadedAuthAdapter = authenticationLoader.loadAuthAdapter(
|
||||
'oauth2Authentication',
|
||||
options
|
||||
);
|
||||
expect(loadedAuthAdapter.adapter).toEqual(oauth2);
|
||||
});
|
||||
|
||||
it('properly loads OAuth2 adapter with options', () => {
|
||||
const options = {
|
||||
oauth2Authentication: {
|
||||
oauth2: true,
|
||||
tokenIntrospectionEndpointUrl: 'https://example.com/introspect',
|
||||
useridField: 'sub',
|
||||
appidField: 'appId',
|
||||
appIds: ['a', 'b'],
|
||||
authorizationHeader: 'Basic dXNlcm5hbWU6cGFzc3dvcmQ=',
|
||||
debug: true,
|
||||
},
|
||||
};
|
||||
const loadedAuthAdapter = authenticationLoader.loadAuthAdapter(
|
||||
'oauth2Authentication',
|
||||
options
|
||||
);
|
||||
const appIds = loadedAuthAdapter.appIds;
|
||||
const providerOptions = loadedAuthAdapter.providerOptions;
|
||||
expect(providerOptions.tokenIntrospectionEndpointUrl).toEqual(
|
||||
'https://example.com/introspect'
|
||||
);
|
||||
expect(providerOptions.useridField).toEqual('sub');
|
||||
expect(providerOptions.appidField).toEqual('appId');
|
||||
expect(appIds).toEqual(['a', 'b']);
|
||||
expect(providerOptions.authorizationHeader).toEqual(
|
||||
'Basic dXNlcm5hbWU6cGFzc3dvcmQ='
|
||||
);
|
||||
expect(providerOptions.debug).toEqual(true);
|
||||
});
|
||||
|
||||
it('validateAppId should fail if OAuth2 tokenIntrospectionEndpointUrl is not configured properly', async () => {
|
||||
const options = {
|
||||
oauth2Authentication: {
|
||||
oauth2: true,
|
||||
appIds: ['a', 'b'],
|
||||
appidField: 'appId',
|
||||
},
|
||||
};
|
||||
const authData = {
|
||||
id: 'fakeid',
|
||||
access_token: 'sometoken',
|
||||
};
|
||||
const {
|
||||
adapter,
|
||||
appIds,
|
||||
providerOptions,
|
||||
} = authenticationLoader.loadAuthAdapter('oauth2Authentication', options);
|
||||
try {
|
||||
await adapter.validateAppId(appIds, authData, providerOptions);
|
||||
} catch (e) {
|
||||
expect(e.message).toBe(
|
||||
'OAuth2 token introspection endpoint URL is missing from configuration!'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('validateAppId appidField optional', async () => {
|
||||
const options = {
|
||||
oauth2Authentication: {
|
||||
oauth2: true,
|
||||
tokenIntrospectionEndpointUrl: 'https://example.com/introspect',
|
||||
},
|
||||
};
|
||||
const authData = {
|
||||
id: 'fakeid',
|
||||
access_token: 'sometoken',
|
||||
};
|
||||
const {
|
||||
adapter,
|
||||
appIds,
|
||||
providerOptions,
|
||||
} = authenticationLoader.loadAuthAdapter('oauth2Authentication', options);
|
||||
try {
|
||||
await adapter.validateAppId(appIds, authData, providerOptions);
|
||||
} catch (e) {
|
||||
// Should not reach here
|
||||
fail(e);
|
||||
}
|
||||
});
|
||||
|
||||
it('validateAppId should fail without appIds', async () => {
|
||||
const options = {
|
||||
oauth2Authentication: {
|
||||
oauth2: true,
|
||||
tokenIntrospectionEndpointUrl: 'https://example.com/introspect',
|
||||
appidField: 'appId',
|
||||
},
|
||||
};
|
||||
const authData = {
|
||||
id: 'fakeid',
|
||||
access_token: 'sometoken',
|
||||
};
|
||||
const {
|
||||
adapter,
|
||||
appIds,
|
||||
providerOptions,
|
||||
} = authenticationLoader.loadAuthAdapter('oauth2Authentication', options);
|
||||
try {
|
||||
await adapter.validateAppId(appIds, authData, providerOptions);
|
||||
} catch (e) {
|
||||
expect(e.message).toBe(
|
||||
'OAuth2 configuration is missing the client app IDs ("appIds" config parameter).'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('validateAppId should fail empty appIds', async () => {
|
||||
const options = {
|
||||
oauth2Authentication: {
|
||||
oauth2: true,
|
||||
tokenIntrospectionEndpointUrl: 'https://example.com/introspect',
|
||||
appidField: 'appId',
|
||||
appIds: [],
|
||||
},
|
||||
};
|
||||
const authData = {
|
||||
id: 'fakeid',
|
||||
access_token: 'sometoken',
|
||||
};
|
||||
const {
|
||||
adapter,
|
||||
appIds,
|
||||
providerOptions,
|
||||
} = authenticationLoader.loadAuthAdapter('oauth2Authentication', options);
|
||||
try {
|
||||
await adapter.validateAppId(appIds, authData, providerOptions);
|
||||
} catch (e) {
|
||||
expect(e.message).toBe(
|
||||
'OAuth2 configuration is missing the client app IDs ("appIds" config parameter).'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('validateAppId invalid accessToken', async () => {
|
||||
const options = {
|
||||
oauth2Authentication: {
|
||||
oauth2: true,
|
||||
tokenIntrospectionEndpointUrl: 'https://example.com/introspect',
|
||||
appidField: 'appId',
|
||||
appIds: ['a', 'b'],
|
||||
},
|
||||
};
|
||||
const authData = {
|
||||
id: 'fakeid',
|
||||
access_token: 'sometoken',
|
||||
};
|
||||
const {
|
||||
adapter,
|
||||
appIds,
|
||||
providerOptions,
|
||||
} = authenticationLoader.loadAuthAdapter('oauth2Authentication', options);
|
||||
spyOn(httpsRequest, 'request').and.callFake(() => {
|
||||
return Promise.resolve({});
|
||||
});
|
||||
try {
|
||||
await adapter.validateAppId(appIds, authData, providerOptions);
|
||||
} catch (e) {
|
||||
expect(e.message).toBe('OAuth2 access token is invalid for this user.');
|
||||
}
|
||||
});
|
||||
|
||||
it('validateAppId invalid accessToken appId', async () => {
|
||||
const options = {
|
||||
oauth2Authentication: {
|
||||
oauth2: true,
|
||||
tokenIntrospectionEndpointUrl: 'https://example.com/introspect',
|
||||
appidField: 'appId',
|
||||
appIds: ['a', 'b'],
|
||||
},
|
||||
};
|
||||
const authData = {
|
||||
id: 'fakeid',
|
||||
access_token: 'sometoken',
|
||||
};
|
||||
const {
|
||||
adapter,
|
||||
appIds,
|
||||
providerOptions,
|
||||
} = authenticationLoader.loadAuthAdapter('oauth2Authentication', options);
|
||||
spyOn(httpsRequest, 'request').and.callFake(() => {
|
||||
return Promise.resolve({ active: true });
|
||||
});
|
||||
try {
|
||||
await adapter.validateAppId(appIds, authData, providerOptions);
|
||||
} catch (e) {
|
||||
expect(e.message).toBe(
|
||||
"OAuth2: the access_token's appID is empty or is not in the list of permitted appIDs in the auth configuration."
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('validateAppId valid accessToken appId', async () => {
|
||||
const options = {
|
||||
oauth2Authentication: {
|
||||
oauth2: true,
|
||||
tokenIntrospectionEndpointUrl: 'https://example.com/introspect',
|
||||
appidField: 'appId',
|
||||
appIds: ['a', 'b'],
|
||||
},
|
||||
};
|
||||
const authData = {
|
||||
id: 'fakeid',
|
||||
access_token: 'sometoken',
|
||||
};
|
||||
const {
|
||||
adapter,
|
||||
appIds,
|
||||
providerOptions,
|
||||
} = authenticationLoader.loadAuthAdapter('oauth2Authentication', options);
|
||||
spyOn(httpsRequest, 'request').and.callFake(() => {
|
||||
return Promise.resolve({
|
||||
active: true,
|
||||
appId: 'a',
|
||||
});
|
||||
});
|
||||
try {
|
||||
await adapter.validateAppId(appIds, authData, providerOptions);
|
||||
} catch (e) {
|
||||
// Should not enter here
|
||||
fail(e);
|
||||
}
|
||||
});
|
||||
|
||||
it('validateAppId valid accessToken appId array', async () => {
|
||||
const options = {
|
||||
oauth2Authentication: {
|
||||
oauth2: true,
|
||||
tokenIntrospectionEndpointUrl: 'https://example.com/introspect',
|
||||
appidField: 'appId',
|
||||
appIds: ['a', 'b'],
|
||||
},
|
||||
};
|
||||
const authData = {
|
||||
id: 'fakeid',
|
||||
access_token: 'sometoken',
|
||||
};
|
||||
const {
|
||||
adapter,
|
||||
appIds,
|
||||
providerOptions,
|
||||
} = authenticationLoader.loadAuthAdapter('oauth2Authentication', options);
|
||||
spyOn(httpsRequest, 'request').and.callFake(() => {
|
||||
return Promise.resolve({
|
||||
active: true,
|
||||
appId: ['a'],
|
||||
});
|
||||
});
|
||||
try {
|
||||
await adapter.validateAppId(appIds, authData, providerOptions);
|
||||
} catch (e) {
|
||||
// Should not enter here
|
||||
fail(e);
|
||||
}
|
||||
});
|
||||
|
||||
it('validateAppId valid accessToken invalid appId', async () => {
|
||||
const options = {
|
||||
oauth2Authentication: {
|
||||
oauth2: true,
|
||||
tokenIntrospectionEndpointUrl: 'https://example.com/introspect',
|
||||
appidField: 'appId',
|
||||
appIds: ['a', 'b'],
|
||||
},
|
||||
};
|
||||
const authData = {
|
||||
id: 'fakeid',
|
||||
access_token: 'sometoken',
|
||||
};
|
||||
const {
|
||||
adapter,
|
||||
appIds,
|
||||
providerOptions,
|
||||
} = authenticationLoader.loadAuthAdapter('oauth2Authentication', options);
|
||||
spyOn(httpsRequest, 'request').and.callFake(() => {
|
||||
return Promise.resolve({
|
||||
active: true,
|
||||
appId: 'unknown',
|
||||
});
|
||||
});
|
||||
try {
|
||||
await adapter.validateAppId(appIds, authData, providerOptions);
|
||||
} catch (e) {
|
||||
expect(e.message).toBe(
|
||||
"OAuth2: the access_token's appID is empty or is not in the list of permitted appIDs in the auth configuration."
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('validateAuthData should fail if OAuth2 tokenIntrospectionEndpointUrl is not configured properly', async () => {
|
||||
const options = {
|
||||
oauth2Authentication: {
|
||||
oauth2: true,
|
||||
},
|
||||
};
|
||||
const authData = {
|
||||
id: 'fakeid',
|
||||
access_token: 'sometoken',
|
||||
};
|
||||
const { adapter, providerOptions } = authenticationLoader.loadAuthAdapter(
|
||||
'oauth2Authentication',
|
||||
options
|
||||
);
|
||||
try {
|
||||
await adapter.validateAuthData(authData, providerOptions);
|
||||
} catch (e) {
|
||||
expect(e.message).toBe(
|
||||
'OAuth2 token introspection endpoint URL is missing from configuration!'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('validateAuthData invalid accessToken', async () => {
|
||||
const options = {
|
||||
oauth2Authentication: {
|
||||
oauth2: true,
|
||||
tokenIntrospectionEndpointUrl: 'https://example.com/introspect',
|
||||
useridField: 'sub',
|
||||
appidField: 'appId',
|
||||
appIds: ['a', 'b'],
|
||||
authorizationHeader: 'Basic dXNlcm5hbWU6cGFzc3dvcmQ=',
|
||||
},
|
||||
};
|
||||
const authData = {
|
||||
id: 'fakeid',
|
||||
access_token: 'sometoken',
|
||||
};
|
||||
const { adapter, providerOptions } = authenticationLoader.loadAuthAdapter(
|
||||
'oauth2Authentication',
|
||||
options
|
||||
);
|
||||
spyOn(httpsRequest, 'request').and.callFake(() => {
|
||||
return Promise.resolve({});
|
||||
});
|
||||
try {
|
||||
await adapter.validateAuthData(authData, providerOptions);
|
||||
} catch (e) {
|
||||
expect(e.message).toBe('OAuth2 access token is invalid for this user.');
|
||||
}
|
||||
expect(httpsRequest.request).toHaveBeenCalledWith(
|
||||
{
|
||||
hostname: 'example.com',
|
||||
path: '/introspect',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Content-Length': 15,
|
||||
Authorization: 'Basic dXNlcm5hbWU6cGFzc3dvcmQ=',
|
||||
},
|
||||
},
|
||||
'token=sometoken'
|
||||
);
|
||||
});
|
||||
|
||||
it('validateAuthData valid accessToken', async () => {
|
||||
const options = {
|
||||
oauth2Authentication: {
|
||||
oauth2: true,
|
||||
tokenIntrospectionEndpointUrl: 'https://example.com/introspect',
|
||||
useridField: 'sub',
|
||||
appidField: 'appId',
|
||||
appIds: ['a', 'b'],
|
||||
},
|
||||
};
|
||||
const authData = {
|
||||
id: 'fakeid',
|
||||
access_token: 'sometoken',
|
||||
};
|
||||
const { adapter, providerOptions } = authenticationLoader.loadAuthAdapter(
|
||||
'oauth2Authentication',
|
||||
options
|
||||
);
|
||||
spyOn(httpsRequest, 'request').and.callFake(() => {
|
||||
return Promise.resolve({
|
||||
active: true,
|
||||
sub: 'fakeid',
|
||||
});
|
||||
});
|
||||
try {
|
||||
await adapter.validateAuthData(authData, providerOptions);
|
||||
} catch (e) {
|
||||
// Should not enter here
|
||||
fail(e);
|
||||
}
|
||||
expect(httpsRequest.request).toHaveBeenCalledWith(
|
||||
{
|
||||
hostname: 'example.com',
|
||||
path: '/introspect',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Content-Length': 15,
|
||||
},
|
||||
},
|
||||
'token=sometoken'
|
||||
);
|
||||
});
|
||||
|
||||
it('validateAuthData valid accessToken without useridField', async () => {
|
||||
const options = {
|
||||
oauth2Authentication: {
|
||||
oauth2: true,
|
||||
tokenIntrospectionEndpointUrl: 'https://example.com/introspect',
|
||||
appidField: 'appId',
|
||||
appIds: ['a', 'b'],
|
||||
},
|
||||
};
|
||||
const authData = {
|
||||
id: 'fakeid',
|
||||
access_token: 'sometoken',
|
||||
};
|
||||
const { adapter, providerOptions } = authenticationLoader.loadAuthAdapter(
|
||||
'oauth2Authentication',
|
||||
options
|
||||
);
|
||||
spyOn(httpsRequest, 'request').and.callFake(() => {
|
||||
return Promise.resolve({
|
||||
active: true,
|
||||
sub: 'fakeid',
|
||||
});
|
||||
});
|
||||
try {
|
||||
await adapter.validateAuthData(authData, providerOptions);
|
||||
} catch (e) {
|
||||
// Should not enter here
|
||||
fail(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,9 +3,10 @@ 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
|
||||
*/
|
||||
validateAppId(appIds, authData) {
|
||||
validateAppId(appIds, authData, options) {
|
||||
return Promise.resolve({});
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ const vkontakte = require('./vkontakte');
|
||||
const qq = require('./qq');
|
||||
const wechat = require('./wechat');
|
||||
const weibo = require('./weibo');
|
||||
const oauth2 = require('./oauth2');
|
||||
|
||||
const anonymous = {
|
||||
validateAuthData: () => {
|
||||
@@ -45,6 +46,7 @@ const providers = {
|
||||
wechat,
|
||||
weibo,
|
||||
};
|
||||
|
||||
function authDataValidator(adapter, appIds, options) {
|
||||
return function(authData) {
|
||||
return adapter.validateAuthData(authData, options).then(() => {
|
||||
@@ -57,14 +59,21 @@ function authDataValidator(adapter, appIds, options) {
|
||||
}
|
||||
|
||||
function loadAuthAdapter(provider, authOptions) {
|
||||
const defaultAdapter = providers[provider];
|
||||
const adapter = Object.assign({}, defaultAdapter);
|
||||
let defaultAdapter = providers[provider];
|
||||
const providerOptions = authOptions[provider];
|
||||
if (
|
||||
providerOptions &&
|
||||
providerOptions.hasOwnProperty('oauth2') &&
|
||||
providerOptions['oauth2'] === true
|
||||
) {
|
||||
defaultAdapter = oauth2;
|
||||
}
|
||||
|
||||
if (!defaultAdapter && !providerOptions) {
|
||||
return;
|
||||
}
|
||||
|
||||
const adapter = Object.assign({}, defaultAdapter);
|
||||
const appIds = providerOptions ? providerOptions.appIds : undefined;
|
||||
|
||||
// Try the configuration methods
|
||||
@@ -83,6 +92,10 @@ 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;
|
||||
}
|
||||
|
||||
139
src/Adapters/Auth/oauth2.js
Normal file
139
src/Adapters/Auth/oauth2.js
Normal file
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
* 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).
|
||||
*
|
||||
* 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:
|
||||
*
|
||||
* {
|
||||
* "someadapter": {
|
||||
* "id": "user's OAuth2 provider-specific id as a string",
|
||||
* "access_token": "an authorized OAuth2 access token for the user",
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
|
||||
const Parse = require('parse/node').Parse;
|
||||
const url = require('url');
|
||||
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!';
|
||||
|
||||
// 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function validateAppId(appIds, authData, options) {
|
||||
if (!options || !options.appidField) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
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)) {
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 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 = url.parse(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);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
validateAppId: validateAppId,
|
||||
validateAuthData: validateAuthData,
|
||||
};
|
||||
Reference in New Issue
Block a user