Skip to content

Commit ecfdfeb

Browse files
authored
docs: document Grant.LimitExceeded webhook event (#1460)
* docs: document Grant.LimitExceeded webhook event * docs: point revoked grant IDs at audit logs instead of grants list * docs: drop audit log cross-reference for revoked grant IDs
1 parent 0d7195a commit ecfdfeb

4 files changed

Lines changed: 82 additions & 7 deletions

File tree

docs/developers/webhooks/events.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ This guide list the different Logto webhook events and explains when each event
129129
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
130130
| Identifier.Lockout | A user account is locked due to consecutive failed identity verification attempts. Can be triggered in the following flows:<br /><ul><li>Password verification failed</li><li>Code verification failed</li><li>One-time token verification failed</li></ul> |
131131
| Message.RateLimited | An end user exceeds the per-recipient [send rate limit](/security/send-rate-limit) for verification codes. Fires only on the end-user send paths (Experience API and Account API), with a payload of `{ action, recipient }`. |
132+
| Grant.LimitExceeded | A successful authorization pushes a user past an app's [max concurrent authenticated devices](/sessions/session-configs#max-concurrent-authenticated-devices-per-app) limit (`maxAllowedGrants`), so Logto revokes their oldest grants for that app. |
132133

133134
## FAQs \{#faqs}
134135

docs/developers/webhooks/request.mdx

Lines changed: 62 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ The body is a JSON object. Its exact shape depends on which family the event bel
2727
| ----------------- | -------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
2828
| **User flow** | `PostRegister`, `PostSignIn`, `PostResetPassword` | A user completes a sign-up, sign-in, or password-reset flow handled by the Experience API. |
2929
| **Data mutation** | `User.*`, `Role.*`, `Scope.*`, `Organization.*`, `OrganizationRole.*`, `OrganizationScope.*` | The underlying data model is mutated by a Management API call or a user flow on the Experience API. |
30-
| **Exception** | `Identifier.Lockout` | A security incident, for example an account locked after consecutive failed verification attempts. |
30+
| **Exception** | `Identifier.Lockout`, `Message.RateLimited`, `Grant.LimitExceeded` | A security incident, for example an account locked after consecutive failed verification attempts. |
3131

3232
Every family shares a small set of [common fields](#common-fields). Each family then layers on its own request-context fields plus an event-specific payload.
3333

@@ -365,14 +365,15 @@ type OrganizationScope = {
365365

366366
## Exception event payloads \{#exception-event-payloads}
367367

368-
**Events:** `Identifier.Lockout`.
368+
**Events:** `Identifier.Lockout`, `Message.RateLimited`, `Grant.LimitExceeded`.
369369

370-
Fired on security incidents, for example an account locked after consecutive failed verification attempts. These events always originate from a user-facing flow, so the body carries:
370+
Fired on security incidents, for example an account locked after consecutive failed verification attempts, or grants evicted because an app's concurrent-device limit was exceeded.
371371

372-
- The [common fields](#common-fields).
373-
- The `ip` field (same shape as data-mutation events).
374-
- The [Experience API context fields](#experience-api-context-fields).
375-
- The exception-specific fields below.
372+
Every exception event carries the [common fields](#common-fields) and an `ip` field (same shape as data-mutation events). The remaining fields depend on the event.
373+
374+
### Identifier.Lockout \{#identifierlockout-payload}
375+
376+
Originates from a user-facing flow, so the body also carries the [Experience API context fields](#experience-api-context-fields), plus:
376377

377378
```tsx
378379
enum SignInIdentifier {
@@ -386,3 +387,57 @@ enum SignInIdentifier {
386387
| ----- | ------------------ | -------- | ----------------------------------------------------------- |
387388
| type | `SignInIdentifier` | | The user's identifier type, e.g., email, phone or username. |
388389
| value | `string` | | The user's identifier value that triggered the lockout. |
390+
391+
### Message.RateLimited \{#messageratelimited-payload}
392+
393+
Originates from a user-facing flow, so the body also carries the [Experience API context fields](#experience-api-context-fields), plus:
394+
395+
| Field | Type | Optional | Notes |
396+
| --------- | -------- | -------- | -------------------------------------------------------------------------------------------- |
397+
| action | `string` | | The rate-limited action, e.g., `VerificationCodeSend`. |
398+
| recipient | `string` | | The email address or phone number that hit the [send rate limit](/security/send-rate-limit). |
399+
400+
### Grant.LimitExceeded \{#grantlimitexceeded-payload}
401+
402+
Fired when a successful authorization pushes a user past an app's [max concurrent authenticated devices](/sessions/session-configs#max-concurrent-authenticated-devices-per-app) limit (`maxAllowedGrants`) and Logto revokes their oldest grants for that app.
403+
404+
This event is emitted from the OIDC authorization endpoint rather than the Experience API, so it does **not** carry `interactionEvent` or `sessionId`. Alongside the common fields and `ip`, the body carries:
405+
406+
| Field | Type | Optional | Notes |
407+
| ----------------------------- | ------------------- | -------- | -------------------------------------------------------------------------------------------------------------------- |
408+
| userId | `string` | | The user whose grants were revoked. |
409+
| applicationId | `string` | | The application whose `maxAllowedGrants` limit was exceeded. |
410+
| application | `ApplicationEntity` || The application entity. Omitted if the application can't be resolved at delivery time. |
411+
| maxAllowedGrants | `number` | | The limit configured on the application when the event fired. |
412+
| preRevocationActiveGrantCount | `number` | | The number of active grants the user held for this application before the revocation, including the one just issued. |
413+
| revokedGrantIds | `string[]` | | The IDs of the grants that were actually revoked, oldest first. |
414+
415+
Example payload:
416+
417+
```json
418+
{
419+
"hookId": "hook_abc",
420+
"event": "Grant.LimitExceeded",
421+
"createdAt": "2024-01-01T00:00:00.000Z",
422+
"ip": "192.168.0.1",
423+
"userAgent": "Mozilla/5.0",
424+
"userId": "u_001",
425+
"applicationId": "app_xyz",
426+
"application": {
427+
"id": "app_xyz",
428+
"type": "SPA",
429+
"name": "My app",
430+
"description": "My app description"
431+
},
432+
"maxAllowedGrants": 2,
433+
"preRevocationActiveGrantCount": 3,
434+
"revokedGrantIds": ["grant_001"]
435+
}
436+
```
437+
438+
Delivery notes:
439+
440+
- The revoked grant records are destroyed as part of the revocation, so the IDs in `revokedGrantIds` no longer resolve through the grant listing endpoints or the Console — those return **active** grants only. Treat the payload as the record, and persist the IDs on your side if you need them later.
441+
- The event fires only when at least one grant was **actually revoked**. An authorization that stays within the limit produces no event.
442+
- It fires on every authorization that exceeds the limit, so a user repeatedly signing in from more devices than allowed produces one event per eviction.
443+
- Dispatch is fire-and-forget: a slow or failing endpoint never blocks or fails the user's authorization. Failed deliveries are recorded in the audit logs like any other webhook.

docs/integrate-logto/application-data-structure.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,8 @@ The OpenID Connect backchannel logout endpoint. See [Federated sign-out: Back-ch
203203

204204
This setting is useful when you want to cap concurrent authenticated devices per app.
205205

206+
Each eviction triggers the `Grant.LimitExceeded` [webhook event](/developers/webhooks/webhooks-events#exception-hook-events), so you can notify the user or track how often the limit is hit.
207+
206208
:::note
207209

208210
This field is not supported for:

docs/sessions/session-configs.mdx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,20 @@ This setting is not supported for machine-to-machine apps, protected apps, and S
9696

9797
:::
9898

99+
### Monitor evictions with a webhook \{#monitor-evictions-with-a-webhook}
100+
101+
When Logto revokes a user's oldest grants to enforce `maxAllowedGrants`, it triggers the `Grant.LimitExceeded` [webhook event](/developers/webhooks/webhooks-events#exception-hook-events), so you can react to devices being signed out rather than discovering it from support tickets.
102+
103+
The payload identifies the user and app, the limit in effect, how many grants were active before the eviction, and the grant IDs that were revoked. See [Grant.LimitExceeded payload](/developers/webhooks/webhooks-request#grantlimitexceeded-payload) for the full structure.
104+
105+
**Common use cases:**
106+
107+
- Notify the user that they were signed out on another device, and from where.
108+
- Feed device-eviction rates into your analytics to tell whether the configured limit is too low.
109+
- Flag unusual eviction patterns for a single user as a possible credential-sharing or account-takeover signal.
110+
111+
Navigate to <CloudLink to="/webhooks">Console > Webhooks</CloudLink> to subscribe, or create the hook via the Management API. See [Webhooks](/developers/webhooks) for configuration details.
112+
99113
## Related resources \{#related-resources}
100114

101115
<Url href="/sessions">Sessions</Url>
@@ -104,4 +118,7 @@ This setting is not supported for machine-to-machine apps, protected apps, and S
104118
<Url href="/integrate-logto/application-data-structure#max-allowed-grants-maxallowedgrants">
105119
Application data structure: maxAllowedGrants
106120
</Url>
121+
<Url href="/developers/webhooks/webhooks-events#exception-hook-events">
122+
Webhooks events: exception hook events
123+
</Url>
107124
<Url href="/integrate-logto/interact-with-management-api">Interact with Management API</Url>

0 commit comments

Comments
 (0)