# Deep Dive: Authelia Architecture, OIDC/IDP, and Building Your Own Spring Boot IDP

## Table of Contents
1. [What is Authelia?](#what-is-authelia)
2. [Architecture Deep Dive](#architecture-deep-dive)
3. [Understanding IDP and OIDC](#understanding-idp-and-oidc)
4. [Why Authelia is Called an IDP](#why-authelia-is-called-an-idp)
5. [Building Your Own Spring Boot IDP](#building-your-own-spring-boot-idp)
6. [Complete Implementation Examples](#complete-implementation-examples)

---

## What is Authelia?

Authelia is a lightweight, open-source authentication and authorization server written in Go. It provides:
- **2FA/MFA support** (TOTP, WebAuthn, Duo push notifications)
- **SSO (Single Sign-On)** capabilities
- **OpenID Connect 1.0 Provider** certification
- **Session management** across multiple applications
- **Authorization policies** for granular access control

It acts as a **companion to reverse proxies** (Traefik, Nginx, Caddy, HAProxy, etc.), not as a direct application server.

---

## Architecture Deep Dive

### High-Level Architecture

```
┌─────────────────────────────────────────────────────────────┐
│                     Client/Browser                          │
└────────────────────────┬────────────────────────────────────┘
                         │
                         ▼
┌─────────────────────────────────────────────────────────────┐
│               Reverse Proxy (Traefik/Nginx)                 │
│  - Routes incoming requests                                  │
│  - Performs auth checks via Authelia                         │
│  - Forwards auth headers to backend apps                     │
└────────┬──────────────────────────────┬──────────────────────┘
         │                              │
         ▼                              ▼
    ┌──────────────────────┐      ┌──────────────────────┐
    │  Authelia Service    │      │  Backend Apps        │
    │  - Authentication    │      │  (REST/GraphQL/etc)  │
    │  - Authorization     │      │                      │
    │  - Session Management│      │  Never see auth      │
    │  - Login Portal      │      │  details directly    │
    │  - OIDC Provider     │      └──────────────────────┘
    └──────────────────────┘

Key Point: Authelia sits BETWEEN the reverse proxy and apps.
It NEVER directly handles user request payloads.
```

### Request Flow When User Accesses Protected Resource

**Scenario: User tries to access https://app.example.com**

```
1. User hits https://app.example.com
   │
   ▼
2. Reverse Proxy intercepts request
   - Extracts session cookie (if present)
   - Makes auth check request to Authelia
   │
   ▼
3a. If user NOT authenticated:
    - Authelia responds: "DENY, redirect to login"
    - Reverse proxy redirects user to https://auth.example.com/login
    │
    ▼
    User enters credentials on Authelia's login portal
    │
    ▼
    Authelia validates credentials against auth backend:
    - LDAP directory
    - YAML file
    - Database
    │
    ▼
    If valid:
    - Sets session cookie valid for *.example.com
    - Performs 2FA if required
    - Creates signed JWT tokens (for OIDC)
    - Redirects user back to original URL with session cookie
    │
    ▼
3b. If user IS authenticated:
    - Authelia verifies session cookie
    - Checks authorization policies (access rules)
    - Responds: "ALLOW" or "DENY"
    │
    ▼
4. Reverse Proxy decision:
    - If ALLOW: Forward request to backend app
             Add security headers (Remote-User, groups, etc.)
    - If DENY: Return 403 Forbidden
```

### Key Architectural Components

**1. Authentication Backends**
- **LDAP**: For enterprise directory integration
- **File-based**: YAML file with plaintext/hashed passwords
- **Database**: SQL-based user storage

**2. Storage Providers** (for user/session data)
- **SQLite3**: Good for testing/lightweight deployments
- **PostgreSQL**: Recommended for production
- **MySQL/MariaDB**: Alternative SQL option
- **Memory**: Stateful, not recommended for HA

**3. Session Handling**
- Session cookies scoped to SSO domain (e.g., .example.com)
- Configurable expiration and inactivity timeouts
- Remember-me functionality
- Stateless option via Redis for HA deployments

**4. Reverse Proxy Integration Methods**

Different proxies use different auth protocols:

```
Forward Auth (Traefik, Caddy):
┌──────────────────────────────────┐
│ Reverse Proxy                    │
│ Receives request                 │
│ Calls: GET /api/authz/forward    │
│ Sends request details to Authelia│
└──────────────────────────────────┘
         │
         ▼
┌──────────────────────────────────┐
│ Authelia Checks:                 │
│ - Session valid?                 │
│ - User authorized?               │
│ Returns: 200 OK + headers OR 401 │
└──────────────────────────────────┘

Remote User (nginx, HAProxy):
Similar but uses different endpoints/header names
```

**5. OIDC/OAuth2 Provider Component**

Authelia can act as an identity provider for other applications:
- Issues JWT ID tokens
- Provides access tokens
- UserInfo endpoint with user claims
- Multiple OIDC flows support

---

## Understanding IDP and OIDC

### Identity Provider (IDP) - Definition

An **Identity Provider** is a trusted service that:
1. **Authenticates users** (verifies who they are)
2. **Manages user identities** (stores user data)
3. **Issues tokens/credentials** for access to resources
4. **Provides SSO capabilities** (one login for many apps)

Examples: Google, Microsoft Azure AD, Okta, Keycloak, Authelia

### OpenID Connect (OIDC) - Definition

**OIDC** is an industry standard authentication protocol built on top of OAuth 2.0 that:

```
OAuth 2.0 (Authorization Framework)
├─ Purpose: "What resources can this user access?"
├─ Issues: Access tokens for API access
└─ Problem: Doesn't tell you WHO the user is

OpenID Connect (Authentication Layer on OAuth 2.0)
├─ Purpose: "Who is this user?" + Authorization
├─ Adds: ID tokens containing user identity info
├─ Uses: JWTs (JSON Web Tokens) for signed assertions
└─ Solution: Standardized way to verify user identity
```

### OIDC Flow Diagram

```
┌──────────────────────────────────────────────────────────────┐
│                    User Wants to Login                        │
└─────────────────────────┬──────────────────────────────────────┘
                          │
                          ▼
         ┌────────────────────────────────────┐
         │  1. User clicks "Login with OIDC"  │
         │     on Relying Party (RP) app      │
         └────────────────┬───────────────────┘
                          │
                          ▼
    ┌─────────────────────────────────────────────────────┐
    │  2. RP redirects to OpenID Provider (IDP)            │
    │     GET /authorize?client_id=xxx&redirect_uri=...  │
    │     &scope=openid profile email&state=xxx           │
    └────────────┬────────────────────────────────────────┘
                 │
                 ▼
    ┌─────────────────────────────────────────────────────┐
    │  3. IDP shows login page                             │
    │     User enters username/password                    │
    └────────────┬────────────────────────────────────────┘
                 │
                 ▼
    ┌─────────────────────────────────────────────────────┐
    │  4. IDP validates credentials                        │
    │     If valid, shows consent screen                   │
    │     "This app wants access to: name, email, profile"│
    └────────────┬────────────────────────────────────────┘
                 │
                 ▼
    ┌─────────────────────────────────────────────────────┐
    │  5. User grants permission                           │
    │     IDP generates authorization code                 │
    └────────────┬────────────────────────────────────────┘
                 │
                 ▼
    ┌─────────────────────────────────────────────────────┐
    │  6. IDP redirects back to RP with code               │
    │     GET redirect_uri?code=xxx&state=xxx              │
    └────────────┬────────────────────────────────────────┘
                 │
                 ▼
    ┌─────────────────────────────────────────────────────┐
    │  7. RP exchanges code for tokens (server-to-server)  │
    │     POST /token                                      │
    │     client_id, client_secret, code, redirect_uri    │
    │                                                       │
    │     IDP returns:                                      │
    │     {                                                 │
    │       "id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI...",  │
    │       "access_token": "2YotnFZFEjr1zCsicMWpAA...",   │
    │       "token_type": "Bearer",                        │
    │       "expires_in": 3600                             │
    │     }                                                 │
    └────────────┬────────────────────────────────────────┘
                 │
                 ▼
    ┌─────────────────────────────────────────────────────┐
    │  8. RP decodes and validates ID token                │
    │     Verifies signature using IDP's public key        │
    │     Extracts claims: sub, name, email, groups, etc   │
    └────────────┬────────────────────────────────────────┘
                 │
                 ▼
    ┌─────────────────────────────────────────────────────┐
    │  9. RP creates session with user info               │
    │     Returns user to logged-in state                 │
    └─────────────────────────────────────────────────────┘
```

### Key OIDC Concepts

**ID Token** - JWT containing user identity info
```json
{
  "iss": "https://auth.example.com",
  "sub": "user123",
  "aud": "my-app-client-id",
  "exp": 1234567890,
  "iat": 1234564290,
  "auth_time": 1234564290,
  "name": "John Doe",
  "email": "john@example.com",
  "email_verified": true,
  "groups": ["developers", "admins"]
}
```

**Access Token** - Used to call APIs on behalf of user
```json
{
  "iss": "https://auth.example.com",
  "sub": "user123",
  "aud": "my-api",
  "exp": 1234567890,
  "iat": 1234564290,
  "scope": "openid profile email api"
}
```

**Claims** - Pieces of user information
- Standard: `sub` (subject), `iss` (issuer), `aud` (audience), `exp` (expiration)
- Profile: `name`, `email`, `family_name`, `given_name`
- Custom: `groups`, `roles`, `department`, `tenant`

---

## Why Authelia is Called an IDP

Authelia is called an IDP because it:

1. **Authenticates users** - Validates credentials against LDAP, files, or databases
2. **Manages identities** - Stores user profiles, groups, roles
3. **Issues tokens** - Creates JWT tokens with identity claims
4. **Provides SSO** - Single login across all protected apps via session cookies
5. **Implements OIDC** - Fully certified OpenID Connect 1.0 Provider

### OIDC Provider Implementation in Authelia

```
Authelia OIDC Endpoints:
├─ GET  /.well-known/openid-configuration
│       Returns OIDC discovery document
├─ GET  /oauth2/authorization
│       Authorization endpoint for login flow
├─ POST /oauth2/token
│       Token endpoint to exchange code for tokens
├─ GET  /oauth2/userinfo
│       Returns authenticated user claims
├─ GET  /.well-known/jwks.json
│       Returns public keys for token verification
└─ POST /oauth2/revoke
        Revokes tokens
```

This makes Authelia both:
- **Forward Auth Provider** (SSO for reverse-proxied apps)
- **OpenID Connect Provider** (Can be used by OIDC-compliant apps)

---

## Building Your Own Spring Boot IDP

### Project Structure

```
spring-boot-oidc-provider/
├── pom.xml
├── src/main/java/com/example/oidc/
│   ├── config/
│   │   ├── SecurityConfig.java
│   │   ├── OidcConfig.java
│   │   └── JwtConfig.java
│   ├── controller/
│   │   ├── AuthorizationController.java
│   │   ├── TokenController.java
│   │   └── UserInfoController.java
│   ├── service/
│   │   ├── UserService.java
│   │   ├── TokenService.java
│   │   ├── AuthorizationCodeService.java
│   │   └── JwtTokenProvider.java
│   ├── model/
│   │   ├── User.java
│   │   ├── Client.java
│   │   ├── OidcToken.java
│   │   └── AuthorizationCode.java
│   ├── repository/
│   │   ├── UserRepository.java
│   │   ├── ClientRepository.java
│   │   ├── AuthorizationCodeRepository.java
│   │   └── TokenRepository.java
│   ├── util/
│   │   ├── JwtUtils.java
│   │   └── SecurityUtils.java
│   └── OidcProviderApplication.java
├── src/main/resources/
│   ├── application.yml
│   └── db/
│       └── migration/
└── src/test/java/...
```

### Step 1: Maven Dependencies (pom.xml)

```xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>spring-boot-oidc-provider</artifactId>
    <version>1.0.0</version>
    <name>Spring Boot OIDC Provider</name>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.0</version>
        <relativePath/>
    </parent>

    <dependencies>
        <!-- Spring Boot Starters -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

        <!-- Database -->
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <scope>runtime</scope>
        </dependency>

        <!-- JWT -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>0.12.3</version>
        </dependency>

        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>0.12.3</version>
            <scope>runtime</scope>
        </dependency>

        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId>
            <version>0.12.3</version>
            <scope>runtime</scope>
        </dependency>

        <!-- Lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!-- Testing -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
```

### Step 2: Application Properties (application.yml)

```yaml
spring:
  application:
    name: spring-boot-oidc-provider
  datasource:
    url: jdbc:postgresql://localhost:5432/oidc_db
    username: postgres
    password: postgres
    driver-class-name: org.postgresql.Driver
  jpa:
    hibernate:
      ddl-auto: validate
    properties:
      hibernate:
        dialect: org.hibernate.dialect.PostgreSQLDialect
        format_sql: true
        jdbc:
          batch_size: 20
  mvc:
    throw-exception-if-no-handler-found: true
    static-path-pattern: /static/**

server:
  port: 9000
  servlet:
    context-path: /

logging:
  level:
    root: INFO
    com.example.oidc: DEBUG

app:
  oidc:
    issuer: http://localhost:9000
    jwt:
      secret: your-super-secret-key-min-32-characters-long!
      expiration: 3600  # 1 hour in seconds
      refresh-expiration: 604800  # 7 days in seconds
    token:
      code-expiration: 600  # 10 minutes in seconds
```

### Step 3: Entity Models

**User.java**
```java
package com.example.oidc.model;

import jakarta.persistence.*;
import lombok.*;

@Entity
@Table(name = "users")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true, nullable = false)
    private String username;

    @Column(nullable = false)
    private String password;

    @Column(unique = true, nullable = false)
    private String email;

    private String firstName;
    private String lastName;

    @Column(nullable = false)
    private boolean enabled = true;

    private String phoneNumber;
    private String department;

    @ElementCollection(fetch = FetchType.EAGER)
    private java.util.Set<String> roles = new java.util.HashSet<>();

    @ElementCollection(fetch = FetchType.EAGER)
    private java.util.Set<String> groups = new java.util.HashSet<>();
}
```

**Client.java** (OAuth2 registered applications)
```java
package com.example.oidc.model;

import jakarta.persistence.*;
import lombok.*;

@Entity
@Table(name = "oauth_clients")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Client {
    @Id
    private String clientId;

    @Column(nullable = false)
    private String clientSecret;

    @Column(nullable = false)
    private String clientName;

    @ElementCollection(fetch = FetchType.EAGER)
    private java.util.Set<String> redirectUris;

    @ElementCollection(fetch = FetchType.EAGER)
    private java.util.Set<String> allowedScopes = 
        java.util.Set.of("openid", "profile", "email");

    @Column(nullable = false)
    private boolean enabled = true;

    private String logoUri;
    private String description;
}
```

**AuthorizationCode.java**
```java
package com.example.oidc.model;

import jakarta.persistence.*;
import lombok.*;
import java.time.LocalDateTime;

@Entity
@Table(name = "authorization_codes")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class AuthorizationCode {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, unique = true)
    private String code;

    @ManyToOne
    @JoinColumn(name = "user_id", nullable = false)
    private User user;

    @ManyToOne
    @JoinColumn(name = "client_id", nullable = false)
    private Client client;

    @Column(nullable = false)
    private String redirectUri;

    @ElementCollection(fetch = FetchType.EAGER)
    private java.util.Set<String> grantedScopes;

    @Column(nullable = false)
    private LocalDateTime expiresAt;

    @Column(nullable = false)
    private LocalDateTime createdAt = LocalDateTime.now();

    private boolean used = false;
}
```

### Step 4: JWT Token Provider

```java
package com.example.oidc.util;

import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import com.example.oidc.model.User;

import javax.crypto.SecretKey;
import java.time.Instant;
import java.util.*;

@Component
@Slf4j
public class JwtTokenProvider {

    @Value("${app.oidc.jwt.secret}")
    private String jwtSecret;

    @Value("${app.oidc.jwt.expiration}")
    private int jwtExpirationMs;

    @Value("${app.oidc.issuer}")
    private String issuer;

    private SecretKey getSigningKey() {
        return Keys.hmacShaKeyFor(jwtSecret.getBytes());
    }

    /**
     * Generate ID Token (for OIDC authentication)
     */
    public String generateIdToken(User user, String clientId, String nonce) {
        return Jwts.builder()
                .subject(user.getId().toString())
                .issuer(issuer)
                .audience().add(clientId).and()
                .claim("name", user.getFirstName() + " " + user.getLastName())
                .claim("email", user.getEmail())
                .claim("email_verified", true)
                .claim("given_name", user.getFirstName())
                .claim("family_name", user.getLastName())
                .claim("groups", user.getGroups())
                .claim("roles", user.getRoles())
                .claim("nonce", nonce)
                .issuedAt(new Date())
                .expiration(new Date(System.currentTimeMillis() + jwtExpirationMs * 1000))
                .signWith(getSigningKey(), SignatureAlgorithm.HS256)
                .compact();
    }

    /**
     * Generate Access Token (for API access)
     */
    public String generateAccessToken(User user, String clientId, Set<String> scopes) {
        return Jwts.builder()
                .subject(user.getId().toString())
                .issuer(issuer)
                .audience().add(clientId).and()
                .claim("username", user.getUsername())
                .claim("scope", String.join(" ", scopes))
                .claim("groups", user.getGroups())
                .claim("roles", user.getRoles())
                .issuedAt(new Date())
                .expiration(new Date(System.currentTimeMillis() + jwtExpirationMs * 1000))
                .signWith(getSigningKey(), SignatureAlgorithm.HS256)
                .compact();
    }

    /**
     * Get user ID from token
     */
    public String getUserIdFromToken(String token) {
        return Jwts.parserBuilder()
                .setSigningKey(getSigningKey())
                .build()
                .parseClaimsJws(token)
                .getBody()
                .getSubject();
    }

    /**
     * Validate token
     */
    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder()
                    .setSigningKey(getSigningKey())
                    .build()
                    .parseClaimsJws(token);
            return true;
        } catch (SecurityException e) {
            log.error("Invalid JWT signature", e);
        } catch (MalformedJwtException e) {
            log.error("Invalid JWT token", e);
        } catch (ExpiredJwtException e) {
            log.error("Expired JWT token", e);
        } catch (UnsupportedJwtException e) {
            log.error("Unsupported JWT token", e);
        } catch (IllegalArgumentException e) {
            log.error("JWT claims string is empty", e);
        }
        return false;
    }

    /**
     * Get claims from token
     */
    public Claims getClaimsFromToken(String token) {
        return Jwts.parserBuilder()
                .setSigningKey(getSigningKey())
                .build()
                .parseClaimsJws(token)
                .getBody();
    }
}
```

### Step 5: Controllers

**AuthorizationController.java** (handles /authorize endpoint)
```java
package com.example.oidc.controller;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import com.example.oidc.model.AuthorizationCode;
import com.example.oidc.model.Client;
import com.example.oidc.model.User;
import com.example.oidc.repository.AuthorizationCodeRepository;
import com.example.oidc.repository.ClientRepository;
import com.example.oidc.repository.UserRepository;

import java.time.LocalDateTime;
import java.util.*;

@Controller
@Slf4j
@RequiredArgsConstructor
public class AuthorizationController {

    private final ClientRepository clientRepository;
    private final UserRepository userRepository;
    private final AuthorizationCodeRepository authorizationCodeRepository;

    /**
     * Authorization endpoint
     * GET /oauth2/authorization?client_id=xxx&redirect_uri=xxx&scope=xxx&response_type=code&state=xxx&nonce=xxx
     */
    @GetMapping("/oauth2/authorization")
    public String authorize(
            @RequestParam(value = "client_id") String clientId,
            @RequestParam(value = "redirect_uri") String redirectUri,
            @RequestParam(value = "scope", defaultValue = "openid") String scope,
            @RequestParam(value = "response_type", defaultValue = "code") String responseType,
            @RequestParam(value = "state") String state,
            @RequestParam(value = "nonce", required = false) String nonce,
            @RequestParam(value = "prompt", required = false) String prompt,
            Authentication authentication,
            Model model) {

        log.info("Authorization request from client: {}", clientId);

        // Validate client
        Client client = clientRepository.findById(clientId)
                .orElseThrow(() -> new IllegalArgumentException("Invalid client_id"));

        if (!client.isEnabled()) {
            throw new IllegalArgumentException("Client is disabled");
        }

        if (!client.getRedirectUris().contains(redirectUri)) {
            throw new IllegalArgumentException("Invalid redirect_uri");
        }

        // Validate scopes
        Set<String> requestedScopes = new HashSet<>(Arrays.asList(scope.split(" ")));
        if (!client.getAllowedScopes().containsAll(requestedScopes)) {
            throw new IllegalArgumentException("Invalid scope");
        }

        // If user not authenticated, redirect to login
        if (authentication == null || !authentication.isAuthenticated()) {
            model.addAttribute("client_id", clientId);
            model.addAttribute("redirect_uri", redirectUri);
            model.addAttribute("scope", scope);
            model.addAttribute("state", state);
            model.addAttribute("nonce", nonce);
            return "redirect:/login";
        }

        // User is authenticated, show consent screen
        model.addAttribute("client", client);
        model.addAttribute("requestedScopes", requestedScopes);
        model.addAttribute("client_id", clientId);
        model.addAttribute("redirect_uri", redirectUri);
        model.addAttribute("scope", scope);
        model.addAttribute("state", state);
        model.addAttribute("nonce", nonce);

        return "consent";
    }

    /**
     * User approves the authorization request
     */
    @PostMapping("/oauth2/authorization/approve")
    public String approveAuthorization(
            @RequestParam(value = "client_id") String clientId,
            @RequestParam(value = "redirect_uri") String redirectUri,
            @RequestParam(value = "scope") String scope,
            @RequestParam(value = "state") String state,
            @RequestParam(value = "nonce", required = false) String nonce,
            Authentication authentication) {

        log.info("Authorization approved for client: {}", clientId);

        // Get authenticated user
        String username = authentication.getName();
        User user = userRepository.findByUsername(username)
                .orElseThrow(() -> new IllegalArgumentException("User not found"));

        // Generate authorization code
        String code = UUID.randomUUID().toString();
        LocalDateTime expiresAt = LocalDateTime.now().plusMinutes(10);

        AuthorizationCode authCode = AuthorizationCode.builder()
                .code(code)
                .user(user)
                .client(clientRepository.findById(clientId).orElseThrow())
                .redirectUri(redirectUri)
                .grantedScopes(new HashSet<>(Arrays.asList(scope.split(" "))))
                .expiresAt(expiresAt)
                .build();

        authorizationCodeRepository.save(authCode);

        // Redirect to client with code and state
        String redirectUrl = redirectUri + "?code=" + code + "&state=" + state;
        if (nonce != null) {
            redirectUrl += "&nonce=" + nonce;
        }

        return "redirect:" + redirectUrl;
    }
}
```

**TokenController.java** (handles /token endpoint)
```java
package com.example.oidc.controller;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import com.example.oidc.model.AuthorizationCode;
import com.example.oidc.model.Client;
import com.example.oidc.model.User;
import com.example.oidc.repository.AuthorizationCodeRepository;
import com.example.oidc.repository.ClientRepository;
import com.example.oidc.util.JwtTokenProvider;

import java.time.LocalDateTime;
import java.util.*;

@RestController
@RequestMapping("/oauth2")
@Slf4j
@RequiredArgsConstructor
public class TokenController {

    private final ClientRepository clientRepository;
    private final AuthorizationCodeRepository authorizationCodeRepository;
    private final JwtTokenProvider jwtTokenProvider;

    /**
     * Token endpoint
     * POST /oauth2/token
     * Body: grant_type=authorization_code&code=xxx&client_id=xxx&client_secret=xxx&redirect_uri=xxx
     */
    @PostMapping("/token")
    public Map<String, Object> token(
            @RequestParam("grant_type") String grantType,
            @RequestParam("code") String code,
            @RequestParam("client_id") String clientId,
            @RequestParam("client_secret") String clientSecret,
            @RequestParam("redirect_uri") String redirectUri) {

        log.info("Token request from client: {}", clientId);

        if (!"authorization_code".equals(grantType)) {
            return errorResponse("unsupported_grant_type", "Only authorization_code grant type is supported");
        }

        // Validate client
        Client client = clientRepository.findById(clientId)
                .orElseThrow(() -> new IllegalArgumentException("Invalid client_id"));

        if (!client.getClientSecret().equals(clientSecret)) {
            return errorResponse("invalid_client", "Invalid client_secret");
        }

        // Validate authorization code
        AuthorizationCode authCode = authorizationCodeRepository.findByCode(code)
                .orElseThrow(() -> new IllegalArgumentException("Invalid code"));

        if (authCode.isUsed()) {
            authorizationCodeRepository.delete(authCode);
            return errorResponse("invalid_grant", "Authorization code already used");
        }

        if (authCode.getExpiresAt().isBefore(LocalDateTime.now())) {
            return errorResponse("invalid_grant", "Authorization code expired");
        }

        if (!authCode.getRedirectUri().equals(redirectUri)) {
            return errorResponse("invalid_grant", "Invalid redirect_uri");
        }

        // Mark code as used
        authCode.setUsed(true);
        authorizationCodeRepository.save(authCode);

        // Generate tokens
        User user = authCode.getUser();
        String idToken = jwtTokenProvider.generateIdToken(user, clientId, authCode.getCode());
        String accessToken = jwtTokenProvider.generateAccessToken(user, clientId, authCode.getGrantedScopes());

        Map<String, Object> response = new LinkedHashMap<>();
        response.put("access_token", accessToken);
        response.put("token_type", "Bearer");
        response.put("expires_in", 3600);
        response.put("id_token", idToken);
        response.put("scope", String.join(" ", authCode.getGrantedScopes()));

        log.info("Token issued successfully for user: {}", user.getUsername());
        return response;
    }

    /**
     * UserInfo endpoint - returns authenticated user claims
     * GET /oauth2/userinfo
     * Header: Authorization: Bearer <access_token>
     */
    @GetMapping("/userinfo")
    public Map<String, Object> userInfo(@RequestHeader("Authorization") String authHeader) {
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            return errorResponse("invalid_token", "Missing or invalid authorization header");
        }

        String token = authHeader.substring(7);

        if (!jwtTokenProvider.validateToken(token)) {
            return errorResponse("invalid_token", "Invalid or expired access token");
        }

        String userId = jwtTokenProvider.getUserIdFromToken(token);
        var claims = jwtTokenProvider.getClaimsFromToken(token);

        Map<String, Object> response = new LinkedHashMap<>();
        response.put("sub", userId);
        response.put("name", claims.get("name"));
        response.put("email", claims.get("email"));
        response.put("email_verified", claims.get("email_verified"));
        response.put("given_name", claims.get("given_name"));
        response.put("family_name", claims.get("family_name"));
        response.put("groups", claims.get("groups"));
        response.put("roles", claims.get("roles"));

        return response;
    }

    private Map<String, Object> errorResponse(String error, String description) {
        Map<String, Object> response = new LinkedHashMap<>();
        response.put("error", error);
        response.put("error_description", description);
        return response;
    }
}
```

### Step 6: OIDC Discovery Endpoint

```java
package com.example.oidc.controller;

import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.*;

@RestController
@RequiredArgsConstructor
public class DiscoveryController {

    @Value("${app.oidc.issuer}")
    private String issuer;

    /**
     * OpenID Connect Discovery endpoint
     * GET /.well-known/openid-configuration
     */
    @GetMapping("/.well-known/openid-configuration")
    public Map<String, Object> discoverOidcConfiguration() {
        Map<String, Object> config = new LinkedHashMap<>();

        config.put("issuer", issuer);
        config.put("authorization_endpoint", issuer + "/oauth2/authorization");
        config.put("token_endpoint", issuer + "/oauth2/token");
        config.put("userinfo_endpoint", issuer + "/oauth2/userinfo");
        config.put("jwks_uri", issuer + "/.well-known/jwks.json");
        config.put("registration_endpoint", issuer + "/oauth2/register");
        config.put("end_session_endpoint", issuer + "/logout");

        config.put("scopes_supported", List.of("openid", "profile", "email", "address", "phone"));
        config.put("response_types_supported", List.of("code", "id_token", "token"));
        config.put("response_modes_supported", List.of("query", "fragment"));
        config.put("grant_types_supported", List.of("authorization_code", "refresh_token"));
        config.put("subject_types_supported", List.of("public"));
        config.put("id_token_signing_alg_values_supported", List.of("HS256", "RS256"));
        config.put("token_endpoint_auth_methods_supported", 
            List.of("client_secret_basic", "client_secret_post"));

        config.put("claims_supported", List.of(
            "sub", "iss", "aud", "exp", "iat", "auth_time",
            "name", "email", "email_verified", "given_name", "family_name",
            "groups", "roles"
        ));

        config.put("service_documentation", issuer + "/docs");
        config.put("ui_locales_supported", List.of("en-US"));
        config.put("require_request_uri_registration", false);

        return config;
    }

    /**
     * JWKS endpoint - returns public keys for token verification
     * GET /.well-known/jwks.json
     */
    @GetMapping("/.well-known/jwks.json")
    public Map<String, Object> getJwks() {
        // In production, implement proper JWKS retrieval
        Map<String, Object> jwks = new LinkedHashMap<>();
        jwks.put("keys", new ArrayList<>());
        return jwks;
    }
}
```

### Step 7: Security Configuration

```java
package com.example.oidc.config;

import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .authorizeHttpRequests(authz -> authz
                .requestMatchers("/.well-known/**").permitAll()
                .requestMatchers("/oauth2/token").permitAll()
                .requestMatchers("/login").permitAll()
                .requestMatchers("/register").permitAll()
                .requestMatchers("/actuator/**").permitAll()
                .anyRequest().authenticated()
            )
            .formLogin()
                .loginPage("/login")
                .defaultSuccessUrl("/dashboard")
                .permitAll()
            .and()
            .logout()
                .logoutUrl("/logout")
                .logoutSuccessUrl("/")
                .permitAll();

        return http.build();
    }
}
```

---

## Complete Implementation Examples

### Example 1: Testing the Authorization Flow

```bash
# 1. Start Authorization Request
curl -X GET "http://localhost:9000/oauth2/authorization?client_id=my-app&redirect_uri=http://localhost:3000/callback&scope=openid%20profile%20email&response_type=code&state=abc123&nonce=xyz789"

# Result: User is redirected to login page (or consent if already logged in)

# 2. After user logs in and approves, they get a code:
# http://localhost:3000/callback?code=<code>&state=abc123

# 3. Exchange code for tokens (backend-to-backend)
curl -X POST http://localhost:9000/oauth2/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code&code=<code>&client_id=my-app&client_secret=secret123&redirect_uri=http://localhost:3000/callback"

# Result:
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "scope": "openid profile email"
}

# 4. Get user info using access token
curl -X GET http://localhost:9000/oauth2/userinfo \
  -H "Authorization: Bearer <access_token>"

# Result:
{
  "sub": "1",
  "name": "John Doe",
  "email": "john@example.com",
  "email_verified": true,
  "given_name": "John",
  "family_name": "Doe",
  "groups": ["developers"],
  "roles": ["user"]
}
```

### Example 2: Client Application Integration

```javascript
// React Client using OIDC Authorization Code Flow with PKCE

import React, { useEffect } from 'react';
import axios from 'axios';

const OIDC_PROVIDER = 'http://localhost:9000';
const CLIENT_ID = 'my-app';
const REDIRECT_URI = 'http://localhost:3000/callback';
const SCOPES = 'openid profile email';

function LoginButton() {
  const handleLogin = () => {
    // Generate PKCE parameters
    const codeVerifier = generateRandomString(64);
    const codeChallenge = generateCodeChallenge(codeVerifier);
    
    const state = generateRandomString(16);
    const nonce = generateRandomString(16);

    // Store for later validation
    localStorage.setItem('pkce_verifier', codeVerifier);
    localStorage.setItem('state', state);
    localStorage.setItem('nonce', nonce);

    // Redirect to OIDC provider
    const authUrl = new URL(`${OIDC_PROVIDER}/oauth2/authorization`);
    authUrl.searchParams.append('client_id', CLIENT_ID);
    authUrl.searchParams.append('redirect_uri', REDIRECT_URI);
    authUrl.searchParams.append('response_type', 'code');
    authUrl.searchParams.append('scope', SCOPES);
    authUrl.searchParams.append('state', state);
    authUrl.searchParams.append('nonce', nonce);
    authUrl.searchParams.append('code_challenge', codeChallenge);
    authUrl.searchParams.append('code_challenge_method', 'S256');

    window.location.href = authUrl.toString();
  };

  return <button onClick={handleLogin}>Login with OIDC</button>;
}

function Callback() {
  useEffect(() => {
    const handleCallback = async () => {
      const params = new URLSearchParams(window.location.search);
      const code = params.get('code');
      const state = params.get('state');

      // Validate state
      if (state !== localStorage.getItem('state')) {
        console.error('State mismatch - possible CSRF attack');
        return;
      }

      try {
        // Exchange code for tokens
        const response = await axios.post(`${OIDC_PROVIDER}/oauth2/token`, null, {
          params: {
            grant_type: 'authorization_code',
            code: code,
            client_id: CLIENT_ID,
            client_secret: 'your-client-secret', // NEVER expose in frontend!
            redirect_uri: REDIRECT_URI,
            code_verifier: localStorage.getItem('pkce_verifier')
          }
        });

        const { access_token, id_token } = response.data;

        // Decode and verify ID token
        const idTokenDecoded = decodeToken(id_token);
        if (idTokenDecoded.nonce !== localStorage.getItem('nonce')) {
          console.error('Nonce mismatch');
          return;
        }

        // Store tokens
        localStorage.setItem('access_token', access_token);
        localStorage.setItem('id_token', id_token);
        localStorage.setItem('user', JSON.stringify(idTokenDecoded));

        // Redirect to dashboard
        window.location.href = '/dashboard';
      } catch (error) {
        console.error('Token exchange failed:', error);
      }
    };

    handleCallback();
  }, []);

  return <div>Processing login...</div>;
}

function Dashboard() {
  const user = JSON.parse(localStorage.getItem('user'));

  return (
    <div>
      <h1>Welcome, {user.name}</h1>
      <p>Email: {user.email}</p>
      <p>Groups: {user.groups.join(', ')}</p>
    </div>
  );
}
```

---

## Key Differences: Authelia vs Custom Spring Boot IDP

| Aspect | Authelia | Spring Boot IDP |
|--------|----------|-----------------|
| **Architecture** | Reverse proxy companion | Standalone service |
| **Session Management** | Cookie-based via reverse proxy | Token-based (JWT) |
| **Integration** | Forward Auth protocols | OIDC/OAuth2 standard |
| **User Storage** | LDAP, Files, Database | Database (custom) |
| **2FA Support** | Built-in (TOTP, WebAuthn, Duo) | Must implement |
| **Complexity** | Low - configuration based | High - code based |
| **Scalability** | Lightweight (< 30MB) | Standard Spring Boot |
| **Use Case** | Quick SSO for internal apps | Custom IDPwith business logic |

---

## Security Best Practices for Your IDP

1. **Always use HTTPS** for all endpoints
2. **Rotate JWT secrets** regularly
3. **Implement rate limiting** on login/token endpoints
4. **Use strong password hashing** (BCrypt, Argon2)
5. **Validate redirect URIs** strictly
6. **Implement PKCE** for public clients
7. **Use short expiration times** for authorization codes
8. **Store client secrets** securely (encrypted in database)
9. **Implement CORS** properly
10. **Log security events** (failed logins, token issuance)

---

## References

- [OpenID Connect Specification](https://openid.net/connect/)
- [OAuth 2.0 RFC 6749](https://tools.ietf.org/html/rfc6749)
- [JWT RFC 7519](https://tools.ietf.org/html/rfc7519)
- [Authelia Documentation](https://www.authelia.com/)
- [Spring Security OAuth2](https://spring.io/projects/spring-security-oauth2-resource-server)
