Identity assertion (ID-JAG)
How an agent signed in at a trusted identity provider registers with an ID-JAG, gets a user's approval for read access and selected changes, and how the provider revokes it.
Identity assertion (ID-JAG)
An agent that already holds an identity from a trusted identity provider can register with
an Identity Assertion JWT Authorization Grant (ID-JAG) instead of anonymous or
service_auth. If the verified email in that assertion belongs to an Uptimeify account, the
account holder approves the agent once on the /claim page and chooses which changes it may
make; from then on the agent re-registers with a fresh ID-JAG and receives its token without
another approval.
This registration type is off unless Uptimeify trusts at least one identity provider. Check
the authorization-server metadata: only when agent_auth.identity_types_supported contains
"identity_assertion" are the endpoints on this page live. Otherwise
POST /agent/identity {"type":"identity_assertion"} returns 400 issuer_not_enabled and
POST /agent/event/notify returns 404.
1. Register with an ID-JAG
POST https://uptimeify.io/agent/identity
| Field | Type | Required | Description |
|---|---|---|---|
type | string | yes | "identity_assertion" |
assertion_type | string | yes | "urn:ietf:params:oauth:token-type:id-jag" |
assertion | string | yes | The ID-JAG issued by your identity provider. |
scope | string | no | Space-separated scopes you are asking for, for example "api.read api.write.monitors". Unknown values are ignored; without any known scope the request counts as "api.read". |
The ID-JAG must:
- be signed by a key from the JWKS of an issuer on Uptimeify's trust list (
iss), - carry
aud=https://uptimeify.io/api/, an unexpiredexpand ajtithat has never been used before (everyjtiis accepted exactly once), - carry
emailwithemail_verified: true, - carry an
auth_timeno older than 3600 seconds; otherwise the answer is401 login_requiredwithmax_age: 3600, and you need a fresh sign-in at your provider.
curl -X POST https://uptimeify.io/agent/identity \
-H 'Content-Type: application/json' \
-d '{"type":"identity_assertion","assertion_type":"urn:ietf:params:oauth:token-type:id-jag","assertion":"<ID-JAG>","scope":"api.read api.write.monitors"}'The response is one of three outcomes. All of them are sent with Cache-Control: no-store.
Approval needed: 401 interaction_required
The email belongs to an Uptimeify account, and this agent has not been approved yet.
{
"error": "interaction_required",
"registration_id": "reg_…",
"claim": {
"user_code": "WDJB-MJHT",
"verification_uri": "https://uptimeify.io/claim",
"verification_uri_complete": "https://uptimeify.io/claim?claim_attempt=cat_…",
"expires_in": 600,
"interval": 5
},
"claim_token": "clm_…",
"requested_scope": "api.read api.write.monitors"
}Send your user to verification_uri_complete. Signed in as the account with that email, they
see what you asked for: read access, and every requested write area as a separate checkbox.
No write area is selected by default; the user ticks only what the agent really needs. Then
poll POST /oauth2/token with the claim grant and the claim_token, exactly as in
Claim flow, step 4. The success response
carries the approved scope and an identity_assertion for later exchanges.
An email match never links an account on its own: nothing is granted until the signed-in account
holder confirms on /claim.
Already approved: 200 with an account assertion
The registration for your provider identity (iss, sub) has been approved before.
{
"registration_id": "reg_…",
"identity_assertion": "<jwt>",
"scope": "api.read api.write.monitors",
"token_endpoint": "https://uptimeify.io/oauth2/token"
}scope is what you asked for if the approval covers it, otherwise everything that was approved.
Without a scope field you receive everything that was approved. Exchange the
identity_assertion via the jwt-bearer grant as described in
Agent authentication.
No matching account: 200 with tools.public
No Uptimeify account has this email. The agent is registered and can use the public check tools straight away, nothing more.
{
"registration_id": "reg_…",
"identity_assertion": "<jwt>",
"scope": "tools.public",
"token_endpoint": "https://uptimeify.io/oauth2/token"
}2. What the agent may do
- Never more than the approving user. Every time a token is minted, Uptimeify reads that user's current role and customer assignment. If the role has changed since the approval, the token gets the current one; the role is never stored with the registration.
- Only what was approved. An exchange asking for a scope that was not approved is refused
with
400 invalid_grant(Identity-assertion grant refused: scope_exceeds_grant.). - Global supporters never write. For such an account every write area is dropped at approval
and again at every exchange; asking for write access alone then returns
400 invalid_grant(nothing_left). - A changed customer assignment needs a new approval. If the user's customer binding differs
from the one approved, the exchange returns
400 invalid_grant(binding_changed). - No user, no token. If the approving user has been deleted, the exchange returns
400 invalid_grant(no_human). - Writes follow the same rules as every agent token: they need the write kill-switch on this deployment, and each write area reaches only its own paths; see error codes. Where Uptimeify records who made a change, a change made by the agent is attributed to the approving user.
3. Revocation
The account holder sees the agent under Settings → Connected apps as
<provider host> (Agent) and can revoke it there (see
Connected apps). Revocation ends the registration and every access token
derived from it immediately.
The identity provider can revoke it too, with a Security Event Token (RFC 8417) pushed to Uptimeify (RFC 8935):
POST https://uptimeify.io/agent/event/notify
curl -X POST https://uptimeify.io/agent/event/notify \
-H 'Content-Type: application/secevent+jwt' \
--data-binary '<SET>'The SET must be signed by a trusted issuer, carry aud = https://uptimeify.io/api/ and the
sub of the ID-JAG, and name one of the event types listed in the metadata under
agent_auth.events_supported:
https://schemas.openid.net/secevent/risc/event-type/session-revokedhttps://schemas.openid.net/secevent/caep/event-type/session-revoked
A SET that revoked a registration is answered with 202 and an empty body. Anything else
(signature, issuer, audience, event type, unknown sub) is answered with 400 and
{"err":"invalid_request"}, without saying which check failed, and revokes nothing. The endpoint
URL is also published as agent_auth.events_endpoint.
Common errors
| HTTP | error / err | Where | Meaning |
|---|---|---|---|
| 400 | issuer_not_enabled | POST /agent/identity | Uptimeify trusts no identity provider on this deployment. |
| 400 | invalid_request | POST /agent/identity | Wrong assertion_type, missing assertion, or the ID-JAG failed a check (signature, untrusted issuer, audience, expiry, reused jti, unverified email). |
| 401 | login_required | POST /agent/identity | auth_time is older than max_age (3600 s): sign in again at your provider. |
| 401 | interaction_required | POST /agent/identity | Not an error to retry: the account holder has to approve on /claim first. |
| 400 | invalid_grant | POST /oauth2/token (jwt-bearer) | The exchange exceeds the approval, the customer binding changed, the user was deleted, or nothing is left after dropping write access. |
| 400 | expired_token | POST /oauth2/token (claim grant) | As in the claim flow; also when the approving user can no longer be resolved at the moment of minting. |
| 400 | invalid_request (err) | POST /agent/event/notify | Wrong Content-Type, empty body, or the SET was not accepted. |
| 404 | - | POST /agent/event/notify | Uptimeify trusts no identity provider, or requested on a custom domain. |
See the error codes reference for the full list.