@better-auth/core 1.7.1 → 1.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/index.d.mts +3 -0
- package/dist/context/endpoint-context.d.mts +19 -5
- package/dist/context/endpoint-context.mjs +35 -16
- package/dist/context/global.mjs +5 -2
- package/dist/context/index.d.mts +2 -2
- package/dist/context/index.mjs +2 -2
- package/dist/context/transaction.mjs +3 -0
- package/dist/db/adapter/atomic-fallback.mjs +134 -0
- package/dist/db/adapter/factory.mjs +22 -4
- package/dist/db/adapter/index.d.mts +15 -11
- package/dist/db/get-tables.mjs +1 -9
- package/dist/db/index.d.mts +2 -2
- package/dist/db/index.mjs +2 -2
- package/dist/db/internal.d.mts +3 -1
- package/dist/db/internal.mjs +3 -1
- package/dist/db/schema/account.d.mts +2 -13
- package/dist/db/schema/account.mjs +1 -19
- package/dist/db/schema-check.d.mts +48 -0
- package/dist/db/schema-check.mjs +80 -0
- package/dist/db/schema-diff.d.mts +104 -0
- package/dist/db/schema-diff.mjs +154 -0
- package/dist/env/logger.mjs +16 -1
- package/dist/instrumentation/tracer.mjs +1 -1
- package/dist/oauth2/index.d.mts +2 -2
- package/dist/oauth2/oauth-provider.d.mts +0 -10
- package/dist/oauth2/token-endpoint-auth.d.mts +26 -2
- package/dist/oauth2/token-endpoint-auth.mjs +11 -0
- package/dist/social-providers/apple.d.mts +0 -1
- package/dist/social-providers/apple.mjs +0 -1
- package/dist/social-providers/cloudflare.d.mts +132 -0
- package/dist/social-providers/cloudflare.mjs +85 -0
- package/dist/social-providers/cognito.d.mts +0 -1
- package/dist/social-providers/cognito.mjs +0 -1
- package/dist/social-providers/facebook.d.mts +0 -1
- package/dist/social-providers/facebook.mjs +0 -1
- package/dist/social-providers/google.d.mts +0 -1
- package/dist/social-providers/google.mjs +0 -1
- package/dist/social-providers/index.d.mts +53 -21
- package/dist/social-providers/index.mjs +3 -1
- package/dist/social-providers/line.d.mts +0 -1
- package/dist/social-providers/line.mjs +0 -1
- package/dist/social-providers/microsoft-entra-id.d.mts +0 -3
- package/dist/social-providers/microsoft-entra-id.mjs +0 -1
- package/dist/social-providers/paybin.d.mts +0 -1
- package/dist/social-providers/paybin.mjs +0 -1
- package/dist/social-providers/paypal.d.mts +3 -11
- package/dist/social-providers/paypal.mjs +20 -47
- package/dist/social-providers/reddit.mjs +22 -23
- package/dist/social-providers/roblox.mjs +5 -1
- package/dist/social-providers/tiktok.d.mts +1 -0
- package/dist/social-providers/tiktok.mjs +19 -10
- package/dist/social-providers/twitter.mjs +5 -1
- package/dist/social-providers/wechat.mjs +6 -1
- package/dist/types/context.d.mts +11 -0
- package/dist/types/init-options.d.mts +11 -0
- package/dist/utils/ip.mjs +11 -9
- package/dist/utils/url.d.mts +10 -1
- package/dist/utils/url.mjs +21 -1
- package/package.json +3 -3
- package/src/context/endpoint-context.ts +46 -21
- package/src/context/global.ts +7 -0
- package/src/context/index.ts +2 -0
- package/src/context/transaction.ts +5 -0
- package/src/db/adapter/atomic-fallback.ts +237 -0
- package/src/db/adapter/factory.ts +33 -17
- package/src/db/adapter/index.ts +15 -11
- package/src/db/get-tables.ts +1 -14
- package/src/db/index.ts +0 -2
- package/src/db/internal.ts +19 -0
- package/src/db/schema/account.ts +3 -22
- package/src/db/schema/user.ts +1 -1
- package/src/db/schema-check.ts +107 -0
- package/src/db/schema-diff.ts +270 -0
- package/src/env/logger.ts +22 -1
- package/src/oauth2/index.ts +2 -0
- package/src/oauth2/oauth-provider.ts +0 -10
- package/src/oauth2/token-endpoint-auth.ts +39 -6
- package/src/social-providers/apple.ts +0 -1
- package/src/social-providers/cloudflare.ts +221 -0
- package/src/social-providers/cognito.ts +0 -1
- package/src/social-providers/facebook.ts +0 -1
- package/src/social-providers/google.ts +0 -1
- package/src/social-providers/index.ts +3 -0
- package/src/social-providers/line.ts +0 -1
- package/src/social-providers/microsoft-entra-id.ts +0 -1
- package/src/social-providers/paybin.ts +0 -1
- package/src/social-providers/paypal.ts +30 -71
- package/src/social-providers/reddit.ts +34 -37
- package/src/social-providers/roblox.ts +5 -3
- package/src/social-providers/tiktok.ts +25 -14
- package/src/social-providers/twitter.ts +8 -2
- package/src/social-providers/wechat.ts +6 -6
- package/src/types/context.ts +11 -0
- package/src/types/init-options.ts +11 -0
- package/src/utils/ip.ts +13 -9
- package/src/utils/url.ts +43 -0
|
@@ -251,16 +251,6 @@ export interface OAuthProvider<
|
|
|
251
251
|
* against this value to prevent authorization server mix-up attacks.
|
|
252
252
|
*/
|
|
253
253
|
issuer?: string | undefined;
|
|
254
|
-
/**
|
|
255
|
-
* Stable issuer used with the provider subject to recognize an account.
|
|
256
|
-
*
|
|
257
|
-
* Use the validated OpenID Connect issuer for OIDC providers. A resolver is
|
|
258
|
-
* supported for tenant-specific issuers and receives only provider-verified
|
|
259
|
-
* data. OAuth providers without an issuer omit this property and are scoped
|
|
260
|
-
* to the synthetic `local:oauth:<encoded providerId>` issuer, where the
|
|
261
|
-
* provider ID segment is percent-encoded.
|
|
262
|
-
*/
|
|
263
|
-
accountIssuer?: string | OAuthAccountKeyResolver<T, string> | undefined;
|
|
264
254
|
/**
|
|
265
255
|
* Require shared OAuth redirect routes to bind ID-token verification to an
|
|
266
256
|
* authorization request nonce. When true, routes generate `idTokenNonce`,
|
|
@@ -19,23 +19,44 @@ export type TokenEndpointAuth =
|
|
|
19
19
|
| {
|
|
20
20
|
method: "private_key_jwt";
|
|
21
21
|
getClientAssertion: ClientAssertionGetter;
|
|
22
|
+
}
|
|
23
|
+
| {
|
|
24
|
+
method: "custom";
|
|
25
|
+
/**
|
|
26
|
+
* Customize the token request after standard grant parameters are set.
|
|
27
|
+
*/
|
|
28
|
+
customizeRequest: TokenEndpointRequestHook;
|
|
22
29
|
};
|
|
23
30
|
|
|
24
31
|
export type TokenEndpointAuthMethod = TokenEndpointAuth["method"];
|
|
25
32
|
|
|
26
33
|
export type TokenEndpointSecretAuthentication = "basic" | "post";
|
|
27
34
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
export interface ApplyTokenEndpointAuthInput {
|
|
35
|
+
/**
|
|
36
|
+
* Mutable token request state passed to a custom authentication strategy.
|
|
37
|
+
*/
|
|
38
|
+
export interface TokenEndpointRequestContext {
|
|
34
39
|
body: URLSearchParams;
|
|
35
40
|
headers: Record<string, string>;
|
|
36
41
|
options: TokenEndpointClientOptions;
|
|
37
42
|
tokenEndpoint: string;
|
|
38
43
|
grantType: ClientAssertionGrantType;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Applies provider-specific authentication to a token request.
|
|
48
|
+
*/
|
|
49
|
+
export type TokenEndpointRequestHook = (
|
|
50
|
+
context: TokenEndpointRequestContext,
|
|
51
|
+
) => void | Promise<void>;
|
|
52
|
+
|
|
53
|
+
export interface TokenEndpointClientOptions {
|
|
54
|
+
clientId?: string | string[] | undefined;
|
|
55
|
+
clientSecret?: string | undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface ApplyTokenEndpointAuthInput
|
|
59
|
+
extends TokenEndpointRequestContext {
|
|
39
60
|
tokenEndpointAuth?: TokenEndpointAuth | undefined;
|
|
40
61
|
authentication?: TokenEndpointSecretAuthentication | undefined;
|
|
41
62
|
}
|
|
@@ -172,6 +193,18 @@ export async function applyTokenEndpointAuth({
|
|
|
172
193
|
const auth =
|
|
173
194
|
tokenEndpointAuth ?? getDefaultTokenEndpointAuth(options, authentication);
|
|
174
195
|
|
|
196
|
+
if (auth.method === "custom") {
|
|
197
|
+
await auth.customizeRequest({
|
|
198
|
+
body,
|
|
199
|
+
headers,
|
|
200
|
+
options,
|
|
201
|
+
tokenEndpoint,
|
|
202
|
+
grantType,
|
|
203
|
+
});
|
|
204
|
+
assertCompleteManualClientAssertion(body);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
175
208
|
if (auth.method === "private_key_jwt") {
|
|
176
209
|
assertNoClientSecret(auth.method, options, body);
|
|
177
210
|
assertClientIdConfigured(auth.method, clientId);
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { betterFetch } from "@better-fetch/fetch";
|
|
2
|
+
import { logger } from "../env";
|
|
3
|
+
import type {
|
|
4
|
+
OAuthProvider,
|
|
5
|
+
ProviderOptions,
|
|
6
|
+
TokenEndpointAuth,
|
|
7
|
+
} from "../oauth2";
|
|
8
|
+
import {
|
|
9
|
+
createAuthorizationURL,
|
|
10
|
+
refreshAccessToken,
|
|
11
|
+
validateAuthorizationCode,
|
|
12
|
+
} from "../oauth2";
|
|
13
|
+
|
|
14
|
+
const authorizationEndpoint = "https://dash.cloudflare.com/oauth2/auth";
|
|
15
|
+
const tokenEndpoint = "https://dash.cloudflare.com/oauth2/token";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Cloudflare's OIDC `userinfo` endpoint only returns the `sub` claim, so it
|
|
19
|
+
* cannot be used to build a user. The user's profile (email, name, ...) is
|
|
20
|
+
* read from the Cloudflare API `/user` endpoint instead, which the access
|
|
21
|
+
* token can call when the `user-details.read` scope is granted.
|
|
22
|
+
*/
|
|
23
|
+
const userEndpoint = "https://api.cloudflare.com/client/v4/user";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The user profile returned by the Cloudflare API `/user` endpoint.
|
|
27
|
+
*
|
|
28
|
+
* @see https://developers.cloudflare.com/api/resources/user/methods/get/
|
|
29
|
+
*/
|
|
30
|
+
export interface CloudflareProfile {
|
|
31
|
+
/**
|
|
32
|
+
* Identifier of the user.
|
|
33
|
+
*/
|
|
34
|
+
id: string;
|
|
35
|
+
/**
|
|
36
|
+
* Current email address of the user.
|
|
37
|
+
*/
|
|
38
|
+
email: string;
|
|
39
|
+
/**
|
|
40
|
+
* The user's first name.
|
|
41
|
+
*/
|
|
42
|
+
first_name?: string | null | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* The user's last name.
|
|
45
|
+
*/
|
|
46
|
+
last_name?: string | null | undefined;
|
|
47
|
+
/**
|
|
48
|
+
* The country in which the user lives.
|
|
49
|
+
*/
|
|
50
|
+
country?: string | null | undefined;
|
|
51
|
+
/**
|
|
52
|
+
* The user's telephone number.
|
|
53
|
+
*/
|
|
54
|
+
telephone?: string | null | undefined;
|
|
55
|
+
/**
|
|
56
|
+
* The zipcode or postal code where the user lives.
|
|
57
|
+
*/
|
|
58
|
+
zipcode?: string | null | undefined;
|
|
59
|
+
/**
|
|
60
|
+
* Indicates whether two-factor authentication is enabled for the user account.
|
|
61
|
+
*/
|
|
62
|
+
two_factor_authentication_enabled?: boolean | undefined;
|
|
63
|
+
/**
|
|
64
|
+
* Indicates whether the user has been suspended.
|
|
65
|
+
*/
|
|
66
|
+
suspended?: boolean | undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The standard Cloudflare API response envelope for the `/user` endpoint.
|
|
71
|
+
*/
|
|
72
|
+
interface CloudflareUserResponse {
|
|
73
|
+
success: boolean;
|
|
74
|
+
errors: { code: number; message: string }[];
|
|
75
|
+
result: CloudflareProfile | null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Token endpoint authentication supported by Cloudflare OAuth clients.
|
|
80
|
+
*
|
|
81
|
+
* @see https://developers.cloudflare.com/fundamentals/oauth/create-an-oauth-client/#choose-a-flow
|
|
82
|
+
*/
|
|
83
|
+
type CloudflareClientAuthentication =
|
|
84
|
+
| {
|
|
85
|
+
/**
|
|
86
|
+
* The client secret of a confidential Cloudflare OAuth client.
|
|
87
|
+
*/
|
|
88
|
+
clientSecret: string;
|
|
89
|
+
/**
|
|
90
|
+
* The authentication method configured for the token endpoint.
|
|
91
|
+
*
|
|
92
|
+
* @default "client_secret_basic"
|
|
93
|
+
*/
|
|
94
|
+
tokenEndpointAuthMethod?:
|
|
95
|
+
| "client_secret_basic"
|
|
96
|
+
| "client_secret_post"
|
|
97
|
+
| undefined;
|
|
98
|
+
}
|
|
99
|
+
| {
|
|
100
|
+
/**
|
|
101
|
+
* Clients that use PKCE do not have a client secret.
|
|
102
|
+
*/
|
|
103
|
+
clientSecret?: undefined;
|
|
104
|
+
/**
|
|
105
|
+
* Clients without a secret do not authenticate at the token endpoint.
|
|
106
|
+
*
|
|
107
|
+
* @default "none"
|
|
108
|
+
*/
|
|
109
|
+
tokenEndpointAuthMethod?: "none" | undefined;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
interface CloudflareBaseOptions extends ProviderOptions<CloudflareProfile> {
|
|
113
|
+
/**
|
|
114
|
+
* The client ID of the Cloudflare OAuth client.
|
|
115
|
+
*/
|
|
116
|
+
clientId: string;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Options for configuring the Cloudflare social provider.
|
|
121
|
+
*/
|
|
122
|
+
export type CloudflareOptions = CloudflareBaseOptions &
|
|
123
|
+
CloudflareClientAuthentication;
|
|
124
|
+
|
|
125
|
+
const getTokenEndpointAuth = (
|
|
126
|
+
options: CloudflareOptions,
|
|
127
|
+
): TokenEndpointAuth => {
|
|
128
|
+
const defaultMethod = options.clientSecret ? "client_secret_basic" : "none";
|
|
129
|
+
const method = options.tokenEndpointAuthMethod ?? defaultMethod;
|
|
130
|
+
|
|
131
|
+
return { method };
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export const cloudflare = (options: CloudflareOptions) => {
|
|
135
|
+
return {
|
|
136
|
+
id: "cloudflare",
|
|
137
|
+
name: "Cloudflare",
|
|
138
|
+
accountSubject: ({ profile }) => profile.id,
|
|
139
|
+
createAuthorizationURL({ state, scopes, codeVerifier, redirectURI }) {
|
|
140
|
+
const _scopes = options.disableDefaultScope ? [] : ["user-details.read"];
|
|
141
|
+
|
|
142
|
+
if (options.scope?.length) {
|
|
143
|
+
_scopes.push(...options.scope);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (scopes?.length) {
|
|
147
|
+
_scopes.push(...scopes);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return createAuthorizationURL({
|
|
151
|
+
id: "cloudflare",
|
|
152
|
+
options,
|
|
153
|
+
authorizationEndpoint,
|
|
154
|
+
scopes: _scopes.length ? [...new Set(_scopes)] : undefined,
|
|
155
|
+
state,
|
|
156
|
+
codeVerifier,
|
|
157
|
+
redirectURI,
|
|
158
|
+
});
|
|
159
|
+
},
|
|
160
|
+
validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
|
|
161
|
+
return validateAuthorizationCode({
|
|
162
|
+
code,
|
|
163
|
+
codeVerifier,
|
|
164
|
+
redirectURI,
|
|
165
|
+
options,
|
|
166
|
+
tokenEndpoint,
|
|
167
|
+
tokenEndpointAuth: getTokenEndpointAuth(options),
|
|
168
|
+
});
|
|
169
|
+
},
|
|
170
|
+
refreshAccessToken: options.refreshAccessToken
|
|
171
|
+
? options.refreshAccessToken
|
|
172
|
+
: async (refreshToken) => {
|
|
173
|
+
return refreshAccessToken({
|
|
174
|
+
refreshToken,
|
|
175
|
+
options: {
|
|
176
|
+
clientId: options.clientId,
|
|
177
|
+
clientKey: options.clientKey,
|
|
178
|
+
clientSecret: options.clientSecret,
|
|
179
|
+
},
|
|
180
|
+
tokenEndpoint,
|
|
181
|
+
tokenEndpointAuth: getTokenEndpointAuth(options),
|
|
182
|
+
});
|
|
183
|
+
},
|
|
184
|
+
async getUserInfo(token) {
|
|
185
|
+
if (options.getUserInfo) {
|
|
186
|
+
return options.getUserInfo(token);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const { data, error } = await betterFetch<CloudflareUserResponse>(
|
|
190
|
+
userEndpoint,
|
|
191
|
+
{ headers: { authorization: `Bearer ${token.accessToken}` } },
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
if (error || !data?.success || !data.result) {
|
|
195
|
+
logger.error(
|
|
196
|
+
"Failed to fetch user info from Cloudflare:",
|
|
197
|
+
error ?? data?.errors,
|
|
198
|
+
);
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const profile = data.result;
|
|
203
|
+
const name =
|
|
204
|
+
[profile.first_name, profile.last_name].filter(Boolean).join(" ") ||
|
|
205
|
+
profile.email;
|
|
206
|
+
const userMap = await options.mapProfileToUser?.(profile);
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
user: {
|
|
210
|
+
name,
|
|
211
|
+
email: profile.email,
|
|
212
|
+
// Cloudflare does not expose email verification status
|
|
213
|
+
emailVerified: false,
|
|
214
|
+
...userMap,
|
|
215
|
+
},
|
|
216
|
+
data: profile,
|
|
217
|
+
};
|
|
218
|
+
},
|
|
219
|
+
options,
|
|
220
|
+
} satisfies OAuthProvider<CloudflareProfile>;
|
|
221
|
+
};
|
|
@@ -74,7 +74,6 @@ export const cognito = (options: CognitoOptions) => {
|
|
|
74
74
|
id: "cognito",
|
|
75
75
|
name: "Cognito",
|
|
76
76
|
accountSubject: ({ profile }) => profile.sub,
|
|
77
|
-
accountIssuer: `https://cognito-idp.${options.region}.amazonaws.com/${options.userPoolId}`,
|
|
78
77
|
async createAuthorizationURL({
|
|
79
78
|
state,
|
|
80
79
|
scopes,
|
|
@@ -108,7 +108,6 @@ export const facebook = (options: FacebookOptions) => {
|
|
|
108
108
|
name: "Facebook",
|
|
109
109
|
accountSubject: ({ profile }) =>
|
|
110
110
|
"sub" in profile ? profile.sub : profile.id,
|
|
111
|
-
accountIssuer: "https://www.facebook.com",
|
|
112
111
|
async createAuthorizationURL({
|
|
113
112
|
state,
|
|
114
113
|
scopes,
|
|
@@ -2,6 +2,7 @@ import * as z from "zod";
|
|
|
2
2
|
import type { AwaitableFunction } from "../types";
|
|
3
3
|
import { apple } from "./apple";
|
|
4
4
|
import { atlassian } from "./atlassian";
|
|
5
|
+
import { cloudflare } from "./cloudflare";
|
|
5
6
|
import { cognito } from "./cognito";
|
|
6
7
|
import { discord } from "./discord";
|
|
7
8
|
import { dropbox } from "./dropbox";
|
|
@@ -39,6 +40,7 @@ import { zoom } from "./zoom";
|
|
|
39
40
|
export const socialProviders = {
|
|
40
41
|
apple,
|
|
41
42
|
atlassian,
|
|
43
|
+
cloudflare,
|
|
42
44
|
cognito,
|
|
43
45
|
discord,
|
|
44
46
|
facebook,
|
|
@@ -95,6 +97,7 @@ export type SocialProviders = {
|
|
|
95
97
|
|
|
96
98
|
export * from "./apple";
|
|
97
99
|
export * from "./atlassian";
|
|
100
|
+
export * from "./cloudflare";
|
|
98
101
|
export * from "./cognito";
|
|
99
102
|
export * from "./discord";
|
|
100
103
|
export * from "./dropbox";
|
|
@@ -188,7 +188,6 @@ export const microsoft = (options: MicrosoftOptions) => {
|
|
|
188
188
|
id: "microsoft",
|
|
189
189
|
name: "Microsoft EntraID",
|
|
190
190
|
accountSubject: ({ profile }) => profile.oid,
|
|
191
|
-
accountIssuer: ({ profile }) => profile.iss,
|
|
192
191
|
createAuthorizationURL(data) {
|
|
193
192
|
// Microsoft Entra supports public clients (SPA / native apps with
|
|
194
193
|
// PKCE only), so clientSecret is intentionally not required here.
|
|
@@ -1,10 +1,17 @@
|
|
|
1
|
-
import { base64 } from "@better-auth/utils/base64";
|
|
2
1
|
import { betterFetch } from "@better-fetch/fetch";
|
|
3
2
|
import { decodeJwt } from "jose";
|
|
4
3
|
import { logger } from "../env";
|
|
5
4
|
import { BetterAuthError } from "../error";
|
|
6
|
-
import type {
|
|
7
|
-
|
|
5
|
+
import type {
|
|
6
|
+
OAuthProvider,
|
|
7
|
+
ProviderOptions,
|
|
8
|
+
TokenEndpointAuth,
|
|
9
|
+
} from "../oauth2";
|
|
10
|
+
import {
|
|
11
|
+
createAuthorizationURL,
|
|
12
|
+
refreshAccessToken,
|
|
13
|
+
validateAuthorizationCode,
|
|
14
|
+
} from "../oauth2";
|
|
8
15
|
|
|
9
16
|
export interface PayPalProfile {
|
|
10
17
|
sub?: string | undefined;
|
|
@@ -75,6 +82,13 @@ export const paypal = (options: PayPalOptions) => {
|
|
|
75
82
|
const userInfoEndpoint = isSandbox
|
|
76
83
|
? "https://api-m.sandbox.paypal.com/v1/identity/oauth2/userinfo"
|
|
77
84
|
: "https://api-m.paypal.com/v1/identity/oauth2/userinfo";
|
|
85
|
+
const tokenRequestOptions = {
|
|
86
|
+
clientId: options.clientId,
|
|
87
|
+
clientSecret: options.clientSecret,
|
|
88
|
+
};
|
|
89
|
+
const tokenEndpointAuth = {
|
|
90
|
+
method: "client_secret_basic",
|
|
91
|
+
} satisfies TokenEndpointAuth;
|
|
78
92
|
|
|
79
93
|
return {
|
|
80
94
|
id: "paypal",
|
|
@@ -115,47 +129,16 @@ export const paypal = (options: PayPalOptions) => {
|
|
|
115
129
|
return url;
|
|
116
130
|
},
|
|
117
131
|
|
|
118
|
-
validateAuthorizationCode: async ({ code, redirectURI }) => {
|
|
119
|
-
/**
|
|
120
|
-
* PayPal requires Basic Auth for token exchange
|
|
121
|
-
**/
|
|
122
|
-
|
|
123
|
-
const credentials = base64.encode(
|
|
124
|
-
`${options.clientId}:${options.clientSecret}`,
|
|
125
|
-
);
|
|
126
|
-
|
|
132
|
+
validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
|
|
127
133
|
try {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
},
|
|
136
|
-
body: new URLSearchParams({
|
|
137
|
-
grant_type: "authorization_code",
|
|
138
|
-
code: code,
|
|
139
|
-
redirect_uri: redirectURI,
|
|
140
|
-
}).toString(),
|
|
134
|
+
return await validateAuthorizationCode({
|
|
135
|
+
code,
|
|
136
|
+
codeVerifier,
|
|
137
|
+
redirectURI: options.redirectURI || redirectURI,
|
|
138
|
+
options: tokenRequestOptions,
|
|
139
|
+
tokenEndpoint,
|
|
140
|
+
tokenEndpointAuth,
|
|
141
141
|
});
|
|
142
|
-
|
|
143
|
-
if (!response.data) {
|
|
144
|
-
throw new BetterAuthError("FAILED_TO_GET_ACCESS_TOKEN");
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
const data = response.data as PayPalTokenResponse;
|
|
148
|
-
|
|
149
|
-
const result = {
|
|
150
|
-
accessToken: data.access_token,
|
|
151
|
-
refreshToken: data.refresh_token,
|
|
152
|
-
accessTokenExpiresAt: data.expires_in
|
|
153
|
-
? new Date(Date.now() + data.expires_in * 1000)
|
|
154
|
-
: undefined,
|
|
155
|
-
idToken: data.id_token,
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
return result;
|
|
159
142
|
} catch (error) {
|
|
160
143
|
logger.error("PayPal token exchange failed:", error);
|
|
161
144
|
throw new BetterAuthError("FAILED_TO_GET_ACCESS_TOKEN");
|
|
@@ -165,37 +148,13 @@ export const paypal = (options: PayPalOptions) => {
|
|
|
165
148
|
refreshAccessToken: options.refreshAccessToken
|
|
166
149
|
? options.refreshAccessToken
|
|
167
150
|
: async (refreshToken) => {
|
|
168
|
-
const credentials = base64.encode(
|
|
169
|
-
`${options.clientId}:${options.clientSecret}`,
|
|
170
|
-
);
|
|
171
|
-
|
|
172
151
|
try {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
"Accept-Language": "en_US",
|
|
179
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
180
|
-
},
|
|
181
|
-
body: new URLSearchParams({
|
|
182
|
-
grant_type: "refresh_token",
|
|
183
|
-
refresh_token: refreshToken,
|
|
184
|
-
}).toString(),
|
|
152
|
+
return await refreshAccessToken({
|
|
153
|
+
refreshToken,
|
|
154
|
+
options: tokenRequestOptions,
|
|
155
|
+
tokenEndpoint,
|
|
156
|
+
tokenEndpointAuth,
|
|
185
157
|
});
|
|
186
|
-
|
|
187
|
-
if (!response.data) {
|
|
188
|
-
throw new BetterAuthError("FAILED_TO_REFRESH_ACCESS_TOKEN");
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const data = response.data as any;
|
|
192
|
-
return {
|
|
193
|
-
accessToken: data.access_token,
|
|
194
|
-
refreshToken: data.refresh_token,
|
|
195
|
-
accessTokenExpiresAt: data.expires_in
|
|
196
|
-
? new Date(Date.now() + data.expires_in * 1000)
|
|
197
|
-
: undefined,
|
|
198
|
-
};
|
|
199
158
|
} catch (error) {
|
|
200
159
|
logger.error("PayPal token refresh failed:", error);
|
|
201
160
|
throw new BetterAuthError("FAILED_TO_REFRESH_ACCESS_TOKEN");
|
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
import { base64 } from "@better-auth/utils/base64";
|
|
2
1
|
import { betterFetch } from "@better-fetch/fetch";
|
|
3
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
OAuthProvider,
|
|
4
|
+
ProviderOptions,
|
|
5
|
+
TokenEndpointAuth,
|
|
6
|
+
} from "../oauth2";
|
|
4
7
|
import {
|
|
5
8
|
createAuthorizationURL,
|
|
6
|
-
getOAuth2Tokens,
|
|
7
9
|
refreshAccessToken,
|
|
10
|
+
validateAuthorizationCode,
|
|
8
11
|
} from "../oauth2";
|
|
12
|
+
import { createPlaceholderEmail } from "../utils/email";
|
|
9
13
|
|
|
10
14
|
export interface RedditProfile {
|
|
11
15
|
id: string;
|
|
@@ -22,6 +26,15 @@ export interface RedditOptions extends ProviderOptions<RedditProfile> {
|
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
export const reddit = (options: RedditOptions) => {
|
|
29
|
+
const tokenEndpoint = "https://www.reddit.com/api/v1/access_token";
|
|
30
|
+
const tokenRequestOptions = {
|
|
31
|
+
clientId: options.clientId,
|
|
32
|
+
clientSecret: options.clientSecret,
|
|
33
|
+
};
|
|
34
|
+
const tokenEndpointAuth = {
|
|
35
|
+
method: "client_secret_basic",
|
|
36
|
+
} satisfies TokenEndpointAuth;
|
|
37
|
+
|
|
25
38
|
return {
|
|
26
39
|
id: "reddit",
|
|
27
40
|
name: "Reddit",
|
|
@@ -42,34 +55,17 @@ export const reddit = (options: RedditOptions) => {
|
|
|
42
55
|
});
|
|
43
56
|
},
|
|
44
57
|
validateAuthorizationCode: async ({ code, redirectURI }) => {
|
|
45
|
-
|
|
46
|
-
grant_type: "authorization_code",
|
|
58
|
+
return validateAuthorizationCode({
|
|
47
59
|
code,
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
`${options.clientId}:${options.clientSecret}`,
|
|
56
|
-
)}`,
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
const { data, error } = await betterFetch<object>(
|
|
60
|
-
"https://www.reddit.com/api/v1/access_token",
|
|
61
|
-
{
|
|
62
|
-
method: "POST",
|
|
63
|
-
headers,
|
|
64
|
-
body: body.toString(),
|
|
60
|
+
redirectURI: options.redirectURI || redirectURI,
|
|
61
|
+
options: tokenRequestOptions,
|
|
62
|
+
tokenEndpoint,
|
|
63
|
+
tokenEndpointAuth,
|
|
64
|
+
headers: {
|
|
65
|
+
accept: "text/plain",
|
|
66
|
+
"user-agent": "better-auth",
|
|
65
67
|
},
|
|
66
|
-
);
|
|
67
|
-
|
|
68
|
-
if (error) {
|
|
69
|
-
throw error;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return getOAuth2Tokens(data);
|
|
68
|
+
});
|
|
73
69
|
},
|
|
74
70
|
|
|
75
71
|
refreshAccessToken: options.refreshAccessToken
|
|
@@ -77,13 +73,9 @@ export const reddit = (options: RedditOptions) => {
|
|
|
77
73
|
: async (refreshToken) => {
|
|
78
74
|
return refreshAccessToken({
|
|
79
75
|
refreshToken,
|
|
80
|
-
options:
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
clientSecret: options.clientSecret,
|
|
84
|
-
},
|
|
85
|
-
authentication: "basic",
|
|
86
|
-
tokenEndpoint: "https://www.reddit.com/api/v1/access_token",
|
|
76
|
+
options: tokenRequestOptions,
|
|
77
|
+
tokenEndpoint,
|
|
78
|
+
tokenEndpointAuth,
|
|
87
79
|
});
|
|
88
80
|
},
|
|
89
81
|
async getUserInfo(token) {
|
|
@@ -110,7 +102,12 @@ export const reddit = (options: RedditOptions) => {
|
|
|
110
102
|
// non-routable placeholder (RFC 2606 `.invalid`) keyed to the user's
|
|
111
103
|
// Reddit id rather than the routable `reddit.com`, which could collide
|
|
112
104
|
// with a real address. Left unverified; `mapProfileToUser` can override.
|
|
113
|
-
const email =
|
|
105
|
+
const email =
|
|
106
|
+
userMap?.email ||
|
|
107
|
+
createPlaceholderEmail({
|
|
108
|
+
identifier: profile.id,
|
|
109
|
+
namespace: "reddit",
|
|
110
|
+
});
|
|
114
111
|
return {
|
|
115
112
|
user: {
|
|
116
113
|
name: profile.name,
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
refreshAccessToken,
|
|
6
6
|
validateAuthorizationCode,
|
|
7
7
|
} from "../oauth2";
|
|
8
|
+
import { createPlaceholderEmail } from "../utils/email";
|
|
8
9
|
|
|
9
10
|
export interface RobloxProfile extends Record<string, any> {
|
|
10
11
|
/** the user's id */
|
|
@@ -97,13 +98,14 @@ export const roblox = (options: RobloxOptions) => {
|
|
|
97
98
|
}
|
|
98
99
|
|
|
99
100
|
const userMap = await options.mapProfileToUser?.(profile);
|
|
100
|
-
// Roblox does not provide email or email_verified claim.
|
|
101
|
-
// We default to false for security consistency.
|
|
102
101
|
return {
|
|
103
102
|
user: {
|
|
104
103
|
name: profile.nickname || profile.preferred_username || "",
|
|
105
104
|
image: profile.picture,
|
|
106
|
-
email:
|
|
105
|
+
email: createPlaceholderEmail({
|
|
106
|
+
identifier: profile.sub,
|
|
107
|
+
namespace: "roblox",
|
|
108
|
+
}),
|
|
107
109
|
emailVerified: false,
|
|
108
110
|
...userMap,
|
|
109
111
|
},
|