better-auth 1.8.0-beta.1 → 1.8.0-beta.2
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/rate-limiter/index.mjs +1 -1
- package/dist/api/routes/callback.mjs +10 -51
- package/dist/api/routes/session.mjs +1 -1
- package/dist/db/get-migration.mjs +1 -0
- package/dist/oauth2/callback.mjs +68 -0
- package/dist/package.mjs +1 -1
- package/dist/plugins/jwt/adapter.mjs +14 -3
- package/dist/plugins/jwt/cookie-cache.mjs +3 -8
- package/dist/plugins/jwt/index.mjs +2 -2
- package/dist/plugins/jwt/sign.mjs +6 -5
- package/dist/plugins/jwt/verify.mjs +10 -4
- package/dist/plugins/oauth-proxy/index.mjs +14 -41
- package/package.json +9 -9
|
@@ -88,7 +88,7 @@ function createDatabaseStorageWrapper(ctx) {
|
|
|
88
88
|
value: key
|
|
89
89
|
}]
|
|
90
90
|
}))[0];
|
|
91
|
-
if (
|
|
91
|
+
if (data) data.lastRequest = Number(data.lastRequest);
|
|
92
92
|
return data;
|
|
93
93
|
};
|
|
94
94
|
const consume = async (key, rule) => {
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { isAPIError } from "../../utils/is-api-error.mjs";
|
|
2
2
|
import { setSessionCookie } from "../../cookies/index.mjs";
|
|
3
3
|
import { getAwaitableValue } from "../../context/helpers.mjs";
|
|
4
|
-
import { resolveOAuthAccountKey, toOAuthProfileRecord } from "../../oauth2/account-key.mjs";
|
|
5
4
|
import { OAUTH_CALLBACK_ERROR_CODES, missingEmailLogMessage } from "../../oauth2/errors.mjs";
|
|
6
5
|
import { getOAuthCallbackPath } from "../../oauth2/utils.mjs";
|
|
7
6
|
import { handleOAuthUserInfo, linkOAuthAccount } from "../../oauth2/link-account.mjs";
|
|
8
7
|
import { generateIdTokenNonce, generateState, parseState } from "../../oauth2/state.mjs";
|
|
8
|
+
import { exchangeOAuthCallback } from "../../oauth2/callback.mjs";
|
|
9
9
|
import { HIDE_METADATA } from "../../utils/hide-metadata.mjs";
|
|
10
|
-
import { safeJSONParse } from "@better-auth/core/utils/json";
|
|
11
10
|
import { appendQueryParams } from "@better-auth/core/utils/url";
|
|
12
11
|
import { createAuthEndpoint } from "@better-auth/core/api";
|
|
13
12
|
import * as z from "zod";
|
|
@@ -92,56 +91,16 @@ const callbackOAuth = createAuthEndpoint("/callback/:id", {
|
|
|
92
91
|
c.context.logger.warn("OAuth provider not found", { providerId: c.params.id });
|
|
93
92
|
throw redirectOnError(OAUTH_CALLBACK_ERROR_CODES.PROVIDER_NOT_FOUND);
|
|
94
93
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (provider.requiresIdTokenNonce && !idTokenNonce) {
|
|
103
|
-
c.context.logger.error("OAuth id_token nonce binding required but no expected nonce was found in state", { providerId: provider.id });
|
|
104
|
-
throw redirectOnError(OAUTH_CALLBACK_ERROR_CODES.NONCE_BINDING_MISSING);
|
|
105
|
-
}
|
|
106
|
-
let tokens;
|
|
107
|
-
try {
|
|
108
|
-
tokens = await provider.validateAuthorizationCode({
|
|
109
|
-
code,
|
|
110
|
-
codeVerifier,
|
|
111
|
-
deviceId: device_id,
|
|
112
|
-
redirectURI: `${c.context.baseURL}${getOAuthCallbackPath(provider)}`
|
|
113
|
-
});
|
|
114
|
-
} catch (e) {
|
|
115
|
-
c.context.logger.error("", e);
|
|
116
|
-
throw redirectOnError(OAUTH_CALLBACK_ERROR_CODES.INVALID_CODE);
|
|
117
|
-
}
|
|
118
|
-
if (!tokens) throw redirectOnError(OAUTH_CALLBACK_ERROR_CODES.INVALID_CODE);
|
|
119
|
-
const parsedUserData = userData ? safeJSONParse(userData) : null;
|
|
120
|
-
const providerResult = await provider.getUserInfo({
|
|
121
|
-
...tokens,
|
|
122
|
-
...idTokenNonce ? { expectedIdTokenNonce: idTokenNonce } : {},
|
|
123
|
-
/**
|
|
124
|
-
* The user object from the provider
|
|
125
|
-
* This is only available for some providers like Apple
|
|
126
|
-
*/
|
|
127
|
-
user: parsedUserData ?? void 0
|
|
94
|
+
const exchanged = await exchangeOAuthCallback(c, provider, {
|
|
95
|
+
code,
|
|
96
|
+
codeVerifier,
|
|
97
|
+
iss,
|
|
98
|
+
deviceId: device_id,
|
|
99
|
+
user: userData,
|
|
100
|
+
idTokenNonce
|
|
128
101
|
});
|
|
129
|
-
if (
|
|
130
|
-
|
|
131
|
-
return redirectOnError(OAUTH_CALLBACK_ERROR_CODES.UNABLE_TO_GET_USER_INFO);
|
|
132
|
-
}
|
|
133
|
-
const userInfo = providerResult.user;
|
|
134
|
-
const providerProfile = toOAuthProfileRecord(providerResult.data);
|
|
135
|
-
let accountKey;
|
|
136
|
-
try {
|
|
137
|
-
accountKey = await resolveOAuthAccountKey(provider, tokens, providerResult.data);
|
|
138
|
-
} catch (error) {
|
|
139
|
-
c.context.logger.error("Unable to derive provider account identity", {
|
|
140
|
-
providerId: provider.id,
|
|
141
|
-
error
|
|
142
|
-
});
|
|
143
|
-
return redirectOnError(OAUTH_CALLBACK_ERROR_CODES.UNABLE_TO_GET_USER_INFO);
|
|
144
|
-
}
|
|
102
|
+
if (exchanged.error) throw redirectOnError(exchanged.error);
|
|
103
|
+
const { tokens, userInfo, providerProfile, accountKey } = exchanged;
|
|
145
104
|
const { accountId } = accountKey;
|
|
146
105
|
if (!callbackURL) {
|
|
147
106
|
c.context.logger.error("No callback URL found");
|
|
@@ -229,7 +229,7 @@ const getSession = () => createAuthEndpoint("/get-session", {
|
|
|
229
229
|
} catch (error) {
|
|
230
230
|
if (isAPIError(error)) throw error;
|
|
231
231
|
ctx.context.logger.error("INTERNAL_SERVER_ERROR", error);
|
|
232
|
-
throw APIError.from("INTERNAL_SERVER_ERROR", BASE_ERROR_CODES.FAILED_TO_GET_SESSION);
|
|
232
|
+
throw APIError.from("INTERNAL_SERVER_ERROR", BASE_ERROR_CODES.FAILED_TO_GET_SESSION, { cause: error });
|
|
233
233
|
}
|
|
234
234
|
});
|
|
235
235
|
/**
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { resolveOAuthAccountKey, toOAuthProfileRecord } from "./account-key.mjs";
|
|
2
|
+
import { OAUTH_CALLBACK_ERROR_CODES } from "./errors.mjs";
|
|
3
|
+
import { getOAuthCallbackPath } from "./utils.mjs";
|
|
4
|
+
import { safeJSONParse } from "@better-auth/core/utils/json";
|
|
5
|
+
//#region src/oauth2/callback.ts
|
|
6
|
+
/**
|
|
7
|
+
* Turns a provider callback into a verified identity. It checks that the
|
|
8
|
+
* callback answers this authorization request, exchanges the code, and
|
|
9
|
+
* resolves the user and account key.
|
|
10
|
+
*
|
|
11
|
+
* Every route that completes a provider callback goes through here, so the
|
|
12
|
+
* routes run the same checks.
|
|
13
|
+
*/
|
|
14
|
+
async function exchangeOAuthCallback(ctx, provider, callback) {
|
|
15
|
+
if (callback.iss && provider.issuer && callback.iss !== provider.issuer) {
|
|
16
|
+
ctx.context.logger.error("OAuth issuer mismatch", {
|
|
17
|
+
expected: provider.issuer,
|
|
18
|
+
received: callback.iss
|
|
19
|
+
});
|
|
20
|
+
return { error: OAUTH_CALLBACK_ERROR_CODES.ISSUER_MISMATCH };
|
|
21
|
+
}
|
|
22
|
+
if (provider.requiresIdTokenNonce && !callback.idTokenNonce) {
|
|
23
|
+
ctx.context.logger.error("OAuth id_token nonce binding required but no expected nonce was found in state", { providerId: provider.id });
|
|
24
|
+
return { error: OAUTH_CALLBACK_ERROR_CODES.NONCE_BINDING_MISSING };
|
|
25
|
+
}
|
|
26
|
+
let tokens;
|
|
27
|
+
try {
|
|
28
|
+
tokens = await provider.validateAuthorizationCode({
|
|
29
|
+
code: callback.code,
|
|
30
|
+
codeVerifier: callback.codeVerifier,
|
|
31
|
+
deviceId: callback.deviceId,
|
|
32
|
+
redirectURI: `${ctx.context.baseURL}${getOAuthCallbackPath(provider)}`
|
|
33
|
+
});
|
|
34
|
+
} catch (error) {
|
|
35
|
+
ctx.context.logger.error("Failed to validate authorization code", error);
|
|
36
|
+
return { error: OAUTH_CALLBACK_ERROR_CODES.INVALID_CODE };
|
|
37
|
+
}
|
|
38
|
+
if (!tokens) return { error: OAUTH_CALLBACK_ERROR_CODES.INVALID_CODE };
|
|
39
|
+
const result = await provider.getUserInfo({
|
|
40
|
+
...tokens,
|
|
41
|
+
expectedIdTokenNonce: callback.idTokenNonce,
|
|
42
|
+
/**
|
|
43
|
+
* The user object from the provider
|
|
44
|
+
* This is only available for some providers like Apple
|
|
45
|
+
*/
|
|
46
|
+
user: safeJSONParse(callback.user) ?? void 0
|
|
47
|
+
});
|
|
48
|
+
if (!result?.user) {
|
|
49
|
+
ctx.context.logger.error("Unable to get user info");
|
|
50
|
+
return { error: OAUTH_CALLBACK_ERROR_CODES.UNABLE_TO_GET_USER_INFO };
|
|
51
|
+
}
|
|
52
|
+
const accountKey = await resolveOAuthAccountKey(provider, tokens, result.data).catch((error) => {
|
|
53
|
+
ctx.context.logger.error("Unable to derive provider account identity", {
|
|
54
|
+
providerId: provider.id,
|
|
55
|
+
error
|
|
56
|
+
});
|
|
57
|
+
return null;
|
|
58
|
+
});
|
|
59
|
+
if (!accountKey) return { error: OAUTH_CALLBACK_ERROR_CODES.UNABLE_TO_GET_USER_INFO };
|
|
60
|
+
return {
|
|
61
|
+
tokens,
|
|
62
|
+
userInfo: result.user,
|
|
63
|
+
providerProfile: toOAuthProfileRecord(result.data),
|
|
64
|
+
accountKey
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
//#endregion
|
|
68
|
+
export { exchangeOAuthCallback };
|
package/dist/package.mjs
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import { getCurrentAdapter } from "@better-auth/core/context";
|
|
2
2
|
//#region src/plugins/jwt/adapter.ts
|
|
3
|
+
/**
|
|
4
|
+
* Algorithm of the key the plugin generates when `jwks.keyPairConfig` is unset.
|
|
5
|
+
*/
|
|
6
|
+
const DEFAULT_JWK_ALG = "EdDSA";
|
|
7
|
+
/**
|
|
8
|
+
* Algorithm for new keys, and for stored keys saved before the `alg` column
|
|
9
|
+
* existed.
|
|
10
|
+
*/
|
|
11
|
+
function getDefaultKeyAlg(options) {
|
|
12
|
+
return options?.jwks?.keyPairConfig?.alg ?? "EdDSA";
|
|
13
|
+
}
|
|
3
14
|
const getJwksAdapter = (baseAdapter, options) => {
|
|
4
15
|
return {
|
|
5
16
|
getAllKeys: async (ctx) => {
|
|
@@ -38,7 +49,7 @@ const getJwksAdapter = (baseAdapter, options) => {
|
|
|
38
49
|
*
|
|
39
50
|
* Legacy rows persisted before the `alg` column existed have
|
|
40
51
|
* `alg: null`. Per `schema.ts`, those rows are treated as the
|
|
41
|
-
* configured default alg (
|
|
52
|
+
* configured default alg ({@link getDefaultKeyAlg}),
|
|
42
53
|
* so deployments that have only legacy rows can still satisfy an
|
|
43
54
|
* audience-pinned `signingAlgorithm` matching the default.
|
|
44
55
|
*
|
|
@@ -48,7 +59,7 @@ const getJwksAdapter = (baseAdapter, options) => {
|
|
|
48
59
|
getLatestKeyByAlg: async (ctx, alg) => {
|
|
49
60
|
const candidates = options?.adapter?.getJwks ? await options.adapter.getJwks(ctx) : await (await getCurrentAdapter(baseAdapter)).findMany({ model: "jwks" });
|
|
50
61
|
if (!candidates) return void 0;
|
|
51
|
-
const configAlg = options
|
|
62
|
+
const configAlg = getDefaultKeyAlg(options);
|
|
52
63
|
const now = /* @__PURE__ */ new Date();
|
|
53
64
|
return candidates.filter((k) => k.alg === alg || k.alg == null && configAlg === alg).filter((k) => !k.expiresAt || k.expiresAt > now).sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())[0];
|
|
54
65
|
},
|
|
@@ -65,4 +76,4 @@ const getJwksAdapter = (baseAdapter, options) => {
|
|
|
65
76
|
};
|
|
66
77
|
};
|
|
67
78
|
//#endregion
|
|
68
|
-
export { getJwksAdapter };
|
|
79
|
+
export { DEFAULT_JWK_ALG, getDefaultKeyAlg, getJwksAdapter };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SESSION_COOKIE_JWT_AUDIENCE, SESSION_COOKIE_JWT_TYPE, getSessionCookieJwtVerifyOptions, parseSessionCookieJwtPayload } from "../../cookies/jwt.mjs";
|
|
2
|
-
import { getJwksAdapter } from "./adapter.mjs";
|
|
2
|
+
import { getDefaultKeyAlg, getJwksAdapter } from "./adapter.mjs";
|
|
3
3
|
import { resolveSigningKey } from "./sign.mjs";
|
|
4
4
|
import { BetterAuthError } from "@better-auth/core/error";
|
|
5
5
|
import { SignJWT, decodeProtectedHeader, importJWK, jwtVerify } from "jose";
|
|
@@ -9,8 +9,7 @@ function getCookieCacheJwtIssuer(ctx) {
|
|
|
9
9
|
return typeof baseURL === "string" ? baseURL || "better-auth:session-cache" : ctx.context.baseURL || "better-auth:session-cache";
|
|
10
10
|
}
|
|
11
11
|
async function importLocalPublicKey(ctx, token, options) {
|
|
12
|
-
const
|
|
13
|
-
const kid = header.kid;
|
|
12
|
+
const kid = decodeProtectedHeader(token).kid;
|
|
14
13
|
if (!kid) {
|
|
15
14
|
ctx.context.logger.debug("Cookie-cache JWT missing kid in header");
|
|
16
15
|
return null;
|
|
@@ -25,11 +24,7 @@ async function importLocalPublicKey(ctx, token, options) {
|
|
|
25
24
|
ctx.context.logger.debug(`No JWKS key found for cookie-cache JWT kid: ${kid}`);
|
|
26
25
|
return null;
|
|
27
26
|
}
|
|
28
|
-
const alg = key.alg ?? options
|
|
29
|
-
if (!alg) {
|
|
30
|
-
ctx.context.logger.debug(`No JWT algorithm available for cookie-cache JWT kid: ${kid}`);
|
|
31
|
-
return null;
|
|
32
|
-
}
|
|
27
|
+
const alg = key.alg ?? getDefaultKeyAlg(options);
|
|
33
28
|
return {
|
|
34
29
|
alg,
|
|
35
30
|
publicKey: await importJWK(JSON.parse(key.publicKey), alg)
|
|
@@ -2,7 +2,7 @@ import { mergeSchema } from "../../db/schema.mjs";
|
|
|
2
2
|
import { sessionMiddleware } from "../../api/routes/session.mjs";
|
|
3
3
|
import { APIError as APIError$1 } from "../../api/index.mjs";
|
|
4
4
|
import { PACKAGE_VERSION } from "../../version.mjs";
|
|
5
|
-
import { getJwksAdapter } from "./adapter.mjs";
|
|
5
|
+
import { getDefaultKeyAlg, getJwksAdapter } from "./adapter.mjs";
|
|
6
6
|
import { createJwk, generateExportedKeyPair, toExpJWT } from "./utils.mjs";
|
|
7
7
|
import { getJwtToken, resolveSigningKey, signJWT } from "./sign.mjs";
|
|
8
8
|
import { createCookieCacheSigner } from "./cookie-cache.mjs";
|
|
@@ -131,7 +131,7 @@ const jwt = (options) => {
|
|
|
131
131
|
const defaultCrv = keyPairConfig ? "crv" in keyPairConfig ? keyPairConfig.crv : void 0 : void 0;
|
|
132
132
|
return ctx.json({ keys: keys.map((keySet) => {
|
|
133
133
|
return {
|
|
134
|
-
alg: keySet.alg ?? options
|
|
134
|
+
alg: keySet.alg ?? getDefaultKeyAlg(options),
|
|
135
135
|
crv: keySet.crv ?? defaultCrv,
|
|
136
136
|
...JSON.parse(keySet.publicKey),
|
|
137
137
|
kid: keySet.id
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { symmetricDecrypt } from "../../crypto/index.mjs";
|
|
2
|
-
import { getJwksAdapter } from "./adapter.mjs";
|
|
2
|
+
import { getDefaultKeyAlg, getJwksAdapter } from "./adapter.mjs";
|
|
3
3
|
import { createJwk, toExpJWT } from "./utils.mjs";
|
|
4
4
|
import { BetterAuthError } from "@better-auth/core/error";
|
|
5
5
|
import { SignJWT, importJWK } from "jose";
|
|
@@ -29,13 +29,13 @@ async function resolveSigningKey(ctx, options, overrides) {
|
|
|
29
29
|
key = await adapter.getKeyById(ctx, overrides.signingKeyId);
|
|
30
30
|
if (!key) throw new BetterAuthError(`signJWT: signingKeyId "${overrides.signingKeyId}" not found in JWKS. The key must be provisioned before it can be referenced.`);
|
|
31
31
|
if (overrides.signingAlgorithm !== void 0) {
|
|
32
|
-
const configAlg = options
|
|
32
|
+
const configAlg = getDefaultKeyAlg(options);
|
|
33
33
|
if ((key.alg ?? configAlg) !== overrides.signingAlgorithm) throw new BetterAuthError(`signJWT: signingKeyId "${overrides.signingKeyId}" has alg "${key.alg ?? `unset (inherits keyPairConfig.alg "${configAlg}")`}" but signingAlgorithm was set to "${overrides.signingAlgorithm}".`);
|
|
34
34
|
}
|
|
35
35
|
} else if (overrides?.signingAlgorithm !== void 0) {
|
|
36
36
|
key = await adapter.getLatestKeyByAlg(ctx, overrides.signingAlgorithm);
|
|
37
37
|
if (!key) {
|
|
38
|
-
const primaryAlg = options
|
|
38
|
+
const primaryAlg = getDefaultKeyAlg(options);
|
|
39
39
|
const preconfig = options?.jwks?.keyPairConfigs?.find((c) => c.alg === overrides.signingAlgorithm);
|
|
40
40
|
const isPrimary = primaryAlg === overrides.signingAlgorithm;
|
|
41
41
|
if (preconfig || isPrimary) key = await createJwk(ctx, {
|
|
@@ -51,7 +51,7 @@ async function resolveSigningKey(ctx, options, overrides) {
|
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
} else {
|
|
54
|
-
const primaryAlg = options
|
|
54
|
+
const primaryAlg = getDefaultKeyAlg(options);
|
|
55
55
|
key = await adapter.getLatestKeyByAlg(ctx, primaryAlg) ?? await adapter.getLatestKey(ctx);
|
|
56
56
|
}
|
|
57
57
|
if (!key || key.expiresAt && key.expiresAt < /* @__PURE__ */ new Date()) {
|
|
@@ -64,7 +64,7 @@ async function resolveSigningKey(ctx, options, overrides) {
|
|
|
64
64
|
}).catch(() => {
|
|
65
65
|
throw new BetterAuthError("Failed to decrypt private key. Make sure the secret currently in use is the same as the one used to encrypt the private key. If you are using a different secret, either clean up your JWKS or disable private key encryption.");
|
|
66
66
|
}) : key.privateKey;
|
|
67
|
-
const alg = key.alg ?? options
|
|
67
|
+
const alg = key.alg ?? getDefaultKeyAlg(options);
|
|
68
68
|
const privateKey = await importJWK(JSON.parse(privateWebKey), alg);
|
|
69
69
|
return {
|
|
70
70
|
alg,
|
|
@@ -86,6 +86,7 @@ async function signJWT(ctx, config) {
|
|
|
86
86
|
const defaultIss = options?.jwt?.issuer ?? baseURLOrigin;
|
|
87
87
|
const aud = payload.aud;
|
|
88
88
|
const defaultAud = options?.jwt?.audience ?? baseURLOrigin;
|
|
89
|
+
if (!(iss ?? defaultIss) || !(aud ?? defaultAud)) ctx.context.logger.warn("[better-auth] The JWT plugin signed a JWT without an issuer or audience because no base URL was resolved for this call. Set `baseURL`, or `jwt.issuer` and `jwt.audience` in the JWT plugin options.");
|
|
89
90
|
if (options?.jwt?.sign) {
|
|
90
91
|
const jwtPayload = {
|
|
91
92
|
...payload,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getJwksAdapter } from "./adapter.mjs";
|
|
1
|
+
import { getDefaultKeyAlg, getJwksAdapter } from "./adapter.mjs";
|
|
2
2
|
import { getCurrentAuthEndpointContext } from "@better-auth/core/context";
|
|
3
3
|
import { base64 } from "@better-auth/utils/base64";
|
|
4
4
|
import { importJWK, jwtVerify } from "jose";
|
|
@@ -28,11 +28,17 @@ async function verifyJWT(token, options) {
|
|
|
28
28
|
ctx.context.logger.debug(`No JWKS key found for kid: ${kid}`);
|
|
29
29
|
return null;
|
|
30
30
|
}
|
|
31
|
-
const
|
|
31
|
+
const publicKey = JSON.parse(key.publicKey);
|
|
32
|
+
const alg = key.alg ?? getDefaultKeyAlg(options);
|
|
33
|
+
const cryptoKey = await importJWK(publicKey, alg);
|
|
32
34
|
const baseURLOrigin = typeof ctx.context.options.baseURL === "string" ? ctx.context.options.baseURL : void 0;
|
|
35
|
+
const issuer = options?.jwt?.issuer ?? baseURLOrigin;
|
|
36
|
+
const audience = options?.jwt?.audience ?? baseURLOrigin;
|
|
37
|
+
if (!issuer || !audience) ctx.context.logger.warn("[better-auth] The JWT plugin is verifying a JWT without checking its issuer or audience because no base URL was resolved for this call. Set `baseURL`, or `jwt.issuer` and `jwt.audience` in the JWT plugin options.");
|
|
33
38
|
const { payload } = await jwtVerify(token, cryptoKey, {
|
|
34
|
-
issuer
|
|
35
|
-
audience
|
|
39
|
+
issuer,
|
|
40
|
+
audience,
|
|
41
|
+
algorithms: [alg]
|
|
36
42
|
});
|
|
37
43
|
if (!payload.sub || !payload.aud) return null;
|
|
38
44
|
return payload;
|
|
@@ -4,18 +4,16 @@ import { originCheck } from "../../api/middlewares/origin-check.mjs";
|
|
|
4
4
|
import { parseSetCookieHeader } from "../../cookies/cookie-utils.mjs";
|
|
5
5
|
import { symmetricDecrypt, symmetricEncrypt } from "../../crypto/index.mjs";
|
|
6
6
|
import { setSessionCookie } from "../../cookies/index.mjs";
|
|
7
|
-
import { resolveOAuthAccountKey, toOAuthProfileRecord } from "../../oauth2/account-key.mjs";
|
|
8
7
|
import { redirectOnError } from "../../oauth2/errors.mjs";
|
|
9
|
-
import { getOAuthCallbackPath } from "../../oauth2/utils.mjs";
|
|
10
8
|
import { handleOAuthUserInfo, linkOAuthAccount } from "../../oauth2/link-account.mjs";
|
|
11
9
|
import { setOAuthState } from "../../api/state/oauth.mjs";
|
|
12
10
|
import { derivePurposeKey } from "../../crypto/purpose.mjs";
|
|
13
11
|
import { getAuthStateVerificationIdentifier, parseGenericState } from "../../state.mjs";
|
|
12
|
+
import { exchangeOAuthCallback } from "../../oauth2/callback.mjs";
|
|
14
13
|
import { PACKAGE_VERSION } from "../../version.mjs";
|
|
15
14
|
import { parseJSON } from "../../client/parser.mjs";
|
|
16
15
|
import { checkSkipProxy, resolveCurrentURL, stripTrailingSlash } from "./utils.mjs";
|
|
17
16
|
import { accountSchema, userSchema } from "@better-auth/core/db";
|
|
18
|
-
import { safeJSONParse } from "@better-auth/core/utils/json";
|
|
19
17
|
import { defu } from "defu";
|
|
20
18
|
import { createAuthEndpoint, createAuthMiddleware } from "@better-auth/core/api";
|
|
21
19
|
import * as z from "zod";
|
|
@@ -63,7 +61,9 @@ const oauthProxyQuerySchema = z.object({
|
|
|
63
61
|
const oauthCallbackQuerySchema = z.object({
|
|
64
62
|
code: z.string().optional(),
|
|
65
63
|
error: z.string().optional(),
|
|
66
|
-
user: z.string().optional()
|
|
64
|
+
user: z.string().optional(),
|
|
65
|
+
iss: z.string().optional(),
|
|
66
|
+
device_id: z.string().optional()
|
|
67
67
|
});
|
|
68
68
|
const oAuthProxy = (opts) => {
|
|
69
69
|
const maxAge = opts?.maxAge ?? 60;
|
|
@@ -242,7 +242,7 @@ const oAuthProxy = (opts) => {
|
|
|
242
242
|
ctx.context.logger.warn("Invalid OAuth callback query", query.error);
|
|
243
243
|
return;
|
|
244
244
|
}
|
|
245
|
-
const { code, error, user: userData } = query.data;
|
|
245
|
+
const { code, error, user: userData, iss, device_id: deviceId } = query.data;
|
|
246
246
|
let stateData;
|
|
247
247
|
try {
|
|
248
248
|
stateData = parseJSON(await symmetricDecrypt({
|
|
@@ -269,47 +269,20 @@ const oAuthProxy = (opts) => {
|
|
|
269
269
|
ctx.context.logger.warn("OAuth provider not found", { providerId });
|
|
270
270
|
throw redirectOnError(ctx, errorURL, "oauth_provider_not_found");
|
|
271
271
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
} catch (e) {
|
|
280
|
-
ctx.context.logger.error("Failed to validate authorization code", e);
|
|
281
|
-
throw redirectOnError(ctx, errorURL, "invalid_code");
|
|
282
|
-
}
|
|
283
|
-
if (!tokens) throw redirectOnError(ctx, errorURL, "invalid_code");
|
|
284
|
-
const parsedUserData = userData ? safeJSONParse(userData) : null;
|
|
285
|
-
const userInfoResult = await provider.getUserInfo({
|
|
286
|
-
...tokens,
|
|
287
|
-
/**
|
|
288
|
-
* The user object from the provider
|
|
289
|
-
* This is only available for some providers like Apple
|
|
290
|
-
*/
|
|
291
|
-
user: parsedUserData ?? void 0
|
|
272
|
+
const exchanged = await exchangeOAuthCallback(ctx, provider, {
|
|
273
|
+
code,
|
|
274
|
+
codeVerifier: stateData.codeVerifier,
|
|
275
|
+
iss,
|
|
276
|
+
deviceId,
|
|
277
|
+
user: userData,
|
|
278
|
+
idTokenNonce: stateData.idTokenNonce
|
|
292
279
|
});
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
ctx.context.logger.error("Unable to get user info from provider");
|
|
296
|
-
throw redirectOnError(ctx, errorURL, "unable_to_get_user_info");
|
|
297
|
-
}
|
|
280
|
+
if (exchanged.error) throw redirectOnError(ctx, errorURL, exchanged.error);
|
|
281
|
+
const { tokens, userInfo, providerProfile, accountKey } = exchanged;
|
|
298
282
|
if (!userInfo.email) {
|
|
299
283
|
ctx.context.logger.error("Provider did not return email");
|
|
300
284
|
throw redirectOnError(ctx, errorURL, "email_not_found");
|
|
301
285
|
}
|
|
302
|
-
let accountKey;
|
|
303
|
-
try {
|
|
304
|
-
accountKey = await resolveOAuthAccountKey(provider, tokens, userInfoResult.data);
|
|
305
|
-
} catch (error) {
|
|
306
|
-
ctx.context.logger.error("Unable to derive provider account identity", {
|
|
307
|
-
providerId: provider.id,
|
|
308
|
-
error
|
|
309
|
-
});
|
|
310
|
-
throw redirectOnError(ctx, errorURL, "unable_to_get_user_info");
|
|
311
|
-
}
|
|
312
|
-
const providerProfile = toOAuthProfileRecord(userInfoResult.data);
|
|
313
286
|
const proxyCallbackURL = new URL(stateData.callbackURL);
|
|
314
287
|
const finalCallbackURL = proxyCallbackURL.searchParams.get("callbackURL") || stateData.callbackURL;
|
|
315
288
|
const payload = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "better-auth",
|
|
3
|
-
"version": "1.8.0-beta.
|
|
3
|
+
"version": "1.8.0-beta.2",
|
|
4
4
|
"description": "The most comprehensive authentication framework for TypeScript.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -497,19 +497,19 @@
|
|
|
497
497
|
"@better-fetch/fetch": "1.3.2",
|
|
498
498
|
"@noble/ciphers": "^2.2.0",
|
|
499
499
|
"@noble/hashes": "^2.2.0",
|
|
500
|
-
"better-call": "1.4.
|
|
500
|
+
"better-call": "1.4.4",
|
|
501
501
|
"defu": "^6.1.4",
|
|
502
502
|
"jose": "^6.2.3",
|
|
503
503
|
"kysely": "^0.28.17 || ^0.29.0",
|
|
504
504
|
"nanostores": "^1.3.0",
|
|
505
505
|
"zod": "^4.5.4",
|
|
506
|
-
"@better-auth/core": "1.8.0-beta.
|
|
507
|
-
"@better-auth/drizzle-adapter": "1.8.0-beta.
|
|
508
|
-
"@better-auth/kysely-adapter": "1.8.0-beta.
|
|
509
|
-
"@better-auth/memory-adapter": "1.8.0-beta.
|
|
510
|
-
"@better-auth/mongo-adapter": "1.8.0-beta.
|
|
511
|
-
"@better-auth/prisma-adapter": "1.8.0-beta.
|
|
512
|
-
"@better-auth/telemetry": "1.8.0-beta.
|
|
506
|
+
"@better-auth/core": "1.8.0-beta.2",
|
|
507
|
+
"@better-auth/drizzle-adapter": "1.8.0-beta.2",
|
|
508
|
+
"@better-auth/kysely-adapter": "1.8.0-beta.2",
|
|
509
|
+
"@better-auth/memory-adapter": "1.8.0-beta.2",
|
|
510
|
+
"@better-auth/mongo-adapter": "1.8.0-beta.2",
|
|
511
|
+
"@better-auth/prisma-adapter": "1.8.0-beta.2",
|
|
512
|
+
"@better-auth/telemetry": "1.8.0-beta.2"
|
|
513
513
|
},
|
|
514
514
|
"devDependencies": {
|
|
515
515
|
"@lynx-js/react": "^0.121.2",
|