@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 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 `403` with `WWW-Authenticate: Bearer error="insufficient_user_authentication", acr_values="2",
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;