Skip to content

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

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.
  1. Make a code verifier (43 to 128 characters) and its S256 challenge, a state and a nonce. Keep them for the callback.
  2. Send the person to the authorize address. They sign in on accounts.aidenkt.com if they are not already, and allow the app once.
  3. They come back to the redirect URI with code, state and iss, or with error (access_denied when they chose Cancel). Check that state is yours and iss is the issuer.
  4. Exchange the code within 60 seconds. It works once.
  5. 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=S256
// 1. Before sending the person to sign in
const 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/callback
const 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();

The same form, with the client ID and secret as HTTP Basic credentials:

Terminal window
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.

Before trusting it, check:

  • the ES256 signature, from a key in /oauth/jwks.json;
  • iss is https://accounts.aidenkt.com;
  • aud (and azp) is your client ID;
  • nonce is the one you sent;
  • exp is 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=none answers login_required or consent_required instead of showing a page. prompt=consent shows the consent screen again.
  • Errors from the token endpoint follow RFC 6749: invalid_request, invalid_client (401), invalid_grant, unsupported_grant_type. A bad client_id or redirect_uri is 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.
Terminal window
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.