Siirry sisältöön

Web-sovelluksen kirjautuminen

tunnuspalvelu on standardinmukainen OpenID Connect 1.0 -tarjoaja. Tuettu kirjautumisvuo on authorization code -flow PKCE:llä — implisiittistä tai hybridi-flow’ta ei tueta (response_types_supported: ["code"]).

Hae tenantin metadata pyynnön alussa (tai kertakäynnistyksessä ja välimuistita se sovelluksessasi):

GET https://id.esimerkki.fi/.well-known/openid-configuration

Vastauksesta tarvitset ainakin authorization_endpoint, token_endpoint, userinfo_endpoint ja jwks_uri. Koko kenttälistan selitykset: Discovery-dokumentti.

2. Ohjaa käyttäjä /authorize-osoitteeseen

Osio nimeltä “2. Ohjaa käyttäjä /authorize-osoitteeseen”
Parametri Pakollinen Selitys
response_type kyllä Aina code
client_id kyllä Clientin tunniste
redirect_uri kyllä Täsmää merkilleen clientille rekisteröityyn URIin
scope kyllä Väliviivalla eroteltu lista, esim. openid profile email. Oletus jos puuttuu: openid
state suositellaan CSRF-suojaus, palautetaan sellaisenaan callbackissa
nonce suositellaan (openid) Palautetaan ID tokenin nonce-claimissa, replay-suojaksi
code_challenge pakollinen public clienteille PKCE-haaste, ks. alla
code_challenge_method pakollinen jos code_challenge annettu Ainoa tuettu arvo on S256
prompt valinnainen none | login | consent | select_account — OIDC Core §3.1.2.1
max_age valinnainen Sekunteina: jos olemassa oleva sessio on tätä vanhempi, käyttäjä pakotetaan kirjautumaan uudelleen

PKCE: generoi satunnainen code_verifier, laske code_challenge = BASE64URL(SHA256(code_verifier)) ja lähetä code_challenge_method=S256. Public clientit (ilman client_secret-arvoa) vaativat PKCE:n aina; confidential clientit voidaan tarvittaessa pakottaa käyttämään sitä myös.

prompt=none palauttaa error=login_required, jos käyttäjä ei ole kirjautunut tai istunto ei kelpaa — käytä tätä hiljaiseen uudelleentodennukseen (esim. iframe-tarkistus). prompt=login ja prompt=select_account ohittavat olemassa olevan istunnon ja pakottavat uuden kirjautumisen.

GET https://id.esimerkki.fi/authorize
?response_type=code
&client_id=esimerkki-web
&redirect_uri=https%3A%2F%2Fapp.esimerkki.fi%2Fcallback
&scope=openid%20profile%20email
&state=<satunnainen>
&nonce=<satunnainen>
&code_challenge=<base64url(sha256(code_verifier))>
&code_challenge_method=S256

Käyttäjä kirjautuu tunnuspalvelun kautta (magic link, passkey, Google tai muu tenantille sallittu tapa — ks. Kirjautumistavat) ja ohjataan takaisin redirect_uri-osoitteeseen kyselyparametrilla code (ja state, jos lähetit sellaisen).

Virhetilanteet palautuvat joko HTML-virhesivuna (tuntematon client_id tai rekisteröimätön redirect_uri — näihin ei voi luottaa uudelleenohjauksessa) tai kyselyparametreina redirect_uri-osoitteeseen (kun se on jo validoitu). Täysi virhekoodilista: Virhekoodit.

POST https://id.esimerkki.fi/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=<authorize-vastauksen code>
&redirect_uri=https://app.esimerkki.fi/callback
&code_verifier=<PKCE-vaiheen alkuperäinen arvo>

Clientin todennus — kaksi tuettua tapaa, valitse toinen:

  • client_secret_basic: Authorization: Basic base64(client_id:client_secret)
  • client_secret_post: client_id ja client_secret lomakekenttinä samassa pyynnössä

Public clientit (ei client_secret-arvoa lainkaan) todennetaan pelkällä PKCE:llä — code_verifier riittää.

Onnistunut vastaus:

{
"access_token": "eyJhbGciOiJFUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3600,
"id_token": "eyJhbGciOiJFUzI1NiIs...",
"scope": "openid profile email"
}

Jos scope sisälsi offline_access ja clientille on sallittu refresh_token-grant, vastauksessa on myös refresh_token.

ID token on ES256-allekirjoitettu JWT. Vähintään:

  1. Hae allekirjoitusavain jwks_uri-osoitteesta (kid tokenin headerista kertoo, mikä avain).
  2. Tarkista allekirjoitus.
  3. Tarkista iss == tenantin issuer-URL.
  4. Tarkista aud == oma client_id.
  5. Tarkista exp ei ole mennyt umpeen.
  6. Jos lähetit nonce-arvon authorize-pyynnössä, tarkista että se täsmää.

Useimmat OIDC-kirjastot (esim. openid-client, Auth.js) tekevät nämä tarkistukset automaattisesti kun annat niille discovery-URL:n. Täysi claim-taulukko: Tokenit ja claimit.

GET https://id.esimerkki.fi/userinfo
Authorization: Bearer <access_token>

Palauttaa myönnetyistä scopeista riippuvat claimit JSON-muodossa (sub aina mukana). Sama endpoint hyväksyy myös DPoP-sidotun tokenin (Authorization: DPoP <access_token> + DPoP-header) — ks. Mobiili- ja natiivisovellukset.

Refresh token on opaakki (ei JWT — sitä ei voi dekoodata) ja rotatoituu joka käytöllä: jokainen /token-kutsu grant_type=refresh_token-arvolla kumoaa käytetyn tokenin ja palauttaa uuden. Jos kumottua (jo käytettyä) refresh tokenia yritetään käyttää uudelleen, koko token-perhe kumotaan — tämä havaitsee varastetun tokenin toisen käyttöyrityksen ja katkaisee kaikki kyseisen käyttäjä+client-parin sessiot.

POST https://id.esimerkki.fi/token
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token
&refresh_token=<edellinen refresh_token>

RP-Initiated Logout -endpoint:

GET https://id.esimerkki.fi/logout
?id_token_hint=<viimeisin ID token>
&post_logout_redirect_uri=https://app.esimerkki.fi
Parametri Selitys
id_token_hint Aiemmin myönnetty ID token — tunnuspalvelu lukee siitä käyttäjän ja clientin
client_id Vaihtoehto id_token_hint:lle, jos sitä ei ole saatavilla
post_logout_redirect_uri Minne käyttäjä ohjataan uloskirjautumisen jälkeen — täytyy olla rekisteröity clientille

Jos clientille on määritetty backchannel_logout_uri, tunnuspalvelu lähettää sinne myös taustakanavan logout_token-JWT:n jokaiselle sessioon liittyvälle clientille — hyödyllinen, jos ylläpidät omaa palvelinpuolen sessiovarastoa etkä voi luottaa pelkkään selaimen uloskirjautumiseen.

8. Step-up: tuore tunnistus ennen arkaluonteisia toimia

Osio nimeltä “8. Step-up: tuore tunnistus ennen arkaluonteisia toimia”

Jokaisessa ID tokenissa on kaksi claimia, joiden avulla sovellus voi vaatia vahvemman tai tuoreemman tunnistuksen vain silloin, kun toimi sitä tarvitsee:

  • auth_time: hetki, jolloin käyttäjä oikeasti tunnistautui. Säilyy, kun tunnuspalvelu käyttää olemassa olevaa istuntoa uudelleen (SSO).
  • amr: tunnistustapa RFC 8176 -arvoina. Passkey tuottaa ["hwk", "user", "mfa"], magic link ["otp"], Google ei mitään. Koko taulukko: Tokenit ja claimit.

Tallenna molemmat omaan istuntoosi kirjautumisen yhteydessä. Kun käyttäjä yrittää arkaluonteista toimea (maksuasetukset, API-avaimen luonti, oikeuksien muutos), tarkista kaksi asiaa:

  1. amr sisältää "mfa", eli viimeisin tunnistus oli passkey tai magic link ja TOTP.
  2. auth_time on riittävän tuore, esimerkiksi enintään 300 sekuntia vanha.

Jos kumpi tahansa ei täyty, ohjaa käyttäjä /authorize-osoitteeseen max_age-parametrilla. Tunnuspalvelu pakottaa uuden tunnistuksen, jos istunto on parametria vanhempi, ja palauttaa uuden ID tokenin tuoreilla auth_time- ja amr-arvoilla.

GET https://id.esimerkki.fi/authorize
?response_type=code
&client_id=esimerkki-web
&redirect_uri=https%3A%2F%2Fapp.esimerkki.fi%2Fcallback
&scope=openid
&max_age=300
&state=<satunnainen>
&nonce=<satunnainen>

Auth.js:llä sama onnistuu välittämällä parametri signIn-kutsulle:

await signIn("tunnuspalvelu", { redirectTo: "/settings/payout" }, { max_age: "300" });

Vertaa amr-arvoja standardin sanastoon, älä omiin nimiin kuten passkey tai email_code. Puuttuva amr (Google-kirjautuminen) ei täytä passkey-vaatimusta, ja näin pitääkin olla: tunnuspalvelu ei voi todentaa ulkoisen tarjoajan tunnistustapaa.

Terminal window
# 1. Discovery
curl -s https://id.esimerkki.fi/.well-known/openid-configuration | jq .
# 2. Avaa /authorize selaimessa (katso yllä oleva URL-rakenne),
# kirjaudu sisään, kaappaa `code` redirectistä
# 3. Vaihda koodi tokeneihin
curl -s -X POST https://id.esimerkki.fi/token \
-d grant_type=authorization_code \
-d code="$CODE" \
-d redirect_uri=https://app.esimerkki.fi/callback \
-d code_verifier="$CODE_VERIFIER" \
-u esimerkki-web:"$CLIENT_SECRET"
# 4. Kutsu userinfo
curl -s https://id.esimerkki.fi/userinfo \
-H "Authorization: Bearer $ACCESS_TOKEN"

Täysi ajettava versio (mukaan lukien PKCE-generointi ja callback-kaappaus): Koodiesimerkit.