TL;DR: OAuth 2.0 handles authorization (who can do what), OpenID Connect adds authentication (who are you) on top. Together they form the backbone of modern login systems like "Sign in with Google". This article breaks down how it all fits together, which grant types exist, what JWTs are, and how to implement it all in TypeScript/NestJS.

🤯 Why Authentication Is Hard

Be honest: have you ever thought about building authentication yourself? Hashing passwords, managing sessions, generating tokens, implementing refresh logic, brute-force protection, rate limiting, password reset flows... The list never ends.

The problem isn't that it's technically impossible. The problem is that one single mistake opens the door for attackers. And that's exactly why standards like OAuth 2.0 and OpenID Connect exist. They handle the hardest parts so you can focus on your actual app.

🔑 OAuth 2.0: Authorization, Not Authentication!

This is the most common misconception: OAuth 2.0 is not an authentication protocol. It's an authorization framework. The difference?

  • Authentication = "Who are you?" (verifying identity)
  • Authorization = "What can you do?" (verifying permissions)

OAuth 2.0 solves this problem: How can an app access a user's resources without the user sharing their password? Imagine a calendar app wants to access your Google contacts. Instead of entering your Google password, you authorize the app through a controlled flow. Pretty neat, right?

🏗️ The Four Roles in OAuth 2.0

OAuth 2.0 defines four core roles:

RoleDescriptionExample
Resource OwnerThe user who owns the dataYou
ClientThe app that wants to access dataYour calendar app
Authorization ServerIssues tokens after successful authorizationGoogle OAuth Server
Resource ServerHolds the protected resourcesGoogle Contacts API

These four roles interact in a defined flow called a Grant Type.

🔄 Grant Types: The Right Flow for the Right Use Case

Authorization Code Grant (+ PKCE) — The Gold Standard

This is the grant type you should use in 90% of cases. Here's how it works:

  1. Your app redirects the user to the Authorization Server
  2. The user logs in and gives consent
  3. The Authorization Server redirects back with an Authorization Code
  4. Your app exchanges the code in the backend for an Access Token

PKCE (Proof Key for Code Exchange, pronounced "Pixie") adds an extra security layer. Your app generates a random code_verifier and sends its hash (code_challenge) with the initial request. When exchanging the token, the original code_verifier must be sent along. This prevents an attacker from intercepting and using the Authorization Code.

// PKCE: Generating code_verifier and code_challenge
import * as crypto from 'crypto';

function generateCodeVerifier(): string {
  return crypto.randomBytes(32).toString('base64url');
}

function generateCodeChallenge(verifier: string): string {
  return crypto
    .createHash('sha256')
    .update(verifier)
    .digest('base64url');
}

const codeVerifier = generateCodeVerifier();
const codeChallenge = generateCodeChallenge(codeVerifier);

// In the Authorization Request:
// ?response_type=code&code_challenge={codeChallenge}&code_challenge_method=S256

Client Credentials Grant — Machine-to-Machine

No user involved. Your backend service authenticates directly with the Authorization Server using its Client ID and Client Secret. Perfect for microservice communication.

# Client Credentials Grant
curl -X POST https://auth.example.com/oauth/token   -d "grant_type=client_credentials"   -d "client_id=YOUR_CLIENT_ID"   -d "client_secret=YOUR_CLIENT_SECRET"   -d "scope=read:users"

Device Code Grant — For Devices Without a Browser

Smart TVs, CLI tools, IoT devices — anywhere you don't have a browser or can't type easily. The device displays a code, you open a URL on your phone, enter the code, and authorize. You've probably seen this with Netflix or GitHub CLI.

⚠️ Deprecated: Implicit Grant & Password Grant

The Implicit Grant returned the token directly in the URL. Problem: URLs end up in browser history, logs, and referer headers. A security nightmare.

The Resource Owner Password Credentials Grant passed the username and password directly to the app. This defeats the entire purpose of OAuth. Both are marked as deprecated in current best practices.

OAuth 2.0
OAuth 2.0 is the industry-standard protocol for authorization.

🪪 OpenID Connect: Authentication on Top

Okay, OAuth 2.0 tells us what someone is allowed to do. But who is that someone? That's exactly where OpenID Connect (OIDC) comes in. It's a thin layer on top of OAuth 2.0 that adds authentication.

The crucial difference: OIDC introduces the ID Token. While an Access Token says "this token has access to X", an ID Token says "this token belongs to user Y with email Z".

How OpenID Connect Works
OpenID Connect explained for developers.

🎫 ID Token vs Access Token vs Refresh Token

Three tokens, three jobs:

TokenPurposeRecipientLifetime
ID TokenUser identity (authentication)Your app (Client)Short (minutes)
Access TokenAccess to protected resources (authorization)Resource Server (API)Short (minutes to hours)
Refresh TokenGet a new Access Token without re-loginAuthorization ServerLong (days to weeks)

Important: Never send an ID Token to an API! The ID Token is for your app to identify the user. For API access, use the Access Token.

🧬 JWT: Anatomy of a Token

Most ID Tokens (and many Access Tokens) are JWTs (JSON Web Tokens). A JWT consists of three parts, separated by dots:

eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.signature
# Header               . Payload                     . Signature

Each part is Base64url-encoded:

// Header — Algorithm and token type
{
  "alg": "RS256",
  "typ": "JWT",
  "kid": "my-key-id"    // Key ID for signature verification
}

// Payload — The claims
{
  "iss": "https://auth.example.com",  // Issuer
  "sub": "user-123",                   // Subject (user ID)
  "aud": "my-app",                     // Audience (intended for)
  "exp": 1700000000,                   // Expiration
  "iat": 1699999000,                   // Issued At
  "email": "[email protected]",         // OIDC Claim
  "name": "Jane Doe"                   // OIDC Claim
}

// Signature — HMAC or RSA signature over Header + Payload
// Created with the Authorization Server's private key
// Can be verified with the public key

The signature ensures nobody has tampered with the content. Your app can fetch the Authorization Server's public key (via JWKS endpoint) and verify the signature without needing to contact the Authorization Server.

🚀 Practical Example: "Login with Google/GitHub"

Here's how a typical "Login with Google" flow works:

  1. User clicks "Sign in with Google" — Your app redirects to Google's Authorization Endpoint
  2. Google shows login page — User enters credentials
  3. Consent screen — "App XY wants to access your profile and email"
  4. Redirect back — With an Authorization Code in the URL
  5. Token exchange — Your backend exchanges the code for ID Token + Access Token
  6. Parse ID Token — Identify user, create session

🎯 Scopes and Claims

Scopes define which permissions are being requested. OIDC defines standard scopes:

  • openid — Required for OIDC, returns the ID Token
  • profile — Name, picture, etc.
  • email — Email address
  • offline_access — Request a Refresh Token

Claims are the individual data points in the token, unlocked by scopes. The email scope unlocks the email and email_verified claims, for example.

Understanding and Using REST APIs
Everything you need to know about REST APIs.

🛠️ Implementation with NestJS

Let's get practical. Here's how you set up JWT-based authentication with NestJS:

# Install dependencies
npm install @nestjs/jwt @nestjs/passport passport passport-jwt
npm install -D @types/passport-jwt
// auth/jwt.strategy.ts
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { passportJwtSecret } from 'jwks-rsa';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      // JWKS endpoint of your OIDC provider
      secretOrKeyProvider: passportJwtSecret({
        cache: true,
        rateLimit: true,
        jwksRequestsPerMinute: 5,
        jwksUri: 'https://auth.example.com/.well-known/jwks.json',
      }),
      issuer: 'https://auth.example.com/',
      audience: 'my-api',
      algorithms: ['RS256'],
    });
  }

  async validate(payload: any) {
    return {
      userId: payload.sub,
      email: payload.email,
      roles: payload.roles || [],
    };
  }
}
// auth/auth.module.ts
import { Module } from '@nestjs/common';
import { PassportModule } from '@nestjs/passport';
import { JwtStrategy } from './jwt.strategy';

@Module({
  imports: [PassportModule.register({ defaultStrategy: 'jwt' })],
  providers: [JwtStrategy],
  exports: [PassportModule],
})
export class AuthModule {}
// Protected route
import { Controller, Get, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Controller('profile')
export class ProfileController {
  @Get()
  @UseGuards(AuthGuard('jwt'))
  getProfile(@Req() req) {
    // req.user contains the validated token payload
    return {
      userId: req.user.userId,
      email: req.user.email,
    };
  }
}

⚠️ Common Mistakes and Security Pitfalls

Here are the most common stumbling blocks I keep seeing:

1. Storing Tokens in localStorage

Problem: Any XSS script can read localStorage. Store Access Tokens in httpOnly cookies instead, or keep them in memory only.

2. Not Validating the Redirect URI

Problem: Without strict redirect URI validation, an attacker can redirect the Authorization Code to their own URL. Register exact redirect URIs, no wildcards.

3. Forgetting CSRF Protection

Problem: Without a state parameter, an attacker can perform a CSRF attack. Always generate a random state value and verify it on callback.

4. Not Verifying Token Signatures

Problem: If you only decode the token but don't verify the signature, anyone can create arbitrary tokens. Always verify the signature against the JWKS endpoint.

5. Token Lifetime Too Long

Problem: Access Tokens should be short-lived (5-15 minutes). Use Refresh Tokens for longer sessions.

CORS: Cross-Origin Resource Sharing Explained
Everything about CORS and why it matters for your API security.
DKIM, DMARC and SPF: The Protective Shield for Your Email Communication
How to secure your email infrastructure with DKIM, DMARC, and SPF.

🏠 Self-Hosting: Auth0, Keycloak & Authentik

You don't have to use a cloud provider. There are excellent self-hosting options:

  • Keycloak — The classic. Java-based, extremely flexible, huge community. Supports SAML, OIDC, LDAP, and more. Can be a resource hog though.
  • Authentik — Modern, Python-based, slick UI. Lighter than Keycloak and easier to configure. My personal favorite for new projects.
  • Auth0 — Technically a cloud service, but with a generous free tier. If you don't want to deal with self-hosting, Auth0 is a solid choice.
# Start Authentik with Docker Compose
# docker-compose.yml
version: "3.4"
services:
  authentik-server:
    image: ghcr.io/goauthentik/server:latest
    command: server
    environment:
      AUTHENTIK_SECRET_KEY: "generate-a-long-random-string"
      AUTHENTIK_REDIS__HOST: redis
      AUTHENTIK_POSTGRESQL__HOST: postgresql
      AUTHENTIK_POSTGRESQL__USER: authentik
      AUTHENTIK_POSTGRESQL__PASSWORD: authentik
      AUTHENTIK_POSTGRESQL__NAME: authentik
    ports:
      - "9000:9000"
      - "9443:9443"

💡 Conclusion

OAuth 2.0 and OpenID Connect aren't rocket science, but they have a lot of moving parts. The key takeaways:

  • OAuth 2.0 = Authorization, OpenID Connect = Authentication on top
  • Use Authorization Code + PKCE as your default flow
  • Forget Implicit and Password Grant — they're deprecated
  • Understand the difference between ID Token, Access Token, and Refresh Token
  • Always verify token signatures
  • Never store tokens in localStorage
  • Use existing solutions like Keycloak or Authentik instead of building your own

Building authentication yourself is like writing your own crypto protocol: technically doable, but almost always a bad idea. Stand on the shoulders of giants.

Artikel teilen:Share article: