@better-auth/core 1.7.0-beta.3 → 1.7.0-beta.5
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 +3 -3
- package/dist/context/global.mjs +1 -1
- package/dist/db/adapter/factory.mjs +62 -0
- package/dist/db/adapter/index.d.mts +35 -1
- package/dist/db/adapter/types.d.mts +1 -1
- package/dist/db/get-tables.mjs +3 -3
- package/dist/db/schema/account.d.mts +1 -1
- package/dist/db/schema/account.mjs +1 -1
- package/dist/db/type.d.mts +12 -0
- package/dist/env/env-impl.mjs +1 -1
- package/dist/error/codes.d.mts +6 -0
- package/dist/error/codes.mjs +6 -0
- package/dist/index.d.mts +2 -2
- package/dist/instrumentation/tracer.mjs +1 -1
- package/dist/oauth2/authorization-params.d.mts +12 -0
- package/dist/oauth2/authorization-params.mjs +12 -0
- package/dist/oauth2/basic-credentials.d.mts +30 -0
- package/dist/oauth2/basic-credentials.mjs +64 -0
- package/dist/oauth2/client-assertion.d.mts +38 -22
- package/dist/oauth2/client-assertion.mjs +63 -28
- package/dist/oauth2/client-credentials-token.d.mts +19 -40
- package/dist/oauth2/client-credentials-token.mjs +18 -29
- package/dist/oauth2/create-authorization-url.d.mts +13 -2
- package/dist/oauth2/create-authorization-url.mjs +28 -7
- package/dist/oauth2/index.d.mts +13 -8
- package/dist/oauth2/index.mjs +11 -7
- package/dist/oauth2/oauth-provider.d.mts +149 -11
- package/dist/oauth2/refresh-access-token.d.mts +20 -40
- package/dist/oauth2/refresh-access-token.mjs +20 -33
- package/dist/oauth2/scopes.d.mts +76 -0
- package/dist/oauth2/scopes.mjs +96 -0
- package/dist/oauth2/token-endpoint-auth.d.mts +17 -0
- package/dist/oauth2/token-endpoint-auth.mjs +89 -0
- package/dist/oauth2/utils.d.mts +9 -1
- package/dist/oauth2/utils.mjs +14 -2
- package/dist/oauth2/validate-authorization-code.d.mts +17 -52
- package/dist/oauth2/validate-authorization-code.mjs +17 -30
- package/dist/oauth2/verify-id-token.d.mts +26 -0
- package/dist/oauth2/verify-id-token.mjs +62 -0
- package/dist/oauth2/verify.d.mts +14 -0
- package/dist/oauth2/verify.mjs +38 -12
- package/dist/social-providers/apple.d.mts +18 -20
- package/dist/social-providers/apple.mjs +15 -28
- package/dist/social-providers/atlassian.d.mts +8 -2
- package/dist/social-providers/atlassian.mjs +9 -6
- package/dist/social-providers/cognito.d.mts +29 -3
- package/dist/social-providers/cognito.mjs +30 -34
- package/dist/social-providers/discord.d.mts +8 -2
- package/dist/social-providers/discord.mjs +20 -6
- package/dist/social-providers/dropbox.d.mts +8 -2
- package/dist/social-providers/dropbox.mjs +10 -9
- package/dist/social-providers/facebook.d.mts +24 -3
- package/dist/social-providers/facebook.mjs +51 -24
- package/dist/social-providers/figma.d.mts +8 -2
- package/dist/social-providers/figma.mjs +8 -7
- package/dist/social-providers/github.d.mts +8 -2
- package/dist/social-providers/github.mjs +9 -8
- package/dist/social-providers/gitlab.d.mts +8 -2
- package/dist/social-providers/gitlab.mjs +8 -7
- package/dist/social-providers/google.d.mts +32 -4
- package/dist/social-providers/google.mjs +26 -29
- package/dist/social-providers/huggingface.d.mts +8 -2
- package/dist/social-providers/huggingface.mjs +11 -10
- package/dist/social-providers/index.d.mts +322 -75
- package/dist/social-providers/kakao.d.mts +8 -2
- package/dist/social-providers/kakao.mjs +11 -10
- package/dist/social-providers/kick.d.mts +8 -2
- package/dist/social-providers/kick.mjs +7 -6
- package/dist/social-providers/line.d.mts +11 -3
- package/dist/social-providers/line.mjs +14 -15
- package/dist/social-providers/linear.d.mts +8 -2
- package/dist/social-providers/linear.mjs +7 -6
- package/dist/social-providers/linkedin.d.mts +8 -2
- package/dist/social-providers/linkedin.mjs +12 -11
- package/dist/social-providers/microsoft-entra-id.d.mts +33 -7
- package/dist/social-providers/microsoft-entra-id.mjs +28 -38
- package/dist/social-providers/naver.d.mts +8 -2
- package/dist/social-providers/naver.mjs +7 -6
- package/dist/social-providers/notion.d.mts +8 -2
- package/dist/social-providers/notion.mjs +9 -6
- package/dist/social-providers/paybin.d.mts +8 -2
- package/dist/social-providers/paybin.mjs +12 -11
- package/dist/social-providers/paypal.d.mts +8 -3
- package/dist/social-providers/paypal.mjs +10 -14
- package/dist/social-providers/polar.d.mts +8 -2
- package/dist/social-providers/polar.mjs +11 -10
- package/dist/social-providers/railway.d.mts +8 -2
- package/dist/social-providers/railway.mjs +11 -10
- package/dist/social-providers/reddit.d.mts +8 -2
- package/dist/social-providers/reddit.mjs +11 -9
- package/dist/social-providers/roblox.d.mts +8 -2
- package/dist/social-providers/roblox.mjs +15 -5
- package/dist/social-providers/salesforce.d.mts +8 -2
- package/dist/social-providers/salesforce.mjs +11 -10
- package/dist/social-providers/slack.d.mts +8 -2
- package/dist/social-providers/slack.mjs +18 -15
- package/dist/social-providers/spotify.d.mts +8 -2
- package/dist/social-providers/spotify.mjs +7 -6
- package/dist/social-providers/tiktok.d.mts +8 -2
- package/dist/social-providers/tiktok.mjs +21 -5
- package/dist/social-providers/twitch.d.mts +8 -2
- package/dist/social-providers/twitch.mjs +7 -6
- package/dist/social-providers/twitter.d.mts +7 -2
- package/dist/social-providers/twitter.mjs +11 -10
- package/dist/social-providers/vercel.d.mts +8 -2
- package/dist/social-providers/vercel.mjs +7 -9
- package/dist/social-providers/vk.d.mts +8 -2
- package/dist/social-providers/vk.mjs +7 -6
- package/dist/social-providers/wechat.d.mts +8 -2
- package/dist/social-providers/wechat.mjs +16 -6
- package/dist/social-providers/zoom.d.mts +10 -3
- package/dist/social-providers/zoom.mjs +14 -15
- package/dist/types/context.d.mts +33 -11
- package/dist/types/index.d.mts +1 -1
- package/dist/types/init-options.d.mts +121 -6
- package/dist/utils/ip.d.mts +5 -4
- package/dist/utils/ip.mjs +3 -3
- package/dist/utils/redirect-uri.d.mts +20 -0
- package/dist/utils/redirect-uri.mjs +48 -0
- package/dist/utils/string.d.mts +5 -1
- package/dist/utils/string.mjs +20 -1
- package/dist/utils/url.d.mts +18 -1
- package/dist/utils/url.mjs +30 -1
- package/package.json +13 -12
- package/src/db/adapter/factory.ts +126 -0
- package/src/db/adapter/index.ts +32 -0
- package/src/db/adapter/types.ts +1 -0
- package/src/db/get-tables.ts +8 -3
- package/src/db/schema/account.ts +14 -2
- package/src/db/type.ts +12 -0
- package/src/env/env-impl.ts +1 -2
- package/src/error/codes.ts +6 -0
- package/src/oauth2/authorization-params.ts +28 -0
- package/src/oauth2/basic-credentials.ts +87 -0
- package/src/oauth2/client-assertion.ts +131 -58
- package/src/oauth2/client-credentials-token.ts +48 -72
- package/src/oauth2/create-authorization-url.ts +30 -8
- package/src/oauth2/index.ts +42 -10
- package/src/oauth2/oauth-provider.ts +161 -12
- package/src/oauth2/refresh-access-token.ts +52 -78
- package/src/oauth2/scopes.ts +118 -0
- package/src/oauth2/token-endpoint-auth.ts +221 -0
- package/src/oauth2/utils.ts +21 -5
- package/src/oauth2/validate-authorization-code.ts +55 -85
- package/src/oauth2/verify-id-token.ts +111 -0
- package/src/oauth2/verify.ts +82 -15
- package/src/social-providers/apple.ts +32 -45
- package/src/social-providers/atlassian.ts +20 -9
- package/src/social-providers/cognito.ts +51 -48
- package/src/social-providers/discord.ts +37 -22
- package/src/social-providers/dropbox.ts +20 -12
- package/src/social-providers/facebook.ts +108 -57
- package/src/social-providers/figma.ts +21 -10
- package/src/social-providers/github.ts +16 -10
- package/src/social-providers/gitlab.ts +16 -8
- package/src/social-providers/google.ts +67 -46
- package/src/social-providers/huggingface.ts +20 -9
- package/src/social-providers/kakao.ts +18 -9
- package/src/social-providers/kick.ts +20 -8
- package/src/social-providers/line.ts +39 -37
- package/src/social-providers/linear.ts +20 -7
- package/src/social-providers/linkedin.ts +16 -10
- package/src/social-providers/microsoft-entra-id.ts +66 -64
- package/src/social-providers/naver.ts +14 -7
- package/src/social-providers/notion.ts +20 -7
- package/src/social-providers/paybin.ts +16 -11
- package/src/social-providers/paypal.ts +12 -25
- package/src/social-providers/polar.ts +20 -9
- package/src/social-providers/railway.ts +20 -9
- package/src/social-providers/reddit.ts +22 -10
- package/src/social-providers/roblox.ts +31 -15
- package/src/social-providers/salesforce.ts +21 -10
- package/src/social-providers/slack.ts +31 -16
- package/src/social-providers/spotify.ts +20 -7
- package/src/social-providers/tiktok.ts +32 -13
- package/src/social-providers/twitch.ts +14 -9
- package/src/social-providers/twitter.ts +18 -8
- package/src/social-providers/vercel.ts +24 -11
- package/src/social-providers/vk.ts +20 -7
- package/src/social-providers/wechat.ts +28 -8
- package/src/social-providers/zoom.ts +28 -19
- package/src/types/context.ts +33 -12
- package/src/types/index.ts +7 -0
- package/src/types/init-options.ts +148 -5
- package/src/utils/ip.ts +12 -13
- package/src/utils/redirect-uri.ts +54 -0
- package/src/utils/string.ts +37 -0
- package/src/utils/url.ts +28 -0
package/src/db/get-tables.ts
CHANGED
|
@@ -261,10 +261,15 @@ export const getAuthTables = (
|
|
|
261
261
|
options.account?.fields?.refreshTokenExpiresAt ||
|
|
262
262
|
"refreshTokenExpiresAt",
|
|
263
263
|
},
|
|
264
|
-
scope
|
|
265
|
-
|
|
264
|
+
// Renamed from the legacy `scope` column. The migration generator
|
|
265
|
+
// only adds this column; it does not transform the legacy `scope`
|
|
266
|
+
// value. Upgrading installs need a manual data migration (split
|
|
267
|
+
// legacy `scope` on comma/space, trim, drop empties, dedupe). Order
|
|
268
|
+
// is insignificant per RFC 6749 §3.3.
|
|
269
|
+
grantedScopes: {
|
|
270
|
+
type: "string[]",
|
|
266
271
|
required: false,
|
|
267
|
-
fieldName: options.account?.fields?.
|
|
272
|
+
fieldName: options.account?.fields?.grantedScopes || "grantedScopes",
|
|
268
273
|
},
|
|
269
274
|
password: {
|
|
270
275
|
type: "string",
|
package/src/db/schema/account.ts
CHANGED
|
@@ -23,9 +23,21 @@ export const accountSchema = coreSchema.extend({
|
|
|
23
23
|
*/
|
|
24
24
|
refreshTokenExpiresAt: z.date().nullish(),
|
|
25
25
|
/**
|
|
26
|
-
* The scopes
|
|
26
|
+
* The scopes the user has granted, as last observed (durable, per-account,
|
|
27
|
+
* the unit of revocation and the refresh ceiling). A native array, not a
|
|
28
|
+
* delimited string: scope order is insignificant per RFC 6749 §3.3, so the
|
|
29
|
+
* value is normalized (trimmed, deduped, sorted) on write.
|
|
30
|
+
*
|
|
31
|
+
* Renamed from the legacy comma-joined `scope` string. Breaking, with no
|
|
32
|
+
* automatic data migration (and no read-time shim): the migration generator
|
|
33
|
+
* only adds the new `grantedScopes` column, so legacy accounts read as empty
|
|
34
|
+
* here until an upgrade backfills `grantedScopes` from the old `scope` values
|
|
35
|
+
* (split on comma/space, trim, drop empties, dedupe). See the release
|
|
36
|
+
* changeset for the backfill.
|
|
37
|
+
*
|
|
38
|
+
* @see https://www.rfc-editor.org/rfc/rfc6749#section-3.3
|
|
27
39
|
*/
|
|
28
|
-
|
|
40
|
+
grantedScopes: z.array(z.string()).nullish(),
|
|
29
41
|
/**
|
|
30
42
|
* Password is only stored in the credential provider
|
|
31
43
|
*/
|
package/src/db/type.ts
CHANGED
|
@@ -311,6 +311,18 @@ export interface SecondaryStorage {
|
|
|
311
311
|
* @returns - Value of the key
|
|
312
312
|
*/
|
|
313
313
|
get: (key: string) => Awaitable<unknown>;
|
|
314
|
+
/**
|
|
315
|
+
* Atomically get a value and delete it from storage.
|
|
316
|
+
*
|
|
317
|
+
* This is optional for backwards compatibility with existing secondary
|
|
318
|
+
* storage implementations. Single-use credential consumers use it when
|
|
319
|
+
* present to avoid a read-then-delete race.
|
|
320
|
+
*
|
|
321
|
+
* TODO(secondary-storage-atomic-consume): make this required in the next
|
|
322
|
+
* breaking release, or require database-backed verification storage for
|
|
323
|
+
* security-sensitive consume paths.
|
|
324
|
+
*/
|
|
325
|
+
getAndDelete?: (key: string) => Awaitable<unknown>;
|
|
314
326
|
set: (
|
|
315
327
|
/**
|
|
316
328
|
* Key to store
|
package/src/env/env-impl.ts
CHANGED
|
@@ -46,8 +46,7 @@ function toBoolean(val: boolean | string | undefined) {
|
|
|
46
46
|
return val ? val !== "false" : false;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
export const nodeENV =
|
|
50
|
-
(typeof process !== "undefined" && process.env && process.env.NODE_ENV) || "";
|
|
49
|
+
export const nodeENV = env.NODE_ENV ?? "";
|
|
51
50
|
|
|
52
51
|
/** Detect if `NODE_ENV` environment variable is `production` */
|
|
53
52
|
export const isProduction = nodeENV === "production";
|
package/src/error/codes.ts
CHANGED
|
@@ -29,6 +29,11 @@ export const BASE_ERROR_CODES = defineErrorCodes({
|
|
|
29
29
|
TOKEN_EXPIRED: "Token expired",
|
|
30
30
|
ID_TOKEN_NOT_SUPPORTED: "id_token not supported",
|
|
31
31
|
FAILED_TO_GET_USER_INFO: "Failed to get user info",
|
|
32
|
+
PROVIDER_NOT_SUPPORTED: "Provider not supported",
|
|
33
|
+
TOKEN_REFRESH_NOT_SUPPORTED: "Token refresh not supported",
|
|
34
|
+
REFRESH_TOKEN_NOT_FOUND: "Refresh token not found",
|
|
35
|
+
FAILED_TO_GET_ACCESS_TOKEN: "Failed to get a valid access token",
|
|
36
|
+
FAILED_TO_REFRESH_ACCESS_TOKEN: "Failed to refresh access token",
|
|
32
37
|
USER_EMAIL_NOT_FOUND: "User email not found",
|
|
33
38
|
EMAIL_NOT_VERIFIED: "Email not verified",
|
|
34
39
|
PASSWORD_TOO_SHORT: "Password too short",
|
|
@@ -37,6 +42,7 @@ export const BASE_ERROR_CODES = defineErrorCodes({
|
|
|
37
42
|
USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL:
|
|
38
43
|
"User already exists. Use another email.",
|
|
39
44
|
EMAIL_CAN_NOT_BE_UPDATED: "Email can not be updated",
|
|
45
|
+
CHANGE_EMAIL_DISABLED: "Change email is disabled",
|
|
40
46
|
CREDENTIAL_ACCOUNT_NOT_FOUND: "Credential account not found",
|
|
41
47
|
SESSION_EXPIRED: "Session expired. Re-authenticate to perform this action.",
|
|
42
48
|
FAILED_TO_UNLINK_LAST_ACCOUNT: "You can't unlink your last account",
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
2
|
+
import {
|
|
3
|
+
RESERVED_AUTHORIZATION_PARAMS,
|
|
4
|
+
RESERVED_AUTHORIZATION_PARAMS_SET,
|
|
5
|
+
} from "./create-authorization-url";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Zod schema for the `additionalParams` field on social sign-in and
|
|
9
|
+
* account-linking request bodies. Rejects any key reserved by the
|
|
10
|
+
* authorization-URL builder (see `RESERVED_AUTHORIZATION_PARAMS`), so
|
|
11
|
+
* a caller cannot overwrite `state`, PKCE, `redirect_uri`, etc.
|
|
12
|
+
*/
|
|
13
|
+
export const additionalAuthorizationParamsSchema = z
|
|
14
|
+
.record(z.string(), z.string())
|
|
15
|
+
.refine(
|
|
16
|
+
(value) =>
|
|
17
|
+
!Object.keys(value).some((key) =>
|
|
18
|
+
RESERVED_AUTHORIZATION_PARAMS_SET.has(key),
|
|
19
|
+
),
|
|
20
|
+
{
|
|
21
|
+
message: `additionalParams cannot include reserved OAuth parameters: ${RESERVED_AUTHORIZATION_PARAMS.join(", ")}`,
|
|
22
|
+
},
|
|
23
|
+
)
|
|
24
|
+
.meta({
|
|
25
|
+
description:
|
|
26
|
+
"Extra query parameters to append to the provider authorization URL (e.g. Cognito identity_provider, Google hd).",
|
|
27
|
+
})
|
|
28
|
+
.optional();
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { base64 } from "@better-auth/utils/base64";
|
|
2
|
+
|
|
3
|
+
// RFC 7235 §2.1: auth scheme is case-insensitive and is followed by one or
|
|
4
|
+
// more SP before the credentials. The trailing capture is everything after
|
|
5
|
+
// the whitespace (may be empty, which downstream checks reject).
|
|
6
|
+
const BASIC_AUTHORIZATION_PATTERN = /^Basic +(.*)$/i;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Encodes a value using `application/x-www-form-urlencoded` per the URL
|
|
10
|
+
* Living Standard. Differs from `encodeURIComponent` in two ways: it escapes
|
|
11
|
+
* `!`, `'`, `(`, and `)`, and it represents space as `+` rather than `%20`.
|
|
12
|
+
* `*` is left unescaped, matching the URL Standard's percent-encode set.
|
|
13
|
+
*/
|
|
14
|
+
function formUrlEncode(value: string): string {
|
|
15
|
+
return new URLSearchParams({ v: value }).toString().slice("v=".length);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Inverse of `formUrlEncode`: decodes a single `application/x-www-form-urlencoded`
|
|
20
|
+
* value, handling both `+` and `%20` as space.
|
|
21
|
+
*/
|
|
22
|
+
function formUrlDecode(value: string): string {
|
|
23
|
+
const decoded = new URLSearchParams(`v=${value}`).get("v");
|
|
24
|
+
if (decoded === null) {
|
|
25
|
+
throw new Error("form-url-encoded value could not be decoded");
|
|
26
|
+
}
|
|
27
|
+
return decoded;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Encodes an OAuth client id and secret as an HTTP Basic credential string.
|
|
32
|
+
*
|
|
33
|
+
* Follows RFC 6749 §2.3.1: both values are `application/x-www-form-urlencoded`
|
|
34
|
+
* prior to base64 encoding. The returned string is the full value of the
|
|
35
|
+
* `Authorization` header, including the `Basic ` prefix.
|
|
36
|
+
*/
|
|
37
|
+
export function encodeBasicCredentials(
|
|
38
|
+
clientId: string,
|
|
39
|
+
clientSecret: string,
|
|
40
|
+
): string {
|
|
41
|
+
const payload = `${formUrlEncode(clientId)}:${formUrlEncode(clientSecret)}`;
|
|
42
|
+
return `Basic ${base64.encode(payload)}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Decodes an `Authorization: Basic …` header value into its OAuth client id
|
|
47
|
+
* and secret.
|
|
48
|
+
*
|
|
49
|
+
* Scheme matching is case-insensitive and tolerates one or more spaces
|
|
50
|
+
* between the scheme and credentials per RFC 7235 §2.1. The base64 payload
|
|
51
|
+
* is split on the first `:` only, so secrets containing colons round-trip
|
|
52
|
+
* correctly. Each half is form-url-decoded per RFC 6749 §2.3.1, accepting
|
|
53
|
+
* both `+` and `%20` as space. Per the URL Living Standard, invalid
|
|
54
|
+
* percent-escapes pass through as-is; downstream client lookup will fail
|
|
55
|
+
* with `invalid_client` for malformed credentials.
|
|
56
|
+
*
|
|
57
|
+
* Throws when the header is not a Basic credential, when the base64 payload
|
|
58
|
+
* contains no `:`, or when either half is empty.
|
|
59
|
+
*/
|
|
60
|
+
export function decodeBasicCredentials(authorization: string): {
|
|
61
|
+
clientId: string;
|
|
62
|
+
clientSecret: string;
|
|
63
|
+
} {
|
|
64
|
+
const match = authorization.match(BASIC_AUTHORIZATION_PATTERN);
|
|
65
|
+
if (!match) {
|
|
66
|
+
throw new Error("Authorization header is not a Basic credential");
|
|
67
|
+
}
|
|
68
|
+
const encoded = match[1] ?? "";
|
|
69
|
+
const decoded = new TextDecoder().decode(base64.decode(encoded));
|
|
70
|
+
const separatorIndex = decoded.indexOf(":");
|
|
71
|
+
if (separatorIndex === -1) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
"Basic credential is missing the client id/secret separator",
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
const rawClientId = decoded.slice(0, separatorIndex);
|
|
77
|
+
const rawClientSecret = decoded.slice(separatorIndex + 1);
|
|
78
|
+
if (!rawClientId || !rawClientSecret) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
"Basic credential client id and secret must both be non-empty",
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
clientId: formUrlDecode(rawClientId),
|
|
85
|
+
clientSecret: formUrlDecode(rawClientSecret),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { JWTHeaderParameters } from "jose";
|
|
2
2
|
import { importJWK, importPKCS8, SignJWT } from "jose";
|
|
3
|
+
import type { Awaitable } from "../types";
|
|
3
4
|
|
|
4
5
|
/** Asymmetric signing algorithms compatible with private_key_jwt (RFC 7523). */
|
|
5
|
-
export const
|
|
6
|
+
export const PRIVATE_KEY_JWT_SIGNING_ALGORITHMS = [
|
|
6
7
|
"RS256",
|
|
7
8
|
"RS384",
|
|
8
9
|
"RS512",
|
|
@@ -15,15 +16,79 @@ export const ASSERTION_SIGNING_ALGORITHMS = [
|
|
|
15
16
|
"EdDSA",
|
|
16
17
|
] as const;
|
|
17
18
|
|
|
18
|
-
export type
|
|
19
|
-
(typeof
|
|
19
|
+
export type PrivateKeyJwtSigningAlgorithm =
|
|
20
|
+
(typeof PRIVATE_KEY_JWT_SIGNING_ALGORITHMS)[number];
|
|
21
|
+
|
|
22
|
+
function assertSupportedPrivateKeyJwtAlgorithm(
|
|
23
|
+
candidate: string,
|
|
24
|
+
): asserts candidate is PrivateKeyJwtSigningAlgorithm {
|
|
25
|
+
if (
|
|
26
|
+
!(PRIVATE_KEY_JWT_SIGNING_ALGORITHMS as readonly string[]).includes(
|
|
27
|
+
candidate,
|
|
28
|
+
)
|
|
29
|
+
) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
`Unsupported private_key_jwt signing algorithm: ${candidate}. Use one of ${PRIVATE_KEY_JWT_SIGNING_ALGORITHMS.join(", ")}.`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Validates `private_key_jwt` options eagerly and returns the algorithm to
|
|
38
|
+
* use for signing.
|
|
39
|
+
*
|
|
40
|
+
* Asserts that key material is configured, that any explicit `algorithm` is
|
|
41
|
+
* supported, that any JWK-embedded `alg` is supported, and that the two
|
|
42
|
+
* agree when both are set.
|
|
43
|
+
*/
|
|
44
|
+
function resolveValidPrivateKeyJwtOptions(options: {
|
|
45
|
+
privateKeyJwk?: JsonWebKey;
|
|
46
|
+
privateKeyPem?: string;
|
|
47
|
+
algorithm?: PrivateKeyJwtSigningAlgorithm;
|
|
48
|
+
}): PrivateKeyJwtSigningAlgorithm {
|
|
49
|
+
if (!options.privateKeyJwk && !options.privateKeyPem) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
"private_key_jwt requires either privateKeyJwk or privateKeyPem",
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
if (options.algorithm) {
|
|
55
|
+
assertSupportedPrivateKeyJwtAlgorithm(options.algorithm);
|
|
56
|
+
}
|
|
57
|
+
const jwkAlg = options.privateKeyJwk?.alg;
|
|
58
|
+
if (typeof jwkAlg === "string") {
|
|
59
|
+
assertSupportedPrivateKeyJwtAlgorithm(jwkAlg);
|
|
60
|
+
}
|
|
61
|
+
if (
|
|
62
|
+
options.algorithm &&
|
|
63
|
+
typeof jwkAlg === "string" &&
|
|
64
|
+
options.algorithm !== jwkAlg
|
|
65
|
+
) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
`JWK alg "${jwkAlg}" does not match configured algorithm "${options.algorithm}". Remove the JWK alg field, or pass an algorithm that matches the JWK.`,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
return options.algorithm ?? (typeof jwkAlg === "string" ? jwkAlg : "RS256");
|
|
71
|
+
}
|
|
20
72
|
|
|
21
73
|
export const CLIENT_ASSERTION_TYPE =
|
|
22
74
|
"urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
|
|
23
75
|
|
|
24
|
-
export
|
|
25
|
-
|
|
26
|
-
|
|
76
|
+
export type ClientAssertionGrantType =
|
|
77
|
+
| "authorization_code"
|
|
78
|
+
| "refresh_token"
|
|
79
|
+
| "client_credentials";
|
|
80
|
+
|
|
81
|
+
export interface ClientAssertionContext {
|
|
82
|
+
clientId: string;
|
|
83
|
+
tokenEndpoint: string;
|
|
84
|
+
grantType: ClientAssertionGrantType;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export type ClientAssertionGetter = (
|
|
88
|
+
context: ClientAssertionContext,
|
|
89
|
+
) => Awaitable<string>;
|
|
90
|
+
|
|
91
|
+
export interface PrivateKeyJwtClientAssertionGetterOptions {
|
|
27
92
|
/** Private key in JWK format for signing. */
|
|
28
93
|
privateKeyJwk?: JsonWebKey;
|
|
29
94
|
/** Private key in PKCS#8 PEM format for signing. */
|
|
@@ -31,9 +96,7 @@ export interface ClientAssertionConfig {
|
|
|
31
96
|
/** Key ID to include in the JWT header. */
|
|
32
97
|
kid?: string;
|
|
33
98
|
/** Asymmetric signing algorithm. Symmetric algorithms (HS256) and "none" are not allowed. @default "RS256" */
|
|
34
|
-
algorithm?:
|
|
35
|
-
/** Token endpoint URL (used as the JWT `aud` claim). */
|
|
36
|
-
tokenEndpoint?: string;
|
|
99
|
+
algorithm?: PrivateKeyJwtSigningAlgorithm;
|
|
37
100
|
/** Assertion lifetime in seconds. @default 120 */
|
|
38
101
|
expiresIn?: number;
|
|
39
102
|
}
|
|
@@ -41,10 +104,16 @@ export interface ClientAssertionConfig {
|
|
|
41
104
|
/**
|
|
42
105
|
* Signs an RFC 7523 client assertion JWT for `private_key_jwt` authentication.
|
|
43
106
|
*
|
|
44
|
-
* The JWT contains
|
|
45
|
-
*
|
|
107
|
+
* The JWT contains these claims:
|
|
108
|
+
*
|
|
109
|
+
* - iss=clientId
|
|
110
|
+
* - sub=clientId
|
|
111
|
+
* - aud=tokenEndpoint
|
|
112
|
+
* - exp=now + 120s
|
|
113
|
+
* - jti=unique
|
|
114
|
+
* - iat=now
|
|
46
115
|
*/
|
|
47
|
-
export async function
|
|
116
|
+
export async function signPrivateKeyJwtClientAssertion({
|
|
48
117
|
clientId,
|
|
49
118
|
tokenEndpoint,
|
|
50
119
|
privateKeyJwk,
|
|
@@ -58,34 +127,30 @@ export async function signClientAssertion({
|
|
|
58
127
|
privateKeyJwk?: JsonWebKey;
|
|
59
128
|
privateKeyPem?: string;
|
|
60
129
|
kid?: string;
|
|
61
|
-
algorithm?:
|
|
130
|
+
algorithm?: PrivateKeyJwtSigningAlgorithm;
|
|
62
131
|
expiresIn?: number;
|
|
63
132
|
}): Promise<string> {
|
|
64
|
-
|
|
65
|
-
|
|
133
|
+
const resolvedAlg = resolveValidPrivateKeyJwtOptions({
|
|
134
|
+
privateKeyJwk,
|
|
135
|
+
privateKeyPem,
|
|
136
|
+
algorithm,
|
|
137
|
+
});
|
|
138
|
+
// Fall back to the JWK-embedded kid when not explicitly provided (RFC 7517).
|
|
139
|
+
// JsonWebKey types include alg but not kid; access kid via index.
|
|
66
140
|
const jwk = privateKeyJwk as Record<string, unknown> | undefined;
|
|
67
141
|
const resolvedKid = kid ?? (jwk?.kid as string | undefined);
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
(privateKeyJwk
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
let key: Awaited<ReturnType<typeof importJWK>>;
|
|
74
|
-
if (privateKeyJwk) {
|
|
75
|
-
key = await importJWK(privateKeyJwk, resolvedAlg);
|
|
76
|
-
} else if (privateKeyPem) {
|
|
77
|
-
key = await importPKCS8(privateKeyPem, resolvedAlg);
|
|
78
|
-
} else {
|
|
79
|
-
throw new Error(
|
|
80
|
-
"private_key_jwt requires either privateKeyJwk or privateKeyPem",
|
|
81
|
-
);
|
|
82
|
-
}
|
|
142
|
+
|
|
143
|
+
const key: Awaited<ReturnType<typeof importJWK>> = privateKeyJwk
|
|
144
|
+
? await importJWK(privateKeyJwk, resolvedAlg)
|
|
145
|
+
: await importPKCS8(privateKeyPem as string, resolvedAlg);
|
|
83
146
|
|
|
84
147
|
const now = Math.floor(Date.now() / 1000);
|
|
85
148
|
const jti = crypto.randomUUID();
|
|
86
149
|
|
|
87
150
|
const header: JWTHeaderParameters = { alg: resolvedAlg, typ: "JWT" };
|
|
88
|
-
if (resolvedKid)
|
|
151
|
+
if (resolvedKid) {
|
|
152
|
+
header.kid = resolvedKid;
|
|
153
|
+
}
|
|
89
154
|
|
|
90
155
|
return new SignJWT({})
|
|
91
156
|
.setProtectedHeader(header)
|
|
@@ -99,36 +164,44 @@ export async function signClientAssertion({
|
|
|
99
164
|
}
|
|
100
165
|
|
|
101
166
|
/**
|
|
102
|
-
*
|
|
103
|
-
*
|
|
167
|
+
* Creates a client assertion getter for `private_key_jwt` authentication.
|
|
168
|
+
*
|
|
169
|
+
* Validates options eagerly (key material, supported algorithm, JWK alg
|
|
170
|
+
* agreement) so misconfiguration surfaces at construction rather than on the
|
|
171
|
+
* first token request. The returned function signs a fresh RFC 7523 JWT
|
|
172
|
+
* assertion for every token endpoint request.
|
|
104
173
|
*/
|
|
105
|
-
export
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
if (!assertion) {
|
|
116
|
-
const audEndpoint = tokenEndpoint ?? clientAssertion.tokenEndpoint;
|
|
117
|
-
if (!audEndpoint) {
|
|
118
|
-
throw new Error(
|
|
119
|
-
"private_key_jwt requires a tokenEndpoint for the JWT audience claim",
|
|
120
|
-
);
|
|
121
|
-
}
|
|
122
|
-
assertion = await signClientAssertion({
|
|
174
|
+
export function createPrivateKeyJwtClientAssertionGetter(
|
|
175
|
+
options: PrivateKeyJwtClientAssertionGetterOptions,
|
|
176
|
+
): ClientAssertionGetter {
|
|
177
|
+
resolveValidPrivateKeyJwtOptions({
|
|
178
|
+
privateKeyJwk: options.privateKeyJwk,
|
|
179
|
+
privateKeyPem: options.privateKeyPem,
|
|
180
|
+
algorithm: options.algorithm,
|
|
181
|
+
});
|
|
182
|
+
return ({ clientId, tokenEndpoint }) =>
|
|
183
|
+
signPrivateKeyJwtClientAssertion({
|
|
123
184
|
clientId,
|
|
124
|
-
tokenEndpoint
|
|
125
|
-
privateKeyJwk:
|
|
126
|
-
privateKeyPem:
|
|
127
|
-
kid:
|
|
128
|
-
algorithm:
|
|
129
|
-
expiresIn:
|
|
185
|
+
tokenEndpoint,
|
|
186
|
+
privateKeyJwk: options.privateKeyJwk,
|
|
187
|
+
privateKeyPem: options.privateKeyPem,
|
|
188
|
+
kid: options.kid,
|
|
189
|
+
algorithm: options.algorithm,
|
|
190
|
+
expiresIn: options.expiresIn,
|
|
130
191
|
});
|
|
131
|
-
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Resolves a client assertion getter into `client_assertion` + `client_assertion_type` params for injection into a token request body.
|
|
196
|
+
*/
|
|
197
|
+
export async function resolveClientAssertionParams({
|
|
198
|
+
getClientAssertion,
|
|
199
|
+
context,
|
|
200
|
+
}: {
|
|
201
|
+
getClientAssertion: ClientAssertionGetter;
|
|
202
|
+
context: ClientAssertionContext;
|
|
203
|
+
}): Promise<Record<string, string>> {
|
|
204
|
+
const assertion = await getClientAssertion(context);
|
|
132
205
|
return {
|
|
133
206
|
client_assertion: assertion,
|
|
134
207
|
client_assertion_type: CLIENT_ASSERTION_TYPE,
|
|
@@ -1,72 +1,70 @@
|
|
|
1
|
-
import { base64 } from "@better-auth/utils/base64";
|
|
2
1
|
import { betterFetch } from "@better-fetch/fetch";
|
|
3
2
|
import type { AwaitableFunction } from "../types";
|
|
4
|
-
import type { ClientAssertionConfig } from "./client-assertion";
|
|
5
|
-
import { resolveAssertionParams } from "./client-assertion";
|
|
6
3
|
import type { OAuth2Tokens, ProviderOptions } from "./oauth-provider";
|
|
4
|
+
import type {
|
|
5
|
+
TokenEndpointAuth,
|
|
6
|
+
TokenEndpointSecretAuthentication,
|
|
7
|
+
} from "./token-endpoint-auth";
|
|
8
|
+
import { applyTokenEndpointAuth } from "./token-endpoint-auth";
|
|
9
|
+
|
|
10
|
+
interface ClientCredentialsTokenRequestInput {
|
|
11
|
+
options: AwaitableFunction<ProviderOptions>;
|
|
12
|
+
scope?: string | undefined;
|
|
13
|
+
authentication?: TokenEndpointSecretAuthentication | undefined;
|
|
14
|
+
tokenEndpointAuth?: TokenEndpointAuth | undefined;
|
|
15
|
+
tokenEndpoint?: string | undefined;
|
|
16
|
+
resource?: (string | string[]) | undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface ClientCredentialsTokenRequestBaseInput {
|
|
20
|
+
options: ProviderOptions;
|
|
21
|
+
scope?: string | undefined;
|
|
22
|
+
resource?: (string | string[]) | undefined;
|
|
23
|
+
extraParams?: Record<string, string> | undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface ClientCredentialsTokenInput
|
|
27
|
+
extends ClientCredentialsTokenRequestInput {
|
|
28
|
+
tokenEndpoint: string;
|
|
29
|
+
scope: string;
|
|
30
|
+
}
|
|
7
31
|
|
|
8
32
|
export async function clientCredentialsTokenRequest({
|
|
9
33
|
options,
|
|
10
34
|
scope,
|
|
11
35
|
authentication,
|
|
12
|
-
|
|
36
|
+
tokenEndpointAuth,
|
|
13
37
|
tokenEndpoint,
|
|
14
38
|
resource,
|
|
15
|
-
}: {
|
|
16
|
-
options: AwaitableFunction<ProviderOptions>;
|
|
17
|
-
scope?: string | undefined;
|
|
18
|
-
authentication?: ("basic" | "post" | "private_key_jwt") | undefined;
|
|
19
|
-
clientAssertion?: ClientAssertionConfig | undefined;
|
|
20
|
-
/** Token endpoint URL. Used as the JWT `aud` claim when signing assertions. */
|
|
21
|
-
tokenEndpoint?: string | undefined;
|
|
22
|
-
resource?: (string | string[]) | undefined;
|
|
23
|
-
}) {
|
|
39
|
+
}: ClientCredentialsTokenRequestInput) {
|
|
24
40
|
options = typeof options === "function" ? await options() : options;
|
|
25
|
-
|
|
26
|
-
let extraParams: Record<string, string> | undefined;
|
|
27
|
-
if (authentication === "private_key_jwt") {
|
|
28
|
-
if (!clientAssertion) {
|
|
29
|
-
throw new Error(
|
|
30
|
-
"private_key_jwt authentication requires a clientAssertion configuration",
|
|
31
|
-
);
|
|
32
|
-
}
|
|
33
|
-
const primaryClientId = Array.isArray(options.clientId)
|
|
34
|
-
? options.clientId[0]
|
|
35
|
-
: options.clientId;
|
|
36
|
-
extraParams = await resolveAssertionParams({
|
|
37
|
-
clientAssertion,
|
|
38
|
-
clientId: primaryClientId,
|
|
39
|
-
tokenEndpoint,
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return createClientCredentialsTokenRequest({
|
|
41
|
+
const request = buildClientCredentialsTokenRequest({
|
|
44
42
|
options,
|
|
45
43
|
scope,
|
|
46
|
-
authentication,
|
|
47
44
|
resource,
|
|
48
|
-
extraParams,
|
|
49
45
|
});
|
|
46
|
+
|
|
47
|
+
await applyTokenEndpointAuth({
|
|
48
|
+
body: request.body,
|
|
49
|
+
headers: request.headers,
|
|
50
|
+
options,
|
|
51
|
+
tokenEndpoint: tokenEndpoint ?? "",
|
|
52
|
+
grantType: "client_credentials",
|
|
53
|
+
tokenEndpointAuth,
|
|
54
|
+
authentication,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return request;
|
|
50
58
|
}
|
|
51
59
|
|
|
52
|
-
|
|
53
|
-
* @deprecated use async'd clientCredentialsTokenRequest instead
|
|
54
|
-
*/
|
|
55
|
-
export function createClientCredentialsTokenRequest({
|
|
60
|
+
function buildClientCredentialsTokenRequest({
|
|
56
61
|
options,
|
|
57
62
|
scope,
|
|
58
|
-
authentication,
|
|
59
63
|
resource,
|
|
60
64
|
extraParams,
|
|
61
|
-
}: {
|
|
62
|
-
options: ProviderOptions;
|
|
63
|
-
scope?: string | undefined;
|
|
64
|
-
authentication?: ("basic" | "post" | "private_key_jwt") | undefined;
|
|
65
|
-
resource?: (string | string[]) | undefined;
|
|
66
|
-
extraParams?: Record<string, string> | undefined;
|
|
67
|
-
}) {
|
|
65
|
+
}: ClientCredentialsTokenRequestBaseInput) {
|
|
68
66
|
const body = new URLSearchParams();
|
|
69
|
-
const headers: Record<string,
|
|
67
|
+
const headers: Record<string, string> = {
|
|
70
68
|
"content-type": "application/x-www-form-urlencoded",
|
|
71
69
|
accept: "application/json",
|
|
72
70
|
};
|
|
@@ -82,21 +80,6 @@ export function createClientCredentialsTokenRequest({
|
|
|
82
80
|
}
|
|
83
81
|
}
|
|
84
82
|
}
|
|
85
|
-
const primaryClientId = Array.isArray(options.clientId)
|
|
86
|
-
? options.clientId[0]
|
|
87
|
-
: options.clientId;
|
|
88
|
-
if (authentication === "basic") {
|
|
89
|
-
const encodedCredentials = base64.encode(
|
|
90
|
-
`${primaryClientId}:${options.clientSecret ?? ""}`,
|
|
91
|
-
);
|
|
92
|
-
headers["authorization"] = `Basic ${encodedCredentials}`;
|
|
93
|
-
} else {
|
|
94
|
-
body.set("client_id", primaryClientId);
|
|
95
|
-
if (authentication !== "private_key_jwt" && options.clientSecret) {
|
|
96
|
-
body.set("client_secret", options.clientSecret);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
83
|
if (extraParams) {
|
|
101
84
|
for (const [key, value] of Object.entries(extraParams)) {
|
|
102
85
|
if (!body.has(key)) body.append(key, value);
|
|
@@ -114,21 +97,14 @@ export async function clientCredentialsToken({
|
|
|
114
97
|
tokenEndpoint,
|
|
115
98
|
scope,
|
|
116
99
|
authentication,
|
|
117
|
-
|
|
100
|
+
tokenEndpointAuth,
|
|
118
101
|
resource,
|
|
119
|
-
}: {
|
|
120
|
-
options: AwaitableFunction<ProviderOptions>;
|
|
121
|
-
tokenEndpoint: string;
|
|
122
|
-
scope: string;
|
|
123
|
-
authentication?: ("basic" | "post" | "private_key_jwt") | undefined;
|
|
124
|
-
clientAssertion?: ClientAssertionConfig | undefined;
|
|
125
|
-
resource?: (string | string[]) | undefined;
|
|
126
|
-
}): Promise<OAuth2Tokens> {
|
|
102
|
+
}: ClientCredentialsTokenInput): Promise<OAuth2Tokens> {
|
|
127
103
|
const { body, headers } = await clientCredentialsTokenRequest({
|
|
128
104
|
options,
|
|
129
105
|
scope,
|
|
130
106
|
authentication,
|
|
131
|
-
|
|
107
|
+
tokenEndpointAuth,
|
|
132
108
|
tokenEndpoint,
|
|
133
109
|
resource,
|
|
134
110
|
});
|
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
import type { AwaitableFunction } from "../types";
|
|
2
2
|
import type { ProviderOptions } from "./index";
|
|
3
|
-
import { generateCodeChallenge } from "./utils";
|
|
3
|
+
import { generateCodeChallenge, getPrimaryClientId } from "./utils";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Query-parameter names that are populated by the framework as part of the
|
|
7
|
+
* authorization request and must not be overridden by caller-supplied
|
|
8
|
+
* `additionalParams`. Overriding `state`, PKCE, or `redirect_uri` would
|
|
9
|
+
* break the callback correlation and session pinning guarantees.
|
|
10
|
+
*/
|
|
11
|
+
export const RESERVED_AUTHORIZATION_PARAMS = [
|
|
12
|
+
"state",
|
|
13
|
+
"client_id",
|
|
14
|
+
"redirect_uri",
|
|
15
|
+
"response_type",
|
|
16
|
+
"code_challenge",
|
|
17
|
+
"code_challenge_method",
|
|
18
|
+
"scope",
|
|
19
|
+
] as const;
|
|
20
|
+
|
|
21
|
+
export const RESERVED_AUTHORIZATION_PARAMS_SET: ReadonlySet<string> = new Set(
|
|
22
|
+
RESERVED_AUTHORIZATION_PARAMS,
|
|
23
|
+
);
|
|
4
24
|
|
|
5
25
|
export async function createAuthorizationURL({
|
|
6
26
|
id,
|
|
@@ -44,12 +64,13 @@ export async function createAuthorizationURL({
|
|
|
44
64
|
options = typeof options === "function" ? await options() : options;
|
|
45
65
|
const url = new URL(options.authorizationEndpoint || authorizationEndpoint);
|
|
46
66
|
url.searchParams.set("response_type", responseType || "code");
|
|
47
|
-
const primaryClientId =
|
|
48
|
-
|
|
49
|
-
|
|
67
|
+
const primaryClientId = getPrimaryClientId(options.clientId);
|
|
68
|
+
if (!primaryClientId) {
|
|
69
|
+
throw new Error("OAuth provider requires clientId");
|
|
70
|
+
}
|
|
50
71
|
url.searchParams.set("client_id", primaryClientId);
|
|
51
72
|
url.searchParams.set("state", state);
|
|
52
|
-
if (scopes) {
|
|
73
|
+
if (scopes?.length) {
|
|
53
74
|
url.searchParams.set("scope", scopes.join(scopeJoiner || " "));
|
|
54
75
|
}
|
|
55
76
|
url.searchParams.set("redirect_uri", options.redirectURI || redirectURI);
|
|
@@ -81,9 +102,10 @@ export async function createAuthorizationURL({
|
|
|
81
102
|
);
|
|
82
103
|
}
|
|
83
104
|
if (additionalParams) {
|
|
84
|
-
|
|
105
|
+
for (const [key, value] of Object.entries(additionalParams)) {
|
|
106
|
+
if (RESERVED_AUTHORIZATION_PARAMS_SET.has(key)) continue;
|
|
85
107
|
url.searchParams.set(key, value);
|
|
86
|
-
}
|
|
108
|
+
}
|
|
87
109
|
}
|
|
88
|
-
return url;
|
|
110
|
+
return { url, requestedScopes: scopes ?? [] };
|
|
89
111
|
}
|