@better-auth/core 1.7.0-beta.4 → 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 +2 -2
- 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/env/env-impl.mjs +1 -1
- package/dist/error/codes.d.mts +5 -0
- package/dist/error/codes.mjs +5 -0
- package/dist/index.d.mts +2 -2
- package/dist/instrumentation/tracer.mjs +1 -1
- package/dist/oauth2/create-authorization-url.d.mts +4 -1
- package/dist/oauth2/create-authorization-url.mjs +5 -2
- package/dist/oauth2/index.d.mts +4 -2
- package/dist/oauth2/index.mjs +3 -1
- package/dist/oauth2/oauth-provider.d.mts +128 -9
- package/dist/oauth2/refresh-access-token.mjs +1 -1
- package/dist/oauth2/scopes.d.mts +76 -0
- package/dist/oauth2/scopes.mjs +96 -0
- package/dist/oauth2/utils.mjs +2 -1
- 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 +23 -7
- package/dist/social-providers/apple.d.mts +14 -2
- package/dist/social-providers/apple.mjs +12 -36
- package/dist/social-providers/atlassian.d.mts +5 -1
- package/dist/social-providers/atlassian.mjs +4 -4
- package/dist/social-providers/cognito.d.mts +13 -2
- package/dist/social-providers/cognito.mjs +24 -32
- package/dist/social-providers/discord.d.mts +5 -1
- package/dist/social-providers/discord.mjs +7 -6
- package/dist/social-providers/dropbox.d.mts +5 -1
- package/dist/social-providers/dropbox.mjs +5 -5
- package/dist/social-providers/facebook.d.mts +21 -2
- package/dist/social-providers/facebook.mjs +46 -22
- package/dist/social-providers/figma.d.mts +5 -1
- package/dist/social-providers/figma.mjs +5 -5
- package/dist/social-providers/github.d.mts +5 -1
- package/dist/social-providers/github.mjs +4 -4
- package/dist/social-providers/gitlab.d.mts +5 -1
- package/dist/social-providers/gitlab.mjs +6 -6
- package/dist/social-providers/google.d.mts +29 -3
- package/dist/social-providers/google.mjs +24 -30
- package/dist/social-providers/huggingface.d.mts +5 -1
- package/dist/social-providers/huggingface.mjs +8 -8
- package/dist/social-providers/index.d.mts +221 -42
- package/dist/social-providers/kakao.d.mts +5 -1
- package/dist/social-providers/kakao.mjs +8 -8
- package/dist/social-providers/kick.d.mts +5 -1
- package/dist/social-providers/kick.mjs +4 -4
- package/dist/social-providers/line.d.mts +8 -2
- package/dist/social-providers/line.mjs +12 -14
- package/dist/social-providers/linear.d.mts +5 -1
- package/dist/social-providers/linear.mjs +4 -4
- package/dist/social-providers/linkedin.d.mts +5 -1
- package/dist/social-providers/linkedin.mjs +10 -10
- package/dist/social-providers/microsoft-entra-id.d.mts +31 -6
- package/dist/social-providers/microsoft-entra-id.mjs +26 -37
- package/dist/social-providers/naver.d.mts +5 -1
- package/dist/social-providers/naver.mjs +4 -4
- package/dist/social-providers/notion.d.mts +5 -1
- package/dist/social-providers/notion.mjs +4 -4
- package/dist/social-providers/paybin.d.mts +5 -1
- package/dist/social-providers/paybin.mjs +10 -10
- package/dist/social-providers/paypal.d.mts +5 -2
- package/dist/social-providers/paypal.mjs +8 -13
- package/dist/social-providers/polar.d.mts +5 -1
- package/dist/social-providers/polar.mjs +8 -8
- package/dist/social-providers/railway.d.mts +5 -1
- package/dist/social-providers/railway.mjs +9 -9
- package/dist/social-providers/reddit.d.mts +5 -1
- package/dist/social-providers/reddit.mjs +9 -8
- package/dist/social-providers/roblox.d.mts +5 -1
- package/dist/social-providers/roblox.mjs +5 -5
- package/dist/social-providers/salesforce.d.mts +5 -1
- package/dist/social-providers/salesforce.mjs +8 -8
- package/dist/social-providers/slack.d.mts +5 -1
- package/dist/social-providers/slack.mjs +9 -9
- package/dist/social-providers/spotify.d.mts +5 -1
- package/dist/social-providers/spotify.mjs +5 -5
- package/dist/social-providers/tiktok.d.mts +5 -1
- package/dist/social-providers/tiktok.mjs +9 -5
- package/dist/social-providers/twitch.d.mts +5 -1
- package/dist/social-providers/twitch.mjs +4 -4
- package/dist/social-providers/twitter.d.mts +6 -4
- package/dist/social-providers/twitter.mjs +9 -9
- package/dist/social-providers/vercel.d.mts +5 -1
- package/dist/social-providers/vercel.mjs +4 -7
- package/dist/social-providers/vk.d.mts +5 -1
- package/dist/social-providers/vk.mjs +5 -5
- package/dist/social-providers/wechat.d.mts +5 -1
- package/dist/social-providers/wechat.mjs +9 -5
- package/dist/social-providers/zoom.d.mts +6 -1
- package/dist/social-providers/zoom.mjs +15 -9
- package/dist/types/context.d.mts +10 -8
- package/dist/types/index.d.mts +1 -1
- package/dist/types/init-options.d.mts +92 -1
- package/package.json +5 -5
- package/src/db/adapter/factory.ts +10 -2
- package/src/db/get-tables.ts +8 -3
- package/src/db/schema/account.ts +14 -2
- package/src/env/env-impl.ts +1 -2
- package/src/error/codes.ts +5 -0
- package/src/oauth2/create-authorization-url.ts +2 -2
- package/src/oauth2/index.ts +17 -1
- package/src/oauth2/oauth-provider.ts +140 -10
- package/src/oauth2/refresh-access-token.ts +2 -2
- package/src/oauth2/scopes.ts +118 -0
- package/src/oauth2/utils.ts +2 -5
- package/src/oauth2/verify-id-token.ts +111 -0
- package/src/oauth2/verify.ts +62 -11
- package/src/social-providers/apple.ts +24 -61
- package/src/social-providers/atlassian.ts +12 -8
- package/src/social-providers/cognito.ts +25 -47
- package/src/social-providers/discord.ts +19 -8
- package/src/social-providers/dropbox.ts +13 -7
- package/src/social-providers/facebook.ts +97 -51
- package/src/social-providers/figma.ts +13 -9
- package/src/social-providers/github.ts +12 -8
- package/src/social-providers/gitlab.ts +14 -8
- package/src/social-providers/google.ts +66 -47
- package/src/social-providers/huggingface.ts +12 -8
- package/src/social-providers/kakao.ts +16 -8
- package/src/social-providers/kick.ts +12 -7
- package/src/social-providers/line.ts +37 -37
- package/src/social-providers/linear.ts +12 -6
- package/src/social-providers/linkedin.ts +14 -10
- package/src/social-providers/microsoft-entra-id.ts +65 -64
- package/src/social-providers/naver.ts +12 -6
- package/src/social-providers/notion.ts +12 -6
- package/src/social-providers/paybin.ts +14 -11
- package/src/social-providers/paypal.ts +6 -25
- package/src/social-providers/polar.ts +12 -8
- package/src/social-providers/railway.ts +13 -9
- package/src/social-providers/reddit.ts +21 -10
- package/src/social-providers/roblox.ts +18 -7
- package/src/social-providers/salesforce.ts +12 -8
- package/src/social-providers/slack.ts +18 -9
- package/src/social-providers/spotify.ts +13 -7
- package/src/social-providers/tiktok.ts +13 -7
- package/src/social-providers/twitch.ts +12 -8
- package/src/social-providers/twitter.ts +17 -8
- package/src/social-providers/vercel.ts +16 -10
- package/src/social-providers/vk.ts +13 -7
- package/src/social-providers/wechat.ts +20 -8
- package/src/social-providers/zoom.ts +19 -6
- package/src/types/context.ts +8 -8
- package/src/types/index.ts +7 -0
- package/src/types/init-options.ts +119 -0
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { betterFetch } from "@better-fetch/fetch";
|
|
2
2
|
|
|
3
|
-
import { decodeJwt,
|
|
3
|
+
import { decodeJwt, importJWK } from "jose";
|
|
4
4
|
import { logger } from "../env";
|
|
5
5
|
import { APIError, BetterAuthError } from "../error";
|
|
6
|
-
import type {
|
|
6
|
+
import type { ProviderOptions, UpstreamProvider } from "../oauth2";
|
|
7
7
|
import {
|
|
8
8
|
createAuthorizationURL,
|
|
9
9
|
getPrimaryClientId,
|
|
10
10
|
refreshAccessToken,
|
|
11
|
+
resolveRequestedScopes,
|
|
11
12
|
validateAuthorizationCode,
|
|
12
13
|
} from "../oauth2";
|
|
13
14
|
export interface AppleProfile {
|
|
@@ -77,29 +78,14 @@ export interface AppleOptions extends ProviderOptions<AppleProfile> {
|
|
|
77
78
|
audience?: (string | string[]) | undefined;
|
|
78
79
|
}
|
|
79
80
|
|
|
80
|
-
|
|
81
|
-
const data = new TextEncoder().encode(value);
|
|
82
|
-
const digest = await crypto.subtle.digest("SHA-256", data);
|
|
83
|
-
return Array.from(new Uint8Array(digest))
|
|
84
|
-
.map((byte) => byte.toString(16).padStart(2, "0"))
|
|
85
|
-
.join("");
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
async function nonceMatches(jwtNonce: unknown, nonce: string) {
|
|
89
|
-
if (typeof jwtNonce !== "string") {
|
|
90
|
-
return false;
|
|
91
|
-
}
|
|
92
|
-
if (jwtNonce === nonce) {
|
|
93
|
-
return true;
|
|
94
|
-
}
|
|
95
|
-
return jwtNonce === (await sha256Hex(nonce));
|
|
96
|
-
}
|
|
81
|
+
const APPLE_DEFAULT_SCOPES = ["email", "name"];
|
|
97
82
|
|
|
98
83
|
export const apple = (options: AppleOptions) => {
|
|
99
84
|
const tokenEndpoint = "https://appleid.apple.com/auth/token";
|
|
100
85
|
return {
|
|
101
86
|
id: "apple",
|
|
102
87
|
name: "Apple",
|
|
88
|
+
callbackPath: "/callback/apple",
|
|
103
89
|
async createAuthorizationURL({
|
|
104
90
|
state,
|
|
105
91
|
scopes,
|
|
@@ -112,21 +98,22 @@ export const apple = (options: AppleOptions) => {
|
|
|
112
98
|
);
|
|
113
99
|
throw new BetterAuthError("CLIENT_ID_AND_SECRET_REQUIRED");
|
|
114
100
|
}
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
101
|
+
const requestedScopes = resolveRequestedScopes(
|
|
102
|
+
options,
|
|
103
|
+
APPLE_DEFAULT_SCOPES,
|
|
104
|
+
scopes,
|
|
105
|
+
);
|
|
106
|
+
return createAuthorizationURL({
|
|
119
107
|
id: "apple",
|
|
120
108
|
options,
|
|
121
109
|
authorizationEndpoint: "https://appleid.apple.com/auth/authorize",
|
|
122
|
-
scopes:
|
|
110
|
+
scopes: requestedScopes,
|
|
123
111
|
state,
|
|
124
112
|
redirectURI,
|
|
125
113
|
responseMode: "form_post",
|
|
126
114
|
responseType: "code id_token",
|
|
127
115
|
additionalParams,
|
|
128
116
|
});
|
|
129
|
-
return url;
|
|
130
117
|
},
|
|
131
118
|
validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
|
|
132
119
|
return validateAuthorizationCode({
|
|
@@ -137,41 +124,17 @@ export const apple = (options: AppleOptions) => {
|
|
|
137
124
|
tokenEndpoint,
|
|
138
125
|
});
|
|
139
126
|
},
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const publicKey = await getApplePublicKey(kid);
|
|
152
|
-
const { payload: jwtClaims } = await jwtVerify(token, publicKey, {
|
|
153
|
-
algorithms: [jwtAlg],
|
|
154
|
-
issuer: "https://appleid.apple.com",
|
|
155
|
-
audience:
|
|
156
|
-
options.audience && options.audience.length
|
|
157
|
-
? options.audience
|
|
158
|
-
: options.appBundleIdentifier
|
|
159
|
-
? options.appBundleIdentifier
|
|
160
|
-
: options.clientId,
|
|
161
|
-
maxTokenAge: "1h",
|
|
162
|
-
});
|
|
163
|
-
["email_verified", "is_private_email"].forEach((field) => {
|
|
164
|
-
if (jwtClaims[field] !== undefined) {
|
|
165
|
-
jwtClaims[field] = Boolean(jwtClaims[field]);
|
|
166
|
-
}
|
|
167
|
-
});
|
|
168
|
-
if (nonce && !(await nonceMatches(jwtClaims.nonce, nonce))) {
|
|
169
|
-
return false;
|
|
170
|
-
}
|
|
171
|
-
return !!jwtClaims;
|
|
172
|
-
} catch {
|
|
173
|
-
return false;
|
|
174
|
-
}
|
|
127
|
+
idToken: {
|
|
128
|
+
jwks: (header) => getApplePublicKey(header.kid!),
|
|
129
|
+
issuer: "https://appleid.apple.com",
|
|
130
|
+
audience:
|
|
131
|
+
options.audience && options.audience.length
|
|
132
|
+
? options.audience
|
|
133
|
+
: options.appBundleIdentifier
|
|
134
|
+
? options.appBundleIdentifier
|
|
135
|
+
: options.clientId,
|
|
136
|
+
maxTokenAge: "1h",
|
|
137
|
+
nonceComparison: "exact-or-sha256",
|
|
175
138
|
},
|
|
176
139
|
refreshAccessToken: options.refreshAccessToken
|
|
177
140
|
? options.refreshAccessToken
|
|
@@ -226,7 +189,7 @@ export const apple = (options: AppleOptions) => {
|
|
|
226
189
|
};
|
|
227
190
|
},
|
|
228
191
|
options,
|
|
229
|
-
} satisfies
|
|
192
|
+
} satisfies UpstreamProvider<AppleProfile>;
|
|
230
193
|
};
|
|
231
194
|
|
|
232
195
|
export const getApplePublicKey = async (kid: string) => {
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { betterFetch } from "@better-fetch/fetch";
|
|
2
2
|
import { logger } from "../env";
|
|
3
3
|
import { BetterAuthError } from "../error";
|
|
4
|
-
import type {
|
|
4
|
+
import type { ProviderOptions, UpstreamProvider } from "../oauth2";
|
|
5
5
|
import {
|
|
6
6
|
createAuthorizationURL,
|
|
7
7
|
refreshAccessToken,
|
|
8
|
+
resolveRequestedScopes,
|
|
8
9
|
validateAuthorizationCode,
|
|
9
10
|
} from "../oauth2";
|
|
10
11
|
|
|
@@ -29,11 +30,14 @@ export interface AtlassianOptions extends ProviderOptions<AtlassianProfile> {
|
|
|
29
30
|
clientId: string;
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
const ATLASSIAN_DEFAULT_SCOPES = ["read:jira-user", "offline_access"];
|
|
34
|
+
|
|
32
35
|
export const atlassian = (options: AtlassianOptions) => {
|
|
33
36
|
const tokenEndpoint = "https://auth.atlassian.com/oauth/token";
|
|
34
37
|
return {
|
|
35
38
|
id: "atlassian",
|
|
36
39
|
name: "Atlassian",
|
|
40
|
+
callbackPath: "/callback/atlassian",
|
|
37
41
|
|
|
38
42
|
async createAuthorizationURL({
|
|
39
43
|
state,
|
|
@@ -50,17 +54,17 @@ export const atlassian = (options: AtlassianOptions) => {
|
|
|
50
54
|
throw new BetterAuthError("codeVerifier is required for Atlassian");
|
|
51
55
|
}
|
|
52
56
|
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
const requestedScopes = resolveRequestedScopes(
|
|
58
|
+
options,
|
|
59
|
+
ATLASSIAN_DEFAULT_SCOPES,
|
|
60
|
+
scopes,
|
|
61
|
+
);
|
|
58
62
|
|
|
59
63
|
return createAuthorizationURL({
|
|
60
64
|
id: "atlassian",
|
|
61
65
|
options,
|
|
62
66
|
authorizationEndpoint: "https://auth.atlassian.com/authorize",
|
|
63
|
-
scopes:
|
|
67
|
+
scopes: requestedScopes,
|
|
64
68
|
state,
|
|
65
69
|
codeVerifier,
|
|
66
70
|
redirectURI,
|
|
@@ -136,5 +140,5 @@ export const atlassian = (options: AtlassianOptions) => {
|
|
|
136
140
|
},
|
|
137
141
|
|
|
138
142
|
options,
|
|
139
|
-
} satisfies
|
|
143
|
+
} satisfies UpstreamProvider<AtlassianProfile>;
|
|
140
144
|
};
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { betterFetch } from "@better-fetch/fetch";
|
|
2
|
-
import { decodeJwt,
|
|
2
|
+
import { decodeJwt, importJWK } from "jose";
|
|
3
3
|
import { logger } from "../env";
|
|
4
4
|
import { APIError, BetterAuthError } from "../error";
|
|
5
|
-
import type {
|
|
5
|
+
import type { ProviderOptions, UpstreamProvider } from "../oauth2";
|
|
6
6
|
import {
|
|
7
7
|
createAuthorizationURL,
|
|
8
8
|
getPrimaryClientId,
|
|
9
9
|
refreshAccessToken,
|
|
10
|
+
resolveRequestedScopes,
|
|
10
11
|
validateAuthorizationCode,
|
|
11
12
|
} from "../oauth2";
|
|
12
13
|
|
|
@@ -57,6 +58,8 @@ export interface CognitoOptions extends ProviderOptions<CognitoProfile> {
|
|
|
57
58
|
identityProvider?: string | undefined;
|
|
58
59
|
}
|
|
59
60
|
|
|
61
|
+
const COGNITO_DEFAULT_SCOPES = ["openid", "profile", "email"];
|
|
62
|
+
|
|
60
63
|
export const cognito = (options: CognitoOptions) => {
|
|
61
64
|
if (!options.domain || !options.region || !options.userPoolId) {
|
|
62
65
|
logger.error(
|
|
@@ -73,6 +76,7 @@ export const cognito = (options: CognitoOptions) => {
|
|
|
73
76
|
return {
|
|
74
77
|
id: "cognito",
|
|
75
78
|
name: "Cognito",
|
|
79
|
+
callbackPath: "/callback/cognito",
|
|
76
80
|
async createAuthorizationURL({
|
|
77
81
|
state,
|
|
78
82
|
scopes,
|
|
@@ -93,19 +97,19 @@ export const cognito = (options: CognitoOptions) => {
|
|
|
93
97
|
);
|
|
94
98
|
throw new BetterAuthError("CLIENT_SECRET_REQUIRED");
|
|
95
99
|
}
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
100
|
+
const requestedScopes = resolveRequestedScopes(
|
|
101
|
+
options,
|
|
102
|
+
COGNITO_DEFAULT_SCOPES,
|
|
103
|
+
scopes,
|
|
104
|
+
);
|
|
101
105
|
|
|
102
|
-
const url = await createAuthorizationURL({
|
|
106
|
+
const { url } = await createAuthorizationURL({
|
|
103
107
|
id: "cognito",
|
|
104
108
|
options: {
|
|
105
109
|
...options,
|
|
106
110
|
},
|
|
107
111
|
authorizationEndpoint,
|
|
108
|
-
scopes:
|
|
112
|
+
scopes: requestedScopes,
|
|
109
113
|
state,
|
|
110
114
|
codeVerifier,
|
|
111
115
|
redirectURI,
|
|
@@ -126,9 +130,12 @@ export const cognito = (options: CognitoOptions) => {
|
|
|
126
130
|
// Manually append the scope with proper encoding to the URL
|
|
127
131
|
const urlString = url.toString();
|
|
128
132
|
const separator = urlString.includes("?") ? "&" : "?";
|
|
129
|
-
return
|
|
133
|
+
return {
|
|
134
|
+
url: new URL(`${urlString}${separator}scope=${encodedScope}`),
|
|
135
|
+
requestedScopes,
|
|
136
|
+
};
|
|
130
137
|
}
|
|
131
|
-
return url;
|
|
138
|
+
return { url, requestedScopes };
|
|
132
139
|
},
|
|
133
140
|
|
|
134
141
|
validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
|
|
@@ -155,41 +162,12 @@ export const cognito = (options: CognitoOptions) => {
|
|
|
155
162
|
});
|
|
156
163
|
},
|
|
157
164
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
try {
|
|
167
|
-
const decodedHeader = decodeProtectedHeader(token);
|
|
168
|
-
const { kid, alg: jwtAlg } = decodedHeader;
|
|
169
|
-
if (!kid || !jwtAlg) return false;
|
|
170
|
-
|
|
171
|
-
const publicKey = await getCognitoPublicKey(
|
|
172
|
-
kid,
|
|
173
|
-
options.region,
|
|
174
|
-
options.userPoolId,
|
|
175
|
-
);
|
|
176
|
-
const expectedIssuer = `https://cognito-idp.${options.region}.amazonaws.com/${options.userPoolId}`;
|
|
177
|
-
|
|
178
|
-
const { payload: jwtClaims } = await jwtVerify(token, publicKey, {
|
|
179
|
-
algorithms: [jwtAlg],
|
|
180
|
-
issuer: expectedIssuer,
|
|
181
|
-
audience: options.clientId,
|
|
182
|
-
maxTokenAge: "1h",
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
if (nonce && jwtClaims.nonce !== nonce) {
|
|
186
|
-
return false;
|
|
187
|
-
}
|
|
188
|
-
return true;
|
|
189
|
-
} catch (error) {
|
|
190
|
-
logger.error("Failed to verify ID token:", error);
|
|
191
|
-
return false;
|
|
192
|
-
}
|
|
165
|
+
idToken: {
|
|
166
|
+
jwks: (header) =>
|
|
167
|
+
getCognitoPublicKey(header.kid!, options.region, options.userPoolId),
|
|
168
|
+
issuer: `https://cognito-idp.${options.region}.amazonaws.com/${options.userPoolId}`,
|
|
169
|
+
audience: options.clientId,
|
|
170
|
+
maxTokenAge: "1h",
|
|
193
171
|
},
|
|
194
172
|
|
|
195
173
|
async getUserInfo(token) {
|
|
@@ -265,7 +243,7 @@ export const cognito = (options: CognitoOptions) => {
|
|
|
265
243
|
},
|
|
266
244
|
|
|
267
245
|
options,
|
|
268
|
-
} satisfies
|
|
246
|
+
} satisfies UpstreamProvider<CognitoProfile>;
|
|
269
247
|
};
|
|
270
248
|
|
|
271
249
|
export const getCognitoPublicKey = async (
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { betterFetch } from "@better-fetch/fetch";
|
|
2
|
-
import type {
|
|
2
|
+
import type { ProviderOptions, UpstreamProvider } from "../oauth2";
|
|
3
3
|
import {
|
|
4
4
|
createAuthorizationURL,
|
|
5
5
|
refreshAccessToken,
|
|
6
|
+
resolveRequestedScopes,
|
|
6
7
|
validateAuthorizationCode,
|
|
7
8
|
} from "../oauth2";
|
|
8
9
|
export interface DiscordProfile extends Record<string, any> {
|
|
@@ -83,21 +84,31 @@ export interface DiscordOptions extends ProviderOptions<DiscordProfile> {
|
|
|
83
84
|
permissions?: number | undefined;
|
|
84
85
|
}
|
|
85
86
|
|
|
87
|
+
const DISCORD_DEFAULT_SCOPES = ["identify", "email"];
|
|
88
|
+
|
|
86
89
|
export const discord = (options: DiscordOptions) => {
|
|
87
90
|
const tokenEndpoint = "https://discord.com/api/oauth2/token";
|
|
88
91
|
return {
|
|
89
92
|
id: "discord",
|
|
90
93
|
name: "Discord",
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
94
|
+
callbackPath: "/callback/discord",
|
|
95
|
+
async createAuthorizationURL({
|
|
96
|
+
state,
|
|
97
|
+
scopes,
|
|
98
|
+
redirectURI,
|
|
99
|
+
additionalParams,
|
|
100
|
+
}) {
|
|
101
|
+
const requestedScopes = resolveRequestedScopes(
|
|
102
|
+
options,
|
|
103
|
+
DISCORD_DEFAULT_SCOPES,
|
|
104
|
+
scopes,
|
|
105
|
+
);
|
|
106
|
+
const hasBotScope = requestedScopes.includes("bot");
|
|
96
107
|
return createAuthorizationURL({
|
|
97
108
|
id: "discord",
|
|
98
109
|
options,
|
|
99
110
|
authorizationEndpoint: "https://discord.com/api/oauth2/authorize",
|
|
100
|
-
scopes:
|
|
111
|
+
scopes: requestedScopes,
|
|
101
112
|
state,
|
|
102
113
|
redirectURI,
|
|
103
114
|
prompt: options.prompt || "none",
|
|
@@ -170,5 +181,5 @@ export const discord = (options: DiscordOptions) => {
|
|
|
170
181
|
};
|
|
171
182
|
},
|
|
172
183
|
options,
|
|
173
|
-
} satisfies
|
|
184
|
+
} satisfies UpstreamProvider<DiscordProfile>;
|
|
174
185
|
};
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { betterFetch } from "@better-fetch/fetch";
|
|
2
|
-
import type {
|
|
2
|
+
import type { ProviderOptions, UpstreamProvider } from "../oauth2";
|
|
3
3
|
import {
|
|
4
4
|
createAuthorizationURL,
|
|
5
5
|
refreshAccessToken,
|
|
6
|
+
resolveRequestedScopes,
|
|
6
7
|
validateAuthorizationCode,
|
|
7
8
|
} from "../oauth2";
|
|
8
9
|
|
|
@@ -25,12 +26,15 @@ export interface DropboxOptions extends ProviderOptions<DropboxProfile> {
|
|
|
25
26
|
accessType?: ("offline" | "online" | "legacy") | undefined;
|
|
26
27
|
}
|
|
27
28
|
|
|
29
|
+
const DROPBOX_DEFAULT_SCOPES = ["account_info.read"];
|
|
30
|
+
|
|
28
31
|
export const dropbox = (options: DropboxOptions) => {
|
|
29
32
|
const tokenEndpoint = "https://api.dropboxapi.com/oauth2/token";
|
|
30
33
|
|
|
31
34
|
return {
|
|
32
35
|
id: "dropbox",
|
|
33
36
|
name: "Dropbox",
|
|
37
|
+
callbackPath: "/callback/dropbox",
|
|
34
38
|
createAuthorizationURL: async ({
|
|
35
39
|
state,
|
|
36
40
|
scopes,
|
|
@@ -38,14 +42,16 @@ export const dropbox = (options: DropboxOptions) => {
|
|
|
38
42
|
redirectURI,
|
|
39
43
|
additionalParams,
|
|
40
44
|
}) => {
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
+
const requestedScopes = resolveRequestedScopes(
|
|
46
|
+
options,
|
|
47
|
+
DROPBOX_DEFAULT_SCOPES,
|
|
48
|
+
scopes,
|
|
49
|
+
);
|
|
50
|
+
return createAuthorizationURL({
|
|
45
51
|
id: "dropbox",
|
|
46
52
|
options,
|
|
47
53
|
authorizationEndpoint: "https://www.dropbox.com/oauth2/authorize",
|
|
48
|
-
scopes:
|
|
54
|
+
scopes: requestedScopes,
|
|
49
55
|
state,
|
|
50
56
|
redirectURI,
|
|
51
57
|
codeVerifier,
|
|
@@ -110,5 +116,5 @@ export const dropbox = (options: DropboxOptions) => {
|
|
|
110
116
|
};
|
|
111
117
|
},
|
|
112
118
|
options,
|
|
113
|
-
} satisfies
|
|
119
|
+
} satisfies UpstreamProvider<DropboxProfile>;
|
|
114
120
|
};
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { betterFetch } from "@better-fetch/fetch";
|
|
2
|
-
import { createRemoteJWKSet, decodeJwt
|
|
2
|
+
import { createRemoteJWKSet, decodeJwt } from "jose";
|
|
3
3
|
import { logger } from "../env";
|
|
4
4
|
import { BetterAuthError } from "../error";
|
|
5
|
-
import type {
|
|
5
|
+
import type { ProviderOptions, UpstreamProvider } from "../oauth2";
|
|
6
6
|
import {
|
|
7
7
|
createAuthorizationURL,
|
|
8
8
|
getPrimaryClientId,
|
|
9
9
|
refreshAccessToken,
|
|
10
|
+
resolveRequestedScopes,
|
|
10
11
|
validateAuthorizationCode,
|
|
11
12
|
} from "../oauth2";
|
|
12
13
|
export interface FacebookProfile {
|
|
@@ -24,6 +25,58 @@ export interface FacebookProfile {
|
|
|
24
25
|
};
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
interface FacebookDebugTokenData {
|
|
29
|
+
app_id?: string;
|
|
30
|
+
is_valid?: boolean;
|
|
31
|
+
user_id?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Validate an opaque Facebook access token against the configured app.
|
|
36
|
+
*
|
|
37
|
+
* Facebook access tokens are not audience-bound at the Graph `/me` endpoint: a
|
|
38
|
+
* token minted for any Facebook app returns that app's profile. Without this
|
|
39
|
+
* check, a token issued to an unrelated app could be presented to this
|
|
40
|
+
* app's direct sign-in path and accepted as proof of identity. We call the
|
|
41
|
+
* `debug_token` endpoint and require the token to be valid, bound to one of the
|
|
42
|
+
* configured client ids, and tied to a user.
|
|
43
|
+
*
|
|
44
|
+
* @see https://developers.facebook.com/docs/facebook-login/guides/access-tokens/debugging
|
|
45
|
+
*
|
|
46
|
+
* @returns the inspected token's `user_id` when the token is valid and bound to
|
|
47
|
+
* the configured app, otherwise `null`.
|
|
48
|
+
*/
|
|
49
|
+
async function verifyFacebookAccessToken(
|
|
50
|
+
accessToken: string,
|
|
51
|
+
options: FacebookOptions,
|
|
52
|
+
): Promise<string | null> {
|
|
53
|
+
const primaryClientId = getPrimaryClientId(options.clientId);
|
|
54
|
+
if (!primaryClientId || !options.clientSecret) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const clientIds = Array.isArray(options.clientId)
|
|
58
|
+
? options.clientId
|
|
59
|
+
: [options.clientId];
|
|
60
|
+
const appAccessToken = `${primaryClientId}|${options.clientSecret}`;
|
|
61
|
+
const { data, error } = await betterFetch<{ data?: FacebookDebugTokenData }>(
|
|
62
|
+
"https://graph.facebook.com/debug_token",
|
|
63
|
+
{
|
|
64
|
+
query: {
|
|
65
|
+
input_token: accessToken,
|
|
66
|
+
access_token: appAccessToken,
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
);
|
|
70
|
+
if (error || !data?.data) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
const { is_valid, app_id, user_id } = data.data;
|
|
74
|
+
if (is_valid !== true || !app_id || !clientIds.includes(app_id) || !user_id) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
return user_id;
|
|
78
|
+
}
|
|
79
|
+
|
|
27
80
|
export interface FacebookOptions extends ProviderOptions<FacebookProfile> {
|
|
28
81
|
clientId: string | string[];
|
|
29
82
|
/**
|
|
@@ -39,10 +92,13 @@ export interface FacebookOptions extends ProviderOptions<FacebookProfile> {
|
|
|
39
92
|
configId?: string | undefined;
|
|
40
93
|
}
|
|
41
94
|
|
|
95
|
+
const FACEBOOK_DEFAULT_SCOPES = ["email", "public_profile"];
|
|
96
|
+
|
|
42
97
|
export const facebook = (options: FacebookOptions) => {
|
|
43
98
|
return {
|
|
44
99
|
id: "facebook",
|
|
45
100
|
name: "Facebook",
|
|
101
|
+
callbackPath: "/callback/facebook",
|
|
46
102
|
async createAuthorizationURL({
|
|
47
103
|
state,
|
|
48
104
|
scopes,
|
|
@@ -56,16 +112,16 @@ export const facebook = (options: FacebookOptions) => {
|
|
|
56
112
|
);
|
|
57
113
|
throw new BetterAuthError("CLIENT_ID_AND_SECRET_REQUIRED");
|
|
58
114
|
}
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
return
|
|
115
|
+
const requestedScopes = resolveRequestedScopes(
|
|
116
|
+
options,
|
|
117
|
+
FACEBOOK_DEFAULT_SCOPES,
|
|
118
|
+
scopes,
|
|
119
|
+
);
|
|
120
|
+
return createAuthorizationURL({
|
|
65
121
|
id: "facebook",
|
|
66
122
|
options,
|
|
67
123
|
authorizationEndpoint: "https://www.facebook.com/v24.0/dialog/oauth",
|
|
68
|
-
scopes:
|
|
124
|
+
scopes: requestedScopes,
|
|
69
125
|
state,
|
|
70
126
|
redirectURI,
|
|
71
127
|
loginHint,
|
|
@@ -83,46 +139,17 @@ export const facebook = (options: FacebookOptions) => {
|
|
|
83
139
|
tokenEndpoint: "https://graph.facebook.com/v24.0/oauth/access_token",
|
|
84
140
|
});
|
|
85
141
|
},
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (token.split(".").length === 3) {
|
|
98
|
-
try {
|
|
99
|
-
const { payload: jwtClaims } = await jwtVerify(
|
|
100
|
-
token,
|
|
101
|
-
createRemoteJWKSet(
|
|
102
|
-
// https://developers.facebook.com/docs/facebook-login/limited-login/token/#jwks
|
|
103
|
-
new URL(
|
|
104
|
-
"https://limited.facebook.com/.well-known/oauth/openid/jwks/",
|
|
105
|
-
),
|
|
106
|
-
),
|
|
107
|
-
{
|
|
108
|
-
algorithms: ["RS256"],
|
|
109
|
-
audience: options.clientId,
|
|
110
|
-
issuer: "https://www.facebook.com",
|
|
111
|
-
},
|
|
112
|
-
);
|
|
113
|
-
|
|
114
|
-
if (nonce && jwtClaims.nonce !== nonce) {
|
|
115
|
-
return false;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
return !!jwtClaims;
|
|
119
|
-
} catch {
|
|
120
|
-
return false;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/* access_token */
|
|
125
|
-
return true;
|
|
142
|
+
idToken: {
|
|
143
|
+
// https://developers.facebook.com/docs/facebook-login/limited-login/token/#jwks
|
|
144
|
+
jwks: createRemoteJWKSet(
|
|
145
|
+
new URL("https://limited.facebook.com/.well-known/oauth/openid/jwks/"),
|
|
146
|
+
),
|
|
147
|
+
issuer: "https://www.facebook.com",
|
|
148
|
+
audience: options.clientId,
|
|
149
|
+
algorithms: ["RS256"],
|
|
150
|
+
// Facebook also accepts an opaque Graph access token on the client sign-in path;
|
|
151
|
+
// identity is then resolved by getUserInfo via the Graph API, which validates it.
|
|
152
|
+
allowOpaqueToken: true,
|
|
126
153
|
},
|
|
127
154
|
refreshAccessToken: options.refreshAccessToken
|
|
128
155
|
? options.refreshAccessToken
|
|
@@ -183,6 +210,21 @@ export const facebook = (options: FacebookOptions) => {
|
|
|
183
210
|
};
|
|
184
211
|
}
|
|
185
212
|
|
|
213
|
+
// The profile is fetched with `accessToken`, which is the credential
|
|
214
|
+
// that actually proves identity here. It is a separate request field
|
|
215
|
+
// from the `idToken` checked by the shared id_token verifier via the
|
|
216
|
+
// declarative `idToken` config. Since an opaque token is not app-bound
|
|
217
|
+
// at `/me`, validate this exact token against the configured app
|
|
218
|
+
// before trusting the profile it returns.
|
|
219
|
+
const accessToken = token.accessToken;
|
|
220
|
+
if (!accessToken) {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
const tokenUserId = await verifyFacebookAccessToken(accessToken, options);
|
|
224
|
+
if (!tokenUserId) {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
|
|
186
228
|
const fields = [
|
|
187
229
|
"id",
|
|
188
230
|
"name",
|
|
@@ -195,13 +237,17 @@ export const facebook = (options: FacebookOptions) => {
|
|
|
195
237
|
{
|
|
196
238
|
auth: {
|
|
197
239
|
type: "Bearer",
|
|
198
|
-
token:
|
|
240
|
+
token: accessToken,
|
|
199
241
|
},
|
|
200
242
|
},
|
|
201
243
|
);
|
|
202
244
|
if (error) {
|
|
203
245
|
return null;
|
|
204
246
|
}
|
|
247
|
+
// Bind the validated token to the profile it returned.
|
|
248
|
+
if (profile.id !== tokenUserId) {
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
205
251
|
const userMap = await options.mapProfileToUser?.(profile);
|
|
206
252
|
return {
|
|
207
253
|
user: {
|
|
@@ -216,5 +262,5 @@ export const facebook = (options: FacebookOptions) => {
|
|
|
216
262
|
};
|
|
217
263
|
},
|
|
218
264
|
options,
|
|
219
|
-
} satisfies
|
|
265
|
+
} satisfies UpstreamProvider<FacebookProfile>;
|
|
220
266
|
};
|