Koodiesimerkit
Kolme itsenäistä, kopioitavaa esimerkkiä. Kaikki käyttävät sivun muissa osissa esiteltyjä rajapintoja sellaisenaan.
1. Next.js + Auth.js (selainkirjautuminen)
Osio nimeltä “1. Next.js + Auth.js (selainkirjautuminen)”Vähimmäistoteutus, joka autentikoi käyttäjän tunnuspalvelun kautta OIDC:llä.
AUTH_SECRET=<openssl rand -base64 32>AUTH_TUNNUSPALVELU_ISSUER=https://id.esimerkki.fiAUTH_CLIENT_ID=esimerkki-webAUTH_CLIENT_SECRET=<client-luonnin tuloste>import NextAuth from "next-auth";
export const { handlers, auth, signIn, signOut } = NextAuth({ providers: [ { id: "tunnuspalvelu", name: "Tunnuspalvelu", type: "oidc", issuer: process.env.AUTH_TUNNUSPALVELU_ISSUER, clientId: process.env.AUTH_CLIENT_ID, clientSecret: process.env.AUTH_CLIENT_SECRET, }, ], callbacks: { async jwt({ token, account }) { // Säilytä access ja ID token kirjautumisen jälkeen if (account) { token.accessToken = account.access_token; token.idToken = account.id_token; } return token; }, async session({ session, token }) { // Vie access token client-sessioon tarvittaessa session.accessToken = token.accessToken as string; return session; }, },});import { handlers } from "@/auth";export const { GET, POST } = handlers;// Palvelinkomponentti — sessiotarkistusimport { auth } from "@/auth";import { redirect } from "next/navigation";
export default async function Page() { const session = await auth(); if (!session) redirect("/api/auth/signin"); // session.user.id on käyttäjän pysyvä UUID (sub)}Auth.js hakee discovery-dokumentin automaattisesti issuer-arvosta. Lisää
authorization: { params: { scope: "openid email profile orgs roles" } }
provider-määrittelyyn, jos tarvitset laajempia claimeja
(ks. Tokenit ja claimit).
2. M2M TypeScript-client
Osio nimeltä “2. M2M TypeScript-client”Riippumaton, nolla-riippuvuuksinen apuluokka machine API -kutsujen allekirjoittamiseen. Toimii Node.js 18+:ssa ja Cloudflare Workersissa (Web Crypto -rajapinnan kautta). Älä käytä sitä selaimessa: HMAC-avain antaa kaikki clientille myönnetyt Machine API -oikeudet, joten se kuuluu vain luotettuun taustapalveluun.
export interface TunnuspalveluConfig { baseUrl: string; // esim. "https://id.esimerkki.fi" clientId: string; hmacKeyHex: string; // 64-merkkinen hex-avain}
async function importKey(hexKey: string): Promise<CryptoKey> { const bytes = new Uint8Array(hexKey.match(/.{2}/g)!.map((h) => parseInt(h, 16))); return crypto.subtle.importKey("raw", bytes, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);}
async function sha256Hex(data: Uint8Array): Promise<string> { return toHex(await crypto.subtle.digest("SHA-256", data));}
async function hmacHex(key: CryptoKey, message: string): Promise<string> { return toHex(await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(message)));}
function toHex(buf: ArrayBuffer): string { return Array.from(new Uint8Array(buf)).map((b) => b.toString(16).padStart(2, "0")).join("");}
export async function signedPost( config: TunnuspalveluConfig, path: string, body: unknown,): Promise<Response> { const bodyBytes = new TextEncoder().encode(JSON.stringify(body)); const timestamp = String(Math.floor(Date.now() / 1000)); const bodyHash = await sha256Hex(bodyBytes); const signingString = `POST\n${path}\n${timestamp}\n${bodyHash}`;
const key = await importKey(config.hmacKeyHex); const signature = await hmacHex(key, signingString);
return fetch(`${config.baseUrl}${path}`, { method: "POST", headers: { "Content-Type": "application/json", "X-Client-Id": config.clientId, "X-Timestamp": timestamp, "X-Signature": signature, }, body: bodyBytes, });}
export async function upsertUser( config: TunnuspalveluConfig, user: { email: string; name?: string; phone?: string },): Promise<{ sub: string }> { const resp = await signedPost(config, "/admin/users/upsert", user); if (!resp.ok) { throw new Error(`upsertUser ${resp.status}: ${await resp.text()}`); } return resp.json();}Käyttö:
const config = { baseUrl: "https://id.esimerkki.fi", clientId: "esimerkki-backend", hmacKeyHex: process.env.TUNNUSPALVELU_HMAC_KEY!,};
const { sub } = await upsertUser(config, { email: "kayttaja@esimerkki.fi", name: "Matti Meikäläinen",});console.log(sub); // pysyvä UUID — tallenna omaan tietokantaasiCloudflare Workersissa sama toimii sellaisenaan — sekä crypto.subtle että
fetch ovat globaaleja myös Workers-ympäristössä:
export default { async fetch(request: Request, env: Env) { const config = { baseUrl: "https://id.esimerkki.fi", clientId: "esimerkki-backend", hmacKeyHex: env.TUNNUSPALVELU_HMAC_KEY, }; const { sub } = await upsertUser(config, { email: "kayttaja@esimerkki.fi" }); return Response.json({ sub }); },};Täysi allekirjoitusprotokollan ja kaikkien reittien kuvaus: Machine API.
3. Raaka curl — authorization code + PKCE
Osio nimeltä “3. Raaka curl — authorization code + PKCE”Näyttää tarkalleen, mitä OIDC-kirjasto tekee konepellin alla, ilman riippuvuuksia.
ISSUER="https://id.esimerkki.fi"CLIENT_ID="esimerkki-web"REDIRECT_URI="http://localhost:8765/callback"SCOPE="openid profile email"
# 1. DiscoveryDISCOVERY=$(curl -sf "${ISSUER}/.well-known/openid-configuration")AUTH_ENDPOINT=$(echo "$DISCOVERY" | python3 -c "import sys,json; print(json.load(sys.stdin)['authorization_endpoint'])")TOKEN_ENDPOINT=$(echo "$DISCOVERY" | python3 -c "import sys,json; print(json.load(sys.stdin)['token_endpoint'])")
# 2. PKCE: satunnainen code_verifier + S256-haasteCODE_VERIFIER=$(python3 -c "import base64, osprint(base64.urlsafe_b64encode(os.urandom(32)).rstrip(b'=').decode())")CODE_CHALLENGE=$(python3 -c "import base64, hashlibv = '$CODE_VERIFIER'.encode()print(base64.urlsafe_b64encode(hashlib.sha256(v).digest()).rstrip(b'=').decode())")STATE=$(python3 -c "import base64, os; print(base64.urlsafe_b64encode(os.urandom(16)).decode())")
# 3. Rakenna /authorize-URL ja avaa se selaimessaAUTH_URL="${AUTH_ENDPOINT}?response_type=code&client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URI}&scope=${SCOPE// /%20}&state=${STATE}&code_challenge=${CODE_CHALLENGE}&code_challenge_method=S256"echo "Avaa selaimessa: $AUTH_URL"
# 4. Kirjaudu sisään selaimessa, kaappaa `code`-parametri redirect_uri:staread -p "Liitä code-parametri callbackista: " CODE
# 5. Vaihda koodi tokeneihinTOKEN_RESP=$(curl -sf -X POST "$TOKEN_ENDPOINT" \ -d "grant_type=authorization_code" \ -d "client_id=${CLIENT_ID}" \ -d "code=${CODE}" \ -d "redirect_uri=${REDIRECT_URI}" \ -d "code_verifier=${CODE_VERIFIER}")
ACCESS_TOKEN=$(echo "$TOKEN_RESP" | python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])")
# 6. Dekoodaa access token (header.payload)echo "$ACCESS_TOKEN" | python3 -c "import sys, base64, jsonparts = sys.stdin.read().strip().split('.')def decode(s): s += '=' * (4 - len(s) % 4) return json.loads(base64.urlsafe_b64decode(s))print('Header:', json.dumps(decode(parts[0]), indent=2))print('Payload:', json.dumps(decode(parts[1]), indent=2))"
# 7. Kutsu /userinfocurl -sf -H "Authorization: Bearer $ACCESS_TOKEN" "${ISSUER}/userinfo" | python3 -m json.toolJulkinen client (ei client_secret-arvoa) todennetaan pelkällä PKCE:llä —
client_id + code_verifier riittävät. Confidential clientille lisää
-u "${CLIENT_ID}:${CLIENT_SECRET}" vaiheeseen 5.
Katso myös
Osio nimeltä “Katso myös”- Web-sovelluksen kirjautuminen — koko OIDC-flow selitettynä
- Machine API — täysi HMAC-allekirjoitusprotokolla
- Tokenit ja claimit — mitä dekoodattu access token sisältää