Sign in with KT Accounts
Share
KT Accounts is an OpenID Connect provider. Use it for apps on any domain, for mobile apps, and for servers that need to know who is signed in. Standard libraries work: give them the issuer and they read the rest from the discovery document.
| Issuer | https://accounts.aidenkt.com |
| Discovery | https://accounts.aidenkt.com/.well-known/openid-configuration |
| Authorize | https://accounts.aidenkt.com/oauth/authorize |
| Token | https://accounts.aidenkt.com/oauth/token |
| UserInfo | https://accounts.aidenkt.com/oauth/userinfo |
| Keys | https://accounts.aidenkt.com/oauth/jwks.json (ES256) |
| Scopes | openid (required), profile, email, permissions |
| Flow | Authorization code only, response_mode=query |
| PKCE | S256 only. Required for public clients, recommended for all |
| Client authentication | client_secret_basic, client_secret_post, or none for public clients |
Before You Start
Section titled “Before You Start”Register the app in the Developer Portal:
- Redirect URIs: the exact callback addresses, for example
https://notes.example.com/auth/callback. They are compared as exact strings. - Client Type: Server (Has a Client Secret) for a confidential client, or Browser or Mobile (PKCE, No Secret) for a public client.
The Flow
Section titled “The Flow”- Make a code verifier (43 to 128 characters) and its S256 challenge, a
stateand anonce. Keep them for the callback. - Send the person to the authorize address. They sign in on
accounts.aidenkt.comif they are not already, and allow the app once. - They come back to the redirect URI with
code,stateandiss, or witherror(access_deniedwhen they chose Cancel). Check thatstateis yours andissis the issuer. - Exchange the code within 60 seconds. It works once.
- Check the ID token, then start your own session.
https://accounts.aidenkt.com/oauth/authorize?client_id=ktc_… &redirect_uri=https%3A%2F%2Fnotes.example.com%2Fauth%2Fcallback &response_type=code&scope=openid%20profile&state=STATE&nonce=NONCE &code_challenge=CHALLENGE&code_challenge_method=S256In a Browser App (Public Client)
Section titled “In a Browser App (Public Client)”// 1. Before sending the person to sign inconst b64 = (bytes) => btoa(String.fromCharCode(...bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");const verifier = b64(crypto.getRandomValues(new Uint8Array(32)));const challenge = b64(new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier))));const state = b64(crypto.getRandomValues(new Uint8Array(16)));const nonce = b64(crypto.getRandomValues(new Uint8Array(16)));sessionStorage.setItem("kt", JSON.stringify({ verifier, state, nonce }));location.assign("https://accounts.aidenkt.com/oauth/authorize?" + new URLSearchParams({ client_id: "YOUR_CLIENT_ID", redirect_uri: "https://your.app/callback", response_type: "code", scope: "openid profile", state, nonce, code_challenge: challenge, code_challenge_method: "S256",}));
// 2. On https://your.app/callbackconst q = new URLSearchParams(location.search);const saved = JSON.parse(sessionStorage.getItem("kt"));if (q.get("state") !== saved.state) throw new Error("state does not match");const r = await fetch("https://accounts.aidenkt.com/oauth/token", { method: "POST", body: new URLSearchParams({ grant_type: "authorization_code", client_id: "YOUR_CLIENT_ID", code: q.get("code"), redirect_uri: "https://your.app/callback", code_verifier: saved.verifier, }),});const { access_token, id_token } = await r.json();On a Server (Confidential Client)
Section titled “On a Server (Confidential Client)”The same form, with the client ID and secret as HTTP Basic credentials:
curl -s https://accounts.aidenkt.com/oauth/token -u "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" \ -d grant_type=authorization_code --data-urlencode code=CODE \ --data-urlencode redirect_uri=https://your.app/callback --data-urlencode code_verifier=VERIFIER{ "access_token": "kto_…", "token_type": "Bearer", "expires_in": 3600, "scope": "openid profile", "id_token": "eyJ…" }The body must be application/x-www-form-urlencoded. A public client sends client_id in the form and no secret.
A complete callback in a Worker or Node 18+ with jose:
import { createRemoteJWKSet, jwtVerify } from "jose";
const ISSUER = "https://accounts.aidenkt.com";const jwks = createRemoteJWKSet(new URL(`${ISSUER}/oauth/jwks.json`));
async function finishSignIn(url, saved, env) { if (url.searchParams.get("state") !== saved.state) throw new Error("state"); if (url.searchParams.get("iss") !== ISSUER) throw new Error("iss"); const r = await fetch(`${ISSUER}/oauth/token`, { method: "POST", headers: { authorization: "Basic " + btoa(`${env.CLIENT_ID}:${env.CLIENT_SECRET}`) }, body: new URLSearchParams({ grant_type: "authorization_code", code: url.searchParams.get("code"), redirect_uri: env.REDIRECT_URI, code_verifier: saved.verifier, }), }); if (!r.ok) throw new Error(`token ${r.status}`); const { id_token } = await r.json(); const { payload } = await jwtVerify(id_token, jwks, { issuer: ISSUER, audience: env.CLIENT_ID, algorithms: ["ES256"] }); if (payload.nonce !== saved.nonce) throw new Error("nonce"); return payload; // payload.sub is the person's KT Accounts ID}openid-client (Node), Auth.js, authlib (Python) and go-oidc (Go) do all of this from the discovery address.
Check the ID Token
Section titled “Check the ID Token”Before trusting it, check:
- the ES256 signature, from a key in
/oauth/jwks.json; issishttps://accounts.aidenkt.com;aud(andazp) is your client ID;nonceis the one you sent;expis in the future.
sub is the person’s KT Accounts ID. It is stable and is the value to store. ID tokens and access tokens last 1 hour.
- Redirect URIs are compared as exact strings. Send the one you registered character for character.
- Codes last 60 seconds and work once. A second use also revokes the tokens the first one gave.
- No refresh tokens. When the access token expires, send the person to the authorize address again. It is instant once they have allowed the app.
prompt=noneanswerslogin_requiredorconsent_requiredinstead of showing a page.prompt=consentshows the consent screen again.- Errors from the token endpoint follow RFC 6749:
invalid_request,invalid_client(401),invalid_grant,unsupported_grant_type. A badclient_idorredirect_uriis shown on KT Accounts’ own error page and never sent back to the app. - Consent is remembered per person and app. A person can remove the app in Account Settings at any time, which ends its tokens and codes.
UserInfo
Section titled “UserInfo”curl -s https://accounts.aidenkt.com/oauth/userinfo -H "Authorization: Bearer $ACCESS_TOKEN"{ "sub": "…", "name": "…", "preferred_username": "…", "picture": "…" }It answers with the claims for the scopes the person allowed. See Scopes and Claims.