/* * 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 + ":" + 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 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 = 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); } module.exports = { validateAppId: validateAppId, validateAuthData: validateAuthData, };