From Password to OAuth2: A Journey through Modern Authentication

From Password to OAuth2: A Journey through Modern Authentication
By Matthias Petermann / on 09.11.2025

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.
💡 Note
The classic login model is stateful. The server knows who you are - but only as long as it knows your session ID.

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
💡 Note
A Token is like a signed admission ticket. The auth server signs it, and each API can check for itself whether it is genuine.

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
💡 Note
OAuth answers: “Can this app do something?” OIDC adds: “Who is the user?”
📝 Notice
Don’t be confused: OAuth 1.0 was an older, more complicated predecessor and is no longer relevant today. When we talk about OAuth 2.0, we are talking about the modern standard that is used together with OpenID Connect.

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.

💡 What is PKCE?
PKCE (Proof Key for Code Exchange) protects the authorization code flow from “code intercept attacks”. The client initially generates a random code verifier and only forwards its hash (code challenge) to the auth server. When exchanging tokens, he must send the original verifier - only if both values match, the code is accepted. This means that an intercepted authorization code cannot be reused by another client.

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

💡 Note
The auth server authenticates, the API authorizes - based on a verifiable token.

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.

💡 Note
The Client Credentials Flow is used to authenticate technical services. It only provides an access token - not a user identity.

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.

💡 Note
Rotation prevents token reuse: Each refresh token is disposable and is replaced after use.

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.

💡 Note
The API first uses the Discovery endpoint to find the JWKS endpoint, and then validates tokens independently using the public keys. APIs typically cache the JWKS keys for a few minutes to save requests.

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.

✅ Note
First understand the roles and flows - after that the code is just craft.
📝 Further sources

Official Specifications and Standards: