@better-auth/core 1.3.26 → 1.3.28
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/.turbo/turbo-build.log +60 -9
- package/build.config.ts +7 -0
- package/dist/db/adapter/index.cjs +2 -0
- package/dist/db/adapter/index.d.cts +14 -0
- package/dist/db/adapter/index.d.mts +14 -0
- package/dist/db/adapter/index.d.ts +14 -0
- package/dist/db/adapter/index.mjs +1 -0
- package/dist/db/index.cjs +89 -0
- package/dist/db/index.d.cts +16 -107
- package/dist/db/index.d.mts +16 -107
- package/dist/db/index.d.ts +16 -107
- package/dist/db/index.mjs +69 -0
- package/dist/env/index.cjs +312 -0
- package/dist/env/index.d.cts +36 -0
- package/dist/env/index.d.mts +36 -0
- package/dist/env/index.d.ts +36 -0
- package/dist/env/index.mjs +297 -0
- package/dist/error/index.cjs +44 -0
- package/dist/error/index.d.cts +33 -0
- package/dist/error/index.d.mts +33 -0
- package/dist/error/index.d.ts +33 -0
- package/dist/error/index.mjs +41 -0
- package/dist/index.d.cts +179 -1
- package/dist/index.d.mts +179 -1
- package/dist/index.d.ts +179 -1
- package/dist/middleware/index.cjs +25 -0
- package/dist/middleware/index.d.cts +14 -0
- package/dist/middleware/index.d.mts +14 -0
- package/dist/middleware/index.d.ts +14 -0
- package/dist/middleware/index.mjs +21 -0
- package/dist/oauth2/index.cjs +368 -0
- package/dist/oauth2/index.d.cts +100 -0
- package/dist/oauth2/index.d.mts +100 -0
- package/dist/oauth2/index.d.ts +100 -0
- package/dist/oauth2/index.mjs +357 -0
- package/dist/shared/core.BJPBStdk.d.ts +1693 -0
- package/dist/shared/core.Bl6TpxyD.d.mts +181 -0
- package/dist/shared/core.Bqe5IGAi.d.ts +13 -0
- package/dist/shared/core.BwoNUcJQ.d.cts +53 -0
- package/dist/shared/core.BwoNUcJQ.d.mts +53 -0
- package/dist/shared/core.BwoNUcJQ.d.ts +53 -0
- package/dist/shared/core.CajxAutx.d.cts +143 -0
- package/dist/shared/core.CajxAutx.d.mts +143 -0
- package/dist/shared/core.CajxAutx.d.ts +143 -0
- package/dist/shared/core.CkkLHQWc.d.mts +1693 -0
- package/dist/shared/core.DkdZ1o38.d.ts +181 -0
- package/dist/shared/core.Dl-70uns.d.cts +84 -0
- package/dist/shared/core.Dl-70uns.d.mts +84 -0
- package/dist/shared/core.Dl-70uns.d.ts +84 -0
- package/dist/shared/core.DyEdx0m7.d.cts +181 -0
- package/dist/shared/core.E9DfzGLz.d.mts +13 -0
- package/dist/shared/core.HqYn20Fi.d.cts +13 -0
- package/dist/shared/core.gYIBmdi1.d.cts +1693 -0
- package/dist/social-providers/index.cjs +2793 -0
- package/dist/social-providers/index.d.cts +3903 -0
- package/dist/social-providers/index.d.mts +3903 -0
- package/dist/social-providers/index.d.ts +3903 -0
- package/dist/social-providers/index.mjs +2743 -0
- package/dist/utils/index.cjs +7 -0
- package/dist/utils/index.d.cts +10 -0
- package/dist/utils/index.d.mts +10 -0
- package/dist/utils/index.d.ts +10 -0
- package/dist/utils/index.mjs +5 -0
- package/package.json +109 -2
- package/src/db/adapter/index.ts +448 -0
- package/src/db/index.ts +13 -0
- package/src/db/plugin.ts +11 -0
- package/src/db/schema/account.ts +34 -0
- package/src/db/schema/rate-limit.ts +21 -0
- package/src/db/schema/session.ts +17 -0
- package/src/db/schema/shared.ts +7 -0
- package/src/db/schema/user.ts +16 -0
- package/src/db/schema/verification.ts +15 -0
- package/src/db/type.ts +50 -0
- package/src/env/color-depth.ts +172 -0
- package/src/env/env-impl.ts +123 -0
- package/src/env/index.ts +23 -0
- package/src/env/logger.test.ts +33 -0
- package/src/env/logger.ts +145 -0
- package/src/error/codes.ts +31 -0
- package/src/error/index.ts +11 -0
- package/src/index.ts +1 -1
- package/src/middleware/index.ts +33 -0
- package/src/oauth2/client-credentials-token.ts +102 -0
- package/src/oauth2/create-authorization-url.ts +85 -0
- package/src/oauth2/index.ts +22 -0
- package/src/oauth2/oauth-provider.ts +194 -0
- package/src/oauth2/refresh-access-token.ts +124 -0
- package/src/oauth2/utils.ts +36 -0
- package/src/oauth2/validate-authorization-code.ts +156 -0
- package/src/social-providers/apple.ts +213 -0
- package/src/social-providers/atlassian.ts +130 -0
- package/src/social-providers/cognito.ts +269 -0
- package/src/social-providers/discord.ts +172 -0
- package/src/social-providers/dropbox.ts +112 -0
- package/src/social-providers/facebook.ts +204 -0
- package/src/social-providers/figma.ts +115 -0
- package/src/social-providers/github.ts +154 -0
- package/src/social-providers/gitlab.ts +152 -0
- package/src/social-providers/google.ts +171 -0
- package/src/social-providers/huggingface.ts +116 -0
- package/src/social-providers/index.ts +118 -0
- package/src/social-providers/kakao.ts +178 -0
- package/src/social-providers/kick.ts +95 -0
- package/src/social-providers/line.ts +169 -0
- package/src/social-providers/linear.ts +120 -0
- package/src/social-providers/linkedin.ts +110 -0
- package/src/social-providers/microsoft-entra-id.ts +243 -0
- package/src/social-providers/naver.ts +112 -0
- package/src/social-providers/notion.ts +106 -0
- package/src/social-providers/paypal.ts +261 -0
- package/src/social-providers/reddit.ts +122 -0
- package/src/social-providers/roblox.ts +110 -0
- package/src/social-providers/salesforce.ts +157 -0
- package/src/social-providers/slack.ts +114 -0
- package/src/social-providers/spotify.ts +93 -0
- package/src/social-providers/tiktok.ts +211 -0
- package/src/social-providers/twitch.ts +111 -0
- package/src/social-providers/twitter.ts +194 -0
- package/src/social-providers/vk.ts +128 -0
- package/src/social-providers/zoom.ts +218 -0
- package/src/types/context.ts +313 -0
- package/src/types/cookie.ts +7 -0
- package/src/types/helper.ts +5 -0
- package/src/types/index.ts +20 -1
- package/src/types/init-options.ts +1161 -0
- package/src/types/plugin-client.ts +69 -0
- package/src/types/plugin.ts +134 -0
- package/src/utils/error-codes.ts +51 -0
- package/src/utils/index.ts +1 -0
|
@@ -0,0 +1,2743 @@
|
|
|
1
|
+
import * as z from 'zod';
|
|
2
|
+
import { betterFetch } from '@better-fetch/fetch';
|
|
3
|
+
import { APIError } from 'better-call';
|
|
4
|
+
import { decodeJwt, decodeProtectedHeader, jwtVerify, importJWK, createRemoteJWKSet } from 'jose';
|
|
5
|
+
import { refreshAccessToken, validateAuthorizationCode, createAuthorizationURL, getOAuth2Tokens, generateCodeChallenge } from '@better-auth/core/oauth2';
|
|
6
|
+
import { BetterAuthError } from '../error/index.mjs';
|
|
7
|
+
import { logger } from '@better-auth/core/env';
|
|
8
|
+
import { base64 } from '@better-auth/utils/base64';
|
|
9
|
+
import '@better-auth/core/utils';
|
|
10
|
+
|
|
11
|
+
const apple = (options) => {
|
|
12
|
+
const tokenEndpoint = "https://appleid.apple.com/auth/token";
|
|
13
|
+
return {
|
|
14
|
+
id: "apple",
|
|
15
|
+
name: "Apple",
|
|
16
|
+
async createAuthorizationURL({ state, scopes, redirectURI }) {
|
|
17
|
+
const _scope = options.disableDefaultScope ? [] : ["email", "name"];
|
|
18
|
+
options.scope && _scope.push(...options.scope);
|
|
19
|
+
scopes && _scope.push(...scopes);
|
|
20
|
+
const url = await createAuthorizationURL({
|
|
21
|
+
id: "apple",
|
|
22
|
+
options,
|
|
23
|
+
authorizationEndpoint: "https://appleid.apple.com/auth/authorize",
|
|
24
|
+
scopes: _scope,
|
|
25
|
+
state,
|
|
26
|
+
redirectURI,
|
|
27
|
+
responseMode: "form_post",
|
|
28
|
+
responseType: "code id_token"
|
|
29
|
+
});
|
|
30
|
+
return url;
|
|
31
|
+
},
|
|
32
|
+
validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
|
|
33
|
+
return validateAuthorizationCode({
|
|
34
|
+
code,
|
|
35
|
+
codeVerifier,
|
|
36
|
+
redirectURI,
|
|
37
|
+
options,
|
|
38
|
+
tokenEndpoint
|
|
39
|
+
});
|
|
40
|
+
},
|
|
41
|
+
async verifyIdToken(token, nonce) {
|
|
42
|
+
if (options.disableIdTokenSignIn) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
if (options.verifyIdToken) {
|
|
46
|
+
return options.verifyIdToken(token, nonce);
|
|
47
|
+
}
|
|
48
|
+
const decodedHeader = decodeProtectedHeader(token);
|
|
49
|
+
const { kid, alg: jwtAlg } = decodedHeader;
|
|
50
|
+
if (!kid || !jwtAlg) return false;
|
|
51
|
+
const publicKey = await getApplePublicKey(kid);
|
|
52
|
+
const { payload: jwtClaims } = await jwtVerify(token, publicKey, {
|
|
53
|
+
algorithms: [jwtAlg],
|
|
54
|
+
issuer: "https://appleid.apple.com",
|
|
55
|
+
audience: options.audience && options.audience.length ? options.audience : options.appBundleIdentifier ? options.appBundleIdentifier : options.clientId,
|
|
56
|
+
maxTokenAge: "1h"
|
|
57
|
+
});
|
|
58
|
+
["email_verified", "is_private_email"].forEach((field) => {
|
|
59
|
+
if (jwtClaims[field] !== void 0) {
|
|
60
|
+
jwtClaims[field] = Boolean(jwtClaims[field]);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
if (nonce && jwtClaims.nonce !== nonce) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
return !!jwtClaims;
|
|
67
|
+
},
|
|
68
|
+
refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => {
|
|
69
|
+
return refreshAccessToken({
|
|
70
|
+
refreshToken,
|
|
71
|
+
options: {
|
|
72
|
+
clientId: options.clientId,
|
|
73
|
+
clientKey: options.clientKey,
|
|
74
|
+
clientSecret: options.clientSecret
|
|
75
|
+
},
|
|
76
|
+
tokenEndpoint: "https://appleid.apple.com/auth/token"
|
|
77
|
+
});
|
|
78
|
+
},
|
|
79
|
+
async getUserInfo(token) {
|
|
80
|
+
if (options.getUserInfo) {
|
|
81
|
+
return options.getUserInfo(token);
|
|
82
|
+
}
|
|
83
|
+
if (!token.idToken) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
const profile = decodeJwt(token.idToken);
|
|
87
|
+
if (!profile) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
const name = token.user ? `${token.user.name?.firstName} ${token.user.name?.lastName}` : profile.name || profile.email;
|
|
91
|
+
const emailVerified = typeof profile.email_verified === "boolean" ? profile.email_verified : profile.email_verified === "true";
|
|
92
|
+
const enrichedProfile = {
|
|
93
|
+
...profile,
|
|
94
|
+
name
|
|
95
|
+
};
|
|
96
|
+
const userMap = await options.mapProfileToUser?.(enrichedProfile);
|
|
97
|
+
return {
|
|
98
|
+
user: {
|
|
99
|
+
id: profile.sub,
|
|
100
|
+
name: enrichedProfile.name,
|
|
101
|
+
emailVerified,
|
|
102
|
+
email: profile.email,
|
|
103
|
+
...userMap
|
|
104
|
+
},
|
|
105
|
+
data: enrichedProfile
|
|
106
|
+
};
|
|
107
|
+
},
|
|
108
|
+
options
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
const getApplePublicKey = async (kid) => {
|
|
112
|
+
const APPLE_BASE_URL = "https://appleid.apple.com";
|
|
113
|
+
const JWKS_APPLE_URI = "/auth/keys";
|
|
114
|
+
const { data } = await betterFetch(`${APPLE_BASE_URL}${JWKS_APPLE_URI}`);
|
|
115
|
+
if (!data?.keys) {
|
|
116
|
+
throw new APIError("BAD_REQUEST", {
|
|
117
|
+
message: "Keys not found"
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
const jwk = data.keys.find((key) => key.kid === kid);
|
|
121
|
+
if (!jwk) {
|
|
122
|
+
throw new Error(`JWK with kid ${kid} not found`);
|
|
123
|
+
}
|
|
124
|
+
return await importJWK(jwk, jwk.alg);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const atlassian = (options) => {
|
|
128
|
+
return {
|
|
129
|
+
id: "atlassian",
|
|
130
|
+
name: "Atlassian",
|
|
131
|
+
async createAuthorizationURL({ state, scopes, codeVerifier, redirectURI }) {
|
|
132
|
+
if (!options.clientId || !options.clientSecret) {
|
|
133
|
+
logger.error("Client Id and Secret are required for Atlassian");
|
|
134
|
+
throw new BetterAuthError("CLIENT_ID_AND_SECRET_REQUIRED");
|
|
135
|
+
}
|
|
136
|
+
if (!codeVerifier) {
|
|
137
|
+
throw new BetterAuthError("codeVerifier is required for Atlassian");
|
|
138
|
+
}
|
|
139
|
+
const _scopes = options.disableDefaultScope ? [] : ["read:jira-user", "offline_access"];
|
|
140
|
+
options.scope && _scopes.push(...options.scope);
|
|
141
|
+
scopes && _scopes.push(...scopes);
|
|
142
|
+
return createAuthorizationURL({
|
|
143
|
+
id: "atlassian",
|
|
144
|
+
options,
|
|
145
|
+
authorizationEndpoint: "https://auth.atlassian.com/authorize",
|
|
146
|
+
scopes: _scopes,
|
|
147
|
+
state,
|
|
148
|
+
codeVerifier,
|
|
149
|
+
redirectURI,
|
|
150
|
+
additionalParams: {
|
|
151
|
+
audience: "api.atlassian.com"
|
|
152
|
+
},
|
|
153
|
+
prompt: options.prompt
|
|
154
|
+
});
|
|
155
|
+
},
|
|
156
|
+
validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
|
|
157
|
+
return validateAuthorizationCode({
|
|
158
|
+
code,
|
|
159
|
+
codeVerifier,
|
|
160
|
+
redirectURI,
|
|
161
|
+
options,
|
|
162
|
+
tokenEndpoint: "https://auth.atlassian.com/oauth/token"
|
|
163
|
+
});
|
|
164
|
+
},
|
|
165
|
+
refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => {
|
|
166
|
+
return refreshAccessToken({
|
|
167
|
+
refreshToken,
|
|
168
|
+
options: {
|
|
169
|
+
clientId: options.clientId,
|
|
170
|
+
clientSecret: options.clientSecret
|
|
171
|
+
},
|
|
172
|
+
tokenEndpoint: "https://auth.atlassian.com/oauth/token"
|
|
173
|
+
});
|
|
174
|
+
},
|
|
175
|
+
async getUserInfo(token) {
|
|
176
|
+
if (options.getUserInfo) {
|
|
177
|
+
return options.getUserInfo(token);
|
|
178
|
+
}
|
|
179
|
+
if (!token.accessToken) {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
const { data: profile } = await betterFetch("https://api.atlassian.com/me", {
|
|
184
|
+
headers: { Authorization: `Bearer ${token.accessToken}` }
|
|
185
|
+
});
|
|
186
|
+
if (!profile) return null;
|
|
187
|
+
const userMap = await options.mapProfileToUser?.(profile);
|
|
188
|
+
return {
|
|
189
|
+
user: {
|
|
190
|
+
id: profile.account_id,
|
|
191
|
+
name: profile.name,
|
|
192
|
+
email: profile.email,
|
|
193
|
+
image: profile.picture,
|
|
194
|
+
emailVerified: false,
|
|
195
|
+
...userMap
|
|
196
|
+
},
|
|
197
|
+
data: profile
|
|
198
|
+
};
|
|
199
|
+
} catch (error) {
|
|
200
|
+
logger.error("Failed to fetch user info from Figma:", error);
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
options
|
|
205
|
+
};
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const cognito = (options) => {
|
|
209
|
+
if (!options.domain || !options.region || !options.userPoolId) {
|
|
210
|
+
logger.error(
|
|
211
|
+
"Domain, region and userPoolId are required for Amazon Cognito. Make sure to provide them in the options."
|
|
212
|
+
);
|
|
213
|
+
throw new BetterAuthError("DOMAIN_AND_REGION_REQUIRED");
|
|
214
|
+
}
|
|
215
|
+
const cleanDomain = options.domain.replace(/^https?:\/\//, "");
|
|
216
|
+
const authorizationEndpoint = `https://${cleanDomain}/oauth2/authorize`;
|
|
217
|
+
const tokenEndpoint = `https://${cleanDomain}/oauth2/token`;
|
|
218
|
+
const userInfoEndpoint = `https://${cleanDomain}/oauth2/userinfo`;
|
|
219
|
+
return {
|
|
220
|
+
id: "cognito",
|
|
221
|
+
name: "Cognito",
|
|
222
|
+
async createAuthorizationURL({ state, scopes, codeVerifier, redirectURI }) {
|
|
223
|
+
if (!options.clientId) {
|
|
224
|
+
logger.error(
|
|
225
|
+
"ClientId is required for Amazon Cognito. Make sure to provide them in the options."
|
|
226
|
+
);
|
|
227
|
+
throw new BetterAuthError("CLIENT_ID_AND_SECRET_REQUIRED");
|
|
228
|
+
}
|
|
229
|
+
if (options.requireClientSecret && !options.clientSecret) {
|
|
230
|
+
logger.error(
|
|
231
|
+
"Client Secret is required when requireClientSecret is true. Make sure to provide it in the options."
|
|
232
|
+
);
|
|
233
|
+
throw new BetterAuthError("CLIENT_SECRET_REQUIRED");
|
|
234
|
+
}
|
|
235
|
+
const _scopes = options.disableDefaultScope ? [] : ["openid", "profile", "email"];
|
|
236
|
+
options.scope && _scopes.push(...options.scope);
|
|
237
|
+
scopes && _scopes.push(...scopes);
|
|
238
|
+
const url = await createAuthorizationURL({
|
|
239
|
+
id: "cognito",
|
|
240
|
+
options: {
|
|
241
|
+
...options
|
|
242
|
+
},
|
|
243
|
+
authorizationEndpoint,
|
|
244
|
+
scopes: _scopes,
|
|
245
|
+
state,
|
|
246
|
+
codeVerifier,
|
|
247
|
+
redirectURI,
|
|
248
|
+
prompt: options.prompt
|
|
249
|
+
});
|
|
250
|
+
return url;
|
|
251
|
+
},
|
|
252
|
+
validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
|
|
253
|
+
return validateAuthorizationCode({
|
|
254
|
+
code,
|
|
255
|
+
codeVerifier,
|
|
256
|
+
redirectURI,
|
|
257
|
+
options,
|
|
258
|
+
tokenEndpoint
|
|
259
|
+
});
|
|
260
|
+
},
|
|
261
|
+
refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => {
|
|
262
|
+
return refreshAccessToken({
|
|
263
|
+
refreshToken,
|
|
264
|
+
options: {
|
|
265
|
+
clientId: options.clientId,
|
|
266
|
+
clientKey: options.clientKey,
|
|
267
|
+
clientSecret: options.clientSecret
|
|
268
|
+
},
|
|
269
|
+
tokenEndpoint
|
|
270
|
+
});
|
|
271
|
+
},
|
|
272
|
+
async verifyIdToken(token, nonce) {
|
|
273
|
+
if (options.disableIdTokenSignIn) {
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
if (options.verifyIdToken) {
|
|
277
|
+
return options.verifyIdToken(token, nonce);
|
|
278
|
+
}
|
|
279
|
+
try {
|
|
280
|
+
const decodedHeader = decodeProtectedHeader(token);
|
|
281
|
+
const { kid, alg: jwtAlg } = decodedHeader;
|
|
282
|
+
if (!kid || !jwtAlg) return false;
|
|
283
|
+
const publicKey = await getCognitoPublicKey(
|
|
284
|
+
kid,
|
|
285
|
+
options.region,
|
|
286
|
+
options.userPoolId
|
|
287
|
+
);
|
|
288
|
+
const expectedIssuer = `https://cognito-idp.${options.region}.amazonaws.com/${options.userPoolId}`;
|
|
289
|
+
const { payload: jwtClaims } = await jwtVerify(token, publicKey, {
|
|
290
|
+
algorithms: [jwtAlg],
|
|
291
|
+
issuer: expectedIssuer,
|
|
292
|
+
audience: options.clientId,
|
|
293
|
+
maxTokenAge: "1h"
|
|
294
|
+
});
|
|
295
|
+
if (nonce && jwtClaims.nonce !== nonce) {
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
return true;
|
|
299
|
+
} catch (error) {
|
|
300
|
+
logger.error("Failed to verify ID token:", error);
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
},
|
|
304
|
+
async getUserInfo(token) {
|
|
305
|
+
if (options.getUserInfo) {
|
|
306
|
+
return options.getUserInfo(token);
|
|
307
|
+
}
|
|
308
|
+
if (token.idToken) {
|
|
309
|
+
try {
|
|
310
|
+
const profile = decodeJwt(token.idToken);
|
|
311
|
+
if (!profile) {
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
const name = profile.name || profile.given_name || profile.username || profile.email;
|
|
315
|
+
const enrichedProfile = {
|
|
316
|
+
...profile,
|
|
317
|
+
name
|
|
318
|
+
};
|
|
319
|
+
const userMap = await options.mapProfileToUser?.(enrichedProfile);
|
|
320
|
+
return {
|
|
321
|
+
user: {
|
|
322
|
+
id: profile.sub,
|
|
323
|
+
name: enrichedProfile.name,
|
|
324
|
+
email: profile.email,
|
|
325
|
+
image: profile.picture,
|
|
326
|
+
emailVerified: profile.email_verified,
|
|
327
|
+
...userMap
|
|
328
|
+
},
|
|
329
|
+
data: enrichedProfile
|
|
330
|
+
};
|
|
331
|
+
} catch (error) {
|
|
332
|
+
logger.error("Failed to decode ID token:", error);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
if (token.accessToken) {
|
|
336
|
+
try {
|
|
337
|
+
const { data: userInfo } = await betterFetch(
|
|
338
|
+
userInfoEndpoint,
|
|
339
|
+
{
|
|
340
|
+
headers: {
|
|
341
|
+
Authorization: `Bearer ${token.accessToken}`
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
);
|
|
345
|
+
if (userInfo) {
|
|
346
|
+
const userMap = await options.mapProfileToUser?.(userInfo);
|
|
347
|
+
return {
|
|
348
|
+
user: {
|
|
349
|
+
id: userInfo.sub,
|
|
350
|
+
name: userInfo.name || userInfo.given_name || userInfo.username,
|
|
351
|
+
email: userInfo.email,
|
|
352
|
+
image: userInfo.picture,
|
|
353
|
+
emailVerified: userInfo.email_verified,
|
|
354
|
+
...userMap
|
|
355
|
+
},
|
|
356
|
+
data: userInfo
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
} catch (error) {
|
|
360
|
+
logger.error("Failed to fetch user info from Cognito:", error);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return null;
|
|
364
|
+
},
|
|
365
|
+
options
|
|
366
|
+
};
|
|
367
|
+
};
|
|
368
|
+
const getCognitoPublicKey = async (kid, region, userPoolId) => {
|
|
369
|
+
const COGNITO_JWKS_URI = `https://cognito-idp.${region}.amazonaws.com/${userPoolId}/.well-known/jwks.json`;
|
|
370
|
+
try {
|
|
371
|
+
const { data } = await betterFetch(COGNITO_JWKS_URI);
|
|
372
|
+
if (!data?.keys) {
|
|
373
|
+
throw new APIError("BAD_REQUEST", {
|
|
374
|
+
message: "Keys not found"
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
const jwk = data.keys.find((key) => key.kid === kid);
|
|
378
|
+
if (!jwk) {
|
|
379
|
+
throw new Error(`JWK with kid ${kid} not found`);
|
|
380
|
+
}
|
|
381
|
+
return await importJWK(jwk, jwk.alg);
|
|
382
|
+
} catch (error) {
|
|
383
|
+
logger.error("Failed to fetch Cognito public key:", error);
|
|
384
|
+
throw error;
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
const discord = (options) => {
|
|
389
|
+
return {
|
|
390
|
+
id: "discord",
|
|
391
|
+
name: "Discord",
|
|
392
|
+
createAuthorizationURL({ state, scopes, redirectURI }) {
|
|
393
|
+
const _scopes = options.disableDefaultScope ? [] : ["identify", "email"];
|
|
394
|
+
scopes && _scopes.push(...scopes);
|
|
395
|
+
options.scope && _scopes.push(...options.scope);
|
|
396
|
+
const hasBotScope = _scopes.includes("bot");
|
|
397
|
+
const permissionsParam = hasBotScope && options.permissions !== void 0 ? `&permissions=${options.permissions}` : "";
|
|
398
|
+
return new URL(
|
|
399
|
+
`https://discord.com/api/oauth2/authorize?scope=${_scopes.join(
|
|
400
|
+
"+"
|
|
401
|
+
)}&response_type=code&client_id=${options.clientId}&redirect_uri=${encodeURIComponent(
|
|
402
|
+
options.redirectURI || redirectURI
|
|
403
|
+
)}&state=${state}&prompt=${options.prompt || "none"}${permissionsParam}`
|
|
404
|
+
);
|
|
405
|
+
},
|
|
406
|
+
validateAuthorizationCode: async ({ code, redirectURI }) => {
|
|
407
|
+
return validateAuthorizationCode({
|
|
408
|
+
code,
|
|
409
|
+
redirectURI,
|
|
410
|
+
options,
|
|
411
|
+
tokenEndpoint: "https://discord.com/api/oauth2/token"
|
|
412
|
+
});
|
|
413
|
+
},
|
|
414
|
+
refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => {
|
|
415
|
+
return refreshAccessToken({
|
|
416
|
+
refreshToken,
|
|
417
|
+
options: {
|
|
418
|
+
clientId: options.clientId,
|
|
419
|
+
clientKey: options.clientKey,
|
|
420
|
+
clientSecret: options.clientSecret
|
|
421
|
+
},
|
|
422
|
+
tokenEndpoint: "https://discord.com/api/oauth2/token"
|
|
423
|
+
});
|
|
424
|
+
},
|
|
425
|
+
async getUserInfo(token) {
|
|
426
|
+
if (options.getUserInfo) {
|
|
427
|
+
return options.getUserInfo(token);
|
|
428
|
+
}
|
|
429
|
+
const { data: profile, error } = await betterFetch(
|
|
430
|
+
"https://discord.com/api/users/@me",
|
|
431
|
+
{
|
|
432
|
+
headers: {
|
|
433
|
+
authorization: `Bearer ${token.accessToken}`
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
);
|
|
437
|
+
if (error) {
|
|
438
|
+
return null;
|
|
439
|
+
}
|
|
440
|
+
if (profile.avatar === null) {
|
|
441
|
+
const defaultAvatarNumber = profile.discriminator === "0" ? Number(BigInt(profile.id) >> BigInt(22)) % 6 : parseInt(profile.discriminator) % 5;
|
|
442
|
+
profile.image_url = `https://cdn.discordapp.com/embed/avatars/${defaultAvatarNumber}.png`;
|
|
443
|
+
} else {
|
|
444
|
+
const format = profile.avatar.startsWith("a_") ? "gif" : "png";
|
|
445
|
+
profile.image_url = `https://cdn.discordapp.com/avatars/${profile.id}/${profile.avatar}.${format}`;
|
|
446
|
+
}
|
|
447
|
+
const userMap = await options.mapProfileToUser?.(profile);
|
|
448
|
+
return {
|
|
449
|
+
user: {
|
|
450
|
+
id: profile.id,
|
|
451
|
+
name: profile.global_name || profile.username || "",
|
|
452
|
+
email: profile.email,
|
|
453
|
+
emailVerified: profile.verified,
|
|
454
|
+
image: profile.image_url,
|
|
455
|
+
...userMap
|
|
456
|
+
},
|
|
457
|
+
data: profile
|
|
458
|
+
};
|
|
459
|
+
},
|
|
460
|
+
options
|
|
461
|
+
};
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
const facebook = (options) => {
|
|
465
|
+
return {
|
|
466
|
+
id: "facebook",
|
|
467
|
+
name: "Facebook",
|
|
468
|
+
async createAuthorizationURL({ state, scopes, redirectURI, loginHint }) {
|
|
469
|
+
const _scopes = options.disableDefaultScope ? [] : ["email", "public_profile"];
|
|
470
|
+
options.scope && _scopes.push(...options.scope);
|
|
471
|
+
scopes && _scopes.push(...scopes);
|
|
472
|
+
return await createAuthorizationURL({
|
|
473
|
+
id: "facebook",
|
|
474
|
+
options,
|
|
475
|
+
authorizationEndpoint: "https://www.facebook.com/v21.0/dialog/oauth",
|
|
476
|
+
scopes: _scopes,
|
|
477
|
+
state,
|
|
478
|
+
redirectURI,
|
|
479
|
+
loginHint,
|
|
480
|
+
additionalParams: options.configId ? {
|
|
481
|
+
config_id: options.configId
|
|
482
|
+
} : {}
|
|
483
|
+
});
|
|
484
|
+
},
|
|
485
|
+
validateAuthorizationCode: async ({ code, redirectURI }) => {
|
|
486
|
+
return validateAuthorizationCode({
|
|
487
|
+
code,
|
|
488
|
+
redirectURI,
|
|
489
|
+
options,
|
|
490
|
+
tokenEndpoint: "https://graph.facebook.com/oauth/access_token"
|
|
491
|
+
});
|
|
492
|
+
},
|
|
493
|
+
async verifyIdToken(token, nonce) {
|
|
494
|
+
if (options.disableIdTokenSignIn) {
|
|
495
|
+
return false;
|
|
496
|
+
}
|
|
497
|
+
if (options.verifyIdToken) {
|
|
498
|
+
return options.verifyIdToken(token, nonce);
|
|
499
|
+
}
|
|
500
|
+
if (token.split(".").length === 3) {
|
|
501
|
+
try {
|
|
502
|
+
const { payload: jwtClaims } = await jwtVerify(
|
|
503
|
+
token,
|
|
504
|
+
createRemoteJWKSet(
|
|
505
|
+
// https://developers.facebook.com/docs/facebook-login/limited-login/token/#jwks
|
|
506
|
+
new URL(
|
|
507
|
+
"https://limited.facebook.com/.well-known/oauth/openid/jwks/"
|
|
508
|
+
)
|
|
509
|
+
),
|
|
510
|
+
{
|
|
511
|
+
algorithms: ["RS256"],
|
|
512
|
+
audience: options.clientId,
|
|
513
|
+
issuer: "https://www.facebook.com"
|
|
514
|
+
}
|
|
515
|
+
);
|
|
516
|
+
if (nonce && jwtClaims.nonce !== nonce) {
|
|
517
|
+
return false;
|
|
518
|
+
}
|
|
519
|
+
return !!jwtClaims;
|
|
520
|
+
} catch (error) {
|
|
521
|
+
return false;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return true;
|
|
525
|
+
},
|
|
526
|
+
refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => {
|
|
527
|
+
return refreshAccessToken({
|
|
528
|
+
refreshToken,
|
|
529
|
+
options: {
|
|
530
|
+
clientId: options.clientId,
|
|
531
|
+
clientKey: options.clientKey,
|
|
532
|
+
clientSecret: options.clientSecret
|
|
533
|
+
},
|
|
534
|
+
tokenEndpoint: "https://graph.facebook.com/v18.0/oauth/access_token"
|
|
535
|
+
});
|
|
536
|
+
},
|
|
537
|
+
async getUserInfo(token) {
|
|
538
|
+
if (options.getUserInfo) {
|
|
539
|
+
return options.getUserInfo(token);
|
|
540
|
+
}
|
|
541
|
+
if (token.idToken && token.idToken.split(".").length === 3) {
|
|
542
|
+
const profile2 = decodeJwt(token.idToken);
|
|
543
|
+
const user = {
|
|
544
|
+
id: profile2.sub,
|
|
545
|
+
name: profile2.name,
|
|
546
|
+
email: profile2.email,
|
|
547
|
+
picture: {
|
|
548
|
+
data: {
|
|
549
|
+
url: profile2.picture,
|
|
550
|
+
height: 100,
|
|
551
|
+
width: 100,
|
|
552
|
+
is_silhouette: false
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
const userMap2 = await options.mapProfileToUser?.({
|
|
557
|
+
...user,
|
|
558
|
+
email_verified: true
|
|
559
|
+
});
|
|
560
|
+
return {
|
|
561
|
+
user: {
|
|
562
|
+
...user,
|
|
563
|
+
emailVerified: true,
|
|
564
|
+
...userMap2
|
|
565
|
+
},
|
|
566
|
+
data: profile2
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
const fields = [
|
|
570
|
+
"id",
|
|
571
|
+
"name",
|
|
572
|
+
"email",
|
|
573
|
+
"picture",
|
|
574
|
+
...options?.fields || []
|
|
575
|
+
];
|
|
576
|
+
const { data: profile, error } = await betterFetch(
|
|
577
|
+
"https://graph.facebook.com/me?fields=" + fields.join(","),
|
|
578
|
+
{
|
|
579
|
+
auth: {
|
|
580
|
+
type: "Bearer",
|
|
581
|
+
token: token.accessToken
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
);
|
|
585
|
+
if (error) {
|
|
586
|
+
return null;
|
|
587
|
+
}
|
|
588
|
+
const userMap = await options.mapProfileToUser?.(profile);
|
|
589
|
+
return {
|
|
590
|
+
user: {
|
|
591
|
+
id: profile.id,
|
|
592
|
+
name: profile.name,
|
|
593
|
+
email: profile.email,
|
|
594
|
+
image: profile.picture.data.url,
|
|
595
|
+
emailVerified: profile.email_verified,
|
|
596
|
+
...userMap
|
|
597
|
+
},
|
|
598
|
+
data: profile
|
|
599
|
+
};
|
|
600
|
+
},
|
|
601
|
+
options
|
|
602
|
+
};
|
|
603
|
+
};
|
|
604
|
+
|
|
605
|
+
const figma = (options) => {
|
|
606
|
+
return {
|
|
607
|
+
id: "figma",
|
|
608
|
+
name: "Figma",
|
|
609
|
+
async createAuthorizationURL({ state, scopes, codeVerifier, redirectURI }) {
|
|
610
|
+
if (!options.clientId || !options.clientSecret) {
|
|
611
|
+
logger.error(
|
|
612
|
+
"Client Id and Client Secret are required for Figma. Make sure to provide them in the options."
|
|
613
|
+
);
|
|
614
|
+
throw new BetterAuthError("CLIENT_ID_AND_SECRET_REQUIRED");
|
|
615
|
+
}
|
|
616
|
+
if (!codeVerifier) {
|
|
617
|
+
throw new BetterAuthError("codeVerifier is required for Figma");
|
|
618
|
+
}
|
|
619
|
+
const _scopes = options.disableDefaultScope ? [] : ["file_read"];
|
|
620
|
+
options.scope && _scopes.push(...options.scope);
|
|
621
|
+
scopes && _scopes.push(...scopes);
|
|
622
|
+
const url = await createAuthorizationURL({
|
|
623
|
+
id: "figma",
|
|
624
|
+
options,
|
|
625
|
+
authorizationEndpoint: "https://www.figma.com/oauth",
|
|
626
|
+
scopes: _scopes,
|
|
627
|
+
state,
|
|
628
|
+
codeVerifier,
|
|
629
|
+
redirectURI
|
|
630
|
+
});
|
|
631
|
+
return url;
|
|
632
|
+
},
|
|
633
|
+
validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
|
|
634
|
+
return validateAuthorizationCode({
|
|
635
|
+
code,
|
|
636
|
+
codeVerifier,
|
|
637
|
+
redirectURI,
|
|
638
|
+
options,
|
|
639
|
+
tokenEndpoint: "https://www.figma.com/api/oauth/token"
|
|
640
|
+
});
|
|
641
|
+
},
|
|
642
|
+
refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => {
|
|
643
|
+
return refreshAccessToken({
|
|
644
|
+
refreshToken,
|
|
645
|
+
options: {
|
|
646
|
+
clientId: options.clientId,
|
|
647
|
+
clientKey: options.clientKey,
|
|
648
|
+
clientSecret: options.clientSecret
|
|
649
|
+
},
|
|
650
|
+
tokenEndpoint: "https://www.figma.com/api/oauth/token"
|
|
651
|
+
});
|
|
652
|
+
},
|
|
653
|
+
async getUserInfo(token) {
|
|
654
|
+
if (options.getUserInfo) {
|
|
655
|
+
return options.getUserInfo(token);
|
|
656
|
+
}
|
|
657
|
+
try {
|
|
658
|
+
const { data: profile } = await betterFetch(
|
|
659
|
+
"https://api.figma.com/v1/me",
|
|
660
|
+
{
|
|
661
|
+
headers: {
|
|
662
|
+
Authorization: `Bearer ${token.accessToken}`
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
);
|
|
666
|
+
if (!profile) {
|
|
667
|
+
logger.error("Failed to fetch user from Figma");
|
|
668
|
+
return null;
|
|
669
|
+
}
|
|
670
|
+
const userMap = await options.mapProfileToUser?.(profile);
|
|
671
|
+
return {
|
|
672
|
+
user: {
|
|
673
|
+
id: profile.id,
|
|
674
|
+
name: profile.handle,
|
|
675
|
+
email: profile.email,
|
|
676
|
+
image: profile.img_url,
|
|
677
|
+
emailVerified: !!profile.email,
|
|
678
|
+
...userMap
|
|
679
|
+
},
|
|
680
|
+
data: profile
|
|
681
|
+
};
|
|
682
|
+
} catch (error) {
|
|
683
|
+
logger.error("Failed to fetch user info from Figma:", error);
|
|
684
|
+
return null;
|
|
685
|
+
}
|
|
686
|
+
},
|
|
687
|
+
options
|
|
688
|
+
};
|
|
689
|
+
};
|
|
690
|
+
|
|
691
|
+
const github = (options) => {
|
|
692
|
+
const tokenEndpoint = "https://github.com/login/oauth/access_token";
|
|
693
|
+
return {
|
|
694
|
+
id: "github",
|
|
695
|
+
name: "GitHub",
|
|
696
|
+
createAuthorizationURL({ state, scopes, loginHint, redirectURI }) {
|
|
697
|
+
const _scopes = options.disableDefaultScope ? [] : ["read:user", "user:email"];
|
|
698
|
+
options.scope && _scopes.push(...options.scope);
|
|
699
|
+
scopes && _scopes.push(...scopes);
|
|
700
|
+
return createAuthorizationURL({
|
|
701
|
+
id: "github",
|
|
702
|
+
options,
|
|
703
|
+
authorizationEndpoint: "https://github.com/login/oauth/authorize",
|
|
704
|
+
scopes: _scopes,
|
|
705
|
+
state,
|
|
706
|
+
redirectURI,
|
|
707
|
+
loginHint,
|
|
708
|
+
prompt: options.prompt
|
|
709
|
+
});
|
|
710
|
+
},
|
|
711
|
+
validateAuthorizationCode: async ({ code, redirectURI }) => {
|
|
712
|
+
return validateAuthorizationCode({
|
|
713
|
+
code,
|
|
714
|
+
redirectURI,
|
|
715
|
+
options,
|
|
716
|
+
tokenEndpoint
|
|
717
|
+
});
|
|
718
|
+
},
|
|
719
|
+
refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => {
|
|
720
|
+
return refreshAccessToken({
|
|
721
|
+
refreshToken,
|
|
722
|
+
options: {
|
|
723
|
+
clientId: options.clientId,
|
|
724
|
+
clientKey: options.clientKey,
|
|
725
|
+
clientSecret: options.clientSecret
|
|
726
|
+
},
|
|
727
|
+
tokenEndpoint: "https://github.com/login/oauth/access_token"
|
|
728
|
+
});
|
|
729
|
+
},
|
|
730
|
+
async getUserInfo(token) {
|
|
731
|
+
if (options.getUserInfo) {
|
|
732
|
+
return options.getUserInfo(token);
|
|
733
|
+
}
|
|
734
|
+
const { data: profile, error } = await betterFetch(
|
|
735
|
+
"https://api.github.com/user",
|
|
736
|
+
{
|
|
737
|
+
headers: {
|
|
738
|
+
"User-Agent": "better-auth",
|
|
739
|
+
authorization: `Bearer ${token.accessToken}`
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
);
|
|
743
|
+
if (error) {
|
|
744
|
+
return null;
|
|
745
|
+
}
|
|
746
|
+
const { data: emails } = await betterFetch("https://api.github.com/user/emails", {
|
|
747
|
+
headers: {
|
|
748
|
+
Authorization: `Bearer ${token.accessToken}`,
|
|
749
|
+
"User-Agent": "better-auth"
|
|
750
|
+
}
|
|
751
|
+
});
|
|
752
|
+
if (!profile.email && emails) {
|
|
753
|
+
profile.email = (emails.find((e) => e.primary) ?? emails[0])?.email;
|
|
754
|
+
}
|
|
755
|
+
const emailVerified = emails?.find((e) => e.email === profile.email)?.verified ?? false;
|
|
756
|
+
const userMap = await options.mapProfileToUser?.(profile);
|
|
757
|
+
return {
|
|
758
|
+
user: {
|
|
759
|
+
id: profile.id,
|
|
760
|
+
name: profile.name || profile.login,
|
|
761
|
+
email: profile.email,
|
|
762
|
+
image: profile.avatar_url,
|
|
763
|
+
emailVerified,
|
|
764
|
+
...userMap
|
|
765
|
+
},
|
|
766
|
+
data: profile
|
|
767
|
+
};
|
|
768
|
+
},
|
|
769
|
+
options
|
|
770
|
+
};
|
|
771
|
+
};
|
|
772
|
+
|
|
773
|
+
const google = (options) => {
|
|
774
|
+
return {
|
|
775
|
+
id: "google",
|
|
776
|
+
name: "Google",
|
|
777
|
+
async createAuthorizationURL({
|
|
778
|
+
state,
|
|
779
|
+
scopes,
|
|
780
|
+
codeVerifier,
|
|
781
|
+
redirectURI,
|
|
782
|
+
loginHint,
|
|
783
|
+
display
|
|
784
|
+
}) {
|
|
785
|
+
if (!options.clientId || !options.clientSecret) {
|
|
786
|
+
logger.error(
|
|
787
|
+
"Client Id and Client Secret is required for Google. Make sure to provide them in the options."
|
|
788
|
+
);
|
|
789
|
+
throw new BetterAuthError("CLIENT_ID_AND_SECRET_REQUIRED");
|
|
790
|
+
}
|
|
791
|
+
if (!codeVerifier) {
|
|
792
|
+
throw new BetterAuthError("codeVerifier is required for Google");
|
|
793
|
+
}
|
|
794
|
+
const _scopes = options.disableDefaultScope ? [] : ["email", "profile", "openid"];
|
|
795
|
+
options.scope && _scopes.push(...options.scope);
|
|
796
|
+
scopes && _scopes.push(...scopes);
|
|
797
|
+
const url = await createAuthorizationURL({
|
|
798
|
+
id: "google",
|
|
799
|
+
options,
|
|
800
|
+
authorizationEndpoint: "https://accounts.google.com/o/oauth2/auth",
|
|
801
|
+
scopes: _scopes,
|
|
802
|
+
state,
|
|
803
|
+
codeVerifier,
|
|
804
|
+
redirectURI,
|
|
805
|
+
prompt: options.prompt,
|
|
806
|
+
accessType: options.accessType,
|
|
807
|
+
display: display || options.display,
|
|
808
|
+
loginHint,
|
|
809
|
+
hd: options.hd,
|
|
810
|
+
additionalParams: {
|
|
811
|
+
include_granted_scopes: "true"
|
|
812
|
+
}
|
|
813
|
+
});
|
|
814
|
+
return url;
|
|
815
|
+
},
|
|
816
|
+
validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
|
|
817
|
+
return validateAuthorizationCode({
|
|
818
|
+
code,
|
|
819
|
+
codeVerifier,
|
|
820
|
+
redirectURI,
|
|
821
|
+
options,
|
|
822
|
+
tokenEndpoint: "https://oauth2.googleapis.com/token"
|
|
823
|
+
});
|
|
824
|
+
},
|
|
825
|
+
refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => {
|
|
826
|
+
return refreshAccessToken({
|
|
827
|
+
refreshToken,
|
|
828
|
+
options: {
|
|
829
|
+
clientId: options.clientId,
|
|
830
|
+
clientKey: options.clientKey,
|
|
831
|
+
clientSecret: options.clientSecret
|
|
832
|
+
},
|
|
833
|
+
tokenEndpoint: "https://www.googleapis.com/oauth2/v4/token"
|
|
834
|
+
});
|
|
835
|
+
},
|
|
836
|
+
async verifyIdToken(token, nonce) {
|
|
837
|
+
if (options.disableIdTokenSignIn) {
|
|
838
|
+
return false;
|
|
839
|
+
}
|
|
840
|
+
if (options.verifyIdToken) {
|
|
841
|
+
return options.verifyIdToken(token, nonce);
|
|
842
|
+
}
|
|
843
|
+
const googlePublicKeyUrl = `https://www.googleapis.com/oauth2/v3/tokeninfo?id_token=${token}`;
|
|
844
|
+
const { data: tokenInfo } = await betterFetch(googlePublicKeyUrl);
|
|
845
|
+
if (!tokenInfo) {
|
|
846
|
+
return false;
|
|
847
|
+
}
|
|
848
|
+
const isValid = tokenInfo.aud === options.clientId && (tokenInfo.iss === "https://accounts.google.com" || tokenInfo.iss === "accounts.google.com");
|
|
849
|
+
return isValid;
|
|
850
|
+
},
|
|
851
|
+
async getUserInfo(token) {
|
|
852
|
+
if (options.getUserInfo) {
|
|
853
|
+
return options.getUserInfo(token);
|
|
854
|
+
}
|
|
855
|
+
if (!token.idToken) {
|
|
856
|
+
return null;
|
|
857
|
+
}
|
|
858
|
+
const user = decodeJwt(token.idToken);
|
|
859
|
+
const userMap = await options.mapProfileToUser?.(user);
|
|
860
|
+
return {
|
|
861
|
+
user: {
|
|
862
|
+
id: user.sub,
|
|
863
|
+
name: user.name,
|
|
864
|
+
email: user.email,
|
|
865
|
+
image: user.picture,
|
|
866
|
+
emailVerified: user.email_verified,
|
|
867
|
+
...userMap
|
|
868
|
+
},
|
|
869
|
+
data: user
|
|
870
|
+
};
|
|
871
|
+
},
|
|
872
|
+
options
|
|
873
|
+
};
|
|
874
|
+
};
|
|
875
|
+
|
|
876
|
+
const kick = (options) => {
|
|
877
|
+
return {
|
|
878
|
+
id: "kick",
|
|
879
|
+
name: "Kick",
|
|
880
|
+
createAuthorizationURL({ state, scopes, redirectURI, codeVerifier }) {
|
|
881
|
+
const _scopes = options.disableDefaultScope ? [] : ["user:read"];
|
|
882
|
+
options.scope && _scopes.push(...options.scope);
|
|
883
|
+
scopes && _scopes.push(...scopes);
|
|
884
|
+
return createAuthorizationURL({
|
|
885
|
+
id: "kick",
|
|
886
|
+
redirectURI,
|
|
887
|
+
options,
|
|
888
|
+
authorizationEndpoint: "https://id.kick.com/oauth/authorize",
|
|
889
|
+
scopes: _scopes,
|
|
890
|
+
codeVerifier,
|
|
891
|
+
state
|
|
892
|
+
});
|
|
893
|
+
},
|
|
894
|
+
async validateAuthorizationCode({ code, redirectURI, codeVerifier }) {
|
|
895
|
+
return validateAuthorizationCode({
|
|
896
|
+
code,
|
|
897
|
+
redirectURI,
|
|
898
|
+
options,
|
|
899
|
+
tokenEndpoint: "https://id.kick.com/oauth/token",
|
|
900
|
+
codeVerifier
|
|
901
|
+
});
|
|
902
|
+
},
|
|
903
|
+
async getUserInfo(token) {
|
|
904
|
+
if (options.getUserInfo) {
|
|
905
|
+
return options.getUserInfo(token);
|
|
906
|
+
}
|
|
907
|
+
const { data, error } = await betterFetch("https://api.kick.com/public/v1/users", {
|
|
908
|
+
method: "GET",
|
|
909
|
+
headers: {
|
|
910
|
+
Authorization: `Bearer ${token.accessToken}`
|
|
911
|
+
}
|
|
912
|
+
});
|
|
913
|
+
if (error) {
|
|
914
|
+
return null;
|
|
915
|
+
}
|
|
916
|
+
const profile = data.data[0];
|
|
917
|
+
const userMap = await options.mapProfileToUser?.(profile);
|
|
918
|
+
return {
|
|
919
|
+
user: {
|
|
920
|
+
id: profile.user_id,
|
|
921
|
+
name: profile.name,
|
|
922
|
+
email: profile.email,
|
|
923
|
+
image: profile.profile_picture,
|
|
924
|
+
emailVerified: true,
|
|
925
|
+
...userMap
|
|
926
|
+
},
|
|
927
|
+
data: profile
|
|
928
|
+
};
|
|
929
|
+
},
|
|
930
|
+
options
|
|
931
|
+
};
|
|
932
|
+
};
|
|
933
|
+
|
|
934
|
+
const huggingface = (options) => {
|
|
935
|
+
return {
|
|
936
|
+
id: "huggingface",
|
|
937
|
+
name: "Hugging Face",
|
|
938
|
+
createAuthorizationURL({ state, scopes, codeVerifier, redirectURI }) {
|
|
939
|
+
const _scopes = options.disableDefaultScope ? [] : ["openid", "profile", "email"];
|
|
940
|
+
options.scope && _scopes.push(...options.scope);
|
|
941
|
+
scopes && _scopes.push(...scopes);
|
|
942
|
+
return createAuthorizationURL({
|
|
943
|
+
id: "huggingface",
|
|
944
|
+
options,
|
|
945
|
+
authorizationEndpoint: "https://huggingface.co/oauth/authorize",
|
|
946
|
+
scopes: _scopes,
|
|
947
|
+
state,
|
|
948
|
+
codeVerifier,
|
|
949
|
+
redirectURI
|
|
950
|
+
});
|
|
951
|
+
},
|
|
952
|
+
validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
|
|
953
|
+
return validateAuthorizationCode({
|
|
954
|
+
code,
|
|
955
|
+
codeVerifier,
|
|
956
|
+
redirectURI,
|
|
957
|
+
options,
|
|
958
|
+
tokenEndpoint: "https://huggingface.co/oauth/token"
|
|
959
|
+
});
|
|
960
|
+
},
|
|
961
|
+
refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => {
|
|
962
|
+
return refreshAccessToken({
|
|
963
|
+
refreshToken,
|
|
964
|
+
options: {
|
|
965
|
+
clientId: options.clientId,
|
|
966
|
+
clientKey: options.clientKey,
|
|
967
|
+
clientSecret: options.clientSecret
|
|
968
|
+
},
|
|
969
|
+
tokenEndpoint: "https://huggingface.co/oauth/token"
|
|
970
|
+
});
|
|
971
|
+
},
|
|
972
|
+
async getUserInfo(token) {
|
|
973
|
+
if (options.getUserInfo) {
|
|
974
|
+
return options.getUserInfo(token);
|
|
975
|
+
}
|
|
976
|
+
const { data: profile, error } = await betterFetch(
|
|
977
|
+
"https://huggingface.co/oauth/userinfo",
|
|
978
|
+
{
|
|
979
|
+
method: "GET",
|
|
980
|
+
headers: {
|
|
981
|
+
Authorization: `Bearer ${token.accessToken}`
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
);
|
|
985
|
+
if (error) {
|
|
986
|
+
return null;
|
|
987
|
+
}
|
|
988
|
+
const userMap = await options.mapProfileToUser?.(profile);
|
|
989
|
+
return {
|
|
990
|
+
user: {
|
|
991
|
+
id: profile.sub,
|
|
992
|
+
name: profile.name || profile.preferred_username,
|
|
993
|
+
email: profile.email,
|
|
994
|
+
image: profile.picture,
|
|
995
|
+
emailVerified: profile.email_verified ?? false,
|
|
996
|
+
...userMap
|
|
997
|
+
},
|
|
998
|
+
data: profile
|
|
999
|
+
};
|
|
1000
|
+
},
|
|
1001
|
+
options
|
|
1002
|
+
};
|
|
1003
|
+
};
|
|
1004
|
+
|
|
1005
|
+
const microsoft = (options) => {
|
|
1006
|
+
const tenant = options.tenantId || "common";
|
|
1007
|
+
const authority = options.authority || "https://login.microsoftonline.com";
|
|
1008
|
+
const authorizationEndpoint = `${authority}/${tenant}/oauth2/v2.0/authorize`;
|
|
1009
|
+
const tokenEndpoint = `${authority}/${tenant}/oauth2/v2.0/token`;
|
|
1010
|
+
return {
|
|
1011
|
+
id: "microsoft",
|
|
1012
|
+
name: "Microsoft EntraID",
|
|
1013
|
+
createAuthorizationURL(data) {
|
|
1014
|
+
const scopes = options.disableDefaultScope ? [] : ["openid", "profile", "email", "User.Read", "offline_access"];
|
|
1015
|
+
options.scope && scopes.push(...options.scope);
|
|
1016
|
+
data.scopes && scopes.push(...data.scopes);
|
|
1017
|
+
return createAuthorizationURL({
|
|
1018
|
+
id: "microsoft",
|
|
1019
|
+
options,
|
|
1020
|
+
authorizationEndpoint,
|
|
1021
|
+
state: data.state,
|
|
1022
|
+
codeVerifier: data.codeVerifier,
|
|
1023
|
+
scopes,
|
|
1024
|
+
redirectURI: data.redirectURI,
|
|
1025
|
+
prompt: options.prompt,
|
|
1026
|
+
loginHint: data.loginHint
|
|
1027
|
+
});
|
|
1028
|
+
},
|
|
1029
|
+
validateAuthorizationCode({ code, codeVerifier, redirectURI }) {
|
|
1030
|
+
return validateAuthorizationCode({
|
|
1031
|
+
code,
|
|
1032
|
+
codeVerifier,
|
|
1033
|
+
redirectURI,
|
|
1034
|
+
options,
|
|
1035
|
+
tokenEndpoint
|
|
1036
|
+
});
|
|
1037
|
+
},
|
|
1038
|
+
async getUserInfo(token) {
|
|
1039
|
+
if (options.getUserInfo) {
|
|
1040
|
+
return options.getUserInfo(token);
|
|
1041
|
+
}
|
|
1042
|
+
if (!token.idToken) {
|
|
1043
|
+
return null;
|
|
1044
|
+
}
|
|
1045
|
+
const user = decodeJwt(token.idToken);
|
|
1046
|
+
const profilePhotoSize = options.profilePhotoSize || 48;
|
|
1047
|
+
await betterFetch(
|
|
1048
|
+
`https://graph.microsoft.com/v1.0/me/photos/${profilePhotoSize}x${profilePhotoSize}/$value`,
|
|
1049
|
+
{
|
|
1050
|
+
headers: {
|
|
1051
|
+
Authorization: `Bearer ${token.accessToken}`
|
|
1052
|
+
},
|
|
1053
|
+
async onResponse(context) {
|
|
1054
|
+
if (options.disableProfilePhoto || !context.response.ok) {
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
try {
|
|
1058
|
+
const response = context.response.clone();
|
|
1059
|
+
const pictureBuffer = await response.arrayBuffer();
|
|
1060
|
+
const pictureBase64 = base64.encode(pictureBuffer);
|
|
1061
|
+
user.picture = `data:image/jpeg;base64, ${pictureBase64}`;
|
|
1062
|
+
} catch (e) {
|
|
1063
|
+
logger.error(
|
|
1064
|
+
e && typeof e === "object" && "name" in e ? e.name : "",
|
|
1065
|
+
e
|
|
1066
|
+
);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
);
|
|
1071
|
+
const userMap = await options.mapProfileToUser?.(user);
|
|
1072
|
+
return {
|
|
1073
|
+
user: {
|
|
1074
|
+
id: user.sub,
|
|
1075
|
+
name: user.name,
|
|
1076
|
+
email: user.email,
|
|
1077
|
+
image: user.picture,
|
|
1078
|
+
emailVerified: true,
|
|
1079
|
+
...userMap
|
|
1080
|
+
},
|
|
1081
|
+
data: user
|
|
1082
|
+
};
|
|
1083
|
+
},
|
|
1084
|
+
refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => {
|
|
1085
|
+
const scopes = options.disableDefaultScope ? [] : ["openid", "profile", "email", "User.Read", "offline_access"];
|
|
1086
|
+
options.scope && scopes.push(...options.scope);
|
|
1087
|
+
return refreshAccessToken({
|
|
1088
|
+
refreshToken,
|
|
1089
|
+
options: {
|
|
1090
|
+
clientId: options.clientId,
|
|
1091
|
+
clientSecret: options.clientSecret
|
|
1092
|
+
},
|
|
1093
|
+
extraParams: {
|
|
1094
|
+
scope: scopes.join(" ")
|
|
1095
|
+
// Include the scopes in request to microsoft
|
|
1096
|
+
},
|
|
1097
|
+
tokenEndpoint
|
|
1098
|
+
});
|
|
1099
|
+
},
|
|
1100
|
+
options
|
|
1101
|
+
};
|
|
1102
|
+
};
|
|
1103
|
+
|
|
1104
|
+
const slack = (options) => {
|
|
1105
|
+
return {
|
|
1106
|
+
id: "slack",
|
|
1107
|
+
name: "Slack",
|
|
1108
|
+
createAuthorizationURL({ state, scopes, redirectURI }) {
|
|
1109
|
+
const _scopes = options.disableDefaultScope ? [] : ["openid", "profile", "email"];
|
|
1110
|
+
scopes && _scopes.push(...scopes);
|
|
1111
|
+
options.scope && _scopes.push(...options.scope);
|
|
1112
|
+
const url = new URL("https://slack.com/openid/connect/authorize");
|
|
1113
|
+
url.searchParams.set("scope", _scopes.join(" "));
|
|
1114
|
+
url.searchParams.set("response_type", "code");
|
|
1115
|
+
url.searchParams.set("client_id", options.clientId);
|
|
1116
|
+
url.searchParams.set("redirect_uri", options.redirectURI || redirectURI);
|
|
1117
|
+
url.searchParams.set("state", state);
|
|
1118
|
+
return url;
|
|
1119
|
+
},
|
|
1120
|
+
validateAuthorizationCode: async ({ code, redirectURI }) => {
|
|
1121
|
+
return validateAuthorizationCode({
|
|
1122
|
+
code,
|
|
1123
|
+
redirectURI,
|
|
1124
|
+
options,
|
|
1125
|
+
tokenEndpoint: "https://slack.com/api/openid.connect.token"
|
|
1126
|
+
});
|
|
1127
|
+
},
|
|
1128
|
+
refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => {
|
|
1129
|
+
return refreshAccessToken({
|
|
1130
|
+
refreshToken,
|
|
1131
|
+
options: {
|
|
1132
|
+
clientId: options.clientId,
|
|
1133
|
+
clientKey: options.clientKey,
|
|
1134
|
+
clientSecret: options.clientSecret
|
|
1135
|
+
},
|
|
1136
|
+
tokenEndpoint: "https://slack.com/api/openid.connect.token"
|
|
1137
|
+
});
|
|
1138
|
+
},
|
|
1139
|
+
async getUserInfo(token) {
|
|
1140
|
+
if (options.getUserInfo) {
|
|
1141
|
+
return options.getUserInfo(token);
|
|
1142
|
+
}
|
|
1143
|
+
const { data: profile, error } = await betterFetch(
|
|
1144
|
+
"https://slack.com/api/openid.connect.userInfo",
|
|
1145
|
+
{
|
|
1146
|
+
headers: {
|
|
1147
|
+
authorization: `Bearer ${token.accessToken}`
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
);
|
|
1151
|
+
if (error) {
|
|
1152
|
+
return null;
|
|
1153
|
+
}
|
|
1154
|
+
const userMap = await options.mapProfileToUser?.(profile);
|
|
1155
|
+
return {
|
|
1156
|
+
user: {
|
|
1157
|
+
id: profile["https://slack.com/user_id"],
|
|
1158
|
+
name: profile.name || "",
|
|
1159
|
+
email: profile.email,
|
|
1160
|
+
emailVerified: profile.email_verified,
|
|
1161
|
+
image: profile.picture || profile["https://slack.com/user_image_512"],
|
|
1162
|
+
...userMap
|
|
1163
|
+
},
|
|
1164
|
+
data: profile
|
|
1165
|
+
};
|
|
1166
|
+
},
|
|
1167
|
+
options
|
|
1168
|
+
};
|
|
1169
|
+
};
|
|
1170
|
+
|
|
1171
|
+
const notion = (options) => {
|
|
1172
|
+
const tokenEndpoint = "https://api.notion.com/v1/oauth/token";
|
|
1173
|
+
return {
|
|
1174
|
+
id: "notion",
|
|
1175
|
+
name: "Notion",
|
|
1176
|
+
createAuthorizationURL({ state, scopes, loginHint, redirectURI }) {
|
|
1177
|
+
const _scopes = options.disableDefaultScope ? [] : [];
|
|
1178
|
+
options.scope && _scopes.push(...options.scope);
|
|
1179
|
+
scopes && _scopes.push(...scopes);
|
|
1180
|
+
return createAuthorizationURL({
|
|
1181
|
+
id: "notion",
|
|
1182
|
+
options,
|
|
1183
|
+
authorizationEndpoint: "https://api.notion.com/v1/oauth/authorize",
|
|
1184
|
+
scopes: _scopes,
|
|
1185
|
+
state,
|
|
1186
|
+
redirectURI,
|
|
1187
|
+
loginHint,
|
|
1188
|
+
additionalParams: {
|
|
1189
|
+
owner: "user"
|
|
1190
|
+
}
|
|
1191
|
+
});
|
|
1192
|
+
},
|
|
1193
|
+
validateAuthorizationCode: async ({ code, redirectURI }) => {
|
|
1194
|
+
return validateAuthorizationCode({
|
|
1195
|
+
code,
|
|
1196
|
+
redirectURI,
|
|
1197
|
+
options,
|
|
1198
|
+
tokenEndpoint,
|
|
1199
|
+
authentication: "basic"
|
|
1200
|
+
});
|
|
1201
|
+
},
|
|
1202
|
+
refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => {
|
|
1203
|
+
return refreshAccessToken({
|
|
1204
|
+
refreshToken,
|
|
1205
|
+
options: {
|
|
1206
|
+
clientId: options.clientId,
|
|
1207
|
+
clientKey: options.clientKey,
|
|
1208
|
+
clientSecret: options.clientSecret
|
|
1209
|
+
},
|
|
1210
|
+
tokenEndpoint
|
|
1211
|
+
});
|
|
1212
|
+
},
|
|
1213
|
+
async getUserInfo(token) {
|
|
1214
|
+
if (options.getUserInfo) {
|
|
1215
|
+
return options.getUserInfo(token);
|
|
1216
|
+
}
|
|
1217
|
+
const { data: profile, error } = await betterFetch("https://api.notion.com/v1/users/me", {
|
|
1218
|
+
headers: {
|
|
1219
|
+
Authorization: `Bearer ${token.accessToken}`,
|
|
1220
|
+
"Notion-Version": "2022-06-28"
|
|
1221
|
+
}
|
|
1222
|
+
});
|
|
1223
|
+
if (error || !profile) {
|
|
1224
|
+
return null;
|
|
1225
|
+
}
|
|
1226
|
+
const userProfile = profile.bot?.owner?.user;
|
|
1227
|
+
if (!userProfile) {
|
|
1228
|
+
return null;
|
|
1229
|
+
}
|
|
1230
|
+
const userMap = await options.mapProfileToUser?.(userProfile);
|
|
1231
|
+
return {
|
|
1232
|
+
user: {
|
|
1233
|
+
id: userProfile.id,
|
|
1234
|
+
name: userProfile.name || "Notion User",
|
|
1235
|
+
email: userProfile.person?.email || null,
|
|
1236
|
+
image: userProfile.avatar_url,
|
|
1237
|
+
emailVerified: !!userProfile.person?.email,
|
|
1238
|
+
...userMap
|
|
1239
|
+
},
|
|
1240
|
+
data: userProfile
|
|
1241
|
+
};
|
|
1242
|
+
},
|
|
1243
|
+
options
|
|
1244
|
+
};
|
|
1245
|
+
};
|
|
1246
|
+
|
|
1247
|
+
const spotify = (options) => {
|
|
1248
|
+
return {
|
|
1249
|
+
id: "spotify",
|
|
1250
|
+
name: "Spotify",
|
|
1251
|
+
createAuthorizationURL({ state, scopes, codeVerifier, redirectURI }) {
|
|
1252
|
+
const _scopes = options.disableDefaultScope ? [] : ["user-read-email"];
|
|
1253
|
+
options.scope && _scopes.push(...options.scope);
|
|
1254
|
+
scopes && _scopes.push(...scopes);
|
|
1255
|
+
return createAuthorizationURL({
|
|
1256
|
+
id: "spotify",
|
|
1257
|
+
options,
|
|
1258
|
+
authorizationEndpoint: "https://accounts.spotify.com/authorize",
|
|
1259
|
+
scopes: _scopes,
|
|
1260
|
+
state,
|
|
1261
|
+
codeVerifier,
|
|
1262
|
+
redirectURI
|
|
1263
|
+
});
|
|
1264
|
+
},
|
|
1265
|
+
validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
|
|
1266
|
+
return validateAuthorizationCode({
|
|
1267
|
+
code,
|
|
1268
|
+
codeVerifier,
|
|
1269
|
+
redirectURI,
|
|
1270
|
+
options,
|
|
1271
|
+
tokenEndpoint: "https://accounts.spotify.com/api/token"
|
|
1272
|
+
});
|
|
1273
|
+
},
|
|
1274
|
+
refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => {
|
|
1275
|
+
return refreshAccessToken({
|
|
1276
|
+
refreshToken,
|
|
1277
|
+
options: {
|
|
1278
|
+
clientId: options.clientId,
|
|
1279
|
+
clientKey: options.clientKey,
|
|
1280
|
+
clientSecret: options.clientSecret
|
|
1281
|
+
},
|
|
1282
|
+
tokenEndpoint: "https://accounts.spotify.com/api/token"
|
|
1283
|
+
});
|
|
1284
|
+
},
|
|
1285
|
+
async getUserInfo(token) {
|
|
1286
|
+
if (options.getUserInfo) {
|
|
1287
|
+
return options.getUserInfo(token);
|
|
1288
|
+
}
|
|
1289
|
+
const { data: profile, error } = await betterFetch(
|
|
1290
|
+
"https://api.spotify.com/v1/me",
|
|
1291
|
+
{
|
|
1292
|
+
method: "GET",
|
|
1293
|
+
headers: {
|
|
1294
|
+
Authorization: `Bearer ${token.accessToken}`
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
);
|
|
1298
|
+
if (error) {
|
|
1299
|
+
return null;
|
|
1300
|
+
}
|
|
1301
|
+
const userMap = await options.mapProfileToUser?.(profile);
|
|
1302
|
+
return {
|
|
1303
|
+
user: {
|
|
1304
|
+
id: profile.id,
|
|
1305
|
+
name: profile.display_name,
|
|
1306
|
+
email: profile.email,
|
|
1307
|
+
image: profile.images[0]?.url,
|
|
1308
|
+
emailVerified: false,
|
|
1309
|
+
...userMap
|
|
1310
|
+
},
|
|
1311
|
+
data: profile
|
|
1312
|
+
};
|
|
1313
|
+
},
|
|
1314
|
+
options
|
|
1315
|
+
};
|
|
1316
|
+
};
|
|
1317
|
+
|
|
1318
|
+
const twitch = (options) => {
|
|
1319
|
+
return {
|
|
1320
|
+
id: "twitch",
|
|
1321
|
+
name: "Twitch",
|
|
1322
|
+
createAuthorizationURL({ state, scopes, redirectURI }) {
|
|
1323
|
+
const _scopes = options.disableDefaultScope ? [] : ["user:read:email", "openid"];
|
|
1324
|
+
options.scope && _scopes.push(...options.scope);
|
|
1325
|
+
scopes && _scopes.push(...scopes);
|
|
1326
|
+
return createAuthorizationURL({
|
|
1327
|
+
id: "twitch",
|
|
1328
|
+
redirectURI,
|
|
1329
|
+
options,
|
|
1330
|
+
authorizationEndpoint: "https://id.twitch.tv/oauth2/authorize",
|
|
1331
|
+
scopes: _scopes,
|
|
1332
|
+
state,
|
|
1333
|
+
claims: options.claims || [
|
|
1334
|
+
"email",
|
|
1335
|
+
"email_verified",
|
|
1336
|
+
"preferred_username",
|
|
1337
|
+
"picture"
|
|
1338
|
+
]
|
|
1339
|
+
});
|
|
1340
|
+
},
|
|
1341
|
+
validateAuthorizationCode: async ({ code, redirectURI }) => {
|
|
1342
|
+
return validateAuthorizationCode({
|
|
1343
|
+
code,
|
|
1344
|
+
redirectURI,
|
|
1345
|
+
options,
|
|
1346
|
+
tokenEndpoint: "https://id.twitch.tv/oauth2/token"
|
|
1347
|
+
});
|
|
1348
|
+
},
|
|
1349
|
+
refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => {
|
|
1350
|
+
return refreshAccessToken({
|
|
1351
|
+
refreshToken,
|
|
1352
|
+
options: {
|
|
1353
|
+
clientId: options.clientId,
|
|
1354
|
+
clientKey: options.clientKey,
|
|
1355
|
+
clientSecret: options.clientSecret
|
|
1356
|
+
},
|
|
1357
|
+
tokenEndpoint: "https://id.twitch.tv/oauth2/token"
|
|
1358
|
+
});
|
|
1359
|
+
},
|
|
1360
|
+
async getUserInfo(token) {
|
|
1361
|
+
if (options.getUserInfo) {
|
|
1362
|
+
return options.getUserInfo(token);
|
|
1363
|
+
}
|
|
1364
|
+
const idToken = token.idToken;
|
|
1365
|
+
if (!idToken) {
|
|
1366
|
+
logger.error("No idToken found in token");
|
|
1367
|
+
return null;
|
|
1368
|
+
}
|
|
1369
|
+
const profile = decodeJwt(idToken);
|
|
1370
|
+
const userMap = await options.mapProfileToUser?.(profile);
|
|
1371
|
+
return {
|
|
1372
|
+
user: {
|
|
1373
|
+
id: profile.sub,
|
|
1374
|
+
name: profile.preferred_username,
|
|
1375
|
+
email: profile.email,
|
|
1376
|
+
image: profile.picture,
|
|
1377
|
+
emailVerified: profile.email_verified,
|
|
1378
|
+
...userMap
|
|
1379
|
+
},
|
|
1380
|
+
data: profile
|
|
1381
|
+
};
|
|
1382
|
+
},
|
|
1383
|
+
options
|
|
1384
|
+
};
|
|
1385
|
+
};
|
|
1386
|
+
|
|
1387
|
+
const twitter = (options) => {
|
|
1388
|
+
return {
|
|
1389
|
+
id: "twitter",
|
|
1390
|
+
name: "Twitter",
|
|
1391
|
+
createAuthorizationURL(data) {
|
|
1392
|
+
const _scopes = options.disableDefaultScope ? [] : ["users.read", "tweet.read", "offline.access", "users.email"];
|
|
1393
|
+
options.scope && _scopes.push(...options.scope);
|
|
1394
|
+
data.scopes && _scopes.push(...data.scopes);
|
|
1395
|
+
return createAuthorizationURL({
|
|
1396
|
+
id: "twitter",
|
|
1397
|
+
options,
|
|
1398
|
+
authorizationEndpoint: "https://x.com/i/oauth2/authorize",
|
|
1399
|
+
scopes: _scopes,
|
|
1400
|
+
state: data.state,
|
|
1401
|
+
codeVerifier: data.codeVerifier,
|
|
1402
|
+
redirectURI: data.redirectURI
|
|
1403
|
+
});
|
|
1404
|
+
},
|
|
1405
|
+
validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
|
|
1406
|
+
return validateAuthorizationCode({
|
|
1407
|
+
code,
|
|
1408
|
+
codeVerifier,
|
|
1409
|
+
authentication: "basic",
|
|
1410
|
+
redirectURI,
|
|
1411
|
+
options,
|
|
1412
|
+
tokenEndpoint: "https://api.x.com/2/oauth2/token"
|
|
1413
|
+
});
|
|
1414
|
+
},
|
|
1415
|
+
refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => {
|
|
1416
|
+
return refreshAccessToken({
|
|
1417
|
+
refreshToken,
|
|
1418
|
+
options: {
|
|
1419
|
+
clientId: options.clientId,
|
|
1420
|
+
clientKey: options.clientKey,
|
|
1421
|
+
clientSecret: options.clientSecret
|
|
1422
|
+
},
|
|
1423
|
+
authentication: "basic",
|
|
1424
|
+
tokenEndpoint: "https://api.x.com/2/oauth2/token"
|
|
1425
|
+
});
|
|
1426
|
+
},
|
|
1427
|
+
async getUserInfo(token) {
|
|
1428
|
+
if (options.getUserInfo) {
|
|
1429
|
+
return options.getUserInfo(token);
|
|
1430
|
+
}
|
|
1431
|
+
const { data: profile, error: profileError } = await betterFetch(
|
|
1432
|
+
"https://api.x.com/2/users/me?user.fields=profile_image_url",
|
|
1433
|
+
{
|
|
1434
|
+
method: "GET",
|
|
1435
|
+
headers: {
|
|
1436
|
+
Authorization: `Bearer ${token.accessToken}`
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
);
|
|
1440
|
+
if (profileError) {
|
|
1441
|
+
return null;
|
|
1442
|
+
}
|
|
1443
|
+
const { data: emailData, error: emailError } = await betterFetch("https://api.x.com/2/users/me?user.fields=confirmed_email", {
|
|
1444
|
+
method: "GET",
|
|
1445
|
+
headers: {
|
|
1446
|
+
Authorization: `Bearer ${token.accessToken}`
|
|
1447
|
+
}
|
|
1448
|
+
});
|
|
1449
|
+
let emailVerified = false;
|
|
1450
|
+
if (!emailError && emailData?.data?.confirmed_email) {
|
|
1451
|
+
profile.data.email = emailData.data.confirmed_email;
|
|
1452
|
+
emailVerified = true;
|
|
1453
|
+
}
|
|
1454
|
+
const userMap = await options.mapProfileToUser?.(profile);
|
|
1455
|
+
return {
|
|
1456
|
+
user: {
|
|
1457
|
+
id: profile.data.id,
|
|
1458
|
+
name: profile.data.name,
|
|
1459
|
+
email: profile.data.email || profile.data.username || null,
|
|
1460
|
+
image: profile.data.profile_image_url,
|
|
1461
|
+
emailVerified,
|
|
1462
|
+
...userMap
|
|
1463
|
+
},
|
|
1464
|
+
data: profile
|
|
1465
|
+
};
|
|
1466
|
+
},
|
|
1467
|
+
options
|
|
1468
|
+
};
|
|
1469
|
+
};
|
|
1470
|
+
|
|
1471
|
+
const dropbox = (options) => {
|
|
1472
|
+
const tokenEndpoint = "https://api.dropboxapi.com/oauth2/token";
|
|
1473
|
+
return {
|
|
1474
|
+
id: "dropbox",
|
|
1475
|
+
name: "Dropbox",
|
|
1476
|
+
createAuthorizationURL: async ({
|
|
1477
|
+
state,
|
|
1478
|
+
scopes,
|
|
1479
|
+
codeVerifier,
|
|
1480
|
+
redirectURI
|
|
1481
|
+
}) => {
|
|
1482
|
+
const _scopes = options.disableDefaultScope ? [] : ["account_info.read"];
|
|
1483
|
+
options.scope && _scopes.push(...options.scope);
|
|
1484
|
+
scopes && _scopes.push(...scopes);
|
|
1485
|
+
const additionalParams = {};
|
|
1486
|
+
if (options.accessType) {
|
|
1487
|
+
additionalParams.token_access_type = options.accessType;
|
|
1488
|
+
}
|
|
1489
|
+
return await createAuthorizationURL({
|
|
1490
|
+
id: "dropbox",
|
|
1491
|
+
options,
|
|
1492
|
+
authorizationEndpoint: "https://www.dropbox.com/oauth2/authorize",
|
|
1493
|
+
scopes: _scopes,
|
|
1494
|
+
state,
|
|
1495
|
+
redirectURI,
|
|
1496
|
+
codeVerifier,
|
|
1497
|
+
additionalParams
|
|
1498
|
+
});
|
|
1499
|
+
},
|
|
1500
|
+
validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
|
|
1501
|
+
return await validateAuthorizationCode({
|
|
1502
|
+
code,
|
|
1503
|
+
codeVerifier,
|
|
1504
|
+
redirectURI,
|
|
1505
|
+
options,
|
|
1506
|
+
tokenEndpoint
|
|
1507
|
+
});
|
|
1508
|
+
},
|
|
1509
|
+
refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => {
|
|
1510
|
+
return refreshAccessToken({
|
|
1511
|
+
refreshToken,
|
|
1512
|
+
options: {
|
|
1513
|
+
clientId: options.clientId,
|
|
1514
|
+
clientKey: options.clientKey,
|
|
1515
|
+
clientSecret: options.clientSecret
|
|
1516
|
+
},
|
|
1517
|
+
tokenEndpoint: "https://api.dropbox.com/oauth2/token"
|
|
1518
|
+
});
|
|
1519
|
+
},
|
|
1520
|
+
async getUserInfo(token) {
|
|
1521
|
+
if (options.getUserInfo) {
|
|
1522
|
+
return options.getUserInfo(token);
|
|
1523
|
+
}
|
|
1524
|
+
const { data: profile, error } = await betterFetch(
|
|
1525
|
+
"https://api.dropboxapi.com/2/users/get_current_account",
|
|
1526
|
+
{
|
|
1527
|
+
method: "POST",
|
|
1528
|
+
headers: {
|
|
1529
|
+
Authorization: `Bearer ${token.accessToken}`
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
);
|
|
1533
|
+
if (error) {
|
|
1534
|
+
return null;
|
|
1535
|
+
}
|
|
1536
|
+
const userMap = await options.mapProfileToUser?.(profile);
|
|
1537
|
+
return {
|
|
1538
|
+
user: {
|
|
1539
|
+
id: profile.account_id,
|
|
1540
|
+
name: profile.name?.display_name,
|
|
1541
|
+
email: profile.email,
|
|
1542
|
+
emailVerified: profile.email_verified || false,
|
|
1543
|
+
image: profile.profile_photo_url,
|
|
1544
|
+
...userMap
|
|
1545
|
+
},
|
|
1546
|
+
data: profile
|
|
1547
|
+
};
|
|
1548
|
+
},
|
|
1549
|
+
options
|
|
1550
|
+
};
|
|
1551
|
+
};
|
|
1552
|
+
|
|
1553
|
+
const linear = (options) => {
|
|
1554
|
+
const tokenEndpoint = "https://api.linear.app/oauth/token";
|
|
1555
|
+
return {
|
|
1556
|
+
id: "linear",
|
|
1557
|
+
name: "Linear",
|
|
1558
|
+
createAuthorizationURL({ state, scopes, loginHint, redirectURI }) {
|
|
1559
|
+
const _scopes = options.disableDefaultScope ? [] : ["read"];
|
|
1560
|
+
options.scope && _scopes.push(...options.scope);
|
|
1561
|
+
scopes && _scopes.push(...scopes);
|
|
1562
|
+
return createAuthorizationURL({
|
|
1563
|
+
id: "linear",
|
|
1564
|
+
options,
|
|
1565
|
+
authorizationEndpoint: "https://linear.app/oauth/authorize",
|
|
1566
|
+
scopes: _scopes,
|
|
1567
|
+
state,
|
|
1568
|
+
redirectURI,
|
|
1569
|
+
loginHint
|
|
1570
|
+
});
|
|
1571
|
+
},
|
|
1572
|
+
validateAuthorizationCode: async ({ code, redirectURI }) => {
|
|
1573
|
+
return validateAuthorizationCode({
|
|
1574
|
+
code,
|
|
1575
|
+
redirectURI,
|
|
1576
|
+
options,
|
|
1577
|
+
tokenEndpoint
|
|
1578
|
+
});
|
|
1579
|
+
},
|
|
1580
|
+
refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => {
|
|
1581
|
+
return refreshAccessToken({
|
|
1582
|
+
refreshToken,
|
|
1583
|
+
options: {
|
|
1584
|
+
clientId: options.clientId,
|
|
1585
|
+
clientKey: options.clientKey,
|
|
1586
|
+
clientSecret: options.clientSecret
|
|
1587
|
+
},
|
|
1588
|
+
tokenEndpoint
|
|
1589
|
+
});
|
|
1590
|
+
},
|
|
1591
|
+
async getUserInfo(token) {
|
|
1592
|
+
if (options.getUserInfo) {
|
|
1593
|
+
return options.getUserInfo(token);
|
|
1594
|
+
}
|
|
1595
|
+
const { data: profile, error } = await betterFetch(
|
|
1596
|
+
"https://api.linear.app/graphql",
|
|
1597
|
+
{
|
|
1598
|
+
method: "POST",
|
|
1599
|
+
headers: {
|
|
1600
|
+
"Content-Type": "application/json",
|
|
1601
|
+
Authorization: `Bearer ${token.accessToken}`
|
|
1602
|
+
},
|
|
1603
|
+
body: JSON.stringify({
|
|
1604
|
+
query: `
|
|
1605
|
+
query {
|
|
1606
|
+
viewer {
|
|
1607
|
+
id
|
|
1608
|
+
name
|
|
1609
|
+
email
|
|
1610
|
+
avatarUrl
|
|
1611
|
+
active
|
|
1612
|
+
createdAt
|
|
1613
|
+
updatedAt
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
`
|
|
1617
|
+
})
|
|
1618
|
+
}
|
|
1619
|
+
);
|
|
1620
|
+
if (error || !profile?.data?.viewer) {
|
|
1621
|
+
return null;
|
|
1622
|
+
}
|
|
1623
|
+
const userData = profile.data.viewer;
|
|
1624
|
+
const userMap = await options.mapProfileToUser?.(userData);
|
|
1625
|
+
return {
|
|
1626
|
+
user: {
|
|
1627
|
+
id: profile.data.viewer.id,
|
|
1628
|
+
name: profile.data.viewer.name,
|
|
1629
|
+
email: profile.data.viewer.email,
|
|
1630
|
+
image: profile.data.viewer.avatarUrl,
|
|
1631
|
+
emailVerified: true,
|
|
1632
|
+
...userMap
|
|
1633
|
+
},
|
|
1634
|
+
data: userData
|
|
1635
|
+
};
|
|
1636
|
+
},
|
|
1637
|
+
options
|
|
1638
|
+
};
|
|
1639
|
+
};
|
|
1640
|
+
|
|
1641
|
+
const linkedin = (options) => {
|
|
1642
|
+
const authorizationEndpoint = "https://www.linkedin.com/oauth/v2/authorization";
|
|
1643
|
+
const tokenEndpoint = "https://www.linkedin.com/oauth/v2/accessToken";
|
|
1644
|
+
return {
|
|
1645
|
+
id: "linkedin",
|
|
1646
|
+
name: "Linkedin",
|
|
1647
|
+
createAuthorizationURL: async ({
|
|
1648
|
+
state,
|
|
1649
|
+
scopes,
|
|
1650
|
+
redirectURI,
|
|
1651
|
+
loginHint
|
|
1652
|
+
}) => {
|
|
1653
|
+
const _scopes = options.disableDefaultScope ? [] : ["profile", "email", "openid"];
|
|
1654
|
+
options.scope && _scopes.push(...options.scope);
|
|
1655
|
+
scopes && _scopes.push(...scopes);
|
|
1656
|
+
return await createAuthorizationURL({
|
|
1657
|
+
id: "linkedin",
|
|
1658
|
+
options,
|
|
1659
|
+
authorizationEndpoint,
|
|
1660
|
+
scopes: _scopes,
|
|
1661
|
+
state,
|
|
1662
|
+
loginHint,
|
|
1663
|
+
redirectURI
|
|
1664
|
+
});
|
|
1665
|
+
},
|
|
1666
|
+
validateAuthorizationCode: async ({ code, redirectURI }) => {
|
|
1667
|
+
return await validateAuthorizationCode({
|
|
1668
|
+
code,
|
|
1669
|
+
redirectURI,
|
|
1670
|
+
options,
|
|
1671
|
+
tokenEndpoint
|
|
1672
|
+
});
|
|
1673
|
+
},
|
|
1674
|
+
refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => {
|
|
1675
|
+
return refreshAccessToken({
|
|
1676
|
+
refreshToken,
|
|
1677
|
+
options: {
|
|
1678
|
+
clientId: options.clientId,
|
|
1679
|
+
clientKey: options.clientKey,
|
|
1680
|
+
clientSecret: options.clientSecret
|
|
1681
|
+
},
|
|
1682
|
+
tokenEndpoint
|
|
1683
|
+
});
|
|
1684
|
+
},
|
|
1685
|
+
async getUserInfo(token) {
|
|
1686
|
+
if (options.getUserInfo) {
|
|
1687
|
+
return options.getUserInfo(token);
|
|
1688
|
+
}
|
|
1689
|
+
const { data: profile, error } = await betterFetch(
|
|
1690
|
+
"https://api.linkedin.com/v2/userinfo",
|
|
1691
|
+
{
|
|
1692
|
+
method: "GET",
|
|
1693
|
+
headers: {
|
|
1694
|
+
Authorization: `Bearer ${token.accessToken}`
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
);
|
|
1698
|
+
if (error) {
|
|
1699
|
+
return null;
|
|
1700
|
+
}
|
|
1701
|
+
const userMap = await options.mapProfileToUser?.(profile);
|
|
1702
|
+
return {
|
|
1703
|
+
user: {
|
|
1704
|
+
id: profile.sub,
|
|
1705
|
+
name: profile.name,
|
|
1706
|
+
email: profile.email,
|
|
1707
|
+
emailVerified: profile.email_verified || false,
|
|
1708
|
+
image: profile.picture,
|
|
1709
|
+
...userMap
|
|
1710
|
+
},
|
|
1711
|
+
data: profile
|
|
1712
|
+
};
|
|
1713
|
+
},
|
|
1714
|
+
options
|
|
1715
|
+
};
|
|
1716
|
+
};
|
|
1717
|
+
|
|
1718
|
+
const cleanDoubleSlashes = (input = "") => {
|
|
1719
|
+
return input.split("://").map((str) => str.replace(/\/{2,}/g, "/")).join("://");
|
|
1720
|
+
};
|
|
1721
|
+
const issuerToEndpoints = (issuer) => {
|
|
1722
|
+
let baseUrl = issuer || "https://gitlab.com";
|
|
1723
|
+
return {
|
|
1724
|
+
authorizationEndpoint: cleanDoubleSlashes(`${baseUrl}/oauth/authorize`),
|
|
1725
|
+
tokenEndpoint: cleanDoubleSlashes(`${baseUrl}/oauth/token`),
|
|
1726
|
+
userinfoEndpoint: cleanDoubleSlashes(`${baseUrl}/api/v4/user`)
|
|
1727
|
+
};
|
|
1728
|
+
};
|
|
1729
|
+
const gitlab = (options) => {
|
|
1730
|
+
const { authorizationEndpoint, tokenEndpoint, userinfoEndpoint } = issuerToEndpoints(options.issuer);
|
|
1731
|
+
const issuerId = "gitlab";
|
|
1732
|
+
const issuerName = "Gitlab";
|
|
1733
|
+
return {
|
|
1734
|
+
id: issuerId,
|
|
1735
|
+
name: issuerName,
|
|
1736
|
+
createAuthorizationURL: async ({
|
|
1737
|
+
state,
|
|
1738
|
+
scopes,
|
|
1739
|
+
codeVerifier,
|
|
1740
|
+
loginHint,
|
|
1741
|
+
redirectURI
|
|
1742
|
+
}) => {
|
|
1743
|
+
const _scopes = options.disableDefaultScope ? [] : ["read_user"];
|
|
1744
|
+
options.scope && _scopes.push(...options.scope);
|
|
1745
|
+
scopes && _scopes.push(...scopes);
|
|
1746
|
+
return await createAuthorizationURL({
|
|
1747
|
+
id: issuerId,
|
|
1748
|
+
options,
|
|
1749
|
+
authorizationEndpoint,
|
|
1750
|
+
scopes: _scopes,
|
|
1751
|
+
state,
|
|
1752
|
+
redirectURI,
|
|
1753
|
+
codeVerifier,
|
|
1754
|
+
loginHint
|
|
1755
|
+
});
|
|
1756
|
+
},
|
|
1757
|
+
validateAuthorizationCode: async ({ code, redirectURI, codeVerifier }) => {
|
|
1758
|
+
return validateAuthorizationCode({
|
|
1759
|
+
code,
|
|
1760
|
+
redirectURI,
|
|
1761
|
+
options,
|
|
1762
|
+
codeVerifier,
|
|
1763
|
+
tokenEndpoint
|
|
1764
|
+
});
|
|
1765
|
+
},
|
|
1766
|
+
refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => {
|
|
1767
|
+
return refreshAccessToken({
|
|
1768
|
+
refreshToken,
|
|
1769
|
+
options: {
|
|
1770
|
+
clientId: options.clientId,
|
|
1771
|
+
clientKey: options.clientKey,
|
|
1772
|
+
clientSecret: options.clientSecret
|
|
1773
|
+
},
|
|
1774
|
+
tokenEndpoint
|
|
1775
|
+
});
|
|
1776
|
+
},
|
|
1777
|
+
async getUserInfo(token) {
|
|
1778
|
+
if (options.getUserInfo) {
|
|
1779
|
+
return options.getUserInfo(token);
|
|
1780
|
+
}
|
|
1781
|
+
const { data: profile, error } = await betterFetch(
|
|
1782
|
+
userinfoEndpoint,
|
|
1783
|
+
{ headers: { authorization: `Bearer ${token.accessToken}` } }
|
|
1784
|
+
);
|
|
1785
|
+
if (error || profile.state !== "active" || profile.locked) {
|
|
1786
|
+
return null;
|
|
1787
|
+
}
|
|
1788
|
+
const userMap = await options.mapProfileToUser?.(profile);
|
|
1789
|
+
return {
|
|
1790
|
+
user: {
|
|
1791
|
+
id: profile.id,
|
|
1792
|
+
name: profile.name ?? profile.username,
|
|
1793
|
+
email: profile.email,
|
|
1794
|
+
image: profile.avatar_url,
|
|
1795
|
+
emailVerified: true,
|
|
1796
|
+
...userMap
|
|
1797
|
+
},
|
|
1798
|
+
data: profile
|
|
1799
|
+
};
|
|
1800
|
+
},
|
|
1801
|
+
options
|
|
1802
|
+
};
|
|
1803
|
+
};
|
|
1804
|
+
|
|
1805
|
+
const tiktok = (options) => {
|
|
1806
|
+
return {
|
|
1807
|
+
id: "tiktok",
|
|
1808
|
+
name: "TikTok",
|
|
1809
|
+
createAuthorizationURL({ state, scopes, redirectURI }) {
|
|
1810
|
+
const _scopes = options.disableDefaultScope ? [] : ["user.info.profile"];
|
|
1811
|
+
options.scope && _scopes.push(...options.scope);
|
|
1812
|
+
scopes && _scopes.push(...scopes);
|
|
1813
|
+
return new URL(
|
|
1814
|
+
`https://www.tiktok.com/v2/auth/authorize?scope=${_scopes.join(
|
|
1815
|
+
","
|
|
1816
|
+
)}&response_type=code&client_key=${options.clientKey}&redirect_uri=${encodeURIComponent(
|
|
1817
|
+
options.redirectURI || redirectURI
|
|
1818
|
+
)}&state=${state}`
|
|
1819
|
+
);
|
|
1820
|
+
},
|
|
1821
|
+
validateAuthorizationCode: async ({ code, redirectURI }) => {
|
|
1822
|
+
return validateAuthorizationCode({
|
|
1823
|
+
code,
|
|
1824
|
+
redirectURI: options.redirectURI || redirectURI,
|
|
1825
|
+
options: {
|
|
1826
|
+
clientKey: options.clientKey,
|
|
1827
|
+
clientSecret: options.clientSecret
|
|
1828
|
+
},
|
|
1829
|
+
tokenEndpoint: "https://open.tiktokapis.com/v2/oauth/token/"
|
|
1830
|
+
});
|
|
1831
|
+
},
|
|
1832
|
+
refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => {
|
|
1833
|
+
return refreshAccessToken({
|
|
1834
|
+
refreshToken,
|
|
1835
|
+
options: {
|
|
1836
|
+
clientSecret: options.clientSecret
|
|
1837
|
+
},
|
|
1838
|
+
tokenEndpoint: "https://open.tiktokapis.com/v2/oauth/token/",
|
|
1839
|
+
authentication: "post",
|
|
1840
|
+
extraParams: {
|
|
1841
|
+
client_key: options.clientKey
|
|
1842
|
+
}
|
|
1843
|
+
});
|
|
1844
|
+
},
|
|
1845
|
+
async getUserInfo(token) {
|
|
1846
|
+
if (options.getUserInfo) {
|
|
1847
|
+
return options.getUserInfo(token);
|
|
1848
|
+
}
|
|
1849
|
+
const fields = [
|
|
1850
|
+
"open_id",
|
|
1851
|
+
"avatar_large_url",
|
|
1852
|
+
"display_name",
|
|
1853
|
+
"username"
|
|
1854
|
+
];
|
|
1855
|
+
const { data: profile, error } = await betterFetch(
|
|
1856
|
+
`https://open.tiktokapis.com/v2/user/info/?fields=${fields.join(",")}`,
|
|
1857
|
+
{
|
|
1858
|
+
headers: {
|
|
1859
|
+
authorization: `Bearer ${token.accessToken}`
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
);
|
|
1863
|
+
if (error) {
|
|
1864
|
+
return null;
|
|
1865
|
+
}
|
|
1866
|
+
return {
|
|
1867
|
+
user: {
|
|
1868
|
+
email: profile.data.user.email || profile.data.user.username,
|
|
1869
|
+
id: profile.data.user.open_id,
|
|
1870
|
+
name: profile.data.user.display_name || profile.data.user.username,
|
|
1871
|
+
image: profile.data.user.avatar_large_url,
|
|
1872
|
+
/** @note Tiktok does not provide emailVerified or even email*/
|
|
1873
|
+
emailVerified: profile.data.user.email ? true : false
|
|
1874
|
+
},
|
|
1875
|
+
data: profile
|
|
1876
|
+
};
|
|
1877
|
+
},
|
|
1878
|
+
options
|
|
1879
|
+
};
|
|
1880
|
+
};
|
|
1881
|
+
|
|
1882
|
+
const reddit = (options) => {
|
|
1883
|
+
return {
|
|
1884
|
+
id: "reddit",
|
|
1885
|
+
name: "Reddit",
|
|
1886
|
+
createAuthorizationURL({ state, scopes, redirectURI }) {
|
|
1887
|
+
const _scopes = options.disableDefaultScope ? [] : ["identity"];
|
|
1888
|
+
options.scope && _scopes.push(...options.scope);
|
|
1889
|
+
scopes && _scopes.push(...scopes);
|
|
1890
|
+
return createAuthorizationURL({
|
|
1891
|
+
id: "reddit",
|
|
1892
|
+
options,
|
|
1893
|
+
authorizationEndpoint: "https://www.reddit.com/api/v1/authorize",
|
|
1894
|
+
scopes: _scopes,
|
|
1895
|
+
state,
|
|
1896
|
+
redirectURI,
|
|
1897
|
+
duration: options.duration
|
|
1898
|
+
});
|
|
1899
|
+
},
|
|
1900
|
+
validateAuthorizationCode: async ({ code, redirectURI }) => {
|
|
1901
|
+
const body = new URLSearchParams({
|
|
1902
|
+
grant_type: "authorization_code",
|
|
1903
|
+
code,
|
|
1904
|
+
redirect_uri: options.redirectURI || redirectURI
|
|
1905
|
+
});
|
|
1906
|
+
const headers = {
|
|
1907
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
1908
|
+
accept: "text/plain",
|
|
1909
|
+
"user-agent": "better-auth",
|
|
1910
|
+
Authorization: `Basic ${base64.encode(
|
|
1911
|
+
`${options.clientId}:${options.clientSecret}`
|
|
1912
|
+
)}`
|
|
1913
|
+
};
|
|
1914
|
+
const { data, error } = await betterFetch(
|
|
1915
|
+
"https://www.reddit.com/api/v1/access_token",
|
|
1916
|
+
{
|
|
1917
|
+
method: "POST",
|
|
1918
|
+
headers,
|
|
1919
|
+
body: body.toString()
|
|
1920
|
+
}
|
|
1921
|
+
);
|
|
1922
|
+
if (error) {
|
|
1923
|
+
throw error;
|
|
1924
|
+
}
|
|
1925
|
+
return getOAuth2Tokens(data);
|
|
1926
|
+
},
|
|
1927
|
+
refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => {
|
|
1928
|
+
return refreshAccessToken({
|
|
1929
|
+
refreshToken,
|
|
1930
|
+
options: {
|
|
1931
|
+
clientId: options.clientId,
|
|
1932
|
+
clientKey: options.clientKey,
|
|
1933
|
+
clientSecret: options.clientSecret
|
|
1934
|
+
},
|
|
1935
|
+
authentication: "basic",
|
|
1936
|
+
tokenEndpoint: "https://www.reddit.com/api/v1/access_token"
|
|
1937
|
+
});
|
|
1938
|
+
},
|
|
1939
|
+
async getUserInfo(token) {
|
|
1940
|
+
if (options.getUserInfo) {
|
|
1941
|
+
return options.getUserInfo(token);
|
|
1942
|
+
}
|
|
1943
|
+
const { data: profile, error } = await betterFetch(
|
|
1944
|
+
"https://oauth.reddit.com/api/v1/me",
|
|
1945
|
+
{
|
|
1946
|
+
headers: {
|
|
1947
|
+
Authorization: `Bearer ${token.accessToken}`,
|
|
1948
|
+
"User-Agent": "better-auth"
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
);
|
|
1952
|
+
if (error) {
|
|
1953
|
+
return null;
|
|
1954
|
+
}
|
|
1955
|
+
const userMap = await options.mapProfileToUser?.(profile);
|
|
1956
|
+
return {
|
|
1957
|
+
user: {
|
|
1958
|
+
id: profile.id,
|
|
1959
|
+
name: profile.name,
|
|
1960
|
+
email: profile.oauth_client_id,
|
|
1961
|
+
emailVerified: profile.has_verified_email,
|
|
1962
|
+
image: profile.icon_img?.split("?")[0],
|
|
1963
|
+
...userMap
|
|
1964
|
+
},
|
|
1965
|
+
data: profile
|
|
1966
|
+
};
|
|
1967
|
+
},
|
|
1968
|
+
options
|
|
1969
|
+
};
|
|
1970
|
+
};
|
|
1971
|
+
|
|
1972
|
+
const roblox = (options) => {
|
|
1973
|
+
return {
|
|
1974
|
+
id: "roblox",
|
|
1975
|
+
name: "Roblox",
|
|
1976
|
+
createAuthorizationURL({ state, scopes, redirectURI }) {
|
|
1977
|
+
const _scopes = options.disableDefaultScope ? [] : ["openid", "profile"];
|
|
1978
|
+
options.scope && _scopes.push(...options.scope);
|
|
1979
|
+
scopes && _scopes.push(...scopes);
|
|
1980
|
+
return new URL(
|
|
1981
|
+
`https://apis.roblox.com/oauth/v1/authorize?scope=${_scopes.join(
|
|
1982
|
+
"+"
|
|
1983
|
+
)}&response_type=code&client_id=${options.clientId}&redirect_uri=${encodeURIComponent(
|
|
1984
|
+
options.redirectURI || redirectURI
|
|
1985
|
+
)}&state=${state}&prompt=${options.prompt || "select_account consent"}`
|
|
1986
|
+
);
|
|
1987
|
+
},
|
|
1988
|
+
validateAuthorizationCode: async ({ code, redirectURI }) => {
|
|
1989
|
+
return validateAuthorizationCode({
|
|
1990
|
+
code,
|
|
1991
|
+
redirectURI: options.redirectURI || redirectURI,
|
|
1992
|
+
options,
|
|
1993
|
+
tokenEndpoint: "https://apis.roblox.com/oauth/v1/token",
|
|
1994
|
+
authentication: "post"
|
|
1995
|
+
});
|
|
1996
|
+
},
|
|
1997
|
+
refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => {
|
|
1998
|
+
return refreshAccessToken({
|
|
1999
|
+
refreshToken,
|
|
2000
|
+
options: {
|
|
2001
|
+
clientId: options.clientId,
|
|
2002
|
+
clientKey: options.clientKey,
|
|
2003
|
+
clientSecret: options.clientSecret
|
|
2004
|
+
},
|
|
2005
|
+
tokenEndpoint: "https://apis.roblox.com/oauth/v1/token"
|
|
2006
|
+
});
|
|
2007
|
+
},
|
|
2008
|
+
async getUserInfo(token) {
|
|
2009
|
+
if (options.getUserInfo) {
|
|
2010
|
+
return options.getUserInfo(token);
|
|
2011
|
+
}
|
|
2012
|
+
const { data: profile, error } = await betterFetch(
|
|
2013
|
+
"https://apis.roblox.com/oauth/v1/userinfo",
|
|
2014
|
+
{
|
|
2015
|
+
headers: {
|
|
2016
|
+
authorization: `Bearer ${token.accessToken}`
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
);
|
|
2020
|
+
if (error) {
|
|
2021
|
+
return null;
|
|
2022
|
+
}
|
|
2023
|
+
const userMap = await options.mapProfileToUser?.(profile);
|
|
2024
|
+
return {
|
|
2025
|
+
user: {
|
|
2026
|
+
id: profile.sub,
|
|
2027
|
+
name: profile.nickname || profile.preferred_username || "",
|
|
2028
|
+
image: profile.picture,
|
|
2029
|
+
email: profile.preferred_username || null,
|
|
2030
|
+
// Roblox does not provide email
|
|
2031
|
+
emailVerified: true,
|
|
2032
|
+
...userMap
|
|
2033
|
+
},
|
|
2034
|
+
data: {
|
|
2035
|
+
...profile
|
|
2036
|
+
}
|
|
2037
|
+
};
|
|
2038
|
+
},
|
|
2039
|
+
options
|
|
2040
|
+
};
|
|
2041
|
+
};
|
|
2042
|
+
|
|
2043
|
+
const salesforce = (options) => {
|
|
2044
|
+
const environment = options.environment ?? "production";
|
|
2045
|
+
const isSandbox = environment === "sandbox";
|
|
2046
|
+
const authorizationEndpoint = options.loginUrl ? `https://${options.loginUrl}/services/oauth2/authorize` : isSandbox ? "https://test.salesforce.com/services/oauth2/authorize" : "https://login.salesforce.com/services/oauth2/authorize";
|
|
2047
|
+
const tokenEndpoint = options.loginUrl ? `https://${options.loginUrl}/services/oauth2/token` : isSandbox ? "https://test.salesforce.com/services/oauth2/token" : "https://login.salesforce.com/services/oauth2/token";
|
|
2048
|
+
const userInfoEndpoint = options.loginUrl ? `https://${options.loginUrl}/services/oauth2/userinfo` : isSandbox ? "https://test.salesforce.com/services/oauth2/userinfo" : "https://login.salesforce.com/services/oauth2/userinfo";
|
|
2049
|
+
return {
|
|
2050
|
+
id: "salesforce",
|
|
2051
|
+
name: "Salesforce",
|
|
2052
|
+
async createAuthorizationURL({ state, scopes, codeVerifier, redirectURI }) {
|
|
2053
|
+
if (!options.clientId || !options.clientSecret) {
|
|
2054
|
+
logger.error(
|
|
2055
|
+
"Client Id and Client Secret are required for Salesforce. Make sure to provide them in the options."
|
|
2056
|
+
);
|
|
2057
|
+
throw new BetterAuthError("CLIENT_ID_AND_SECRET_REQUIRED");
|
|
2058
|
+
}
|
|
2059
|
+
if (!codeVerifier) {
|
|
2060
|
+
throw new BetterAuthError("codeVerifier is required for Salesforce");
|
|
2061
|
+
}
|
|
2062
|
+
const _scopes = options.disableDefaultScope ? [] : ["openid", "email", "profile"];
|
|
2063
|
+
options.scope && _scopes.push(...options.scope);
|
|
2064
|
+
scopes && _scopes.push(...scopes);
|
|
2065
|
+
return createAuthorizationURL({
|
|
2066
|
+
id: "salesforce",
|
|
2067
|
+
options,
|
|
2068
|
+
authorizationEndpoint,
|
|
2069
|
+
scopes: _scopes,
|
|
2070
|
+
state,
|
|
2071
|
+
codeVerifier,
|
|
2072
|
+
redirectURI: options.redirectURI || redirectURI
|
|
2073
|
+
});
|
|
2074
|
+
},
|
|
2075
|
+
validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
|
|
2076
|
+
return validateAuthorizationCode({
|
|
2077
|
+
code,
|
|
2078
|
+
codeVerifier,
|
|
2079
|
+
redirectURI: options.redirectURI || redirectURI,
|
|
2080
|
+
options,
|
|
2081
|
+
tokenEndpoint
|
|
2082
|
+
});
|
|
2083
|
+
},
|
|
2084
|
+
refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => {
|
|
2085
|
+
return refreshAccessToken({
|
|
2086
|
+
refreshToken,
|
|
2087
|
+
options: {
|
|
2088
|
+
clientId: options.clientId,
|
|
2089
|
+
clientSecret: options.clientSecret
|
|
2090
|
+
},
|
|
2091
|
+
tokenEndpoint
|
|
2092
|
+
});
|
|
2093
|
+
},
|
|
2094
|
+
async getUserInfo(token) {
|
|
2095
|
+
if (options.getUserInfo) {
|
|
2096
|
+
return options.getUserInfo(token);
|
|
2097
|
+
}
|
|
2098
|
+
try {
|
|
2099
|
+
const { data: user } = await betterFetch(
|
|
2100
|
+
userInfoEndpoint,
|
|
2101
|
+
{
|
|
2102
|
+
headers: {
|
|
2103
|
+
Authorization: `Bearer ${token.accessToken}`
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
);
|
|
2107
|
+
if (!user) {
|
|
2108
|
+
logger.error("Failed to fetch user info from Salesforce");
|
|
2109
|
+
return null;
|
|
2110
|
+
}
|
|
2111
|
+
const userMap = await options.mapProfileToUser?.(user);
|
|
2112
|
+
return {
|
|
2113
|
+
user: {
|
|
2114
|
+
id: user.user_id,
|
|
2115
|
+
name: user.name,
|
|
2116
|
+
email: user.email,
|
|
2117
|
+
image: user.photos?.picture || user.photos?.thumbnail,
|
|
2118
|
+
emailVerified: user.email_verified ?? false,
|
|
2119
|
+
...userMap
|
|
2120
|
+
},
|
|
2121
|
+
data: user
|
|
2122
|
+
};
|
|
2123
|
+
} catch (error) {
|
|
2124
|
+
logger.error("Failed to fetch user info from Salesforce:", error);
|
|
2125
|
+
return null;
|
|
2126
|
+
}
|
|
2127
|
+
},
|
|
2128
|
+
options
|
|
2129
|
+
};
|
|
2130
|
+
};
|
|
2131
|
+
|
|
2132
|
+
const vk = (options) => {
|
|
2133
|
+
return {
|
|
2134
|
+
id: "vk",
|
|
2135
|
+
name: "VK",
|
|
2136
|
+
async createAuthorizationURL({ state, scopes, codeVerifier, redirectURI }) {
|
|
2137
|
+
const _scopes = options.disableDefaultScope ? [] : ["email", "phone"];
|
|
2138
|
+
options.scope && _scopes.push(...options.scope);
|
|
2139
|
+
scopes && _scopes.push(...scopes);
|
|
2140
|
+
const authorizationEndpoint = "https://id.vk.com/authorize";
|
|
2141
|
+
return createAuthorizationURL({
|
|
2142
|
+
id: "vk",
|
|
2143
|
+
options,
|
|
2144
|
+
authorizationEndpoint,
|
|
2145
|
+
scopes: _scopes,
|
|
2146
|
+
state,
|
|
2147
|
+
redirectURI,
|
|
2148
|
+
codeVerifier
|
|
2149
|
+
});
|
|
2150
|
+
},
|
|
2151
|
+
validateAuthorizationCode: async ({
|
|
2152
|
+
code,
|
|
2153
|
+
codeVerifier,
|
|
2154
|
+
redirectURI,
|
|
2155
|
+
deviceId
|
|
2156
|
+
}) => {
|
|
2157
|
+
return validateAuthorizationCode({
|
|
2158
|
+
code,
|
|
2159
|
+
codeVerifier,
|
|
2160
|
+
redirectURI: options.redirectURI || redirectURI,
|
|
2161
|
+
options,
|
|
2162
|
+
deviceId,
|
|
2163
|
+
tokenEndpoint: "https://id.vk.com/oauth2/auth"
|
|
2164
|
+
});
|
|
2165
|
+
},
|
|
2166
|
+
refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => {
|
|
2167
|
+
return refreshAccessToken({
|
|
2168
|
+
refreshToken,
|
|
2169
|
+
options: {
|
|
2170
|
+
clientId: options.clientId,
|
|
2171
|
+
clientKey: options.clientKey,
|
|
2172
|
+
clientSecret: options.clientSecret
|
|
2173
|
+
},
|
|
2174
|
+
tokenEndpoint: "https://id.vk.com/oauth2/auth"
|
|
2175
|
+
});
|
|
2176
|
+
},
|
|
2177
|
+
async getUserInfo(data) {
|
|
2178
|
+
if (options.getUserInfo) {
|
|
2179
|
+
return options.getUserInfo(data);
|
|
2180
|
+
}
|
|
2181
|
+
if (!data.accessToken) {
|
|
2182
|
+
return null;
|
|
2183
|
+
}
|
|
2184
|
+
const formBody = new URLSearchParams({
|
|
2185
|
+
access_token: data.accessToken,
|
|
2186
|
+
client_id: options.clientId
|
|
2187
|
+
}).toString();
|
|
2188
|
+
const { data: profile, error } = await betterFetch(
|
|
2189
|
+
"https://id.vk.com/oauth2/user_info",
|
|
2190
|
+
{
|
|
2191
|
+
method: "POST",
|
|
2192
|
+
headers: {
|
|
2193
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
2194
|
+
},
|
|
2195
|
+
body: formBody
|
|
2196
|
+
}
|
|
2197
|
+
);
|
|
2198
|
+
if (error) {
|
|
2199
|
+
return null;
|
|
2200
|
+
}
|
|
2201
|
+
if (!profile.user.email) {
|
|
2202
|
+
return null;
|
|
2203
|
+
}
|
|
2204
|
+
const userMap = await options.mapProfileToUser?.(profile);
|
|
2205
|
+
return {
|
|
2206
|
+
user: {
|
|
2207
|
+
id: profile.user.user_id,
|
|
2208
|
+
first_name: profile.user.first_name,
|
|
2209
|
+
last_name: profile.user.last_name,
|
|
2210
|
+
email: profile.user.email,
|
|
2211
|
+
image: profile.user.avatar,
|
|
2212
|
+
/** @note VK does not provide emailVerified*/
|
|
2213
|
+
emailVerified: !!profile.user.email,
|
|
2214
|
+
birthday: profile.user.birthday,
|
|
2215
|
+
sex: profile.user.sex,
|
|
2216
|
+
name: `${profile.user.first_name} ${profile.user.last_name}`,
|
|
2217
|
+
...userMap
|
|
2218
|
+
},
|
|
2219
|
+
data: profile
|
|
2220
|
+
};
|
|
2221
|
+
},
|
|
2222
|
+
options
|
|
2223
|
+
};
|
|
2224
|
+
};
|
|
2225
|
+
|
|
2226
|
+
const zoom = (userOptions) => {
|
|
2227
|
+
const options = {
|
|
2228
|
+
pkce: true,
|
|
2229
|
+
...userOptions
|
|
2230
|
+
};
|
|
2231
|
+
return {
|
|
2232
|
+
id: "zoom",
|
|
2233
|
+
name: "Zoom",
|
|
2234
|
+
createAuthorizationURL: async ({ state, redirectURI, codeVerifier }) => {
|
|
2235
|
+
const params = new URLSearchParams({
|
|
2236
|
+
response_type: "code",
|
|
2237
|
+
redirect_uri: options.redirectURI ? options.redirectURI : redirectURI,
|
|
2238
|
+
client_id: options.clientId,
|
|
2239
|
+
state
|
|
2240
|
+
});
|
|
2241
|
+
if (options.pkce) {
|
|
2242
|
+
const codeChallenge = await generateCodeChallenge(codeVerifier);
|
|
2243
|
+
params.set("code_challenge_method", "S256");
|
|
2244
|
+
params.set("code_challenge", codeChallenge);
|
|
2245
|
+
}
|
|
2246
|
+
const url = new URL("https://zoom.us/oauth/authorize");
|
|
2247
|
+
url.search = params.toString();
|
|
2248
|
+
return url;
|
|
2249
|
+
},
|
|
2250
|
+
validateAuthorizationCode: async ({ code, redirectURI, codeVerifier }) => {
|
|
2251
|
+
return validateAuthorizationCode({
|
|
2252
|
+
code,
|
|
2253
|
+
redirectURI: options.redirectURI || redirectURI,
|
|
2254
|
+
codeVerifier,
|
|
2255
|
+
options,
|
|
2256
|
+
tokenEndpoint: "https://zoom.us/oauth/token",
|
|
2257
|
+
authentication: "post"
|
|
2258
|
+
});
|
|
2259
|
+
},
|
|
2260
|
+
async getUserInfo(token) {
|
|
2261
|
+
if (options.getUserInfo) {
|
|
2262
|
+
return options.getUserInfo(token);
|
|
2263
|
+
}
|
|
2264
|
+
const { data: profile, error } = await betterFetch(
|
|
2265
|
+
"https://api.zoom.us/v2/users/me",
|
|
2266
|
+
{
|
|
2267
|
+
headers: {
|
|
2268
|
+
authorization: `Bearer ${token.accessToken}`
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
);
|
|
2272
|
+
if (error) {
|
|
2273
|
+
return null;
|
|
2274
|
+
}
|
|
2275
|
+
const userMap = await options.mapProfileToUser?.(profile);
|
|
2276
|
+
return {
|
|
2277
|
+
user: {
|
|
2278
|
+
id: profile.id,
|
|
2279
|
+
name: profile.display_name,
|
|
2280
|
+
image: profile.pic_url,
|
|
2281
|
+
email: profile.email,
|
|
2282
|
+
emailVerified: Boolean(profile.verified),
|
|
2283
|
+
...userMap
|
|
2284
|
+
},
|
|
2285
|
+
data: {
|
|
2286
|
+
...profile
|
|
2287
|
+
}
|
|
2288
|
+
};
|
|
2289
|
+
}
|
|
2290
|
+
};
|
|
2291
|
+
};
|
|
2292
|
+
|
|
2293
|
+
const kakao = (options) => {
|
|
2294
|
+
return {
|
|
2295
|
+
id: "kakao",
|
|
2296
|
+
name: "Kakao",
|
|
2297
|
+
createAuthorizationURL({ state, scopes, redirectURI }) {
|
|
2298
|
+
const _scopes = options.disableDefaultScope ? [] : ["account_email", "profile_image", "profile_nickname"];
|
|
2299
|
+
options.scope && _scopes.push(...options.scope);
|
|
2300
|
+
scopes && _scopes.push(...scopes);
|
|
2301
|
+
return createAuthorizationURL({
|
|
2302
|
+
id: "kakao",
|
|
2303
|
+
options,
|
|
2304
|
+
authorizationEndpoint: "https://kauth.kakao.com/oauth/authorize",
|
|
2305
|
+
scopes: _scopes,
|
|
2306
|
+
state,
|
|
2307
|
+
redirectURI
|
|
2308
|
+
});
|
|
2309
|
+
},
|
|
2310
|
+
validateAuthorizationCode: async ({ code, redirectURI }) => {
|
|
2311
|
+
return validateAuthorizationCode({
|
|
2312
|
+
code,
|
|
2313
|
+
redirectURI,
|
|
2314
|
+
options,
|
|
2315
|
+
tokenEndpoint: "https://kauth.kakao.com/oauth/token"
|
|
2316
|
+
});
|
|
2317
|
+
},
|
|
2318
|
+
refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => {
|
|
2319
|
+
return refreshAccessToken({
|
|
2320
|
+
refreshToken,
|
|
2321
|
+
options: {
|
|
2322
|
+
clientId: options.clientId,
|
|
2323
|
+
clientKey: options.clientKey,
|
|
2324
|
+
clientSecret: options.clientSecret
|
|
2325
|
+
},
|
|
2326
|
+
tokenEndpoint: "https://kauth.kakao.com/oauth/token"
|
|
2327
|
+
});
|
|
2328
|
+
},
|
|
2329
|
+
async getUserInfo(token) {
|
|
2330
|
+
if (options.getUserInfo) {
|
|
2331
|
+
return options.getUserInfo(token);
|
|
2332
|
+
}
|
|
2333
|
+
const { data: profile, error } = await betterFetch(
|
|
2334
|
+
"https://kapi.kakao.com/v2/user/me",
|
|
2335
|
+
{
|
|
2336
|
+
headers: {
|
|
2337
|
+
Authorization: `Bearer ${token.accessToken}`
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
);
|
|
2341
|
+
if (error || !profile) {
|
|
2342
|
+
return null;
|
|
2343
|
+
}
|
|
2344
|
+
const userMap = await options.mapProfileToUser?.(profile);
|
|
2345
|
+
const account = profile.kakao_account || {};
|
|
2346
|
+
const kakaoProfile = account.profile || {};
|
|
2347
|
+
const user = {
|
|
2348
|
+
id: String(profile.id),
|
|
2349
|
+
name: kakaoProfile.nickname || account.name || void 0,
|
|
2350
|
+
email: account.email,
|
|
2351
|
+
image: kakaoProfile.profile_image_url || kakaoProfile.thumbnail_image_url,
|
|
2352
|
+
emailVerified: !!account.is_email_valid && !!account.is_email_verified,
|
|
2353
|
+
...userMap
|
|
2354
|
+
};
|
|
2355
|
+
return {
|
|
2356
|
+
user,
|
|
2357
|
+
data: profile
|
|
2358
|
+
};
|
|
2359
|
+
},
|
|
2360
|
+
options
|
|
2361
|
+
};
|
|
2362
|
+
};
|
|
2363
|
+
|
|
2364
|
+
const naver = (options) => {
|
|
2365
|
+
return {
|
|
2366
|
+
id: "naver",
|
|
2367
|
+
name: "Naver",
|
|
2368
|
+
createAuthorizationURL({ state, scopes, redirectURI }) {
|
|
2369
|
+
const _scopes = options.disableDefaultScope ? [] : ["profile", "email"];
|
|
2370
|
+
options.scope && _scopes.push(...options.scope);
|
|
2371
|
+
scopes && _scopes.push(...scopes);
|
|
2372
|
+
return createAuthorizationURL({
|
|
2373
|
+
id: "naver",
|
|
2374
|
+
options,
|
|
2375
|
+
authorizationEndpoint: "https://nid.naver.com/oauth2.0/authorize",
|
|
2376
|
+
scopes: _scopes,
|
|
2377
|
+
state,
|
|
2378
|
+
redirectURI
|
|
2379
|
+
});
|
|
2380
|
+
},
|
|
2381
|
+
validateAuthorizationCode: async ({ code, redirectURI }) => {
|
|
2382
|
+
return validateAuthorizationCode({
|
|
2383
|
+
code,
|
|
2384
|
+
redirectURI,
|
|
2385
|
+
options,
|
|
2386
|
+
tokenEndpoint: "https://nid.naver.com/oauth2.0/token"
|
|
2387
|
+
});
|
|
2388
|
+
},
|
|
2389
|
+
refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => {
|
|
2390
|
+
return refreshAccessToken({
|
|
2391
|
+
refreshToken,
|
|
2392
|
+
options: {
|
|
2393
|
+
clientId: options.clientId,
|
|
2394
|
+
clientKey: options.clientKey,
|
|
2395
|
+
clientSecret: options.clientSecret
|
|
2396
|
+
},
|
|
2397
|
+
tokenEndpoint: "https://nid.naver.com/oauth2.0/token"
|
|
2398
|
+
});
|
|
2399
|
+
},
|
|
2400
|
+
async getUserInfo(token) {
|
|
2401
|
+
if (options.getUserInfo) {
|
|
2402
|
+
return options.getUserInfo(token);
|
|
2403
|
+
}
|
|
2404
|
+
const { data: profile, error } = await betterFetch(
|
|
2405
|
+
"https://openapi.naver.com/v1/nid/me",
|
|
2406
|
+
{
|
|
2407
|
+
headers: {
|
|
2408
|
+
Authorization: `Bearer ${token.accessToken}`
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
);
|
|
2412
|
+
if (error || !profile || profile.resultcode !== "00") {
|
|
2413
|
+
return null;
|
|
2414
|
+
}
|
|
2415
|
+
const userMap = await options.mapProfileToUser?.(profile);
|
|
2416
|
+
const res = profile.response || {};
|
|
2417
|
+
const user = {
|
|
2418
|
+
id: res.id,
|
|
2419
|
+
name: res.name || res.nickname,
|
|
2420
|
+
email: res.email,
|
|
2421
|
+
image: res.profile_image,
|
|
2422
|
+
emailVerified: false,
|
|
2423
|
+
...userMap
|
|
2424
|
+
};
|
|
2425
|
+
return {
|
|
2426
|
+
user,
|
|
2427
|
+
data: profile
|
|
2428
|
+
};
|
|
2429
|
+
},
|
|
2430
|
+
options
|
|
2431
|
+
};
|
|
2432
|
+
};
|
|
2433
|
+
|
|
2434
|
+
const line = (options) => {
|
|
2435
|
+
const authorizationEndpoint = "https://access.line.me/oauth2/v2.1/authorize";
|
|
2436
|
+
const tokenEndpoint = "https://api.line.me/oauth2/v2.1/token";
|
|
2437
|
+
const userInfoEndpoint = "https://api.line.me/oauth2/v2.1/userinfo";
|
|
2438
|
+
const verifyIdTokenEndpoint = "https://api.line.me/oauth2/v2.1/verify";
|
|
2439
|
+
return {
|
|
2440
|
+
id: "line",
|
|
2441
|
+
name: "LINE",
|
|
2442
|
+
async createAuthorizationURL({
|
|
2443
|
+
state,
|
|
2444
|
+
scopes,
|
|
2445
|
+
codeVerifier,
|
|
2446
|
+
redirectURI,
|
|
2447
|
+
loginHint
|
|
2448
|
+
}) {
|
|
2449
|
+
const _scopes = options.disableDefaultScope ? [] : ["openid", "profile", "email"];
|
|
2450
|
+
options.scope && _scopes.push(...options.scope);
|
|
2451
|
+
scopes && _scopes.push(...scopes);
|
|
2452
|
+
return await createAuthorizationURL({
|
|
2453
|
+
id: "line",
|
|
2454
|
+
options,
|
|
2455
|
+
authorizationEndpoint,
|
|
2456
|
+
scopes: _scopes,
|
|
2457
|
+
state,
|
|
2458
|
+
codeVerifier,
|
|
2459
|
+
redirectURI,
|
|
2460
|
+
loginHint
|
|
2461
|
+
});
|
|
2462
|
+
},
|
|
2463
|
+
validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
|
|
2464
|
+
return validateAuthorizationCode({
|
|
2465
|
+
code,
|
|
2466
|
+
codeVerifier,
|
|
2467
|
+
redirectURI,
|
|
2468
|
+
options,
|
|
2469
|
+
tokenEndpoint
|
|
2470
|
+
});
|
|
2471
|
+
},
|
|
2472
|
+
refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => {
|
|
2473
|
+
return refreshAccessToken({
|
|
2474
|
+
refreshToken,
|
|
2475
|
+
options: {
|
|
2476
|
+
clientId: options.clientId,
|
|
2477
|
+
clientSecret: options.clientSecret
|
|
2478
|
+
},
|
|
2479
|
+
tokenEndpoint
|
|
2480
|
+
});
|
|
2481
|
+
},
|
|
2482
|
+
async verifyIdToken(token, nonce) {
|
|
2483
|
+
if (options.disableIdTokenSignIn) {
|
|
2484
|
+
return false;
|
|
2485
|
+
}
|
|
2486
|
+
if (options.verifyIdToken) {
|
|
2487
|
+
return options.verifyIdToken(token, nonce);
|
|
2488
|
+
}
|
|
2489
|
+
const body = new URLSearchParams();
|
|
2490
|
+
body.set("id_token", token);
|
|
2491
|
+
body.set("client_id", options.clientId);
|
|
2492
|
+
if (nonce) body.set("nonce", nonce);
|
|
2493
|
+
const { data, error } = await betterFetch(
|
|
2494
|
+
verifyIdTokenEndpoint,
|
|
2495
|
+
{
|
|
2496
|
+
method: "POST",
|
|
2497
|
+
headers: {
|
|
2498
|
+
"content-type": "application/x-www-form-urlencoded"
|
|
2499
|
+
},
|
|
2500
|
+
body
|
|
2501
|
+
}
|
|
2502
|
+
);
|
|
2503
|
+
if (error || !data) {
|
|
2504
|
+
return false;
|
|
2505
|
+
}
|
|
2506
|
+
if (data.aud !== options.clientId) return false;
|
|
2507
|
+
if (nonce && data.nonce && data.nonce !== nonce) return false;
|
|
2508
|
+
return true;
|
|
2509
|
+
},
|
|
2510
|
+
async getUserInfo(token) {
|
|
2511
|
+
if (options.getUserInfo) {
|
|
2512
|
+
return options.getUserInfo(token);
|
|
2513
|
+
}
|
|
2514
|
+
let profile = null;
|
|
2515
|
+
if (token.idToken) {
|
|
2516
|
+
try {
|
|
2517
|
+
profile = decodeJwt(token.idToken);
|
|
2518
|
+
} catch {
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2521
|
+
if (!profile) {
|
|
2522
|
+
const { data } = await betterFetch(userInfoEndpoint, {
|
|
2523
|
+
headers: {
|
|
2524
|
+
authorization: `Bearer ${token.accessToken}`
|
|
2525
|
+
}
|
|
2526
|
+
});
|
|
2527
|
+
profile = data || null;
|
|
2528
|
+
}
|
|
2529
|
+
if (!profile) return null;
|
|
2530
|
+
const userMap = await options.mapProfileToUser?.(profile);
|
|
2531
|
+
const id = profile.sub || profile.userId;
|
|
2532
|
+
const name = profile.name || profile.displayName;
|
|
2533
|
+
const image = profile.picture || profile.pictureUrl || void 0;
|
|
2534
|
+
const email = profile.email;
|
|
2535
|
+
return {
|
|
2536
|
+
user: {
|
|
2537
|
+
id,
|
|
2538
|
+
name,
|
|
2539
|
+
email,
|
|
2540
|
+
image,
|
|
2541
|
+
// LINE does not expose email verification status in ID token/userinfo
|
|
2542
|
+
emailVerified: false,
|
|
2543
|
+
...userMap
|
|
2544
|
+
},
|
|
2545
|
+
data: profile
|
|
2546
|
+
};
|
|
2547
|
+
},
|
|
2548
|
+
options
|
|
2549
|
+
};
|
|
2550
|
+
};
|
|
2551
|
+
|
|
2552
|
+
const paypal = (options) => {
|
|
2553
|
+
const environment = options.environment || "sandbox";
|
|
2554
|
+
const isSandbox = environment === "sandbox";
|
|
2555
|
+
const authorizationEndpoint = isSandbox ? "https://www.sandbox.paypal.com/signin/authorize" : "https://www.paypal.com/signin/authorize";
|
|
2556
|
+
const tokenEndpoint = isSandbox ? "https://api-m.sandbox.paypal.com/v1/oauth2/token" : "https://api-m.paypal.com/v1/oauth2/token";
|
|
2557
|
+
const userInfoEndpoint = isSandbox ? "https://api-m.sandbox.paypal.com/v1/identity/oauth2/userinfo" : "https://api-m.paypal.com/v1/identity/oauth2/userinfo";
|
|
2558
|
+
return {
|
|
2559
|
+
id: "paypal",
|
|
2560
|
+
name: "PayPal",
|
|
2561
|
+
async createAuthorizationURL({ state, codeVerifier, redirectURI }) {
|
|
2562
|
+
if (!options.clientId || !options.clientSecret) {
|
|
2563
|
+
logger.error(
|
|
2564
|
+
"Client Id and Client Secret is required for PayPal. Make sure to provide them in the options."
|
|
2565
|
+
);
|
|
2566
|
+
throw new BetterAuthError("CLIENT_ID_AND_SECRET_REQUIRED");
|
|
2567
|
+
}
|
|
2568
|
+
const _scopes = [];
|
|
2569
|
+
const url = await createAuthorizationURL({
|
|
2570
|
+
id: "paypal",
|
|
2571
|
+
options,
|
|
2572
|
+
authorizationEndpoint,
|
|
2573
|
+
scopes: _scopes,
|
|
2574
|
+
state,
|
|
2575
|
+
codeVerifier,
|
|
2576
|
+
redirectURI,
|
|
2577
|
+
prompt: options.prompt
|
|
2578
|
+
});
|
|
2579
|
+
return url;
|
|
2580
|
+
},
|
|
2581
|
+
validateAuthorizationCode: async ({ code, redirectURI }) => {
|
|
2582
|
+
const credentials = base64.encode(
|
|
2583
|
+
`${options.clientId}:${options.clientSecret}`
|
|
2584
|
+
);
|
|
2585
|
+
try {
|
|
2586
|
+
const response = await betterFetch(tokenEndpoint, {
|
|
2587
|
+
method: "POST",
|
|
2588
|
+
headers: {
|
|
2589
|
+
Authorization: `Basic ${credentials}`,
|
|
2590
|
+
Accept: "application/json",
|
|
2591
|
+
"Accept-Language": "en_US",
|
|
2592
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
2593
|
+
},
|
|
2594
|
+
body: new URLSearchParams({
|
|
2595
|
+
grant_type: "authorization_code",
|
|
2596
|
+
code,
|
|
2597
|
+
redirect_uri: redirectURI
|
|
2598
|
+
}).toString()
|
|
2599
|
+
});
|
|
2600
|
+
if (!response.data) {
|
|
2601
|
+
throw new BetterAuthError("FAILED_TO_GET_ACCESS_TOKEN");
|
|
2602
|
+
}
|
|
2603
|
+
const data = response.data;
|
|
2604
|
+
const result = {
|
|
2605
|
+
accessToken: data.access_token,
|
|
2606
|
+
refreshToken: data.refresh_token,
|
|
2607
|
+
accessTokenExpiresAt: data.expires_in ? new Date(Date.now() + data.expires_in * 1e3) : void 0,
|
|
2608
|
+
idToken: data.id_token
|
|
2609
|
+
};
|
|
2610
|
+
return result;
|
|
2611
|
+
} catch (error) {
|
|
2612
|
+
logger.error("PayPal token exchange failed:", error);
|
|
2613
|
+
throw new BetterAuthError("FAILED_TO_GET_ACCESS_TOKEN");
|
|
2614
|
+
}
|
|
2615
|
+
},
|
|
2616
|
+
refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => {
|
|
2617
|
+
const credentials = base64.encode(
|
|
2618
|
+
`${options.clientId}:${options.clientSecret}`
|
|
2619
|
+
);
|
|
2620
|
+
try {
|
|
2621
|
+
const response = await betterFetch(tokenEndpoint, {
|
|
2622
|
+
method: "POST",
|
|
2623
|
+
headers: {
|
|
2624
|
+
Authorization: `Basic ${credentials}`,
|
|
2625
|
+
Accept: "application/json",
|
|
2626
|
+
"Accept-Language": "en_US",
|
|
2627
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
2628
|
+
},
|
|
2629
|
+
body: new URLSearchParams({
|
|
2630
|
+
grant_type: "refresh_token",
|
|
2631
|
+
refresh_token: refreshToken
|
|
2632
|
+
}).toString()
|
|
2633
|
+
});
|
|
2634
|
+
if (!response.data) {
|
|
2635
|
+
throw new BetterAuthError("FAILED_TO_REFRESH_ACCESS_TOKEN");
|
|
2636
|
+
}
|
|
2637
|
+
const data = response.data;
|
|
2638
|
+
return {
|
|
2639
|
+
accessToken: data.access_token,
|
|
2640
|
+
refreshToken: data.refresh_token,
|
|
2641
|
+
accessTokenExpiresAt: data.expires_in ? new Date(Date.now() + data.expires_in * 1e3) : void 0
|
|
2642
|
+
};
|
|
2643
|
+
} catch (error) {
|
|
2644
|
+
logger.error("PayPal token refresh failed:", error);
|
|
2645
|
+
throw new BetterAuthError("FAILED_TO_REFRESH_ACCESS_TOKEN");
|
|
2646
|
+
}
|
|
2647
|
+
},
|
|
2648
|
+
async verifyIdToken(token, nonce) {
|
|
2649
|
+
if (options.disableIdTokenSignIn) {
|
|
2650
|
+
return false;
|
|
2651
|
+
}
|
|
2652
|
+
if (options.verifyIdToken) {
|
|
2653
|
+
return options.verifyIdToken(token, nonce);
|
|
2654
|
+
}
|
|
2655
|
+
try {
|
|
2656
|
+
const payload = decodeJwt(token);
|
|
2657
|
+
return !!payload.sub;
|
|
2658
|
+
} catch (error) {
|
|
2659
|
+
logger.error("Failed to verify PayPal ID token:", error);
|
|
2660
|
+
return false;
|
|
2661
|
+
}
|
|
2662
|
+
},
|
|
2663
|
+
async getUserInfo(token) {
|
|
2664
|
+
if (options.getUserInfo) {
|
|
2665
|
+
return options.getUserInfo(token);
|
|
2666
|
+
}
|
|
2667
|
+
if (!token.accessToken) {
|
|
2668
|
+
logger.error("Access token is required to fetch PayPal user info");
|
|
2669
|
+
return null;
|
|
2670
|
+
}
|
|
2671
|
+
try {
|
|
2672
|
+
const response = await betterFetch(
|
|
2673
|
+
`${userInfoEndpoint}?schema=paypalv1.1`,
|
|
2674
|
+
{
|
|
2675
|
+
headers: {
|
|
2676
|
+
Authorization: `Bearer ${token.accessToken}`,
|
|
2677
|
+
Accept: "application/json"
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
);
|
|
2681
|
+
if (!response.data) {
|
|
2682
|
+
logger.error("Failed to fetch user info from PayPal");
|
|
2683
|
+
return null;
|
|
2684
|
+
}
|
|
2685
|
+
const userInfo = response.data;
|
|
2686
|
+
const userMap = await options.mapProfileToUser?.(userInfo);
|
|
2687
|
+
const result = {
|
|
2688
|
+
user: {
|
|
2689
|
+
id: userInfo.user_id,
|
|
2690
|
+
name: userInfo.name,
|
|
2691
|
+
email: userInfo.email,
|
|
2692
|
+
image: userInfo.picture,
|
|
2693
|
+
emailVerified: userInfo.email_verified,
|
|
2694
|
+
...userMap
|
|
2695
|
+
},
|
|
2696
|
+
data: userInfo
|
|
2697
|
+
};
|
|
2698
|
+
return result;
|
|
2699
|
+
} catch (error) {
|
|
2700
|
+
logger.error("Failed to fetch user info from PayPal:", error);
|
|
2701
|
+
return null;
|
|
2702
|
+
}
|
|
2703
|
+
},
|
|
2704
|
+
options
|
|
2705
|
+
};
|
|
2706
|
+
};
|
|
2707
|
+
|
|
2708
|
+
const socialProviders = {
|
|
2709
|
+
apple,
|
|
2710
|
+
atlassian,
|
|
2711
|
+
cognito,
|
|
2712
|
+
discord,
|
|
2713
|
+
facebook,
|
|
2714
|
+
figma,
|
|
2715
|
+
github,
|
|
2716
|
+
microsoft,
|
|
2717
|
+
google,
|
|
2718
|
+
huggingface,
|
|
2719
|
+
slack,
|
|
2720
|
+
spotify,
|
|
2721
|
+
twitch,
|
|
2722
|
+
twitter,
|
|
2723
|
+
dropbox,
|
|
2724
|
+
kick,
|
|
2725
|
+
linear,
|
|
2726
|
+
linkedin,
|
|
2727
|
+
gitlab,
|
|
2728
|
+
tiktok,
|
|
2729
|
+
reddit,
|
|
2730
|
+
roblox,
|
|
2731
|
+
salesforce,
|
|
2732
|
+
vk,
|
|
2733
|
+
zoom,
|
|
2734
|
+
notion,
|
|
2735
|
+
kakao,
|
|
2736
|
+
naver,
|
|
2737
|
+
line,
|
|
2738
|
+
paypal
|
|
2739
|
+
};
|
|
2740
|
+
const socialProviderList = Object.keys(socialProviders);
|
|
2741
|
+
const SocialProviderListEnum = z.enum(socialProviderList).or(z.string());
|
|
2742
|
+
|
|
2743
|
+
export { SocialProviderListEnum, apple, atlassian, cognito, discord, dropbox, facebook, figma, getApplePublicKey, getCognitoPublicKey, github, gitlab, google, huggingface, kakao, kick, line, linear, linkedin, microsoft, naver, notion, paypal, reddit, roblox, salesforce, slack, socialProviderList, socialProviders, spotify, tiktok, twitch, twitter, vk, zoom };
|