Skip to content

Commit

Permalink
fix: integrate auth, fix token expiration
Browse files Browse the repository at this point in the history
  • Loading branch information
kitsonk committed May 16, 2022
1 parent 6c6110d commit 9cced0e
Show file tree
Hide file tree
Showing 2 changed files with 173 additions and 8 deletions.
148 changes: 148 additions & 0 deletions auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import { importPKCS8, SignJWT } from "https://deno.land/x/jose@v4.8.1/index.ts";

// jose uses this and it isn't available under the built in libs in TypeScript
declare global {
interface ErrorConstructor {
// deno-lint-ignore ban-types
captureStackTrace(error: Object, constructor?: Function): void;
}
}

export interface ServiceAccountJSON {
client_email: string;
private_key: string;
private_key_id: string;
}

const ALG = "RS256";

function assert(cond: unknown, message = "Assertion error") {
if (!cond) {
throw new Error(message);
}
}

interface OAuth2TokenJson {
access_token: string;
scope?: string;
token_type: string;
expires_in: number;
}

/** A class that wraps the response from the Google APIs OAuth2 token service. */
export class OAuth2Token {
#created = Date.now();
#json: OAuth2TokenJson;

/** The raw access token string. */
get accessToken(): string {
return this.#json.access_token;
}
/** Returns if the `true` if the token has expired, otherwise `false`. */
get expired(): boolean {
return this.expiresIn <= 0;
}
/** The number of seconds until the token expires. If less than or equal to 0
* then the token has expired. */
get expiresIn(): number {
return (this.#created + (this.#json.expires_in * 1000)) - Date.now();
}
/** Any scopes returned in the authorization response. */
get scope(): string | undefined {
return this.#json.scope;
}
/** The type of token that was returned. */
get tokenType(): string {
return this.#json.token_type;
}

constructor(json: OAuth2TokenJson) {
this.#json = json;
}

/** Returns the token as a value for an `Authorization:` header. */
toString(): string {
return `${this.#json.token_type} ${this.#json.access_token}`;
}
}

/** Generates an OAuth2 token against Google APIs for the provided service
* account and scopes. Provides an instance of {@linkcode OAuth2Token} that
* wraps the response from Google API OAuth2 service.
*
* ### Example
*
* ```ts
* import { createOAuth2Token } from "https://deno.land/x/deno_gcp_admin/auth.ts";
* import keys from "./service-account.json" asserts { type: "json" };
*
* const token = await createOAuth2Token(
* keys,
* "https://www.googleapis.com/auth/cloud-platform"
* );
*
* const response = fetch("https://example.googleapis.com/", {
* headers: {
* authorization: token.toString(),
* }
* });
* ```
*
* @param json A JSON object representing the data from a service account JSON
* file obtained from Google Cloud.
* @param scopes [Scopes](https://developers.google.com/identity/protocols/oauth2/scopes)
* that the authorization is being requested for.
*/
export async function createOAuth2Token(
json: ServiceAccountJSON,
...scopes: string[]
): Promise<OAuth2Token> {
const AUD = "https://oauth2.googleapis.com/token";
const key = await importPKCS8(json.private_key, ALG);
const jwt = await new SignJWT({
scope: scopes.join(" "),
})
.setProtectedHeader({ alg: ALG })
.setIssuer(json.client_email)
.setSubject(json.client_email)
.setAudience(AUD)
.setIssuedAt()
.setExpirationTime("1h")
.sign(key);

const res = await fetch(AUD, {
method: "POST",
body: new URLSearchParams([[
"grant_type",
"urn:ietf:params:oauth:grant-type:jwt-bearer",
], ["assertion", jwt]]),
headers: { "content-type": "application/x-www-form-urlencoded" },
});
assert(
res.status === 200,
`Unexpected authorization response ${res.status} - ${res.statusText}.`,
);
return new OAuth2Token(await res.json());
}

/** Generates a custom token that can be used with Firebase's
* `signInWithCustomToken()` API. */
export async function createCustomToken(
json: ServiceAccountJSON,
claims?: Record<string, unknown>,
): Promise<string> {
const AUD =
"https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit";
const key = await importPKCS8(json.private_key, ALG);
return new SignJWT({
uid: json.private_key_id,
claims,
})
.setProtectedHeader({ alg: ALG })
.setIssuer(json.client_email)
.setSubject(json.client_email)
.setAudience(AUD)
.setIssuedAt()
.setExpirationTime("1h")
.sign(key);
}
33 changes: 25 additions & 8 deletions mod.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
// Copyright 2022 Kitson P. Kelly. All rights reserved. MIT License

/** APIs for using Google Datastore from Deno.
/** APIs for using [Google Datastore](https://cloud.google.com/datastore) from
* Deno.
*
* Google Datastore is Firestore in Datastore more under the hood these days,
* but has a more concise API and lacks the complex rule system that Firestore
* running in its native mode provides.
*
* @module
*/

import type * as types from "./types.d.ts";
import {
createOAuth2Token,
type OAuth2Token,
} from "https://deno.land/x/deno_gcp_admin@0.0.5/auth.ts";
import * as base64 from "https://deno.land/std@0.139.0/encoding/base64.ts";
import type * as types from "./types.d.ts";
import { createOAuth2Token, type OAuth2Token } from "./auth.ts";

export interface DatastoreInit {
client_email: string;
Expand Down Expand Up @@ -349,15 +351,30 @@ class DatastoreOperations {
}
}

/** An interface to [Google Datastore](https://cloud.google.com/datastore).
*
* ### Example
*
* ```ts
* import { Datastore } from "https://deno.land/x/google_datastore/mod.ts";
* import keys from "./service-account.json" assert { type: "json" };
*
* const datastore = new Datastore(keys);
*
* const result = await datastore.query({ kind: "book" });
* ```
*/
export class Datastore {
#auth: Auth;
#indexes: DatastoreIndexes;
#operations: DatastoreOperations;

/** APIs related to creating and managing indexes. */
get indexes(): DatastoreIndexes {
return this.#indexes;
}

/** APIs related to managing operations (long running processes). */
get operations(): DatastoreOperations {
return this.#operations;
}
Expand Down Expand Up @@ -679,8 +696,8 @@ interface EntityMetaData {
[datastoreKey]: types.Key;
}

/** Convert a Datastore `Entity` to a JavaScript object, which can then be
* serialized easily back into an `Entity`. */
/** Convert a Datastore {@linkcode types.Entity Entity} to a JavaScript object,
* which can then be serialized easily back into an `Entity`. */
export function entityToObject<O>(entity: types.Entity): O & EntityMetaData {
// deno-lint-ignore no-explicit-any
const o: any = {};
Expand Down

0 comments on commit 9cced0e

Please sign in to comment.