Skip to content

Commit 08744de

Browse files
authored
support custom auth api domain (#52)
1 parent 618c892 commit 08744de

4 files changed

Lines changed: 131 additions & 8 deletions

File tree

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,17 @@ authKit.registerRoutes(http);
8888
export default http;
8989
```
9090

91+
## Custom domain
92+
93+
If you've configured a [custom WorkOS authentication API domain](https://workos.com/docs/custom-domains/auth-api), pass it as `apiHostname` when constructing the AuthKit client. This routes the WorkOS SDK through your domain and updates the JWT issuer and JWKS URLs to match.
94+
95+
```ts
96+
// convex/auth.ts
97+
export const authKit = new AuthKit<DataModel>(components.workOSAuthKit, {
98+
apiHostname: "auth.example.com",
99+
});
100+
```
101+
91102
## Usage
92103

93104
User create/update/delete in WorkOS will be automatically synced by the

eslint.config.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ export default [
1010
ignores: [
1111
"dist/**",
1212
"example/dist/**",
13-
"*.config.{js,mjs,cjs,ts,tsx}",
14-
"example/**/*.config.{js,mjs,cjs,ts,tsx}",
1513
"**/_generated/",
1614
"initTemplate.mjs",
1715
],

src/client/index.test.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/// <reference types="vite/client" />
2+
import { describe, test, expect, vi, beforeEach, afterEach } from "vitest";
3+
import { WorkOS } from "@workos-inc/node";
4+
import { AuthKit } from "./index.js";
5+
import type { ComponentApi } from "../component/_generated/component.js";
6+
7+
vi.mock("@workos-inc/node", () => {
8+
return {
9+
WorkOS: vi.fn().mockImplementation(function () {
10+
return {};
11+
}),
12+
};
13+
});
14+
15+
const requiredEnv = {
16+
WORKOS_CLIENT_ID: "client_test",
17+
WORKOS_API_KEY: "sk_test",
18+
WORKOS_WEBHOOK_SECRET: "whsec_test",
19+
};
20+
21+
const fakeComponent = {} as ComponentApi;
22+
23+
describe("AuthKit constructor", () => {
24+
beforeEach(() => {
25+
for (const [k, v] of Object.entries(requiredEnv)) {
26+
process.env[k] = v;
27+
}
28+
});
29+
30+
afterEach(() => {
31+
for (const k of Object.keys(requiredEnv)) {
32+
delete process.env[k];
33+
}
34+
vi.clearAllMocks();
35+
});
36+
37+
describe("apiHostname", () => {
38+
test("forwards apiHostname option to the WorkOS SDK", () => {
39+
new AuthKit(fakeComponent, { apiHostname: "auth.example.com" });
40+
expect(vi.mocked(WorkOS)).toHaveBeenCalledWith(
41+
"sk_test",
42+
expect.objectContaining({ apiHostname: "auth.example.com" })
43+
);
44+
});
45+
46+
test("forwards undefined when option is not set", () => {
47+
new AuthKit(fakeComponent);
48+
expect(vi.mocked(WorkOS)).toHaveBeenCalledWith(
49+
"sk_test",
50+
expect.objectContaining({ apiHostname: undefined })
51+
);
52+
});
53+
});
54+
55+
test("clientId is forwarded to the WorkOS SDK", () => {
56+
new AuthKit(fakeComponent);
57+
expect(vi.mocked(WorkOS)).toHaveBeenCalledWith(
58+
"sk_test",
59+
expect.objectContaining({ clientId: "client_test" })
60+
);
61+
});
62+
});
63+
64+
describe("AuthKit.getAuthConfigProviders", () => {
65+
beforeEach(() => {
66+
for (const [k, v] of Object.entries(requiredEnv)) {
67+
process.env[k] = v;
68+
}
69+
});
70+
71+
afterEach(() => {
72+
for (const k of Object.keys(requiredEnv)) {
73+
delete process.env[k];
74+
}
75+
vi.clearAllMocks();
76+
});
77+
78+
test("falls back to api.workos.com when no custom hostname is set", () => {
79+
const authKit = new AuthKit(fakeComponent);
80+
const providers = authKit.getAuthConfigProviders();
81+
expect(providers[0].issuer).toBe("https://api.workos.com/");
82+
expect(providers[0].jwks).toBe(
83+
"https://api.workos.com/sso/jwks/client_test"
84+
);
85+
expect(providers[1].issuer).toBe(
86+
"https://api.workos.com/user_management/client_test"
87+
);
88+
expect(providers[1].jwks).toBe(
89+
"https://api.workos.com/sso/jwks/client_test"
90+
);
91+
});
92+
93+
test("custom hostname rewrites issuer but not jwks", () => {
94+
const authKit = new AuthKit(fakeComponent, {
95+
apiHostname: "auth.example.com",
96+
});
97+
const providers = authKit.getAuthConfigProviders();
98+
expect(providers[0].issuer).toBe("https://auth.example.com/");
99+
expect(providers[0].jwks).toBe(
100+
"https://api.workos.com/sso/jwks/client_test"
101+
);
102+
expect(providers[1].issuer).toBe(
103+
"https://auth.example.com/user_management/client_test"
104+
);
105+
expect(providers[1].jwks).toBe(
106+
"https://api.workos.com/sso/jwks/client_test"
107+
);
108+
});
109+
});

src/client/index.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import {
66
type HttpRouter,
77
createFunctionHandle,
88
httpActionGeneric,
9-
internalActionGeneric,
109
internalMutationGeneric,
1110
} from "convex/server";
1211
import type { RunQueryCtx } from "./types.js";
@@ -31,6 +30,7 @@ type Options = {
3130
authFunctions?: AuthFunctions;
3231
clientId?: string;
3332
apiKey?: string;
33+
apiHostname?: string;
3434
webhookSecret?: string;
3535
webhookPath?: string;
3636
additionalEventTypes?: WorkOSEvent["event"][];
@@ -97,25 +97,30 @@ export class AuthKit<DataModel extends GenericDataModel> {
9797
actionSecret: options?.actionSecret ?? process.env.WORKOS_ACTION_SECRET,
9898
webhookPath: options?.webhookPath ?? "/workos/webhook",
9999
};
100-
this.workos = new WorkOS(this.config.apiKey);
100+
this.workos = new WorkOS(this.config.apiKey, {
101+
clientId: this.config.clientId,
102+
apiHostname: this.config.apiHostname,
103+
});
101104
}
102105

103-
getAuthConfigProviders = () =>
104-
[
106+
getAuthConfigProviders = () => {
107+
const apiBaseUrl = `https://${this.config.apiHostname ?? "api.workos.com"}`;
108+
return [
105109
{
106110
type: "customJwt",
107-
issuer: `https://api.workos.com/`,
111+
issuer: `${apiBaseUrl}/`,
108112
algorithm: "RS256",
109113
jwks: `https://api.workos.com/sso/jwks/${this.config.clientId}`,
110114
applicationID: this.config.clientId,
111115
},
112116
{
113117
type: "customJwt",
114-
issuer: `https://api.workos.com/user_management/${this.config.clientId}`,
118+
issuer: `${apiBaseUrl}/user_management/${this.config.clientId}`,
115119
algorithm: "RS256",
116120
jwks: `https://api.workos.com/sso/jwks/${this.config.clientId}`,
117121
},
118122
] satisfies AuthConfig["providers"];
123+
};
119124

120125
async getAuthUser(ctx: RunQueryCtx) {
121126
const identity = await ctx.auth.getUserIdentity();

0 commit comments

Comments
 (0)