@better-auth/oauth-provider 1.5.3 → 1.5.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { a as ResourceServerMetadata } from "./oauth-7Jc-EFsq.mjs";
1
+ import { a as ResourceServerMetadata } from "./oauth-Chk8ejPr.mjs";
2
2
  import { JWTPayload, JWTVerifyOptions } from "jose";
3
3
  import { Auth } from "better-auth/types";
4
4
 
@@ -1,4 +1,4 @@
1
- import { a as getJwtPlugin, m as handleMcpErrors, o as getOAuthProviderPlugin } from "./utils-D6kv_BUA.mjs";
1
+ import { a as getJwtPlugin, h as handleMcpErrors, o as getOAuthProviderPlugin } from "./utils-DgozotLg.mjs";
2
2
  import { verifyAccessToken } from "better-auth/oauth2";
3
3
  import { APIError } from "better-call";
4
4
  import { logger } from "@better-auth/core/env";
package/dist/client.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import "./oauth-7Jc-EFsq.mjs";
2
- import { n as oauthProvider } from "./oauth-C_QoLKZA.mjs";
1
+ import "./oauth-Chk8ejPr.mjs";
2
+ import { n as oauthProvider } from "./oauth-wm0HlZm9.mjs";
3
3
  import * as _better_fetch_fetch0 from "@better-fetch/fetch";
4
4
 
5
5
  //#region src/client.d.ts
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { _ as Awaitable, a as ResourceServerMetadata, c as OAuthConsent, d as OAuthRefreshToken, f as Prompt, g as VerificationValue, h as StoreTokenType, i as OIDCMetadata, l as OAuthOpaqueAccessToken, m as Scope, n as GrantType, o as AuthorizePrompt, p as SchemaClient, r as OAuthClient, s as OAuthAuthorizationQuery, t as AuthServerMetadata, u as OAuthOptions } from "./oauth-7Jc-EFsq.mjs";
2
- import { n as oauthProvider, t as getOAuthProviderState } from "./oauth-C_QoLKZA.mjs";
1
+ import { _ as Awaitable, a as ResourceServerMetadata, c as OAuthConsent, d as OAuthRefreshToken, f as Prompt, g as VerificationValue, h as StoreTokenType, i as OIDCMetadata, l as OAuthOpaqueAccessToken, m as Scope, n as GrantType, o as AuthorizePrompt, p as SchemaClient, r as OAuthClient, s as OAuthAuthorizationQuery, t as AuthServerMetadata, u as OAuthOptions } from "./oauth-Chk8ejPr.mjs";
2
+ import { n as oauthProvider, t as getOAuthProviderState } from "./oauth-wm0HlZm9.mjs";
3
3
  import { verifyAccessToken } from "better-auth/oauth2";
4
4
  import { JWSAlgorithms, JwtOptions } from "better-auth/plugins";
5
5
  import { JWTPayload } from "jose";
package/dist/index.mjs CHANGED
@@ -1,7 +1,8 @@
1
- import { a as getJwtPlugin, c as isPKCERequired, d as storeClientSecret, f as storeToken, h as mcpHandler, i as getClient, l as parseClientMetadata, n as decryptStoredClientSecret, p as validateClientCredentials, r as deleteFromPrompt, s as getStoredToken, t as basicToClientCredentials, u as parsePrompt } from "./utils-D6kv_BUA.mjs";
1
+ import { a as getJwtPlugin, c as isPKCERequired, d as resolveSubjectIdentifier, f as storeClientSecret, g as mcpHandler, i as getClient, l as parseClientMetadata, m as validateClientCredentials, n as decryptStoredClientSecret, p as storeToken, r as deleteFromPrompt, s as getStoredToken, t as basicToClientCredentials, u as parsePrompt } from "./utils-DgozotLg.mjs";
2
2
  import { APIError, createAuthEndpoint, createAuthMiddleware, getOAuthState, getSessionFromCtx, sessionMiddleware } from "better-auth/api";
3
3
  import { generateCodeChallenge, getJwks, verifyJwsAccessToken } from "better-auth/oauth2";
4
4
  import { APIError as APIError$1 } from "better-call";
5
+ import { isBrowserFetchRequest } from "@better-auth/core/utils/fetch-metadata";
5
6
  import { constantTimeEqual, generateRandomString, makeSignature } from "better-auth/crypto";
6
7
  import { defineRequestState } from "@better-auth/core/context";
7
8
  import { logger } from "@better-auth/core/env";
@@ -206,6 +207,13 @@ async function userInfoEndpoint(ctx, opts) {
206
207
  error: "invalid_request"
207
208
  });
208
209
  const baseUserClaims = userNormalClaims(user, scopes ?? []);
210
+ if (opts.pairwiseSecret) {
211
+ const clientId = jwt.client_id ?? jwt.azp;
212
+ if (clientId) {
213
+ const client = await getClient(ctx, opts, clientId);
214
+ if (client) baseUserClaims.sub = await resolveSubjectIdentifier(user.id, client, opts);
215
+ }
216
+ }
209
217
  const additionalInfoUserClaims = opts.customUserInfoClaims && scopes?.length ? await opts.customUserInfoClaims({
210
218
  user,
211
219
  scopes,
@@ -277,6 +285,7 @@ async function createIdToken(ctx, opts, user, client, scopes, nonce, sessionId,
277
285
  const iat = Math.floor(Date.now() / 1e3);
278
286
  const exp = iat + (opts.idTokenExpiresIn ?? 36e3);
279
287
  const userClaims = userNormalClaims(user, scopes);
288
+ const resolvedSub = await resolveSubjectIdentifier(user.id, client, opts);
280
289
  const authTimeSec = authTime != null ? Math.floor(authTime.getTime() / 1e3) : void 0;
281
290
  const acr = "urn:mace:incommon:iap:bronze";
282
291
  const customClaims = opts.customIdTokenClaims ? await opts.customIdTokenClaims({
@@ -291,7 +300,7 @@ async function createIdToken(ctx, opts, user, client, scopes, nonce, sessionId,
291
300
  auth_time: authTimeSec,
292
301
  acr,
293
302
  iss: jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL,
294
- sub: user.id,
303
+ sub: resolvedSub,
295
304
  aud: client.clientId,
296
305
  nonce,
297
306
  iat,
@@ -908,6 +917,21 @@ async function validateAccessToken(ctx, opts, token, clientId) {
908
917
  error: "invalid_request"
909
918
  });
910
919
  }
920
+ /**
921
+ * Resolves pairwise sub on an introspection payload.
922
+ * Applied at the presentation layer so internal validation functions
923
+ * keep real user.id (needed for user lookup in /userinfo).
924
+ */
925
+ async function resolveIntrospectionSub(opts, payload, client) {
926
+ if (payload.active && payload.sub) {
927
+ const resolvedSub = await resolveSubjectIdentifier(payload.sub, client, opts);
928
+ return {
929
+ ...payload,
930
+ sub: resolvedSub
931
+ };
932
+ }
933
+ return payload;
934
+ }
911
935
  async function introspectEndpoint(ctx, opts) {
912
936
  let { client_id, client_secret, token, token_type_hint } = ctx.body;
913
937
  const authorization = ctx.request?.headers.get("authorization") || null;
@@ -928,7 +952,7 @@ async function introspectEndpoint(ctx, opts) {
928
952
  const client = await validateClientCredentials(ctx, opts, client_id, client_secret);
929
953
  try {
930
954
  if (token_type_hint === void 0 || token_type_hint === "access_token") try {
931
- return await validateAccessToken(ctx, opts, token, client.clientId);
955
+ return resolveIntrospectionSub(opts, await validateAccessToken(ctx, opts, token, client.clientId), client);
932
956
  } catch (error) {
933
957
  if (error instanceof APIError$1) {
934
958
  if (token_type_hint === "access_token") throw error;
@@ -936,7 +960,7 @@ async function introspectEndpoint(ctx, opts) {
936
960
  else throw new Error(error);
937
961
  }
938
962
  if (token_type_hint === void 0 || token_type_hint === "refresh_token") try {
939
- return await validateRefreshToken(ctx, opts, (await decodeRefreshToken(opts, token)).token, client.clientId);
963
+ return resolveIntrospectionSub(opts, await validateRefreshToken(ctx, opts, (await decodeRefreshToken(opts, token)).token, client.clientId), client);
940
964
  } catch (error) {
941
965
  if (error instanceof APIError$1) {
942
966
  if (token_type_hint === "refresh_token") throw error;
@@ -1116,6 +1140,22 @@ async function checkOAuthClient(client, opts, settings) {
1116
1140
  error: "invalid_client_metadata",
1117
1141
  error_description: "When 'authorization_code' grant type is used, 'code' response type must be included"
1118
1142
  });
1143
+ if (client.subject_type !== void 0) {
1144
+ if (client.subject_type !== "public" && client.subject_type !== "pairwise") throw new APIError("BAD_REQUEST", {
1145
+ error: "invalid_client_metadata",
1146
+ error_description: `subject_type must be "public" or "pairwise"`
1147
+ });
1148
+ if (client.subject_type === "pairwise" && !opts.pairwiseSecret) throw new APIError("BAD_REQUEST", {
1149
+ error: "invalid_client_metadata",
1150
+ error_description: "pairwise subject_type requires server pairwiseSecret configuration"
1151
+ });
1152
+ if (client.subject_type === "pairwise" && client.redirect_uris && client.redirect_uris.length > 1) {
1153
+ if (new Set(client.redirect_uris.map((uri) => new URL(uri).host)).size > 1) throw new APIError("BAD_REQUEST", {
1154
+ error: "invalid_client_metadata",
1155
+ error_description: "pairwise clients with redirect_uris on different hosts require a sector_identifier_uri, which is not yet supported. All redirect_uris must share the same host."
1156
+ });
1157
+ }
1158
+ }
1119
1159
  const requestedScopes = (client?.scope)?.split(" ").filter((v) => v.length);
1120
1160
  const allowedScopes = settings?.isRegister ? opts.clientRegistrationAllowedScopes ?? opts.scopes : opts.scopes;
1121
1161
  if (allowedScopes) {
@@ -1182,7 +1222,7 @@ async function createOAuthClientEndpoint(ctx, opts, settings) {
1182
1222
  * @returns
1183
1223
  */
1184
1224
  function oauthToSchema(input) {
1185
- const { client_id: clientId, client_secret: clientSecret, client_secret_expires_at: _expiresAt, scope: _scope, user_id: userId, client_id_issued_at: _createdAt, client_name: name, client_uri: uri, logo_uri: icon, contacts, tos_uri: tos, policy_uri: policy, jwks: _jwks, jwks_uri: _jwksUri, software_id: softwareId, software_version: softwareVersion, software_statement: softwareStatement, redirect_uris: redirectUris, post_logout_redirect_uris: postLogoutRedirectUris, token_endpoint_auth_method: tokenEndpointAuthMethod, grant_types: grantTypes, response_types: responseTypes, public: _public, type, disabled, skip_consent: skipConsent, enable_end_session: enableEndSession, require_pkce: requirePKCE, reference_id: referenceId, metadata: inputMetadata, ...rest } = input;
1225
+ const { client_id: clientId, client_secret: clientSecret, client_secret_expires_at: _expiresAt, scope: _scope, user_id: userId, client_id_issued_at: _createdAt, client_name: name, client_uri: uri, logo_uri: icon, contacts, tos_uri: tos, policy_uri: policy, jwks: _jwks, jwks_uri: _jwksUri, software_id: softwareId, software_version: softwareVersion, software_statement: softwareStatement, redirect_uris: redirectUris, post_logout_redirect_uris: postLogoutRedirectUris, token_endpoint_auth_method: tokenEndpointAuthMethod, grant_types: grantTypes, response_types: responseTypes, public: _public, type, disabled, skip_consent: skipConsent, enable_end_session: enableEndSession, require_pkce: requirePKCE, subject_type: subjectType, reference_id: referenceId, metadata: inputMetadata, ...rest } = input;
1186
1226
  const expiresAt = _expiresAt ? /* @__PURE__ */ new Date(_expiresAt * 1e3) : void 0;
1187
1227
  const createdAt = _createdAt ? /* @__PURE__ */ new Date(_createdAt * 1e3) : void 0;
1188
1228
  const scopes = _scope?.split(" ");
@@ -1217,6 +1257,7 @@ function oauthToSchema(input) {
1217
1257
  skipConsent,
1218
1258
  enableEndSession,
1219
1259
  requirePKCE,
1260
+ subjectType,
1220
1261
  referenceId,
1221
1262
  metadata: Object.keys(metadataObj).length ? JSON.stringify(metadataObj) : void 0
1222
1263
  };
@@ -1228,7 +1269,7 @@ function oauthToSchema(input) {
1228
1269
  * @returns
1229
1270
  */
1230
1271
  function schemaToOAuth(input) {
1231
- const { clientId, clientSecret, disabled, scopes, userId, createdAt, updatedAt: _updatedAt, expiresAt, name, uri, icon, contacts, tos, policy, softwareId, softwareVersion, softwareStatement, redirectUris, postLogoutRedirectUris, tokenEndpointAuthMethod, grantTypes, responseTypes, public: _public, type, skipConsent, enableEndSession, requirePKCE, referenceId, metadata } = input;
1272
+ const { clientId, clientSecret, disabled, scopes, userId, createdAt, updatedAt: _updatedAt, expiresAt, name, uri, icon, contacts, tos, policy, softwareId, softwareVersion, softwareStatement, redirectUris, postLogoutRedirectUris, tokenEndpointAuthMethod, grantTypes, responseTypes, public: _public, type, skipConsent, enableEndSession, requirePKCE, subjectType, referenceId, metadata } = input;
1232
1273
  const _expiresAt = expiresAt ? Math.round(new Date(expiresAt).getTime() / 1e3) : void 0;
1233
1274
  const _createdAt = createdAt ? Math.round(new Date(createdAt).getTime() / 1e3) : void 0;
1234
1275
  const _scopes = scopes?.join(" ");
@@ -1260,6 +1301,7 @@ function schemaToOAuth(input) {
1260
1301
  skip_consent: skipConsent ?? void 0,
1261
1302
  enable_end_session: enableEndSession ?? void 0,
1262
1303
  require_pkce: requirePKCE ?? void 0,
1304
+ subject_type: subjectType ?? void 0,
1263
1305
  reference_id: referenceId ?? void 0
1264
1306
  };
1265
1307
  }
@@ -1572,6 +1614,7 @@ const adminCreateOAuthClient = (opts) => createAuthEndpoint("/admin/oauth2/creat
1572
1614
  skip_consent: z.boolean().optional(),
1573
1615
  enable_end_session: z.boolean().optional(),
1574
1616
  require_pkce: z.boolean().optional(),
1617
+ subject_type: z.enum(["public", "pairwise"]).optional(),
1575
1618
  metadata: z.record(z.string(), z.unknown()).optional()
1576
1619
  }),
1577
1620
  metadata: {
@@ -2366,6 +2409,10 @@ const schema = {
2366
2409
  type: "boolean",
2367
2410
  required: false
2368
2411
  },
2412
+ subjectType: {
2413
+ type: "string",
2414
+ required: false
2415
+ },
2369
2416
  scopes: {
2370
2417
  type: "string[]",
2371
2418
  required: false
@@ -2662,6 +2709,7 @@ const oauthProvider = (options) => {
2662
2709
  claims: Array.from(claims),
2663
2710
  clientRegistrationAllowedScopes
2664
2711
  };
2712
+ if (opts.pairwiseSecret && opts.pairwiseSecret.length < 32) throw new BetterAuthError("pairwiseSecret must be at least 32 characters long for adequate HMAC-SHA256 security");
2665
2713
  if (opts.grantTypes && opts.grantTypes.includes("refresh_token") && !opts.grantTypes.includes("authorization_code")) throw new BetterAuthError("refresh_token grant requires authorization_code grant");
2666
2714
  if (opts.disableJwtPlugin && (opts.storeClientSecret === "hashed" || typeof opts.storeClientSecret === "object" && "hash" in opts.storeClientSecret)) throw new BetterAuthError("unable to store hashed secrets because id tokens will be signed with secret");
2667
2715
  if (!opts.disableJwtPlugin && (opts.storeClientSecret === "encrypted" || typeof opts.storeClientSecret === "object" && ("encrypt" in opts.storeClientSecret || "decrypt" in opts.storeClientSecret))) throw new BetterAuthError("encryption method not recommended, please use 'hashed' or the 'hash' function");
@@ -3371,7 +3419,8 @@ const oauthProvider = (options) => {
3371
3419
  "web",
3372
3420
  "native",
3373
3421
  "user-agent-based"
3374
- ]).optional()
3422
+ ]).optional(),
3423
+ subject_type: z.enum(["public", "pairwise"]).optional()
3375
3424
  }),
3376
3425
  metadata: { openapi: {
3377
3426
  description: "Register an OAuth2 application",
@@ -3576,7 +3625,9 @@ function formatErrorURL(url, error, description, state, iss) {
3576
3625
  return `${url}${url.includes("?") ? "&" : "?"}${searchParams.toString()}`;
3577
3626
  }
3578
3627
  const handleRedirect = (ctx, uri) => {
3579
- if (ctx.headers?.get("accept")?.includes("application/json")) return {
3628
+ const fromFetch = isBrowserFetchRequest(ctx.request?.headers);
3629
+ const acceptJson = ctx.headers?.get("accept")?.includes("application/json");
3630
+ if (fromFetch || acceptJson) return {
3580
3631
  redirect: true,
3581
3632
  url: uri.toString()
3582
3633
  };
@@ -3818,7 +3869,7 @@ function oidcServerMetadata(ctx, opts) {
3818
3869
  }),
3819
3870
  claims_supported: opts?.advertisedMetadata?.claims_supported ?? opts?.claims ?? [],
3820
3871
  userinfo_endpoint: `${baseURL}/oauth2/userinfo`,
3821
- subject_types_supported: ["public"],
3872
+ subject_types_supported: opts.pairwiseSecret ? ["public", "pairwise"] : ["public"],
3822
3873
  id_token_signing_alg_values_supported: jwtPluginOptions?.jwks?.keyPairConfig?.alg ? [jwtPluginOptions?.jwks?.keyPairConfig?.alg] : opts.disableJwtPlugin ? ["HS256"] : ["EdDSA"],
3823
3874
  end_session_endpoint: `${baseURL}/oauth2/end-session`,
3824
3875
  acr_values_supported: ["urn:mace:incommon:iap:bronze"],