@better-auth/core 1.6.1 → 1.7.0-beta.0

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.
@@ -2,7 +2,7 @@
2
2
  const symbol = Symbol.for("better-auth:global");
3
3
  let bind = null;
4
4
  const __context = {};
5
- const __betterAuthVersion = "1.6.1";
5
+ const __betterAuthVersion = "1.7.0-beta.0";
6
6
  /**
7
7
  * We store context instance in the globalThis.
8
8
  *
@@ -1,7 +1,7 @@
1
1
  import { ATTR_HTTP_RESPONSE_STATUS_CODE } from "./attributes.mjs";
2
2
  import { SpanStatusCode, trace } from "@opentelemetry/api";
3
3
  //#region src/instrumentation/tracer.ts
4
- const tracer = trace.getTracer("better-auth", "1.6.1");
4
+ const tracer = trace.getTracer("better-auth", "1.7.0-beta.0");
5
5
  /**
6
6
  * Better-auth uses `throw ctx.redirect(url)` for flow control (e.g. OAuth
7
7
  * callbacks). These are APIErrors with 3xx status codes and should not be
@@ -0,0 +1,59 @@
1
+ //#region src/oauth2/client-assertion.d.ts
2
+ /** Asymmetric signing algorithms compatible with private_key_jwt (RFC 7523). */
3
+ declare const ASSERTION_SIGNING_ALGORITHMS: readonly ["RS256", "RS384", "RS512", "PS256", "PS384", "PS512", "ES256", "ES384", "ES512", "EdDSA"];
4
+ type AssertionSigningAlgorithm = (typeof ASSERTION_SIGNING_ALGORITHMS)[number];
5
+ declare const CLIENT_ASSERTION_TYPE = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
6
+ interface ClientAssertionConfig {
7
+ /** Pre-signed JWT assertion string. If provided, signing is skipped. */
8
+ assertion?: string;
9
+ /** Private key in JWK format for signing. */
10
+ privateKeyJwk?: JsonWebKey;
11
+ /** Private key in PKCS#8 PEM format for signing. */
12
+ privateKeyPem?: string;
13
+ /** Key ID to include in the JWT header. */
14
+ kid?: string;
15
+ /** Asymmetric signing algorithm. Symmetric algorithms (HS256) and "none" are not allowed. @default "RS256" */
16
+ algorithm?: AssertionSigningAlgorithm;
17
+ /** Token endpoint URL (used as the JWT `aud` claim). */
18
+ tokenEndpoint?: string;
19
+ /** Assertion lifetime in seconds. @default 120 */
20
+ expiresIn?: number;
21
+ }
22
+ /**
23
+ * Signs an RFC 7523 client assertion JWT for `private_key_jwt` authentication.
24
+ *
25
+ * The JWT contains: iss=clientId, sub=clientId, aud=tokenEndpoint,
26
+ * exp=now+120s, jti=unique, iat=now.
27
+ */
28
+ declare function signClientAssertion({
29
+ clientId,
30
+ tokenEndpoint,
31
+ privateKeyJwk,
32
+ privateKeyPem,
33
+ kid,
34
+ algorithm,
35
+ expiresIn
36
+ }: {
37
+ clientId: string;
38
+ tokenEndpoint: string;
39
+ privateKeyJwk?: JsonWebKey;
40
+ privateKeyPem?: string;
41
+ kid?: string;
42
+ algorithm?: AssertionSigningAlgorithm;
43
+ expiresIn?: number;
44
+ }): Promise<string>;
45
+ /**
46
+ * Resolves a ClientAssertionConfig into `client_assertion` + `client_assertion_type`
47
+ * params for injection into a token request body.
48
+ */
49
+ declare function resolveAssertionParams({
50
+ clientAssertion,
51
+ clientId,
52
+ tokenEndpoint
53
+ }: {
54
+ clientAssertion: ClientAssertionConfig;
55
+ clientId: string;
56
+ tokenEndpoint?: string;
57
+ }): Promise<Record<string, string>>;
58
+ //#endregion
59
+ export { ASSERTION_SIGNING_ALGORITHMS, AssertionSigningAlgorithm, CLIENT_ASSERTION_TYPE, ClientAssertionConfig, resolveAssertionParams, signClientAssertion };
@@ -0,0 +1,64 @@
1
+ import { SignJWT, importJWK, importPKCS8 } from "jose";
2
+ //#region src/oauth2/client-assertion.ts
3
+ /** Asymmetric signing algorithms compatible with private_key_jwt (RFC 7523). */
4
+ const ASSERTION_SIGNING_ALGORITHMS = [
5
+ "RS256",
6
+ "RS384",
7
+ "RS512",
8
+ "PS256",
9
+ "PS384",
10
+ "PS512",
11
+ "ES256",
12
+ "ES384",
13
+ "ES512",
14
+ "EdDSA"
15
+ ];
16
+ const CLIENT_ASSERTION_TYPE = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
17
+ /**
18
+ * Signs an RFC 7523 client assertion JWT for `private_key_jwt` authentication.
19
+ *
20
+ * The JWT contains: iss=clientId, sub=clientId, aud=tokenEndpoint,
21
+ * exp=now+120s, jti=unique, iat=now.
22
+ */
23
+ async function signClientAssertion({ clientId, tokenEndpoint, privateKeyJwk, privateKeyPem, kid, algorithm, expiresIn = 120 }) {
24
+ const resolvedKid = kid ?? privateKeyJwk?.kid;
25
+ const resolvedAlg = algorithm ?? privateKeyJwk?.alg ?? "RS256";
26
+ let key;
27
+ if (privateKeyJwk) key = await importJWK(privateKeyJwk, resolvedAlg);
28
+ else if (privateKeyPem) key = await importPKCS8(privateKeyPem, resolvedAlg);
29
+ else throw new Error("private_key_jwt requires either privateKeyJwk or privateKeyPem");
30
+ const now = Math.floor(Date.now() / 1e3);
31
+ const jti = crypto.randomUUID();
32
+ const header = {
33
+ alg: resolvedAlg,
34
+ typ: "JWT"
35
+ };
36
+ if (resolvedKid) header.kid = resolvedKid;
37
+ return new SignJWT({}).setProtectedHeader(header).setIssuer(clientId).setSubject(clientId).setAudience(tokenEndpoint).setIssuedAt(now).setExpirationTime(now + expiresIn).setJti(jti).sign(key);
38
+ }
39
+ /**
40
+ * Resolves a ClientAssertionConfig into `client_assertion` + `client_assertion_type`
41
+ * params for injection into a token request body.
42
+ */
43
+ async function resolveAssertionParams({ clientAssertion, clientId, tokenEndpoint }) {
44
+ let assertion = clientAssertion.assertion;
45
+ if (!assertion) {
46
+ const audEndpoint = tokenEndpoint ?? clientAssertion.tokenEndpoint;
47
+ if (!audEndpoint) throw new Error("private_key_jwt requires a tokenEndpoint for the JWT audience claim");
48
+ assertion = await signClientAssertion({
49
+ clientId,
50
+ tokenEndpoint: audEndpoint,
51
+ privateKeyJwk: clientAssertion.privateKeyJwk,
52
+ privateKeyPem: clientAssertion.privateKeyPem,
53
+ kid: clientAssertion.kid,
54
+ algorithm: clientAssertion.algorithm,
55
+ expiresIn: clientAssertion.expiresIn
56
+ });
57
+ }
58
+ return {
59
+ client_assertion: assertion,
60
+ client_assertion_type: CLIENT_ASSERTION_TYPE
61
+ };
62
+ }
63
+ //#endregion
64
+ export { ASSERTION_SIGNING_ALGORITHMS, CLIENT_ASSERTION_TYPE, resolveAssertionParams, signClientAssertion };
@@ -1,3 +1,4 @@
1
+ import { ClientAssertionConfig } from "./client-assertion.mjs";
1
2
  import { AwaitableFunction } from "../types/helper.mjs";
2
3
  import { OAuth2Tokens, ProviderOptions } from "./oauth-provider.mjs";
3
4
 
@@ -6,13 +7,15 @@ declare function clientCredentialsTokenRequest({
6
7
  options,
7
8
  scope,
8
9
  authentication,
10
+ clientAssertion,
11
+ tokenEndpoint,
9
12
  resource
10
13
  }: {
11
- options: AwaitableFunction<ProviderOptions & {
12
- clientSecret: string;
13
- }>;
14
+ options: AwaitableFunction<ProviderOptions>;
14
15
  scope?: string | undefined;
15
- authentication?: ("basic" | "post") | undefined;
16
+ authentication?: ("basic" | "post" | "private_key_jwt") | undefined;
17
+ clientAssertion?: ClientAssertionConfig | undefined; /** Token endpoint URL. Used as the JWT `aud` claim when signing assertions. */
18
+ tokenEndpoint?: string | undefined;
16
19
  resource?: (string | string[]) | undefined;
17
20
  }): Promise<{
18
21
  body: URLSearchParams;
@@ -25,14 +28,14 @@ declare function createClientCredentialsTokenRequest({
25
28
  options,
26
29
  scope,
27
30
  authentication,
28
- resource
31
+ resource,
32
+ extraParams
29
33
  }: {
30
- options: ProviderOptions & {
31
- clientSecret: string;
32
- };
34
+ options: ProviderOptions;
33
35
  scope?: string | undefined;
34
- authentication?: ("basic" | "post") | undefined;
36
+ authentication?: ("basic" | "post" | "private_key_jwt") | undefined;
35
37
  resource?: (string | string[]) | undefined;
38
+ extraParams?: Record<string, string> | undefined;
36
39
  }): {
37
40
  body: URLSearchParams;
38
41
  headers: Record<string, any>;
@@ -42,14 +45,14 @@ declare function clientCredentialsToken({
42
45
  tokenEndpoint,
43
46
  scope,
44
47
  authentication,
48
+ clientAssertion,
45
49
  resource
46
50
  }: {
47
- options: AwaitableFunction<ProviderOptions & {
48
- clientSecret: string;
49
- }>;
51
+ options: AwaitableFunction<ProviderOptions>;
50
52
  tokenEndpoint: string;
51
53
  scope: string;
52
- authentication?: ("basic" | "post") | undefined;
54
+ authentication?: ("basic" | "post" | "private_key_jwt") | undefined;
55
+ clientAssertion?: ClientAssertionConfig | undefined;
53
56
  resource?: (string | string[]) | undefined;
54
57
  }): Promise<OAuth2Tokens>;
55
58
  //#endregion
@@ -1,19 +1,30 @@
1
- import { base64Url } from "@better-auth/utils/base64";
1
+ import { resolveAssertionParams } from "./client-assertion.mjs";
2
+ import { base64 } from "@better-auth/utils/base64";
2
3
  import { betterFetch } from "@better-fetch/fetch";
3
4
  //#region src/oauth2/client-credentials-token.ts
4
- async function clientCredentialsTokenRequest({ options, scope, authentication, resource }) {
5
+ async function clientCredentialsTokenRequest({ options, scope, authentication, clientAssertion, tokenEndpoint, resource }) {
5
6
  options = typeof options === "function" ? await options() : options;
7
+ let extraParams;
8
+ if (authentication === "private_key_jwt") {
9
+ if (!clientAssertion) throw new Error("private_key_jwt authentication requires a clientAssertion configuration");
10
+ extraParams = await resolveAssertionParams({
11
+ clientAssertion,
12
+ clientId: Array.isArray(options.clientId) ? options.clientId[0] : options.clientId,
13
+ tokenEndpoint
14
+ });
15
+ }
6
16
  return createClientCredentialsTokenRequest({
7
17
  options,
8
18
  scope,
9
19
  authentication,
10
- resource
20
+ resource,
21
+ extraParams
11
22
  });
12
23
  }
13
24
  /**
14
25
  * @deprecated use async'd clientCredentialsTokenRequest instead
15
26
  */
16
- function createClientCredentialsTokenRequest({ options, scope, authentication, resource }) {
27
+ function createClientCredentialsTokenRequest({ options, scope, authentication, resource, extraParams }) {
17
28
  const body = new URLSearchParams();
18
29
  const headers = {
19
30
  "content-type": "application/x-www-form-urlencoded",
@@ -23,24 +34,27 @@ function createClientCredentialsTokenRequest({ options, scope, authentication, r
23
34
  scope && body.set("scope", scope);
24
35
  if (resource) if (typeof resource === "string") body.append("resource", resource);
25
36
  else for (const _resource of resource) body.append("resource", _resource);
26
- if (authentication === "basic") {
27
- const primaryClientId = Array.isArray(options.clientId) ? options.clientId[0] : options.clientId;
28
- headers["authorization"] = `Basic ${base64Url.encode(`${primaryClientId}:${options.clientSecret}`)}`;
29
- } else {
30
- const primaryClientId = Array.isArray(options.clientId) ? options.clientId[0] : options.clientId;
37
+ const primaryClientId = Array.isArray(options.clientId) ? options.clientId[0] : options.clientId;
38
+ if (authentication === "basic") headers["authorization"] = `Basic ${base64.encode(`${primaryClientId}:${options.clientSecret ?? ""}`)}`;
39
+ else {
31
40
  body.set("client_id", primaryClientId);
32
- body.set("client_secret", options.clientSecret);
41
+ if (authentication !== "private_key_jwt" && options.clientSecret) body.set("client_secret", options.clientSecret);
42
+ }
43
+ if (extraParams) {
44
+ for (const [key, value] of Object.entries(extraParams)) if (!body.has(key)) body.append(key, value);
33
45
  }
34
46
  return {
35
47
  body,
36
48
  headers
37
49
  };
38
50
  }
39
- async function clientCredentialsToken({ options, tokenEndpoint, scope, authentication, resource }) {
51
+ async function clientCredentialsToken({ options, tokenEndpoint, scope, authentication, clientAssertion, resource }) {
40
52
  const { body, headers } = await clientCredentialsTokenRequest({
41
53
  options,
42
54
  scope,
43
55
  authentication,
56
+ clientAssertion,
57
+ tokenEndpoint,
44
58
  resource
45
59
  });
46
60
  const { data, error } = await betterFetch(tokenEndpoint, {
@@ -1,3 +1,4 @@
1
+ import { ASSERTION_SIGNING_ALGORITHMS, AssertionSigningAlgorithm, CLIENT_ASSERTION_TYPE, ClientAssertionConfig, resolveAssertionParams, signClientAssertion } from "./client-assertion.mjs";
1
2
  import { OAuth2Tokens, OAuth2UserInfo, OAuthProvider, ProviderOptions } from "./oauth-provider.mjs";
2
3
  import { clientCredentialsToken, clientCredentialsTokenRequest, createClientCredentialsTokenRequest } from "./client-credentials-token.mjs";
3
4
  import { createAuthorizationURL } from "./create-authorization-url.mjs";
@@ -5,4 +6,4 @@ import { createRefreshAccessTokenRequest, refreshAccessToken, refreshAccessToken
5
6
  import { generateCodeChallenge, getOAuth2Tokens } from "./utils.mjs";
6
7
  import { authorizationCodeRequest, createAuthorizationCodeRequest, validateAuthorizationCode, validateToken } from "./validate-authorization-code.mjs";
7
8
  import { getJwks, verifyAccessToken, verifyJwsAccessToken } from "./verify.mjs";
8
- export { type OAuth2Tokens, type OAuth2UserInfo, type OAuthProvider, type ProviderOptions, authorizationCodeRequest, clientCredentialsToken, clientCredentialsTokenRequest, createAuthorizationCodeRequest, createAuthorizationURL, createClientCredentialsTokenRequest, createRefreshAccessTokenRequest, generateCodeChallenge, getJwks, getOAuth2Tokens, refreshAccessToken, refreshAccessTokenRequest, validateAuthorizationCode, validateToken, verifyAccessToken, verifyJwsAccessToken };
9
+ export { ASSERTION_SIGNING_ALGORITHMS, type AssertionSigningAlgorithm, CLIENT_ASSERTION_TYPE, type ClientAssertionConfig, type OAuth2Tokens, type OAuth2UserInfo, type OAuthProvider, type ProviderOptions, authorizationCodeRequest, clientCredentialsToken, clientCredentialsTokenRequest, createAuthorizationCodeRequest, createAuthorizationURL, createClientCredentialsTokenRequest, createRefreshAccessTokenRequest, generateCodeChallenge, getJwks, getOAuth2Tokens, refreshAccessToken, refreshAccessTokenRequest, resolveAssertionParams, signClientAssertion, validateAuthorizationCode, validateToken, verifyAccessToken, verifyJwsAccessToken };
@@ -1,7 +1,8 @@
1
+ import { ASSERTION_SIGNING_ALGORITHMS, CLIENT_ASSERTION_TYPE, resolveAssertionParams, signClientAssertion } from "./client-assertion.mjs";
1
2
  import { clientCredentialsToken, clientCredentialsTokenRequest, createClientCredentialsTokenRequest } from "./client-credentials-token.mjs";
2
3
  import { generateCodeChallenge, getOAuth2Tokens } from "./utils.mjs";
3
4
  import { createAuthorizationURL } from "./create-authorization-url.mjs";
4
5
  import { createRefreshAccessTokenRequest, refreshAccessToken, refreshAccessTokenRequest } from "./refresh-access-token.mjs";
5
6
  import { authorizationCodeRequest, createAuthorizationCodeRequest, validateAuthorizationCode, validateToken } from "./validate-authorization-code.mjs";
6
7
  import { getJwks, verifyAccessToken, verifyJwsAccessToken } from "./verify.mjs";
7
- export { authorizationCodeRequest, clientCredentialsToken, clientCredentialsTokenRequest, createAuthorizationCodeRequest, createAuthorizationURL, createClientCredentialsTokenRequest, createRefreshAccessTokenRequest, generateCodeChallenge, getJwks, getOAuth2Tokens, refreshAccessToken, refreshAccessTokenRequest, validateAuthorizationCode, validateToken, verifyAccessToken, verifyJwsAccessToken };
8
+ export { ASSERTION_SIGNING_ALGORITHMS, CLIENT_ASSERTION_TYPE, authorizationCodeRequest, clientCredentialsToken, clientCredentialsTokenRequest, createAuthorizationCodeRequest, createAuthorizationURL, createClientCredentialsTokenRequest, createRefreshAccessTokenRequest, generateCodeChallenge, getJwks, getOAuth2Tokens, refreshAccessToken, refreshAccessTokenRequest, resolveAssertionParams, signClientAssertion, validateAuthorizationCode, validateToken, verifyAccessToken, verifyJwsAccessToken };
@@ -1,3 +1,4 @@
1
+ import { ClientAssertionConfig } from "./client-assertion.mjs";
1
2
  import { AwaitableFunction } from "../types/helper.mjs";
2
3
  import { OAuth2Tokens, ProviderOptions } from "./oauth-provider.mjs";
3
4
 
@@ -6,12 +7,16 @@ declare function refreshAccessTokenRequest({
6
7
  refreshToken,
7
8
  options,
8
9
  authentication,
10
+ clientAssertion,
11
+ tokenEndpoint,
9
12
  extraParams,
10
13
  resource
11
14
  }: {
12
15
  refreshToken: string;
13
16
  options: AwaitableFunction<Partial<ProviderOptions>>;
14
- authentication?: ("basic" | "post") | undefined;
17
+ authentication?: ("basic" | "post" | "private_key_jwt") | undefined;
18
+ clientAssertion?: ClientAssertionConfig | undefined; /** Token endpoint URL. Used as the JWT `aud` claim when signing assertions. */
19
+ tokenEndpoint?: string | undefined;
15
20
  extraParams?: Record<string, string> | undefined;
16
21
  resource?: (string | string[]) | undefined;
17
22
  }): Promise<{
@@ -30,7 +35,7 @@ declare function createRefreshAccessTokenRequest({
30
35
  }: {
31
36
  refreshToken: string;
32
37
  options: ProviderOptions;
33
- authentication?: ("basic" | "post") | undefined;
38
+ authentication?: ("basic" | "post" | "private_key_jwt") | undefined;
34
39
  extraParams?: Record<string, string> | undefined;
35
40
  resource?: (string | string[]) | undefined;
36
41
  }): {
@@ -42,12 +47,14 @@ declare function refreshAccessToken({
42
47
  options,
43
48
  tokenEndpoint,
44
49
  authentication,
50
+ clientAssertion,
45
51
  extraParams
46
52
  }: {
47
53
  refreshToken: string;
48
54
  options: Partial<ProviderOptions>;
49
55
  tokenEndpoint: string;
50
- authentication?: ("basic" | "post") | undefined;
56
+ authentication?: ("basic" | "post" | "private_key_jwt") | undefined;
57
+ clientAssertion?: ClientAssertionConfig | undefined;
51
58
  extraParams?: Record<string, string> | undefined;
52
59
  }): Promise<OAuth2Tokens>;
53
60
  //#endregion
@@ -1,8 +1,21 @@
1
+ import { resolveAssertionParams } from "./client-assertion.mjs";
1
2
  import { base64 } from "@better-auth/utils/base64";
2
3
  import { betterFetch } from "@better-fetch/fetch";
3
4
  //#region src/oauth2/refresh-access-token.ts
4
- async function refreshAccessTokenRequest({ refreshToken, options, authentication, extraParams, resource }) {
5
+ async function refreshAccessTokenRequest({ refreshToken, options, authentication, clientAssertion, tokenEndpoint, extraParams, resource }) {
5
6
  options = typeof options === "function" ? await options() : options;
7
+ if (authentication === "private_key_jwt") {
8
+ if (!clientAssertion) throw new Error("private_key_jwt authentication requires a clientAssertion configuration");
9
+ const assertionParams = await resolveAssertionParams({
10
+ clientAssertion,
11
+ clientId: Array.isArray(options.clientId) ? options.clientId[0] : options.clientId,
12
+ tokenEndpoint
13
+ });
14
+ extraParams = {
15
+ ...extraParams,
16
+ ...assertionParams
17
+ };
18
+ }
6
19
  return createRefreshAccessTokenRequest({
7
20
  refreshToken,
8
21
  options,
@@ -22,14 +35,12 @@ function createRefreshAccessTokenRequest({ refreshToken, options, authentication
22
35
  };
23
36
  body.set("grant_type", "refresh_token");
24
37
  body.set("refresh_token", refreshToken);
25
- if (authentication === "basic") {
26
- const primaryClientId = Array.isArray(options.clientId) ? options.clientId[0] : options.clientId;
27
- if (primaryClientId) headers["authorization"] = "Basic " + base64.encode(`${primaryClientId}:${options.clientSecret ?? ""}`);
28
- else headers["authorization"] = "Basic " + base64.encode(`:${options.clientSecret ?? ""}`);
29
- } else {
30
- const primaryClientId = Array.isArray(options.clientId) ? options.clientId[0] : options.clientId;
38
+ const primaryClientId = Array.isArray(options.clientId) ? options.clientId[0] : options.clientId;
39
+ if (authentication === "basic") if (primaryClientId) headers["authorization"] = "Basic " + base64.encode(`${primaryClientId}:${options.clientSecret ?? ""}`);
40
+ else headers["authorization"] = "Basic " + base64.encode(`:${options.clientSecret ?? ""}`);
41
+ else {
31
42
  body.set("client_id", primaryClientId);
32
- if (options.clientSecret) body.set("client_secret", options.clientSecret);
43
+ if (authentication !== "private_key_jwt" && options.clientSecret) body.set("client_secret", options.clientSecret);
33
44
  }
34
45
  if (resource) if (typeof resource === "string") body.append("resource", resource);
35
46
  else for (const _resource of resource) body.append("resource", _resource);
@@ -39,11 +50,13 @@ function createRefreshAccessTokenRequest({ refreshToken, options, authentication
39
50
  headers
40
51
  };
41
52
  }
42
- async function refreshAccessToken({ refreshToken, options, tokenEndpoint, authentication, extraParams }) {
43
- const { body, headers } = await createRefreshAccessTokenRequest({
53
+ async function refreshAccessToken({ refreshToken, options, tokenEndpoint, authentication, clientAssertion, extraParams }) {
54
+ const { body, headers } = await refreshAccessTokenRequest({
44
55
  refreshToken,
45
56
  options,
46
57
  authentication,
58
+ clientAssertion,
59
+ tokenEndpoint,
47
60
  extraParams
48
61
  });
49
62
  const { data, error } = await betterFetch(tokenEndpoint, {
@@ -1,3 +1,4 @@
1
+ import { ClientAssertionConfig } from "./client-assertion.mjs";
1
2
  import { AwaitableFunction } from "../types/helper.mjs";
2
3
  import { OAuth2Tokens, ProviderOptions } from "./oauth-provider.mjs";
3
4
  import * as jose from "jose";
@@ -9,6 +10,8 @@ declare function authorizationCodeRequest({
9
10
  redirectURI,
10
11
  options,
11
12
  authentication,
13
+ clientAssertion,
14
+ tokenEndpoint,
12
15
  deviceId,
13
16
  headers,
14
17
  additionalParams,
@@ -19,7 +22,9 @@ declare function authorizationCodeRequest({
19
22
  options: AwaitableFunction<Partial<ProviderOptions>>;
20
23
  codeVerifier?: string | undefined;
21
24
  deviceId?: string | undefined;
22
- authentication?: ("basic" | "post") | undefined;
25
+ authentication?: ("basic" | "post" | "private_key_jwt") | undefined;
26
+ clientAssertion?: ClientAssertionConfig | undefined; /** Token endpoint URL. Used as the JWT `aud` claim when signing assertions. */
27
+ tokenEndpoint?: string | undefined;
23
28
  headers?: Record<string, string> | undefined;
24
29
  additionalParams?: Record<string, string> | undefined;
25
30
  resource?: (string | string[]) | undefined;
@@ -46,7 +51,7 @@ declare function createAuthorizationCodeRequest({
46
51
  options: Partial<ProviderOptions>;
47
52
  codeVerifier?: string | undefined;
48
53
  deviceId?: string | undefined;
49
- authentication?: ("basic" | "post") | undefined;
54
+ authentication?: ("basic" | "post" | "private_key_jwt") | undefined;
50
55
  headers?: Record<string, string> | undefined;
51
56
  additionalParams?: Record<string, string> | undefined;
52
57
  resource?: (string | string[]) | undefined;
@@ -61,6 +66,7 @@ declare function validateAuthorizationCode({
61
66
  options,
62
67
  tokenEndpoint,
63
68
  authentication,
69
+ clientAssertion,
64
70
  deviceId,
65
71
  headers,
66
72
  additionalParams,
@@ -72,7 +78,8 @@ declare function validateAuthorizationCode({
72
78
  codeVerifier?: string | undefined;
73
79
  deviceId?: string | undefined;
74
80
  tokenEndpoint: string;
75
- authentication?: ("basic" | "post") | undefined;
81
+ authentication?: ("basic" | "post" | "private_key_jwt") | undefined;
82
+ clientAssertion?: ClientAssertionConfig | undefined;
76
83
  headers?: Record<string, string> | undefined;
77
84
  additionalParams?: Record<string, string> | undefined;
78
85
  resource?: (string | string[]) | undefined;
@@ -1,10 +1,23 @@
1
+ import { resolveAssertionParams } from "./client-assertion.mjs";
1
2
  import { getOAuth2Tokens } from "./utils.mjs";
3
+ import { createRemoteJWKSet, jwtVerify } from "jose";
2
4
  import { base64 } from "@better-auth/utils/base64";
3
5
  import { betterFetch } from "@better-fetch/fetch";
4
- import { createRemoteJWKSet, jwtVerify } from "jose";
5
6
  //#region src/oauth2/validate-authorization-code.ts
6
- async function authorizationCodeRequest({ code, codeVerifier, redirectURI, options, authentication, deviceId, headers, additionalParams = {}, resource }) {
7
+ async function authorizationCodeRequest({ code, codeVerifier, redirectURI, options, authentication, clientAssertion, tokenEndpoint, deviceId, headers, additionalParams = {}, resource }) {
7
8
  options = typeof options === "function" ? await options() : options;
9
+ if (authentication === "private_key_jwt") {
10
+ if (!clientAssertion) throw new Error("private_key_jwt authentication requires a clientAssertion configuration");
11
+ const assertionParams = await resolveAssertionParams({
12
+ clientAssertion,
13
+ clientId: Array.isArray(options.clientId) ? options.clientId[0] : options.clientId,
14
+ tokenEndpoint
15
+ });
16
+ additionalParams = {
17
+ ...additionalParams,
18
+ ...assertionParams
19
+ };
20
+ }
8
21
  return createAuthorizationCodeRequest({
9
22
  code,
10
23
  codeVerifier,
@@ -35,13 +48,11 @@ function createAuthorizationCodeRequest({ code, codeVerifier, redirectURI, optio
35
48
  body.set("redirect_uri", options.redirectURI || redirectURI);
36
49
  if (resource) if (typeof resource === "string") body.append("resource", resource);
37
50
  else for (const _resource of resource) body.append("resource", _resource);
38
- if (authentication === "basic") {
39
- const primaryClientId = Array.isArray(options.clientId) ? options.clientId[0] : options.clientId;
40
- requestHeaders["authorization"] = `Basic ${base64.encode(`${primaryClientId}:${options.clientSecret ?? ""}`)}`;
41
- } else {
42
- const primaryClientId = Array.isArray(options.clientId) ? options.clientId[0] : options.clientId;
51
+ const primaryClientId = Array.isArray(options.clientId) ? options.clientId[0] : options.clientId;
52
+ if (authentication === "basic") requestHeaders["authorization"] = `Basic ${base64.encode(`${primaryClientId}:${options.clientSecret ?? ""}`)}`;
53
+ else {
43
54
  body.set("client_id", primaryClientId);
44
- if (options.clientSecret) body.set("client_secret", options.clientSecret);
55
+ if (authentication !== "private_key_jwt" && options.clientSecret) body.set("client_secret", options.clientSecret);
45
56
  }
46
57
  for (const [key, value] of Object.entries(additionalParams)) if (!body.has(key)) body.append(key, value);
47
58
  return {
@@ -49,13 +60,15 @@ function createAuthorizationCodeRequest({ code, codeVerifier, redirectURI, optio
49
60
  headers: requestHeaders
50
61
  };
51
62
  }
52
- async function validateAuthorizationCode({ code, codeVerifier, redirectURI, options, tokenEndpoint, authentication, deviceId, headers, additionalParams = {}, resource }) {
63
+ async function validateAuthorizationCode({ code, codeVerifier, redirectURI, options, tokenEndpoint, authentication, clientAssertion, deviceId, headers, additionalParams = {}, resource }) {
53
64
  const { body, headers: requestHeaders } = await authorizationCodeRequest({
54
65
  code,
55
66
  codeVerifier,
56
67
  redirectURI,
57
68
  options,
58
69
  authentication,
70
+ clientAssertion,
71
+ tokenEndpoint,
59
72
  deviceId,
60
73
  headers,
61
74
  additionalParams,
@@ -1,7 +1,7 @@
1
1
  import { logger } from "../env/logger.mjs";
2
2
  import { APIError } from "better-call";
3
- import { betterFetch } from "@better-fetch/fetch";
4
3
  import { UnsecuredJWT, createLocalJWKSet, decodeProtectedHeader, jwtVerify } from "jose";
4
+ import { betterFetch } from "@better-fetch/fetch";
5
5
  //#region src/oauth2/verify.ts
6
6
  /** Last fetched jwks used locally in getJwks @internal */
7
7
  let jwks;
@@ -2,8 +2,8 @@ import { APIError } from "../error/index.mjs";
2
2
  import { createAuthorizationURL } from "../oauth2/create-authorization-url.mjs";
3
3
  import { refreshAccessToken } from "../oauth2/refresh-access-token.mjs";
4
4
  import { validateAuthorizationCode } from "../oauth2/validate-authorization-code.mjs";
5
- import { betterFetch } from "@better-fetch/fetch";
6
5
  import { decodeJwt, decodeProtectedHeader, importJWK, jwtVerify } from "jose";
6
+ import { betterFetch } from "@better-fetch/fetch";
7
7
  //#region src/social-providers/apple.ts
8
8
  const apple = (options) => {
9
9
  const tokenEndpoint = "https://appleid.apple.com/auth/token";
@@ -3,8 +3,8 @@ import { APIError, BetterAuthError } from "../error/index.mjs";
3
3
  import { createAuthorizationURL } from "../oauth2/create-authorization-url.mjs";
4
4
  import { refreshAccessToken } from "../oauth2/refresh-access-token.mjs";
5
5
  import { validateAuthorizationCode } from "../oauth2/validate-authorization-code.mjs";
6
- import { betterFetch } from "@better-fetch/fetch";
7
6
  import { decodeJwt, decodeProtectedHeader, importJWK, jwtVerify } from "jose";
7
+ import { betterFetch } from "@better-fetch/fetch";
8
8
  //#region src/social-providers/cognito.ts
9
9
  const cognito = (options) => {
10
10
  if (!options.domain || !options.region || !options.userPoolId) {
@@ -1,8 +1,8 @@
1
1
  import { createAuthorizationURL } from "../oauth2/create-authorization-url.mjs";
2
2
  import { refreshAccessToken } from "../oauth2/refresh-access-token.mjs";
3
3
  import { validateAuthorizationCode } from "../oauth2/validate-authorization-code.mjs";
4
- import { betterFetch } from "@better-fetch/fetch";
5
4
  import { createRemoteJWKSet, decodeJwt, jwtVerify } from "jose";
5
+ import { betterFetch } from "@better-fetch/fetch";
6
6
  //#region src/social-providers/facebook.ts
7
7
  const facebook = (options) => {
8
8
  return {
@@ -3,8 +3,8 @@ import { APIError, BetterAuthError } from "../error/index.mjs";
3
3
  import { createAuthorizationURL } from "../oauth2/create-authorization-url.mjs";
4
4
  import { refreshAccessToken } from "../oauth2/refresh-access-token.mjs";
5
5
  import { validateAuthorizationCode } from "../oauth2/validate-authorization-code.mjs";
6
- import { betterFetch } from "@better-fetch/fetch";
7
6
  import { decodeJwt, decodeProtectedHeader, importJWK, jwtVerify } from "jose";
7
+ import { betterFetch } from "@better-fetch/fetch";
8
8
  //#region src/social-providers/google.ts
9
9
  const google = (options) => {
10
10
  return {
@@ -1,8 +1,8 @@
1
1
  import { createAuthorizationURL } from "../oauth2/create-authorization-url.mjs";
2
2
  import { refreshAccessToken } from "../oauth2/refresh-access-token.mjs";
3
3
  import { validateAuthorizationCode } from "../oauth2/validate-authorization-code.mjs";
4
- import { betterFetch } from "@better-fetch/fetch";
5
4
  import { decodeJwt } from "jose";
5
+ import { betterFetch } from "@better-fetch/fetch";
6
6
  //#region src/social-providers/line.ts
7
7
  /**
8
8
  * LINE Login v2.1
@@ -3,9 +3,9 @@ import { APIError } from "../error/index.mjs";
3
3
  import { createAuthorizationURL } from "../oauth2/create-authorization-url.mjs";
4
4
  import { refreshAccessToken } from "../oauth2/refresh-access-token.mjs";
5
5
  import { validateAuthorizationCode } from "../oauth2/validate-authorization-code.mjs";
6
+ import { decodeJwt, decodeProtectedHeader, importJWK, jwtVerify } from "jose";
6
7
  import { base64 } from "@better-auth/utils/base64";
7
8
  import { betterFetch } from "@better-fetch/fetch";
8
- import { decodeJwt, decodeProtectedHeader, importJWK, jwtVerify } from "jose";
9
9
  //#region src/social-providers/microsoft-entra-id.ts
10
10
  const microsoft = (options) => {
11
11
  const tenant = options.tenantId || "common";
@@ -1,9 +1,9 @@
1
1
  import { logger } from "../env/logger.mjs";
2
2
  import { BetterAuthError } from "../error/index.mjs";
3
3
  import { createAuthorizationURL } from "../oauth2/create-authorization-url.mjs";
4
+ import { decodeJwt } from "jose";
4
5
  import { base64 } from "@better-auth/utils/base64";
5
6
  import { betterFetch } from "@better-fetch/fetch";
6
- import { decodeJwt } from "jose";
7
7
  //#region src/social-providers/paypal.ts
8
8
  const paypal = (options) => {
9
9
  const isSandbox = (options.environment || "sandbox") === "sandbox";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@better-auth/core",
3
- "version": "1.6.1",
3
+ "version": "1.7.0-beta.0",
4
4
  "description": "The most comprehensive authentication framework for TypeScript.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -0,0 +1,136 @@
1
+ import type { JWTHeaderParameters } from "jose";
2
+ import { importJWK, importPKCS8, SignJWT } from "jose";
3
+
4
+ /** Asymmetric signing algorithms compatible with private_key_jwt (RFC 7523). */
5
+ export const ASSERTION_SIGNING_ALGORITHMS = [
6
+ "RS256",
7
+ "RS384",
8
+ "RS512",
9
+ "PS256",
10
+ "PS384",
11
+ "PS512",
12
+ "ES256",
13
+ "ES384",
14
+ "ES512",
15
+ "EdDSA",
16
+ ] as const;
17
+
18
+ export type AssertionSigningAlgorithm =
19
+ (typeof ASSERTION_SIGNING_ALGORITHMS)[number];
20
+
21
+ export const CLIENT_ASSERTION_TYPE =
22
+ "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
23
+
24
+ export interface ClientAssertionConfig {
25
+ /** Pre-signed JWT assertion string. If provided, signing is skipped. */
26
+ assertion?: string;
27
+ /** Private key in JWK format for signing. */
28
+ privateKeyJwk?: JsonWebKey;
29
+ /** Private key in PKCS#8 PEM format for signing. */
30
+ privateKeyPem?: string;
31
+ /** Key ID to include in the JWT header. */
32
+ kid?: string;
33
+ /** Asymmetric signing algorithm. Symmetric algorithms (HS256) and "none" are not allowed. @default "RS256" */
34
+ algorithm?: AssertionSigningAlgorithm;
35
+ /** Token endpoint URL (used as the JWT `aud` claim). */
36
+ tokenEndpoint?: string;
37
+ /** Assertion lifetime in seconds. @default 120 */
38
+ expiresIn?: number;
39
+ }
40
+
41
+ /**
42
+ * Signs an RFC 7523 client assertion JWT for `private_key_jwt` authentication.
43
+ *
44
+ * The JWT contains: iss=clientId, sub=clientId, aud=tokenEndpoint,
45
+ * exp=now+120s, jti=unique, iat=now.
46
+ */
47
+ export async function signClientAssertion({
48
+ clientId,
49
+ tokenEndpoint,
50
+ privateKeyJwk,
51
+ privateKeyPem,
52
+ kid,
53
+ algorithm,
54
+ expiresIn = 120,
55
+ }: {
56
+ clientId: string;
57
+ tokenEndpoint: string;
58
+ privateKeyJwk?: JsonWebKey;
59
+ privateKeyPem?: string;
60
+ kid?: string;
61
+ algorithm?: AssertionSigningAlgorithm;
62
+ expiresIn?: number;
63
+ }): Promise<string> {
64
+ // Fall back to JWK-embedded kid/alg when not explicitly provided (RFC 7517).
65
+ // JsonWebKey includes alg but not kid; access kid via index.
66
+ const jwk = privateKeyJwk as Record<string, unknown> | undefined;
67
+ const resolvedKid = kid ?? (jwk?.kid as string | undefined);
68
+ const resolvedAlg =
69
+ algorithm ??
70
+ (privateKeyJwk?.alg as AssertionSigningAlgorithm | undefined) ??
71
+ "RS256";
72
+
73
+ let key: Awaited<ReturnType<typeof importJWK>>;
74
+ if (privateKeyJwk) {
75
+ key = await importJWK(privateKeyJwk, resolvedAlg);
76
+ } else if (privateKeyPem) {
77
+ key = await importPKCS8(privateKeyPem, resolvedAlg);
78
+ } else {
79
+ throw new Error(
80
+ "private_key_jwt requires either privateKeyJwk or privateKeyPem",
81
+ );
82
+ }
83
+
84
+ const now = Math.floor(Date.now() / 1000);
85
+ const jti = crypto.randomUUID();
86
+
87
+ const header: JWTHeaderParameters = { alg: resolvedAlg, typ: "JWT" };
88
+ if (resolvedKid) header.kid = resolvedKid;
89
+
90
+ return new SignJWT({})
91
+ .setProtectedHeader(header)
92
+ .setIssuer(clientId)
93
+ .setSubject(clientId)
94
+ .setAudience(tokenEndpoint)
95
+ .setIssuedAt(now)
96
+ .setExpirationTime(now + expiresIn)
97
+ .setJti(jti)
98
+ .sign(key);
99
+ }
100
+
101
+ /**
102
+ * Resolves a ClientAssertionConfig into `client_assertion` + `client_assertion_type`
103
+ * params for injection into a token request body.
104
+ */
105
+ export async function resolveAssertionParams({
106
+ clientAssertion,
107
+ clientId,
108
+ tokenEndpoint,
109
+ }: {
110
+ clientAssertion: ClientAssertionConfig;
111
+ clientId: string;
112
+ tokenEndpoint?: string;
113
+ }): Promise<Record<string, string>> {
114
+ let assertion = clientAssertion.assertion;
115
+ if (!assertion) {
116
+ const audEndpoint = tokenEndpoint ?? clientAssertion.tokenEndpoint;
117
+ if (!audEndpoint) {
118
+ throw new Error(
119
+ "private_key_jwt requires a tokenEndpoint for the JWT audience claim",
120
+ );
121
+ }
122
+ assertion = await signClientAssertion({
123
+ clientId,
124
+ tokenEndpoint: audEndpoint,
125
+ privateKeyJwk: clientAssertion.privateKeyJwk,
126
+ privateKeyPem: clientAssertion.privateKeyPem,
127
+ kid: clientAssertion.kid,
128
+ algorithm: clientAssertion.algorithm,
129
+ expiresIn: clientAssertion.expiresIn,
130
+ });
131
+ }
132
+ return {
133
+ client_assertion: assertion,
134
+ client_assertion_type: CLIENT_ASSERTION_TYPE,
135
+ };
136
+ }
@@ -1,25 +1,51 @@
1
- import { base64Url } from "@better-auth/utils/base64";
1
+ import { base64 } from "@better-auth/utils/base64";
2
2
  import { betterFetch } from "@better-fetch/fetch";
3
3
  import type { AwaitableFunction } from "../types";
4
+ import type { ClientAssertionConfig } from "./client-assertion";
5
+ import { resolveAssertionParams } from "./client-assertion";
4
6
  import type { OAuth2Tokens, ProviderOptions } from "./oauth-provider";
5
7
 
6
8
  export async function clientCredentialsTokenRequest({
7
9
  options,
8
10
  scope,
9
11
  authentication,
12
+ clientAssertion,
13
+ tokenEndpoint,
10
14
  resource,
11
15
  }: {
12
- options: AwaitableFunction<ProviderOptions & { clientSecret: string }>;
16
+ options: AwaitableFunction<ProviderOptions>;
13
17
  scope?: string | undefined;
14
- authentication?: ("basic" | "post") | undefined;
18
+ authentication?: ("basic" | "post" | "private_key_jwt") | undefined;
19
+ clientAssertion?: ClientAssertionConfig | undefined;
20
+ /** Token endpoint URL. Used as the JWT `aud` claim when signing assertions. */
21
+ tokenEndpoint?: string | undefined;
15
22
  resource?: (string | string[]) | undefined;
16
23
  }) {
17
24
  options = typeof options === "function" ? await options() : options;
25
+
26
+ let extraParams: Record<string, string> | undefined;
27
+ if (authentication === "private_key_jwt") {
28
+ if (!clientAssertion) {
29
+ throw new Error(
30
+ "private_key_jwt authentication requires a clientAssertion configuration",
31
+ );
32
+ }
33
+ const primaryClientId = Array.isArray(options.clientId)
34
+ ? options.clientId[0]
35
+ : options.clientId;
36
+ extraParams = await resolveAssertionParams({
37
+ clientAssertion,
38
+ clientId: primaryClientId,
39
+ tokenEndpoint,
40
+ });
41
+ }
42
+
18
43
  return createClientCredentialsTokenRequest({
19
44
  options,
20
45
  scope,
21
46
  authentication,
22
47
  resource,
48
+ extraParams,
23
49
  });
24
50
  }
25
51
 
@@ -31,11 +57,13 @@ export function createClientCredentialsTokenRequest({
31
57
  scope,
32
58
  authentication,
33
59
  resource,
60
+ extraParams,
34
61
  }: {
35
- options: ProviderOptions & { clientSecret: string };
62
+ options: ProviderOptions;
36
63
  scope?: string | undefined;
37
- authentication?: ("basic" | "post") | undefined;
64
+ authentication?: ("basic" | "post" | "private_key_jwt") | undefined;
38
65
  resource?: (string | string[]) | undefined;
66
+ extraParams?: Record<string, string> | undefined;
39
67
  }) {
40
68
  const body = new URLSearchParams();
41
69
  const headers: Record<string, any> = {
@@ -54,20 +82,25 @@ export function createClientCredentialsTokenRequest({
54
82
  }
55
83
  }
56
84
  }
85
+ const primaryClientId = Array.isArray(options.clientId)
86
+ ? options.clientId[0]
87
+ : options.clientId;
57
88
  if (authentication === "basic") {
58
- const primaryClientId = Array.isArray(options.clientId)
59
- ? options.clientId[0]
60
- : options.clientId;
61
- const encodedCredentials = base64Url.encode(
62
- `${primaryClientId}:${options.clientSecret}`,
89
+ const encodedCredentials = base64.encode(
90
+ `${primaryClientId}:${options.clientSecret ?? ""}`,
63
91
  );
64
92
  headers["authorization"] = `Basic ${encodedCredentials}`;
65
93
  } else {
66
- const primaryClientId = Array.isArray(options.clientId)
67
- ? options.clientId[0]
68
- : options.clientId;
69
94
  body.set("client_id", primaryClientId);
70
- body.set("client_secret", options.clientSecret);
95
+ if (authentication !== "private_key_jwt" && options.clientSecret) {
96
+ body.set("client_secret", options.clientSecret);
97
+ }
98
+ }
99
+
100
+ if (extraParams) {
101
+ for (const [key, value] of Object.entries(extraParams)) {
102
+ if (!body.has(key)) body.append(key, value);
103
+ }
71
104
  }
72
105
 
73
106
  return {
@@ -81,18 +114,22 @@ export async function clientCredentialsToken({
81
114
  tokenEndpoint,
82
115
  scope,
83
116
  authentication,
117
+ clientAssertion,
84
118
  resource,
85
119
  }: {
86
- options: AwaitableFunction<ProviderOptions & { clientSecret: string }>;
120
+ options: AwaitableFunction<ProviderOptions>;
87
121
  tokenEndpoint: string;
88
122
  scope: string;
89
- authentication?: ("basic" | "post") | undefined;
123
+ authentication?: ("basic" | "post" | "private_key_jwt") | undefined;
124
+ clientAssertion?: ClientAssertionConfig | undefined;
90
125
  resource?: (string | string[]) | undefined;
91
126
  }): Promise<OAuth2Tokens> {
92
127
  const { body, headers } = await clientCredentialsTokenRequest({
93
128
  options,
94
129
  scope,
95
130
  authentication,
131
+ clientAssertion,
132
+ tokenEndpoint,
96
133
  resource,
97
134
  });
98
135
 
@@ -1,3 +1,13 @@
1
+ export type {
2
+ AssertionSigningAlgorithm,
3
+ ClientAssertionConfig,
4
+ } from "./client-assertion";
5
+ export {
6
+ ASSERTION_SIGNING_ALGORITHMS,
7
+ CLIENT_ASSERTION_TYPE,
8
+ resolveAssertionParams,
9
+ signClientAssertion,
10
+ } from "./client-assertion";
1
11
  export {
2
12
  clientCredentialsToken,
3
13
  clientCredentialsTokenRequest,
@@ -1,22 +1,47 @@
1
1
  import { base64 } from "@better-auth/utils/base64";
2
2
  import { betterFetch } from "@better-fetch/fetch";
3
3
  import type { AwaitableFunction } from "../types";
4
+ import type { ClientAssertionConfig } from "./client-assertion";
5
+ import { resolveAssertionParams } from "./client-assertion";
4
6
  import type { OAuth2Tokens, ProviderOptions } from "./oauth-provider";
5
7
 
6
8
  export async function refreshAccessTokenRequest({
7
9
  refreshToken,
8
10
  options,
9
11
  authentication,
12
+ clientAssertion,
13
+ tokenEndpoint,
10
14
  extraParams,
11
15
  resource,
12
16
  }: {
13
17
  refreshToken: string;
14
18
  options: AwaitableFunction<Partial<ProviderOptions>>;
15
- authentication?: ("basic" | "post") | undefined;
19
+ authentication?: ("basic" | "post" | "private_key_jwt") | undefined;
20
+ clientAssertion?: ClientAssertionConfig | undefined;
21
+ /** Token endpoint URL. Used as the JWT `aud` claim when signing assertions. */
22
+ tokenEndpoint?: string | undefined;
16
23
  extraParams?: Record<string, string> | undefined;
17
24
  resource?: (string | string[]) | undefined;
18
25
  }) {
19
26
  options = typeof options === "function" ? await options() : options;
27
+
28
+ if (authentication === "private_key_jwt") {
29
+ if (!clientAssertion) {
30
+ throw new Error(
31
+ "private_key_jwt authentication requires a clientAssertion configuration",
32
+ );
33
+ }
34
+ const primaryClientId = Array.isArray(options.clientId)
35
+ ? options.clientId[0]
36
+ : options.clientId;
37
+ const assertionParams = await resolveAssertionParams({
38
+ clientAssertion,
39
+ clientId: primaryClientId,
40
+ tokenEndpoint,
41
+ });
42
+ extraParams = { ...extraParams, ...assertionParams };
43
+ }
44
+
20
45
  return createRefreshAccessTokenRequest({
21
46
  refreshToken,
22
47
  options,
@@ -38,7 +63,7 @@ export function createRefreshAccessTokenRequest({
38
63
  }: {
39
64
  refreshToken: string;
40
65
  options: ProviderOptions;
41
- authentication?: ("basic" | "post") | undefined;
66
+ authentication?: ("basic" | "post" | "private_key_jwt") | undefined;
42
67
  extraParams?: Record<string, string> | undefined;
43
68
  resource?: (string | string[]) | undefined;
44
69
  }) {
@@ -50,12 +75,10 @@ export function createRefreshAccessTokenRequest({
50
75
 
51
76
  body.set("grant_type", "refresh_token");
52
77
  body.set("refresh_token", refreshToken);
53
- // Use standard Base64 encoding for HTTP Basic Auth (OAuth2 spec, RFC 7617)
54
- // Fixes compatibility with providers like Notion, Twitter, etc.
78
+ const primaryClientId = Array.isArray(options.clientId)
79
+ ? options.clientId[0]
80
+ : options.clientId;
55
81
  if (authentication === "basic") {
56
- const primaryClientId = Array.isArray(options.clientId)
57
- ? options.clientId[0]
58
- : options.clientId;
59
82
  if (primaryClientId) {
60
83
  headers["authorization"] =
61
84
  "Basic " +
@@ -65,11 +88,8 @@ export function createRefreshAccessTokenRequest({
65
88
  "Basic " + base64.encode(`:${options.clientSecret ?? ""}`);
66
89
  }
67
90
  } else {
68
- const primaryClientId = Array.isArray(options.clientId)
69
- ? options.clientId[0]
70
- : options.clientId;
71
91
  body.set("client_id", primaryClientId);
72
- if (options.clientSecret) {
92
+ if (authentication !== "private_key_jwt" && options.clientSecret) {
73
93
  body.set("client_secret", options.clientSecret);
74
94
  }
75
95
  }
@@ -100,18 +120,22 @@ export async function refreshAccessToken({
100
120
  options,
101
121
  tokenEndpoint,
102
122
  authentication,
123
+ clientAssertion,
103
124
  extraParams,
104
125
  }: {
105
126
  refreshToken: string;
106
127
  options: Partial<ProviderOptions>;
107
128
  tokenEndpoint: string;
108
- authentication?: ("basic" | "post") | undefined;
129
+ authentication?: ("basic" | "post" | "private_key_jwt") | undefined;
130
+ clientAssertion?: ClientAssertionConfig | undefined;
109
131
  extraParams?: Record<string, string> | undefined;
110
132
  }): Promise<OAuth2Tokens> {
111
- const { body, headers } = await createRefreshAccessTokenRequest({
133
+ const { body, headers } = await refreshAccessTokenRequest({
112
134
  refreshToken,
113
135
  options,
114
136
  authentication,
137
+ clientAssertion,
138
+ tokenEndpoint,
115
139
  extraParams,
116
140
  });
117
141
 
@@ -2,6 +2,8 @@ import { base64 } from "@better-auth/utils/base64";
2
2
  import { betterFetch } from "@better-fetch/fetch";
3
3
  import { createRemoteJWKSet, jwtVerify } from "jose";
4
4
  import type { AwaitableFunction } from "../types";
5
+ import type { ClientAssertionConfig } from "./client-assertion";
6
+ import { resolveAssertionParams } from "./client-assertion";
5
7
  import type { ProviderOptions } from "./index";
6
8
  import { getOAuth2Tokens } from "./index";
7
9
 
@@ -11,6 +13,8 @@ export async function authorizationCodeRequest({
11
13
  redirectURI,
12
14
  options,
13
15
  authentication,
16
+ clientAssertion,
17
+ tokenEndpoint,
14
18
  deviceId,
15
19
  headers,
16
20
  additionalParams = {},
@@ -21,12 +25,33 @@ export async function authorizationCodeRequest({
21
25
  options: AwaitableFunction<Partial<ProviderOptions>>;
22
26
  codeVerifier?: string | undefined;
23
27
  deviceId?: string | undefined;
24
- authentication?: ("basic" | "post") | undefined;
28
+ authentication?: ("basic" | "post" | "private_key_jwt") | undefined;
29
+ clientAssertion?: ClientAssertionConfig | undefined;
30
+ /** Token endpoint URL. Used as the JWT `aud` claim when signing assertions. */
31
+ tokenEndpoint?: string | undefined;
25
32
  headers?: Record<string, string> | undefined;
26
33
  additionalParams?: Record<string, string> | undefined;
27
34
  resource?: (string | string[]) | undefined;
28
35
  }) {
29
36
  options = typeof options === "function" ? await options() : options;
37
+
38
+ if (authentication === "private_key_jwt") {
39
+ if (!clientAssertion) {
40
+ throw new Error(
41
+ "private_key_jwt authentication requires a clientAssertion configuration",
42
+ );
43
+ }
44
+ const primaryClientId = Array.isArray(options.clientId)
45
+ ? options.clientId[0]
46
+ : options.clientId;
47
+ const assertionParams = await resolveAssertionParams({
48
+ clientAssertion,
49
+ clientId: primaryClientId,
50
+ tokenEndpoint,
51
+ });
52
+ additionalParams = { ...additionalParams, ...assertionParams };
53
+ }
54
+
30
55
  return createAuthorizationCodeRequest({
31
56
  code,
32
57
  codeVerifier,
@@ -59,7 +84,7 @@ export function createAuthorizationCodeRequest({
59
84
  options: Partial<ProviderOptions>;
60
85
  codeVerifier?: string | undefined;
61
86
  deviceId?: string | undefined;
62
- authentication?: ("basic" | "post") | undefined;
87
+ authentication?: ("basic" | "post" | "private_key_jwt") | undefined;
63
88
  headers?: Record<string, string> | undefined;
64
89
  additionalParams?: Record<string, string> | undefined;
65
90
  resource?: (string | string[]) | undefined;
@@ -86,22 +111,17 @@ export function createAuthorizationCodeRequest({
86
111
  }
87
112
  }
88
113
  }
89
- // Use standard Base64 encoding for HTTP Basic Auth (OAuth2 spec, RFC 7617)
90
- // Fixes compatibility with providers like Notion, Twitter, etc.
114
+ const primaryClientId = Array.isArray(options.clientId)
115
+ ? options.clientId[0]
116
+ : options.clientId;
91
117
  if (authentication === "basic") {
92
- const primaryClientId = Array.isArray(options.clientId)
93
- ? options.clientId[0]
94
- : options.clientId;
95
118
  const encodedCredentials = base64.encode(
96
119
  `${primaryClientId}:${options.clientSecret ?? ""}`,
97
120
  );
98
121
  requestHeaders["authorization"] = `Basic ${encodedCredentials}`;
99
122
  } else {
100
- const primaryClientId = Array.isArray(options.clientId)
101
- ? options.clientId[0]
102
- : options.clientId;
103
123
  body.set("client_id", primaryClientId);
104
- if (options.clientSecret) {
124
+ if (authentication !== "private_key_jwt" && options.clientSecret) {
105
125
  body.set("client_secret", options.clientSecret);
106
126
  }
107
127
  }
@@ -123,6 +143,7 @@ export async function validateAuthorizationCode({
123
143
  options,
124
144
  tokenEndpoint,
125
145
  authentication,
146
+ clientAssertion,
126
147
  deviceId,
127
148
  headers,
128
149
  additionalParams = {},
@@ -134,7 +155,8 @@ export async function validateAuthorizationCode({
134
155
  codeVerifier?: string | undefined;
135
156
  deviceId?: string | undefined;
136
157
  tokenEndpoint: string;
137
- authentication?: ("basic" | "post") | undefined;
158
+ authentication?: ("basic" | "post" | "private_key_jwt") | undefined;
159
+ clientAssertion?: ClientAssertionConfig | undefined;
138
160
  headers?: Record<string, string> | undefined;
139
161
  additionalParams?: Record<string, string> | undefined;
140
162
  resource?: (string | string[]) | undefined;
@@ -145,6 +167,8 @@ export async function validateAuthorizationCode({
145
167
  redirectURI,
146
168
  options,
147
169
  authentication,
170
+ clientAssertion,
171
+ tokenEndpoint,
148
172
  deviceId,
149
173
  headers,
150
174
  additionalParams,