Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions packages/api-client-core/spec/GadgetConnection-suite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
31 changes: 23 additions & 8 deletions packages/api-client-core/src/ClientOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>;
processTransactionConnectionParams(params: Record<string, any>): Promise<void>;
Expand Down
7 changes: 7 additions & 0 deletions packages/api-client-core/src/GadgetConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export enum AuthenticationMode {
APIKey = "api-key",
InternalAuthToken = "internal-auth-token",
Anonymous = "anonymous",
ExternalJWT = "jwt",
Custom = "custom",
}

Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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) {
Expand Down