207 lines
5.6 KiB
JavaScript
207 lines
5.6 KiB
JavaScript
/**
|
|
* 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.
|
|
var Parse = require('parse/node').Parse;
|
|
|
|
const https = require('https');
|
|
const jwt = require('jsonwebtoken');
|
|
const authUtils = require('./utils');
|
|
|
|
const TOKEN_ISSUER = 'accounts.google.com';
|
|
const HTTPS_TOKEN_ISSUER = 'https://accounts.google.com';
|
|
|
|
let cache = {};
|
|
|
|
// Retrieve Google Signin Keys (with cache control)
|
|
function getGoogleKeyByKeyId(keyId) {
|
|
if (cache[keyId] && cache.expiresAt > new Date()) {
|
|
return cache[keyId];
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
https
|
|
.get(`https://www.googleapis.com/oauth2/v3/certs`, res => {
|
|
let data = '';
|
|
res.on('data', chunk => {
|
|
data += chunk.toString('utf8');
|
|
});
|
|
res.on('end', () => {
|
|
const { keys } = JSON.parse(data);
|
|
const pems = keys.reduce(
|
|
(pems, { n: modulus, e: exposant, kid }) =>
|
|
Object.assign(pems, {
|
|
[kid]: rsaPublicKeyToPEM(modulus, exposant),
|
|
}),
|
|
{}
|
|
);
|
|
|
|
if (res.headers['cache-control']) {
|
|
var expire = res.headers['cache-control'].match(/max-age=([0-9]+)/);
|
|
|
|
if (expire) {
|
|
cache = Object.assign({}, pems, {
|
|
expiresAt: new Date(new Date().getTime() + Number(expire[1]) * 1000),
|
|
});
|
|
}
|
|
}
|
|
|
|
resolve(pems[keyId]);
|
|
});
|
|
})
|
|
.on('error', reject);
|
|
});
|
|
}
|
|
|
|
async function verifyIdToken({ id_token: token, id }, { clientId }) {
|
|
if (!token) {
|
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `id token is invalid for this user.`);
|
|
}
|
|
|
|
const { kid: keyId, alg: algorithm } = authUtils.getHeaderFromToken(token);
|
|
let jwtClaims;
|
|
const googleKey = await getGoogleKeyByKeyId(keyId);
|
|
|
|
try {
|
|
jwtClaims = jwt.verify(token, googleKey, {
|
|
algorithms: algorithm,
|
|
audience: clientId,
|
|
});
|
|
} catch (exception) {
|
|
const message = exception.message;
|
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `${message}`);
|
|
}
|
|
|
|
if (jwtClaims.iss !== TOKEN_ISSUER && jwtClaims.iss !== HTTPS_TOKEN_ISSUER) {
|
|
throw new Parse.Error(
|
|
Parse.Error.OBJECT_NOT_FOUND,
|
|
`id token not issued by correct provider - expected: ${TOKEN_ISSUER} or ${HTTPS_TOKEN_ISSUER} | from: ${jwtClaims.iss}`
|
|
);
|
|
}
|
|
|
|
if (jwtClaims.sub !== id) {
|
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `auth data is invalid for this user.`);
|
|
}
|
|
|
|
if (clientId && jwtClaims.aud !== clientId) {
|
|
throw new Parse.Error(
|
|
Parse.Error.OBJECT_NOT_FOUND,
|
|
`id token not authorized for this clientId.`
|
|
);
|
|
}
|
|
|
|
return jwtClaims;
|
|
}
|
|
|
|
// Returns a promise that fulfills if this user id is valid.
|
|
function validateAuthData(authData, options = {}) {
|
|
return verifyIdToken(authData, options);
|
|
}
|
|
|
|
// Returns a promise that fulfills if this app id is valid.
|
|
function validateAppId() {
|
|
return Promise.resolve();
|
|
}
|
|
|
|
module.exports = {
|
|
validateAppId: validateAppId,
|
|
validateAuthData: validateAuthData,
|
|
};
|
|
|
|
// Helpers functions to convert the RSA certs to PEM (from jwks-rsa)
|
|
function rsaPublicKeyToPEM(modulusB64, exponentB64) {
|
|
const modulus = new Buffer(modulusB64, 'base64');
|
|
const exponent = new Buffer(exponentB64, 'base64');
|
|
const modulusHex = prepadSigned(modulus.toString('hex'));
|
|
const exponentHex = prepadSigned(exponent.toString('hex'));
|
|
const modlen = modulusHex.length / 2;
|
|
const explen = exponentHex.length / 2;
|
|
|
|
const encodedModlen = encodeLengthHex(modlen);
|
|
const encodedExplen = encodeLengthHex(explen);
|
|
const encodedPubkey =
|
|
'30' +
|
|
encodeLengthHex(modlen + explen + encodedModlen.length / 2 + encodedExplen.length / 2 + 2) +
|
|
'02' +
|
|
encodedModlen +
|
|
modulusHex +
|
|
'02' +
|
|
encodedExplen +
|
|
exponentHex;
|
|
|
|
const der = new Buffer(encodedPubkey, 'hex').toString('base64');
|
|
|
|
let pem = '-----BEGIN RSA PUBLIC KEY-----\n';
|
|
pem += `${der.match(/.{1,64}/g).join('\n')}`;
|
|
pem += '\n-----END RSA PUBLIC KEY-----\n';
|
|
return pem;
|
|
}
|
|
|
|
function prepadSigned(hexStr) {
|
|
const msb = hexStr[0];
|
|
if (msb < '0' || msb > '7') {
|
|
return `00${hexStr}`;
|
|
}
|
|
return hexStr;
|
|
}
|
|
|
|
function toHex(number) {
|
|
const nstr = number.toString(16);
|
|
if (nstr.length % 2) {
|
|
return `0${nstr}`;
|
|
}
|
|
return nstr;
|
|
}
|
|
|
|
function encodeLengthHex(n) {
|
|
if (n <= 127) {
|
|
return toHex(n);
|
|
}
|
|
const nHex = toHex(n);
|
|
const lengthOfLengthByte = 128 + nHex.length / 2;
|
|
return toHex(lengthOfLengthByte) + nHex;
|
|
}
|