better-auth 1.7.0-rc.2 → 1.7.0-rc.3
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/api/index.d.mts +30 -0
- package/dist/api/routes/sign-out.d.mts +16 -0
- package/dist/api/routes/sign-out.mjs +68 -2
- package/dist/client/path-to-object.d.mts +3 -2
- package/dist/package.mjs +1 -1
- package/dist/plugins/device-authorization/index.d.mts +41 -2
- package/dist/plugins/device-authorization/index.mjs +3 -3
- package/dist/plugins/device-authorization/routes.mjs +81 -10
- package/dist/plugins/device-authorization/schema.mjs +47 -39
- package/dist/plugins/generic-oauth/index.d.mts +1 -1
- package/dist/plugins/generic-oauth/index.mjs +22 -0
- package/dist/plugins/generic-oauth/providers/auth0.mjs +3 -0
- package/dist/plugins/generic-oauth/providers/gumroad.mjs +3 -0
- package/dist/plugins/generic-oauth/providers/hubspot.mjs +3 -0
- package/dist/plugins/generic-oauth/providers/keycloak.mjs +3 -0
- package/dist/plugins/generic-oauth/providers/line.mjs +3 -0
- package/dist/plugins/generic-oauth/providers/microsoft-entra-id.mjs +35 -9
- package/dist/plugins/generic-oauth/providers/okta.mjs +3 -0
- package/dist/plugins/generic-oauth/providers/patreon.mjs +3 -0
- package/dist/plugins/generic-oauth/providers/slack.mjs +3 -0
- package/dist/plugins/generic-oauth/providers/yandex.mjs +3 -0
- package/dist/plugins/generic-oauth/types.d.mts +27 -0
- package/dist/plugins/jwt/adapter.mjs +7 -6
- package/dist/plugins/one-tap/client.d.mts +3 -2
- package/dist/test-utils/http-test-instance.mjs +2 -1
- package/dist/test-utils/test-instance.d.mts +1 -0
- package/dist/test-utils/test-instance.mjs +4 -2
- package/package.json +8 -8
package/dist/api/index.d.mts
CHANGED
|
@@ -290,6 +290,11 @@ declare function getEndpoints<Option extends BetterAuthOptions>(ctx: Awaitable<A
|
|
|
290
290
|
} | null>;
|
|
291
291
|
readonly signOut: import("better-call").StrictEndpoint<"/sign-out", {
|
|
292
292
|
method: "POST";
|
|
293
|
+
body: import("zod").ZodOptional<import("zod").ZodObject<{
|
|
294
|
+
callbackURL: import("zod").ZodOptional<import("zod").ZodString>;
|
|
295
|
+
disableRedirect: import("zod").ZodOptional<import("zod").ZodBoolean>;
|
|
296
|
+
state: import("zod").ZodOptional<import("zod").ZodString>;
|
|
297
|
+
}, import("zod/v4/core").$strip>>;
|
|
293
298
|
operationId: string;
|
|
294
299
|
requireHeaders: true;
|
|
295
300
|
metadata: {
|
|
@@ -307,6 +312,14 @@ declare function getEndpoints<Option extends BetterAuthOptions>(ctx: Awaitable<A
|
|
|
307
312
|
success: {
|
|
308
313
|
type: string;
|
|
309
314
|
};
|
|
315
|
+
url: {
|
|
316
|
+
type: string;
|
|
317
|
+
description: string;
|
|
318
|
+
};
|
|
319
|
+
redirect: {
|
|
320
|
+
type: string;
|
|
321
|
+
description: string;
|
|
322
|
+
};
|
|
310
323
|
};
|
|
311
324
|
};
|
|
312
325
|
};
|
|
@@ -317,6 +330,8 @@ declare function getEndpoints<Option extends BetterAuthOptions>(ctx: Awaitable<A
|
|
|
317
330
|
};
|
|
318
331
|
}, {
|
|
319
332
|
success: boolean;
|
|
333
|
+
url: string | undefined;
|
|
334
|
+
redirect: boolean | undefined;
|
|
320
335
|
}>;
|
|
321
336
|
readonly signUpEmail: import("better-call").StrictEndpoint<"/sign-up/email", {
|
|
322
337
|
method: "POST";
|
|
@@ -2297,6 +2312,11 @@ declare const router: <Option extends BetterAuthOptions>(ctx: AuthContext, optio
|
|
|
2297
2312
|
} | null>;
|
|
2298
2313
|
readonly signOut: import("better-call").StrictEndpoint<"/sign-out", {
|
|
2299
2314
|
method: "POST";
|
|
2315
|
+
body: import("zod").ZodOptional<import("zod").ZodObject<{
|
|
2316
|
+
callbackURL: import("zod").ZodOptional<import("zod").ZodString>;
|
|
2317
|
+
disableRedirect: import("zod").ZodOptional<import("zod").ZodBoolean>;
|
|
2318
|
+
state: import("zod").ZodOptional<import("zod").ZodString>;
|
|
2319
|
+
}, import("zod/v4/core").$strip>>;
|
|
2300
2320
|
operationId: string;
|
|
2301
2321
|
requireHeaders: true;
|
|
2302
2322
|
metadata: {
|
|
@@ -2314,6 +2334,14 @@ declare const router: <Option extends BetterAuthOptions>(ctx: AuthContext, optio
|
|
|
2314
2334
|
success: {
|
|
2315
2335
|
type: string;
|
|
2316
2336
|
};
|
|
2337
|
+
url: {
|
|
2338
|
+
type: string;
|
|
2339
|
+
description: string;
|
|
2340
|
+
};
|
|
2341
|
+
redirect: {
|
|
2342
|
+
type: string;
|
|
2343
|
+
description: string;
|
|
2344
|
+
};
|
|
2317
2345
|
};
|
|
2318
2346
|
};
|
|
2319
2347
|
};
|
|
@@ -2324,6 +2352,8 @@ declare const router: <Option extends BetterAuthOptions>(ctx: AuthContext, optio
|
|
|
2324
2352
|
};
|
|
2325
2353
|
}, {
|
|
2326
2354
|
success: boolean;
|
|
2355
|
+
url: string | undefined;
|
|
2356
|
+
redirect: boolean | undefined;
|
|
2327
2357
|
}>;
|
|
2328
2358
|
readonly signUpEmail: import("better-call").StrictEndpoint<"/sign-up/email", {
|
|
2329
2359
|
method: "POST";
|
|
@@ -1,6 +1,12 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
1
2
|
//#region src/api/routes/sign-out.d.ts
|
|
2
3
|
declare const signOut: import("better-call").StrictEndpoint<"/sign-out", {
|
|
3
4
|
method: "POST";
|
|
5
|
+
body: z.ZodOptional<z.ZodObject<{
|
|
6
|
+
callbackURL: z.ZodOptional<z.ZodString>;
|
|
7
|
+
disableRedirect: z.ZodOptional<z.ZodBoolean>;
|
|
8
|
+
state: z.ZodOptional<z.ZodString>;
|
|
9
|
+
}, z.core.$strip>>;
|
|
4
10
|
operationId: string;
|
|
5
11
|
requireHeaders: true;
|
|
6
12
|
metadata: {
|
|
@@ -18,6 +24,14 @@ declare const signOut: import("better-call").StrictEndpoint<"/sign-out", {
|
|
|
18
24
|
success: {
|
|
19
25
|
type: string;
|
|
20
26
|
};
|
|
27
|
+
url: {
|
|
28
|
+
type: string;
|
|
29
|
+
description: string;
|
|
30
|
+
};
|
|
31
|
+
redirect: {
|
|
32
|
+
type: string;
|
|
33
|
+
description: string;
|
|
34
|
+
};
|
|
21
35
|
};
|
|
22
36
|
};
|
|
23
37
|
};
|
|
@@ -28,6 +42,8 @@ declare const signOut: import("better-call").StrictEndpoint<"/sign-out", {
|
|
|
28
42
|
};
|
|
29
43
|
}, {
|
|
30
44
|
success: boolean;
|
|
45
|
+
url: string | undefined;
|
|
46
|
+
redirect: boolean | undefined;
|
|
31
47
|
}>;
|
|
32
48
|
//#endregion
|
|
33
49
|
export { signOut };
|
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
import { deleteSessionCookie } from "../../cookies/index.mjs";
|
|
2
2
|
import { createAuthEndpoint } from "@better-auth/core/api";
|
|
3
|
+
import * as z from "zod";
|
|
3
4
|
//#region src/api/routes/sign-out.ts
|
|
5
|
+
const signOutBodySchema = z.object({
|
|
6
|
+
callbackURL: z.string().meta({ description: "The URL to redirect to after provider logout" }).optional(),
|
|
7
|
+
disableRedirect: z.boolean().meta({ description: "Return the provider logout URL without redirecting" }).optional(),
|
|
8
|
+
state: z.string().meta({ description: "State to pass to the provider logout endpoint" }).optional()
|
|
9
|
+
}).optional();
|
|
10
|
+
const signOutResponse = (providerLogout) => ({
|
|
11
|
+
success: true,
|
|
12
|
+
url: providerLogout?.url,
|
|
13
|
+
redirect: providerLogout?.redirect
|
|
14
|
+
});
|
|
4
15
|
const signOut = createAuthEndpoint("/sign-out", {
|
|
5
16
|
method: "POST",
|
|
17
|
+
body: signOutBodySchema,
|
|
6
18
|
operationId: "signOut",
|
|
7
19
|
requireHeaders: true,
|
|
8
20
|
metadata: { openapi: {
|
|
@@ -12,19 +24,73 @@ const signOut = createAuthEndpoint("/sign-out", {
|
|
|
12
24
|
description: "Success",
|
|
13
25
|
content: { "application/json": { schema: {
|
|
14
26
|
type: "object",
|
|
15
|
-
properties: {
|
|
27
|
+
properties: {
|
|
28
|
+
success: { type: "boolean" },
|
|
29
|
+
url: {
|
|
30
|
+
type: "string",
|
|
31
|
+
description: "Provider logout URL when RP-initiated logout is available"
|
|
32
|
+
},
|
|
33
|
+
redirect: {
|
|
34
|
+
type: "boolean",
|
|
35
|
+
description: "Whether the client should redirect to the provider logout URL"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
16
38
|
} } }
|
|
17
39
|
} }
|
|
18
40
|
} }
|
|
19
41
|
}, async (ctx) => {
|
|
20
42
|
const sessionCookieToken = await ctx.getSignedCookie(ctx.context.authCookies.sessionToken.name, ctx.context.secret);
|
|
43
|
+
let currentSession = null;
|
|
44
|
+
if (sessionCookieToken) try {
|
|
45
|
+
currentSession = await ctx.context.internalAdapter.findSession(sessionCookieToken);
|
|
46
|
+
} catch (e) {
|
|
47
|
+
ctx.context.logger.error("Failed to read session from database", e);
|
|
48
|
+
}
|
|
21
49
|
if (sessionCookieToken) try {
|
|
22
50
|
await ctx.context.internalAdapter.deleteSession(sessionCookieToken);
|
|
23
51
|
} catch (e) {
|
|
24
52
|
ctx.context.logger.error("Failed to delete session from database", e);
|
|
25
53
|
}
|
|
26
54
|
deleteSessionCookie(ctx);
|
|
27
|
-
|
|
55
|
+
const providerLogoutResult = await (async () => {
|
|
56
|
+
try {
|
|
57
|
+
if (!currentSession) return null;
|
|
58
|
+
const accounts = await ctx.context.internalAdapter.findAccounts(currentSession.user.id);
|
|
59
|
+
const providersById = new Map(ctx.context.socialProviders.map((provider) => [provider.id, provider]));
|
|
60
|
+
const logoutAccounts = accounts.filter((account) => Boolean(providersById.get(account.providerId)?.createEndSessionURL)).sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime());
|
|
61
|
+
if (logoutAccounts.length === 0) return null;
|
|
62
|
+
const seenProviderIds = /* @__PURE__ */ new Set();
|
|
63
|
+
const postLogoutRedirectURI = ctx.body?.callbackURL ? new URL(ctx.body.callbackURL, ctx.context.baseURL).toString() : void 0;
|
|
64
|
+
for (const account of logoutAccounts) {
|
|
65
|
+
if (seenProviderIds.has(account.providerId)) continue;
|
|
66
|
+
seenProviderIds.add(account.providerId);
|
|
67
|
+
const provider = providersById.get(account.providerId);
|
|
68
|
+
try {
|
|
69
|
+
const url = await provider?.createEndSessionURL?.({
|
|
70
|
+
idToken: account.idToken,
|
|
71
|
+
postLogoutRedirectURI,
|
|
72
|
+
state: ctx.body?.state
|
|
73
|
+
});
|
|
74
|
+
if (url) return url.toString();
|
|
75
|
+
} catch (e) {
|
|
76
|
+
ctx.context.logger.error(`Failed to create logout URL for provider "${account.providerId}"`, e);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
} catch (e) {
|
|
81
|
+
ctx.context.logger.error("Failed to create provider logout URL", e);
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
})();
|
|
85
|
+
if (providerLogoutResult) {
|
|
86
|
+
const shouldRedirect = !ctx.body?.disableRedirect;
|
|
87
|
+
if (shouldRedirect) ctx.setHeader("Location", providerLogoutResult);
|
|
88
|
+
return ctx.json(signOutResponse({
|
|
89
|
+
url: providerLogoutResult,
|
|
90
|
+
redirect: shouldRedirect
|
|
91
|
+
}));
|
|
92
|
+
}
|
|
93
|
+
return ctx.json(signOutResponse());
|
|
28
94
|
});
|
|
29
95
|
//#endregion
|
|
30
96
|
export { signOut };
|
|
@@ -77,7 +77,8 @@ type InferBodyCtx<Body, FetchOptions extends ClientFetchOption> = Body extends R
|
|
|
77
77
|
} : never;
|
|
78
78
|
type PrettifyUnion<T> = T extends unknown ? Prettify$1<T> : never;
|
|
79
79
|
type HasRequiredKeysInUnion<T> = true extends (T extends unknown ? HasRequiredKeys<T> : never) ? true : false;
|
|
80
|
-
type
|
|
80
|
+
type HasRequiredCtx<C extends InputContext<any, any>, FetchOptions extends ClientFetchOption> = IsAny<C["body"]> extends true ? HasRequiredKeysInUnion<InferCtx<C, FetchOptions>> : undefined extends C["body"] ? HasRequiredKeysInUnion<InferCtxQuery<C, FetchOptions>> : HasRequiredKeysInUnion<InferCtx<C, FetchOptions>>;
|
|
81
|
+
type InferCtx<C extends InputContext<any, any>, FetchOptions extends ClientFetchOption> = IsAny<C["body"]> extends true ? InferCtxQuery<C, FetchOptions> : [NonNullable<C["body"]>] extends [never] ? InferCtxQuery<C, FetchOptions> : NonNullable<C["body"]> extends Record<string, any> ? InferBodyCtx<NonNullable<C["body"]>, FetchOptions> : InferCtxQuery<C, FetchOptions>;
|
|
81
82
|
type MergeRoutes<T> = UnionToIntersection<T>;
|
|
82
83
|
type InferRoute<API, COpts extends BetterAuthClientOptions> = API extends Record<string, infer T> ? T extends Endpoint ? T["options"]["metadata"] extends {
|
|
83
84
|
isAction: false;
|
|
@@ -87,7 +88,7 @@ type InferRoute<API, COpts extends BetterAuthClientOptions> = API extends Record
|
|
|
87
88
|
scope: "http";
|
|
88
89
|
} | {
|
|
89
90
|
scope: "server";
|
|
90
|
-
} ? {} : PathToObject<T["path"], T extends ((ctx: infer C) => infer R) ? C extends InputContext<any, any> ? <FetchOptions extends ClientFetchOption<Partial<C["body"]> & Record<string, any>, Partial<C["query"]> & Record<string, any>, C["params"]>>(...data:
|
|
91
|
+
} ? {} : PathToObject<T["path"], T extends ((ctx: infer C) => infer R) ? C extends InputContext<any, any> ? <FetchOptions extends ClientFetchOption<Partial<C["body"]> & Record<string, any>, Partial<C["query"]> & Record<string, any>, C["params"]>>(...data: HasRequiredCtx<C, FetchOptions> extends true ? [PrettifyUnion<T["path"] extends `/sign-up/email` ? InferSignUpEmailCtx<COpts, FetchOptions> : T["path"] extends `/sign-in/social` ? Omit<InferCtx<C, FetchOptions>, "provider"> & {
|
|
91
92
|
provider: SocialProviderList[number] | InferGenericOAuthProviderIds<COpts> | (string & {});
|
|
92
93
|
fetchOptions?: FetchOptions | undefined;
|
|
93
94
|
} : InferCtx<C, FetchOptions>>, FetchOptions?] : [PrettifyUnion<T["path"] extends `/update-user` ? InferUserUpdateCtx<COpts, FetchOptions> : T["path"] extends `/update-session` ? InferSessionUpdateCtx<COpts, FetchOptions> : InferCtx<C, FetchOptions>>?, FetchOptions?]) => Promise<BetterFetchResponse<T["options"]["metadata"] extends {
|
package/dist/package.mjs
CHANGED
|
@@ -16,7 +16,7 @@ declare const deviceAuthorizationOptionsSchema: z.ZodObject<{
|
|
|
16
16
|
generateDeviceCode: z.ZodOptional<z.ZodCustom<() => string | Promise<string>, () => string | Promise<string>>>;
|
|
17
17
|
generateUserCode: z.ZodOptional<z.ZodCustom<() => string | Promise<string>, () => string | Promise<string>>>;
|
|
18
18
|
validateClient: z.ZodOptional<z.ZodCustom<(clientId: string) => boolean | Promise<boolean>, (clientId: string) => boolean | Promise<boolean>>>;
|
|
19
|
-
onDeviceAuthRequest: z.ZodOptional<z.ZodCustom<(clientId: string, scope: string | undefined) => void | Promise<void>, (clientId: string, scope: string | undefined) => void | Promise<void>>>;
|
|
19
|
+
onDeviceAuthRequest: z.ZodOptional<z.ZodCustom<(clientId: string, scope: string | undefined, resource?: string | string[]) => void | Promise<void>, (clientId: string, scope: string | undefined, resource?: string | string[]) => void | Promise<void>>>;
|
|
20
20
|
verificationUri: z.ZodOptional<z.ZodString>;
|
|
21
21
|
schema: z.ZodOptional<z.ZodCustom<{
|
|
22
22
|
deviceCode?: {
|
|
@@ -31,6 +31,7 @@ declare const deviceAuthorizationOptionsSchema: z.ZodObject<{
|
|
|
31
31
|
pollingInterval?: string | undefined;
|
|
32
32
|
clientId?: string | undefined;
|
|
33
33
|
scope?: string | undefined;
|
|
34
|
+
resource?: string | undefined;
|
|
34
35
|
} | undefined;
|
|
35
36
|
} | undefined;
|
|
36
37
|
}, {
|
|
@@ -46,6 +47,7 @@ declare const deviceAuthorizationOptionsSchema: z.ZodObject<{
|
|
|
46
47
|
pollingInterval?: string | undefined;
|
|
47
48
|
clientId?: string | undefined;
|
|
48
49
|
scope?: string | undefined;
|
|
50
|
+
resource?: string | undefined;
|
|
49
51
|
} | undefined;
|
|
50
52
|
} | undefined;
|
|
51
53
|
}>>;
|
|
@@ -93,26 +95,39 @@ declare const deviceAuthorization: (options?: Partial<DeviceAuthorizationOptions
|
|
|
93
95
|
type: "string";
|
|
94
96
|
required: false;
|
|
95
97
|
};
|
|
98
|
+
resource: {
|
|
99
|
+
type: "string";
|
|
100
|
+
required: false;
|
|
101
|
+
};
|
|
96
102
|
};
|
|
103
|
+
indexes: {
|
|
104
|
+
fields: [string];
|
|
105
|
+
}[];
|
|
97
106
|
};
|
|
98
107
|
};
|
|
99
108
|
endpoints: {
|
|
100
109
|
deviceCode: import("better-call").StrictEndpoint<"/device/code", {
|
|
101
110
|
method: "POST";
|
|
111
|
+
cloneRequest: true;
|
|
102
112
|
body: z.ZodObject<{
|
|
103
113
|
client_id: z.ZodString;
|
|
104
114
|
user_id: z.ZodOptional<z.ZodString>;
|
|
105
115
|
scope: z.ZodOptional<z.ZodString>;
|
|
116
|
+
resource: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
|
|
106
117
|
}, z.core.$strip>;
|
|
107
118
|
error: z.ZodObject<{
|
|
108
119
|
error: z.ZodEnum<{
|
|
109
120
|
invalid_request: "invalid_request";
|
|
110
121
|
invalid_client: "invalid_client";
|
|
122
|
+
unauthorized_client: "unauthorized_client";
|
|
123
|
+
invalid_scope: "invalid_scope";
|
|
124
|
+
invalid_target: "invalid_target";
|
|
111
125
|
}>;
|
|
112
126
|
error_description: z.ZodString;
|
|
113
127
|
}, z.core.$strip>;
|
|
114
128
|
metadata: {
|
|
115
129
|
noStore: boolean;
|
|
130
|
+
allowedMediaTypes: string[];
|
|
116
131
|
openapi: {
|
|
117
132
|
description: string;
|
|
118
133
|
responses: {
|
|
@@ -284,6 +299,26 @@ declare const deviceAuthorization: (options?: Partial<DeviceAuthorizationOptions
|
|
|
284
299
|
enum: string[];
|
|
285
300
|
description: string;
|
|
286
301
|
};
|
|
302
|
+
client_id: {
|
|
303
|
+
type: string;
|
|
304
|
+
description: string;
|
|
305
|
+
};
|
|
306
|
+
scope: {
|
|
307
|
+
type: string;
|
|
308
|
+
description: string;
|
|
309
|
+
};
|
|
310
|
+
resource: {
|
|
311
|
+
oneOf: ({
|
|
312
|
+
type: string;
|
|
313
|
+
items?: undefined;
|
|
314
|
+
} | {
|
|
315
|
+
type: string;
|
|
316
|
+
items: {
|
|
317
|
+
type: string;
|
|
318
|
+
};
|
|
319
|
+
})[];
|
|
320
|
+
description: string;
|
|
321
|
+
};
|
|
287
322
|
};
|
|
288
323
|
};
|
|
289
324
|
};
|
|
@@ -293,6 +328,9 @@ declare const deviceAuthorization: (options?: Partial<DeviceAuthorizationOptions
|
|
|
293
328
|
};
|
|
294
329
|
};
|
|
295
330
|
}, {
|
|
331
|
+
client_id?: string | undefined;
|
|
332
|
+
scope?: string | undefined;
|
|
333
|
+
resource?: string | string[] | undefined;
|
|
296
334
|
user_code: string;
|
|
297
335
|
status: string;
|
|
298
336
|
}>;
|
|
@@ -401,7 +439,7 @@ declare const deviceAuthorization: (options?: Partial<DeviceAuthorizationOptions
|
|
|
401
439
|
generateDeviceCode?: (() => string | Promise<string>) | undefined;
|
|
402
440
|
generateUserCode?: (() => string | Promise<string>) | undefined;
|
|
403
441
|
validateClient?: ((clientId: string) => boolean | Promise<boolean>) | undefined;
|
|
404
|
-
onDeviceAuthRequest?: ((clientId: string, scope: string | undefined) => void | Promise<void>) | undefined;
|
|
442
|
+
onDeviceAuthRequest?: ((clientId: string, scope: string | undefined, resource?: string | string[]) => void | Promise<void>) | undefined;
|
|
405
443
|
verificationUri?: string | undefined;
|
|
406
444
|
schema?: {
|
|
407
445
|
deviceCode?: {
|
|
@@ -416,6 +454,7 @@ declare const deviceAuthorization: (options?: Partial<DeviceAuthorizationOptions
|
|
|
416
454
|
pollingInterval?: string | undefined;
|
|
417
455
|
clientId?: string | undefined;
|
|
418
456
|
scope?: string | undefined;
|
|
457
|
+
resource?: string | undefined;
|
|
419
458
|
} | undefined;
|
|
420
459
|
} | undefined;
|
|
421
460
|
} | undefined;
|
|
@@ -2,8 +2,8 @@ import { mergeSchema } from "../../db/schema.mjs";
|
|
|
2
2
|
import { ms } from "../../utils/time.mjs";
|
|
3
3
|
import { PACKAGE_VERSION } from "../../version.mjs";
|
|
4
4
|
import { DEVICE_AUTHORIZATION_ERROR_CODES } from "./error-codes.mjs";
|
|
5
|
-
import { deviceApprove, deviceCode, deviceDeny, deviceToken, deviceVerify } from "./routes.mjs";
|
|
6
5
|
import { schema } from "./schema.mjs";
|
|
6
|
+
import { deviceApprove, deviceCode, deviceDeny, deviceToken, deviceVerify } from "./routes.mjs";
|
|
7
7
|
import * as z from "zod";
|
|
8
8
|
//#region src/plugins/device-authorization/index.ts
|
|
9
9
|
const timeStringSchema = z.custom((val) => {
|
|
@@ -18,8 +18,8 @@ const timeStringSchema = z.custom((val) => {
|
|
|
18
18
|
const deviceAuthorizationOptionsSchema = z.object({
|
|
19
19
|
expiresIn: timeStringSchema.default("30m").describe("Time in seconds until the device code expires. Use formats like '30m', '5s', '1h', etc."),
|
|
20
20
|
interval: timeStringSchema.default("5s").describe("Time in seconds between polling attempts. Use formats like '30m', '5s', '1h', etc."),
|
|
21
|
-
deviceCodeLength: z.number().int().positive().default(40).describe(
|
|
22
|
-
userCodeLength: z.number().int().positive().default(8).describe(
|
|
21
|
+
deviceCodeLength: z.number().int().positive().max(191).default(40).describe(`Length of the device code to be generated. Must be at most 191 characters. Default is 40 characters.`),
|
|
22
|
+
userCodeLength: z.number().int().positive().max(191).default(8).describe(`Length of the user code to be generated. Must be at most 191 characters. Default is 8 characters.`),
|
|
23
23
|
generateDeviceCode: z.custom((val) => typeof val === "function", { message: "generateDeviceCode must be a function that returns a string or a promise that resolves to a string." }).optional().describe("Function to generate a device code. If not provided, a default random string generator will be used."),
|
|
24
24
|
generateUserCode: z.custom((val) => typeof val === "function", { message: "generateUserCode must be a function that returns a string or a promise that resolves to a string." }).optional().describe("Function to generate a user code. If not provided, a default random string generator will be used."),
|
|
25
25
|
validateClient: z.custom((val) => typeof val === "function", { message: "validateClient must be a function that returns a boolean or a promise that resolves to a boolean." }).optional().describe("Function to validate the client ID. If not provided, no validation will be performed."),
|
|
@@ -7,30 +7,71 @@ import { createAuthEndpoint } from "@better-auth/core/api";
|
|
|
7
7
|
import * as z from "zod";
|
|
8
8
|
//#region src/plugins/device-authorization/routes.ts
|
|
9
9
|
const defaultCharset = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
|
10
|
+
function validateGeneratedCode(code, label) {
|
|
11
|
+
if (typeof code !== "string") throw new APIError("BAD_REQUEST", {
|
|
12
|
+
error: "invalid_request",
|
|
13
|
+
error_description: `Generated ${label} code must be a string`
|
|
14
|
+
});
|
|
15
|
+
if (Array.from(code).length > 191) throw new APIError("BAD_REQUEST", {
|
|
16
|
+
error: "invalid_request",
|
|
17
|
+
error_description: `Generated ${label} code must be at most 191 characters`
|
|
18
|
+
});
|
|
19
|
+
return code;
|
|
20
|
+
}
|
|
21
|
+
function serializeResource(resource) {
|
|
22
|
+
return typeof resource === "string" ? resource : JSON.stringify(resource);
|
|
23
|
+
}
|
|
24
|
+
function parseStoredResource(resource) {
|
|
25
|
+
if (!resource) return void 0;
|
|
26
|
+
if (!resource.startsWith("[")) return resource;
|
|
27
|
+
try {
|
|
28
|
+
const parsed = JSON.parse(resource);
|
|
29
|
+
if (Array.isArray(parsed) && parsed.every((value) => typeof value === "string")) return parsed;
|
|
30
|
+
} catch {}
|
|
31
|
+
return resource;
|
|
32
|
+
}
|
|
33
|
+
async function extractFormResources(request) {
|
|
34
|
+
const contentType = request?.headers.get("content-type")?.toLowerCase() ?? "";
|
|
35
|
+
if (!request || !contentType.includes("application/x-www-form-urlencoded")) return;
|
|
36
|
+
try {
|
|
37
|
+
const params = new URLSearchParams(await request.text());
|
|
38
|
+
if (!params.has("resource")) return void 0;
|
|
39
|
+
return params.getAll("resource").filter(Boolean);
|
|
40
|
+
} catch {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
10
44
|
const deviceCodeBodySchema = z.object({
|
|
11
45
|
client_id: z.string().meta({ description: "The client ID of the application" }),
|
|
12
46
|
user_id: z.string().meta({ description: "The user ID to which the device code should be pre-bound." }).optional(),
|
|
13
|
-
scope: z.string().meta({ description: "Space-separated list of scopes" }).optional()
|
|
47
|
+
scope: z.string().meta({ description: "Space-separated list of scopes" }).optional(),
|
|
48
|
+
resource: z.union([z.string(), z.array(z.string())]).meta({ description: "RFC 8707 resource indicator(s) to bind to this authorization request" }).optional()
|
|
14
49
|
});
|
|
15
50
|
const deviceCodeErrorSchema = z.object({
|
|
16
|
-
error: z.enum([
|
|
51
|
+
error: z.enum([
|
|
52
|
+
"invalid_request",
|
|
53
|
+
"invalid_client",
|
|
54
|
+
"unauthorized_client",
|
|
55
|
+
"invalid_scope",
|
|
56
|
+
"invalid_target"
|
|
57
|
+
]).meta({ description: "Error code" }),
|
|
17
58
|
error_description: z.string().meta({ description: "Detailed error description" })
|
|
18
59
|
});
|
|
19
60
|
const deviceCode = (opts) => {
|
|
20
61
|
const generateDeviceCode = async () => {
|
|
21
|
-
|
|
22
|
-
return defaultGenerateDeviceCode(opts.deviceCodeLength);
|
|
62
|
+
return validateGeneratedCode(opts.generateDeviceCode ? await opts.generateDeviceCode() : defaultGenerateDeviceCode(opts.deviceCodeLength), "device");
|
|
23
63
|
};
|
|
24
64
|
const generateUserCode = async () => {
|
|
25
|
-
|
|
26
|
-
return defaultGenerateUserCode(opts.userCodeLength);
|
|
65
|
+
return validateGeneratedCode(opts.generateUserCode ? await opts.generateUserCode() : defaultGenerateUserCode(opts.userCodeLength), "user");
|
|
27
66
|
};
|
|
28
67
|
return createAuthEndpoint("/device/code", {
|
|
29
68
|
method: "POST",
|
|
69
|
+
cloneRequest: true,
|
|
30
70
|
body: deviceCodeBodySchema,
|
|
31
71
|
error: deviceCodeErrorSchema,
|
|
32
72
|
metadata: {
|
|
33
73
|
noStore: true,
|
|
74
|
+
allowedMediaTypes: ["application/json", "application/x-www-form-urlencoded"],
|
|
34
75
|
openapi: {
|
|
35
76
|
description: `Request a device and user code
|
|
36
77
|
|
|
@@ -77,7 +118,13 @@ Follow [rfc8628#section-3.2](https://datatracker.ietf.org/doc/html/rfc8628#secti
|
|
|
77
118
|
properties: {
|
|
78
119
|
error: {
|
|
79
120
|
type: "string",
|
|
80
|
-
enum: [
|
|
121
|
+
enum: [
|
|
122
|
+
"invalid_request",
|
|
123
|
+
"invalid_client",
|
|
124
|
+
"unauthorized_client",
|
|
125
|
+
"invalid_scope",
|
|
126
|
+
"invalid_target"
|
|
127
|
+
]
|
|
81
128
|
},
|
|
82
129
|
error_description: { type: "string" }
|
|
83
130
|
}
|
|
@@ -87,13 +134,15 @@ Follow [rfc8628#section-3.2](https://datatracker.ietf.org/doc/html/rfc8628#secti
|
|
|
87
134
|
}
|
|
88
135
|
}
|
|
89
136
|
}, async (ctx) => {
|
|
137
|
+
const formResources = await extractFormResources(ctx.request);
|
|
138
|
+
if (formResources) ctx.body.resource = formResources.length === 1 ? formResources[0] : formResources;
|
|
90
139
|
if (opts.validateClient) {
|
|
91
140
|
if (!await opts.validateClient(ctx.body.client_id)) throw new APIError("BAD_REQUEST", {
|
|
92
141
|
error: "invalid_client",
|
|
93
142
|
error_description: "Invalid client ID"
|
|
94
143
|
});
|
|
95
144
|
}
|
|
96
|
-
if (opts.onDeviceAuthRequest) await opts.onDeviceAuthRequest(ctx.body.client_id, ctx.body.scope);
|
|
145
|
+
if (opts.onDeviceAuthRequest) await opts.onDeviceAuthRequest(ctx.body.client_id, ctx.body.scope, ctx.body.resource);
|
|
97
146
|
const deviceCode = await generateDeviceCode();
|
|
98
147
|
const userCode = await generateUserCode();
|
|
99
148
|
const expiresIn = ms(opts.expiresIn);
|
|
@@ -108,7 +157,8 @@ Follow [rfc8628#section-3.2](https://datatracker.ietf.org/doc/html/rfc8628#secti
|
|
|
108
157
|
status: "pending",
|
|
109
158
|
pollingInterval: ms(opts.interval),
|
|
110
159
|
clientId: ctx.body.client_id,
|
|
111
|
-
scope: ctx.body.scope
|
|
160
|
+
scope: ctx.body.scope,
|
|
161
|
+
resource: ctx.body.resource ? serializeResource(ctx.body.resource) : null
|
|
112
162
|
}
|
|
113
163
|
});
|
|
114
164
|
const { verificationUri, verificationUriComplete } = buildVerificationUris(opts.verificationUri, ctx.context.baseURL, userCode);
|
|
@@ -331,6 +381,21 @@ const deviceVerify = createAuthEndpoint("/device", {
|
|
|
331
381
|
"denied"
|
|
332
382
|
],
|
|
333
383
|
description: "Current status of the device authorization"
|
|
384
|
+
},
|
|
385
|
+
client_id: {
|
|
386
|
+
type: "string",
|
|
387
|
+
description: "The client requesting authorization, returned only to the authenticated user who owns this request"
|
|
388
|
+
},
|
|
389
|
+
scope: {
|
|
390
|
+
type: "string",
|
|
391
|
+
description: "The requested scopes, returned only to the authenticated user who owns this request"
|
|
392
|
+
},
|
|
393
|
+
resource: {
|
|
394
|
+
oneOf: [{ type: "string" }, {
|
|
395
|
+
type: "array",
|
|
396
|
+
items: { type: "string" }
|
|
397
|
+
}],
|
|
398
|
+
description: "The requested resource indicators, returned only to the authenticated user who owns this request"
|
|
334
399
|
}
|
|
335
400
|
}
|
|
336
401
|
} } }
|
|
@@ -377,9 +442,15 @@ const deviceVerify = createAuthEndpoint("/device", {
|
|
|
377
442
|
set: { userId: session.user.id }
|
|
378
443
|
})) deviceCodeRecord.userId = session.user.id;
|
|
379
444
|
}
|
|
445
|
+
const canReviewRequest = session?.user.id !== void 0 && deviceCodeRecord.userId === session.user.id;
|
|
380
446
|
return ctx.json({
|
|
381
447
|
user_code,
|
|
382
|
-
status: deviceCodeRecord.status
|
|
448
|
+
status: deviceCodeRecord.status,
|
|
449
|
+
...canReviewRequest ? {
|
|
450
|
+
client_id: deviceCodeRecord.clientId,
|
|
451
|
+
scope: deviceCodeRecord.scope,
|
|
452
|
+
resource: parseStoredResource(deviceCodeRecord.resource)
|
|
453
|
+
} : {}
|
|
383
454
|
});
|
|
384
455
|
});
|
|
385
456
|
const deviceApprove = createAuthEndpoint("/device/approve", {
|
|
@@ -1,43 +1,50 @@
|
|
|
1
1
|
import * as z from "zod";
|
|
2
2
|
//#region src/plugins/device-authorization/schema.ts
|
|
3
|
-
const schema = { deviceCode: {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
3
|
+
const schema = { deviceCode: {
|
|
4
|
+
fields: {
|
|
5
|
+
deviceCode: {
|
|
6
|
+
type: "string",
|
|
7
|
+
required: true
|
|
8
|
+
},
|
|
9
|
+
userCode: {
|
|
10
|
+
type: "string",
|
|
11
|
+
required: true
|
|
12
|
+
},
|
|
13
|
+
userId: {
|
|
14
|
+
type: "string",
|
|
15
|
+
required: false
|
|
16
|
+
},
|
|
17
|
+
expiresAt: {
|
|
18
|
+
type: "date",
|
|
19
|
+
required: true
|
|
20
|
+
},
|
|
21
|
+
status: {
|
|
22
|
+
type: "string",
|
|
23
|
+
required: true
|
|
24
|
+
},
|
|
25
|
+
lastPolledAt: {
|
|
26
|
+
type: "date",
|
|
27
|
+
required: false
|
|
28
|
+
},
|
|
29
|
+
pollingInterval: {
|
|
30
|
+
type: "number",
|
|
31
|
+
required: false
|
|
32
|
+
},
|
|
33
|
+
clientId: {
|
|
34
|
+
type: "string",
|
|
35
|
+
required: false
|
|
36
|
+
},
|
|
37
|
+
scope: {
|
|
38
|
+
type: "string",
|
|
39
|
+
required: false
|
|
40
|
+
},
|
|
41
|
+
resource: {
|
|
42
|
+
type: "string",
|
|
43
|
+
required: false
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
indexes: [{ fields: ["deviceCode"] }, { fields: ["userCode"] }]
|
|
47
|
+
} };
|
|
41
48
|
z.object({
|
|
42
49
|
id: z.string(),
|
|
43
50
|
deviceCode: z.string(),
|
|
@@ -48,7 +55,8 @@ z.object({
|
|
|
48
55
|
lastPolledAt: z.date().optional(),
|
|
49
56
|
pollingInterval: z.number().optional(),
|
|
50
57
|
clientId: z.string().optional(),
|
|
51
|
-
scope: z.string().optional()
|
|
58
|
+
scope: z.string().optional(),
|
|
59
|
+
resource: z.string().optional()
|
|
52
60
|
});
|
|
53
61
|
//#endregion
|
|
54
62
|
export { schema };
|
|
@@ -24,7 +24,7 @@ declare module "@better-auth/core" {
|
|
|
24
24
|
* Base type for OAuth provider options.
|
|
25
25
|
* Extracts common fields from GenericOAuthConfig for provider helpers.
|
|
26
26
|
*/
|
|
27
|
-
type BaseOAuthProviderOptions = Pick<GenericOAuthConfig, "clientId" | "clientSecret" | "tokenEndpointAuth" | "scopes" | "redirectURI" | "pkce" | "disableImplicitSignUp" | "disableSignUp" | "overrideUserInfo">;
|
|
27
|
+
type BaseOAuthProviderOptions = Pick<GenericOAuthConfig, "clientId" | "clientSecret" | "tokenEndpointAuth" | "scopes" | "redirectURI" | "endSessionEndpoint" | "postLogoutRedirectURI" | "disableProviderLogout" | "pkce" | "disableImplicitSignUp" | "disableSignUp" | "overrideUserInfo">;
|
|
28
28
|
/**
|
|
29
29
|
* A generic OAuth plugin that registers any OAuth/OIDC provider
|
|
30
30
|
* as a first-class social provider.
|
|
@@ -83,6 +83,7 @@ const genericOAuth = (options) => {
|
|
|
83
83
|
let authorizationUrl = c.authorizationUrl;
|
|
84
84
|
let tokenUrl = c.tokenUrl;
|
|
85
85
|
let userInfoUrl = c.userInfoUrl;
|
|
86
|
+
let endSessionEndpoint = c.endSessionEndpoint;
|
|
86
87
|
let issuer;
|
|
87
88
|
let isOidc = false;
|
|
88
89
|
let idTokenConfig;
|
|
@@ -96,6 +97,7 @@ const genericOAuth = (options) => {
|
|
|
96
97
|
authorizationUrl ??= discovered.authorization_endpoint;
|
|
97
98
|
tokenUrl ??= discovered.token_endpoint;
|
|
98
99
|
userInfoUrl ??= discovered.userinfo_endpoint;
|
|
100
|
+
endSessionEndpoint ??= discovered.end_session_endpoint;
|
|
99
101
|
issuer = discovered.issuer;
|
|
100
102
|
const signingAlgs = discovered.id_token_signing_alg_values_supported;
|
|
101
103
|
isOidc = Array.isArray(signingAlgs) && signingAlgs.length > 0;
|
|
@@ -116,6 +118,7 @@ const genericOAuth = (options) => {
|
|
|
116
118
|
} else if (!c.accountIssuer) throw new Error(`Provider "${c.providerId}": discovery returned no valid data. Provider initialization stopped to keep its account issuer stable.`);
|
|
117
119
|
else if (!authorizationUrl || !tokenUrl) ctx.logger.error(`Provider "${c.providerId}": discovery returned no data and no explicit endpoints configured. OAuth sign-in will fail for this provider.`);
|
|
118
120
|
}
|
|
121
|
+
if (c.requireIdTokenVerification && !idTokenConfig) throw new Error(`Provider "${c.providerId}": requires verified ID tokens, but discovery did not provide a usable issuer and jwks_uri.`);
|
|
119
122
|
const tokenEndpointAuth = c.tokenEndpointAuth;
|
|
120
123
|
if (c.clientSecret && isSecretlessTokenEndpointAuth(tokenEndpointAuth)) throw new Error(`Provider "${c.providerId}": tokenEndpointAuth.method "${tokenEndpointAuth?.method}" cannot be combined with clientSecret`);
|
|
121
124
|
if (!c.clientSecret && isClientSecretTokenEndpointAuth(tokenEndpointAuth)) throw new Error(`Provider "${c.providerId}": tokenEndpointAuth.method "${tokenEndpointAuth?.method}" requires clientSecret`);
|
|
@@ -141,6 +144,25 @@ const genericOAuth = (options) => {
|
|
|
141
144
|
idToken: idTokenConfig,
|
|
142
145
|
requiresIdTokenNonce: idTokenConfig !== void 0 && c.disableIdTokenNonceBinding !== true,
|
|
143
146
|
allowIdpInitiated: c.allowIdpInitiated,
|
|
147
|
+
async createEndSessionURL(data) {
|
|
148
|
+
if (c.disableProviderLogout) return null;
|
|
149
|
+
if (!endSessionEndpoint) return null;
|
|
150
|
+
let url;
|
|
151
|
+
try {
|
|
152
|
+
url = new URL(endSessionEndpoint);
|
|
153
|
+
} catch {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
if (data.idToken) url.searchParams.set("id_token_hint", data.idToken);
|
|
157
|
+
const configuredRedirectURI = data.postLogoutRedirectURI ?? c.postLogoutRedirectURI;
|
|
158
|
+
const postLogoutRedirectURI = configuredRedirectURI ? new URL(configuredRedirectURI, ctx.baseURL).toString() : void 0;
|
|
159
|
+
if (postLogoutRedirectURI) {
|
|
160
|
+
url.searchParams.set("post_logout_redirect_uri", postLogoutRedirectURI);
|
|
161
|
+
url.searchParams.set("client_id", c.clientId);
|
|
162
|
+
if (data.state) url.searchParams.set("state", data.state);
|
|
163
|
+
} else if (!data.idToken) url.searchParams.set("client_id", c.clientId);
|
|
164
|
+
return url;
|
|
165
|
+
},
|
|
144
166
|
createAuthorizationURL(data) {
|
|
145
167
|
if (!authorizationUrl) throw APIError.from("BAD_REQUEST", GENERIC_OAUTH_ERROR_CODES.INVALID_OAUTH_CONFIGURATION);
|
|
146
168
|
return createAuthorizationURL({
|
|
@@ -38,6 +38,9 @@ function auth0(options) {
|
|
|
38
38
|
tokenEndpointAuth: options.tokenEndpointAuth,
|
|
39
39
|
scopes: options.scopes ?? defaultScopes,
|
|
40
40
|
redirectURI: options.redirectURI,
|
|
41
|
+
endSessionEndpoint: options.endSessionEndpoint,
|
|
42
|
+
postLogoutRedirectURI: options.postLogoutRedirectURI,
|
|
43
|
+
disableProviderLogout: options.disableProviderLogout,
|
|
41
44
|
pkce: options.pkce,
|
|
42
45
|
disableImplicitSignUp: options.disableImplicitSignUp,
|
|
43
46
|
disableSignUp: options.disableSignUp,
|
|
@@ -48,6 +48,9 @@ function gumroad(options) {
|
|
|
48
48
|
tokenEndpointAuth: options.tokenEndpointAuth,
|
|
49
49
|
scopes: options.scopes ?? ["view_profile"],
|
|
50
50
|
redirectURI: options.redirectURI,
|
|
51
|
+
endSessionEndpoint: options.endSessionEndpoint,
|
|
52
|
+
postLogoutRedirectURI: options.postLogoutRedirectURI,
|
|
53
|
+
disableProviderLogout: options.disableProviderLogout,
|
|
51
54
|
pkce: options.pkce,
|
|
52
55
|
disableImplicitSignUp: options.disableImplicitSignUp,
|
|
53
56
|
disableSignUp: options.disableSignUp,
|
|
@@ -47,6 +47,9 @@ function hubspot(options) {
|
|
|
47
47
|
tokenEndpointAuth: options.tokenEndpointAuth,
|
|
48
48
|
scopes: options.scopes ?? defaultScopes,
|
|
49
49
|
redirectURI: options.redirectURI,
|
|
50
|
+
endSessionEndpoint: options.endSessionEndpoint,
|
|
51
|
+
postLogoutRedirectURI: options.postLogoutRedirectURI,
|
|
52
|
+
disableProviderLogout: options.disableProviderLogout,
|
|
50
53
|
authentication: "post",
|
|
51
54
|
pkce: options.pkce,
|
|
52
55
|
disableImplicitSignUp: options.disableImplicitSignUp,
|
|
@@ -37,6 +37,9 @@ function keycloak(options) {
|
|
|
37
37
|
tokenEndpointAuth: options.tokenEndpointAuth,
|
|
38
38
|
scopes: options.scopes ?? defaultScopes,
|
|
39
39
|
redirectURI: options.redirectURI,
|
|
40
|
+
endSessionEndpoint: options.endSessionEndpoint,
|
|
41
|
+
postLogoutRedirectURI: options.postLogoutRedirectURI,
|
|
42
|
+
disableProviderLogout: options.disableProviderLogout,
|
|
40
43
|
pkce: options.pkce,
|
|
41
44
|
disableImplicitSignUp: options.disableImplicitSignUp,
|
|
42
45
|
disableSignUp: options.disableSignUp,
|
|
@@ -80,6 +80,9 @@ function line(options) {
|
|
|
80
80
|
tokenEndpointAuth: options.tokenEndpointAuth,
|
|
81
81
|
scopes: options.scopes ?? defaultScopes,
|
|
82
82
|
redirectURI: options.redirectURI,
|
|
83
|
+
endSessionEndpoint: options.endSessionEndpoint,
|
|
84
|
+
postLogoutRedirectURI: options.postLogoutRedirectURI,
|
|
85
|
+
disableProviderLogout: options.disableProviderLogout,
|
|
83
86
|
pkce: options.pkce,
|
|
84
87
|
disableImplicitSignUp: options.disableImplicitSignUp,
|
|
85
88
|
disableSignUp: options.disableSignUp,
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
import { decodeJwt } from "jose";
|
|
1
2
|
import { betterFetch } from "@better-fetch/fetch";
|
|
2
3
|
//#region src/plugins/generic-oauth/providers/microsoft-entra-id.ts
|
|
4
|
+
function getMicrosoftProfileName(profile) {
|
|
5
|
+
return profile.name ?? (`${profile.given_name ?? profile.givenname ?? ""} ${profile.family_name ?? profile.familyname ?? ""}`.trim() || void 0);
|
|
6
|
+
}
|
|
3
7
|
/**
|
|
4
8
|
* Microsoft Entra ID (Azure AD) OAuth provider helper
|
|
5
9
|
*
|
|
@@ -32,23 +36,42 @@ function microsoftEntraId(options) {
|
|
|
32
36
|
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(tenantId)) throw new Error("The generic microsoftEntraId helper requires a concrete Microsoft Entra tenant GUID. Use the built-in Microsoft provider for common, organizations, or consumers.");
|
|
33
37
|
const authorizationUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize`;
|
|
34
38
|
const tokenUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`;
|
|
39
|
+
const discoveryUrl = `https://login.microsoftonline.com/${tenantId}/v2.0/.well-known/openid-configuration`;
|
|
35
40
|
const userInfoUrl = "https://graph.microsoft.com/oidc/userinfo";
|
|
36
|
-
const tenantIssuer = `https://login.microsoftonline.com/${tenantId}/v2.0`;
|
|
37
41
|
const getUserInfo = async (tokens) => {
|
|
42
|
+
if (!tokens.idToken) return null;
|
|
43
|
+
let tokenProfile;
|
|
44
|
+
try {
|
|
45
|
+
tokenProfile = decodeJwt(tokens.idToken);
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
if (!(typeof tokenProfile.oid === "string" && tokenProfile.oid.trim().length > 0 ? tokenProfile.oid : void 0)) return null;
|
|
50
|
+
const tokenUserInfo = {
|
|
51
|
+
...tokenProfile,
|
|
52
|
+
name: getMicrosoftProfileName(tokenProfile),
|
|
53
|
+
email: tokenProfile.email ?? tokenProfile.preferred_username ?? void 0,
|
|
54
|
+
image: tokenProfile.picture,
|
|
55
|
+
emailVerified: tokenProfile.email_verified ?? false
|
|
56
|
+
};
|
|
57
|
+
if (!tokens.accessToken) return tokenUserInfo;
|
|
38
58
|
const { data: profile, error } = await betterFetch(userInfoUrl, { headers: { Authorization: `Bearer ${tokens.accessToken}` } });
|
|
39
|
-
if (error || !profile) return
|
|
59
|
+
if (error || !profile) return tokenUserInfo;
|
|
60
|
+
if (typeof tokenProfile.sub !== "string" || profile.sub !== tokenProfile.sub) return tokenUserInfo;
|
|
40
61
|
return {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
62
|
+
...profile,
|
|
63
|
+
...tokenProfile,
|
|
64
|
+
name: getMicrosoftProfileName(tokenProfile) ?? getMicrosoftProfileName(profile),
|
|
65
|
+
email: tokenProfile.email ?? profile.email ?? tokenProfile.preferred_username ?? profile.preferred_username ?? void 0,
|
|
66
|
+
image: tokenProfile.picture ?? profile.picture,
|
|
67
|
+
emailVerified: tokenProfile.email_verified ?? profile.email_verified ?? false
|
|
46
68
|
};
|
|
47
69
|
};
|
|
48
70
|
return {
|
|
49
71
|
providerId: "microsoft-entra-id",
|
|
50
|
-
accountSubject: ({ profile }) => profile.
|
|
51
|
-
|
|
72
|
+
accountSubject: ({ profile }) => typeof profile.oid === "string" ? profile.oid : "",
|
|
73
|
+
discoveryUrl,
|
|
74
|
+
requireIdTokenVerification: true,
|
|
52
75
|
authorizationUrl,
|
|
53
76
|
tokenUrl,
|
|
54
77
|
userInfoUrl,
|
|
@@ -57,6 +80,9 @@ function microsoftEntraId(options) {
|
|
|
57
80
|
tokenEndpointAuth: options.tokenEndpointAuth,
|
|
58
81
|
scopes: options.scopes ?? defaultScopes,
|
|
59
82
|
redirectURI: options.redirectURI,
|
|
83
|
+
endSessionEndpoint: options.endSessionEndpoint,
|
|
84
|
+
postLogoutRedirectURI: options.postLogoutRedirectURI,
|
|
85
|
+
disableProviderLogout: options.disableProviderLogout,
|
|
60
86
|
pkce: options.pkce,
|
|
61
87
|
disableImplicitSignUp: options.disableImplicitSignUp,
|
|
62
88
|
disableSignUp: options.disableSignUp,
|
|
@@ -37,6 +37,9 @@ function okta(options) {
|
|
|
37
37
|
tokenEndpointAuth: options.tokenEndpointAuth,
|
|
38
38
|
scopes: options.scopes ?? defaultScopes,
|
|
39
39
|
redirectURI: options.redirectURI,
|
|
40
|
+
endSessionEndpoint: options.endSessionEndpoint,
|
|
41
|
+
postLogoutRedirectURI: options.postLogoutRedirectURI,
|
|
42
|
+
disableProviderLogout: options.disableProviderLogout,
|
|
40
43
|
pkce: options.pkce,
|
|
41
44
|
disableImplicitSignUp: options.disableImplicitSignUp,
|
|
42
45
|
disableSignUp: options.disableSignUp,
|
|
@@ -47,6 +47,9 @@ function patreon(options) {
|
|
|
47
47
|
tokenEndpointAuth: options.tokenEndpointAuth,
|
|
48
48
|
scopes: options.scopes ?? defaultScopes,
|
|
49
49
|
redirectURI: options.redirectURI,
|
|
50
|
+
endSessionEndpoint: options.endSessionEndpoint,
|
|
51
|
+
postLogoutRedirectURI: options.postLogoutRedirectURI,
|
|
52
|
+
disableProviderLogout: options.disableProviderLogout,
|
|
50
53
|
pkce: options.pkce,
|
|
51
54
|
disableImplicitSignUp: options.disableImplicitSignUp,
|
|
52
55
|
disableSignUp: options.disableSignUp,
|
|
@@ -50,6 +50,9 @@ function slack(options) {
|
|
|
50
50
|
tokenEndpointAuth: options.tokenEndpointAuth,
|
|
51
51
|
scopes: options.scopes ?? defaultScopes,
|
|
52
52
|
redirectURI: options.redirectURI,
|
|
53
|
+
endSessionEndpoint: options.endSessionEndpoint,
|
|
54
|
+
postLogoutRedirectURI: options.postLogoutRedirectURI,
|
|
55
|
+
disableProviderLogout: options.disableProviderLogout,
|
|
53
56
|
pkce: options.pkce,
|
|
54
57
|
disableImplicitSignUp: options.disableImplicitSignUp,
|
|
55
58
|
disableSignUp: options.disableSignUp,
|
|
@@ -53,6 +53,9 @@ function yandex(options) {
|
|
|
53
53
|
tokenEndpointAuth: options.tokenEndpointAuth,
|
|
54
54
|
scopes: options.scopes ?? defaultScopes,
|
|
55
55
|
redirectURI: options.redirectURI,
|
|
56
|
+
endSessionEndpoint: options.endSessionEndpoint,
|
|
57
|
+
postLogoutRedirectURI: options.postLogoutRedirectURI,
|
|
58
|
+
disableProviderLogout: options.disableProviderLogout,
|
|
56
59
|
pkce: options.pkce,
|
|
57
60
|
disableImplicitSignUp: options.disableImplicitSignUp,
|
|
58
61
|
disableSignUp: options.disableSignUp,
|
|
@@ -49,6 +49,17 @@ interface GenericOAuthConfig<ID extends string = string> {
|
|
|
49
49
|
* If provided, the authorization and token endpoints will be fetched from this URL.
|
|
50
50
|
*/
|
|
51
51
|
discoveryUrl?: string | undefined;
|
|
52
|
+
/**
|
|
53
|
+
* Require discovery to provide the issuer and JWKS metadata needed to verify
|
|
54
|
+
* ID tokens before this provider is registered.
|
|
55
|
+
*
|
|
56
|
+
* Enable this when provider identity is derived from ID-token claims. This
|
|
57
|
+
* prevents an unavailable or incomplete discovery document from silently
|
|
58
|
+
* downgrading the provider to unverified token decoding.
|
|
59
|
+
*
|
|
60
|
+
* @default false
|
|
61
|
+
*/
|
|
62
|
+
requireIdTokenVerification?: boolean | undefined;
|
|
52
63
|
/**
|
|
53
64
|
* URL for the authorization endpoint.
|
|
54
65
|
* Optional if using discoveryUrl.
|
|
@@ -64,6 +75,22 @@ interface GenericOAuthConfig<ID extends string = string> {
|
|
|
64
75
|
* Optional if using discoveryUrl.
|
|
65
76
|
*/
|
|
66
77
|
userInfoUrl?: string | undefined;
|
|
78
|
+
/**
|
|
79
|
+
* URL for the OIDC RP-Initiated Logout endpoint.
|
|
80
|
+
* Optional if using discoveryUrl and the discovery document includes
|
|
81
|
+
* `end_session_endpoint`.
|
|
82
|
+
*/
|
|
83
|
+
endSessionEndpoint?: string | undefined;
|
|
84
|
+
/**
|
|
85
|
+
* URL the provider should redirect back to after logout.
|
|
86
|
+
* This must also be registered with the provider as a post-logout redirect URI.
|
|
87
|
+
*/
|
|
88
|
+
postLogoutRedirectURI?: string | undefined;
|
|
89
|
+
/**
|
|
90
|
+
* Disable automatic provider logout on `authClient.signOut()`.
|
|
91
|
+
* When set, sign out only clears the Better Auth session.
|
|
92
|
+
*/
|
|
93
|
+
disableProviderLogout?: boolean | undefined;
|
|
67
94
|
/** OAuth client ID */
|
|
68
95
|
clientId: string;
|
|
69
96
|
/** OAuth client secret */
|
|
@@ -1,15 +1,16 @@
|
|
|
1
|
+
import { getCurrentAdapter } from "@better-auth/core/context";
|
|
1
2
|
//#region src/plugins/jwt/adapter.ts
|
|
2
|
-
const getJwksAdapter = (
|
|
3
|
+
const getJwksAdapter = (baseAdapter, options) => {
|
|
3
4
|
return {
|
|
4
5
|
getAllKeys: async (ctx) => {
|
|
5
6
|
if (options?.adapter?.getJwks) return await options.adapter.getJwks(ctx);
|
|
6
|
-
return await
|
|
7
|
+
return await (await getCurrentAdapter(baseAdapter)).findMany({ model: "jwks" });
|
|
7
8
|
},
|
|
8
9
|
getLatestKey: async (ctx) => {
|
|
9
10
|
const now = /* @__PURE__ */ new Date();
|
|
10
11
|
const isLive = (k) => !k.expiresAt || k.expiresAt > now;
|
|
11
12
|
if (options?.adapter?.getJwks) return (await options.adapter.getJwks(ctx))?.filter(isLive).sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())[0];
|
|
12
|
-
return (await
|
|
13
|
+
return (await (await getCurrentAdapter(baseAdapter)).findMany({ model: "jwks" }))?.filter(isLive).sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())[0];
|
|
13
14
|
},
|
|
14
15
|
/**
|
|
15
16
|
* Look up a key by its `id` (matches the JWS `kid` header). Returns
|
|
@@ -21,7 +22,7 @@ const getJwksAdapter = (adapter, options) => {
|
|
|
21
22
|
*/
|
|
22
23
|
getKeyById: async (ctx, id) => {
|
|
23
24
|
if (options?.adapter?.getJwks) return (await options.adapter.getJwks(ctx))?.find((k) => k.id === id);
|
|
24
|
-
return await
|
|
25
|
+
return await (await getCurrentAdapter(baseAdapter)).findOne({
|
|
25
26
|
model: "jwks",
|
|
26
27
|
where: [{
|
|
27
28
|
field: "id",
|
|
@@ -45,7 +46,7 @@ const getJwksAdapter = (adapter, options) => {
|
|
|
45
46
|
* rationale.
|
|
46
47
|
*/
|
|
47
48
|
getLatestKeyByAlg: async (ctx, alg) => {
|
|
48
|
-
const candidates = options?.adapter?.getJwks ? await options.adapter.getJwks(ctx) : await
|
|
49
|
+
const candidates = options?.adapter?.getJwks ? await options.adapter.getJwks(ctx) : await (await getCurrentAdapter(baseAdapter)).findMany({ model: "jwks" });
|
|
49
50
|
if (!candidates) return void 0;
|
|
50
51
|
const configAlg = options?.jwks?.keyPairConfig?.alg ?? "EdDSA";
|
|
51
52
|
const now = /* @__PURE__ */ new Date();
|
|
@@ -53,7 +54,7 @@ const getJwksAdapter = (adapter, options) => {
|
|
|
53
54
|
},
|
|
54
55
|
createJwk: async (ctx, webKey) => {
|
|
55
56
|
if (options?.adapter?.createJwk) return await options.adapter.createJwk(webKey, ctx);
|
|
56
|
-
return await
|
|
57
|
+
return await (await getCurrentAdapter(baseAdapter)).create({
|
|
57
58
|
model: "jwks",
|
|
58
59
|
data: {
|
|
59
60
|
...webKey,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ClientFetchOption } from "@better-auth/core";
|
|
2
|
+
import { BetterFetch } from "@better-fetch/fetch";
|
|
2
3
|
//#region src/plugins/one-tap/client.d.ts
|
|
3
4
|
declare global {
|
|
4
5
|
interface Window {
|
|
@@ -162,10 +163,10 @@ declare const oneTapClient: (options: GoogleOneTapOptions) => {
|
|
|
162
163
|
onResponse(ctx: import("@better-fetch/fetch").ResponseContext): Promise<void>;
|
|
163
164
|
};
|
|
164
165
|
}[];
|
|
165
|
-
getActions: ($fetch:
|
|
166
|
+
getActions: ($fetch: BetterFetch, _: import("@better-auth/core").ClientStore) => {
|
|
166
167
|
oneTap: (opts?: GoogleOneTapActionOptions | undefined, fetchOptions?: ClientFetchOption | undefined) => Promise<void>;
|
|
167
168
|
};
|
|
168
|
-
getAtoms($fetch:
|
|
169
|
+
getAtoms($fetch: BetterFetch): {};
|
|
169
170
|
};
|
|
170
171
|
//#endregion
|
|
171
172
|
export { GoogleOneTapActionOptions, GoogleOneTapOptions, GsiButtonConfiguration, oneTapClient };
|
|
@@ -88,7 +88,8 @@ async function getHttpTestInstance(options, config) {
|
|
|
88
88
|
clientOptions: config?.clientOptions,
|
|
89
89
|
disableTestUser: config?.disableTestUser,
|
|
90
90
|
testUser: config?.testUser,
|
|
91
|
-
testWith: config?.testWith
|
|
91
|
+
testWith: config?.testWith,
|
|
92
|
+
transaction: config?.transaction
|
|
92
93
|
});
|
|
93
94
|
server.setRequestHandler(config?.handler ? config.handler(instance.auth) : toNodeHandler(instance.auth.handler));
|
|
94
95
|
return {
|
|
@@ -15,6 +15,7 @@ declare function getTestInstance<O extends Partial<BetterAuthOptions>, C extends
|
|
|
15
15
|
disableTestUser?: boolean;
|
|
16
16
|
testUser?: Partial<User>;
|
|
17
17
|
testWith?: "sqlite" | "postgres" | "mongodb" | "mysql";
|
|
18
|
+
transaction?: boolean;
|
|
18
19
|
} | undefined): Promise<{
|
|
19
20
|
auth: Auth<O>;
|
|
20
21
|
client: AuthClient<(C extends undefined ? {} : C) & {
|
|
@@ -67,10 +67,12 @@ async function getTestInstance(options, config) {
|
|
|
67
67
|
secret: "better-auth-secret-that-is-long-enough-for-validation-test",
|
|
68
68
|
database: testWith === "postgres" ? {
|
|
69
69
|
db: await getPostgres(),
|
|
70
|
-
type: "postgres"
|
|
70
|
+
type: "postgres",
|
|
71
|
+
transaction: config?.transaction
|
|
71
72
|
} : testWith === "mongodb" ? await Promise.all([mongodbClient(), await import("../adapters/mongodb-adapter/index.mjs")]).then(([db, { mongodbAdapter }]) => mongodbAdapter(db)) : testWith === "mysql" ? {
|
|
72
73
|
db: await getMysql(),
|
|
73
|
-
type: "mysql"
|
|
74
|
+
type: "mysql",
|
|
75
|
+
transaction: config?.transaction
|
|
74
76
|
} : await getSqlite(),
|
|
75
77
|
emailAndPassword: { enabled: true },
|
|
76
78
|
rateLimit: { enabled: false },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "better-auth",
|
|
3
|
-
"version": "1.7.0-rc.
|
|
3
|
+
"version": "1.7.0-rc.3",
|
|
4
4
|
"description": "The most comprehensive authentication framework for TypeScript.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -465,13 +465,13 @@
|
|
|
465
465
|
"kysely": "^0.28.17 || ^0.29.0",
|
|
466
466
|
"nanostores": "^1.3.0",
|
|
467
467
|
"zod": "^4.3.6",
|
|
468
|
-
"@better-auth/core": "1.7.0-rc.
|
|
469
|
-
"@better-auth/drizzle-adapter": "1.7.0-rc.
|
|
470
|
-
"@better-auth/kysely-adapter": "1.7.0-rc.
|
|
471
|
-
"@better-auth/memory-adapter": "1.7.0-rc.
|
|
472
|
-
"@better-auth/mongo-adapter": "1.7.0-rc.
|
|
473
|
-
"@better-auth/prisma-adapter": "1.7.0-rc.
|
|
474
|
-
"@better-auth/telemetry": "1.7.0-rc.
|
|
468
|
+
"@better-auth/core": "1.7.0-rc.3",
|
|
469
|
+
"@better-auth/drizzle-adapter": "1.7.0-rc.3",
|
|
470
|
+
"@better-auth/kysely-adapter": "1.7.0-rc.3",
|
|
471
|
+
"@better-auth/memory-adapter": "1.7.0-rc.3",
|
|
472
|
+
"@better-auth/mongo-adapter": "1.7.0-rc.3",
|
|
473
|
+
"@better-auth/prisma-adapter": "1.7.0-rc.3",
|
|
474
|
+
"@better-auth/telemetry": "1.7.0-rc.3"
|
|
475
475
|
},
|
|
476
476
|
"devDependencies": {
|
|
477
477
|
"@lynx-js/react": "^0.121.2",
|