@hyper.authio/client 0.8.0 → 0.9.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 +9 -3
- package/dist/index.cjs +30 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +48 -4
- package/dist/index.d.ts +48 -4
- package/dist/index.js +29 -4
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -102,6 +102,11 @@ callback that is not for the transaction throws `AuthioCallbackError` (`reason`:
|
|
|
102
102
|
`missing_issuer`, `missing_code`); an OAuth error Authio sent back (`login_required` after `prompt: "none"`, for one)
|
|
103
103
|
throws `AuthioProtocolError`.
|
|
104
104
|
|
|
105
|
+
`acrValues` asks for a sign-in level; it does not guarantee one. Authio may sign the person in with less (no second
|
|
106
|
+
factor enrolled and none required by the client), as OpenID Connect allows (`acr_values` is a voluntary claim), so
|
|
107
|
+
read `idToken.acr` after the callback and refuse a sign-in below the level the app needs, as
|
|
108
|
+
[`examples/web-login`](../../examples/web-login/server.mjs) does.
|
|
109
|
+
|
|
105
110
|
### ID tokens and sign-out
|
|
106
111
|
|
|
107
112
|
```ts
|
|
@@ -126,8 +131,9 @@ client's front-channel logout URI in an iframe; `validateFrontChannelLogout(requ
|
|
|
126
131
|
|
|
127
132
|
### Step-up
|
|
128
133
|
|
|
129
|
-
When an API answers `
|
|
130
|
-
max_age=300` (RFC 9470
|
|
134
|
+
When an API answers `401` with `WWW-Authenticate: Bearer error="insufficient_user_authentication", acr_values="2",
|
|
135
|
+
max_age=300` (RFC 9470 §3; servers before `@hyper.authio/server` 0.8.1 and `@hyper.authio/mcp` 0.4.1 answer `403`, and
|
|
136
|
+
the challenge reads the same):
|
|
131
137
|
|
|
132
138
|
```ts
|
|
133
139
|
const challenge = parseAuthioBearerChallenge(response.headers.get("www-authenticate"));
|
|
@@ -237,7 +243,7 @@ const user = asUserEvent(event); // { id, username, emailConfirmed, … } for us
|
|
|
237
243
|
|
|
238
244
|
Every event type has its accessor (`asLoginEvent`, `asUserLockedOutEvent`, `asTokenRevokedEvent`,
|
|
239
245
|
`asKeysRotatedEvent`, `asTokenReuseDetectedEvent`, `asAdminResourceEvent`, `asUserSuspensionEvent`,
|
|
240
|
-
`asAttackDetectedEvent`, `asCredentialCompromisedEvent`, `asPingEvent`), which exposes every member of `data` in
|
|
246
|
+
`asAttackDetectedEvent`, `asCredentialCompromisedEvent`, `asAuthenticatorAppEvent`, `asPingEvent`), which exposes every member of `data` in
|
|
241
247
|
camelCase with its value unchanged; `event.data` stays as delivered (snake_case). An unknown type still parses
|
|
242
248
|
(`isKnownWebhookEventType` is `false`, `event.data` is the raw JSON).
|
|
243
249
|
|
package/dist/index.cjs
CHANGED
|
@@ -129,6 +129,10 @@ var AuthioWebhookEventTypes = {
|
|
|
129
129
|
UserSuspensionEnded: "user_suspension_ended",
|
|
130
130
|
AttackDetected: "attack_detected",
|
|
131
131
|
CredentialCompromised: "credential_compromised",
|
|
132
|
+
UserAuthenticatorAppEnrolled: "user_authenticator_app_enrolled",
|
|
133
|
+
UserAuthenticatorAppRemoved: "user_authenticator_app_removed",
|
|
134
|
+
UserRecoveryCodesRegenerated: "user_recovery_codes_regenerated",
|
|
135
|
+
UserRecoveryCodeUsed: "user_recovery_code_used",
|
|
132
136
|
Ping: "ping"
|
|
133
137
|
};
|
|
134
138
|
var AuthioWebhookHeaders = {
|
|
@@ -164,6 +168,8 @@ var AuthioDefaults = {
|
|
|
164
168
|
CredentialClientIdPrefix: "cred_",
|
|
165
169
|
ApiKeyHeader: "x-api-key",
|
|
166
170
|
RequestIdHeader: "X-Request-Id",
|
|
171
|
+
/** The header `typ` of an access token (RFC 9068 §2.1). */
|
|
172
|
+
AccessTokenType: "at+jwt",
|
|
167
173
|
/** The header `typ` of a back-channel logout token. */
|
|
168
174
|
LogoutTokenType: "logout+jwt",
|
|
169
175
|
/** The event a back-channel logout token carries. */
|
|
@@ -962,6 +968,14 @@ async function validateAuthioIdToken(token, context, expectations = {}) {
|
|
|
962
968
|
}
|
|
963
969
|
return payload;
|
|
964
970
|
}
|
|
971
|
+
function requireAccessTokenType(header) {
|
|
972
|
+
const type = typeof header.typ === "string" ? header.typ.toLowerCase() : void 0;
|
|
973
|
+
if (type !== AuthioDefaults.AccessTokenType && type !== `application/${AuthioDefaults.AccessTokenType}`)
|
|
974
|
+
throw new AuthioTokenValidationError(
|
|
975
|
+
"not_access_token",
|
|
976
|
+
`Only access tokens are accepted; this token's header typ is '${header.typ ?? ""}', not '${AuthioDefaults.AccessTokenType}'.`
|
|
977
|
+
);
|
|
978
|
+
}
|
|
965
979
|
async function validateAuthioLogoutToken(token, context) {
|
|
966
980
|
const signed = isAuthioTokenEncrypted(token) ? await context.decrypt(token) : token;
|
|
967
981
|
const invalid = (message) => new AuthioTokenValidationError("invalid_logout_token", message);
|
|
@@ -1293,12 +1307,13 @@ function createAuthioClient(options) {
|
|
|
1293
1307
|
const signed = await decrypt(accessToken);
|
|
1294
1308
|
if (signed.split(".").length !== 3) return void 0;
|
|
1295
1309
|
const document = await discovery.get(callOptions.signal);
|
|
1296
|
-
const { payload } = await verifyAuthioJwt(signed, withClientSecret(await signingKeys(callOptions.signal), clientSecret), {
|
|
1310
|
+
const { payload, header } = await verifyAuthioJwt(signed, withClientSecret(await signingKeys(callOptions.signal), clientSecret), {
|
|
1297
1311
|
issuer: connection.issuer,
|
|
1298
1312
|
algorithms: acceptedSigningAlgorithms(accessTokenAlgorithm, document.id_token_signing_alg_values_supported),
|
|
1299
1313
|
clockSkew,
|
|
1300
1314
|
now: connection.clock.now()
|
|
1301
1315
|
});
|
|
1316
|
+
requireAccessTokenType(header);
|
|
1302
1317
|
return payload;
|
|
1303
1318
|
},
|
|
1304
1319
|
async createEndSessionUrl(request = {}) {
|
|
@@ -1339,13 +1354,16 @@ function createAuthioClient(options) {
|
|
|
1339
1354
|
};
|
|
1340
1355
|
const token = await clientCredentials.getToken({ ...key, signal: request.signal });
|
|
1341
1356
|
const response = await attempt(token);
|
|
1342
|
-
if (response.status !== 401) return response;
|
|
1357
|
+
if (response.status !== 401 || isStepUpChallenge(response)) return response;
|
|
1343
1358
|
clientCredentials.invalidate({ ...key, accessToken: token.accessToken });
|
|
1344
1359
|
return attempt(await clientCredentials.getToken({ ...key, signal: request.signal }));
|
|
1345
1360
|
};
|
|
1346
1361
|
}
|
|
1347
1362
|
};
|
|
1348
1363
|
}
|
|
1364
|
+
function isStepUpChallenge(response) {
|
|
1365
|
+
return parseAuthioBearerChallenge(response.headers.get("www-authenticate"))?.error === AuthioOAuthErrors.InsufficientUserAuthentication;
|
|
1366
|
+
}
|
|
1349
1367
|
function createTokenClient(connection, discovery, authenticator) {
|
|
1350
1368
|
const requestToken = async (parameters, signal, authenticate = true) => {
|
|
1351
1369
|
const response = await postForm(connection, discovery, authenticate ? authenticator : void 0, "token_endpoint", parameters, signal);
|
|
@@ -1708,7 +1726,7 @@ function createAuthioAccountCredential(options) {
|
|
|
1708
1726
|
if (cached && clock.now() < cached.refreshAt) return cached;
|
|
1709
1727
|
pending ??= (async () => {
|
|
1710
1728
|
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 });
|
|
1729
|
+
const answer = kind === "client_secret" ? await client.tokens.clientCredentials({ scopes: scope, signal }) : await client.tokens.personalAccessToken({ token: value, clientId: AuthioDefaults.PlatformClientId, scopes: scope, signal });
|
|
1712
1730
|
cached = toClientCredentialsToken(answer, issuedAt, refreshSkewMs);
|
|
1713
1731
|
return cached;
|
|
1714
1732
|
})().finally(() => {
|
|
@@ -2021,6 +2039,13 @@ var asAdminResourceEvent = (event) => dataWhen(
|
|
|
2021
2039
|
var asUserSuspensionEvent = (event) => dataWhen(event, AuthioWebhookEventTypes.UserSuspended, AuthioWebhookEventTypes.UserSuspensionEnded);
|
|
2022
2040
|
var asAttackDetectedEvent = (event) => dataWhen(event, AuthioWebhookEventTypes.AttackDetected);
|
|
2023
2041
|
var asCredentialCompromisedEvent = (event) => dataWhen(event, AuthioWebhookEventTypes.CredentialCompromised);
|
|
2042
|
+
var asAuthenticatorAppEvent = (event) => dataWhen(
|
|
2043
|
+
event,
|
|
2044
|
+
AuthioWebhookEventTypes.UserAuthenticatorAppEnrolled,
|
|
2045
|
+
AuthioWebhookEventTypes.UserAuthenticatorAppRemoved,
|
|
2046
|
+
AuthioWebhookEventTypes.UserRecoveryCodesRegenerated,
|
|
2047
|
+
AuthioWebhookEventTypes.UserRecoveryCodeUsed
|
|
2048
|
+
);
|
|
2024
2049
|
var asPingEvent = (event) => dataWhen(event, AuthioWebhookEventTypes.Ping);
|
|
2025
2050
|
var knownEventTypes = new Set(Object.values(AuthioWebhookEventTypes));
|
|
2026
2051
|
function isKnownWebhookEventType(type) {
|
|
@@ -2105,6 +2130,7 @@ exports.AuthioWebhookEventTypes = AuthioWebhookEventTypes;
|
|
|
2105
2130
|
exports.AuthioWebhookHeaders = AuthioWebhookHeaders;
|
|
2106
2131
|
exports.asAdminResourceEvent = asAdminResourceEvent;
|
|
2107
2132
|
exports.asAttackDetectedEvent = asAttackDetectedEvent;
|
|
2133
|
+
exports.asAuthenticatorAppEvent = asAuthenticatorAppEvent;
|
|
2108
2134
|
exports.asCredentialCompromisedEvent = asCredentialCompromisedEvent;
|
|
2109
2135
|
exports.asKeysRotatedEvent = asKeysRotatedEvent;
|
|
2110
2136
|
exports.asLoginEvent = asLoginEvent;
|
|
@@ -2137,6 +2163,7 @@ exports.randomValue = randomValue;
|
|
|
2137
2163
|
exports.readAuthioAdminResponse = readAuthioAdminResponse;
|
|
2138
2164
|
exports.readAuthioFrontChannelLogout = readAuthioFrontChannelLogout;
|
|
2139
2165
|
exports.readWebhookEventData = readWebhookEventData;
|
|
2166
|
+
exports.requireAccessTokenType = requireAccessTokenType;
|
|
2140
2167
|
exports.requireHmacSecretLength = requireHmacSecretLength;
|
|
2141
2168
|
exports.resolveConnection = resolveConnection;
|
|
2142
2169
|
exports.resolveIssuer = resolveIssuer;
|