@hyper.authio/client 0.1.1 → 0.8.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/README.md +139 -11
- package/dist/index.cjs +919 -49
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +880 -117
- package/dist/index.d.ts +880 -117
- package/dist/index.js +888 -51
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -20,6 +20,8 @@ var AuthioClaimTypes = {
|
|
|
20
20
|
AuthenticationMethods: "amr",
|
|
21
21
|
IdentityProvider: "idp",
|
|
22
22
|
Nonce: "nonce",
|
|
23
|
+
AccessTokenHash: "at_hash",
|
|
24
|
+
Events: "events",
|
|
23
25
|
OrganizationId: "org_id",
|
|
24
26
|
OrganizationAlias: "org_alias",
|
|
25
27
|
AccountId: "account_id",
|
|
@@ -39,6 +41,12 @@ var AuthioClaimTypes = {
|
|
|
39
41
|
ConsumerName: "consumer_name",
|
|
40
42
|
CredentialId: "credential_id",
|
|
41
43
|
CredentialType: "credential_type",
|
|
44
|
+
CredentialName: "credential_name",
|
|
45
|
+
CredentialRealm: "credential_realm",
|
|
46
|
+
CredentialClient: "credential_client",
|
|
47
|
+
ActorType: "actor_type",
|
|
48
|
+
OwnerUserId: "owner_user_id",
|
|
49
|
+
OwnerUsername: "owner_username",
|
|
42
50
|
UserResourceAccess: "user_resource_access",
|
|
43
51
|
RealmResourceAccess: "realm_resource_access",
|
|
44
52
|
ClientResourceAccess: "client_resource_access",
|
|
@@ -96,7 +104,13 @@ var AuthioOAuthErrors = {
|
|
|
96
104
|
ConnectionSelectionRequired: "connection_selection_required",
|
|
97
105
|
ConnectionPolicyViolation: "connection_policy_violation",
|
|
98
106
|
ServerError: "server_error",
|
|
99
|
-
TemporarilyUnavailable: "temporarily_unavailable"
|
|
107
|
+
TemporarilyUnavailable: "temporarily_unavailable",
|
|
108
|
+
LoginRequired: "login_required",
|
|
109
|
+
InteractionRequired: "interaction_required",
|
|
110
|
+
ConsentRequired: "consent_required",
|
|
111
|
+
AccountSelectionRequired: "account_selection_required",
|
|
112
|
+
InsufficientScope: "insufficient_scope",
|
|
113
|
+
InsufficientUserAuthentication: "insufficient_user_authentication"
|
|
100
114
|
};
|
|
101
115
|
var AuthioWebhookEventTypes = {
|
|
102
116
|
UserCreated: "user_created",
|
|
@@ -113,6 +127,8 @@ var AuthioWebhookEventTypes = {
|
|
|
113
127
|
AdminResourceDeleted: "admin_resource_deleted",
|
|
114
128
|
UserSuspended: "user_suspended",
|
|
115
129
|
UserSuspensionEnded: "user_suspension_ended",
|
|
130
|
+
AttackDetected: "attack_detected",
|
|
131
|
+
CredentialCompromised: "credential_compromised",
|
|
116
132
|
Ping: "ping"
|
|
117
133
|
};
|
|
118
134
|
var AuthioWebhookHeaders = {
|
|
@@ -120,6 +136,8 @@ var AuthioWebhookHeaders = {
|
|
|
120
136
|
Event: "X-Authio-Event",
|
|
121
137
|
EventId: "X-Authio-Event-Id"
|
|
122
138
|
};
|
|
139
|
+
var AuthioCallerKinds = ["user", "client", "consumer", "account_credential"];
|
|
140
|
+
var AuthioPrompts = ["none", "login", "select_account", "consent"];
|
|
123
141
|
var AuthioAsymmetricAlgorithms = [
|
|
124
142
|
"RS256",
|
|
125
143
|
"RS384",
|
|
@@ -131,14 +149,27 @@ var AuthioAsymmetricAlgorithms = [
|
|
|
131
149
|
"ES384",
|
|
132
150
|
"ES512"
|
|
133
151
|
];
|
|
152
|
+
var AuthioHmacAlgorithms = ["HS256", "HS384", "HS512"];
|
|
153
|
+
var AuthioHmacMinimumSecretBytes = Object.freeze({ HS256: 32, HS384: 48, HS512: 64 });
|
|
134
154
|
var AuthioDefaults = {
|
|
135
155
|
DiscoveryPath: "/.well-known/openid-configuration",
|
|
156
|
+
ProtectedResourceMetadataPath: "/.well-known/oauth-protected-resource",
|
|
136
157
|
RedactedValue: "[redacted]",
|
|
137
158
|
PlatformRealm: "authio",
|
|
138
159
|
PlatformClientId: "authio",
|
|
139
160
|
ApiKeyPrefix: "ak_",
|
|
140
161
|
ClientSecretPrefix: "cs_",
|
|
141
162
|
PersonalAccessTokenPrefix: "pat_",
|
|
163
|
+
/** The `client_id` an account credential's client secret is exchanged with: `cred_` and the credential id (format N). */
|
|
164
|
+
CredentialClientIdPrefix: "cred_",
|
|
165
|
+
ApiKeyHeader: "x-api-key",
|
|
166
|
+
RequestIdHeader: "X-Request-Id",
|
|
167
|
+
/** The header `typ` of a back-channel logout token. */
|
|
168
|
+
LogoutTokenType: "logout+jwt",
|
|
169
|
+
/** The event a back-channel logout token carries. */
|
|
170
|
+
BackchannelLogoutEvent: "http://schemas.openid.net/event/backchannel-logout",
|
|
171
|
+
/** Seconds. */
|
|
172
|
+
ClockSkew: 30,
|
|
142
173
|
/** Seconds. */
|
|
143
174
|
ClientAssertionLifetime: 120,
|
|
144
175
|
/** Seconds. */
|
|
@@ -190,6 +221,47 @@ var AuthioDiscoveryError = class extends AuthioError {
|
|
|
190
221
|
};
|
|
191
222
|
var AuthioConfigurationError = class extends AuthioError {
|
|
192
223
|
};
|
|
224
|
+
var AuthioTokenValidationError = class extends AuthioError {
|
|
225
|
+
/** Why, as a stable code. */
|
|
226
|
+
reason;
|
|
227
|
+
constructor(reason, message, options) {
|
|
228
|
+
super(message, options);
|
|
229
|
+
this.reason = reason;
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
var AuthioCallbackError = class extends AuthioError {
|
|
233
|
+
/** Why, as a stable code. */
|
|
234
|
+
reason;
|
|
235
|
+
constructor(reason, message) {
|
|
236
|
+
super(message);
|
|
237
|
+
this.reason = reason;
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
var AuthioAdminError = class extends AuthioError {
|
|
241
|
+
/** The HTTP status. */
|
|
242
|
+
status;
|
|
243
|
+
/** Authio's error code, such as `ValidationFailed` or `RealmNotFound`. */
|
|
244
|
+
errorCode;
|
|
245
|
+
/** The messages, with the field each one is about. */
|
|
246
|
+
notifications;
|
|
247
|
+
/** The request id, from the envelope or the `X-Request-Id` header. */
|
|
248
|
+
requestId;
|
|
249
|
+
/** Seconds to wait before retrying, from `Retry-After` (`429` and `503`). */
|
|
250
|
+
retryAfterSeconds;
|
|
251
|
+
constructor(details) {
|
|
252
|
+
const notifications = details.notifications ?? [];
|
|
253
|
+
const summary = notifications.map((notification) => notification.message).filter(Boolean).join(" ");
|
|
254
|
+
super(
|
|
255
|
+
`The Authio Admin API answered HTTP ${details.status}${details.errorCode ? ` (${details.errorCode})` : ""}${summary ? `: ${summary}` : "."}`,
|
|
256
|
+
{ cause: details.cause }
|
|
257
|
+
);
|
|
258
|
+
this.status = details.status;
|
|
259
|
+
this.errorCode = details.errorCode;
|
|
260
|
+
this.notifications = Object.freeze([...notifications]);
|
|
261
|
+
this.requestId = details.requestId;
|
|
262
|
+
this.retryAfterSeconds = details.retryAfterSeconds;
|
|
263
|
+
}
|
|
264
|
+
};
|
|
193
265
|
|
|
194
266
|
// src/options.ts
|
|
195
267
|
var systemClock = {
|
|
@@ -210,6 +282,12 @@ var systemClock = {
|
|
|
210
282
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
211
283
|
})
|
|
212
284
|
};
|
|
285
|
+
var AuthioTokenEndpointAuthMethods = [
|
|
286
|
+
"client_secret_basic",
|
|
287
|
+
"client_secret_post",
|
|
288
|
+
"private_key_jwt",
|
|
289
|
+
"none"
|
|
290
|
+
];
|
|
213
291
|
function resolveConnection(options) {
|
|
214
292
|
const requireHttps = options.requireHttps ?? true;
|
|
215
293
|
const issuer = resolveIssuer(options);
|
|
@@ -525,7 +603,9 @@ function createClientAuthenticator(settings, connection) {
|
|
|
525
603
|
const clientId = settings.clientId?.trim();
|
|
526
604
|
if (!clientId) throw new AuthioConfigurationError("The Authio clientId is required.");
|
|
527
605
|
const hasKey = settings.clientPrivateKey !== void 0 || !!settings.clientPrivateKeyPath;
|
|
528
|
-
const method = settings.
|
|
606
|
+
const method = settings.tokenEndpointAuthMethod ?? (settings.clientSecret ? "client_secret_basic" : hasKey ? "private_key_jwt" : "none");
|
|
607
|
+
if (!AuthioTokenEndpointAuthMethods.includes(method))
|
|
608
|
+
throw new AuthioConfigurationError(`The Authio tokenEndpointAuthMethod '${String(method)}' is not one of ${AuthioTokenEndpointAuthMethods.join(", ")}.`);
|
|
529
609
|
if ((method === "client_secret_basic" || method === "client_secret_post") && !settings.clientSecret)
|
|
530
610
|
throw new AuthioConfigurationError(`The Authio clientSecret is required with ${method}.`);
|
|
531
611
|
if (method === "private_key_jwt" && !hasKey)
|
|
@@ -536,17 +616,22 @@ function createClientAuthenticator(settings, connection) {
|
|
|
536
616
|
`The Authio clientAssertionLifetime must be more than 0 and at most ${AuthioDefaults.MaxClientAssertionLifetime} seconds.`
|
|
537
617
|
);
|
|
538
618
|
const loadKey = method === "private_key_jwt" ? createKeyLoader(settings) : void 0;
|
|
619
|
+
const secret = async () => {
|
|
620
|
+
const value = typeof settings.clientSecret === "function" ? await settings.clientSecret() : settings.clientSecret;
|
|
621
|
+
if (typeof value !== "string" || !value) throw new AuthioConfigurationError("The Authio clientSecret function returned no secret.");
|
|
622
|
+
return value;
|
|
623
|
+
};
|
|
539
624
|
return {
|
|
540
625
|
clientId,
|
|
541
626
|
method,
|
|
542
627
|
async apply(form, headers) {
|
|
543
628
|
switch (method) {
|
|
544
629
|
case "client_secret_basic":
|
|
545
|
-
headers.set("authorization", toBasicAuthorization(clientId,
|
|
630
|
+
headers.set("authorization", toBasicAuthorization(clientId, await secret()));
|
|
546
631
|
return;
|
|
547
632
|
case "client_secret_post":
|
|
548
633
|
form.set("client_id", clientId);
|
|
549
|
-
form.set("client_secret",
|
|
634
|
+
form.set("client_secret", await secret());
|
|
550
635
|
return;
|
|
551
636
|
case "private_key_jwt":
|
|
552
637
|
form.set("client_id", clientId);
|
|
@@ -600,17 +685,28 @@ function createKeyLoader(settings) {
|
|
|
600
685
|
};
|
|
601
686
|
}
|
|
602
687
|
var AuthioKeyManagementAlgorithms = ["RSA-OAEP", "RSA-OAEP-256"];
|
|
688
|
+
var AuthioLegacyKeyManagementAlgorithms = ["RSA1_5"];
|
|
603
689
|
var AuthioContentEncryptionAlgorithms = ["A128CBC-HS256", "A192CBC-HS384", "A256CBC-HS512"];
|
|
690
|
+
var AuthioSupportedContentEncryptionAlgorithms = [...AuthioContentEncryptionAlgorithms, "A128GCM", "A192GCM", "A256GCM"];
|
|
604
691
|
var rsaAlgorithmIdentifier = [48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 1, 5, 0];
|
|
605
692
|
function isAuthioTokenEncrypted(token) {
|
|
606
693
|
return typeof token === "string" && token.split(".").length === 5;
|
|
607
694
|
}
|
|
608
|
-
function createAuthioTokenDecrypter(key,
|
|
695
|
+
function createAuthioTokenDecrypter(key, options = {}) {
|
|
696
|
+
const settings = typeof options === "string" ? { setting: options } : options;
|
|
697
|
+
const setting = settings.setting ?? "clientEncryptionKey";
|
|
698
|
+
const keyManagementAlgorithms = resolveAllowlist(settings.encryptionAlgorithms, AuthioKeyManagementAlgorithms, AuthioKeyManagementAlgorithms, "encryptionAlgorithms");
|
|
699
|
+
const contentEncryptionAlgorithms = resolveAllowlist(
|
|
700
|
+
settings.encryptionMethods,
|
|
701
|
+
AuthioContentEncryptionAlgorithms,
|
|
702
|
+
AuthioSupportedContentEncryptionAlgorithms,
|
|
703
|
+
"encryptionMethods"
|
|
704
|
+
);
|
|
609
705
|
const imported = /* @__PURE__ */ new Map();
|
|
610
706
|
const importFor = (algorithm) => {
|
|
611
707
|
let pending = imported.get(algorithm);
|
|
612
708
|
if (!pending) {
|
|
613
|
-
pending = importDecryptionKey(
|
|
709
|
+
pending = Promise.resolve(typeof key === "function" ? key() : key).then((value) => importDecryptionKey(value, algorithm, setting));
|
|
614
710
|
imported.set(algorithm, pending);
|
|
615
711
|
pending.catch(() => imported.delete(algorithm));
|
|
616
712
|
}
|
|
@@ -618,17 +714,83 @@ function createAuthioTokenDecrypter(key, setting = "clientEncryptionKey") {
|
|
|
618
714
|
};
|
|
619
715
|
return async (token) => {
|
|
620
716
|
if (!isAuthioTokenEncrypted(token)) return token;
|
|
621
|
-
|
|
622
|
-
|
|
717
|
+
const algorithm = algorithmOf(token);
|
|
718
|
+
if (AuthioLegacyKeyManagementAlgorithms.includes(algorithm))
|
|
719
|
+
throw new AuthioTokenValidationError(
|
|
720
|
+
"legacy_algorithm",
|
|
721
|
+
settings.allowLegacyEncryptionAlgorithms ? "The token is encrypted with RSA1_5 (PKCS#1 v1.5), which Node and WebCrypto cannot decrypt even with allowLegacyEncryptionAlgorithms; register RSA-OAEP-256 as the client's encryption algorithm in Authio." : "The token is encrypted with the legacy RSA1_5 algorithm, which is refused; register RSA-OAEP-256 as the client's encryption algorithm in Authio."
|
|
722
|
+
);
|
|
723
|
+
if (!keyManagementAlgorithms.includes(algorithm))
|
|
724
|
+
throw new AuthioTokenValidationError(
|
|
725
|
+
"decryption_failed",
|
|
726
|
+
`The token is encrypted with '${algorithm}', and encryptionAlgorithms accepts ${keyManagementAlgorithms.join(" or ")}.`
|
|
727
|
+
);
|
|
728
|
+
const method = contentMethodOf(token);
|
|
729
|
+
if (!contentEncryptionAlgorithms.includes(method))
|
|
730
|
+
throw new AuthioTokenValidationError(
|
|
731
|
+
"decryption_failed",
|
|
732
|
+
`The token's content is encrypted with '${method}', and encryptionMethods accepts ${contentEncryptionAlgorithms.join(" or ")}.`
|
|
733
|
+
);
|
|
734
|
+
if (key === void 0 || key === null || key === "")
|
|
735
|
+
throw new AuthioTokenValidationError(
|
|
736
|
+
"decryption_failed",
|
|
623
737
|
`Authio encrypted this token to the client's own key; set ${setting} to the private key Authio showed when it generated the pair.`
|
|
624
738
|
);
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
739
|
+
try {
|
|
740
|
+
const { plaintext } = await jose.compactDecrypt(token, (header) => importFor(header.alg), {
|
|
741
|
+
keyManagementAlgorithms: [...keyManagementAlgorithms],
|
|
742
|
+
contentEncryptionAlgorithms: [...contentEncryptionAlgorithms]
|
|
743
|
+
});
|
|
744
|
+
return new TextDecoder().decode(plaintext);
|
|
745
|
+
} catch (error) {
|
|
746
|
+
if (error instanceof AuthioConfigurationError) throw error;
|
|
747
|
+
if (error instanceof jose.errors.JOSEError)
|
|
748
|
+
throw new AuthioTokenValidationError("decryption_failed", `The token could not be decrypted with ${setting}: ${error.message}`, { cause: error });
|
|
749
|
+
throw error;
|
|
750
|
+
}
|
|
630
751
|
};
|
|
631
752
|
}
|
|
753
|
+
function encryptionKeyFromFile(path, setting = "clientEncryptionKeyPath") {
|
|
754
|
+
let current;
|
|
755
|
+
return async () => {
|
|
756
|
+
const nodeCrypto = await loadNodeCrypto();
|
|
757
|
+
if (!nodeCrypto) throw new AuthioConfigurationError(`${setting} needs Node; pass clientEncryptionKey instead.`);
|
|
758
|
+
const { readFile, stat } = await import('fs/promises');
|
|
759
|
+
let written;
|
|
760
|
+
try {
|
|
761
|
+
written = (await stat(path)).mtimeMs;
|
|
762
|
+
} catch (error) {
|
|
763
|
+
throw new AuthioConfigurationError(`${setting} '${path}' could not be read.`, { cause: error });
|
|
764
|
+
}
|
|
765
|
+
if (!current || current.written !== written) current = { written, key: readFile(path, "utf8") };
|
|
766
|
+
return current.key;
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
function algorithmOf(token) {
|
|
770
|
+
try {
|
|
771
|
+
return String(jose.decodeProtectedHeader(token).alg ?? "");
|
|
772
|
+
} catch (error) {
|
|
773
|
+
throw new AuthioTokenValidationError("decryption_failed", "The encrypted token's header could not be read.", { cause: error });
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
function contentMethodOf(token) {
|
|
777
|
+
try {
|
|
778
|
+
return String(jose.decodeProtectedHeader(token).enc ?? "");
|
|
779
|
+
} catch (error) {
|
|
780
|
+
throw new AuthioTokenValidationError("decryption_failed", "The encrypted token's header could not be read.", { cause: error });
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
function resolveAllowlist(configured, defaults, supported, setting) {
|
|
784
|
+
if (configured === void 0 || configured.length === 0) return defaults;
|
|
785
|
+
for (const value of configured) {
|
|
786
|
+
if (AuthioLegacyKeyManagementAlgorithms.includes(value))
|
|
787
|
+
throw new AuthioConfigurationError(
|
|
788
|
+
`${setting} cannot hold ${value}: Node and WebCrypto cannot decrypt PKCS#1 v1.5; register RSA-OAEP-256 as the client's encryption algorithm in Authio.`
|
|
789
|
+
);
|
|
790
|
+
if (!supported.includes(value)) throw new AuthioConfigurationError(`${setting} holds '${value}'; it accepts ${supported.join(", ")}.`);
|
|
791
|
+
}
|
|
792
|
+
return [...new Set(configured)];
|
|
793
|
+
}
|
|
632
794
|
async function importDecryptionKey(key, algorithm, setting) {
|
|
633
795
|
try {
|
|
634
796
|
if (typeof key !== "string") return await importPrivateJwk(key, algorithm, setting);
|
|
@@ -668,14 +830,362 @@ function toBase64(bytes) {
|
|
|
668
830
|
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
669
831
|
return btoa(binary);
|
|
670
832
|
}
|
|
833
|
+
function createAuthioSigningKeys(connection, discovery) {
|
|
834
|
+
let keys;
|
|
835
|
+
return async (signal) => {
|
|
836
|
+
const document = await discovery.get(signal);
|
|
837
|
+
const uri = document.jwks_uri;
|
|
838
|
+
if (!uri) throw new AuthioError("The Authio discovery document has no jwks_uri.");
|
|
839
|
+
if (!keys || keys.uri !== uri)
|
|
840
|
+
keys = { uri, set: jose.createRemoteJWKSet(new URL(uri), { [jose.customFetch]: connection.fetch, timeoutDuration: connection.httpTimeoutMs }) };
|
|
841
|
+
return keys.set;
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
async function verifyAuthioJwt(token, keys, expected) {
|
|
845
|
+
if (!looksLikeJws(token))
|
|
846
|
+
throw new AuthioTokenValidationError("reference_token", "The token is not a JWT; only introspection can resolve it ('introspectionOnly').");
|
|
847
|
+
try {
|
|
848
|
+
const { payload, protectedHeader } = await jose.jwtVerify(token, keys, {
|
|
849
|
+
issuer: expected.issuer,
|
|
850
|
+
audience: expected.audiences ? [...expected.audiences] : void 0,
|
|
851
|
+
algorithms: [...expected.algorithms && expected.algorithms.length > 0 ? expected.algorithms : AuthioAsymmetricAlgorithms],
|
|
852
|
+
clockTolerance: expected.clockSkew ?? AuthioDefaults.ClockSkew,
|
|
853
|
+
requiredClaims: [.../* @__PURE__ */ new Set(["exp", ...expected.requiredClaims ?? []])],
|
|
854
|
+
currentDate: new Date(expected.now)
|
|
855
|
+
});
|
|
856
|
+
return { payload, header: protectedHeader };
|
|
857
|
+
} catch (error) {
|
|
858
|
+
if (error instanceof AuthioConfigurationError) throw error;
|
|
859
|
+
throw toTokenValidationError(error, expected.audiences ?? [], token);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
function withClientSecret(keys, secret) {
|
|
863
|
+
if (!secret) return keys;
|
|
864
|
+
return async (header, token) => {
|
|
865
|
+
const algorithm = String(header.alg);
|
|
866
|
+
if (!AuthioHmacAlgorithms.includes(algorithm)) return keys(header, token);
|
|
867
|
+
const bytes = new TextEncoder().encode(await secret());
|
|
868
|
+
requireHmacSecretLength(algorithm, bytes.length);
|
|
869
|
+
return bytes;
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
function requireHmacSecretLength(algorithm, length) {
|
|
873
|
+
const minimum = AuthioHmacMinimumSecretBytes[algorithm];
|
|
874
|
+
if (minimum !== void 0 && length < minimum)
|
|
875
|
+
throw new AuthioConfigurationError(
|
|
876
|
+
`The Authio clientSecret has ${length} bytes, and ${algorithm} needs at least ${minimum} (RFC 7518 \xA73.2): Authio refuses a shorter secret for it.`
|
|
877
|
+
);
|
|
878
|
+
}
|
|
879
|
+
function acceptedSigningAlgorithms(configured, announced) {
|
|
880
|
+
if (configured) return [configured];
|
|
881
|
+
const asymmetric = (Array.isArray(announced) ? announced : []).filter(
|
|
882
|
+
(algorithm) => typeof algorithm === "string" && AuthioAsymmetricAlgorithms.includes(algorithm)
|
|
883
|
+
);
|
|
884
|
+
return asymmetric.length > 0 ? asymmetric : AuthioAsymmetricAlgorithms;
|
|
885
|
+
}
|
|
886
|
+
function toTokenValidationError(error, audiences, token) {
|
|
887
|
+
if (error instanceof AuthioTokenValidationError) return error;
|
|
888
|
+
const refuse = (reason, message) => new AuthioTokenValidationError(reason, message, { cause: error });
|
|
889
|
+
if (error instanceof jose.errors.JWTExpired) {
|
|
890
|
+
const exp = error.payload.exp;
|
|
891
|
+
return refuse("expired", typeof exp === "number" ? `The token expired at '${new Date(exp * 1e3).toISOString()}'` : "The token has expired");
|
|
892
|
+
}
|
|
893
|
+
if (error instanceof jose.errors.JWTClaimValidationFailed) {
|
|
894
|
+
switch (error.claim) {
|
|
895
|
+
case "aud":
|
|
896
|
+
return refuse("invalid_audience", describeAudience([error.payload.aud ?? []].flat(), audiences));
|
|
897
|
+
case "iss":
|
|
898
|
+
return refuse("invalid_issuer", `The issuer '${error.payload.iss ?? ""}' is invalid`);
|
|
899
|
+
case "nbf":
|
|
900
|
+
return refuse(
|
|
901
|
+
"not_yet_valid",
|
|
902
|
+
typeof error.payload.nbf === "number" ? `The token is not valid before '${new Date(error.payload.nbf * 1e3).toISOString()}'` : "The token is not valid yet"
|
|
903
|
+
);
|
|
904
|
+
case "exp":
|
|
905
|
+
return refuse("expired", "The token has no expiration");
|
|
906
|
+
default:
|
|
907
|
+
return refuse("malformed", `The token's ${error.claim} claim is invalid`);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
if (error instanceof jose.errors.JWSSignatureVerificationFailed) return refuse("invalid_signature", "The signature is invalid");
|
|
911
|
+
if (error instanceof jose.errors.JWKSNoMatchingKey || error instanceof jose.errors.JWKSMultipleMatchingKeys)
|
|
912
|
+
return refuse("invalid_signature", "The signature key was not found");
|
|
913
|
+
if (error instanceof jose.errors.JOSEAlgNotAllowed) return refuse("invalid_algorithm", `The algorithm '${algorithmOf2(token)}' is not available`);
|
|
914
|
+
if (error instanceof jose.errors.JWKSTimeout || error instanceof jose.errors.JWKSInvalid)
|
|
915
|
+
return refuse("signing_keys_unavailable", "Authio could not be reached to read its signing keys.");
|
|
916
|
+
if (error instanceof jose.errors.JWTInvalid || error instanceof jose.errors.JWSInvalid) return refuse("malformed", "The token is malformed");
|
|
917
|
+
if (error instanceof jose.errors.JOSEError) return refuse("malformed", error.message);
|
|
918
|
+
return refuse("signing_keys_unavailable", "Authio could not be reached to read its signing keys.");
|
|
919
|
+
}
|
|
920
|
+
async function computeAuthioTokenHash(value, algorithm) {
|
|
921
|
+
const size = /(\d{3})$/.exec(algorithm)?.[1];
|
|
922
|
+
const hash = size === "384" ? "SHA-384" : size === "512" ? "SHA-512" : "SHA-256";
|
|
923
|
+
const digest = new Uint8Array(await crypto.subtle.digest(hash, new TextEncoder().encode(value)));
|
|
924
|
+
return toBase64Url(digest.subarray(0, digest.length / 2));
|
|
925
|
+
}
|
|
926
|
+
async function validateAuthioIdToken(token, context, expectations = {}) {
|
|
927
|
+
const signed = isAuthioTokenEncrypted(token) ? await context.decrypt(token) : token;
|
|
928
|
+
const { payload, header } = await verifyAuthioJwt(signed, context.keys, {
|
|
929
|
+
issuer: context.issuer,
|
|
930
|
+
audiences: [context.clientId],
|
|
931
|
+
algorithms: context.algorithms,
|
|
932
|
+
clockSkew: context.clockSkew,
|
|
933
|
+
now: context.now,
|
|
934
|
+
requiredClaims: ["sub", "iat"]
|
|
935
|
+
});
|
|
936
|
+
const type = typeof payload.typ === "string" ? payload.typ : void 0;
|
|
937
|
+
if (type !== void 0 && type.toLowerCase() !== "id")
|
|
938
|
+
throw new AuthioTokenValidationError("not_id_token", `Only ID tokens are accepted; this token's typ is '${type}'.`);
|
|
939
|
+
const audiences = [payload.aud ?? []].flat();
|
|
940
|
+
if (audiences.length > 1 && payload.azp !== context.clientId)
|
|
941
|
+
throw new AuthioTokenValidationError("invalid_audience", `The ID token has several audiences and its azp is not '${context.clientId}'.`);
|
|
942
|
+
if (expectations.nonce !== void 0 && payload[AuthioClaimTypes.Nonce] !== expectations.nonce)
|
|
943
|
+
throw new AuthioTokenValidationError(
|
|
944
|
+
"invalid_nonce",
|
|
945
|
+
payload[AuthioClaimTypes.Nonce] === void 0 ? "The ID token carries no nonce, although one was sent." : "The ID token's nonce is not the one sent."
|
|
946
|
+
);
|
|
947
|
+
const atHash = payload[AuthioClaimTypes.AccessTokenHash];
|
|
948
|
+
if (expectations.accessToken !== void 0 && typeof atHash === "string") {
|
|
949
|
+
const expected = await computeAuthioTokenHash(expectations.accessToken, String(header.alg ?? ""));
|
|
950
|
+
if (expected !== atHash) throw new AuthioTokenValidationError("invalid_at_hash", "The ID token's at_hash does not match the access token.");
|
|
951
|
+
}
|
|
952
|
+
if (expectations.maxAge !== void 0) {
|
|
953
|
+
const authTime = payload[AuthioClaimTypes.AuthTime];
|
|
954
|
+
const skew = context.clockSkew ?? AuthioDefaults.ClockSkew;
|
|
955
|
+
if (typeof authTime !== "number")
|
|
956
|
+
throw new AuthioTokenValidationError("authentication_too_old", "max_age was requested, and the ID token carries no auth_time.");
|
|
957
|
+
if ((authTime + expectations.maxAge + skew) * 1e3 < context.now)
|
|
958
|
+
throw new AuthioTokenValidationError(
|
|
959
|
+
"authentication_too_old",
|
|
960
|
+
`The person signed in at '${new Date(authTime * 1e3).toISOString()}', more than ${expectations.maxAge} seconds ago.`
|
|
961
|
+
);
|
|
962
|
+
}
|
|
963
|
+
return payload;
|
|
964
|
+
}
|
|
965
|
+
async function validateAuthioLogoutToken(token, context) {
|
|
966
|
+
const signed = isAuthioTokenEncrypted(token) ? await context.decrypt(token) : token;
|
|
967
|
+
const invalid = (message) => new AuthioTokenValidationError("invalid_logout_token", message);
|
|
968
|
+
let headerType;
|
|
969
|
+
try {
|
|
970
|
+
const header = jose.decodeProtectedHeader(signed);
|
|
971
|
+
headerType = typeof header.typ === "string" ? header.typ.toLowerCase() : void 0;
|
|
972
|
+
} catch (error) {
|
|
973
|
+
throw new AuthioTokenValidationError("malformed", "The logout token is malformed", { cause: error });
|
|
974
|
+
}
|
|
975
|
+
const { payload } = await verifyAuthioJwt(signed, context.keys, {
|
|
976
|
+
issuer: context.issuer,
|
|
977
|
+
audiences: [context.clientId],
|
|
978
|
+
algorithms: context.algorithms,
|
|
979
|
+
clockSkew: context.clockSkew,
|
|
980
|
+
now: context.now,
|
|
981
|
+
requiredClaims: ["iat"]
|
|
982
|
+
});
|
|
983
|
+
if (headerType !== AuthioDefaults.LogoutTokenType && headerType !== `application/${AuthioDefaults.LogoutTokenType}`)
|
|
984
|
+
throw invalid(`The token's header typ is '${headerType ?? ""}', not '${AuthioDefaults.LogoutTokenType}'.`);
|
|
985
|
+
const events = payload[AuthioClaimTypes.Events];
|
|
986
|
+
if (!events || typeof events !== "object" || Array.isArray(events) || typeof events[AuthioDefaults.BackchannelLogoutEvent] !== "object")
|
|
987
|
+
throw invalid("The token carries no back-channel logout event.");
|
|
988
|
+
if (payload[AuthioClaimTypes.Nonce] !== void 0) throw invalid("A logout token must not carry a nonce.");
|
|
989
|
+
const subject = typeof payload.sub === "string" && payload.sub ? payload.sub : void 0;
|
|
990
|
+
const sessionId = typeof payload.sid === "string" && payload.sid ? payload.sid : void 0;
|
|
991
|
+
if (!subject && !sessionId) throw invalid("The logout token names neither a subject nor a session.");
|
|
992
|
+
return Object.freeze({ subject, sessionId, tokenId: typeof payload.jti === "string" ? payload.jti : void 0, claims: Object.freeze({ ...payload }) });
|
|
993
|
+
}
|
|
994
|
+
function describeAudience(tokenAudiences, audiences) {
|
|
995
|
+
const known = tokenAudiences.length === 0 ? "" : ` ${tokenAudiences.map((audience) => `'${audience}'`).join(" and ")}`;
|
|
996
|
+
const accepted = audiences.map((audience) => `'${audience}'`).join(" or ");
|
|
997
|
+
return audiences.length === 1 ? `The token is not addressed to this API: its audience${known} is not ${accepted}. Request the token for that resource (RFC 8707).` : `The token is not addressed to this API: its audience${known} is not ${accepted}. Request the token for one of those resources (RFC 8707).`;
|
|
998
|
+
}
|
|
999
|
+
function looksLikeJws(token) {
|
|
1000
|
+
const parts = token.split(".");
|
|
1001
|
+
return parts.length === 3 && parts[0].length > 0;
|
|
1002
|
+
}
|
|
1003
|
+
function algorithmOf2(token) {
|
|
1004
|
+
try {
|
|
1005
|
+
return String(jose.decodeProtectedHeader(token).alg ?? "");
|
|
1006
|
+
} catch {
|
|
1007
|
+
return "";
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
function toBase64Url(bytes) {
|
|
1011
|
+
let binary = "";
|
|
1012
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
1013
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
// src/authorization.ts
|
|
1017
|
+
var AuthioChecks = ["pkce", "state", "nonce", "none"];
|
|
1018
|
+
async function createAuthioPkce(method = "S256") {
|
|
1019
|
+
if (method !== "S256" && method !== "plain") throw new TypeError(`The PKCE method '${String(method)}' is not S256 or plain.`);
|
|
1020
|
+
const codeVerifier = randomValue(32);
|
|
1021
|
+
const codeChallenge = method === "plain" ? codeVerifier : toBase64Url2(new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(codeVerifier))));
|
|
1022
|
+
return { codeVerifier, codeChallenge, codeChallengeMethod: method };
|
|
1023
|
+
}
|
|
1024
|
+
function randomValue(bytes = 32) {
|
|
1025
|
+
return toBase64Url2(crypto.getRandomValues(new Uint8Array(bytes)));
|
|
1026
|
+
}
|
|
1027
|
+
function resolveChecks(checks, setting = "checks") {
|
|
1028
|
+
const resolved = [];
|
|
1029
|
+
for (const check of checks ?? []) {
|
|
1030
|
+
if (!AuthioChecks.includes(check))
|
|
1031
|
+
throw new AuthioConfigurationError(`The Authio ${setting} value '${String(check)}' is not one of ${AuthioChecks.join(", ")}.`);
|
|
1032
|
+
if (check !== "none" && !resolved.includes(check)) resolved.push(check);
|
|
1033
|
+
}
|
|
1034
|
+
return Object.freeze(resolved);
|
|
1035
|
+
}
|
|
1036
|
+
async function buildAuthorizationUrl(endpoint, clientId, request) {
|
|
1037
|
+
const checks = resolveChecks(request.checks);
|
|
1038
|
+
const pkce = checks.includes("pkce") ? await createAuthioPkce(request.codeChallengeMethod ?? "S256") : void 0;
|
|
1039
|
+
const state = checks.includes("state") ? request.state?.trim() || randomValue() : void 0;
|
|
1040
|
+
const nonce = checks.includes("nonce") ? request.nonce?.trim() || randomValue() : void 0;
|
|
1041
|
+
if (request.maxAge !== void 0 && (!Number.isInteger(request.maxAge) || request.maxAge < 0))
|
|
1042
|
+
throw new RangeError("maxAge must be a non-negative whole number of seconds.");
|
|
1043
|
+
const display = request.popup ? "popup" : request.display;
|
|
1044
|
+
const responseMode = request.popup ? "web_message" : request.responseMode;
|
|
1045
|
+
if (responseMode !== void 0 && responseMode !== "query" && responseMode !== "web_message")
|
|
1046
|
+
throw new AuthioConfigurationError(`The Authio responseMode '${String(responseMode)}' is not query or web_message.`);
|
|
1047
|
+
const url = new URL(endpoint);
|
|
1048
|
+
const parameters = {
|
|
1049
|
+
...request.extraParameters,
|
|
1050
|
+
response_type: "code",
|
|
1051
|
+
client_id: clientId,
|
|
1052
|
+
redirect_uri: request.redirectUri,
|
|
1053
|
+
scope: toScopeString(request.scopes) ?? "openid",
|
|
1054
|
+
state: state ?? request.state?.trim(),
|
|
1055
|
+
nonce: nonce ?? request.nonce?.trim(),
|
|
1056
|
+
code_challenge: pkce?.codeChallenge,
|
|
1057
|
+
code_challenge_method: pkce?.codeChallengeMethod,
|
|
1058
|
+
resource: request.resource,
|
|
1059
|
+
organization: request.organization,
|
|
1060
|
+
prompt: joined(request.prompt),
|
|
1061
|
+
max_age: request.maxAge?.toString(),
|
|
1062
|
+
acr_values: joined(request.acrValues),
|
|
1063
|
+
login_hint: request.loginHint,
|
|
1064
|
+
id_token_hint: request.idTokenHint,
|
|
1065
|
+
ui_locales: joined(request.uiLocales),
|
|
1066
|
+
display,
|
|
1067
|
+
response_mode: responseMode,
|
|
1068
|
+
idp_hint: request.idpHint
|
|
1069
|
+
};
|
|
1070
|
+
for (const [name, value] of Object.entries(parameters)) if (value !== void 0 && value !== "") url.searchParams.set(name, value);
|
|
1071
|
+
return {
|
|
1072
|
+
url: url.toString(),
|
|
1073
|
+
transaction: Object.freeze({
|
|
1074
|
+
checks,
|
|
1075
|
+
...state ? { state } : {},
|
|
1076
|
+
...nonce ? { nonce } : {},
|
|
1077
|
+
...pkce ? { codeVerifier: pkce.codeVerifier, codeChallengeMethod: pkce.codeChallengeMethod } : {},
|
|
1078
|
+
redirectUri: request.redirectUri,
|
|
1079
|
+
...request.resource ? { resource: request.resource } : {},
|
|
1080
|
+
...request.maxAge !== void 0 ? { maxAge: request.maxAge } : {}
|
|
1081
|
+
})
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
function readCallbackParameters(callback) {
|
|
1085
|
+
if (callback instanceof URLSearchParams) return Object.fromEntries(callback);
|
|
1086
|
+
if (callback instanceof URL) return Object.fromEntries(callback.searchParams);
|
|
1087
|
+
if (typeof callback === "string") {
|
|
1088
|
+
const text = callback.trim();
|
|
1089
|
+
const query = /^[a-z][a-z0-9+.-]*:/i.test(text) ? new URL(text).search : text.slice(text.indexOf("?") + 1);
|
|
1090
|
+
return Object.fromEntries(new URLSearchParams(query));
|
|
1091
|
+
}
|
|
1092
|
+
const parameters = {};
|
|
1093
|
+
for (const [name, value] of Object.entries(callback ?? {})) {
|
|
1094
|
+
const first = Array.isArray(value) ? value[0] : value;
|
|
1095
|
+
if (typeof first === "string") parameters[name] = first;
|
|
1096
|
+
}
|
|
1097
|
+
return parameters;
|
|
1098
|
+
}
|
|
1099
|
+
function readAuthioFrontChannelLogout(request, issuer) {
|
|
1100
|
+
const parameters = readCallbackParameters(request);
|
|
1101
|
+
const sessionId = parameters.sid?.trim() || void 0;
|
|
1102
|
+
const sentIssuer = parameters.iss?.trim() || void 0;
|
|
1103
|
+
if (sentIssuer !== void 0 && sentIssuer.replace(/\/+$/, "") !== issuer.replace(/\/+$/, ""))
|
|
1104
|
+
throw new AuthioCallbackError("issuer_mismatch", `The front-channel logout comes from the issuer '${sentIssuer}', not '${issuer}'.`);
|
|
1105
|
+
if (sessionId !== void 0 && sentIssuer === void 0)
|
|
1106
|
+
throw new AuthioCallbackError("missing_issuer", "The front-channel logout names a sid without its iss.");
|
|
1107
|
+
return Object.freeze({ sessionId, issuer: sentIssuer });
|
|
1108
|
+
}
|
|
1109
|
+
function parseAuthioBearerChallenge(header) {
|
|
1110
|
+
if (!header || !/^\s*bearer(\s|$)/i.test(header)) return void 0;
|
|
1111
|
+
const values = {};
|
|
1112
|
+
for (const match of header.matchAll(/([a-zA-Z_]+)\s*=\s*"((?:[^"\\]|\\.)*)"|([a-zA-Z_]+)\s*=\s*([^,\s]+)/g)) {
|
|
1113
|
+
const name = (match[1] ?? match[3] ?? "").toLowerCase();
|
|
1114
|
+
values[name] ??= (match[2] ?? match[4] ?? "").replace(/\\(.)/g, "$1");
|
|
1115
|
+
}
|
|
1116
|
+
const maxAge = values.max_age !== void 0 && /^\d+$/.test(values.max_age) ? Number(values.max_age) : void 0;
|
|
1117
|
+
return {
|
|
1118
|
+
error: values.error,
|
|
1119
|
+
errorDescription: values.error_description,
|
|
1120
|
+
scope: values.scope,
|
|
1121
|
+
acrValues: values.acr_values,
|
|
1122
|
+
maxAge,
|
|
1123
|
+
resourceMetadata: values.resource_metadata
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
function joined(value) {
|
|
1127
|
+
if (value === void 0) return void 0;
|
|
1128
|
+
const text = (typeof value === "string" ? [value] : [...value]).map((item) => item.trim()).filter(Boolean).join(" ");
|
|
1129
|
+
return text || void 0;
|
|
1130
|
+
}
|
|
1131
|
+
function toBase64Url2(bytes) {
|
|
1132
|
+
let binary = "";
|
|
1133
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
1134
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
// src/client.ts
|
|
671
1138
|
function createAuthioClient(options) {
|
|
672
1139
|
const connection = resolveConnection(options);
|
|
673
1140
|
const discovery = createAuthioDiscoveryClient(connection);
|
|
674
1141
|
const authenticator = createClientAuthenticator(options, connection);
|
|
675
1142
|
const refreshSkewMs = seconds(options.tokenRefreshSkew, AuthioDefaults.TokenRefreshSkew, "tokenRefreshSkew");
|
|
676
|
-
const
|
|
1143
|
+
const clockSkew = seconds(options.clockSkew, AuthioDefaults.ClockSkew, "clockSkew") / 1e3;
|
|
1144
|
+
const encryptionKey = resolveEncryptionKey(options);
|
|
1145
|
+
const decrypt = createAuthioTokenDecrypter(encryptionKey, {
|
|
1146
|
+
setting: options.clientEncryptionKey === void 0 && options.clientEncryptionKeyPath ? "clientEncryptionKeyPath" : "clientEncryptionKey",
|
|
1147
|
+
allowLegacyEncryptionAlgorithms: options.allowLegacyEncryptionAlgorithms,
|
|
1148
|
+
encryptionAlgorithms: options.encryptionAlgorithms,
|
|
1149
|
+
encryptionMethods: options.encryptionMethods
|
|
1150
|
+
});
|
|
1151
|
+
const checks = resolveChecks(options.checks);
|
|
1152
|
+
const codeChallengeMethod = resolveCodeChallengeMethod(options.codeChallengeMethod);
|
|
1153
|
+
const responseMode = resolveResponseMode(options.responseMode);
|
|
1154
|
+
const idTokenAlgorithm = resolveSigningAlgorithm(options.idTokenSignedResponseAlg, "idTokenSignedResponseAlg", options.clientSecret);
|
|
1155
|
+
const userInfoAlgorithm = resolveSigningAlgorithm(options.userinfoSignedResponseAlg, "userinfoSignedResponseAlg", options.clientSecret) ?? idTokenAlgorithm;
|
|
1156
|
+
const accessTokenAlgorithm = resolveSigningAlgorithm(options.accessTokenSignedResponseAlg, "accessTokenSignedResponseAlg", options.clientSecret);
|
|
1157
|
+
const clientSecret = options.clientSecret === void 0 ? void 0 : () => readSecret(options.clientSecret);
|
|
1158
|
+
const signingKeys = createAuthioSigningKeys(connection, discovery);
|
|
677
1159
|
const tokens = createTokenClient(connection, discovery, authenticator);
|
|
678
1160
|
const clientCredentials = createClientCredentialsCache(connection, authenticator.clientId, tokens, refreshSkewMs);
|
|
1161
|
+
const validationContext = async (signal) => {
|
|
1162
|
+
const document = await discovery.get(signal);
|
|
1163
|
+
return {
|
|
1164
|
+
keys: withClientSecret(await signingKeys(signal), clientSecret),
|
|
1165
|
+
decrypt,
|
|
1166
|
+
issuer: connection.issuer,
|
|
1167
|
+
clientId: authenticator.clientId,
|
|
1168
|
+
algorithms: acceptedSigningAlgorithms(idTokenAlgorithm, document.id_token_signing_alg_values_supported),
|
|
1169
|
+
clockSkew,
|
|
1170
|
+
now: connection.clock.now()
|
|
1171
|
+
};
|
|
1172
|
+
};
|
|
1173
|
+
const validateIdToken = async (idToken, expectations = {}) => {
|
|
1174
|
+
requireValue(idToken, "idToken");
|
|
1175
|
+
return validateAuthioIdToken(idToken, await validationContext(expectations.signal), expectations);
|
|
1176
|
+
};
|
|
1177
|
+
const createAuthorizationUrl = async (request = {}) => {
|
|
1178
|
+
const redirectUri = request.redirectUri ?? options.redirectUri;
|
|
1179
|
+
requireValue(redirectUri, "redirectUri");
|
|
1180
|
+
const document = await discovery.get(request.signal);
|
|
1181
|
+
return buildAuthorizationUrl(requireEndpoint(document, "authorization_endpoint"), authenticator.clientId, {
|
|
1182
|
+
...request,
|
|
1183
|
+
redirectUri,
|
|
1184
|
+
checks: request.checks ?? checks,
|
|
1185
|
+
codeChallengeMethod: request.codeChallengeMethod ?? codeChallengeMethod,
|
|
1186
|
+
responseMode: request.responseMode ?? responseMode
|
|
1187
|
+
});
|
|
1188
|
+
};
|
|
679
1189
|
return {
|
|
680
1190
|
issuer: connection.issuer,
|
|
681
1191
|
clientId: authenticator.clientId,
|
|
@@ -718,7 +1228,19 @@ function createAuthioClient(options) {
|
|
|
718
1228
|
);
|
|
719
1229
|
const mediaType = response.headers.get("content-type")?.split(";")[0]?.trim().toLowerCase();
|
|
720
1230
|
if (response.ok && mediaType === "application/jwt")
|
|
721
|
-
return readUserInfoJwt(
|
|
1231
|
+
return readUserInfoJwt(
|
|
1232
|
+
(await response.text()).trim(),
|
|
1233
|
+
document,
|
|
1234
|
+
connection,
|
|
1235
|
+
authenticator.clientId,
|
|
1236
|
+
decrypt,
|
|
1237
|
+
withClientSecret(await signingKeys(callOptions.signal), clientSecret),
|
|
1238
|
+
{
|
|
1239
|
+
algorithms: acceptedSigningAlgorithms(userInfoAlgorithm, document.userinfo_signing_alg_values_supported ?? document.id_token_signing_alg_values_supported),
|
|
1240
|
+
clockSkew,
|
|
1241
|
+
status: response.status
|
|
1242
|
+
}
|
|
1243
|
+
);
|
|
722
1244
|
if (!response.ok) {
|
|
723
1245
|
const body = await response.text();
|
|
724
1246
|
const challenge = toErrorResponse(body) ? void 0 : readBearerChallenge(response);
|
|
@@ -726,6 +1248,84 @@ function createAuthioClient(options) {
|
|
|
726
1248
|
}
|
|
727
1249
|
return readProtocolJson(response);
|
|
728
1250
|
},
|
|
1251
|
+
createAuthorizationUrl,
|
|
1252
|
+
async handleAuthorizationCallback(callback, transaction, callOptions = {}) {
|
|
1253
|
+
if (!transaction?.redirectUri || !Array.isArray(transaction.checks))
|
|
1254
|
+
throw new TypeError("transaction is required: keep what createAuthorizationUrl returned.");
|
|
1255
|
+
const parameters = readCallbackParameters(callback);
|
|
1256
|
+
if (transaction.checks.includes("state") && (!transaction.state || parameters.state !== transaction.state))
|
|
1257
|
+
throw new AuthioCallbackError("state_mismatch", "The callback's state is not the one this sign-in was started with.");
|
|
1258
|
+
const document = await discovery.get(callOptions.signal);
|
|
1259
|
+
if (parameters.iss !== void 0) {
|
|
1260
|
+
if (parameters.iss.replace(/\/+$/, "") !== connection.issuer)
|
|
1261
|
+
throw new AuthioCallbackError("issuer_mismatch", `The callback comes from the issuer '${parameters.iss}', not '${connection.issuer}' (RFC 9207).`);
|
|
1262
|
+
} else if (document.authorization_response_iss_parameter_supported === true) {
|
|
1263
|
+
throw new AuthioCallbackError("missing_issuer", "The callback carries no iss parameter, although Authio sends one (RFC 9207).");
|
|
1264
|
+
}
|
|
1265
|
+
if (parameters.error) throw new AuthioProtocolError(parameters.error, parameters.error_description, parameters.error_code);
|
|
1266
|
+
if (!parameters.code) throw new AuthioCallbackError("missing_code", "The callback carries neither a code nor an error.");
|
|
1267
|
+
const answer = await tokens.authorizationCode({
|
|
1268
|
+
code: parameters.code,
|
|
1269
|
+
redirectUri: transaction.redirectUri,
|
|
1270
|
+
codeVerifier: transaction.checks.includes("pkce") ? transaction.codeVerifier : void 0,
|
|
1271
|
+
resource: transaction.resource,
|
|
1272
|
+
signal: callOptions.signal
|
|
1273
|
+
});
|
|
1274
|
+
const idToken = answer.id_token ? await validateIdToken(answer.id_token, {
|
|
1275
|
+
nonce: transaction.checks.includes("nonce") ? transaction.nonce : void 0,
|
|
1276
|
+
accessToken: answer.access_token,
|
|
1277
|
+
maxAge: transaction.maxAge,
|
|
1278
|
+
signal: callOptions.signal
|
|
1279
|
+
}) : void 0;
|
|
1280
|
+
return { tokens: answer, idToken };
|
|
1281
|
+
},
|
|
1282
|
+
validateIdToken,
|
|
1283
|
+
async validateLogoutToken(logoutToken, callOptions = {}) {
|
|
1284
|
+
requireValue(logoutToken, "logoutToken");
|
|
1285
|
+
return validateAuthioLogoutToken(logoutToken, await validationContext(callOptions.signal));
|
|
1286
|
+
},
|
|
1287
|
+
validateFrontChannelLogout(request) {
|
|
1288
|
+
return readAuthioFrontChannelLogout(request, connection.issuer);
|
|
1289
|
+
},
|
|
1290
|
+
async readAccessTokenClaims(accessToken, callOptions = {}) {
|
|
1291
|
+
requireValue(accessToken, "accessToken");
|
|
1292
|
+
if (isAuthioTokenEncrypted(accessToken) && encryptionKey === void 0) return void 0;
|
|
1293
|
+
const signed = await decrypt(accessToken);
|
|
1294
|
+
if (signed.split(".").length !== 3) return void 0;
|
|
1295
|
+
const document = await discovery.get(callOptions.signal);
|
|
1296
|
+
const { payload } = await verifyAuthioJwt(signed, withClientSecret(await signingKeys(callOptions.signal), clientSecret), {
|
|
1297
|
+
issuer: connection.issuer,
|
|
1298
|
+
algorithms: acceptedSigningAlgorithms(accessTokenAlgorithm, document.id_token_signing_alg_values_supported),
|
|
1299
|
+
clockSkew,
|
|
1300
|
+
now: connection.clock.now()
|
|
1301
|
+
});
|
|
1302
|
+
return payload;
|
|
1303
|
+
},
|
|
1304
|
+
async createEndSessionUrl(request = {}) {
|
|
1305
|
+
const document = await discovery.get(request.signal);
|
|
1306
|
+
const url = new URL(requireEndpoint(document, "end_session_endpoint"));
|
|
1307
|
+
const locales = request.uiLocales === void 0 ? void 0 : (typeof request.uiLocales === "string" ? [request.uiLocales] : [...request.uiLocales]).join(" ");
|
|
1308
|
+
const parameters = {
|
|
1309
|
+
id_token_hint: request.idTokenHint,
|
|
1310
|
+
post_logout_redirect_uri: request.postLogoutRedirectUri ?? options.postLogoutRedirectUri,
|
|
1311
|
+
state: request.state,
|
|
1312
|
+
ui_locales: locales,
|
|
1313
|
+
client_id: request.includeClientId === false ? void 0 : authenticator.clientId
|
|
1314
|
+
};
|
|
1315
|
+
for (const [name, value] of Object.entries(parameters)) if (value) url.searchParams.set(name, value);
|
|
1316
|
+
return url.toString();
|
|
1317
|
+
},
|
|
1318
|
+
async createStepUpAuthorizationUrl(challenge, request = {}) {
|
|
1319
|
+
const parsed = typeof challenge === "string" ? parseAuthioBearerChallenge(challenge) : challenge;
|
|
1320
|
+
if (!parsed || parsed.error !== AuthioOAuthErrors.InsufficientUserAuthentication)
|
|
1321
|
+
throw new TypeError("The challenge is not an insufficient_user_authentication bearer challenge.");
|
|
1322
|
+
return createAuthorizationUrl({
|
|
1323
|
+
...request,
|
|
1324
|
+
acrValues: request.acrValues ?? parsed.acrValues,
|
|
1325
|
+
maxAge: request.maxAge ?? parsed.maxAge,
|
|
1326
|
+
prompt: request.prompt ?? (parsed.maxAge === 0 ? "login" : void 0)
|
|
1327
|
+
});
|
|
1328
|
+
},
|
|
729
1329
|
clientCredentialsFetch(fetchOptions = {}) {
|
|
730
1330
|
const inner = fetchOptions.fetch ?? connection.fetch;
|
|
731
1331
|
const key = { scopes: fetchOptions.scopes, resource: fetchOptions.resource };
|
|
@@ -747,8 +1347,8 @@ function createAuthioClient(options) {
|
|
|
747
1347
|
};
|
|
748
1348
|
}
|
|
749
1349
|
function createTokenClient(connection, discovery, authenticator) {
|
|
750
|
-
const requestToken = async (parameters, signal) => {
|
|
751
|
-
const response = await postForm(connection, discovery, authenticator, "token_endpoint", parameters, signal);
|
|
1350
|
+
const requestToken = async (parameters, signal, authenticate = true) => {
|
|
1351
|
+
const response = await postForm(connection, discovery, authenticate ? authenticator : void 0, "token_endpoint", parameters, signal);
|
|
752
1352
|
const token = await readProtocolJson(response);
|
|
753
1353
|
return redact(token, ["access_token", "refresh_token", "id_token"]);
|
|
754
1354
|
};
|
|
@@ -764,12 +1364,9 @@ function createTokenClient(connection, discovery, authenticator) {
|
|
|
764
1364
|
return await requestToken({ grant_type: AuthioGrantTypes.DeviceCode, device_code: authorization.device_code }, options.signal);
|
|
765
1365
|
} catch (error) {
|
|
766
1366
|
if (!(error instanceof AuthioProtocolError)) throw error;
|
|
767
|
-
|
|
768
|
-
if (
|
|
769
|
-
|
|
770
|
-
continue;
|
|
771
|
-
}
|
|
772
|
-
throw error;
|
|
1367
|
+
const outcome = toDevicePollOutcome(error.error);
|
|
1368
|
+
if (outcome.outcome === "failed") throw error;
|
|
1369
|
+
wait += outcome.intervalIncrease * 1e3;
|
|
773
1370
|
}
|
|
774
1371
|
}
|
|
775
1372
|
};
|
|
@@ -795,7 +1392,7 @@ function createTokenClient(connection, discovery, authenticator) {
|
|
|
795
1392
|
refresh: (options) => {
|
|
796
1393
|
requireValue(options?.refreshToken, "refreshToken");
|
|
797
1394
|
return requestToken(
|
|
798
|
-
{ grant_type: AuthioGrantTypes.RefreshToken, refresh_token: options.refreshToken, scope: toScopeString(options.scopes) },
|
|
1395
|
+
{ grant_type: AuthioGrantTypes.RefreshToken, refresh_token: options.refreshToken, scope: toScopeString(options.scopes), resource: options.resource },
|
|
799
1396
|
options.signal
|
|
800
1397
|
);
|
|
801
1398
|
},
|
|
@@ -845,6 +1442,20 @@ function createTokenClient(connection, discovery, authenticator) {
|
|
|
845
1442
|
return redact(await readProtocolJson(response), ["device_code"]);
|
|
846
1443
|
},
|
|
847
1444
|
pollDevice,
|
|
1445
|
+
personalAccessToken: (options) => {
|
|
1446
|
+
requireValue(options?.token, "token");
|
|
1447
|
+
return requestToken(
|
|
1448
|
+
{
|
|
1449
|
+
grant_type: AuthioGrantTypes.PersonalAccessToken,
|
|
1450
|
+
client_secret: options.token,
|
|
1451
|
+
client_id: options.clientId,
|
|
1452
|
+
scope: toScopeString(options.scopes),
|
|
1453
|
+
resource: options.resource
|
|
1454
|
+
},
|
|
1455
|
+
options.signal,
|
|
1456
|
+
false
|
|
1457
|
+
);
|
|
1458
|
+
},
|
|
848
1459
|
password: (options) => {
|
|
849
1460
|
requireValue(options?.username, "username");
|
|
850
1461
|
if (typeof options.password !== "string") throw new TypeError("password is required.");
|
|
@@ -902,48 +1513,90 @@ function toConnectionExchangeResult(body, status, provider) {
|
|
|
902
1513
|
const token = parseProtocolJson(body, status);
|
|
903
1514
|
if (typeof token.access_token === "string" && token.access_token)
|
|
904
1515
|
return redact(
|
|
905
|
-
{
|
|
1516
|
+
withoutUndefined({
|
|
906
1517
|
status: "granted",
|
|
907
1518
|
accessToken: token.access_token,
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
},
|
|
1519
|
+
issuedTokenType: textOf(token.issued_token_type),
|
|
1520
|
+
tokenType: textOf(token.token_type),
|
|
1521
|
+
expiresIn: typeof token.expires_in === "number" ? token.expires_in : void 0,
|
|
1522
|
+
scope: textOf(token.scope),
|
|
1523
|
+
connectionId: textOf(token.connection_id),
|
|
1524
|
+
provider: textOf(token.provider) ?? provider,
|
|
1525
|
+
providerType: textOf(token.provider_type),
|
|
1526
|
+
externalAccount: token.external_account && typeof token.external_account === "object" ? token.external_account : void 0
|
|
1527
|
+
}),
|
|
917
1528
|
["accessToken"]
|
|
918
1529
|
);
|
|
919
1530
|
}
|
|
920
1531
|
const error = toErrorResponse(body);
|
|
921
|
-
const
|
|
1532
|
+
const errorDescription = error?.error_description?.trim() ? error.error_description : `Authio answered HTTP ${status} to the connected-account token exchange.`;
|
|
1533
|
+
const errorCode = textOf(error?.error_code);
|
|
922
1534
|
switch (error?.error) {
|
|
923
1535
|
case AuthioOAuthErrors.ConnectionRequired:
|
|
924
|
-
return {
|
|
1536
|
+
return withoutUndefined({
|
|
1537
|
+
status: "required",
|
|
1538
|
+
connectUri: textOf(error.connect_uri),
|
|
1539
|
+
provider: textOf(error.connection_provider) ?? provider,
|
|
1540
|
+
errorCode,
|
|
1541
|
+
errorDescription
|
|
1542
|
+
});
|
|
925
1543
|
case AuthioOAuthErrors.ConnectionSelectionRequired:
|
|
926
|
-
return {
|
|
1544
|
+
return withoutUndefined({
|
|
1545
|
+
status: "selection_required",
|
|
1546
|
+
provider: textOf(error.connection_provider) ?? provider,
|
|
1547
|
+
connections: (Array.isArray(error.connections) ? error.connections : []).map(toConnectionChoice),
|
|
1548
|
+
errorCode,
|
|
1549
|
+
errorDescription
|
|
1550
|
+
});
|
|
927
1551
|
default:
|
|
928
|
-
return {
|
|
1552
|
+
return withoutUndefined({
|
|
1553
|
+
status: "refused",
|
|
1554
|
+
error: error?.error ?? `http_${status}`,
|
|
1555
|
+
errorCode,
|
|
1556
|
+
errorDescription,
|
|
1557
|
+
connectUri: textOf(error?.connect_uri)
|
|
1558
|
+
});
|
|
929
1559
|
}
|
|
930
1560
|
}
|
|
1561
|
+
function toDevicePollOutcome(error) {
|
|
1562
|
+
if (error === AuthioOAuthErrors.AuthorizationPending) return { outcome: "keep_polling", intervalIncrease: 0 };
|
|
1563
|
+
if (error === AuthioOAuthErrors.SlowDown) return { outcome: "keep_polling", intervalIncrease: AuthioDefaults.DeviceSlowDownIncrement };
|
|
1564
|
+
return { outcome: "failed", error };
|
|
1565
|
+
}
|
|
1566
|
+
function toConnectionChoice(value) {
|
|
1567
|
+
const record = value && typeof value === "object" ? value : {};
|
|
1568
|
+
return withoutUndefined({ id: textOf(record.id) ?? "", label: textOf(record.label), externalLogin: textOf(record.external_login) });
|
|
1569
|
+
}
|
|
1570
|
+
function textOf(value) {
|
|
1571
|
+
return typeof value === "string" && value ? value : void 0;
|
|
1572
|
+
}
|
|
1573
|
+
function withoutUndefined(value) {
|
|
1574
|
+
for (const key of Object.keys(value)) if (value[key] === void 0) delete value[key];
|
|
1575
|
+
return value;
|
|
1576
|
+
}
|
|
1577
|
+
function resolveEncryptionKey(options) {
|
|
1578
|
+
if (options.clientEncryptionKey !== void 0) return options.clientEncryptionKey;
|
|
1579
|
+
if (options.clientEncryptionKeyPath) return encryptionKeyFromFile(options.clientEncryptionKeyPath);
|
|
1580
|
+
return void 0;
|
|
1581
|
+
}
|
|
931
1582
|
async function postForm(connection, discovery, authenticator, endpoint, parameters, signal) {
|
|
932
1583
|
const document = await discovery.get(signal);
|
|
933
1584
|
const url = requireEndpoint(document, endpoint);
|
|
934
1585
|
const form = formBody(parameters);
|
|
935
1586
|
const headers = new Headers({ accept: "application/json", "content-type": "application/x-www-form-urlencoded" });
|
|
936
|
-
await authenticator
|
|
1587
|
+
await authenticator?.apply(form, headers);
|
|
937
1588
|
return send(connection, url, { method: "POST", headers, body: form.toString() }, signal);
|
|
938
1589
|
}
|
|
939
|
-
async function readUserInfoJwt(jwt, document, connection, clientId, decrypt,
|
|
1590
|
+
async function readUserInfoJwt(jwt, document, connection, clientId, decrypt, keys, settings) {
|
|
940
1591
|
try {
|
|
941
1592
|
const signed = isAuthioTokenEncrypted(jwt) ? await decrypt(jwt) : jwt;
|
|
942
|
-
const
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
1593
|
+
const { payload } = await verifyAuthioJwt(signed, keys, {
|
|
1594
|
+
issuer: document.issuer.replace(/\/+$/, ""),
|
|
1595
|
+
audiences: [clientId],
|
|
1596
|
+
algorithms: settings.algorithms,
|
|
1597
|
+
clockSkew: settings.clockSkew,
|
|
1598
|
+
now: connection.clock.now(),
|
|
1599
|
+
requiredClaims: ["sub"]
|
|
947
1600
|
});
|
|
948
1601
|
return payload;
|
|
949
1602
|
} catch (error) {
|
|
@@ -951,7 +1604,7 @@ async function readUserInfoJwt(jwt, document, connection, clientId, decrypt, sta
|
|
|
951
1604
|
"invalid_userinfo",
|
|
952
1605
|
`The userinfo JWT could not be verified: ${error?.message ?? "unknown reason."}`,
|
|
953
1606
|
void 0,
|
|
954
|
-
status,
|
|
1607
|
+
settings.status,
|
|
955
1608
|
{ cause: error }
|
|
956
1609
|
);
|
|
957
1610
|
}
|
|
@@ -969,10 +1622,180 @@ function readBearerChallenge(response) {
|
|
|
969
1622
|
}
|
|
970
1623
|
return error ? { error, description } : void 0;
|
|
971
1624
|
}
|
|
1625
|
+
function resolveCodeChallengeMethod(method) {
|
|
1626
|
+
if (method !== void 0 && method !== "S256" && method !== "plain")
|
|
1627
|
+
throw new AuthioConfigurationError(`The Authio codeChallengeMethod '${String(method)}' is not S256 or plain.`);
|
|
1628
|
+
return method;
|
|
1629
|
+
}
|
|
1630
|
+
function resolveResponseMode(mode) {
|
|
1631
|
+
if (mode !== void 0 && mode !== "query" && mode !== "web_message")
|
|
1632
|
+
throw new AuthioConfigurationError(`The Authio responseMode '${String(mode)}' is not query or web_message.`);
|
|
1633
|
+
return mode;
|
|
1634
|
+
}
|
|
1635
|
+
function resolveSigningAlgorithm(algorithm, setting, clientSecret) {
|
|
1636
|
+
if (algorithm === void 0 || algorithm === "") return void 0;
|
|
1637
|
+
const symmetric = AuthioHmacAlgorithms.includes(algorithm);
|
|
1638
|
+
if (!symmetric && !AuthioAsymmetricAlgorithms.includes(algorithm))
|
|
1639
|
+
throw new AuthioConfigurationError(
|
|
1640
|
+
`The Authio ${setting} '${algorithm}' is not one of ${[...AuthioAsymmetricAlgorithms, ...AuthioHmacAlgorithms].join(", ")}.`
|
|
1641
|
+
);
|
|
1642
|
+
if (symmetric && !clientSecret)
|
|
1643
|
+
throw new AuthioConfigurationError(`The Authio ${setting} ${algorithm} is verified with the client secret (OpenID Connect Core \xA710.1): set clientSecret.`);
|
|
1644
|
+
if (symmetric && typeof clientSecret === "string") requireHmacSecretLength(algorithm, new TextEncoder().encode(clientSecret).length);
|
|
1645
|
+
return algorithm;
|
|
1646
|
+
}
|
|
1647
|
+
async function readSecret(source) {
|
|
1648
|
+
const value = typeof source === "function" ? await source() : source;
|
|
1649
|
+
if (typeof value !== "string" || !value) throw new AuthioConfigurationError("The Authio clientSecret function returned no secret.");
|
|
1650
|
+
return value;
|
|
1651
|
+
}
|
|
972
1652
|
function requireValue(value, name) {
|
|
973
1653
|
if (typeof value !== "string" || !value.trim()) throw new TypeError(`${name} is required.`);
|
|
974
1654
|
}
|
|
975
1655
|
|
|
1656
|
+
// src/admin.ts
|
|
1657
|
+
function parseAuthioAdminEnvelope(status, body, headers) {
|
|
1658
|
+
const parsed = parseJson(body);
|
|
1659
|
+
if (status >= 200 && status < 300) {
|
|
1660
|
+
if (!parsed) return { ok: true };
|
|
1661
|
+
return withoutUndefined2({ ok: true, data: parsed.data });
|
|
1662
|
+
}
|
|
1663
|
+
const notifications = Array.isArray(parsed?.notifications) ? parsed.notifications.map(toNotification).filter((item) => item.message) : [];
|
|
1664
|
+
return {
|
|
1665
|
+
ok: false,
|
|
1666
|
+
error: new AuthioAdminError({
|
|
1667
|
+
status,
|
|
1668
|
+
errorCode: textOf2(parsed?.errorCode),
|
|
1669
|
+
notifications,
|
|
1670
|
+
requestId: textOf2(parsed?.requestId) ?? headerOf(headers, AuthioDefaults.RequestIdHeader),
|
|
1671
|
+
retryAfterSeconds: retryAfterOf(headerOf(headers, "Retry-After"))
|
|
1672
|
+
})
|
|
1673
|
+
};
|
|
1674
|
+
}
|
|
1675
|
+
async function readAuthioAdminResponse(response) {
|
|
1676
|
+
const envelope = parseAuthioAdminEnvelope(response.status, response.status === 204 ? null : await response.text(), response.headers);
|
|
1677
|
+
if (!envelope.ok) throw envelope.error;
|
|
1678
|
+
return envelope;
|
|
1679
|
+
}
|
|
1680
|
+
function createAuthioAccountCredential(options) {
|
|
1681
|
+
const value = options.credential?.trim();
|
|
1682
|
+
if (!value) throw new AuthioConfigurationError("The account credential is required.");
|
|
1683
|
+
const kind = value.startsWith(AuthioDefaults.ApiKeyPrefix) ? "api_key" : value.startsWith(AuthioDefaults.ClientSecretPrefix) ? "client_secret" : value.startsWith(AuthioDefaults.PersonalAccessTokenPrefix) ? "personal_access_token" : void 0;
|
|
1684
|
+
if (!kind)
|
|
1685
|
+
throw new AuthioConfigurationError(
|
|
1686
|
+
`The account credential must start with ${AuthioDefaults.ApiKeyPrefix}, ${AuthioDefaults.ClientSecretPrefix} or ${AuthioDefaults.PersonalAccessTokenPrefix}.`
|
|
1687
|
+
);
|
|
1688
|
+
const exchanged = kind === "client_secret" || kind === "personal_access_token" && options.exchangePersonalAccessToken === true;
|
|
1689
|
+
const clock = options.clock ?? systemClock;
|
|
1690
|
+
const inner = options.fetch ?? globalThis.fetch;
|
|
1691
|
+
const scope = toScopeString(options.scopes);
|
|
1692
|
+
const refreshSkewMs = seconds(options.tokenRefreshSkew, AuthioDefaults.TokenRefreshSkew, "tokenRefreshSkew");
|
|
1693
|
+
let client;
|
|
1694
|
+
let cached;
|
|
1695
|
+
let pending;
|
|
1696
|
+
if (exchanged) {
|
|
1697
|
+
if (kind === "client_secret" && !options.credentialId?.trim())
|
|
1698
|
+
throw new AuthioConfigurationError("A cs_ client secret is exchanged with its credential id: set credentialId.");
|
|
1699
|
+
client = createAuthioClient({
|
|
1700
|
+
...options,
|
|
1701
|
+
realm: AuthioDefaults.PlatformRealm,
|
|
1702
|
+
clientId: kind === "client_secret" ? toCredentialClientId(options.credentialId) : AuthioDefaults.PlatformClientId,
|
|
1703
|
+
clientSecret: kind === "client_secret" ? value : void 0,
|
|
1704
|
+
tokenEndpointAuthMethod: kind === "client_secret" ? "client_secret_post" : "none"
|
|
1705
|
+
});
|
|
1706
|
+
}
|
|
1707
|
+
const exchange = async (signal) => {
|
|
1708
|
+
if (cached && clock.now() < cached.refreshAt) return cached;
|
|
1709
|
+
pending ??= (async () => {
|
|
1710
|
+
const issuedAt = clock.now();
|
|
1711
|
+
const answer = kind === "client_secret" ? await client.tokens.clientCredentials({ scopes: scope, signal }) : await client.tokens.personalAccessToken({ token: value, scopes: scope, signal });
|
|
1712
|
+
cached = toClientCredentialsToken(answer, issuedAt, refreshSkewMs);
|
|
1713
|
+
return cached;
|
|
1714
|
+
})().finally(() => {
|
|
1715
|
+
pending = void 0;
|
|
1716
|
+
});
|
|
1717
|
+
return pending;
|
|
1718
|
+
};
|
|
1719
|
+
const headers = async (signal) => {
|
|
1720
|
+
if (exchanged) {
|
|
1721
|
+
const token = await exchange(signal);
|
|
1722
|
+
return { authorization: `${token.tokenType} ${token.accessToken}` };
|
|
1723
|
+
}
|
|
1724
|
+
return options.presentAs === "x-api-key" ? { [AuthioDefaults.ApiKeyHeader]: value } : { authorization: `Bearer ${value}` };
|
|
1725
|
+
};
|
|
1726
|
+
const authenticatedFetch = async (input, init) => {
|
|
1727
|
+
const request = new Request(input, init);
|
|
1728
|
+
const body = request.body ? await request.arrayBuffer() : void 0;
|
|
1729
|
+
const idempotent = ["GET", "HEAD", "PUT", "DELETE"].includes(request.method.toUpperCase());
|
|
1730
|
+
const attempt = async () => {
|
|
1731
|
+
const merged = new Headers(request.headers);
|
|
1732
|
+
for (const [name, header] of Object.entries(await headers(request.signal))) merged.set(name, header);
|
|
1733
|
+
return inner(request.url, { method: request.method, headers: merged, body, signal: request.signal, redirect: request.redirect });
|
|
1734
|
+
};
|
|
1735
|
+
let response = await attempt();
|
|
1736
|
+
if (response.status === 401 && exchanged) {
|
|
1737
|
+
cached = void 0;
|
|
1738
|
+
response = await attempt();
|
|
1739
|
+
}
|
|
1740
|
+
if ((response.status === 429 || response.status === 503) && idempotent) {
|
|
1741
|
+
const wait = retryAfterOf(response.headers.get("retry-after") ?? void 0);
|
|
1742
|
+
if (wait !== void 0) {
|
|
1743
|
+
await clock.sleep(wait * 1e3, request.signal);
|
|
1744
|
+
response = await attempt();
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
return response;
|
|
1748
|
+
};
|
|
1749
|
+
return {
|
|
1750
|
+
kind,
|
|
1751
|
+
exchanged,
|
|
1752
|
+
headers,
|
|
1753
|
+
invalidate() {
|
|
1754
|
+
cached = void 0;
|
|
1755
|
+
},
|
|
1756
|
+
fetch: authenticatedFetch
|
|
1757
|
+
};
|
|
1758
|
+
}
|
|
1759
|
+
function toCredentialClientId(credentialId) {
|
|
1760
|
+
const id = credentialId.trim();
|
|
1761
|
+
return id.startsWith(AuthioDefaults.CredentialClientIdPrefix) ? id : `${AuthioDefaults.CredentialClientIdPrefix}${id.replace(/[{}-]/g, "").toLowerCase()}`;
|
|
1762
|
+
}
|
|
1763
|
+
function parseJson(body) {
|
|
1764
|
+
if (!body?.trim()) return void 0;
|
|
1765
|
+
try {
|
|
1766
|
+
return asRecord(JSON.parse(body));
|
|
1767
|
+
} catch {
|
|
1768
|
+
return void 0;
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
function asRecord(value) {
|
|
1772
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
1773
|
+
}
|
|
1774
|
+
function toNotification(value) {
|
|
1775
|
+
const record = asRecord(value) ?? {};
|
|
1776
|
+
return withoutUndefined2({ message: textOf2(record.message) ?? "", field: textOf2(record.field), code: textOf2(record.code) });
|
|
1777
|
+
}
|
|
1778
|
+
function headerOf(headers, name) {
|
|
1779
|
+
if (!headers) return void 0;
|
|
1780
|
+
if (typeof headers.get === "function") return headers.get(name) ?? void 0;
|
|
1781
|
+
const wanted = name.toLowerCase();
|
|
1782
|
+
for (const [key, value] of Object.entries(headers)) if (key.toLowerCase() === wanted) return value;
|
|
1783
|
+
return void 0;
|
|
1784
|
+
}
|
|
1785
|
+
function retryAfterOf(value) {
|
|
1786
|
+
if (!value?.trim()) return void 0;
|
|
1787
|
+
if (/^\d+$/.test(value.trim())) return Number(value.trim());
|
|
1788
|
+
const date = Date.parse(value);
|
|
1789
|
+
return Number.isNaN(date) ? void 0 : Math.max(0, Math.ceil((date - Date.now()) / 1e3));
|
|
1790
|
+
}
|
|
1791
|
+
function textOf2(value) {
|
|
1792
|
+
return typeof value === "string" && value ? value : void 0;
|
|
1793
|
+
}
|
|
1794
|
+
function withoutUndefined2(value) {
|
|
1795
|
+
for (const key of Object.keys(value)) if (value[key] === void 0) delete value[key];
|
|
1796
|
+
return value;
|
|
1797
|
+
}
|
|
1798
|
+
|
|
976
1799
|
// src/resource-access.ts
|
|
977
1800
|
var claimSources = new Map(Object.entries(AuthioResourceAccessClaims).map(([name, source]) => [name.toLowerCase(), source]));
|
|
978
1801
|
var AuthioResourceAccess = class _AuthioResourceAccess {
|
|
@@ -1052,7 +1875,7 @@ function parseResourceAccess(claims) {
|
|
|
1052
1875
|
return roles.length === 0 ? AuthioResourceAccess.empty : new AuthioResourceAccess(roles);
|
|
1053
1876
|
}
|
|
1054
1877
|
function parseResourceAccessClaim(source, value) {
|
|
1055
|
-
const root = typeof value === "string" ?
|
|
1878
|
+
const root = typeof value === "string" ? parseJson2(value) : value;
|
|
1056
1879
|
if (!isPlainObject(root)) return [];
|
|
1057
1880
|
const roles = [];
|
|
1058
1881
|
for (const [name, rolePermissions] of Object.entries(root)) {
|
|
@@ -1109,7 +1932,7 @@ function distinctIgnoringCase(values) {
|
|
|
1109
1932
|
return true;
|
|
1110
1933
|
});
|
|
1111
1934
|
}
|
|
1112
|
-
function
|
|
1935
|
+
function parseJson2(text) {
|
|
1113
1936
|
if (!text.trim()) return void 0;
|
|
1114
1937
|
try {
|
|
1115
1938
|
return JSON.parse(text);
|
|
@@ -1196,9 +2019,23 @@ var asAdminResourceEvent = (event) => dataWhen(
|
|
|
1196
2019
|
AuthioWebhookEventTypes.AdminResourceDeleted
|
|
1197
2020
|
);
|
|
1198
2021
|
var asUserSuspensionEvent = (event) => dataWhen(event, AuthioWebhookEventTypes.UserSuspended, AuthioWebhookEventTypes.UserSuspensionEnded);
|
|
2022
|
+
var asAttackDetectedEvent = (event) => dataWhen(event, AuthioWebhookEventTypes.AttackDetected);
|
|
2023
|
+
var asCredentialCompromisedEvent = (event) => dataWhen(event, AuthioWebhookEventTypes.CredentialCompromised);
|
|
1199
2024
|
var asPingEvent = (event) => dataWhen(event, AuthioWebhookEventTypes.Ping);
|
|
2025
|
+
var knownEventTypes = new Set(Object.values(AuthioWebhookEventTypes));
|
|
2026
|
+
function isKnownWebhookEventType(type) {
|
|
2027
|
+
return knownEventTypes.has(type);
|
|
2028
|
+
}
|
|
2029
|
+
function readWebhookEventData(event) {
|
|
2030
|
+
return isKnownWebhookEventType(event.type) ? dataWhen(event, event.type) : void 0;
|
|
2031
|
+
}
|
|
2032
|
+
function toCamelCasedMembers(value) {
|
|
2033
|
+
const result = {};
|
|
2034
|
+
for (const [name, member] of Object.entries(value)) result[name.replace(/_([a-z0-9])/g, (_, letter) => letter.toUpperCase())] = member;
|
|
2035
|
+
return result;
|
|
2036
|
+
}
|
|
1200
2037
|
function dataWhen(event, ...types) {
|
|
1201
|
-
return types.includes(event.type) && event.data !== null && event.data
|
|
2038
|
+
return types.includes(event.type) && event.data !== null && typeof event.data === "object" && !Array.isArray(event.data) ? toCamelCasedMembers(event.data) : void 0;
|
|
1202
2039
|
}
|
|
1203
2040
|
function parseSignatureHeader(header) {
|
|
1204
2041
|
let timestamp = 0;
|
|
@@ -1237,7 +2074,11 @@ function constantTimeEquals(left, right) {
|
|
|
1237
2074
|
return difference === 0;
|
|
1238
2075
|
}
|
|
1239
2076
|
|
|
2077
|
+
exports.AuthioAdminError = AuthioAdminError;
|
|
1240
2078
|
exports.AuthioAsymmetricAlgorithms = AuthioAsymmetricAlgorithms;
|
|
2079
|
+
exports.AuthioCallbackError = AuthioCallbackError;
|
|
2080
|
+
exports.AuthioCallerKinds = AuthioCallerKinds;
|
|
2081
|
+
exports.AuthioChecks = AuthioChecks;
|
|
1241
2082
|
exports.AuthioClaimTypes = AuthioClaimTypes;
|
|
1242
2083
|
exports.AuthioClientAssertionAlgorithms = AuthioClientAssertionAlgorithms;
|
|
1243
2084
|
exports.AuthioConfigurationError = AuthioConfigurationError;
|
|
@@ -1246,16 +2087,25 @@ exports.AuthioDefaults = AuthioDefaults;
|
|
|
1246
2087
|
exports.AuthioDiscoveryError = AuthioDiscoveryError;
|
|
1247
2088
|
exports.AuthioError = AuthioError;
|
|
1248
2089
|
exports.AuthioGrantTypes = AuthioGrantTypes;
|
|
2090
|
+
exports.AuthioHmacAlgorithms = AuthioHmacAlgorithms;
|
|
2091
|
+
exports.AuthioHmacMinimumSecretBytes = AuthioHmacMinimumSecretBytes;
|
|
1249
2092
|
exports.AuthioKeyManagementAlgorithms = AuthioKeyManagementAlgorithms;
|
|
2093
|
+
exports.AuthioLegacyKeyManagementAlgorithms = AuthioLegacyKeyManagementAlgorithms;
|
|
1250
2094
|
exports.AuthioOAuthErrors = AuthioOAuthErrors;
|
|
2095
|
+
exports.AuthioPrompts = AuthioPrompts;
|
|
1251
2096
|
exports.AuthioProtocolError = AuthioProtocolError;
|
|
1252
2097
|
exports.AuthioResourceAccess = AuthioResourceAccess;
|
|
1253
2098
|
exports.AuthioResourceAccessClaims = AuthioResourceAccessClaims;
|
|
1254
2099
|
exports.AuthioScopes = AuthioScopes;
|
|
2100
|
+
exports.AuthioSupportedContentEncryptionAlgorithms = AuthioSupportedContentEncryptionAlgorithms;
|
|
2101
|
+
exports.AuthioTokenEndpointAuthMethods = AuthioTokenEndpointAuthMethods;
|
|
1255
2102
|
exports.AuthioTokenTypes = AuthioTokenTypes;
|
|
2103
|
+
exports.AuthioTokenValidationError = AuthioTokenValidationError;
|
|
1256
2104
|
exports.AuthioWebhookEventTypes = AuthioWebhookEventTypes;
|
|
1257
2105
|
exports.AuthioWebhookHeaders = AuthioWebhookHeaders;
|
|
1258
2106
|
exports.asAdminResourceEvent = asAdminResourceEvent;
|
|
2107
|
+
exports.asAttackDetectedEvent = asAttackDetectedEvent;
|
|
2108
|
+
exports.asCredentialCompromisedEvent = asCredentialCompromisedEvent;
|
|
1259
2109
|
exports.asKeysRotatedEvent = asKeysRotatedEvent;
|
|
1260
2110
|
exports.asLoginEvent = asLoginEvent;
|
|
1261
2111
|
exports.asPingEvent = asPingEvent;
|
|
@@ -1264,26 +2114,46 @@ exports.asTokenRevokedEvent = asTokenRevokedEvent;
|
|
|
1264
2114
|
exports.asUserEvent = asUserEvent;
|
|
1265
2115
|
exports.asUserLockedOutEvent = asUserLockedOutEvent;
|
|
1266
2116
|
exports.asUserSuspensionEvent = asUserSuspensionEvent;
|
|
2117
|
+
exports.computeAuthioTokenHash = computeAuthioTokenHash;
|
|
1267
2118
|
exports.computeWebhookSignatureHeader = computeWebhookSignatureHeader;
|
|
2119
|
+
exports.createAuthioAccountCredential = createAuthioAccountCredential;
|
|
1268
2120
|
exports.createAuthioClient = createAuthioClient;
|
|
1269
2121
|
exports.createAuthioDiscoveryClient = createAuthioDiscoveryClient;
|
|
2122
|
+
exports.createAuthioPkce = createAuthioPkce;
|
|
2123
|
+
exports.createAuthioSigningKeys = createAuthioSigningKeys;
|
|
1270
2124
|
exports.createAuthioTokenDecrypter = createAuthioTokenDecrypter;
|
|
2125
|
+
exports.encryptionKeyFromFile = encryptionKeyFromFile;
|
|
1271
2126
|
exports.escapeDataString = escapeDataString;
|
|
1272
2127
|
exports.isAllowedUrl = isAllowedUrl;
|
|
1273
2128
|
exports.isAuthioTokenEncrypted = isAuthioTokenEncrypted;
|
|
2129
|
+
exports.isKnownWebhookEventType = isKnownWebhookEventType;
|
|
1274
2130
|
exports.isWebhookEventOfType = isWebhookEventOfType;
|
|
2131
|
+
exports.parseAuthioAdminEnvelope = parseAuthioAdminEnvelope;
|
|
2132
|
+
exports.parseAuthioBearerChallenge = parseAuthioBearerChallenge;
|
|
1275
2133
|
exports.parseResourceAccess = parseResourceAccess;
|
|
1276
2134
|
exports.parseResourceAccessClaim = parseResourceAccessClaim;
|
|
1277
2135
|
exports.parseWebhookEvent = parseWebhookEvent;
|
|
2136
|
+
exports.randomValue = randomValue;
|
|
2137
|
+
exports.readAuthioAdminResponse = readAuthioAdminResponse;
|
|
2138
|
+
exports.readAuthioFrontChannelLogout = readAuthioFrontChannelLogout;
|
|
2139
|
+
exports.readWebhookEventData = readWebhookEventData;
|
|
2140
|
+
exports.requireHmacSecretLength = requireHmacSecretLength;
|
|
1278
2141
|
exports.resolveConnection = resolveConnection;
|
|
1279
2142
|
exports.resolveIssuer = resolveIssuer;
|
|
1280
2143
|
exports.resourceAccessSourceOf = resourceAccessSourceOf;
|
|
1281
2144
|
exports.seconds = seconds;
|
|
1282
2145
|
exports.systemClock = systemClock;
|
|
1283
2146
|
exports.toBasicAuthorization = toBasicAuthorization;
|
|
2147
|
+
exports.toCamelCasedMembers = toCamelCasedMembers;
|
|
1284
2148
|
exports.toClientCredentialsToken = toClientCredentialsToken;
|
|
2149
|
+
exports.toConnectionExchangeResult = toConnectionExchangeResult;
|
|
2150
|
+
exports.toCredentialClientId = toCredentialClientId;
|
|
2151
|
+
exports.toDevicePollOutcome = toDevicePollOutcome;
|
|
1285
2152
|
exports.toScopeString = toScopeString;
|
|
2153
|
+
exports.toTokenValidationError = toTokenValidationError;
|
|
1286
2154
|
exports.tryParseWebhookEvent = tryParseWebhookEvent;
|
|
2155
|
+
exports.verifyAuthioJwt = verifyAuthioJwt;
|
|
1287
2156
|
exports.verifyWebhookSignature = verifyWebhookSignature;
|
|
2157
|
+
exports.withClientSecret = withClientSecret;
|
|
1288
2158
|
//# sourceMappingURL=index.cjs.map
|
|
1289
2159
|
//# sourceMappingURL=index.cjs.map
|