@farm.js/workos 0.1.0-beta.97 → 0.1.0-beta.98

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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC1C,OAAO,EAAuC,KAAK,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAWhG,OAAO,KAAK,EAAuB,oBAAoB,EAAuB,MAAM,aAAa,CAAC;AAGlG,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,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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;mBAsJG,MAAM;;;;yBAoE7B,OAAO;;GAe3C"}
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;AAWhG,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,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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;mBAqMG,MAAM;;;;yBAoE7B,OAAO;;GAe3C"}
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
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";
3
4
  import { clearRequestCookie, createPathInferredClientApi, createDocumentNavigationMatchers, createRequestCookie, getCookieValue, getReturnTo, integrationConfig, normalizeMatchers, } from "@farm.js/integration-utils";
@@ -80,19 +81,62 @@ export function workos(input = {}) {
80
81
  apiKey,
81
82
  clientId,
82
83
  });
84
+ // The OAuth `state` must be unguessable and bound to the browser that started
85
+ // the flow. We keep a random nonce in an HMAC-signed, http-only cookie and
86
+ // require the callback's state to match it, so an attacker cannot feed a
87
+ // victim a code/state pair from their own login (login CSRF / code injection).
88
+ const stateCookieName = "farm_workos_state";
89
+ function signState(payload) {
90
+ const encoded = Buffer.from(JSON.stringify(payload), "utf8").toString("base64url");
91
+ const signature = createHmac("sha256", cookiePassword).update(encoded).digest("hex");
92
+ return `${encoded}.${signature}`;
93
+ }
94
+ function unsignState(signed) {
95
+ if (!signed)
96
+ return null;
97
+ const separator = signed.lastIndexOf(".");
98
+ if (separator <= 0)
99
+ return null;
100
+ const encoded = signed.slice(0, separator);
101
+ const signature = signed.slice(separator + 1);
102
+ const expected = createHmac("sha256", cookiePassword).update(encoded).digest("hex");
103
+ if (signature.length !== expected.length)
104
+ return null;
105
+ if (!timingSafeEqual(Buffer.from(signature, "utf8"), Buffer.from(expected, "utf8"))) {
106
+ return null;
107
+ }
108
+ try {
109
+ const parsed = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
110
+ if (!parsed || typeof parsed !== "object")
111
+ return null;
112
+ const { state, returnTo } = parsed;
113
+ if (typeof state !== "string" || typeof returnTo !== "string")
114
+ return null;
115
+ return { state, returnTo };
116
+ }
117
+ catch {
118
+ return null;
119
+ }
120
+ }
83
121
  async function redirectToAuth(request, screenHint) {
84
122
  const requestUrl = new URL(request.url);
85
123
  const returnTo = getReturnTo(requestUrl.searchParams.get("returnTo"), "/dashboard");
86
124
  const callbackUrl = new URL(callbackPath, requestUrl.origin);
125
+ const state = randomBytes(16).toString("hex");
87
126
  const authorizationUrl = workos.userManagement.getAuthorizationUrl({
88
127
  provider: "authkit",
89
128
  clientId,
90
129
  redirectUri: callbackUrl.toString(),
91
130
  screenHint,
92
- state: JSON.stringify({ returnTo }),
131
+ state,
93
132
  });
133
+ const headers = new Headers();
134
+ headers.append("set-cookie", createRequestCookie(stateCookieName, signState({ state, returnTo }), request, {
135
+ maxAge: 600,
136
+ }));
94
137
  return {
95
- redirectTo: authorizationUrl,
138
+ result: { redirectTo: authorizationUrl },
139
+ headers,
96
140
  };
97
141
  }
98
142
  return defineIntegration({
@@ -135,21 +179,23 @@ export function workos(input = {}) {
135
179
  integrationRoute.get(loginPath, {
136
180
  responseFormat: "json",
137
181
  async handler(request) {
138
- const result = await redirectToAuth(request, "sign-in");
182
+ const { result, headers } = await redirectToAuth(request, "sign-in");
139
183
  if (request.headers.get("x-farm-integration-client") === "1") {
140
- return Response.json(result);
184
+ return Response.json(result, { headers });
141
185
  }
142
- return Response.redirect(result.redirectTo, 302);
186
+ headers.set("location", result.redirectTo);
187
+ return new Response(null, { status: 302, headers });
143
188
  },
144
189
  }),
145
190
  integrationRoute.get(signUpPath, {
146
191
  responseFormat: "json",
147
192
  async handler(request) {
148
- const result = await redirectToAuth(request, "sign-up");
193
+ const { result, headers } = await redirectToAuth(request, "sign-up");
149
194
  if (request.headers.get("x-farm-integration-client") === "1") {
150
- return Response.json(result);
195
+ return Response.json(result, { headers });
151
196
  }
152
- return Response.redirect(result.redirectTo, 302);
197
+ headers.set("location", result.redirectTo);
198
+ return new Response(null, { status: 302, headers });
153
199
  },
154
200
  }),
155
201
  integrationRoute.get(callbackPath, {
@@ -164,6 +210,13 @@ export function workos(input = {}) {
164
210
  if (!code) {
165
211
  return new Response("Missing WorkOS authorization code.", { status: 400 });
166
212
  }
213
+ // Bind the callback to the browser that started the flow. Verify before
214
+ // the code is exchanged so an injected code is never redeemed.
215
+ const statePayload = unsignState(getCookieValue(request.headers, stateCookieName));
216
+ const state = requestUrl.searchParams.get("state");
217
+ if (!statePayload || !state || statePayload.state !== state) {
218
+ return new Response("Invalid WorkOS authentication state.", { status: 400 });
219
+ }
167
220
  const authentication = await workos.userManagement.authenticateWithCode({
168
221
  clientId,
169
222
  code,
@@ -175,19 +228,11 @@ export function workos(input = {}) {
175
228
  if (!authentication.sealedSession) {
176
229
  return new Response("WorkOS did not return a sealed session.", { status: 500 });
177
230
  }
178
- let returnTo = "/dashboard";
179
- const rawState = requestUrl.searchParams.get("state");
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
- }
231
+ // Taken from the signed cookie, never from the untrusted query string.
232
+ const returnTo = getReturnTo(statePayload.returnTo, "/dashboard");
189
233
  const headers = new Headers();
190
- headers.set("set-cookie", createRequestCookie(cookieName, authentication.sealedSession, request));
234
+ headers.append("set-cookie", createRequestCookie(cookieName, authentication.sealedSession, request));
235
+ headers.append("set-cookie", clearRequestCookie(stateCookieName, request));
191
236
  headers.set("location", new URL(returnTo, requestUrl.origin).toString());
192
237
  return new Response(null, {
193
238
  status: 302,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@farm.js/workos",
3
- "version": "0.1.0-beta.97",
3
+ "version": "0.1.0-beta.98",
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/core": "0.1.0-beta.97",
34
- "@farm.js/integration-utils": "0.1.0-beta.97"
33
+ "@farm.js/core": "0.1.0-beta.98",
34
+ "@farm.js/integration-utils": "0.1.0-beta.98"
35
35
  },
36
36
  "scripts": {
37
37
  "build": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc",