mosquito-transport
Version:
Quickly spawn server infrastructure along robust authentication, database, storage, and cross-platform compatibility
128 lines (113 loc) • 4.41 kB
JavaScript
import { decodeBinary, timeoutFetch } from "../../helpers/utils";
import { ADMIN_DB_NAME, ADMIN_DB_URL, AUTH_PROVIDER_ID, EnginePath, ERRORS } from "../../helpers/values";
import { queryDocument, writeDocument } from "../database";
import { signinCustom, signupCustom } from "./email_auth";
import { Validator } from "guard-object";
import { verifyPublicKey } from "./rsa_verifier";
export const validateGoogleAuthConfig = (config) => {
if (!Validator.OBJECT(config)) throw 'expected a raw object for googleAuthConfig';
const { clientID, clientSecret } = config;
if (!Validator.NON_EMPTY_STRING(clientID)) throw 'clientID in googleAuthConfig is invalid';
if (clientSecret !== undefined && !Validator.NON_EMPTY_STRING(clientSecret))
throw 'clientSecret in googleAuthConfig is invalid';
};
const isIdToken = token => token.split('.').length === 3;
const tokenVerifier = verifyPublicKey({
endpoint: 'https://www.googleapis.com/oauth2/v3/certs',
issuers: ['https://accounts.google.com', 'accounts.google.com']
});
export const doGoogleSignin = async ({ googleAuthConfig, token, projectName, mergeAuthAccount }, req, interceptNewAuth, metax) => {
if (!googleAuthConfig) throw ERRORS.GOOGLE_AUTH_DISABLED;
const { clientID, clientSecret } = googleAuthConfig;
let userInfo;
if (isIdToken(token)) {
userInfo = await tokenVerifier(token, clientID.split(',').map(v => v.trim()));
} else {
if (!clientSecret) throw 'clientSecret in googleAuthConfig is required when using authorization code to signin';
token = await timeoutFetch('https://oauth2.googleapis.com/token', {
body: JSON.stringify({
code: token,
'client_id': clientID,
'client_secret': clientSecret,
'redirect_uri': 'postmessage',
'grant_type': 'authorization_code'
}),
method: 'POST'
}).then(async r => (await r.json()).id_token);
userInfo = JSON.parse(decodeBinary(token.split('.')[1]));
}
if (!userInfo?.email || !userInfo?.email_verified) throw ERRORS.GOOGLE_AUTH_FAILED;
if (Date.now() > userInfo.exp * 1000) throw ERRORS.GOOGLE_TOKEN_EXPIRED;
const { name, given_name, family_name, picture, sub } = userInfo;
const email = userInfo.email.toLowerCase().trim();
const namex = name || given_name || family_name;
const [subAccount, emailAccount] = await Promise.all([
{ [AUTH_PROVIDER_ID.GOOGLE]: sub },
...mergeAuthAccount ? [{ email }] : []
].map(q =>
queryDocument(
{
path: EnginePath.userAcct,
find: q
},
projectName,
ADMIN_DB_NAME,
ADMIN_DB_URL
)
));
const userRecord = subAccount[0] || emailAccount?.[0];
if (userRecord) {
if (userRecord[AUTH_PROVIDER_ID.GOOGLE] !== sub) {
userRecord[AUTH_PROVIDER_ID.GOOGLE] = sub;
await writeDocument({
path: EnginePath.userAcct,
find: { _id: userRecord._id },
value: { $set: { [AUTH_PROVIDER_ID.GOOGLE]: sub } },
scope: 'updateOne'
}, projectName, ADMIN_DB_NAME, ADMIN_DB_URL)
}
return {
...(await signinCustom(email, undefined, AUTH_PROVIDER_ID.GOOGLE, projectName, userRecord)),
isNewUser: false
};
}
const aBuild = {
email,
name: namex,
photo: picture,
request: req,
metadata: metax,
method: AUTH_PROVIDER_ID.GOOGLE,
token,
providerData: userInfo
};
const {
metadata = metax || {},
profile,
uid: d_uid
} = (await interceptNewAuth?.(aBuild)) || {};
return {
...(await signupCustom(
email,
undefined,
AUTH_PROVIDER_ID.GOOGLE,
{
name: namex,
photo: picture,
email
},
projectName,
{
sub: sub,
metadata: {
...Validator.OBJECT(metadata) ? metadata : {}
},
profile: {
...Validator.OBJECT(profile) ? profile : {}
},
d_uid
}
)),
isNewUser: true
};
};