@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 CHANGED
@@ -1,8 +1,10 @@
1
1
  # @hyper.authio/client
2
2
 
3
- The Authio client for Node.js 20.19+ and edge runtimes: discovery, every grant, client authentication with a secret
4
- or `private_key_jwt`, introspection, revocation, UserInfo, a cached client-credentials `fetch`, the roles and
5
- permissions of a token, and webhook verification. Its only dependency is [`jose`](https://github.com/panva/jose).
3
+ The Authio client for Node.js 20.19+ and edge runtimes: discovery, interactive sign-in (PKCE, ID tokens, end session,
4
+ back-channel logout tokens, step-up), every grant, client authentication with a secret or `private_key_jwt`,
5
+ introspection, revocation, UserInfo, JWE, a cached client-credentials `fetch`, account credentials for the Admin API,
6
+ the roles and permissions of a token, and webhook verification. Its only dependency is
7
+ [`jose`](https://github.com/panva/jose).
6
8
 
7
9
  ```bash
8
10
  npm install @hyper.authio/client
@@ -24,11 +26,22 @@ const authio = createAuthioClient({
24
26
  | --- | --- | --- |
25
27
  | `issuer`, or `authority` + `realm` | | The realm's issuer. Must be HTTPS, except on loopback or with `requireHttps: false`. |
26
28
  | `clientId` | | Required. |
27
- | `clientAuthentication` | `private_key_jwt` with only a key, else `client_secret_basic` | Also `client_secret_post` and `none` (a public client). |
28
- | `clientSecret` | | For `client_secret_basic` / `client_secret_post`. |
29
+ | `tokenEndpointAuthMethod` | `client_secret_basic` with a secret, `private_key_jwt` with only a key, `none` with neither | The client's authentication method in Authio, which matches it exactly: `client_secret_basic`, `client_secret_post`, `private_key_jwt` or `none` (a public client). |
30
+ | `clientSecret` | | For `client_secret_basic` / `client_secret_post`. A function returning the secret is called for every request, so a rotated secret is used without a restart. |
29
31
  | `clientPrivateKey`, `clientPrivateKeyPath`, `clientKeyId`, `clientSigningAlgorithm` | RS256 for RSA, the curve's ES algorithm for EC | For `private_key_jwt`: PEM (PKCS#8, PKCS#1, SEC1) or JWK; the file is re-read when it changes; RSA keys need 2048 bits. |
30
32
  | `clientAssertionLifetime` | 120 s | At most 3600. |
31
- | `clientEncryptionKey` | | The client's own key, to read a UserInfo Authio answers encrypted. |
33
+ | `checks` | none | What the sign-in checks: `pkce`, `state`, `nonce` (or `none`). Only the listed checks are made. |
34
+ | `codeChallengeMethod` | `S256` | `S256` or `plain`, with the `pkce` check: one of the client's allowed PKCE methods. |
35
+ | `responseMode` | not sent | `query` or `web_message`. |
36
+ | `idTokenSignedResponseAlg` | the asymmetric algorithms the discovery document announces | The client's signing algorithm: RS/PS/ES 256/384/512, or HS256/384/512 verified with `clientSecret` (at least 32, 48 or 64 bytes, RFC 7518 §3.2). Also used for logout tokens. |
37
+ | `accessTokenSignedResponseAlg` | the asymmetric algorithms the discovery document announces | The algorithm of the client's access tokens (Authio's second signing setting), for `readAccessTokenClaims`; HS* with `clientSecret`. |
38
+ | `userinfoSignedResponseAlg` | `idTokenSignedResponseAlg` | For UserInfo answered as a signed JWT. |
39
+ | `clientEncryptionKey`, `clientEncryptionKeyPath` | | The private half of the client's encryption key (JWK or PEM, or a function returning it; the path is Node only), to read ID tokens, UserInfo and logout tokens Authio encrypts. Without it nothing is decrypted. |
40
+ | `encryptionAlgorithms` | `RSA-OAEP`, `RSA-OAEP-256` | The JWE `alg` accepted. RSA1_5 cannot be listed: Node and WebCrypto cannot decrypt it. |
41
+ | `encryptionMethods` | `A128CBC-HS256`, `A192CBC-HS384`, `A256CBC-HS512` | The JWE `enc` accepted; `A128GCM`, `A192GCM` and `A256GCM` may be listed. |
42
+ | `allowLegacyEncryptionAlgorithms` | `false` | Accepted for parity; RSA1_5 stays refused in Node, which cannot decrypt it (`legacy_algorithm`). |
43
+ | `redirectUri`, `postLogoutRedirectUri` | | Defaults for sign-in and sign-out requests. |
44
+ | `clockSkew` | 30 s | Tolerated on ID tokens, logout tokens and signed UserInfo. |
32
45
  | `discoveryCacheDuration` | 3600 s | |
33
46
  | `httpTimeout` | 30 s | |
34
47
  | `tokenRefreshSkew` | 60 s | How early cached tokens are renewed (at most half their lifetime). |
@@ -37,14 +50,99 @@ const authio = createAuthioClient({
37
50
  Every endpoint is read from the discovery document, which is cached, read once for concurrent callers, and refused
38
51
  when it names another issuer.
39
52
 
53
+ Option names follow the shared `conformance/options.md`; there are no aliases for older names.
54
+
55
+ ## Mirror the client
56
+
57
+ The options describe the client as it is registered in Authio's console, and an option left out is off: the library
58
+ hard-codes none of it. Authio refuses a request that does not match the client and accepts what the client does not
59
+ require.
60
+
61
+ | In the console | Options |
62
+ | --- | --- |
63
+ | PKCE required, method S256 | `checks: ["pkce", "state"]` (add `"nonce"` to bind the ID token), `codeChallengeMethod: "S256"` |
64
+ | PKCE required, method plain only | `checks: ["pkce", "state"]`, `codeChallengeMethod: "plain"` |
65
+ | PKCE off | `checks: ["state"]`, or leave `checks` out to check nothing (sending PKCE anyway is accepted) |
66
+ | Authentication on, `client_secret_post` | `clientSecret`, `tokenEndpointAuthMethod: "client_secret_post"` |
67
+ | Authentication on, `private_key_jwt` | `clientPrivateKey` (or `clientPrivateKeyPath`), `clientKeyId` |
68
+ | Authentication off (public client) | no secret nor key: `tokenEndpointAuthMethod` is `none` |
69
+ | ID token signing algorithm HS256 | `clientSecret`, `idTokenSignedResponseAlg: "HS256"` |
70
+ | Access token signing algorithm HS256 | `clientSecret`, `accessTokenSignedResponseAlg: "HS256"` (an API: the verifier's `accessTokenSignedResponseAlg`) |
71
+ | Tokens encrypted with the client's key, RSA-OAEP-256 | `clientEncryptionKey`, `encryptionAlgorithms: ["RSA-OAEP-256"]` |
72
+
73
+ ## Sign people in
74
+
75
+ ```ts
76
+ const authio = createAuthioClient({
77
+ issuer, clientId: "storefront", // a public client: no secret nor key, so tokenEndpointAuthMethod is none
78
+ checks: ["pkce", "state", "nonce"], codeChallengeMethod: "S256", // the client requires PKCE with S256
79
+ redirectUri: "https://shop.example.com/callback",
80
+ });
81
+
82
+ // 1. Send the person to Authio, keeping the transaction (session, encrypted cookie…).
83
+ const { url, transaction } = await authio.createAuthorizationUrl({
84
+ scopes: ["openid", "profile", "offline_access"],
85
+ resource: "https://orders.example.com", // RFC 8707
86
+ organization: "acme-europe", // optional: sign in for one organization
87
+ prompt: "login", // none | login | select_account | consent
88
+ maxAge: 900, acrValues: "2", loginHint, uiLocales: "pt-BR", idpHint: "google",
89
+ });
90
+
91
+ // 2. On the callback: state (with the state check) and iss (RFC 9207) are checked, the code is redeemed (with the
92
+ // PKCE verifier, with the pkce check), and the ID token validated (nonce with the nonce check, at_hash, auth_time
93
+ // against maxAge).
94
+ const { tokens, idToken } = await authio.handleAuthorizationCallback(request.url, transaction);
95
+ ```
96
+
97
+ Each check generates its parameter and is verified on the callback; the transaction lists the checks it was made with,
98
+ and a `state` it did not ask for is ignored. A request may pass its own `checks`, `codeChallengeMethod` and
99
+ `responseMode`. `popup: true` sets `display=popup` and
100
+ `response_mode=web_message`; pass the parameters the popup posted to the opener to `handleAuthorizationCallback`. A
101
+ callback that is not for the transaction throws `AuthioCallbackError` (`reason`: `state_mismatch`, `issuer_mismatch`,
102
+ `missing_issuer`, `missing_code`); an OAuth error Authio sent back (`login_required` after `prompt: "none"`, for one)
103
+ throws `AuthioProtocolError`.
104
+
105
+ ### ID tokens and sign-out
106
+
107
+ ```ts
108
+ const claims = await authio.validateIdToken(idToken, { nonce, accessToken, maxAge }); // AuthioTokenValidationError otherwise
109
+ const logoutUrl = await authio.createEndSessionUrl({ idTokenHint, postLogoutRedirectUri, state });
110
+ const logout = await authio.validateLogoutToken(form.logout_token); // { subject, sessionId, tokenId, claims }
111
+ ```
112
+
113
+ ID tokens are checked against the realm's JWKS (cached, re-read on an unknown `kid`), or against `clientSecret` for
114
+ the HS* algorithm `idTokenSignedResponseAlg` names: `iss`, `aud` = the client id,
115
+ `exp` with `clockSkew`, payload `typ` = `ID`, `azp` when there are several audiences, `nonce`, `at_hash` (hash of the
116
+ signing algorithm) and `auth_time` against `maxAge`. An encrypted ID token is decrypted with `clientEncryptionKey`
117
+ first. A back-channel logout token must be typed `logout+jwt`, carry the back-channel event, no `nonce`, and a `sub`
118
+ or `sid`. Every refusal is an `AuthioTokenValidationError` whose `reason` uses the outcome names of
119
+ `conformance/tokens.json` (`invalid_nonce`, `invalid_at_hash`, `invalid_logout_token`, `invalid_algorithm`, `expired`,
120
+ …). In Express, `authioBackchannelLogout` from `@hyper.authio/express` is the endpoint.
121
+
122
+ `readAccessTokenClaims(accessToken)` returns the payload of an access token the client received once it is decrypted
123
+ and its signature, `iss` and `exp` verify (`undefined` for an opaque token). For front-channel logout, Authio loads the
124
+ client's front-channel logout URI in an iframe; `validateFrontChannelLogout(request.url)` checks `iss` and returns the
125
+ `sessionId` (`sid`) to end (`authioFrontchannelLogout` in `@hyper.authio/express`).
126
+
127
+ ### Step-up
128
+
129
+ When an API answers `403` with `WWW-Authenticate: Bearer error="insufficient_user_authentication", acr_values="2",
130
+ max_age=300` (RFC 9470):
131
+
132
+ ```ts
133
+ const challenge = parseAuthioBearerChallenge(response.headers.get("www-authenticate"));
134
+ const { url, transaction } = await authio.createStepUpAuthorizationUrl(challenge!); // acr_values, max_age, prompt=login for max_age=0
135
+ ```
136
+
40
137
  ## Grants
41
138
 
42
139
  ```ts
43
140
  await authio.tokens.clientCredentials({ scopes: ["orders:read"], resource: "https://orders.example.com" });
44
141
  await authio.tokens.authorizationCode({ code, redirectUri, codeVerifier });
45
- await authio.tokens.refresh({ refreshToken }); // store the rotated refresh_token it returns
142
+ await authio.tokens.refresh({ refreshToken, resource }); // store the rotated refresh_token it returns; resource: RFC 8707
46
143
  await authio.tokens.exchange({ subjectToken, resource }); // on-behalf-of (RFC 8693)
47
144
  await authio.tokens.password({ username, password });
145
+ await authio.tokens.personalAccessToken({ token: "pat_…", clientId: "billing-api" }); // clientId only for a consumer's PAT
48
146
 
49
147
  const device = await authio.tokens.deviceAuthorization({ scopes: "openid" });
50
148
  console.log(device.verification_uri_complete);
@@ -52,13 +150,16 @@ const token = await authio.tokens.pollDevice(device); // waits, slows down,
52
150
 
53
151
  const connection = await authio.tokens.exchangeForConnection({ subjectToken, provider: "github" });
54
152
  switch (connection.status) {
55
- case "granted": /* connection.accessToken */ break;
153
+ case "granted": /* accessToken, issuedTokenType, tokenType, expiresIn, scope, connectionId, provider, providerType, externalAccount */ break;
56
154
  case "required": /* send the person to connection.connectUri */ break;
57
- case "selection_required": /* ask again with connectionId */ break;
58
- case "refused": /* connection.error */ break;
155
+ case "selection_required": /* ask again with one of connection.connections[].id (label, externalLogin) */ break;
156
+ case "refused": /* connection.error, connection.errorCode */ break;
59
157
  }
60
158
  ```
61
159
 
160
+ Device polling waits `interval`, keeps polling on `authorization_pending`, adds 5 seconds on `slow_down`, and stops
161
+ with the error on `access_denied` and `expired_token` (`toDevicePollOutcome` tells which).
162
+
62
163
  OAuth errors throw `AuthioProtocolError` (`error`, `errorDescription`, `errorCode`, `status`); `error` is
63
164
  `http_{status}` when Authio sent none, `invalid_response` when a success body could not be read. Tokens print as
64
165
  `[redacted]` in `console.log` and `util.inspect`, and serialize normally.
@@ -84,6 +185,27 @@ const info = await authio.userInfo(accessToken); // JSON, or a signed / encrypte
84
185
  Authio answers introspection according to who asks: a token requested for a resource is active only to the client
85
186
  that resource server names as its introspection client.
86
187
 
188
+ ## Admin API credentials
189
+
190
+ ```ts
191
+ import { createAuthioAccountCredential, readAuthioAdminResponse } from "@hyper.authio/client";
192
+
193
+ const credential = createAuthioAccountCredential({
194
+ authority: "https://auth.example.com",
195
+ credential: process.env.AUTHIO_CREDENTIAL, // ak_…, cs_… or pat_…
196
+ credentialId: process.env.AUTHIO_CREDENTIAL_ID, // needed for a cs_ secret
197
+ });
198
+ const response = await credential.fetch("https://auth.example.com/api/v1/admin/realms");
199
+ const { data } = await readAuthioAdminResponse(response); // { items, totalCount, pageNumber, pageSize } for a listing; AuthioAdminError otherwise
200
+ ```
201
+
202
+ `ak_` and `pat_` go directly as `Authorization: Bearer` (or `x-api-key` with `presentAs`); a `cs_` secret is exchanged
203
+ at `{authority}/realms/authio` with `client_credentials` and the `client_id` `cred_{id}`, and a `pat_` may be exchanged
204
+ with the `personal_access_token` grant (`exchangePersonalAccessToken: true`). The `fetch` retries an idempotent request
205
+ once after `Retry-After` on `429`/`503`. `AuthioAdminError` carries `status` (the HTTP one), `errorCode`,
206
+ `notifications`, `requestId` (body or `X-Request-Id`) and `retryAfterSeconds`; `parseAuthioAdminEnvelope` reads an
207
+ answer you fetched yourself. The full Admin client comes in a later release.
208
+
87
209
  ## Roles and permissions
88
210
 
89
211
  ```ts
@@ -110,9 +232,15 @@ const result = await verifyWebhookSignature(request.headers["x-authio-signature"
110
232
  if (!result.valid) return reply(401);
111
233
 
112
234
  const event = parseWebhookEvent(rawBody);
113
- const user = asUserEvent(event); // typed data for user_created / user_updated / user_deleted, else undefined
235
+ const user = asUserEvent(event); // { id, username, emailConfirmed, … } for user_created / user_updated / user_deleted, else undefined
114
236
  ```
115
237
 
238
+ Every event type has its accessor (`asLoginEvent`, `asUserLockedOutEvent`, `asTokenRevokedEvent`,
239
+ `asKeysRotatedEvent`, `asTokenReuseDetectedEvent`, `asAdminResourceEvent`, `asUserSuspensionEvent`,
240
+ `asAttackDetectedEvent`, `asCredentialCompromisedEvent`, `asPingEvent`), which exposes every member of `data` in
241
+ camelCase with its value unchanged; `event.data` stays as delivered (snake_case). An unknown type still parses
242
+ (`isKnownWebhookEventType` is `false`, `event.data` is the raw JSON).
243
+
116
244
  The signature is HMAC-SHA256 of `{t}.{body}`, compared in constant time; the timestamp must be within 5 minutes.
117
245
  [`@hyper.authio/server`](../server/README.md) adds deduplication and dispatch.
118
246