@farm.js/auth0 0.1.0-beta.0
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/LICENSE +22 -0
- package/README.md +11 -0
- package/dist/client.d.ts +17 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +15 -0
- package/dist/index.d.ts +89 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +393 -0
- package/package.json +41 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Farm.js Team
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
package/README.md
ADDED
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface Auth0RedirectQuery {
|
|
2
|
+
returnTo?: string;
|
|
3
|
+
}
|
|
4
|
+
export interface Auth0RedirectResult {
|
|
5
|
+
redirectTo: string;
|
|
6
|
+
}
|
|
7
|
+
export interface Auth0ProfileResult {
|
|
8
|
+
authenticated: boolean;
|
|
9
|
+
user?: Record<string, unknown>;
|
|
10
|
+
}
|
|
11
|
+
export declare const auth0Client: {
|
|
12
|
+
login: import("@farm.js/core/client").FarmIntegrationAPIOperation<never, Auth0RedirectQuery, Auth0RedirectResult, false, "GET">;
|
|
13
|
+
signup: import("@farm.js/core/client").FarmIntegrationAPIOperation<never, Auth0RedirectQuery, Auth0RedirectResult, false, "GET">;
|
|
14
|
+
logout: import("@farm.js/core/client").FarmIntegrationAPIOperation<never, never, Auth0RedirectResult, false, "GET">;
|
|
15
|
+
profile: import("@farm.js/core/client").FarmIntegrationAPIOperation<never, never, Auth0ProfileResult, false, "GET">;
|
|
16
|
+
};
|
|
17
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,mBAAmB;IAClC,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,kBAAkB;IACjC,aAAa,EAAE,OAAO,CAAC;IACvB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC;AAED,eAAO,MAAM,WAAW;;;;;CAavB,CAAC"}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { api } from "@farm.js/core/client";
|
|
2
|
+
export const auth0Client = {
|
|
3
|
+
login: api.get("/auth/login", {
|
|
4
|
+
responseFormat: "json",
|
|
5
|
+
}),
|
|
6
|
+
signup: api.get("/auth/signup", {
|
|
7
|
+
responseFormat: "json",
|
|
8
|
+
}),
|
|
9
|
+
logout: api.get("/auth/logout", {
|
|
10
|
+
responseFormat: "json",
|
|
11
|
+
}),
|
|
12
|
+
profile: api.get("/auth/profile", {
|
|
13
|
+
responseFormat: "json",
|
|
14
|
+
}),
|
|
15
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { type FarmIntegrationLogger } from "@farm.js/core";
|
|
2
|
+
export interface Auth0Instance {
|
|
3
|
+
middleware(request: Request): Promise<Response | void> | Response | void;
|
|
4
|
+
matcher?: string | string[];
|
|
5
|
+
}
|
|
6
|
+
export interface Auth0IntegrationInput {
|
|
7
|
+
instance?: Auth0Instance;
|
|
8
|
+
domain?: string;
|
|
9
|
+
clientId?: string;
|
|
10
|
+
clientSecret?: string;
|
|
11
|
+
secret?: string;
|
|
12
|
+
appBaseUrl?: string;
|
|
13
|
+
callbackUrl?: string;
|
|
14
|
+
loginPath?: string;
|
|
15
|
+
signUpPath?: string;
|
|
16
|
+
callbackPath?: string;
|
|
17
|
+
logoutPath?: string;
|
|
18
|
+
profilePath?: string;
|
|
19
|
+
protectedRoutes?: string | string[];
|
|
20
|
+
audience?: string;
|
|
21
|
+
scopes?: string[];
|
|
22
|
+
tokenEndpointAuthMethod?: "auto" | "client_secret_basic" | "client_secret_post" | "none";
|
|
23
|
+
log?: FarmIntegrationLogger;
|
|
24
|
+
}
|
|
25
|
+
interface ResolvedAuth0Env {
|
|
26
|
+
domain: string;
|
|
27
|
+
clientId: string;
|
|
28
|
+
clientSecret?: string;
|
|
29
|
+
secret: string;
|
|
30
|
+
appBaseUrl?: string;
|
|
31
|
+
}
|
|
32
|
+
export declare function auth0(input?: Auth0IntegrationInput): import("@farm.js/core").DefinedIntegration<{
|
|
33
|
+
category: "auth";
|
|
34
|
+
type: string;
|
|
35
|
+
instance: Auth0Instance;
|
|
36
|
+
log: FarmIntegrationLogger | undefined;
|
|
37
|
+
middleware: {
|
|
38
|
+
matcher: string | string[];
|
|
39
|
+
handler(request: Request): void | Response | Promise<void | Response>;
|
|
40
|
+
}[];
|
|
41
|
+
}> | import("@farm.js/core").DefinedIntegration<{
|
|
42
|
+
category: "auth";
|
|
43
|
+
type: string;
|
|
44
|
+
instance: {
|
|
45
|
+
domain: string;
|
|
46
|
+
clientId: string;
|
|
47
|
+
protectedRoutes: string[];
|
|
48
|
+
};
|
|
49
|
+
config: import("@farm.js/core").FarmIntegrationConfigDefinition<ResolvedAuth0Env, import("@farm.js/core").FarmIntegrationSchema | undefined>;
|
|
50
|
+
api: {
|
|
51
|
+
[x: string]: {
|
|
52
|
+
get: {
|
|
53
|
+
readonly kind: "farm-integration-api-operation";
|
|
54
|
+
path: string;
|
|
55
|
+
method: "GET";
|
|
56
|
+
bodyFormat?: import("@farm.js/core/client").FarmIntegrationAPIBodyFormat | undefined;
|
|
57
|
+
responseFormat?: import("@farm.js/core/client").FarmIntegrationAPIResponseFormat | undefined;
|
|
58
|
+
headers?: {
|
|
59
|
+
[x: string]: string;
|
|
60
|
+
} | undefined;
|
|
61
|
+
credentials?: RequestCredentials | undefined;
|
|
62
|
+
isServer?: false | undefined;
|
|
63
|
+
__pathless?: boolean | undefined;
|
|
64
|
+
__types?: {
|
|
65
|
+
body: never;
|
|
66
|
+
query: never;
|
|
67
|
+
response: {
|
|
68
|
+
redirectTo: string;
|
|
69
|
+
authenticated: boolean;
|
|
70
|
+
user?: {
|
|
71
|
+
[x: string]: unknown;
|
|
72
|
+
} | undefined;
|
|
73
|
+
};
|
|
74
|
+
} | undefined;
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
log: FarmIntegrationLogger | undefined;
|
|
79
|
+
documentNavigations: {
|
|
80
|
+
matcher: string[];
|
|
81
|
+
}[];
|
|
82
|
+
routes: import("@farm.js/core").FarmTypedIntegrationRoute<string, never, any, unknown, false, "GET", import("@farm.js/core").FarmIntegrationSchema | undefined>[];
|
|
83
|
+
middleware: {
|
|
84
|
+
matcher: string[];
|
|
85
|
+
handler(request: Request): Response | undefined;
|
|
86
|
+
}[];
|
|
87
|
+
}>;
|
|
88
|
+
export {};
|
|
89
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAuC,KAAK,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAkBhG,MAAM,WAAW,aAAa;IAC5B,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,GAAG,QAAQ,GAAG,IAAI,CAAC;IACzE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;CAC7B;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,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,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,uBAAuB,CAAC,EAAE,MAAM,GAAG,qBAAqB,GAAG,oBAAoB,GAAG,MAAM,CAAC;IACzF,GAAG,CAAC,EAAE,qBAAqB,CAAC;CAC7B;AAED,UAAU,gBAAgB;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AA2PD,wBAAgB,KAAK,CAAC,KAAK,GAAE,qBAA0B;;;;;;;yBAU5B,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;yBA6QH,OAAO;;GAmBrC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import { createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { defineIntegration, integrationRoute } from "@farm.js/core";
|
|
3
|
+
import { clearRequestCookie, createPathInferredClientApi, createDocumentNavigationMatchers, createRequestCookie, getOrigin, getReturnTo, integrationConfig, normalizeMatchers, parseCookieHeaderMap, resolveCallbackSettings, } from "@farm.js/integration-utils";
|
|
4
|
+
import { auth0Client } from "./client.js";
|
|
5
|
+
const DEV_SECRET = "farmjs-auth0-development-secret-2026";
|
|
6
|
+
function createAuth0Api(input) {
|
|
7
|
+
return createPathInferredClientApi({
|
|
8
|
+
path: input.loginPath,
|
|
9
|
+
operation: auth0Client.login,
|
|
10
|
+
}, {
|
|
11
|
+
path: input.signUpPath,
|
|
12
|
+
operation: auth0Client.signup,
|
|
13
|
+
}, {
|
|
14
|
+
path: input.logoutPath,
|
|
15
|
+
operation: auth0Client.logout,
|
|
16
|
+
}, {
|
|
17
|
+
path: input.profilePath,
|
|
18
|
+
operation: auth0Client.profile,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
function normalizeDomain(value) {
|
|
22
|
+
return value.replace(/^https?:\/\//, "").replace(/\/+$/, "");
|
|
23
|
+
}
|
|
24
|
+
function resolveEnv(input) {
|
|
25
|
+
const domain = normalizeDomain(input.domain ?? process.env.AUTH0_DOMAIN ?? "");
|
|
26
|
+
const clientId = input.clientId ?? process.env.AUTH0_CLIENT_ID ?? "";
|
|
27
|
+
const clientSecret = input.clientSecret ?? process.env.AUTH0_CLIENT_SECRET ?? "";
|
|
28
|
+
const secret = input.secret ??
|
|
29
|
+
process.env.AUTH0_SECRET ??
|
|
30
|
+
(process.env.NODE_ENV === "production" ? "" : DEV_SECRET);
|
|
31
|
+
const appBaseUrl = input.appBaseUrl ?? process.env.APP_BASE_URL ?? undefined;
|
|
32
|
+
if (!domain || !clientId) {
|
|
33
|
+
throw new Error("Auth0 integration requires AUTH0_DOMAIN and AUTH0_CLIENT_ID.");
|
|
34
|
+
}
|
|
35
|
+
if (!secret) {
|
|
36
|
+
throw new Error("Auth0 integration requires AUTH0_SECRET in production.");
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
domain,
|
|
40
|
+
clientId,
|
|
41
|
+
clientSecret: clientSecret || undefined,
|
|
42
|
+
secret,
|
|
43
|
+
appBaseUrl,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function createCodeVerifier() {
|
|
47
|
+
return randomBytes(32).toString("base64url");
|
|
48
|
+
}
|
|
49
|
+
function createCodeChallenge(verifier) {
|
|
50
|
+
return createHash("sha256").update(verifier).digest("base64url");
|
|
51
|
+
}
|
|
52
|
+
function signValue(value, secret) {
|
|
53
|
+
const payload = Buffer.from(JSON.stringify(value), "utf8").toString("base64url");
|
|
54
|
+
const signature = createHmac("sha256", secret).update(payload).digest("hex");
|
|
55
|
+
return `${payload}.${signature}`;
|
|
56
|
+
}
|
|
57
|
+
function unsignValue(signed, secret) {
|
|
58
|
+
if (!signed) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
const separator = signed.lastIndexOf(".");
|
|
62
|
+
if (separator === -1) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
const payload = signed.slice(0, separator);
|
|
66
|
+
const signature = signed.slice(separator + 1);
|
|
67
|
+
const expected = createHmac("sha256", secret).update(payload).digest("hex");
|
|
68
|
+
if (signature.length !== expected.length) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
if (!timingSafeEqual(Buffer.from(signature, "utf8"), Buffer.from(expected, "utf8"))) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
return JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function decodeJwtPayload(token) {
|
|
82
|
+
if (!token) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
const parts = token.split(".");
|
|
86
|
+
if (parts.length < 2) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
return JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function getSession(request, cookieName, secret) {
|
|
97
|
+
const cookies = parseCookieHeaderMap(request.headers.get("cookie"));
|
|
98
|
+
const session = unsignValue(cookies[cookieName], secret);
|
|
99
|
+
if (!session) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
if (session.expiresAt <= Date.now()) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
return session;
|
|
106
|
+
}
|
|
107
|
+
async function createUserProfile(domain, accessToken, idToken) {
|
|
108
|
+
if (accessToken) {
|
|
109
|
+
const response = await fetch(`https://${domain}/userinfo`, {
|
|
110
|
+
headers: {
|
|
111
|
+
authorization: `Bearer ${accessToken}`,
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
if (response.ok) {
|
|
115
|
+
return (await response.json());
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return decodeJwtPayload(idToken) || {};
|
|
119
|
+
}
|
|
120
|
+
async function exchangeCode(domain, clientId, clientSecret, code, redirectUri, codeVerifier, tokenEndpointAuthMethod = "auto") {
|
|
121
|
+
const methods = tokenEndpointAuthMethod && tokenEndpointAuthMethod !== "auto"
|
|
122
|
+
? [tokenEndpointAuthMethod]
|
|
123
|
+
: clientSecret
|
|
124
|
+
? ["client_secret_basic", "client_secret_post", "none"]
|
|
125
|
+
: ["none"];
|
|
126
|
+
let lastFailure = "Auth0 token exchange failed.";
|
|
127
|
+
for (const method of methods) {
|
|
128
|
+
if ((method === "client_secret_basic" || method === "client_secret_post") && !clientSecret) {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const body = new URLSearchParams({
|
|
132
|
+
grant_type: "authorization_code",
|
|
133
|
+
code,
|
|
134
|
+
redirect_uri: redirectUri,
|
|
135
|
+
client_id: clientId,
|
|
136
|
+
code_verifier: codeVerifier,
|
|
137
|
+
});
|
|
138
|
+
const headers = new Headers({
|
|
139
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
140
|
+
});
|
|
141
|
+
if (method === "client_secret_post") {
|
|
142
|
+
body.set("client_secret", clientSecret);
|
|
143
|
+
}
|
|
144
|
+
if (method === "client_secret_basic") {
|
|
145
|
+
headers.set("authorization", `Basic ${Buffer.from(`${clientId}:${clientSecret}`, "utf8").toString("base64")}`);
|
|
146
|
+
body.delete("client_id");
|
|
147
|
+
}
|
|
148
|
+
const response = await fetch(`https://${domain}/oauth/token`, {
|
|
149
|
+
method: "POST",
|
|
150
|
+
headers,
|
|
151
|
+
body,
|
|
152
|
+
});
|
|
153
|
+
if (response.ok) {
|
|
154
|
+
return (await response.json());
|
|
155
|
+
}
|
|
156
|
+
const message = await response.text();
|
|
157
|
+
lastFailure = message || "Auth0 token exchange failed.";
|
|
158
|
+
// Retry only when Auth0 is rejecting the client authentication method.
|
|
159
|
+
if (response.status === 401 || response.status === 403) {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
throw new Error(lastFailure);
|
|
163
|
+
}
|
|
164
|
+
throw new Error(lastFailure);
|
|
165
|
+
}
|
|
166
|
+
export function auth0(input = {}) {
|
|
167
|
+
if (input.instance) {
|
|
168
|
+
return defineIntegration({
|
|
169
|
+
category: "auth",
|
|
170
|
+
type: "auth0",
|
|
171
|
+
instance: input.instance,
|
|
172
|
+
log: input.log,
|
|
173
|
+
middleware: [
|
|
174
|
+
{
|
|
175
|
+
matcher: input.instance.matcher || ["/auth(.*)"],
|
|
176
|
+
handler(request) {
|
|
177
|
+
return input.instance.middleware(request);
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
const { domain, clientId, clientSecret, secret, appBaseUrl } = resolveEnv(input);
|
|
184
|
+
const protectedMatchers = normalizeMatchers(input.protectedRoutes);
|
|
185
|
+
const loginPath = input.loginPath ?? "/auth/login";
|
|
186
|
+
const signUpPath = input.signUpPath ?? "/auth/signup";
|
|
187
|
+
const logoutPath = input.logoutPath ?? "/auth/logout";
|
|
188
|
+
const profilePath = input.profilePath ?? "/auth/profile";
|
|
189
|
+
const scopes = input.scopes?.length ? input.scopes : ["openid", "profile", "email"];
|
|
190
|
+
const stateCookieName = "farm_auth0_state";
|
|
191
|
+
const sessionCookieName = "farm_auth0_session";
|
|
192
|
+
const callbackSettings = resolveCallbackSettings({
|
|
193
|
+
callbackUrl: input.callbackUrl,
|
|
194
|
+
callbackPath: input.callbackPath,
|
|
195
|
+
defaultPath: "/auth/callback",
|
|
196
|
+
label: "Auth0 integration",
|
|
197
|
+
}, appBaseUrl);
|
|
198
|
+
const callbackPath = callbackSettings.callbackPath;
|
|
199
|
+
async function redirectToAuth(request, screenHint) {
|
|
200
|
+
const requestUrl = new URL(request.url);
|
|
201
|
+
const callbackUrl = callbackSettings.getCallbackUrl(request);
|
|
202
|
+
const returnTo = getReturnTo(requestUrl.searchParams.get("returnTo"), "/dashboard");
|
|
203
|
+
const state = randomBytes(16).toString("hex");
|
|
204
|
+
const codeVerifier = createCodeVerifier();
|
|
205
|
+
const codeChallenge = createCodeChallenge(codeVerifier);
|
|
206
|
+
const authorizeUrl = new URL("/authorize", `https://${domain}`);
|
|
207
|
+
authorizeUrl.searchParams.set("response_type", "code");
|
|
208
|
+
authorizeUrl.searchParams.set("client_id", clientId);
|
|
209
|
+
authorizeUrl.searchParams.set("redirect_uri", callbackUrl);
|
|
210
|
+
authorizeUrl.searchParams.set("scope", scopes.join(" "));
|
|
211
|
+
authorizeUrl.searchParams.set("state", state);
|
|
212
|
+
authorizeUrl.searchParams.set("code_challenge", codeChallenge);
|
|
213
|
+
authorizeUrl.searchParams.set("code_challenge_method", "S256");
|
|
214
|
+
if (input.audience) {
|
|
215
|
+
authorizeUrl.searchParams.set("audience", input.audience);
|
|
216
|
+
}
|
|
217
|
+
if (screenHint) {
|
|
218
|
+
authorizeUrl.searchParams.set("screen_hint", screenHint);
|
|
219
|
+
}
|
|
220
|
+
const headers = new Headers();
|
|
221
|
+
headers.append("set-cookie", createRequestCookie(stateCookieName, signValue({ state, returnTo, codeVerifier }, secret), request, { maxAge: 600 }));
|
|
222
|
+
return {
|
|
223
|
+
redirectTo: authorizeUrl.toString(),
|
|
224
|
+
headers,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
return defineIntegration({
|
|
228
|
+
category: "auth",
|
|
229
|
+
type: "auth0",
|
|
230
|
+
instance: {
|
|
231
|
+
domain,
|
|
232
|
+
clientId,
|
|
233
|
+
protectedRoutes: protectedMatchers,
|
|
234
|
+
},
|
|
235
|
+
config: integrationConfig({
|
|
236
|
+
label: "Auth0 integration",
|
|
237
|
+
env: {
|
|
238
|
+
domain: "AUTH0_DOMAIN",
|
|
239
|
+
clientId: "AUTH0_CLIENT_ID",
|
|
240
|
+
clientSecret: "AUTH0_CLIENT_SECRET",
|
|
241
|
+
secret: "AUTH0_SECRET",
|
|
242
|
+
appBaseUrl: "APP_BASE_URL",
|
|
243
|
+
},
|
|
244
|
+
input: {
|
|
245
|
+
domain,
|
|
246
|
+
clientId,
|
|
247
|
+
clientSecret,
|
|
248
|
+
secret,
|
|
249
|
+
appBaseUrl,
|
|
250
|
+
},
|
|
251
|
+
required: ["domain", "clientId", "secret"],
|
|
252
|
+
}),
|
|
253
|
+
api: createAuth0Api({
|
|
254
|
+
loginPath,
|
|
255
|
+
signUpPath,
|
|
256
|
+
logoutPath,
|
|
257
|
+
profilePath,
|
|
258
|
+
}),
|
|
259
|
+
log: input.log,
|
|
260
|
+
documentNavigations: [
|
|
261
|
+
{
|
|
262
|
+
matcher: createDocumentNavigationMatchers(loginPath, signUpPath, callbackPath, logoutPath),
|
|
263
|
+
},
|
|
264
|
+
],
|
|
265
|
+
routes: [
|
|
266
|
+
integrationRoute.get(loginPath, {
|
|
267
|
+
responseFormat: "json",
|
|
268
|
+
async handler(request) {
|
|
269
|
+
const result = await redirectToAuth(request);
|
|
270
|
+
const headers = result.headers;
|
|
271
|
+
const redirectTo = result.redirectTo;
|
|
272
|
+
if (request.headers.get("x-farm-integration-client") === "1") {
|
|
273
|
+
return Response.json({ redirectTo }, { status: 200, headers });
|
|
274
|
+
}
|
|
275
|
+
headers.set("location", redirectTo);
|
|
276
|
+
return new Response(null, {
|
|
277
|
+
status: 302,
|
|
278
|
+
headers,
|
|
279
|
+
});
|
|
280
|
+
},
|
|
281
|
+
}),
|
|
282
|
+
integrationRoute.get(signUpPath, {
|
|
283
|
+
responseFormat: "json",
|
|
284
|
+
async handler(request) {
|
|
285
|
+
const result = await redirectToAuth(request, "signup");
|
|
286
|
+
const headers = result.headers;
|
|
287
|
+
const redirectTo = result.redirectTo;
|
|
288
|
+
if (request.headers.get("x-farm-integration-client") === "1") {
|
|
289
|
+
return Response.json({ redirectTo }, { status: 200, headers });
|
|
290
|
+
}
|
|
291
|
+
headers.set("location", redirectTo);
|
|
292
|
+
return new Response(null, {
|
|
293
|
+
status: 302,
|
|
294
|
+
headers,
|
|
295
|
+
});
|
|
296
|
+
},
|
|
297
|
+
}),
|
|
298
|
+
integrationRoute.get(callbackPath, {
|
|
299
|
+
handler(request) {
|
|
300
|
+
return (async () => {
|
|
301
|
+
const requestUrl = new URL(request.url);
|
|
302
|
+
const error = requestUrl.searchParams.get("error");
|
|
303
|
+
const errorDescription = requestUrl.searchParams.get("error_description");
|
|
304
|
+
if (error) {
|
|
305
|
+
return new Response(errorDescription || error, { status: 400 });
|
|
306
|
+
}
|
|
307
|
+
const code = requestUrl.searchParams.get("code");
|
|
308
|
+
const state = requestUrl.searchParams.get("state");
|
|
309
|
+
if (!code || !state) {
|
|
310
|
+
return new Response("Missing Auth0 authorization code or state.", { status: 400 });
|
|
311
|
+
}
|
|
312
|
+
const cookies = parseCookieHeaderMap(request.headers.get("cookie"));
|
|
313
|
+
const statePayload = unsignValue(cookies[stateCookieName], secret);
|
|
314
|
+
if (!statePayload || statePayload.state !== state) {
|
|
315
|
+
return new Response("Invalid Auth0 state.", { status: 400 });
|
|
316
|
+
}
|
|
317
|
+
const origin = getOrigin(request, appBaseUrl);
|
|
318
|
+
const redirectUri = callbackSettings.getCallbackUrl(request);
|
|
319
|
+
const tokenResponse = await exchangeCode(domain, clientId, clientSecret, code, redirectUri, statePayload.codeVerifier, input.tokenEndpointAuthMethod);
|
|
320
|
+
const user = await createUserProfile(domain, tokenResponse.access_token, tokenResponse.id_token);
|
|
321
|
+
const expiresAt = Date.now() + Math.max(tokenResponse.expires_in || 3600, 60) * 1000;
|
|
322
|
+
const headers = new Headers();
|
|
323
|
+
headers.append("set-cookie", createRequestCookie(sessionCookieName, signValue({ user, expiresAt }, secret), request, {
|
|
324
|
+
maxAge: Math.max((expiresAt - Date.now()) / 1000, 60),
|
|
325
|
+
}));
|
|
326
|
+
headers.append("set-cookie", clearRequestCookie(stateCookieName, request));
|
|
327
|
+
headers.set("location", new URL(statePayload.returnTo || "/dashboard", origin).toString());
|
|
328
|
+
return new Response(null, {
|
|
329
|
+
status: 302,
|
|
330
|
+
headers,
|
|
331
|
+
});
|
|
332
|
+
})();
|
|
333
|
+
},
|
|
334
|
+
}),
|
|
335
|
+
integrationRoute.get(logoutPath, {
|
|
336
|
+
responseFormat: "json",
|
|
337
|
+
handler(request) {
|
|
338
|
+
const origin = getOrigin(request, appBaseUrl);
|
|
339
|
+
const redirectTo = new URL("/v2/logout", `https://${domain}`);
|
|
340
|
+
redirectTo.searchParams.set("client_id", clientId);
|
|
341
|
+
redirectTo.searchParams.set("returnTo", origin);
|
|
342
|
+
const headers = new Headers();
|
|
343
|
+
headers.append("set-cookie", clearRequestCookie(sessionCookieName, request));
|
|
344
|
+
if (request.headers.get("x-farm-integration-client") === "1") {
|
|
345
|
+
return Response.json({
|
|
346
|
+
redirectTo: redirectTo.toString(),
|
|
347
|
+
}, {
|
|
348
|
+
status: 200,
|
|
349
|
+
headers,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
headers.set("location", redirectTo.toString());
|
|
353
|
+
return new Response(null, {
|
|
354
|
+
status: 302,
|
|
355
|
+
headers,
|
|
356
|
+
});
|
|
357
|
+
},
|
|
358
|
+
}),
|
|
359
|
+
integrationRoute.get(profilePath, {
|
|
360
|
+
responseFormat: "json",
|
|
361
|
+
handler(request) {
|
|
362
|
+
const session = getSession(request, sessionCookieName, secret);
|
|
363
|
+
if (!session) {
|
|
364
|
+
return Response.json({
|
|
365
|
+
authenticated: false,
|
|
366
|
+
}, { status: 401 });
|
|
367
|
+
}
|
|
368
|
+
return Response.json({
|
|
369
|
+
authenticated: true,
|
|
370
|
+
user: session.user,
|
|
371
|
+
});
|
|
372
|
+
},
|
|
373
|
+
}),
|
|
374
|
+
],
|
|
375
|
+
middleware: protectedMatchers.length > 0
|
|
376
|
+
? [
|
|
377
|
+
{
|
|
378
|
+
matcher: protectedMatchers,
|
|
379
|
+
handler(request) {
|
|
380
|
+
const session = getSession(request, sessionCookieName, secret);
|
|
381
|
+
if (session) {
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
const requestUrl = new URL(request.url);
|
|
385
|
+
const redirectUrl = new URL(loginPath, getOrigin(request, appBaseUrl));
|
|
386
|
+
redirectUrl.searchParams.set("returnTo", `${requestUrl.pathname}${requestUrl.search}`);
|
|
387
|
+
return Response.redirect(redirectUrl, 307);
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
]
|
|
391
|
+
: [],
|
|
392
|
+
});
|
|
393
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@farm.js/auth0",
|
|
3
|
+
"version": "0.1.0-beta.0",
|
|
4
|
+
"description": "Auth0 integration for Farm.js",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/farming-labs/farm.js",
|
|
9
|
+
"directory": "packages/farm-auth0"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist"
|
|
13
|
+
],
|
|
14
|
+
"type": "module",
|
|
15
|
+
"main": "./dist/index.js",
|
|
16
|
+
"module": "./dist/index.js",
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"import": "./dist/index.js"
|
|
22
|
+
},
|
|
23
|
+
"./client": {
|
|
24
|
+
"types": "./dist/client.d.ts",
|
|
25
|
+
"import": "./dist/client.js"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@farm.js/core": "0.1.0-beta.0",
|
|
33
|
+
"@farm.js/integration-utils": "0.1.0-beta.0"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc",
|
|
37
|
+
"dev": "tsc --watch",
|
|
38
|
+
"type-check": "tsc --noEmit",
|
|
39
|
+
"test": "echo 'No tests in this package'"
|
|
40
|
+
}
|
|
41
|
+
}
|