From b8dae421d9f12e23c95c7cd39705c100e1e1532c Mon Sep 17 00:00:00 2001 From: Harry Brundage Date: Tue, 29 Aug 2023 16:52:48 +0000 Subject: [PATCH] Add support for the JWT token authentication method for the new JWT Auth connection --- .../spec/GadgetConnection-suite.ts | 37 +++++++++++++++++++ packages/api-client-core/src/ClientOptions.ts | 31 ++++++++++++---- .../api-client-core/src/GadgetConnection.ts | 7 ++++ 3 files changed, 67 insertions(+), 8 deletions(-) diff --git a/packages/api-client-core/spec/GadgetConnection-suite.ts b/packages/api-client-core/spec/GadgetConnection-suite.ts index 485bfeaf0..71c707230 100644 --- a/packages/api-client-core/spec/GadgetConnection-suite.ts +++ b/packages/api-client-core/spec/GadgetConnection-suite.ts @@ -143,6 +143,43 @@ export const GadgetConnectionSharedSuite = (queryExtra = "") => { expect(result.data).toEqual({ meta: { appName: "some app" } }); }); + it("should allow connecting with a JWT from an external system", async () => { + nock("https://someapp.gadget.app") + .post("/api/graphql?operation=meta", { query: `{\n meta {\n appName\n${queryExtra} }\n}`, variables: {} }) + .reply(200, function () { + expect(this.req.headers["authorization"]).toEqual([`Bearer foobarbaz`]); + + return { + data: { + meta: { + appName: "some app", + }, + }, + }; + }); + + const connection = new GadgetConnection({ + endpoint: "https://someapp.gadget.app/api/graphql", + authenticationMode: { jwt: "foobarbaz" }, + }); + + const result = await connection.currentClient + .query( + gql` + { + meta { + appName + } + } + `, + {} + ) + .toPromise(); + + expect(result.error).toBeUndefined(); + expect(result.data).toEqual({ meta: { appName: "some app" } }); + }); + describe("session token storage", () => { it("should allow connecting with no session in a session storage mode", async () => { nock("https://someapp.gadget.app") diff --git a/packages/api-client-core/src/ClientOptions.ts b/packages/api-client-core/src/ClientOptions.ts index d2764ed52..f6f4aad64 100644 --- a/packages/api-client-core/src/ClientOptions.ts +++ b/packages/api-client-core/src/ClientOptions.ts @@ -81,22 +81,37 @@ export enum BrowserSessionStorageType { /** Describes how to authenticate an instance of the client with the Gadget platform */ export interface AuthenticationModeOptions { - // Use an API key to authenticate with Gadget. - // Not strictly required, but without this the client might be useless depending on the app's permissions. + /** + * Use an API key to authenticate with Gadget. + * Not strictly required, but without this the client might be useless depending on the app's permissions. + */ apiKey?: string; - // Use a web browser's `localStorage` or `sessionStorage` to persist authentication information. - // This allows the browser to have a persistent identity as the user navigates around and logs in and out. + /** + * Use a web browser's `localStorage` or `sessionStorage` to persist authentication information. + * This allows the browser to have a persistent identity as the user navigates around and logs in and out. + */ browserSession?: boolean | BrowserSessionAuthenticationModeOptions; - // Use no authentication at all, and get access only to the data that the Unauthenticated backend role has access to. + /** + * Use no authentication at all, and get access only to the data that the Unauthenticated backend role has access to. + */ anonymous?: true; - // @private Use an internal platform auth token for authentication - // This is used to communicate within Gadget itself and shouldn't be used to connect to Gadget from other systems + /** + * Use a JWT token signed by another system to authenticate with Gadget. Requires the JWT Auth Plugin to be set up in your Gadget app. + */ + jwt?: string; + + /** + * @private Use an internal platform auth token for authentication + * This is used to communicate within Gadget itself and shouldn't be used to connect to Gadget from other systems + */ internalAuthToken?: string; - // @private Use a passed custom function for managing authentication. For some fancy integrations that the API client supports, like embedded Shopify apps, we use platform native features to authenticate with the Gadget backend. + /** + * @private Use a passed custom function for managing authentication. For some fancy integrations that the API client supports, like embedded Shopify apps, we use platform native features to authenticate with the Gadget backend. + */ custom?: { processFetch(input: RequestInfo | URL, init: RequestInit): Promise; processTransactionConnectionParams(params: Record): Promise; diff --git a/packages/api-client-core/src/GadgetConnection.ts b/packages/api-client-core/src/GadgetConnection.ts index 86e97ccbd..3f56763df 100644 --- a/packages/api-client-core/src/GadgetConnection.ts +++ b/packages/api-client-core/src/GadgetConnection.ts @@ -61,6 +61,7 @@ export enum AuthenticationMode { APIKey = "api-key", InternalAuthToken = "internal-auth-token", Anonymous = "anonymous", + ExternalJWT = "jwt", Custom = "custom", } @@ -149,6 +150,8 @@ export class GadgetConnection { this.authenticationMode = AuthenticationMode.InternalAuthToken; } else if (options.apiKey) { this.authenticationMode = AuthenticationMode.APIKey; + } else if (options.jwt) { + this.authenticationMode = AuthenticationMode.ExternalJWT; } else if (options.custom) { this.authenticationMode = AuthenticationMode.Custom; } @@ -430,6 +433,8 @@ export class GadgetConnection { connectionParams.auth.token = this.options.authenticationMode!.internalAuthToken!; } else if (this.authenticationMode == AuthenticationMode.BrowserSession) { connectionParams.auth.sessionToken = this.sessionTokenStore!.getItem(this.sessionStorageKey); + } else if (this.authenticationMode == AuthenticationMode.ExternalJWT) { + connectionParams.auth.jwt = this.options.authenticationMode!.jwt!; } else if (this.authenticationMode == AuthenticationMode.Custom) { await this.options.authenticationMode?.custom?.processTransactionConnectionParams(connectionParams); } @@ -464,6 +469,8 @@ export class GadgetConnection { headers.authorization = "Basic " + base64("gadget-internal" + ":" + this.options.authenticationMode!.internalAuthToken!); } else if (this.authenticationMode == AuthenticationMode.APIKey) { headers.authorization = `Bearer ${this.options.authenticationMode?.apiKey}`; + } else if (this.authenticationMode == AuthenticationMode.ExternalJWT) { + headers.authorization = `Bearer ${this.options.authenticationMode?.jwt}`; } else if (this.authenticationMode == AuthenticationMode.BrowserSession) { const val = this.sessionTokenStore!.getItem(this.sessionStorageKey); if (val) {