* 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
140 lines
5.5 KiB
JavaScript
140 lines
5.5 KiB
JavaScript
/*
|
|
* 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,
|
|
};
|