@farm.js/workos 0.1.0-beta.97 → 0.1.0-beta.99
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +92 -20
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -14,6 +14,12 @@ export interface WorkOSIntegrationInput {
|
|
|
14
14
|
logoutPath?: string;
|
|
15
15
|
sessionPath?: string;
|
|
16
16
|
protectedRoutes?: string | string[];
|
|
17
|
+
/**
|
|
18
|
+
* Additional origins allowed to post the sign-out route, using the same
|
|
19
|
+
* pattern syntax as `serverActions.allowedOrigins`. The app's own origin is
|
|
20
|
+
* always trusted.
|
|
21
|
+
*/
|
|
22
|
+
allowedOrigins?: string[];
|
|
17
23
|
log?: FarmIntegrationLogger;
|
|
18
24
|
}
|
|
19
25
|
export type WorkOSIntegrationInstance = WorkOS;
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC1C,OAAO,EAAuC,KAAK,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAgBhG,OAAO,KAAK,EAAuB,oBAAoB,EAAuB,MAAM,aAAa,CAAC;AAQlG,MAAM,WAAW,sBAAsB;IACrC,2FAA2F;IAC3F,QAAQ,CAAC,EAAE,yBAAyB,CAAC;IACrC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IACpC;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,GAAG,CAAC,EAAE,qBAAqB,CAAC;CAC7B;AAED,MAAM,MAAM,yBAAyB,GAAG,MAAM,CAAC;AAc/C,UAAU,oBAAoB;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;CACxB;AAqFD,wBAAgB,MAAM,CAAC,KAAK,GAAE,sBAA2B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;mBAoOG,MAAM;;;;yBAyE7B,OAAO;;GAe3C"}
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
1
2
|
import { WorkOS } from "@workos-inc/node";
|
|
2
3
|
import { defineIntegration, integrationRoute } from "@farm.js/core";
|
|
4
|
+
import { describeIntegrationOriginRejection, resolveIntegrationAllowedOrigins, validateIntegrationRequestOrigin, } from "@farm.js/core/integrations";
|
|
3
5
|
import { clearRequestCookie, createPathInferredClientApi, createDocumentNavigationMatchers, createRequestCookie, getCookieValue, getReturnTo, integrationConfig, normalizeMatchers, } from "@farm.js/integration-utils";
|
|
4
6
|
import { workosClient } from "./client.js";
|
|
5
7
|
const DEV_COOKIE_PASSWORD = "farmjs-workos-cookie-password-development-2026";
|
|
@@ -75,24 +77,89 @@ export function workos(input = {}) {
|
|
|
75
77
|
const callbackPath = input.callbackPath ?? "/callback";
|
|
76
78
|
const logoutPath = input.logoutPath ?? "/logout";
|
|
77
79
|
const sessionPath = input.sessionPath ?? "/auth/session";
|
|
80
|
+
const allowedOrigins = resolveIntegrationAllowedOrigins(input.allowedOrigins, "workos.allowedOrigins");
|
|
78
81
|
const workos = input.instance ??
|
|
79
82
|
new WorkOS({
|
|
80
83
|
apiKey,
|
|
81
84
|
clientId,
|
|
82
85
|
});
|
|
86
|
+
// The OAuth `state` must be unguessable and bound to the browser that started
|
|
87
|
+
// the flow. We keep a random nonce in an HMAC-signed, http-only cookie and
|
|
88
|
+
// require the callback's state to match it, so an attacker cannot feed a
|
|
89
|
+
// victim a code/state pair from their own login (login CSRF / code injection).
|
|
90
|
+
const stateCookieName = "farm_workos_state";
|
|
91
|
+
function signState(payload) {
|
|
92
|
+
const encoded = Buffer.from(JSON.stringify(payload), "utf8").toString("base64url");
|
|
93
|
+
const signature = createHmac("sha256", cookiePassword).update(encoded).digest("hex");
|
|
94
|
+
return `${encoded}.${signature}`;
|
|
95
|
+
}
|
|
96
|
+
function unsignState(signed) {
|
|
97
|
+
if (!signed)
|
|
98
|
+
return null;
|
|
99
|
+
const separator = signed.lastIndexOf(".");
|
|
100
|
+
if (separator <= 0)
|
|
101
|
+
return null;
|
|
102
|
+
const encoded = signed.slice(0, separator);
|
|
103
|
+
const signature = signed.slice(separator + 1);
|
|
104
|
+
const expected = createHmac("sha256", cookiePassword).update(encoded).digest("hex");
|
|
105
|
+
if (signature.length !== expected.length)
|
|
106
|
+
return null;
|
|
107
|
+
if (!timingSafeEqual(Buffer.from(signature, "utf8"), Buffer.from(expected, "utf8"))) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
const parsed = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
|
|
112
|
+
if (!parsed || typeof parsed !== "object")
|
|
113
|
+
return null;
|
|
114
|
+
const { state, returnTo } = parsed;
|
|
115
|
+
if (typeof state !== "string" || typeof returnTo !== "string")
|
|
116
|
+
return null;
|
|
117
|
+
return { state, returnTo };
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Reject a sign-out request that did not come from this app. Returns the
|
|
125
|
+
* response to send, or null when the request may proceed.
|
|
126
|
+
*/
|
|
127
|
+
function rejectForeignOrigin(request) {
|
|
128
|
+
const result = validateIntegrationRequestOrigin(request, {
|
|
129
|
+
allowedOrigins,
|
|
130
|
+
requireOriginMetadata: request.method === "POST",
|
|
131
|
+
});
|
|
132
|
+
if (result.ok) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
const message = describeIntegrationOriginRejection(result.reason);
|
|
136
|
+
if (request.headers.get("x-farm-integration-client") === "1") {
|
|
137
|
+
return Response.json({ error: message }, { status: 403 });
|
|
138
|
+
}
|
|
139
|
+
return new Response(message, {
|
|
140
|
+
status: 403,
|
|
141
|
+
headers: { "content-type": "text/plain; charset=utf-8" },
|
|
142
|
+
});
|
|
143
|
+
}
|
|
83
144
|
async function redirectToAuth(request, screenHint) {
|
|
84
145
|
const requestUrl = new URL(request.url);
|
|
85
146
|
const returnTo = getReturnTo(requestUrl.searchParams.get("returnTo"), "/dashboard");
|
|
86
147
|
const callbackUrl = new URL(callbackPath, requestUrl.origin);
|
|
148
|
+
const state = randomBytes(16).toString("hex");
|
|
87
149
|
const authorizationUrl = workos.userManagement.getAuthorizationUrl({
|
|
88
150
|
provider: "authkit",
|
|
89
151
|
clientId,
|
|
90
152
|
redirectUri: callbackUrl.toString(),
|
|
91
153
|
screenHint,
|
|
92
|
-
state
|
|
154
|
+
state,
|
|
93
155
|
});
|
|
156
|
+
const headers = new Headers();
|
|
157
|
+
headers.append("set-cookie", createRequestCookie(stateCookieName, signState({ state, returnTo }), request, {
|
|
158
|
+
maxAge: 600,
|
|
159
|
+
}));
|
|
94
160
|
return {
|
|
95
|
-
redirectTo: authorizationUrl,
|
|
161
|
+
result: { redirectTo: authorizationUrl },
|
|
162
|
+
headers,
|
|
96
163
|
};
|
|
97
164
|
}
|
|
98
165
|
return defineIntegration({
|
|
@@ -135,21 +202,23 @@ export function workos(input = {}) {
|
|
|
135
202
|
integrationRoute.get(loginPath, {
|
|
136
203
|
responseFormat: "json",
|
|
137
204
|
async handler(request) {
|
|
138
|
-
const result = await redirectToAuth(request, "sign-in");
|
|
205
|
+
const { result, headers } = await redirectToAuth(request, "sign-in");
|
|
139
206
|
if (request.headers.get("x-farm-integration-client") === "1") {
|
|
140
|
-
return Response.json(result);
|
|
207
|
+
return Response.json(result, { headers });
|
|
141
208
|
}
|
|
142
|
-
|
|
209
|
+
headers.set("location", result.redirectTo);
|
|
210
|
+
return new Response(null, { status: 302, headers });
|
|
143
211
|
},
|
|
144
212
|
}),
|
|
145
213
|
integrationRoute.get(signUpPath, {
|
|
146
214
|
responseFormat: "json",
|
|
147
215
|
async handler(request) {
|
|
148
|
-
const result = await redirectToAuth(request, "sign-up");
|
|
216
|
+
const { result, headers } = await redirectToAuth(request, "sign-up");
|
|
149
217
|
if (request.headers.get("x-farm-integration-client") === "1") {
|
|
150
|
-
return Response.json(result);
|
|
218
|
+
return Response.json(result, { headers });
|
|
151
219
|
}
|
|
152
|
-
|
|
220
|
+
headers.set("location", result.redirectTo);
|
|
221
|
+
return new Response(null, { status: 302, headers });
|
|
153
222
|
},
|
|
154
223
|
}),
|
|
155
224
|
integrationRoute.get(callbackPath, {
|
|
@@ -164,6 +233,13 @@ export function workos(input = {}) {
|
|
|
164
233
|
if (!code) {
|
|
165
234
|
return new Response("Missing WorkOS authorization code.", { status: 400 });
|
|
166
235
|
}
|
|
236
|
+
// Bind the callback to the browser that started the flow. Verify before
|
|
237
|
+
// the code is exchanged so an injected code is never redeemed.
|
|
238
|
+
const statePayload = unsignState(getCookieValue(request.headers, stateCookieName));
|
|
239
|
+
const state = requestUrl.searchParams.get("state");
|
|
240
|
+
if (!statePayload || !state || statePayload.state !== state) {
|
|
241
|
+
return new Response("Invalid WorkOS authentication state.", { status: 400 });
|
|
242
|
+
}
|
|
167
243
|
const authentication = await workos.userManagement.authenticateWithCode({
|
|
168
244
|
clientId,
|
|
169
245
|
code,
|
|
@@ -175,19 +251,11 @@ export function workos(input = {}) {
|
|
|
175
251
|
if (!authentication.sealedSession) {
|
|
176
252
|
return new Response("WorkOS did not return a sealed session.", { status: 500 });
|
|
177
253
|
}
|
|
178
|
-
|
|
179
|
-
const
|
|
180
|
-
if (rawState) {
|
|
181
|
-
try {
|
|
182
|
-
const parsed = JSON.parse(rawState);
|
|
183
|
-
returnTo = getReturnTo(parsed.returnTo ?? null, "/dashboard");
|
|
184
|
-
}
|
|
185
|
-
catch {
|
|
186
|
-
returnTo = "/dashboard";
|
|
187
|
-
}
|
|
188
|
-
}
|
|
254
|
+
// Taken from the signed cookie, never from the untrusted query string.
|
|
255
|
+
const returnTo = getReturnTo(statePayload.returnTo, "/dashboard");
|
|
189
256
|
const headers = new Headers();
|
|
190
|
-
headers.
|
|
257
|
+
headers.append("set-cookie", createRequestCookie(cookieName, authentication.sealedSession, request));
|
|
258
|
+
headers.append("set-cookie", clearRequestCookie(stateCookieName, request));
|
|
191
259
|
headers.set("location", new URL(returnTo, requestUrl.origin).toString());
|
|
192
260
|
return new Response(null, {
|
|
193
261
|
status: 302,
|
|
@@ -198,6 +266,10 @@ export function workos(input = {}) {
|
|
|
198
266
|
integrationRoute.post(logoutPath, {
|
|
199
267
|
responseFormat: "json",
|
|
200
268
|
async handler(request) {
|
|
269
|
+
const rejected = rejectForeignOrigin(request);
|
|
270
|
+
if (rejected) {
|
|
271
|
+
return rejected;
|
|
272
|
+
}
|
|
201
273
|
const requestUrl = new URL(request.url);
|
|
202
274
|
const sessionState = await getSession(workos, request, cookieName, cookiePassword);
|
|
203
275
|
const headers = new Headers();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@farm.js/workos",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.99",
|
|
4
4
|
"description": "WorkOS integration for Farm.js",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@workos-inc/node": "^8.9.0",
|
|
33
|
-
"@farm.js/
|
|
34
|
-
"@farm.js/
|
|
33
|
+
"@farm.js/integration-utils": "0.1.0-beta.99",
|
|
34
|
+
"@farm.js/core": "0.1.0-beta.99"
|
|
35
35
|
},
|
|
36
36
|
"scripts": {
|
|
37
37
|
"build": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc",
|