diff options
Diffstat (limited to 'src/server/apis/google')
-rw-r--r-- | src/server/apis/google/GoogleApiServerUtils.ts | 233 |
1 files changed, 114 insertions, 119 deletions
diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts index ff5dc7081..ec7c2cfe1 100644 --- a/src/server/apis/google/GoogleApiServerUtils.ts +++ b/src/server/apis/google/GoogleApiServerUtils.ts @@ -2,7 +2,6 @@ import { google } from "googleapis"; import { readFile } from "fs"; import { OAuth2Client, Credentials, OAuth2ClientOptions } from "google-auth-library"; import { Opt } from "../../../new_fields/Doc"; -import { GlobalOptions } from "googleapis-common"; import { GaxiosResponse } from "gaxios"; import request = require('request-promise'); import * as qs from 'query-string'; @@ -10,19 +9,12 @@ import { Database } from "../../database"; import * as path from "path"; /** - * + * Scopes give Google users fine granularity of control + * over the information they make accessible via the API. + * This is the somewhat overkill list of what Dash requests + * from the user. */ -const prefix = 'https://www.googleapis.com/auth/'; - -/** - * - */ -const refreshEndpoint = "https://oauth2.googleapis.com/token"; - -/** - * - */ -const SCOPES = [ +const scope = [ 'documents.readonly', 'documents', 'presentations', @@ -33,7 +25,7 @@ const SCOPES = [ 'photoslibrary.appendonly', 'photoslibrary.sharing', 'userinfo.profile' -]; +].map(relative => `https://www.googleapis.com/auth/${relative}`); /** * This namespace manages server side authentication for Google API queries, either @@ -42,33 +34,9 @@ const SCOPES = [ export namespace GoogleApiServerUtils { /** - * - */ - export interface CredentialsResult { - credentials: Opt<Credentials>; - refreshed: boolean; - } - - /** - * - */ - export interface UserInfo { - at_hash: string; - aud: string; - azp: string; - exp: number; - family_name: string; - given_name: string; - iat: number; - iss: string; - locale: string; - name: string; - picture: string; - sub: string; - } - - /** - * + * As we expand out to more Google APIs that are accessible from + * the 'googleapis' module imported above, this enum will record + * the list and provide a unified string representation of each API. */ export enum Service { Documents = "Documents", @@ -76,15 +44,10 @@ export namespace GoogleApiServerUtils { } /** - * - */ - export interface CredentialInformation { - credentialsPath: string; - userId: string; - } - - /** - * + * Global credentials read once from a JSON file + * before the server is started that + * allow us to build OAuth2 clients with Dash's + * application specific credentials. */ let installed: OAuth2ClientOptions; @@ -99,27 +62,33 @@ export namespace GoogleApiServerUtils { let worker: OAuth2Client; /** - * + * A briefer format for the response from a 'googleapis' API request */ export type ApiResponse = Promise<GaxiosResponse>; /** - * + * A generic form for a handler that executes some request on the endpoint */ export type ApiRouter = (endpoint: Endpoint, parameters: any) => ApiResponse; /** - * + * A generic form for the asynchronous function that actually submits the + * request to the API and returns the corresporing response. Helpful when + * making an extensible endpoint definition. */ export type ApiHandler = (parameters: any, methodOptions?: any) => ApiResponse; /** - * + * A literal union type indicating the valid actions for these 'googleapis' + * requestions */ export type Action = "create" | "retrieve" | "update"; /** - * + * An interface defining any entity on which one can invoke + * anuy of the following handlers. All 'googleapis' wrappers + * such as google.docs().documents and google.slides().presentations + * satisfy this interface. */ export interface Endpoint { get: ApiHandler; @@ -128,12 +97,10 @@ export namespace GoogleApiServerUtils { } /** - * - */ - export type EndpointParameters = GlobalOptions & { version: "v1" }; - - /** - * + * This function is called once before the server is started, + * reading in Dash's project-specific credentials (client secret + * and client id) for later repeated access. It also sets up the + * global, intentionally unauthenticated worker OAuth2 client instance. */ export async function loadClientSecret(): Promise<void> { return new Promise((resolve, reject) => { @@ -156,75 +123,83 @@ export namespace GoogleApiServerUtils { } /** - * + * Maps the Dash user id of a given user to their single + * associated OAuth2 client, mitigating the creation + * of needless duplicate clients that would arise from + * making one new client instance per request. */ const authenticationClients = new Map<String, OAuth2Client>(); /** - * - * @param sector - * @param userId + * This function receives the target sector ("which G-Suite app's API am I interested in?") + * and the id of the Dash user making the request to the API. With this information, it generates + * an authenticated OAuth2 client and passes it into the relevant 'googleapis' wrapper. + * @param sector the particular desired G-Suite 'googleapis' API (docs, slides, etc.) + * @param userId the id of the Dash user making the request to the API + * @returns the relevant 'googleapis' wrapper, if any */ export async function GetEndpoint(sector: string, userId: string): Promise<Opt<Endpoint>> { - return new Promise(resolve => { - retrieveOAuthClient(userId).then(auth => { - if (!auth) { - return resolve(); - } - let routed: Opt<Endpoint>; - let parameters: EndpointParameters = { auth, version: "v1" }; - switch (sector) { - case Service.Documents: - routed = google.docs(parameters).documents; - break; - case Service.Slides: - routed = google.slides(parameters).presentations; - break; - } - resolve(routed); - }); + return new Promise(async resolve => { + const auth = await retrieveOAuthClient(userId); + if (!auth) { + return resolve(); + } + let routed: Opt<Endpoint>; + let parameters: any = { auth, version: "v1" }; + switch (sector) { + case Service.Documents: + routed = google.docs(parameters).documents; + break; + case Service.Slides: + routed = google.slides(parameters).presentations; + break; + } + resolve(routed); }); } /** - * - * @param userId + * Returns the lengthy string or access token that can be passed into + * the headers of an API request or into the constructor of the Photos + * client API wrapper. + * @param userId the Dash user id of the user requesting his/her associated + * access_token + * @returns the current access_token associated with the requesting + * Dash user. The access_token is valid for only an hour, and + * is then refreshed. */ export async function retrieveAccessToken(userId: string): Promise<string> { - return new Promise(resolve => { - retrieveCredentials(userId).then( - ({ credentials }) => { - if (credentials) { - return resolve(credentials.access_token!); - } - resolve(); - } - ); + return new Promise(async resolve => { + const { credentials } = await retrieveCredentials(userId); + if (!credentials) { + return resolve(); + } + resolve(credentials.access_token!); }); } /** - * Returns an initialized OAuth2 client instance, likely to be passed into Google's + * Manipulates a mapping such that, in the limit, each Dash user has + * an associated authenticated OAuth2 client at their disposal. This + * function ensures that the client's credentials always remain up to date + * @param userId the Dash user id of the user requesting account integration + * @returns returns an initialized OAuth2 client instance, likely to be passed into Google's * npm-installed API wrappers that use authenticated client instances rather than access codes for * security. - * @param userId the Dash user id of the user requesting account integration */ export async function retrieveOAuthClient(userId: string): Promise<OAuth2Client> { - return new Promise((resolve, reject) => { - retrieveCredentials(userId).then( - ({ credentials, refreshed }) => { - if (!credentials) { - return resolve(); - } - let client = authenticationClients.get(userId); - if (!client) { - authenticationClients.set(userId, client = generateClient(credentials)); - } else if (refreshed) { - client.setCredentials(credentials); - } - resolve(client); - } - ); + return new Promise(async resolve => { + const { credentials, refreshed } = await retrieveCredentials(userId); + if (!credentials) { + return resolve(); + } + let client = authenticationClients.get(userId); + if (!client) { + authenticationClients.set(userId, client = generateClient(credentials)); + } else if (refreshed) { + client.setCredentials(credentials); + } + resolve(client); }); } @@ -233,6 +208,7 @@ export namespace GoogleApiServerUtils { * the specific credentials on the client * @param credentials if you have access to the credentials that you'll eventually set on * the client, just pass them in at initialization + * @returns the newly created, potentially certified, OAuth2 client instance */ function generateClient(credentials?: Credentials): OAuth2Client { const client = new google.auth.OAuth2(installed); @@ -244,12 +220,10 @@ export namespace GoogleApiServerUtils { * Calls on the worker (which does not have and does not need * any credentials) to produce a url to which the user can * navigate to give Dash the necessary Google permissions. + * @returns the newly generated url to the authentication landing page */ export function generateAuthenticationUrl(): string { - return worker.generateAuthUrl({ - access_type: 'offline', - scope: SCOPES.map(relative => prefix + relative), - }); + return worker.generateAuthUrl({ scope, access_type: 'offline' }); } /** @@ -306,6 +280,26 @@ export namespace GoogleApiServerUtils { export type EnrichedCredentials = Credentials & { userInfo: UserInfo }; /** + * This interface defines all of the information we + * receive from parsing the base64 encoded info-token + * for a Google user. + */ + export interface UserInfo { + at_hash: string; + aud: string; + azp: string; + exp: number; + family_name: string; + given_name: string; + iat: number; + iss: string; + locale: string; + name: string; + picture: string; + sub: string; + } + + /** * It's pretty cool: the credentials id_token is split into thirds by periods. * The middle third contains a base64-encoded JSON string with all the * user info contained in the interface below. So, we isolate that middle third, @@ -316,7 +310,7 @@ export namespace GoogleApiServerUtils { * in the database. */ function injectUserInfo(credentials: Credentials): EnrichedCredentials { - const userInfo = JSON.parse(atob(credentials.id_token!.split(".")[1])); + const userInfo: UserInfo = JSON.parse(atob(credentials.id_token!.split(".")[1])); return { ...credentials, userInfo }; } @@ -326,15 +320,16 @@ export namespace GoogleApiServerUtils { * automatically refresh the credentials and then resolve with the updated values. * @param userId the id of the Dash user requesting his/her credentials. Eventually, each user might * be associated with multiple different sets of Google credentials. - * @returns the credentials and a flag indicating whether or not they were refreshed during retrieval + * @returns the credentials, or undefined if the user has no stored associated credentials, + * and a flag indicating whether or not they were refreshed during retrieval */ - async function retrieveCredentials(userId: string): Promise<CredentialsResult> { + async function retrieveCredentials(userId: string): Promise<{ credentials: Opt<Credentials>, refreshed: boolean }> { let credentials: Opt<Credentials> = await Database.Auxiliary.GoogleAuthenticationToken.Fetch(userId); let refreshed = false; if (!credentials) { return { credentials: undefined, refreshed }; } - // if the token has expired, submit a request for a refreshed access token + // check for token expiry if (credentials.expiry_date! <= new Date().getTime()) { credentials = await refreshAccessToken(credentials, userId); } @@ -353,7 +348,7 @@ export namespace GoogleApiServerUtils { */ async function refreshAccessToken(credentials: Credentials, userId: string): Promise<Credentials> { let headerParameters = { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }; - let url = `${refreshEndpoint}?${qs.stringify({ + let url = `https://oauth2.googleapis.com/token?${qs.stringify({ refreshToken: credentials.refresh_token, grant_type: "refresh_token", ...installed |