Siirry sisältöön

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ä.

.env.local
AUTH_SECRET=<openssl rand -base64 32>
AUTH_TUNNUSPALVELU_ISSUER=https://id.esimerkki.fi
AUTH_CLIENT_ID=esimerkki-web
AUTH_CLIENT_SECRET=<client-luonnin tuloste>
auth.ts
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;
},
},
});
app/api/auth/[...nextauth]/route.ts
import { handlers } from "@/auth";
export const { GET, POST } = handlers;
// Palvelinkomponentti — sessiotarkistus
import { 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).

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.

tunnuspalvelu-hmac.ts
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 tietokantaasi

Cloudflare 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.

Terminal window
ISSUER="https://id.esimerkki.fi"
CLIENT_ID="esimerkki-web"
REDIRECT_URI="http://localhost:8765/callback"
SCOPE="openid profile email"
# 1. Discovery
DISCOVERY=$(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-haaste
CODE_VERIFIER=$(python3 -c "
import base64, os
print(base64.urlsafe_b64encode(os.urandom(32)).rstrip(b'=').decode())
")
CODE_CHALLENGE=$(python3 -c "
import base64, hashlib
v = '$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 selaimessa
AUTH_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:sta
read -p "Liitä code-parametri callbackista: " CODE
# 5. Vaihda koodi tokeneihin
TOKEN_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, json
parts = 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 /userinfo
curl -sf -H "Authorization: Bearer $ACCESS_TOKEN" "${ISSUER}/userinfo" | python3 -m json.tool

Julkinen 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.