Skip to content

πŸ” OAuth 2.0, OpenID Connect & JWT ReferenceΒΆ

This comprehensive guide covers OAuth 2.0, OpenID Connect (OIDC), and JSON Web Tokens (JWT) implementation using the Neuroglia framework, with practical examples from Mario's Pizzeria.

Based on official IETF specifications and OpenID Foundation standards, this reference provides production-ready patterns for implementing enterprise-grade authentication and authorization.

🎯 What is OAuth 2.0?¢

OAuth 2.0 (RFC 6749) is an authorization framework that enables applications to obtain limited access to user accounts. It works by delegating user authentication to an authorization server and allowing third-party applications to obtain limited access tokens instead of passwords.

Key OAuth 2.0 ConceptsΒΆ

  • Resource Owner: The user who owns the data (pizzeria customer/staff)
  • Client: The application requesting access (Mario's Pizzeria web app)
  • Authorization Server: Issues access tokens (Keycloak, Auth0, etc.)
  • Resource Server: Hosts protected resources (Mario's Pizzeria API)
  • Access Token: Credential used to access protected resources
  • Scope: Permissions granted to the client (orders:read, kitchen:manage)

πŸ†” What is OpenID Connect (OIDC)?ΒΆ

OpenID Connect (OpenID Connect Core 1.0) is an identity layer built on top of OAuth 2.0. While OAuth 2.0 handles authorization (what you can do), OIDC adds authentication (who you are).

OIDC Adds to OAuth 2.0ΒΆ

  • ID Token: Contains user identity information (JWT format)
  • UserInfo Endpoint: Provides additional user profile data
  • Standardized Claims: Email, name, roles, etc.
  • Discovery: Automatic configuration discovery

🏷️ What are JSON Web Tokens (JWT)?¢

JWT (RFC 7519) is a compact, URL-safe means of representing claims between two parties. In our pizzeria context, JWTs contain user identity and permissions.

JWT StructureΒΆ

Header.Payload.Signature

Example JWT for Mario's Pizzeria:

// Header
{
  "alg": "RS256",
  "typ": "JWT",
  "kid": "pizzeria-key-1"
}

// Payload
{
  "sub": "customer_12345",
  "name": "Mario Rossi",
  "email": "mario@example.com",
  "roles": ["customer"],
  "scope": "orders:read orders:write menu:read",
  "iss": "https://auth.mariospizzeria.com",
  "aud": "pizzeria-api",
  "exp": 1695734400,
  "iat": 1695648000
}

// Signature (generated by authorization server)

πŸ”„ OAuth 2.0 Authorization FlowΒΆ

Here's how a customer logs into Mario's Pizzeria:

sequenceDiagram
    participant User as πŸ‘€ Customer
    participant Client as πŸ• Pizzeria Web App
    participant AuthServer as πŸ” Keycloak (Auth Server)
    participant API as 🌐 Pizzeria API

    Note over User,API: OAuth 2.0 Authorization Code Flow

    User->>+Client: 1. Click "Login"
    Client->>+AuthServer: 2. Redirect to authorization endpoint<br/>?client_id=pizzeria&scope=orders:read+orders:write
    AuthServer->>+User: 3. Show login form
    User->>AuthServer: 4. Enter credentials
    AuthServer->>-User: 5. Redirect with authorization code<br/>?code=ABC123

    User->>+Client: 6. Return to app with code
    Client->>+AuthServer: 7. Exchange code for tokens<br/>POST /token
    AuthServer->>-Client: 8. Return access_token + id_token

    Note over Client,API: Making Authenticated API Calls

    Client->>+API: 9. GET /orders<br/>Authorization: Bearer <access_token>
    API->>API: 10. Validate JWT signature & claims
    API->>-Client: 11. Return order data

    Client->>-User: 12. Display user's orders

πŸ” JWT Validation ProcessΒΆ

When Mario's Pizzeria API receives a request, it validates the JWT:

flowchart TD
    A[🌐 API Receives Request] --> B{JWT Present?}
    B -->|No| C[❌ Return 401 Unauthorized]
    B -->|Yes| D[πŸ“ Parse JWT Header/Payload]

    D --> E{Valid Signature?}
    E -->|No| F[❌ Return 401 Invalid Token]
    E -->|Yes| G{Token Expired?}

    G -->|Yes| H[❌ Return 401 Token Expired]
    G -->|No| I{Valid Issuer?}

    I -->|No| J[❌ Return 401 Invalid Issuer]
    I -->|Yes| K{Valid Audience?}

    K -->|No| L[❌ Return 401 Invalid Audience]
    K -->|Yes| M{Required Scope?}

    M -->|No| N[❌ Return 403 Insufficient Scope]
    M -->|Yes| O[βœ… Allow Request]

    O --> P[πŸ• Process Business Logic]

    style A fill:#E3F2FD
    style O fill:#E8F5E8
    style P fill:#E8F5E8
    style C,F,H,J,L,N fill:#FFEBEE

πŸ—οΈ Keycloak Integration with Neuroglia FrameworkΒΆ

Here's how to integrate Keycloak (or any OIDC provider) with Mario's Pizzeria:

1. JWT Authentication MiddlewareΒΆ

from neuroglia.dependency_injection import ServiceCollection
from neuroglia.mvc import ControllerBase
from fastapi import HTTPException, Depends
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
import jwt
from typing import Dict, List

class JWTAuthService:
    def __init__(self,
                 issuer: str = "https://keycloak.mariospizzeria.com/auth/realms/pizzeria",
                 audience: str = "pizzeria-api",
                 jwks_url: str = "https://keycloak.mariospizzeria.com/auth/realms/pizzeria/protocol/openid_connect/certs"):
        self.issuer = issuer
        self.audience = audience
        self.jwks_url = jwks_url
        self._public_keys = {}

    async def validate_token(self, token: str) -> Dict:
        """Validate JWT token and return claims"""
        try:
            # Decode without verification first to get kid
            unverified_header = jwt.get_unverified_header(token)
            kid = unverified_header.get('kid')

            # Get public key for signature verification
            public_key = await self._get_public_key(kid)

            # Verify and decode token
            payload = jwt.decode(
                token,
                public_key,
                algorithms=['RS256'],
                issuer=self.issuer,
                audience=self.audience,
                options={"verify_exp": True}
            )

            return payload

        except jwt.ExpiredSignatureError:
            raise HTTPException(status_code=401, detail="Token has expired")
        except jwt.InvalidTokenError as e:
            raise HTTPException(status_code=401, detail=f"Invalid token: {str(e)}")

    def check_scope(self, required_scope: str, token_scopes: str) -> bool:
        """Check if required scope is present in token scopes"""
        scopes = token_scopes.split(' ') if token_scopes else []
        return required_scope in scopes

    async def _get_public_key(self, kid: str):
        """Fetch and cache public keys from JWKS endpoint"""
        # Implementation would fetch from Keycloak JWKS endpoint
        # and cache the public keys for signature verification
        pass

2. Scope-Based Authorization DecoratorsΒΆ

from functools import wraps
from fastapi import HTTPException

def require_scope(required_scope: str):
    """Decorator to require specific OAuth scope"""
    def decorator(func):
        @wraps(func)
        async def wrapper(*args, **kwargs):
            # Get current user token from dependency injection
            auth_service = kwargs.get('auth_service')  # Injected
            token_data = kwargs.get('current_user')    # From JWT validation

            if not auth_service.check_scope(required_scope, token_data.get('scope', '')):
                raise HTTPException(
                    status_code=403,
                    detail=f"Insufficient permissions. Required scope: {required_scope}"
                )

            return await func(*args, **kwargs)
        return wrapper
    return decorator

3. Protected Controllers with OAuth ScopesΒΆ

from neuroglia.mvc import ControllerBase
from classy_fastapi.decorators import get, post, put, delete
from fastapi import Depends

class OrdersController(ControllerBase):
    def __init__(self,
                 service_provider: ServiceProviderBase,
                 mapper: Mapper,
                 mediator: Mediator,
                 auth_service: JWTAuthService):
        super().__init__(service_provider, mapper, mediator)
        self.auth_service = auth_service

    @get("/", response_model=List[OrderDto])
    @require_scope("orders:read")
    async def get_orders(self,
                        current_user: dict = Depends(get_current_user)) -> List[OrderDto]:
        """Get orders - requires orders:read scope"""
        # Customers see only their orders, staff see all
        if "customer" in current_user.get("roles", []):
            query = GetOrdersByCustomerQuery(customer_id=current_user["sub"])
        else:
            query = GetAllOrdersQuery()

        result = await self.mediator.execute_async(query)
        return self.process(result)

    @post("/", response_model=OrderDto, status_code=201)
    @require_scope("orders:write")
    async def create_order(self,
                          create_order_dto: CreateOrderDto,
                          current_user: dict = Depends(get_current_user)) -> OrderDto:
        """Create new order - requires orders:write scope"""
        command = self.mapper.map(create_order_dto, PlaceOrderCommand)
        command.customer_id = current_user["sub"]  # From JWT

        result = await self.mediator.execute_async(command)
        return self.process(result)

class KitchenController(ControllerBase):

    @get("/status", response_model=KitchenStatusDto)
    @require_scope("kitchen:read")
    async def get_kitchen_status(self,
                                current_user: dict = Depends(get_current_user)) -> KitchenStatusDto:
        """Get kitchen status - requires kitchen:read scope"""
        query = GetKitchenStatusQuery()
        result = await self.mediator.execute_async(query)
        return self.process(result)

    @post("/orders/{order_id}/start")
    @require_scope("kitchen:manage")
    async def start_cooking_order(self,
                                 order_id: str,
                                 current_user: dict = Depends(get_current_user)) -> OrderDto:
        """Start cooking order - requires kitchen:manage scope"""
        command = StartCookingCommand(
            order_id=order_id,
            kitchen_staff_id=current_user["sub"]
        )
        result = await self.mediator.execute_async(command)
        return self.process(result)

4. User Context and Dependency InjectionΒΆ

from fastapi import Depends
from fastapi.security import HTTPBearer

security = HTTPBearer()

async def get_current_user(
    credentials: HTTPAuthorizationCredentials = Depends(security),
    auth_service: JWTAuthService = Depends()
) -> dict:
    """Extract and validate user from JWT token"""
    token = credentials.credentials
    user_data = await auth_service.validate_token(token)
    return user_data

def configure_auth_services(services: ServiceCollection):
    """Configure authentication services"""
    services.add_singleton(JWTAuthService)
    services.add_scoped(lambda sp: get_current_user)

🎭 Role-Based Access Control¢

Mario's Pizzeria defines different user roles with specific scopes:

ROLE_SCOPES = {
    "customer": [
        "orders:read",      # View own orders
        "orders:write",     # Place new orders
        "menu:read"         # Browse menu
    ],
    "kitchen_staff": [
        "orders:read",      # View all orders
        "kitchen:read",     # View kitchen status
        "kitchen:manage",   # Manage cooking queue
        "menu:read"         # View menu
    ],
    "manager": [
        "orders:read",      # View all orders
        "orders:write",     # Create orders for customers
        "kitchen:read",     # Monitor kitchen
        "kitchen:manage",   # Manage kitchen operations
        "menu:read",        # View menu
        "menu:write",       # Update menu items
        "reports:read"      # View analytics
    ],
    "admin": [
        "admin"             # Full access to everything
    ]
}

πŸ”§ Keycloak ConfigurationΒΆ

Keycloak is an open-source identity and access management solution that implements OAuth 2.0 and OpenID Connect standards.

Realm ConfigurationΒΆ

{
  "realm": "pizzeria",
  "enabled": true,
  "displayName": "Mario's Pizzeria",
  "accessTokenLifespan": 3600,
  "ssoSessionMaxLifespan": 86400,
  "clients": [
    {
      "clientId": "pizzeria-web",
      "enabled": true,
      "protocol": "openid-connect",
      "redirectUris": ["https://mariospizzeria.com/auth/callback"],
      "webOrigins": ["https://mariospizzeria.com"],
      "defaultClientScopes": ["profile", "email", "roles"]
    },
    {
      "clientId": "pizzeria-api",
      "enabled": true,
      "bearerOnly": true,
      "protocol": "openid-connect"
    }
  ],
  "clientScopes": [
    {
      "name": "orders:read",
      "description": "Read order information"
    },
    {
      "name": "orders:write",
      "description": "Create and modify orders"
    },
    {
      "name": "kitchen:read",
      "description": "View kitchen status"
    },
    {
      "name": "kitchen:manage",
      "description": "Manage kitchen operations"
    }
  ]
}

πŸ“± Frontend Integration ExampleΒΆ

// React/JavaScript frontend example
class PizzeriaAuthService {
  constructor() {
    this.keycloakConfig = {
      url: "https://keycloak.mariospizzeria.com/auth",
      realm: "pizzeria",
      clientId: "pizzeria-web",
    };
  }

  async login() {
    // Redirect to Keycloak login
    const authUrl =
      `${this.keycloakConfig.url}/realms/${this.keycloakConfig.realm}/protocol/openid_connect/auth` +
      `?client_id=${this.keycloakConfig.clientId}` +
      `&response_type=code` +
      `&scope=openid profile email orders:read orders:write menu:read` +
      `&redirect_uri=${encodeURIComponent(window.location.origin + "/auth/callback")}`;

    window.location.href = authUrl;
  }

  async makeAuthenticatedRequest(url, options = {}) {
    const token = localStorage.getItem("access_token");

    return fetch(url, {
      ...options,
      headers: {
        Authorization: `Bearer ${token}`,
        "Content-Type": "application/json",
        ...options.headers,
      },
    });
  }

  // Example: Place order with authentication
  async placeOrder(orderData) {
    const response = await this.makeAuthenticatedRequest("/api/orders", {
      method: "POST",
      body: JSON.stringify(orderData),
    });

    if (response.status === 401) {
      // Token expired, redirect to login
      this.login();
      return;
    }

    if (response.status === 403) {
      throw new Error("Insufficient permissions to place order");
    }

    return response.json();
  }
}

πŸ§ͺ Testing AuthenticationΒΆ

import pytest
from unittest.mock import Mock, patch

class TestAuthenticatedEndpoints:
    def setup_method(self):
        self.auth_service = Mock(spec=JWTAuthService)
        self.test_user = {
            "sub": "customer_123",
            "name": "Mario Rossi",
            "email": "mario@example.com",
            "roles": ["customer"],
            "scope": "orders:read orders:write menu:read"
        }

    async def test_get_orders_with_valid_token(self, test_client):
        """Test getting orders with valid customer token"""
        self.auth_service.validate_token.return_value = self.test_user
        self.auth_service.check_scope.return_value = True

        headers = {"Authorization": "Bearer valid_token"}
        response = await test_client.get("/orders", headers=headers)

        assert response.status_code == 200
        # Should only return customer's own orders

    async def test_get_orders_insufficient_scope(self, test_client):
        """Test getting orders without required scope"""
        user_without_scope = {**self.test_user, "scope": "menu:read"}
        self.auth_service.validate_token.return_value = user_without_scope
        self.auth_service.check_scope.return_value = False

        headers = {"Authorization": "Bearer limited_token"}
        response = await test_client.get("/orders", headers=headers)

        assert response.status_code == 403
        assert "Insufficient permissions" in response.json()["detail"]

    async def test_kitchen_access_staff_only(self, test_client):
        """Test kitchen endpoints require staff role"""
        staff_user = {
            "sub": "staff_456",
            "roles": ["kitchen_staff"],
            "scope": "kitchen:read kitchen:manage"
        }
        self.auth_service.validate_token.return_value = staff_user
        self.auth_service.check_scope.return_value = True

        headers = {"Authorization": "Bearer staff_token"}
        response = await test_client.get("/kitchen/status", headers=headers)

        assert response.status_code == 200

    async def test_expired_token(self, test_client):
        """Test expired token handling"""
        from jwt import ExpiredSignatureError
        self.auth_service.validate_token.side_effect = ExpiredSignatureError()

        headers = {"Authorization": "Bearer expired_token"}
        response = await test_client.get("/orders", headers=headers)

        assert response.status_code == 401
        assert "expired" in response.json()["detail"].lower()

πŸ“‹ Security Best PracticesΒΆ

Following OAuth 2.0 Security Best Current Practice and JWT Best Current Practices:

1. Token SecurityΒΆ

  • Short-lived access tokens (15-60 minutes)
  • Secure refresh token rotation
  • HTTPS only in production
  • Secure storage (HttpOnly cookies for web)

2. Scope ManagementΒΆ

  • Principle of least privilege - minimal required scopes
  • Granular permissions - specific scopes for each operation
  • Role-based defaults - sensible scope sets per role

3. API SecurityΒΆ

  • Rate limiting on authentication endpoints
  • Input validation on all endpoints
  • Audit logging for sensitive operations
  • CORS configuration for web clients

πŸš€ Production DeploymentΒΆ

# docker-compose.yml for production
version: "3.8"
services:
  keycloak:
    image: quay.io/keycloak/keycloak:latest
    environment:
      KEYCLOAK_ADMIN: admin
      KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD}
      KC_DB: postgres
      KC_DB_URL: jdbc:postgresql://postgres:5432/keycloak
      KC_DB_USERNAME: keycloak
      KC_DB_PASSWORD: ${DB_PASSWORD}
    command: start --optimized
    depends_on:
      - postgres
    ports:
      - "8080:8080"

  pizzeria-api:
    build: .
    environment:
      JWT_ISSUER: "https://keycloak.mariospizzeria.com/auth/realms/pizzeria"
      JWT_AUDIENCE: "pizzeria-api"
      JWKS_URL: "https://keycloak.mariospizzeria.com/auth/realms/pizzeria/protocol/openid_connect/certs"
    depends_on:
      - keycloak
    ports:
      - "8000:8000"

οΏ½ Authoritative References & SpecificationsΒΆ

OAuth 2.0 Official SpecificationsΒΆ

OpenID Connect Official SpecificationsΒΆ

JSON Web Token (JWT) SpecificationsΒΆ

Security Best Practices & GuidelinesΒΆ

Identity Provider DocumentationΒΆ

Python Libraries & ToolsΒΆ

Testing & Development ToolsΒΆ

Educational ResourcesΒΆ

This comprehensive authentication guide demonstrates how to implement enterprise-grade security using OAuth 2.0, OpenID Connect, and JWT tokens with the Neuroglia framework. The examples show real-world patterns for protecting APIs, managing user permissions, and integrating with identity providers like Keycloak.