Teknologien bag platformen

Én side, hele historien: hvorfor central login, hvordan OAuth 2.0 + PKCE virker, hvad OIDC lægger ovenpå, og hvordan tokens lever og dør. Skrevet til undervisning — følg diagrammerne oppefra og ned.

Indhold 1. Hvorfor? 2. OAuth 2.0 + PKCE 3. OIDC 4. Scopes & klienter 5. Token-livscyklus 6. JWT & JWKS 7. Session vs. tokens 8. MFA & passkeys 9. Angreb & forsvar 10. Ordliste 11. Quiz

1. Hvorfor central auth?

Uden en central auth-server skal hver app selv opbevare passwords, selv bygge “glemt adgangskode”, selv håndtere Google/Microsoft-login — og brugeren får én konto pr. app. Med en central server logger brugeren ind ét sted, og alle apps får det samme stabile bruger-ID.

Uden central auth
  • Password-database i hver app (flere angrebsflader)
  • Forskelligt bruger-ID pr. app — svært at koble data
  • Login-kode duplikeres og rådner
Med Mercantec Auth
  • Én identitet: samme sub (bruger-GUID) i alle apps
  • Apps ser aldrig passwords — kun signerede tokens
  • MFA, passkeys og account-linking løses ét sted
Kernen: Din app skal aldrig autentificere brugeren selv — den skal bare kunne bevise at auth-serveren har gjort det. Beviset er en signeret JWT.

2. OAuth 2.0: authorization code + PKCE

OAuth 2.0 er en protokol for delegeret adgang: appen får et token på brugerens vegne uden nogensinde at se brugerens password. Denne platform bruger kun det sikreste browser-flow: authorization code + PKCE (S256). Fire parter er i spil:

sequenceDiagram autonumber actor U as Bruger (browser) participant SPA as Din app (SPA) participant AS as Mercantec Auth participant API as Dit API SPA->>SPA: Generér code_verifier + state SPA->>SPA: code_challenge = SHA256(code_verifier) SPA->>AS: GET /oauth/authorize (client_id, redirect_uri, state, code_challenge, S256) AS->>U: Login-side (password, passkey, Google, Microsoft …) U->>AS: Logger ind (evt. + MFA) AS->>SPA: Redirect til redirect_uri med code + state SPA->>SPA: Tjek at state matcher SPA->>AS: POST /oauth/token (code, code_verifier, client_id) AS->>AS: SHA256(code_verifier) == code_challenge? AS->>SPA: access_token (JWT) + refresh_token SPA->>API: GET /data med Authorization Bearer access_token API->>API: Validér JWT mod JWKS (iss, aud, exp, signatur) API->>SPA: 200 OK — data

Hvorfor PKCE i stedet for client_secret?

En browser-app kan ikke holde på en hemmelighed — alt i JavaScript kan læses af brugeren (og angribere). PKCE (Proof Key for Code Exchange) erstatter den faste hemmelighed med en engangs-hemmelighed pr. login:

state-parameteren løser et andet problem: CSRF. Appen genererer en tilfældig værdi før login og afviser callbacks hvor den ikke matcher — så en angriber ikke kan “plante” sin egen kode i dit callback. Prøv angrebet selv i afsnit 9.
Prøv selv

PKCE-legeplads: verifier → challenge (envejs)

Skriv i verifier-feltet og se challengen ændre sig med det samme — det er ægte SHA-256 i din browser (samme kode som login-knappen på forsiden bruger). Prøv at ændre ét enkelt tegn.

—

Bemærk: challengen ligner volapyk uanset input, og en lille ændring i verifieren giver en helt anden challenge. Der findes ingen vej tilbage fra challenge til verifier — det er hele pointen.

Prøv selv

Byg dit eget authorize-kald

Sådan ser den URL ud som din app sender brugeren til. Redigér felterne og se URL'en opdatere sig — challengen kommer live fra legepladsen ovenfor.

—

Farverne viser de enkelte parametre. response_type=code og code_challenge_method=S256 er faste for denne server.

3. OIDC — identitet ovenpå OAuth

OAuth 2.0 handler om autorisation (“må denne app kalde API'et?”). Det siger strengt taget intet om hvem brugeren er. OpenID Connect (OIDC) er et tyndt identitetslag ovenpå OAuth der standardiserer netop det:

flowchart TB subgraph oidcLayer ["OIDC — identitetslag"] IDT["id_token: signeret JWT om HVEM brugeren er"] DISC["Discovery: /.well-known/openid-configuration"] AMR["amr-claim: HVORDAN blev der logget ind (pwd, otp, webauthn)"] end subgraph oauthLayer ["OAuth 2.0 — autorisationslag"] AZ["/oauth/authorize med PKCE"] TK["/oauth/token: access_token + refresh_token"] BR["Authorization: Bearer mod API'er"] end IDT -.bygger på.-> AZ DISC -.beskriver.-> TK
Huskeregel: OAuth = nøglekort til API'et. OIDC = ID-kort med billede. Samme skab (auth-serveren) udsteder begge dele i samme flow.

4. Scopes og klient-typer

To begreber der ofte springes over, men som afgør hvad et token kan, og hvordan en app skal opføre sig:

Scopes — hvad beder appen om lov til?

scope i authorize-kaldet er en mellemrums-separeret liste af “tilladelser” appen ønsker. Auth-serveren afgør hvad den vil udstede — appen kan aldrig give sig selv mere adgang ved bare at bede pænt:

Scope ≠ rolle. Scope er hvad appen må på brugerens vegne (aftalt ved login). Rolle (role-claim) er hvad brugeren er i systemet (fx Admin). Et API tjekker typisk begge dele.

Offentlige vs. fortrolige klienter

Offentlig klient (public)Fortrolig klient (confidential)
HvemSPA i browseren, mobil-app, desktop-appBackend/server-side webapp
Kan holde på en hemmelighed?Nej — al kode kan læses af brugerenJa — koden kører på en server du kontrollerer
Bevis ved token-kaldPKCE code_verifier (engangs)client_secret (fast, BCrypt-hashet i databasen) — gerne + PKCE
I denne platformIsPublic=true, RequirePkce=trueclient_secret sendes i token-kaldets form body

Demo-SPA'en her er en offentlig klient (client_id=demo). Registrering af nye klienter sker i databasen (ClientApps + redirect-URI-whitelist) — se AI-prompt-siden for hele checklisten.

5. Token-flow og livscyklus

Efter login har din app to (evt. tre) tokens med vidt forskellige roller og levetider:

Access token JWT · RS256
  • Kort levetid (minutter)
  • Sendes som Authorization: Bearer på hvert API-kald
  • Valideres statisk mod JWKS — intet database-opslag
Refresh token Opakt · roterende
  • Lang levetid (dage)
  • Bruges KUN mod /oauth/token for at få nyt access token
  • Roterer: hvert refresh-svar giver et nyt — gem det, glem det gamle
id_token (ved openid) JWT · OIDC
  • Identitetsbevis til din app — ikke til API'er
  • Indeholder amr (login-metode/MFA)
  • Læses én gang efter login
stateDiagram-v2 [*] --> LoggetUd LoggetUd --> Login : Bruger klikker Log ind Login --> AktivToken : /oauth/token udsteder access + refresh AktivToken --> AktivToken : API-kald med Bearer JWT AktivToken --> Udloebet : exp naas (minutter) Udloebet --> AktivToken : POST /oauth/token grant_type=refresh_token (nyt roterende refresh) Udloebet --> LoggetUd : Refresh token udloebet eller afvist AktivToken --> LoggetUd : Logout rydder tokens + GET /signout
Hvorfor to tokens? Korte access tokens begrænser skaden hvis et token lækkes — det dør af sig selv efter få minutter. Det langlivede refresh token rammer aldrig dine API'er og kan tilbagekaldes centralt (fx ved merge af brugere eller mistanke om misbrug tvinges genlogin).
Live

Dit token lige nu — og prøv et rigtigt refresh

Tjekker session …

POST /oauth/token · grant_type=refresh_token

Knappen laver et ægte kald: dit access token udskiftes, og refresh tokenet roterer — se state-diagrammet ovenfor ske i virkeligheden.

6. JWT, RS256 og JWKS — kort version

Access token er en JWT: tre Base64URL-dele (header · payload · signatur). Indholdet kan alle læse — men signaturen er lavet med auth-serverens private RSA-nøgle, og alle kan verificere den med den offentlige nøgle fra /.well-known/jwks.json.

Hele den historie — med live dekodning og verifikation af dit eget token — får du på JWT-siden.

Prøv selv

Base64URL er kodning — ikke kryptering

Skriv hvad som helst og se det “forvandlet” til Base64URL — og straks dekodet igen. Alle kan gøre det samme med en JWT-payload. Der er ingen hemmelighed involveret.

—
—
Live

Hackerværksted: manipulér din egen payload

Payload kan læses og ændres af alle — men signaturen passer kun på den originale. Redigér claims herunder (giv dig fx selv "role": "Admin") og se hvad JWKS-verifikationen siger. E-mail vises maskeret.

7. Session-cookie vs. tokens

To slags “logget ind” lever side om side, og det forvirrer ofte:

flowchart LR subgraph authHost ["Auth-hostens domæne"] COOKIE["Session-cookie (mercantec_auth)"] end subgraph spaHost ["Din apps domæne"] TOKENS["Tokens i sessionStorage"] end COOKIE -->|"Naeste /oauth/authorize: login-UI springes over"| TOKENS TOKENS -->|"Logout: ryd storage OG kald GET /signout"| COOKIE

8. MFA og passkeys

Brugere kan (valgfrit) beskytte kontoen med TOTP (authenticator-app) eller passkeys (WebAuthn — Face ID, Windows Hello, hardware-nøgler). Auth-serveren håndterer det hele; din app ser kun resultatet i amr-claimen.

flowchart TD START["Primær login"] --> METODE{"Hvilken metode?"} METODE -->|"Passkey (WebAuthn)"| FULD["Fuld session — MFA springes over (passkey ER stærk faktor)"] METODE -->|"Password eller OAuth-udbyder"| HARMFA{"Har brugeren TOTP eller passkey?"} HARMFA -->|Nej| FULD HARMFA -->|Ja| PENDING["mfa_pending-session (ca. 10 min)"] PENDING --> STEP["/Account/Mfa: TOTP-kode eller passkey-bekræftelse"] STEP --> FULD FULD --> OAUTH["/oauth/authorize udsteder code — kun ved FULD session"]
Pointen for integratorer: Du skal ikke bygge MFA selv. En mfa_pending-session kan ikke gennemføre OAuth-flowet, så når din app modtager en kode, er brugeren garanteret fuldt verificeret. Vil du vide hvor stærkt der blev logget ind, så bed om openid-scope og læs amr.

9. Angreb og forsvar — prøv dem selv

Hver regel i OAuth findes fordi nogen har udnyttet at den manglede. Her er de tre klassikere — og hvordan protokollen lukker dem.

Simulator

CSRF på callbacken — derfor findes state

Før login gemmer appen en tilfældig state. Callbacks uden det rigtige stempel afvises — også selvom koden i dem er helt ægte (bare udstedt til angriberens session).

Gemt i “sessionStorage”: …

Tryk på en af knapperne.

Prøv selv

redirect_uri: hvorfor eksakt match?

Authorization-koden leveres via redirect — så den der kontrollerer redirect-målet, får koden. Whitelist for denne demo-klient: …

—

Typiske fejl
  • Refresh tokens i localStorage — overlever lukket browser og kan læses ved XSS
  • JWT “valideret” ved kun at dekode payloaden (uden signatur/iss/aud/exp)
  • Tokens i URL'er eller logget i klartekst
  • Prefix-match på redirect_uri (“starter med vores domæne …”)
Forsvar i denne platform
  • PKCE S256 påkrævet for offentlige klienter — stjålne koder er værdiløse
  • Eksakt redirect-URI-whitelist + CORS-whitelist (Cors:SpaOrigins)
  • Korte access tokens + roterende refresh tokens (tilbagekaldes centralt)
  • RS256/JWKS: API'er kan verificere uden delte hemmeligheder

10. Ordliste

BegrebBetydning
issuer (iss)Hvem udstedte tokenet — skal matche en forventet streng præcist.
audience (aud)Hvem tokenet er beregnet til. Dit API afviser tokens med forkert audience.
claimÉt felt i JWT-payloaden (fx sub, email, role).
subSubject: brugerens stabile GUID. Brug den — aldrig e-mail — som nøgle i din database.
scopeHvad appen beder om lov til. openid aktiverer OIDC/id_token.
grantMåden man får tokens på. Her: authorization_code og refresh_token.
bearer“Ihændehaver”: den der har tokenet, har adgangen. Derfor korte levetider + HTTPS.
PKCEProof Key for Code Exchange — engangs-hemmelighed der sikrer code-flowet uden client_secret.
JWKSJSON Web Key Set: serverens offentlige nøgler til at verificere JWT-signaturer.
kidKey ID i JWT-headeren — peger på den rigtige nøgle i JWKS ved nøglerotation.
amrAuthentication Methods References: pwd, otp, webauthn — hvordan brugeren loggede ind.
redirect_uriDin callback-URL. Whitelistes på serveren med eksakt match — ellers afvises flowet.
public clientApp der ikke kan holde på en hemmelighed (SPA, mobil). Beviser sig med PKCE i stedet for client_secret.
confidential clientServer-side app med client_secret. Hemmeligheden bor på serveren — aldrig i browseren.
CSRFCross-Site Request Forgery: fremmed side udløser handlinger i din session. I OAuth stoppes det af state.
XSSCross-Site Scripting: fremmed JavaScript på din side — kan læse localStorage/sessionStorage. Derfor korte tokens og forsigtig lagring.

11. Quiz — test dig selv

Seks spørgsmål, ét svar pr. spørgsmål, forklaring med det samme.

Videre herfra

Demo ved MAGS (underviser) — ikke Mercantecs officielle produkt. [email protected]