Sign in with Revoked¶
Every Revoked server is an OpenID Connect provider. A service — a game server, a shop, a forum — signs people in with their Revoked account through an ordinary OpenID Connect client library, without keeping passwords or accounts of its own. What the service learns is decided by the person on a consent screen, and data it asks for arrives as a living grant rather than a copy.
The endpoint reference is under OpenID Connect.
A complete example service is in
examples/sign-in-demo.
What a service needs¶
Nothing is registered in advance. The values a client library asks for are:
| Setting | Value |
|---|---|
| Issuer | https://<domain> of the person's server. Discovery is at https://<domain>/.well-known/openid-configuration. A server developed on locally answers as http://localhost:<port> when reached there. |
| Client id | The service's origin, e.g. https://game.example. |
| Redirect URI | Any address on that origin, e.g. https://game.example/auth/callback. |
| Client secret | None. The client is public; PKCE with S256 is required in its place. |
| Flow | Authorization code. |
Finding the person's server¶
Which server a person uses is not known in advance, since every instance is self-hosted. The person's Revoked app knows it, so a sign-in usually starts there.
Through the app¶
A "Sign in with Revoked" button opens the app with the authorization request itself, which depends on nothing about the server:
revoked://signin?client_id=https%3A%2F%2Fgame.example
&redirect_uri=https%3A%2F%2Fgame.example%2Fauth%2Fcallback
&response_type=code&scope=openid%20profile%20email
&state=…&nonce=…&code_challenge=…&code_challenge_method=S256
&claims=…
The app opens the authorization page of the server it is signed into, in
the browser, with the same parameters. The person confirms with their passkey
there, and the browser returns to the redirect address with the code, the
state, and iss — the issuer of the server that answered (RFC 9207).
The service then works as with a known server, starting from that issuer:
issis checked like any server the service accepts: anhttpsorigin, on an allow-list if the service keeps one.- Discovery runs against
iss. Client libraries refuse metadata whoseissuerdiffers, and refuse an answer whoseissdiffers from the discovered issuer. - The code is redeemed at that server's token endpoint, with the PKCE
verifier and the
stateandnoncethe service remembered.
A person is known by the issuer and sub together. An answer from a server
other than the person's own cannot sign anyone in as one of that server's
users: the code, the ID token and the subject all belong to the server that
issued them.
The app opens the system browser, so the service's session cookie must be
there for the state to be found: the button belongs on pages opened in the
default browser.
Without the app¶
On a device without the app, revoked:// leads nowhere: most browsers stay on
the page, some show an error. The service then asks for the server's domain
and starts the sign-in there, as above. Remembering the issuer after the
first sign-in spares the question on later visits.
Scopes and claims¶
| Scope | Claims |
|---|---|
openid (required) |
sub |
profile |
name |
email |
email, email_verified |
revoked_identity |
revoked_identity — the fingerprint of the person's primary identity, the same at every service |
sub is different at every service: the same person signing in to two
services gives each its own identifier, so services cannot match their users
with each other. It stays the same for one service across sign-ins.
The person can decline every scope but openid on the consent screen. The
scope returned by the token endpoint lists what was granted.
Asking for vault entries¶
Entries from the person's vault are asked for by key, through the claims
parameter:
{ "userinfo": { "revoked_links": { "values": ["shipping_address", "phone"] } } }
On the consent screen the person picks one of their entries for each key, or none. The userinfo endpoint then returns a grant over the chosen entries:
{
"sub": "4kS0…",
"revoked_links": {
"endpoint": "https://<domain>/api/grants/resolve",
"grants": [ { "id": "q8w2…", "token": "…", "keys": ["shipping_address"] } ]
}
}
The values are read from endpoint with the grant's id and token, whenever
they are needed:
POST /api/grants/resolve
{ "grants": [ { "id": "q8w2…", "token": "…" } ] }
Each answer carries the current values, an etag for asking again cheaply,
and maxAge, the number of seconds a value may be kept before it is read
again. When the person edits the entry, the next read returns the new value;
when they revoke the grant, the next read returns revoked and no values.
The grant appears in the person's app as a link of their own, labelled with the service's origin. Signing in again and sharing again updates that same link and replaces its token.
Sessions¶
Every sign-in is confirmed with the person's passkey, and the provider keeps no session of its own. Consequently:
prompt=noneis always answered withlogin_required.- There are no refresh tokens. The ID token proves a sign-in that just happened; the service keeps its own session after that.
- There is nothing to log out of at the provider.
The signing key¶
ID tokens are signed with RS256 by a key published at /oidc/jwks. It is not
the server's root key but a separate key the root key certifies; the
certification is published with it as revoked_certification. A service that
walks the trust chain from the DNS pin to the root key can
check that certification and so tie every ID token to the domain. Ordinary
client libraries ignore the member and verify tokens against the JWKS as
usual.