Sign in with ILLEST
Let people sign in to your site or app with their ILLEST account. It is OAuth 2 and OpenID Connect, so any library that speaks them works: you send the person to ILLEST, they say yes on ILLEST's own page, and you get back who they are and what they allowed.
What an app never gets, whatever it asks for: a message, a key, a device, a contact, a wallet or an email address. Conversations are end-to-end encrypted and the server holds no key, so there is nothing to give.
Before you start
- Make an application in Settings → Developers in the app, or under Applications here. Its ID is your
client_id. - Open its OAuth2 page. Add your redirect address, written in full, and press Turn on Sign in with ILLEST.
- A site with a server: press Make a secret and keep it on the server. A page in a browser or a program on somebody's device cannot keep a secret: switch on Public client instead, and PKCE is its whole proof.
Every library finds the rest from the discovery document:
https://chat.illest.world/.well-known/openid-configurationThe issuer is https://chat.illest.world, the app's own address, which is where people are signed in.
The flow
- Make a random
state, a random PKCEcode_verifierand its S256code_challenge, and keep all three for this person. - Send them to the authorization endpoint:
https://chat.illest.world/oauth2/authorize
?response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=https%3A%2F%2Fexample.com%2Fauth%2Fillest%2Fcallback
&scope=openid%20profile
&state=STATE
&code_challenge=CODE_CHALLENGE
&code_challenge_method=S256- They come back to your redirect address with
code,stateandiss. Check thatstateis the one you made and thatissishttps://chat.illest.world. - Within a minute, trade the code at the token endpoint, with the verifier:
curl https://chat.illest.world/api/oauth2/token \
-u "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" \
-d grant_type=authorization_code \
-d code=THE_CODE \
-d redirect_uri=https://example.com/auth/illest/callback \
-d code_verifier=THE_VERIFIER{
"access_token": "illest_at_...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "openid profile",
"id_token": "eyJ..."
}- Check the ID token's signature against the JWKS, and that
iss,aud(your client ID),expandnonce(if you sent one) are right. Itssubis the person. A library does all of this for you.
Endpoints
| What | Where |
|---|---|
| Discovery | GET /.well-known/openid-configuration (also /.well-known/oauth-authorization-server) |
| Authorization | /oauth2/authorize, a page in the app |
| Token | POST /api/oauth2/token, form-encoded |
| Revocation | POST /api/oauth2/token/revoke (RFC 7009) |
| UserInfo | GET or POST /api/oauth2/userinfo, with Authorization: Bearer |
| Servers | GET /api/oauth2/guilds, with Authorization: Bearer |
| Keys | GET /api/oauth2/jwks |
Every one is on https://chat.illest.world, answers any origin (CORS, no cookies) and is rate limited per address; a 429 says when
to try again in Retry-After. A token is read from the Authorization header or the form, never from an address.
Scopes
| Scope | What the person is told | What you get |
|---|---|---|
openid | Know it is you when you come back | An ID token and sub |
profile | See your username, display name and profile picture | preferred_username, name, picture, in the ID token and UserInfo |
elite | See whether you have Elite | illest_elite, true or false, from UserInfo |
guilds | See which servers you are in | GET /api/oauth2/guilds: each server's id, name, icon and whether they own it |
guilds.join | Add you to the server named below, once | They are added on the consent page itself |
offline_access | Keep this access until you remove it | A refresh token |
guilds.join needs guild_id in the link: the one server to add them to. Your application's bot must already be in that server
and hold Create Invite, so the server's managers chose to let you; every rule the server keeps for an invite (bans, its join form,
its age rule) holds as it does for one. The join happens once, when they press Allow, and the token you get carries no power to
join anything later.
Parameters
response_type=code, client_id, redirect_uri, scope, state and code_challenge with code_challenge_method=S256 are
required. Optional: nonce (carried into the ID token), prompt (none, login, consent), max_age, and guild_id for
guilds.join. Each parameter may be given once.
- Redirect addresses are compared as text, in full.
https://example.com/callbackandhttps://example.com/callback/are different addresses. The one exception is a loopback address (http://127.0.0.1/cb,http://[::1]/cb), whose port may be any, for a program that listens on a free port (RFC 8252). A program on a phone or a desktop can also register its own scheme in reverse domain order (com.example.app:/callback). - PKCE is required of every app, with S256. A request without it is sent back as
invalid_request. prompt=nonenever shows a page: you get a code if the person allowed your app before, orconsent_required,login_requiredorinteraction_required.- A sign-in older than two hours,
prompt=loginandmax_ageask the person for a code from their authenticator first.auth_timein the ID token says when they gave one.
A request with an unknown client_id or a redirect address you did not register is never sent anywhere: the page shows the person
what was wrong and the address it was given, which is how you find a mistyped character. Any other fault comes back to your
address as error, error_description, state and iss.
Refresh tokens
Ask for offline_access to get one. Each is good once: exchanging it gives a new access token and a new refresh token, and the old
one stops.
curl https://chat.illest.world/api/oauth2/token \
-u "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" \
-d grant_type=refresh_token \
-d refresh_token=illest_rt_...- An access token lasts an hour. A refresh token not used for thirty days expires, and a sign-in's refresh tokens end a year after it (ninety days for a public client); then you send the person through the page again, which is one press.
- A refresh token used twice ends the whole sign-in: it means somebody else has a copy. Within ten seconds of its exchange a
second use is only refused (
refresh_just_rotated), so two of your own requests racing do not sign the person out; use the answer the first one got. - You may ask for fewer scopes with
scope, never more.
Errors
Every token error is JSON with error and error_description. invalid_grant covers many causes, so it also carries
illest_reason, a word to branch on:
illest_reason | What happened |
|---|---|
code_unknown | Not a code ILLEST gave, or the person removed your app |
code_reused | The code was used already. With the right verifier, what it gave the first time is revoked |
code_expired | More than a minute old |
wrong_client | Given to another app |
redirect_mismatch | Not the redirect_uri the code was asked with |
pkce_missing | No code_verifier (sent as invalid_request) |
pkce_mismatch | The verifier does not hash to the challenge |
refresh_unknown | Not a live refresh token: revoked, or the app was removed |
refresh_just_rotated | Exchanged a moment ago; use the newer one |
refresh_reused | Used twice, so its sign-in was revoked |
refresh_expired | Past its time |
A wrong secret, or none from a confidential app, is invalid_client with status 401.
Signing out, and being removed
- Revoke a refresh token when the person signs out of your site: every token of that sign-in ends.
- People see your app in Settings → Authorized apps, with what it may see and Remove, which ends everything it holds at once.
- End every sign-in on your app's OAuth2 page ends every token it holds, for everybody: the button for a leaked secret or database. Reset the secret too.
- An app ILLEST has not verified is marked so on its consent page, may be allowed by up to 100 people besides its maker, and may not be named like ILLEST ("illest", "official", "support", "staff").
The button
Use these as they are, on a page of the matching kind. A press downloads the file.
Or as HTML, which follows your page's font:
<a class="illest-signin" href="/auth/illest">
<svg viewBox="0 0 144 144" width="20" height="20" aria-hidden="true">
<path fill="#f71a14" d="M14 40L40 14L113.03 14L91.03 36L48 36L48 79.03L14 113.03Z"/>
<path fill="#f71a14" d="M130 104L104 130L30.97 130L52.97 108L96 108L96 64.97L130 30.97Z"/>
</svg>
Sign in with ILLEST
</a>
<style>
.illest-signin { display: inline-flex; align-items: center; gap: 10px; height: 40px; padding: 0 14px; border-radius: 8px;
border: 1px solid #2b2b2b; background: #080808; color: #f2f2f2; font-weight: 600; text-decoration: none; }
.illest-signin:hover { border-color: #5c5c5c; }
</style>Keep the mark red, or white or black when the page is one colour. Do not redraw it, and do not put it in a button that does something else.
Node
With openid-client (version 6):
import express from "express";
import session from "express-session";
import * as client from "openid-client";
const ISSUER = new URL("https://chat.illest.world");
const REDIRECT = "https://example.com/auth/illest/callback";
const config = await client.discovery(ISSUER, process.env.ILLEST_CLIENT_ID, process.env.ILLEST_CLIENT_SECRET);
const app = express();
app.use(session({ secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false }));
app.get("/auth/illest", async (req, res) => {
const verifier = client.randomPKCECodeVerifier();
req.session.illest = { verifier, state: client.randomState(), nonce: client.randomNonce() };
const url = client.buildAuthorizationUrl(config, {
redirect_uri: REDIRECT,
scope: "openid profile",
code_challenge: await client.calculatePKCECodeChallenge(verifier),
code_challenge_method: "S256",
state: req.session.illest.state,
nonce: req.session.illest.nonce,
});
res.redirect(url.href);
});
app.get("/auth/illest/callback", async (req, res) => {
const { verifier, state, nonce } = req.session.illest ?? {};
delete req.session.illest;
const tokens = await client.authorizationCodeGrant(config, new URL(req.originalUrl, "https://example.com"), {
pkceCodeVerifier: verifier,
expectedState: state,
expectedNonce: nonce,
});
const claims = tokens.claims(); // checked: signature, iss, aud, exp, nonce
req.session.user = { illestId: claims.sub, name: claims.name };
res.redirect("/");
});
app.listen(3000);Python
With Authlib and Flask:
import os
from authlib.integrations.flask_client import OAuth
from flask import Flask, redirect, session, url_for
app = Flask(__name__)
app.secret_key = os.environ["SESSION_SECRET"]
oauth = OAuth(app)
oauth.register(
"illest",
client_id=os.environ["ILLEST_CLIENT_ID"],
client_secret=os.environ["ILLEST_CLIENT_SECRET"],
server_metadata_url="https://chat.illest.world/.well-known/openid-configuration",
client_kwargs={"scope": "openid profile", "code_challenge_method": "S256"},
)
@app.route("/auth/illest")
def login():
return oauth.illest.authorize_redirect(url_for("callback", _external=True))
@app.route("/auth/illest/callback")
def callback():
token = oauth.illest.authorize_access_token() # checks state, PKCE, the ID token and its nonce
claims = token["userinfo"]
session["user"] = {"illest_id": claims["sub"], "name": claims.get("name")}
return redirect("/")Plain HTTP
Everything above is three requests: the link (step 2 of the flow), the token exchange (step 4), and then whatever the scopes allow:
curl https://chat.illest.world/api/oauth2/userinfo -H "Authorization: Bearer illest_at_..."{ "sub": "123456789", "preferred_username": "mira", "name": "Mira", "picture": "https://chat.illest.world/media/..." }A public client sends client_id in the form instead of a secret. Revoke with the same client proof:
curl https://chat.illest.world/api/oauth2/token/revoke -u "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" -d token=illest_rt_...