Skip to content

This project demonstrates a proper separation of concerns in OAuth2/OpenID Connect architecture by implementing a Spring Authorization Server that uses GitHub as an external Identity Provider (IdP) for user authentication.

Notifications You must be signed in to change notification settings

webcane/hello-spring-oauth2

Repository files navigation

Hello Spring Autho

Build

Overview

This project demonstrates a proper separation of concerns in OAuth2/OpenID Connect architecture by implementing a * Spring Authorization Server* that uses GitHub as an external Identity Provider (IdP) for user authentication.

The Problem It Solves

Instead of each application managing OAuth2 integrations with external IdPs (GitHub, Google, Azure AD, etc.), this architecture centralizes authentication:

┌──────────────┐         ┌─────────────────┐         ┌──────────────┐
│              │         │                 │         │              │
│  Client App  │────────▶│  Authorization  │────────▶│  GitHub IdP  │
│  (Resource   │  OAuth2 │     Server      │  OAuth2 │  (External)  │
│   Server)    │◀────────│  (This project) │◀────────│              │
│              │ own JWT │                 │   JWT   └──────────────┘
└──────────────┘         └─────────────────┘

Key Benefits

  1. Centralized Authentication: One place to manage IdP integration
  2. Separation of Concerns: Client applications only need to trust your Authorization Server
  3. Token Translation: Convert GitHub OAuth2 tokens into your own JWT tokens
  4. Consistent API: Same authentication flow for all client applications
  5. Easy to Scale: Architecture ready to add more IdPs in the future
  6. Enhanced Security: Control token format, lifetime, and claims centrally

Current Implementation

  • GitHub OAuth2 Login as the authentication provider
  • ✅ OAuth2 Authorization Server with OIDC support
  • ✅ PKCE (Proof Key for Code Exchange) support
  • ✅ JWT token generation with RSA signing
  • ✅ Standard Spring Security OAuth2 Login flow

Future Extensions (Planned)

This architecture can be extended to support:

  1. Federated Identity:

    • Custom OAuth2UserService to map external users to internal user model
    • FederatedUser entity to link multiple IdPs to one account
    • User profile management and account linking
  2. Additional Identity Providers:

    • Google OAuth2
    • Microsoft Azure AD / Entra ID
    • Okta / Auth0
    • Custom LDAP/Database authentication
    • SAML 2.0 providers

All extensions can be added without changing client applications!

How Authentication Works

Current Authentication Flow (OAuth2 Login with GitHub)

  1. User accesses a protected resource in the Client App
  2. Client App redirects to the Authorization Server (/oauth2/authorize)
  3. Authorization Server checks if user is authenticated:
    • If not, redirects to GitHub OAuth2 login (/oauth2/authorization/github)
  4. User logs in with GitHub credentials
  5. GitHub redirects back to Authorization Server with authorization code
  6. Authorization Server:
    • Exchanges code for GitHub access token
    • Retrieves user info from GitHub API
    • Creates an authenticated session using Spring Security's OAuth2User
    • Generates its own JWT access token for the client
  7. Client App receives JWT token from Authorization Server
  8. Client App validates JWT and grants access to resources

Key Point: The Authorization Server acts as an intermediary, translating GitHub authentication into JWT tokens that client applications can trust and validate.

JWT Access Token Structure

The Authorization Server issues JWT access tokens with the following claims:

{
  "sub": "github_username",
  "aud": "swagger-ui",
  "nbf": 1767003392,
  "scope": [
    "openid",
    "api.read"
  ],
  "iss": "https://localhost/auth",
  "exp": 1767006992,
  "iat": 1767003392,
  "jti": "27231829-a905-4c82-a043-c9ad44cdc6bz"
}

Claim Descriptions:

  • sub (Subject): Username from the external IdP (GitHub username)
  • aud (Audience): Client ID that the token was issued for (e.g., swagger-ui)
  • nbf (Not Before): Timestamp when the token becomes valid
  • scope: Granted OAuth2 scopes (e.g., openid, api.read)
  • iss (Issuer): Authorization Server URL (https://localhost/auth)
  • exp (Expiration): Timestamp when the token expires (default: 1 hour)
  • iat (Issued At): Timestamp when the token was issued
  • jti (JWT ID): Unique identifier for this token

The Resource Server validates these tokens by:

  1. Fetching public keys from the Authorization Server's JWKS endpoint (/oauth2/jwks)
  2. Verifying the JWT signature using RSA-2048
  3. Checking token expiration and audience claims
  4. Extracting scopes to determine granted authorities

Planned: Federated Identity (Future Enhancement)

A federated user concept will allow unified identity across multiple external IdPs: // Planned implementation FederatedUser

{
  "id": "uuid-1234-5678",
  "username": "john.doe",
  "email": "john@example.com",
  "linkedAccounts": [
    {
      "provider": "github",
      "externalId": "github-123",
      "linkedAt": "2024-01-15"
    },
    {
      "provider": "google",
      "externalId": "google-456",
      "linkedAt": "2024-02-20"
    }
  ]
}

This will require:

  • Custom OAuth2UserService implementation
  • Database entity for FederatedUser
  • Account linking logic
  • User profile management UI

Benefits:

  • Log in with different providers but maintain the same identity
  • Link multiple external accounts to one internal account
  • Switch between providers without losing access

Project Structure

hello-spring-oauth2/
├── hello-sample-sas/              # Spring Authorization Server
│   ├── src/main/java/cane/brothers/spring/authserver/
│   │   ├── App.java               # Main application entry point
│   │   ├── security/
│   │   │   └── SecurityConfig.java # OAuth2 & Security configuration
│   │   └── web/
│   │       └── DevToolsController.java # Development utilities
│   ├── src/main/resources/
│   │   └── application.yml        # Server configuration with GitHub IdP
│   ├── build.gradle               # Dependencies & build configuration
│   └── Dockerfile                 # Container image
│
├── hello-sample-app/              # Sample Resource Server (Client App)
│   ├── src/main/java/cane/brothers/spring/
│   │   ├── App.java               # Main application
│   │   ├── sample/                # Business logic & REST API
│   │   ├── security/              # JWT validation & authorities
│   │   └── swagger/               # API documentation with OAuth2
│   ├── src/main/resources/
│   │   └── application.yml        # JWT validation configuration
│   ├── build.gradle
│   └── Dockerfile
│
├── nginx/                         # Reverse proxy
│   ├── nginx.conf                 # HTTPS termination & routing
│   ├── certs/                     # SSL certificates
│   │   ├── server.crt
│   │   └── server.key
│   └── SETUP.md                   # Nginx configuration guide
│
├── compose.yaml                   # Docker Compose orchestration
├── Makefile                       # Convenient commands
├── .env                           # Environment variables (not in repo)
└── QUICK-REFERENCE.md             # Configuration reference

Key Components

Authorization Server (hello-sample-sas)

  • Purpose: Central authentication & token issuer
  • Technology: Spring Authorization Server 1.3+
  • Features:
    • OAuth2 Authorization Code flow with PKCE
    • OpenID Connect 1.0 (OIDC)
    • OAuth2 Client (for GitHub IdP integration)
    • JWT token generation with RSA keys
    • Session management
    • Health checks & actuator endpoints

Resource Server (hello-sample-app)

  • Purpose: Example application with protected API
  • Technology: Spring Boot 3.5+ with Spring Security
  • Features:
    • JWT validation from Authorization Server
    • Swagger UI with OAuth2 integration
    • Custom authority mapping from JWT claims
    • CORS configuration
    • Protected REST endpoints

Nginx Proxy

  • Purpose: HTTPS termination & request routing
  • Features:
    • SSL/TLS support
    • Path-based routing:
      • /auth → Authorization Server
      • /api → Resource Server
    • Header forwarding for proper OAuth2 redirects

Configuration

GitHub IdP Setup

  1. Register OAuth App in GitHub:

    • Go to: Settings → Developer settings → OAuth Apps → New OAuth App
    • Application name: Hello Spring Auth
    • Homepage URL: https://localhost/auth
    • Authorization callback URL: https://localhost/auth/login/oauth2/code/github
  2. Get Credentials:

    • Copy Client ID and Client Secret
  3. Configure Environment:

Create .env file in project root:

# Server ports (internal)
SAS_SERVER_PORT=9000
APP_SERVER_PORT=8080

# External URLs (browser-facing)
SAS_SERVER_EXTERNAL=https://localhost/auth
APP_SERVER=https://localhost/api

# GitHub OAuth2 credentials
SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GITHUB_CLIENT_ID=your_github_client_id
SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GITHUB_CLIENT_SECRET=your_github_client_secret

# Management endpoints
MANAGEMENT_ENDPOINTS_WEB_BASE_PATH=/management

SSL Certificates

For local development, you need SSL certificates for HTTPS:

# See nginx/SETUP.md for detailed instructions
cd nginx/certs
# Generate self-signed certificate (if not exists)
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
  -keyout server.key -out server.crt \
  -subj "/CN=localhost"

Running the Application

Prerequisites

  • Docker & Docker Compose
  • Make (optional, for convenience commands)
  • GitHub OAuth App credentials

Using Make Commands

# Start all services (build & run in detached mode)
make up

# View logs (follow mode)
make logs

# Stop services
make down

# Stop services and remove volumes
make downv

# Access Authorization Server container
make bash-sas

# Access Sample App container
make bash-app

Using Docker Compose Directly

# Start services
docker compose up --build --detach

# View logs
docker compose logs -f

# Stop services
docker compose down

# Stop and remove volumes
docker compose down --remove-orphans -v

Manual Local Development

Terminal 1 - Authorization Server

cd hello-sample-sas
./gradlew bootRun

Terminal 2 - Resource Server

cd hello-sample-app
./gradlew bootRun

Terminal 3 - Nginx Proxy

# Make sure nginx is installed
# On macOS: brew install nginx
cd nginx
nginx -c $(pwd)/nginx.conf -p $(pwd)

Testing the Setup

1. Health Checks

# Authorization Server
curl -k https://localhost/auth/management/health

# Resource Server
curl -k https://localhost/api/management/health

2. OpenID Configuration

# View Authorization Server metadata
curl -k https://localhost/auth/.well-known/openid-configuration | jq

3. Swagger UI with OAuth2

  1. Open browser: https://localhost/api/swagger-ui
  2. Click "Authorize" button
  3. Select scopes: openid, api.read
  4. Click "Authorize"
  5. Redirect to GitHub login
  6. Authorize the application
  7. You're authenticated! Try protected endpoints

4. Direct OAuth2 Flow

# Step 1: Get authorization code (open in browser)
https://localhost/auth/oauth2/authorize?response_type=code&client_id=swagger-ui&redirect_uri=https://localhost/api/swagger-ui/oauth2-redirect.html&scope=openid%20api.read&code_challenge=CHALLENGE&code_challenge_method=S256

# Step 2: Exchange code for token (use Postman/curl)
curl -X POST https://localhost/auth/oauth2/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code" \
  -d "code=YOUR_CODE" \
  -d "redirect_uri=https://localhost/api/swagger-ui/oauth2-redirect.html" \
  -d "client_id=swagger-ui" \
  -d "code_verifier=VERIFIER"

Key Endpoints

Authorization Server (Port 9000, Path /auth)

Endpoint Description
GET /auth/oauth2/authorize OAuth2 authorization endpoint
POST /auth/oauth2/token Token endpoint
GET /auth/oauth2/jwks JSON Web Key Set (public keys)
GET /auth/.well-known/openid-configuration OIDC discovery
GET /auth/userinfo OIDC user info endpoint
GET /auth/oauth2/authorization/github Redirect to GitHub login
GET /auth/login/oauth2/code/github GitHub callback URL

Resource Server (Port 8080, Path /api)

Endpoint Description
GET /api/swagger-ui Swagger UI with OAuth2
GET /api/v3/api-docs OpenAPI specification
GET /api/samples Protected API endpoint (requires JWT)

Architecture Decisions

Why Spring Authorization Server?

  1. Official Implementation: Spring Security's official OAuth2 server
  2. Production-Ready: Battle-tested, secure, maintained
  3. Flexible: Highly customizable for federated identity
  4. Standards-Compliant: OAuth2.1, OIDC 1.0, PKCE
  5. Integration: Seamless with Spring ecosystem

Why OAuth2 Login as Identity Provider?

Current approach: Using Spring Security OAuth2 Login to integrate with GitHub:

  1. Quick Setup: Minimal configuration required
  2. Standard Flow: Industry-standard OAuth2 authorization code flow
  3. Built-in Support: Spring Security handles token exchange, user info retrieval
  4. Extensible: Easy to add more providers (Google, Azure AD, etc.)

Limitation: Each IdP creates separate user identities without federated linking.

Future: Federated Identity

Planned enhancement to link multiple IdP accounts to a single user:

  1. User Convenience: Let users choose their preferred login method
  2. No Password Management: Delegate to trusted IdPs
  3. Single Identity: One user account, multiple login options
  4. Compliance: Meet enterprise SSO requirements
  5. Future-Proof: Easy to add new authentication methods

Implementation requires:

  • Custom OAuth2UserService for user mapping
  • FederatedUser entity and repository
  • Account linking logic
  • User profile management UI

Security Considerations

  • ✅ HTTPS required for all endpoints
  • ✅ PKCE enabled for public clients
  • ✅ JWT signing with RSA-2048 keys
  • ✅ Token expiration (1 hour default)
  • ✅ CORS properly configured
  • ✅ Forward headers strategy for proxy
  • ⚠️ Self-signed certificates (use real CA in production)
  • ⚠️ In-memory key storage (use persistent in production)
  • ⚠️ No user persistence yet (sessions only)

Adding More Identity Providers

Example: Adding Google IdP (Configuration Only)

You can add more OAuth2 providers using Spring Security's standard OAuth2 Login:

  1. Register app in Google Cloud Console
  2. Update application.yml in hello-sample-sas:
spring:
  security:
    oauth2:
      client:
        registration:
          github:
          # ... existing GitHub config
          google:
            provider: google
            client-id: ${GOOGLE_CLIENT_ID}
            client-secret: ${GOOGLE_CLIENT_SECRET}
            scope: openid,profile,email
        provider:
          google:
            issuer-uri: https://accounts.google.com
  1. Add environment variables to .env
  2. Update entry point in SecurityConfig.java (optional - to offer IdP selection)

Current Limitation: Without federated identity implementation, each provider creates a separate user session. Users logging in via GitHub and Google would be treated as different users, even with the same email.

Implementing Federated Identity (Roadmap)

To properly link multiple IdPs to the same user account, you need to implement:

1. Create FederatedUser entity:

@Entity
public class FederatedUser {
    @Id
    private UUID id;
    private String email;
    private String username;

    @OneToMany(mappedBy = "user")
    private Set<LinkedAccount> linkedAccounts;
}

@Entity
public class LinkedAccount {
    @Id
    private UUID id;
    private String provider; // "github", "google"
    private String externalId;
    private Instant linkedAt;

    @ManyToOne
    private FederatedUser user;
}

2. Implement custom OAuth2UserService:

@Service
public class FederatedUserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) {
        // Delegate to default implementation
        OAuth2UserService<OAuth2UserRequest, OAuth2User> delegate =
                new DefaultOAuth2UserService();
        OAuth2User oauth2User = delegate.loadUser(userRequest);

        // Extract provider and user info
        String provider = userRequest.getClientRegistration().getRegistrationId();
        String email = oauth2User.getAttribute("email");
        String externalId = oauth2User.getName();

        // Find or create federated user
        FederatedUser user = findOrCreateFederatedUser(email, provider, externalId);

        // Return custom user with federated identity
        return new FederatedOAuth2User(user, oauth2User);
    }

    private FederatedUser findOrCreateFederatedUser(String email, String provider, String externalId) {
        // Find existing user by email or linked account
        FederatedUser user = userRepository.findByEmail(email)
                .orElseGet(() -> createNewUser(email));

        // Link account if not already linked
        if (!user.hasLinkedAccount(provider, externalId)) {
            user.linkAccount(provider, externalId);
            userRepository.save(user);
        }

        return user;
    }
}

3. Register custom service in SecurityConfig:

@Bean
@Order(2)
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
    // ...existing config...

    http.oauth2Login(oauth2 -> oauth2
            .userInfoEndpoint(userInfo -> userInfo
                    .userService(federatedUserService)
            )
    );

    return http.build();
}

4. Update token generation to include federated user ID in JWT claims

Troubleshooting

Common Issues

1. SSL Certificate Errors

# Trust self-signed certificate in browser
# Or disable SSL verification (dev only):
curl -k https://localhost/...

2. GitHub OAuth Callback Mismatch

  • Verify GitHub OAuth App callback URL: https://localhost/auth/login/oauth2/code/github
  • Check .env variables match

3. Token Validation Fails

  • Ensure SAS_SERVER points to internal service: http://sas:9000/auth
  • Check JWKS endpoint is accessible: curl http://sas:9000/auth/oauth2/jwks

4. CORS Errors

  • Check management.endpoints.web.cors configuration
  • Verify nginx proxy headers

Debug Mode

Enable detailed logging in application.yml:

logging:
  level:
    org.springframework.security: TRACE
    org.springframework.security.oauth2: TRACE

Production Considerations

Before deploying to production:

  • Use real SSL certificates from trusted CA
  • Store client secrets in secure vault (not .env)
  • Implement persistent key storage (database or HSM)
  • Configure token refresh flow
  • Implement user consent screens
  • Add user profile management
  • Set up monitoring & alerting
  • Configure session clustering
  • Implement rate limiting
  • Add audit logging
  • Use production-ready database for authorization data
  • Implement federated identity with OAuth2UserService and FederatedUser entity

References

License

This is a sample project for educational purposes.

Contributing

Feel free to submit issues and enhancement requests!

About

This project demonstrates a proper separation of concerns in OAuth2/OpenID Connect architecture by implementing a Spring Authorization Server that uses GitHub as an external Identity Provider (IdP) for user authentication.

Topics

Resources

Stars

Watchers

Forks