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.
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 └──────────────┘
└──────────────┘ └─────────────────┘
- Centralized Authentication: One place to manage IdP integration
- Separation of Concerns: Client applications only need to trust your Authorization Server
- Token Translation: Convert GitHub OAuth2 tokens into your own JWT tokens
- Consistent API: Same authentication flow for all client applications
- Easy to Scale: Architecture ready to add more IdPs in the future
- Enhanced Security: Control token format, lifetime, and claims centrally
- ✅ 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
This architecture can be extended to support:
-
Federated Identity:
- Custom
OAuth2UserServiceto map external users to internal user model FederatedUserentity to link multiple IdPs to one account- User profile management and account linking
- Custom
-
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!
- User accesses a protected resource in the Client App
- Client App redirects to the Authorization Server (
/oauth2/authorize) - Authorization Server checks if user is authenticated:
- If not, redirects to GitHub OAuth2 login (
/oauth2/authorization/github)
- If not, redirects to GitHub OAuth2 login (
- User logs in with GitHub credentials
- GitHub redirects back to Authorization Server with authorization code
- 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
- Client App receives JWT token from Authorization Server
- 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.
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 validscope: 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 issuedjti(JWT ID): Unique identifier for this token
The Resource Server validates these tokens by:
- Fetching public keys from the Authorization Server's JWKS endpoint (
/oauth2/jwks) - Verifying the JWT signature using RSA-2048
- Checking token expiration and audience claims
- Extracting scopes to determine granted authorities
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
OAuth2UserServiceimplementation - 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
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
- 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
- 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
- Purpose: HTTPS termination & request routing
- Features:
- SSL/TLS support
- Path-based routing:
/auth→ Authorization Server/api→ Resource Server
- Header forwarding for proper OAuth2 redirects
-
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
-
Get Credentials:
- Copy
Client IDandClient Secret
- Copy
-
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=/managementFor 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"- Docker & Docker Compose
- Make (optional, for convenience commands)
- GitHub OAuth App credentials
# 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# 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 -vcd hello-sample-sas
./gradlew bootRuncd hello-sample-app
./gradlew bootRun# Make sure nginx is installed
# On macOS: brew install nginx
cd nginx
nginx -c $(pwd)/nginx.conf -p $(pwd)# Authorization Server
curl -k https://localhost/auth/management/health
# Resource Server
curl -k https://localhost/api/management/health# View Authorization Server metadata
curl -k https://localhost/auth/.well-known/openid-configuration | jq- Open browser: https://localhost/api/swagger-ui
- Click "Authorize" button
- Select scopes:
openid,api.read - Click "Authorize"
- Redirect to GitHub login
- Authorize the application
- You're authenticated! Try protected endpoints
# 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"| 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 |
| 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) |
- Official Implementation: Spring Security's official OAuth2 server
- Production-Ready: Battle-tested, secure, maintained
- Flexible: Highly customizable for federated identity
- Standards-Compliant: OAuth2.1, OIDC 1.0, PKCE
- Integration: Seamless with Spring ecosystem
Current approach: Using Spring Security OAuth2 Login to integrate with GitHub:
- Quick Setup: Minimal configuration required
- Standard Flow: Industry-standard OAuth2 authorization code flow
- Built-in Support: Spring Security handles token exchange, user info retrieval
- Extensible: Easy to add more providers (Google, Azure AD, etc.)
Limitation: Each IdP creates separate user identities without federated linking.
Planned enhancement to link multiple IdP accounts to a single user:
- User Convenience: Let users choose their preferred login method
- No Password Management: Delegate to trusted IdPs
- Single Identity: One user account, multiple login options
- Compliance: Meet enterprise SSO requirements
- Future-Proof: Easy to add new authentication methods
Implementation requires:
- Custom
OAuth2UserServicefor user mapping FederatedUserentity and repository- Account linking logic
- User profile management UI
- ✅ 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)
You can add more OAuth2 providers using Spring Security's standard OAuth2 Login:
- Register app in Google Cloud Console
- Update
application.ymlinhello-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- Add environment variables to
.env - 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.
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
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
.envvariables match
3. Token Validation Fails
- Ensure
SAS_SERVERpoints 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.corsconfiguration - Verify nginx proxy headers
Enable detailed logging in application.yml:
logging:
level:
org.springframework.security: TRACE
org.springframework.security.oauth2: TRACEBefore 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
OAuth2UserServiceandFederatedUserentity
- Spring Authorization Server Documentation
- OAuth 2.1 Specification
- OpenID Connect Core
- RFC 7636 - PKCE
This is a sample project for educational purposes.
Feel free to submit issues and enhancement requests!