@better-auth/oauth-provider 1.7.9 → 1.8.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.
- package/dist/{authorize-D8r6mvZx.mjs → authorize-92EtLsBi.mjs} +735 -502
- package/dist/client-resource.d.mts +1 -1
- package/dist/client-resource.mjs +2 -2
- package/dist/client.d.mts +1 -1
- package/dist/client.mjs +1 -1
- package/dist/index-sGnh4IfY.d.mts +275 -0
- package/dist/index.d.mts +4 -203
- package/dist/index.mjs +198 -41
- package/dist/internal.d.mts +108 -40
- package/dist/internal.mjs +4 -2
- package/dist/{introspect-7WCMPe5Z.mjs → introspect-CEo9Q06v.mjs} +318 -237
- package/dist/{oauth-DAMtT2vg.d.mts → oauth-BlEuA8oD.d.mts} +380 -20
- package/dist/{oauth-BWgg8X-A.d.mts → oauth-mdkaIwmm.d.mts} +191 -86
- package/dist/{utils-DKU5wblX.mjs → utils-BMNe8Sg2.mjs} +343 -5
- package/dist/{version-DVUrz-nv.mjs → version-DeABnmP9.mjs} +1 -1
- package/package.json +6 -6
|
@@ -6,6 +6,146 @@ import { BetterAuthError } from "@better-auth/core/error";
|
|
|
6
6
|
import { CLIENT_ASSERTION_TYPE, decodeBasicCredentials } from "@better-auth/core/oauth2";
|
|
7
7
|
import { base64Url } from "@better-auth/utils/base64";
|
|
8
8
|
import { createHash } from "@better-auth/utils/hash";
|
|
9
|
+
//#region src/client-metadata.ts
|
|
10
|
+
const OAUTH_CLIENT_RECORD_FIELD_NAMES = Object.freeze(Object.keys({
|
|
11
|
+
clientId: true,
|
|
12
|
+
clientSecret: true,
|
|
13
|
+
clientDiscoveryId: true,
|
|
14
|
+
disabled: true,
|
|
15
|
+
scopes: true,
|
|
16
|
+
clientCredentialsScopes: true,
|
|
17
|
+
userId: true,
|
|
18
|
+
createdAt: true,
|
|
19
|
+
updatedAt: true,
|
|
20
|
+
expiresAt: true,
|
|
21
|
+
name: true,
|
|
22
|
+
uri: true,
|
|
23
|
+
icon: true,
|
|
24
|
+
contacts: true,
|
|
25
|
+
tos: true,
|
|
26
|
+
policy: true,
|
|
27
|
+
softwareId: true,
|
|
28
|
+
softwareVersion: true,
|
|
29
|
+
softwareStatement: true,
|
|
30
|
+
redirectUris: true,
|
|
31
|
+
postLogoutRedirectUris: true,
|
|
32
|
+
backchannelLogoutUri: true,
|
|
33
|
+
backchannelLogoutSessionRequired: true,
|
|
34
|
+
frontchannelLogoutUri: true,
|
|
35
|
+
frontchannelLogoutSessionRequired: true,
|
|
36
|
+
tokenEndpointAuthMethod: true,
|
|
37
|
+
grantTypes: true,
|
|
38
|
+
responseTypes: true,
|
|
39
|
+
applicationType: true,
|
|
40
|
+
jwks: true,
|
|
41
|
+
jwksUri: true,
|
|
42
|
+
requirePKCE: true,
|
|
43
|
+
dpopBoundAccessTokens: true,
|
|
44
|
+
skipConsent: true,
|
|
45
|
+
enableEndSession: true,
|
|
46
|
+
subjectType: true,
|
|
47
|
+
sectorIdentifier: true,
|
|
48
|
+
referenceId: true,
|
|
49
|
+
metadata: true
|
|
50
|
+
}));
|
|
51
|
+
/**
|
|
52
|
+
* Canonical wire fields emitted by schemaToOAuth. They are mapped explicitly
|
|
53
|
+
* and therefore must never also survive inside the opaque metadata envelope.
|
|
54
|
+
*/
|
|
55
|
+
const OAUTH_CLIENT_WIRE_FIELD_NAMES = [
|
|
56
|
+
"client_id",
|
|
57
|
+
"client_secret",
|
|
58
|
+
"client_secret_expires_at",
|
|
59
|
+
"scope",
|
|
60
|
+
"user_id",
|
|
61
|
+
"client_id_issued_at",
|
|
62
|
+
"client_name",
|
|
63
|
+
"client_uri",
|
|
64
|
+
"logo_uri",
|
|
65
|
+
"contacts",
|
|
66
|
+
"tos_uri",
|
|
67
|
+
"policy_uri",
|
|
68
|
+
"jwks",
|
|
69
|
+
"jwks_uri",
|
|
70
|
+
"software_id",
|
|
71
|
+
"software_version",
|
|
72
|
+
"software_statement",
|
|
73
|
+
"redirect_uris",
|
|
74
|
+
"post_logout_redirect_uris",
|
|
75
|
+
"backchannel_logout_uri",
|
|
76
|
+
"backchannel_logout_session_required",
|
|
77
|
+
"frontchannel_logout_uri",
|
|
78
|
+
"frontchannel_logout_session_required",
|
|
79
|
+
"token_endpoint_auth_method",
|
|
80
|
+
"grant_types",
|
|
81
|
+
"response_types",
|
|
82
|
+
"application_type",
|
|
83
|
+
"disabled",
|
|
84
|
+
"skip_consent",
|
|
85
|
+
"enable_end_session",
|
|
86
|
+
"require_pkce",
|
|
87
|
+
"dpop_bound_access_tokens",
|
|
88
|
+
"subject_type",
|
|
89
|
+
"reference_id"
|
|
90
|
+
];
|
|
91
|
+
const OPAQUE_METADATA_RESERVED_FIELDS = new Set([
|
|
92
|
+
...OAUTH_CLIENT_RECORD_FIELD_NAMES,
|
|
93
|
+
...OAUTH_CLIENT_WIRE_FIELD_NAMES,
|
|
94
|
+
"public",
|
|
95
|
+
"type",
|
|
96
|
+
"resources",
|
|
97
|
+
"client_credentials_scopes"
|
|
98
|
+
]);
|
|
99
|
+
const CIMD_FORBIDDEN_SERVER_FIELD_NAMES = new Set([
|
|
100
|
+
"disabled",
|
|
101
|
+
"client_secret",
|
|
102
|
+
"client_secret_expires_at",
|
|
103
|
+
"client_id_issued_at",
|
|
104
|
+
"skip_consent",
|
|
105
|
+
"enable_end_session",
|
|
106
|
+
"require_pkce",
|
|
107
|
+
"reference_id",
|
|
108
|
+
"user_id",
|
|
109
|
+
"resources",
|
|
110
|
+
"clientSecret",
|
|
111
|
+
"clientDiscoveryId",
|
|
112
|
+
"sectorIdentifier",
|
|
113
|
+
"skipConsent",
|
|
114
|
+
"enableEndSession",
|
|
115
|
+
"requirePKCE",
|
|
116
|
+
"referenceId",
|
|
117
|
+
"userId",
|
|
118
|
+
"clientId",
|
|
119
|
+
"applicationType",
|
|
120
|
+
"tokenEndpointAuthMethod",
|
|
121
|
+
"redirectUris",
|
|
122
|
+
"postLogoutRedirectUris",
|
|
123
|
+
"grantTypes",
|
|
124
|
+
"responseTypes",
|
|
125
|
+
"scopes",
|
|
126
|
+
"expiresAt",
|
|
127
|
+
"createdAt",
|
|
128
|
+
"updatedAt",
|
|
129
|
+
"softwareId",
|
|
130
|
+
"softwareVersion",
|
|
131
|
+
"softwareStatement",
|
|
132
|
+
"backchannelLogoutUri",
|
|
133
|
+
"backchannelLogoutSessionRequired",
|
|
134
|
+
"jwksUri",
|
|
135
|
+
"dpopBoundAccessTokens",
|
|
136
|
+
"subjectType"
|
|
137
|
+
]);
|
|
138
|
+
function isForbiddenCimdClientMetadataField(field) {
|
|
139
|
+
return CIMD_FORBIDDEN_SERVER_FIELD_NAMES.has(field);
|
|
140
|
+
}
|
|
141
|
+
function isReservedOAuthClientMetadataField(field) {
|
|
142
|
+
return OPAQUE_METADATA_RESERVED_FIELDS.has(field);
|
|
143
|
+
}
|
|
144
|
+
function stripReservedOAuthClientMetadataExtensions(metadata) {
|
|
145
|
+
if (!metadata) return void 0;
|
|
146
|
+
return Object.fromEntries(Object.entries(metadata).filter(([field]) => !OPAQUE_METADATA_RESERVED_FIELDS.has(field)));
|
|
147
|
+
}
|
|
148
|
+
//#endregion
|
|
9
149
|
//#region src/extensions.ts
|
|
10
150
|
const DEFAULT_GRANT_TYPES = [
|
|
11
151
|
"authorization_code",
|
|
@@ -70,11 +210,16 @@ function collectExtensionKeys(extension) {
|
|
|
70
210
|
}
|
|
71
211
|
const clientDiscoveryIds = (extension.clientDiscovery ? Array.isArray(extension.clientDiscovery) ? extension.clientDiscovery : [extension.clientDiscovery] : []).map((discovery) => discovery.id);
|
|
72
212
|
for (const clientDiscoveryId of clientDiscoveryIds) assertNonEmptyExtensionValue("client discovery id", clientDiscoveryId);
|
|
213
|
+
for (const field of extension.clientMetadata?.fields ?? []) {
|
|
214
|
+
assertNonEmptyExtensionValue("client metadata field", field);
|
|
215
|
+
if (isReservedOAuthClientMetadataField(field)) throw new BetterAuthError(`OAuth Provider extension client metadata field is reserved: ${field}`);
|
|
216
|
+
}
|
|
73
217
|
return {
|
|
74
218
|
grantTypes,
|
|
75
219
|
authMethods,
|
|
76
220
|
assertionTypes,
|
|
77
|
-
clientDiscoveryIds
|
|
221
|
+
clientDiscoveryIds,
|
|
222
|
+
interactionRequestParameters: Object.keys(extension.interactionRequests ?? {})
|
|
78
223
|
};
|
|
79
224
|
}
|
|
80
225
|
function assertNoDuplicateAcrossExtensions(label, values) {
|
|
@@ -97,6 +242,7 @@ function validateOAuthProviderExtensions(extensions) {
|
|
|
97
242
|
assertNoDuplicateAcrossExtensions("token_endpoint_auth_method", keys.flatMap((k) => k.authMethods));
|
|
98
243
|
assertNoDuplicateAcrossExtensions("client_assertion_type", keys.flatMap((k) => k.assertionTypes));
|
|
99
244
|
assertNoDuplicateAcrossExtensions("client discovery id", keys.flatMap((k) => k.clientDiscoveryIds));
|
|
245
|
+
assertNoDuplicateAcrossExtensions("interaction request parameter", keys.flatMap((k) => k.interactionRequestParameters));
|
|
100
246
|
}
|
|
101
247
|
function getOAuthProviderExtensions(opts) {
|
|
102
248
|
return opts.extensions ?? [];
|
|
@@ -153,6 +299,23 @@ function getExtensionGrantTypes(opts) {
|
|
|
153
299
|
function getSupportedGrantTypes(opts) {
|
|
154
300
|
return Array.from(new Set([...opts.grantTypes ?? DEFAULT_GRANT_TYPES, ...getExtensionGrantTypes(opts)]));
|
|
155
301
|
}
|
|
302
|
+
/**
|
|
303
|
+
* Finds the interaction request a query identifies, such as a device approval
|
|
304
|
+
* keyed by `user_code`. Authorization requests, which carry `response_type` or
|
|
305
|
+
* a pushed `request_uri`, never match.
|
|
306
|
+
*/
|
|
307
|
+
function getExtensionInteractionRequest(opts, query) {
|
|
308
|
+
const parameters = query;
|
|
309
|
+
if (parameters.response_type !== void 0 || parameters.request_uri !== void 0) return;
|
|
310
|
+
for (const extension of getOAuthProviderExtensions(opts)) for (const [parameter, handler] of Object.entries(extension.interactionRequests ?? {})) {
|
|
311
|
+
const value = parameters[parameter];
|
|
312
|
+
if (typeof value === "string" && value) return {
|
|
313
|
+
parameter,
|
|
314
|
+
handler,
|
|
315
|
+
value
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
}
|
|
156
319
|
function getExtensionGrantHandler(opts, grantType) {
|
|
157
320
|
for (const extension of getOAuthProviderExtensions(opts)) {
|
|
158
321
|
const handler = extension.grants?.[grantType];
|
|
@@ -230,6 +393,29 @@ function collectExtensionIdTokenClaims(opts, input) {
|
|
|
230
393
|
function collectExtensionUserInfoClaims(opts, input) {
|
|
231
394
|
return collectClaims(opts, (extension) => extension.claims?.userInfo?.(input));
|
|
232
395
|
}
|
|
396
|
+
/** Whether any extension declares client metadata fields or a validator. */
|
|
397
|
+
function hasExtensionClientMetadata(opts) {
|
|
398
|
+
return getOAuthProviderExtensions(opts).some((extension) => extension.clientMetadata);
|
|
399
|
+
}
|
|
400
|
+
/** Registration fields that extensions define, stored in the client's `metadata`. */
|
|
401
|
+
function getExtensionClientMetadataFields(opts) {
|
|
402
|
+
return new Set(getOAuthProviderExtensions(opts).flatMap((extension) => extension.clientMetadata?.fields ?? []));
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Runs every extension's client metadata validator, in registration order.
|
|
406
|
+
* Extensions read their fields from `metadata` only, so `metadata` and
|
|
407
|
+
* declared fields are removed from the client they receive; a metadata
|
|
408
|
+
* document carries declared fields at its top level.
|
|
409
|
+
*/
|
|
410
|
+
async function validateExtensionClientMetadata(opts, client, metadata) {
|
|
411
|
+
const { metadata: _metadata, ...coreClient } = client;
|
|
412
|
+
const extensionFields = getExtensionClientMetadataFields(opts);
|
|
413
|
+
const input = {
|
|
414
|
+
client: Object.fromEntries(Object.entries(coreClient).filter(([field]) => !extensionFields.has(field))),
|
|
415
|
+
metadata: stripReservedOAuthClientMetadataExtensions(metadata) ?? {}
|
|
416
|
+
};
|
|
417
|
+
for (const extension of getOAuthProviderExtensions(opts)) await extension.clientMetadata?.validate?.(input);
|
|
418
|
+
}
|
|
233
419
|
/**
|
|
234
420
|
* Whether any registered extension contributes UserInfo claims. Lets the
|
|
235
421
|
* UserInfo endpoint skip loading the client when nothing needs it.
|
|
@@ -371,6 +557,16 @@ async function verifyOAuthQueryParams(oauth_query, secret) {
|
|
|
371
557
|
return sigs.length === 1 && !!sig && constantTimeEqual(sig, verifySig) && /* @__PURE__ */ new Date(exp * 1e3) >= /* @__PURE__ */ new Date();
|
|
372
558
|
}
|
|
373
559
|
/**
|
|
560
|
+
* Rejects changes to a client listed in `cachedTrustedClients`. Cached trusted
|
|
561
|
+
* clients are immutable through the client management endpoints.
|
|
562
|
+
*/
|
|
563
|
+
function assertNotTrustedClient(opts, clientId) {
|
|
564
|
+
if (opts.cachedTrustedClients?.has(clientId)) throw new APIError("BAD_REQUEST", {
|
|
565
|
+
error: "invalid_client",
|
|
566
|
+
error_description: "trusted clients must be updated manually"
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
374
570
|
* Get a client by ID, checking trusted clients first, then database
|
|
375
571
|
*/
|
|
376
572
|
async function getClient(ctx, options, clientId) {
|
|
@@ -531,6 +727,53 @@ function throwInvalidAuthenticationRequest(errorDescription) {
|
|
|
531
727
|
});
|
|
532
728
|
}
|
|
533
729
|
/**
|
|
730
|
+
* Re-reads a request's raw `application/x-www-form-urlencoded` body, keeping
|
|
731
|
+
* every value of a repeated key. better-call's form-body parser keeps only the
|
|
732
|
+
* last one, so `resource=https://a&resource=https://b` reaches the handler as
|
|
733
|
+
* `{ resource: "https://b" }`. Query strings are unaffected: the router keeps
|
|
734
|
+
* repeated query keys as arrays.
|
|
735
|
+
*
|
|
736
|
+
* Returns `undefined` for other media types or an unreadable or empty body.
|
|
737
|
+
* The endpoint MUST set `cloneRequest: true`, because the body stream is read
|
|
738
|
+
* a second time.
|
|
739
|
+
*
|
|
740
|
+
* @internal
|
|
741
|
+
*/
|
|
742
|
+
async function readFormParams(request) {
|
|
743
|
+
if (!(request.headers.get("content-type")?.toLowerCase() ?? "").includes("application/x-www-form-urlencoded")) return;
|
|
744
|
+
let text;
|
|
745
|
+
try {
|
|
746
|
+
text = await request.clone().text();
|
|
747
|
+
} catch {
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
return text ? new URLSearchParams(text) : void 0;
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* Applies RFC 6749 §3.2 to a form body the parser collapsed to one value per
|
|
754
|
+
* key: a parameter sent without a value counts as omitted, and a parameter
|
|
755
|
+
* sent more than once is rejected with `invalid_request`. A name in
|
|
756
|
+
* `repeatable`, such as RFC 8707 `resource`, keeps every value as an array
|
|
757
|
+
* when it repeats. A value a `before` hook replaced is left as the hook set it.
|
|
758
|
+
*
|
|
759
|
+
* @internal
|
|
760
|
+
*/
|
|
761
|
+
function applyFormParams(params, body, repeatable = []) {
|
|
762
|
+
const merged = { ...body };
|
|
763
|
+
for (const name of new Set(params.keys())) {
|
|
764
|
+
const raw = params.getAll(name);
|
|
765
|
+
const values = raw.filter(Boolean);
|
|
766
|
+
if (values.length > 1 && !repeatable.includes(name)) throw new APIError("BAD_REQUEST", {
|
|
767
|
+
error: "invalid_request",
|
|
768
|
+
error_description: `${name} must not be repeated`
|
|
769
|
+
});
|
|
770
|
+
if (merged[name] !== raw.at(-1)) continue;
|
|
771
|
+
if (values.length === 0) delete merged[name];
|
|
772
|
+
else merged[name] = values.length === 1 ? values[0] : values;
|
|
773
|
+
}
|
|
774
|
+
return merged;
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
534
777
|
* Normalizes client identification and authentication parameters while
|
|
535
778
|
* enforcing RFC 6749 §2.3 and RFC 8628 §3.1 cardinality. Empty values are
|
|
536
779
|
* omitted. Client identification remains separate from authentication-method
|
|
@@ -609,6 +852,37 @@ function clientAllowsGrant(client, grantType) {
|
|
|
609
852
|
return allowedGrants.includes(grantType);
|
|
610
853
|
}
|
|
611
854
|
/**
|
|
855
|
+
* Throws an OAuth error response (RFC 6749 §5.2).
|
|
856
|
+
*
|
|
857
|
+
* @internal
|
|
858
|
+
*/
|
|
859
|
+
function tokenError(status, error, errorDescription) {
|
|
860
|
+
throw new APIError(status, {
|
|
861
|
+
error,
|
|
862
|
+
error_description: errorDescription
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
/**
|
|
866
|
+
* Splits a space-delimited OAuth `scope` value (RFC 6749 §3.3).
|
|
867
|
+
*
|
|
868
|
+
* @internal
|
|
869
|
+
*/
|
|
870
|
+
function parseScopes(scope) {
|
|
871
|
+
const normalized = scope?.trim();
|
|
872
|
+
return normalized ? normalized.split(/\s+/) : [];
|
|
873
|
+
}
|
|
874
|
+
/**
|
|
875
|
+
* Whether a stored consent covers every requested scope and resource. Shared by
|
|
876
|
+
* authorization and grants that need prior consent, so the coverage rule cannot
|
|
877
|
+
* drift between them. Each caller still chooses which consent rows to check.
|
|
878
|
+
*
|
|
879
|
+
* @internal
|
|
880
|
+
*/
|
|
881
|
+
function consentCovers(consent, scopes, resources) {
|
|
882
|
+
const consentedResources = consent.resources ?? [];
|
|
883
|
+
return scopes.every((scope) => consent.scopes.includes(scope)) && resources.every((resource) => consentedResources.includes(resource));
|
|
884
|
+
}
|
|
885
|
+
/**
|
|
612
886
|
* Validates requested scopes against a registered client's allowed scopes.
|
|
613
887
|
*
|
|
614
888
|
* @internal
|
|
@@ -640,7 +914,7 @@ function throwInvalidClient(errorDescription, authentication) {
|
|
|
640
914
|
*/
|
|
641
915
|
async function validateClientCredentials(ctx, options, clientId, clientSecret, scopes, preVerified, grantType, authMethod) {
|
|
642
916
|
const client = await getClient(ctx, options, clientId);
|
|
643
|
-
if (!client) throwInvalidClient("
|
|
917
|
+
if (!client) throwInvalidClient("client not found", { method: authMethod });
|
|
644
918
|
if (client.disabled) throwInvalidClient("client is disabled", { method: authMethod });
|
|
645
919
|
const registeredAuthMethod = client.tokenEndpointAuthMethod ?? "client_secret_basic";
|
|
646
920
|
if (authMethod && registeredAuthMethod !== authMethod) throwInvalidClient(`client registered for ${registeredAuthMethod} cannot use ${authMethod}`, { method: authMethod });
|
|
@@ -712,7 +986,7 @@ async function extractClientCredentials(ctx, opts, expectedAudience) {
|
|
|
712
986
|
confirmation: result.confirmation
|
|
713
987
|
};
|
|
714
988
|
}
|
|
715
|
-
const { verifyClientAssertion: verify } = await import("./authorize-
|
|
989
|
+
const { verifyClientAssertion: verify } = await import("./authorize-92EtLsBi.mjs").then((n) => n.s);
|
|
716
990
|
return {
|
|
717
991
|
kind: "pre_verified",
|
|
718
992
|
method: "private_key_jwt",
|
|
@@ -761,12 +1035,31 @@ function parsePrompt(prompt) {
|
|
|
761
1035
|
return new Set(set);
|
|
762
1036
|
}
|
|
763
1037
|
/**
|
|
764
|
-
*
|
|
1038
|
+
* Derives the pairwise sector to store for a client.
|
|
1039
|
+
*
|
|
1040
|
+
* Only an `https` host identifies who controls a redirect URI, so clients
|
|
1041
|
+
* whose redirect URIs are all `https` share the sector of that host. Any app
|
|
1042
|
+
* can register a custom scheme, and any local process can listen on a
|
|
1043
|
+
* loopback port, so every other client gets a sector of its own. `/` cannot
|
|
1044
|
+
* appear in a URL host, so a client sector never equals a host sector.
|
|
1045
|
+
*
|
|
1046
|
+
* @see https://openid.net/specs/openid-connect-core-1_0.html#PairwiseAlg
|
|
1047
|
+
* @internal
|
|
1048
|
+
*/
|
|
1049
|
+
function deriveSectorIdentifier(clientId, redirectUris) {
|
|
1050
|
+
const urls = (redirectUris ?? []).map((uri) => new URL(uri));
|
|
1051
|
+
const first = urls[0];
|
|
1052
|
+
return first && urls.every((url) => url.protocol === "https:") ? first.host : `client/${clientId}`;
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* Returns the client's stored pairwise sector, or for a client created before
|
|
1056
|
+
* sectors were stored, the host of its first redirect URI.
|
|
765
1057
|
*
|
|
766
1058
|
* @see https://openid.net/specs/openid-connect-core-1_0.html#PairwiseAlg
|
|
767
1059
|
* @internal
|
|
768
1060
|
*/
|
|
769
1061
|
function getSectorIdentifier(client) {
|
|
1062
|
+
if (client.sectorIdentifier) return client.sectorIdentifier;
|
|
770
1063
|
const uri = client.redirectUris?.[0];
|
|
771
1064
|
if (!uri) throw new BetterAuthError("Client has no redirect URIs for sector identifier");
|
|
772
1065
|
return new URL(uri).host;
|
|
@@ -791,6 +1084,51 @@ async function resolveSubjectIdentifier(userId, client, opts) {
|
|
|
791
1084
|
return userId;
|
|
792
1085
|
}
|
|
793
1086
|
/**
|
|
1087
|
+
* Resolves `postLogin.consentReferenceId`, treating an empty value as no reference.
|
|
1088
|
+
*
|
|
1089
|
+
* @internal
|
|
1090
|
+
*/
|
|
1091
|
+
async function getConsentReferenceId(opts, context) {
|
|
1092
|
+
return await opts.postLogin?.consentReferenceId?.(context) || void 0;
|
|
1093
|
+
}
|
|
1094
|
+
/**
|
|
1095
|
+
* Finds the consent stored for exactly this reference. Without a reference,
|
|
1096
|
+
* only consent stored without one (missing, null, or empty) matches.
|
|
1097
|
+
*
|
|
1098
|
+
* @internal
|
|
1099
|
+
*/
|
|
1100
|
+
async function findConsent(ctx, clientId, userId, referenceId) {
|
|
1101
|
+
const where = [{
|
|
1102
|
+
field: "clientId",
|
|
1103
|
+
value: clientId
|
|
1104
|
+
}, {
|
|
1105
|
+
field: "userId",
|
|
1106
|
+
value: userId
|
|
1107
|
+
}];
|
|
1108
|
+
if (referenceId) return ctx.context.adapter.findOne({
|
|
1109
|
+
model: "oauthConsent",
|
|
1110
|
+
where: [...where, {
|
|
1111
|
+
field: "referenceId",
|
|
1112
|
+
value: referenceId
|
|
1113
|
+
}]
|
|
1114
|
+
});
|
|
1115
|
+
const pageSize = 100;
|
|
1116
|
+
for (let offset = 0;; offset += pageSize) {
|
|
1117
|
+
const consents = await ctx.context.adapter.findMany({
|
|
1118
|
+
model: "oauthConsent",
|
|
1119
|
+
where,
|
|
1120
|
+
limit: pageSize,
|
|
1121
|
+
offset,
|
|
1122
|
+
sortBy: {
|
|
1123
|
+
field: "id",
|
|
1124
|
+
direction: "asc"
|
|
1125
|
+
}
|
|
1126
|
+
});
|
|
1127
|
+
const consent = consents.find((row) => !row.referenceId);
|
|
1128
|
+
if (consent || consents.length < pageSize) return consent ?? null;
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
/**
|
|
794
1132
|
* Converts URLSearchParams to a plain object, preserving
|
|
795
1133
|
* multi-valued keys as arrays instead of discarding duplicates.
|
|
796
1134
|
*/
|
|
@@ -849,4 +1187,4 @@ function isPKCERequired(client, request) {
|
|
|
849
1187
|
return false;
|
|
850
1188
|
}
|
|
851
1189
|
//#endregion
|
|
852
|
-
export {
|
|
1190
|
+
export { validateOAuthProviderExtensions as $, storeClientSecret as A, collectExtensionIdTokenClaims as B, parseScopes as C, resolveSessionAuthTime as D, removePromptFromQuery as E, validateClientCredentials as F, getExtensionGrantHandler as G, extendOAuthProvider as H, validateClientScopes as I, getSupportedGrantTypes as J, getExtensionInteractionRequest as K, verifyOAuthQueryParams as L, toAudienceClaim as M, toResourceList as N, resolveSubjectIdentifier as O, tokenError as P, validateExtensionClientMetadata as Q, applyOAuthProviderMetadataExtensions as R, parsePrompt as S, removeMaxAgeFromQuery as T, getClientDiscoveries as U, collectExtensionUserInfoClaims as V, getExtensionClientMetadataFields as W, hasUserInfoClaimExtension as X, hasExtensionClientMetadata as Y, isExtensionTokenEndpointAuthMethod as Z, mergeDiscoveryMetadata as _, decryptStoredClientSecret as a, parseBearerToken as b, extractClientCredentials as c, getConsentReferenceId as d, isForbiddenCimdClientMetadataField as et, getJwtPlugin as f, isSessionFreshForSignedQuery as g, isPKCERequired as h, consentCovers as i, storeToken as j, searchParamsToQuery as k, findConsent as l, getStoredToken as m, assertNotTrustedClient as n, deriveSectorIdentifier as o, getOAuthProviderPlugin as p, getSupportedAuthMethods as q, clientAllowsGrant as r, destructureCredentials as s, applyFormParams as t, stripReservedOAuthClientMetadataExtensions as tt, getClient as u, normalizeClientAuthenticationParameters as v, readFormParams as w, parseClientMetadata as x, normalizeTimestampValue as y, collectExtensionAccessTokenClaims as z };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@better-auth/oauth-provider",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0-beta.0",
|
|
4
4
|
"description": "An oauth provider plugin for Better Auth",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -71,16 +71,16 @@
|
|
|
71
71
|
"devDependencies": {
|
|
72
72
|
"listhen": "^1.9.0",
|
|
73
73
|
"tsdown": "0.21.10",
|
|
74
|
-
"@better-auth/core": "1.
|
|
75
|
-
"@better-auth/memory-adapter": "1.
|
|
76
|
-
"better-auth": "1.
|
|
74
|
+
"@better-auth/core": "1.8.0-beta.0",
|
|
75
|
+
"@better-auth/memory-adapter": "1.8.0-beta.0",
|
|
76
|
+
"better-auth": "1.8.0-beta.0"
|
|
77
77
|
},
|
|
78
78
|
"peerDependencies": {
|
|
79
79
|
"@better-auth/utils": "^0.4.2 || ^0.5.0",
|
|
80
80
|
"@better-fetch/fetch": "1.3.2",
|
|
81
81
|
"better-call": "1.4.2",
|
|
82
|
-
"@better-auth/core": "^1.
|
|
83
|
-
"better-auth": "^1.
|
|
82
|
+
"@better-auth/core": "^1.8.0-beta.0",
|
|
83
|
+
"better-auth": "^1.8.0-beta.0"
|
|
84
84
|
},
|
|
85
85
|
"scripts": {
|
|
86
86
|
"build": "tsdown",
|