Introduction
If you’ve been doing web development for a while, you know the pattern: Login form → username + password → session cookie – and everything works. Until the moment when you suddenly need an API, a mobile app is added or external partners need to connect. Then you quickly notice: the old model has its limits.
I’ve come into contact with OAuth on these topics several times - but mostly from the consumer perspective: “Login with GitHub”, “access via an API token”, all these things that seem so obvious in everyday life. But behind these simple clicks lies a complex web of protocols, tokens and security mechanisms.
Due to a certain dissatisfaction with the existing solutions - often too proprietary, too heavy or simply too complex for small, decentralized edge applications – the desire arose to really understand the topic. My goal: to develop my own, lean auth service that is open, understandable and robust.
This requires a deep understanding of the underlying principles – and this is exactly where this article comes in: a compact and comprehensible explanation, as I would have wished for myself.
1. Classic login: familiar but limited
With classic web login, everything runs in a closed system:
Browser --> POST /login (username, password)
Server --> Set-Cookie: session_id=abc123
Browser --> nutzt Cookie für alle weiteren Requests
This is convenient, but problematic once you outgrow the “one application”.
Where it crunches:
- Multi-device world: Users are active on the web, mobile and desktop at the same time - cookies do not work the same everywhere.
- APIs & Microservices: A session is bound to a server, not to a distributed network.
- Security: Session hijacking, CSRF and session leaks are notorious long-running issues.
- Integrations: External services should be able to authorize users, but not “log in” them directly.
2. Why modern systems rely on tokens
Instead of storing a session in the server, today all the necessary information is packed into a token that is sent with the request.
| Property | Session cookie | Tokens (e.g. JWT) |
|---|---|---|
| Storage | Server side | In the client (header, storage) |
| Condition | Stateful | Stateless |
| Validity | Until logout or timeout | Limited time (exp) |
| Review | Only possible on the auth server | Validable at any API |
| Scalability | Restricted | High (no central session) |
Client --> Authorization: Bearer <token>
API --> prüft Signatur + Ablaufzeit
3. From self-build to standards: OAuth 2.0 & OpenID Connect
Previously, each application built its own login logic. Today there are established standards:
- OAuth 2.0 → regulates authorization (Is app X allowed to access resource Y?)
- OpenID Connect (OIDC) → supplements the Authentication (Who is the user?)
Central roles:
| role | Description |
|---|---|
| Resource Owner | The user whose data is protected |
| Client | The application that acts on behalf of the user |
| Authorization Server | Performs login and issues tokens |
| Resource Server (API) | Checks tokens and provides data |
4. Authorization Code Flow with PKCE – step by step
The Authorization Code Flow is the standard for browser and mobile apps. It consists of three clear phases: Login, Token Exchange and API Access.
🧩 Phase 1: Login & code generation
Teilnehmer: Nutzer | Client | Authorization Server
Nutzer Client Auth-Server
| | |
| 1) App öffnen | |
|--------------->| |
| | 2) GET /authorize?... |
| |-------------------------->|
| | 3) Login-UI anzeigen |
| |<--------------------------|
| 4) Login-Daten eingeben |
|--------------->| |
| | 5) Weiterleiten an Auth |
| |-------------------------->|
| | | 6) Prüfe Nutzer & Consent
| |<--------------------------| 7) Redirect (callback)
| | | mit code=abc123&state=xyz
The user is redirected from the Client to the Authorization Server to log in.
After successful login, the server checks identity and consent and generates a one-time Authorization Code.
This is returned to the client via Redirect (Callback) and serves as temporary proof of successful login.
The redirect takes place to the registered redirect_uri of the client.
This code allows the client to securely request tokens in the next phase - without sharing passwords or sessions.
🧩 Phase 2: Token exchange with PKCE
Teilnehmer: Client | Authorization Server
Client Auth-Server
| |
| 1) POST /token |
| grant_type=authorization_code
| code=abc123 |
| code_verifier=<PKCE> |
|---------------------------->|
| | 2) Validiere Code & PKCE
| | 3) Signiere Tokens (Access, ID, Refresh)
|<----------------------------| 4) Token Response
| |
The client exchanges the authorization code received for real tokens. To do this, it sends a request to the token endpoint of the authorization server, including the previously generated PKCE verifier. The server checks whether the code and verifier match, signs the new tokens and returns them. The result is an Access Token (for API calls), an ID Token (identity of the user) and optionally a Refresh Token (for later renewal).
🧩 Phase 3: Accessing the API
Teilnehmer: Client | Resource Server (API)
Client API
| |
| 1) GET /resource |
| Authorization: Bearer <access_token>
|--------------------------->|
| | 2) Verifiziere Token:
| | - Signatur gültig?
| | - Claims: exp, iss, aud, scope
| | 3) Zugriff erlauben oder verweigern
|<---------------------------| 4) 200 OK (Daten) / 401–403 Fehler
| |
The Client now uses the received Access Token to request a protected API resource. The Resource Server (i.e. the API) checks the token independently: Signature, expiry time and claims must be correct. If everything is valid, the requested data is returned - otherwise a 401/403 response follows.
5. Machine-to-Machine: Client Credentials Flow
Teilnehmer: Service A (Client) | Authorization Server | Service B (API)
Service A Auth-Server Service B
| | |
| 1) POST /token | |
| grant_type= | |
| client_credentials | |
| client_id=... | |
| client_secret=... | |
|--------------------------->| |
| | 2) Prüfe Client-ID |
| | & Secret |
| | Signiere Access Token |
|<---------------------------| access_token |
| |
| 3) GET /resource |
| Authorization: Bearer <access_token> |
|------------------------------------------------------->|
| | 4) Verifiziere
| | Token
| | (JWKS, Claims)
|<-------------------------------------------------------| 200 OK (Daten)
| |
There is no user involved in this flow – a technical client (e.g. a service or cron job) authenticates itself directly to the Authorization Server with its client_id and client_secret. He receives an Access Token with which he can access another API. In this way, service-to-service communication and internal API calls are securely regulated - without any user context.
6. Refresh Tokens & Rotation
Teilnehmer: Client | Authorization Server
Client Auth-Server
| |
| 1) POST /token |
| grant_type=refresh_token |
| refresh_token=rt-xyz |
|---------------------------->|
| | 2) Prüfe Refresh Token:
| | - gültig?
| | - noch nicht verwendet?
| | 3) Rotiere Token:
| | - alten ungültig machen
| | - neuen Access & Refresh Token ausstellen
|<----------------------------| 4) Neue Tokens
| |
When the Access Token expires, the client uses its Refresh Token to obtain new tokens - without the user having to log in again. The Authorization Server checks the validity of the refresh token, issues new tokens and immediately invalidates the old refresh token. This procedure is called Token Rotation and prevents an intercepted token from being used multiple times.
7. Token checking and JWKS (API view)
Teilnehmer: API | OIDC Discovery | JWKS Endpoint
API Discovery JWKS
| | |
| 1) GET /.well-known/ | |
| openid-configuration | |
|-------------------------->| |
| | 2) Antwort mit |
| | jwks_uri |
|<--------------------------| |
| 3) GET /jwks.json (vom jwks_uri) |
|-------------------------------------------------->|
| 4) Antwort mit Keys [{ kid, ... }] |
|<--------------------------------------------------|
|
| Bei Request mit Bearer-Token:
| - Header.kid → passenden Schlüssel aus JWKS wählen
| - Signatur prüfen (z. B. Ed25519/RSA)
| - Claims prüfen: exp, nbf, iss, aud, scope
|
|→ 200 OK (erlaubt) oder 401/403 (verweigert)
The Discovery query provides the API with all important metadata from the Authorization Server - underneath the URL of the JWKS endpoint where the public keys are stored. The API can later use these keys to check each JWT token, without reassuring yourself directly from the authorization server.
8. Summary: Building blocks of modern authentication
| Principle | Meaning |
|---|---|
| Statelessness | No sessions, only verifiable tokens |
| Short-lived | Tokens expire quickly – refresh instead of permanent login |
| Standardization | OAuth 2.0 / OIDC instead of building your own |
| PKCE | Protection for public clients |
| JWKS | Public keys for signature verification |
| Rotation | Renew tokens and keys regularly |
Conclusion
Modern Authentication is not a feature, it is architecture. Anyone developing APIs today should understand that security and trust no longer depend on the server, but stuck in the token – verifiable, standardized, independent of the system context.
OAuth 2.0 and OpenID Connect provide the framework for this: Secure, interoperable, transparent.
Official Specifications and Standards:
-
RFC 6749 – The OAuth 2.0 Authorization Framework Basic standard for OAuth 2.0 flows (Authorization Code, Client Credentials, Refresh Token).
-
RFC 7636 – Proof Key for Code Exchange (PKCE) Extension to protect public clients from code interception attacks.
-
OpenID Connect Core 1.0 Adds authentication to OAuth 2.0 – defines ID Token, UserInfo and Discovery.
-
RFC 8414 – OAuth 2.0 Authorization Server Metadata (OIDC Discovery) Describes the
.well-known/openid-configurationendpoint and metadata structures. -
RFC 7519 – JSON Web Token (JWT) Construction, claims and signature validation of JSON-based tokens.
-
RFC 7517 – JSON Web Key (JWK) Defines the JWKS format (
/jwks.json) for providing public signing keys. -
OAuth 2.1 Draft (IETF, ongoing) Summarizes best security practices (mandatory PKCE, rotation, short token expiry).