diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..b6f0b5f --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,45 @@ +name: Build + +on: + push: + branches: [ main, master, develop, nginx-ssl ] + pull_request: + branches: [ main, master, develop ] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + cache: 'gradle' + + - name: Build SAS + working-directory: ./hello-sample-sas + run: | + chmod +x ./gradlew + ./gradlew build --no-daemon + + - name: Build APP (API) + working-directory: ./hello-sample-app + run: | + chmod +x ./gradlew + ./gradlew build --no-daemon + + - name: Upload build artifacts + if: success() + uses: actions/upload-artifact@v4 + with: + name: build-artifacts + path: | + hello-sample-sas/build/libs/*.jar + hello-sample-app/build/libs/*.jar + retention-days: 7 + diff --git a/CHANGES.md b/CHANGES.md new file mode 100644 index 0000000..41f05ad --- /dev/null +++ b/CHANGES.md @@ -0,0 +1,75 @@ +# Summary of Changes - Swagger UI OAuth2 Redirect URI Configuration + +## Changes Made + +### 1. Environment Variables (.env) +- Added `SAS_SERVER_EXTERNAL=https://localhost/auth` for browser-facing OAuth2 redirects +- Updated `APP_SERVER=https://localhost/api` to use external URL +- Kept `SAS_SERVER=http://sas:9000/auth` for internal service-to-service JWT validation + +### 2. Nginx Configuration (nginx/nginx.conf) +- Added `X-Forwarded-Host` header to both `/auth/` and `/api/` locations +- Added `X-Forwarded-Port 443` header to both locations +- These headers help Spring construct correct redirect URLs when behind a reverse proxy + +### 3. Application Configuration (hello-sample-app/src/main/resources/application.yml) +- Confirmed `forward-headers-strategy: framework` is enabled (allows Spring to use X-Forwarded headers) +- Updated `oauth.issuer-url` to use `${SAS_SERVER_EXTERNAL}` for browser redirects +- Updated `oAuthFlow.authorizationUrl` to use `${SAS_SERVER_EXTERNAL}/oauth2/authorize` +- Updated `oAuthFlow.tokenUrl` to use `${SAS_SERVER_EXTERNAL}/oauth2/token` +- Updated CORS `allowed-origins` to use `${SAS_SERVER_EXTERNAL}` instead of `${SAS_SERVER}` +- JWT validation still uses `${SAS_SERVER}` (internal URL) for jwk-set-uri and issuer-uri + +### 4. Authorization Server Configuration (hello-sample-sas/src/main/resources/application.yml) +- Confirmed `forward-headers-strategy: framework` is enabled +- Confirmed redirect-uris uses `${APP_SERVER}` which now points to external URL +- Updated CORS `allowed-origins` to use `${SAS_SERVER_EXTERNAL}` instead of `${SAS_SERVER}` + +## Why These Changes Were Necessary + +### The Problem +In Docker Compose with nginx reverse proxy: +- Services communicate internally using HTTP (e.g., `http://sas:9000/auth`) +- Browser accesses services through HTTPS nginx (e.g., `https://localhost/auth`) +- Swagger UI OAuth2 redirects must use the external URL that the browser can access +- JWT validation should use internal URL for better performance and reliability + +### The Solution +**Separate Internal and External URLs:** +- `SAS_SERVER` (internal): For service-to-service communication (JWT validation) +- `SAS_SERVER_EXTERNAL` (external): For browser-facing OAuth2 flows +- `APP_SERVER` (external): For browser redirects back to Swagger UI + +**Enable Proxy Header Support:** +- Spring Boot's `forward-headers-strategy: framework` makes it aware of proxy +- Nginx's `X-Forwarded-*` headers tell Spring the original request details +- This ensures Spring generates correct redirect URLs even when behind a proxy + +## How OAuth2 Flow Works Now + +1. **User opens Swagger UI**: `https://localhost/api/swagger-ui` +2. **Click Authorize**: Browser redirects to `https://localhost/auth/oauth2/authorize` +3. **User authenticates**: Via GitHub IDP +4. **Authorization server redirects back**: `https://localhost/api/swagger-ui/oauth2-redirect.html` +5. **Swagger UI exchanges code for token**: POST to `https://localhost/auth/oauth2/token` +6. **Token validation (backend)**: App validates token by calling `http://sas:9000/auth/oauth2/jwks` + +## Testing + +To test the configuration: + +```bash +# Rebuild and start services +docker-compose down +docker-compose up --build + +# Access Swagger UI +open https://localhost/api/swagger-ui + +# Click "Authorize" button and complete OAuth2 flow +``` + +## Documentation + +See [SWAGGER-OAUTH2-SETUP.md](./SWAGGER-OAUTH2-SETUP.md) for detailed explanation of the configuration. + diff --git a/Makefile b/Makefile index 83d715a..bf8c041 100644 --- a/Makefile +++ b/Makefile @@ -1,17 +1,20 @@ # DOCKER COMPOSE COMMANDS up: - @docker compose --project-name hello-spring-auth up --build --detach + @docker compose up --build --detach down: - @docker compose --project-name hello-spring-auth down + @docker compose down downv: - @docker compose --project-name hello-spring-auth down --remove-orphans -v + @docker compose down --remove-orphans -v logs: @docker compose logs -f +restart-nginx: + @docker compose restart nginx + # BASH ACCESS diff --git a/QUICK-REFERENCE.md b/QUICK-REFERENCE.md new file mode 100644 index 0000000..88ab7b6 --- /dev/null +++ b/QUICK-REFERENCE.md @@ -0,0 +1,84 @@ +# Swagger UI OAuth2 - Quick Reference + +## URLs + +### Browser Access (External) +- **Swagger UI**: https://localhost/api/swagger-ui +- **Authorization Server**: https://localhost/auth +- **API**: https://localhost/api + +### Internal Service Communication +- **JWT Validation**: http://sas:9000/auth + +## Key Configuration Points + +### ✅ What's Configured + +1. **Redirect URI**: `https://localhost/api/swagger-ui/oauth2-redirect.html` + - Configured in Swagger UI (app) + - Registered in OAuth2 client (sas) + +2. **OAuth2 Endpoints**: + - Authorization: `https://localhost/auth/oauth2/authorize` + - Token: `https://localhost/auth/oauth2/token` + - JWKS: `http://sas:9000/auth/oauth2/jwks` (internal) + +3. **PKCE**: Enabled (`use-pkce-with-authorization-code-grant: true`) + +4. **Scopes**: `openid`, `api.read` + +5. **Proxy Headers**: Enabled via `forward-headers-strategy: framework` + +## Environment Variables + +```bash +# Internal (service-to-service) +SAS_SERVER=http://sas:9000/auth + +# External (browser-facing) +SAS_SERVER_EXTERNAL=https://localhost/auth +APP_SERVER=https://localhost/api +``` + +## Quick Start + +```bash +# Start services +docker-compose up --build + +# Access Swagger UI +open https://localhost/api/swagger-ui + +# Test OAuth2 flow +1. Click "Authorize" button +2. Login with GitHub +3. Allow access +4. You should be redirected back to Swagger UI with access token +``` + +## Troubleshooting + +| Issue | Check | +|-------|-------| +| Redirect URI mismatch | Verify `APP_SERVER` in `.env` | +| CORS errors | Check `allowed-origins` uses external URLs | +| JWT validation fails | Verify `SAS_SERVER` uses internal URL | +| Wrong redirect URL | Check `forward-headers-strategy: framework` is set | +| SSL warnings | Accept self-signed certificate in browser | + +## Architecture + +``` +┌─────────┐ HTTPS ┌───────┐ +│ Browser │ ──────────────────────▶│ Nginx │ +└─────────┘ https://localhost └───────┘ + │ + ┌──────────────────┴──────────────────┐ + │ │ + HTTP HTTP + │ │ + ┌─────▼─────┐ ┌─────▼─────┐ + │ SAS │◀───── JWT Validate ────│ App │ + │ :9000 │ (internal) │ :8080 │ + └───────────┘ └───────────┘ +``` diff --git a/README.md b/README.md new file mode 100644 index 0000000..9299d2e --- /dev/null +++ b/README.md @@ -0,0 +1,665 @@ +# Spring Authorization Server with Federated Identity + +[![Build](https://github.com/webcane/hello-spring-oauth2/actions/workflows/build.yml/badge.svg)](https://github.com/webcane/hello-spring-oauth2/actions/workflows/build.yml) + +## 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: + +```json +{ + "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 + +```json +{ + "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: + +```bash +# 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: + +```bash +# 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 + +```bash +# 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 + +```bash +# 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 + +```bash +cd hello-sample-sas +./gradlew bootRun +``` + +#### Terminal 2 - Resource Server + +```bash +cd hello-sample-app +./gradlew bootRun +``` + +#### Terminal 3 - Nginx Proxy + +```bash +# 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 + +```bash +# Authorization Server +curl -k https://localhost/auth/management/health + +# Resource Server +curl -k https://localhost/api/management/health +``` + +### 2. OpenID Configuration + +```bash +# 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 + +```bash +# 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`: + +```yaml +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 +``` + +3. **Add environment variables** to `.env` +4. **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**: + +```java + +@Entity +public class FederatedUser { + @Id + private UUID id; + private String email; + private String username; + + @OneToMany(mappedBy = "user") + private Set 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`**: + +```java + +@Service +public class FederatedUserService implements OAuth2UserService { + + @Override + public OAuth2User loadUser(OAuth2UserRequest userRequest) { + // Delegate to default implementation + OAuth2UserService 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`**: + +```java + +@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** + +```bash +# 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`: + +```yaml +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 + +- [Spring Authorization Server Documentation](https://docs.spring.io/spring-authorization-server/reference/) +- [OAuth 2.1 Specification](https://oauth.net/2.1/) +- [OpenID Connect Core](https://openid.net/specs/openid-connect-core-1_0.html) +- [RFC 7636 - PKCE](https://tools.ietf.org/html/rfc7636) + +## License + +This is a sample project for educational purposes. + +## Contributing + +Feel free to submit issues and enhancement requests! + diff --git a/SWAGGER-OAUTH2-SETUP.md b/SWAGGER-OAUTH2-SETUP.md new file mode 100644 index 0000000..13159ff --- /dev/null +++ b/SWAGGER-OAUTH2-SETUP.md @@ -0,0 +1,166 @@ +# Swagger UI OAuth2 Configuration for Docker Compose + +## Overview + +This document explains how Swagger UI OAuth2 redirect URIs are configured to work with Docker Compose deployment behind an nginx reverse proxy. + +## Architecture + +``` +Browser → HTTPS (nginx:443) → HTTP (app:8080 or sas:9000) +``` + +## Key Concepts + +### 1. Two Types of URLs + +**Internal URLs** (service-to-service communication): +- Used for backend services to communicate with each other +- Example: `http://sas:9000/auth` (app validates JWT tokens) + +**External URLs** (browser-facing): +- Used by the browser to access services through nginx +- Example: `https://localhost/auth`, `https://localhost/api` +- Used for OAuth2 redirects and CORS + +### 2. Environment Variables + +In `.env` file: +```bash +# Internal - for JWT validation (app → sas) +SAS_SERVER=http://sas:9000/auth + +# External - for browser redirects +SAS_SERVER_EXTERNAL=https://localhost/auth +APP_SERVER=https://localhost/api +``` + +### 3. Configuration Files + +#### Application Configuration (`hello-sample-app/src/main/resources/application.yml`) + +```yaml +server: + forward-headers-strategy: framework # Essential for proxied environment + +spring: + security: + oauth2: + resourceserver: + jwt: + # Internal URL - app validates tokens against auth server + jwk-set-uri: ${SAS_SERVER}/oauth2/jwks + issuer-uri: ${SAS_SERVER} + +springdoc: + swagger-ui: + oauth: + # External URL - browser redirects + issuer-url: ${SAS_SERVER_EXTERNAL} + # External URL - where browser returns after OAuth2 flow + oauth2-redirect-url: ${APP_SERVER}/swagger-ui/oauth2-redirect.html + oAuthFlow: + # External URLs - browser initiates OAuth2 flow + authorizationUrl: ${SAS_SERVER_EXTERNAL}/oauth2/authorize + tokenUrl: ${SAS_SERVER_EXTERNAL}/oauth2/token +``` + +#### Authorization Server Configuration (`hello-sample-sas/src/main/resources/application.yml`) + +```yaml +server: + forward-headers-strategy: framework # Essential for proxied environment + +spring: + security: + oauth2: + authorizationserver: + client: + swagger-ui: + registration: + redirect-uris: + # External URL - must match Swagger UI redirect + - "${APP_SERVER}/swagger-ui/oauth2-redirect.html" +``` + +#### Nginx Configuration (`nginx/nginx.conf`) + +```nginx +location /api/ { + proxy_pass http://app:8080; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port 443; +} +``` + +The `X-Forwarded-*` headers tell Spring the external URL scheme and port. + +## OAuth2 Authorization Code Flow with PKCE + +1. User clicks "Authorize" in Swagger UI at `https://localhost/api/swagger-ui` +2. Browser redirects to Authorization Server: `https://localhost/auth/oauth2/authorize` +3. User authenticates (via GitHub) +4. Authorization Server redirects back to: `https://localhost/api/swagger-ui/oauth2-redirect.html` +5. Swagger UI exchanges authorization code for access token at: `https://localhost/auth/oauth2/token` +6. Swagger UI uses access token for API calls + +## Troubleshooting + +### Redirect URI Mismatch + +**Symptom**: `redirect_uri_mismatch` error + +**Solution**: Ensure that: +1. `APP_SERVER` in `.env` is set to the external URL: `https://localhost/api` +2. Authorization server's registered redirect URI matches: `${APP_SERVER}/swagger-ui/oauth2-redirect.html` +3. Swagger UI's `oauth2-redirect-url` matches the same + +### CORS Errors + +**Symptom**: Browser blocks requests due to CORS + +**Solution**: +1. Set `allowed-origins` to include external URLs +2. Use `SAS_SERVER_EXTERNAL` and `APP_SERVER` (not internal URLs) + +### Wrong Issuer URI + +**Symptom**: JWT validation fails with issuer mismatch + +**Solution**: +- For JWT validation: use `SAS_SERVER` (internal URL) +- For browser OAuth: use `SAS_SERVER_EXTERNAL` (external URL) + +### SSL Certificate Issues + +**Symptom**: Browser shows SSL warnings + +**Solution**: Accept the self-signed certificate or add it to your system trust store + +## Testing + +1. Start the services: + ```bash + docker-compose up --build + ``` + +2. Open Swagger UI: + ``` + https://localhost/api/swagger-ui + ``` + +3. Click "Authorize" button +4. Complete OAuth2 flow (login with GitHub) +5. Test protected endpoints + +## Key Takeaways + +✅ Use **internal URLs** for service-to-service communication (JWT validation) +✅ Use **external URLs** for browser-facing OAuth2 flows +✅ Enable `forward-headers-strategy: framework` for Spring to respect proxy headers +✅ Configure nginx to pass `X-Forwarded-*` headers +✅ Match redirect URIs exactly between Swagger UI config and OAuth2 client registration + diff --git a/compose.yaml b/compose.yaml index 248da1b..34b9731 100644 --- a/compose.yaml +++ b/compose.yaml @@ -1,64 +1,66 @@ +name: ${COMPOSE_PROJECT_NAME:-hello-spring-auth} + services: -# sas-gradle-builder: -# image: gradle:8.14.2-jdk21-ubi-minimal -# container_name: sas-gradle-builder -# working_dir: /home/gradle/project -# volumes: -# - ./hello-sample-sas:/home/gradle/project # Mount the current directory to `/app` in the container -# - sas-data:/home/gradle/project/build/libs # Persist built JAR files -# - sas-gradle-cache:/home/gradle/.gradle # Reuse Gradle cache for faster builds -# command: gradle bootJar - hello-sample-sas: + nginx: + image: nginx:1.25-alpine + container_name: nginx-proxy + ports: + - "443:443" + volumes: + - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro + - ./nginx/certs:/etc/nginx/certs:ro + networks: + network: { } + + sas: build: context: ./hello-sample-sas dockerfile: Dockerfile container_name: auth-server - ports: - - "$SAS_SERVER_PORT:$SAS_SERVER_PORT" + expose: + - "$SAS_SERVER_PORT" env_file: - .env environment: + - SAS_SERVER_EXTERNAL=https://localhost/auth + - APP_SERVER=https://localhost/api - GITHUB_CLIENT_ID=$SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GITHUB_CLIENT_ID - GITHUB_CLIENT_SECRET=$SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GITHUB_CLIENT_SECRET healthcheck: - test: [ "CMD-SHELL", "curl -f http://localhost:$SAS_SERVER_PORT$MANAGEMENT_ENDPOINTS_WEB_BASE_PATH/health || exit 1" ] + test: [ "CMD-SHELL", "curl -f http://localhost:$SAS_SERVER_PORT/auth$MANAGEMENT_ENDPOINTS_WEB_BASE_PATH/health || exit 1" ] interval: 20s timeout: 10s retries: 3 start_period: 10s networks: - hello-network: { } + network: { } - hello-sample-app: + app: build: context: ./hello-sample-app dockerfile: Dockerfile container_name: sample-app - ports: - - "$APP_SERVER_PORT:$APP_SERVER_PORT" + expose: + - "$APP_SERVER_PORT" env_file: - .env + environment: + - SAS_SERVER=http://sas:9000/auth + - SAS_SERVER_EXTERNAL=https://localhost/auth + - APP_SERVER=https://localhost/api depends_on: - hello-sample-sas: + sas: condition: service_healthy healthcheck: - test: [ "CMD-SHELL", "curl -f http://localhost:$APP_SERVER_PORT$MANAGEMENT_ENDPOINTS_WEB_BASE_PATH/health || exit 1" ] + test: [ "CMD-SHELL", "curl -f http://localhost:$APP_SERVER_PORT/api$MANAGEMENT_ENDPOINTS_WEB_BASE_PATH/health || exit 1" ] interval: 20s timeout: 10s retries: 3 start_period: 10s restart: on-failure -# volumes: -# - app-gradle-cache:/home/gradle/.gradle # Reuse Gradle cache for faster builds networks: - hello-network: { } - -#volumes: -# sas-gradle-cache: -# sas-data: -# app-gradle-cache: -# app-data: + network: { } networks: - hello-network: \ No newline at end of file + network: \ No newline at end of file diff --git a/etc/keystore.p12 b/etc/keystore.p12 deleted file mode 100644 index e119d22..0000000 Binary files a/etc/keystore.p12 and /dev/null differ diff --git a/etc/truststore.jks b/etc/truststore.jks deleted file mode 100644 index e61af7f..0000000 Binary files a/etc/truststore.jks and /dev/null differ diff --git a/hello-sample-app/src/main/java/cane/brothers/spring/sample/Sample.java b/hello-sample-app/src/main/java/cane/brothers/spring/sample/Sample.java index ec87f73..f032d8c 100644 --- a/hello-sample-app/src/main/java/cane/brothers/spring/sample/Sample.java +++ b/hello-sample-app/src/main/java/cane/brothers/spring/sample/Sample.java @@ -2,4 +2,5 @@ import java.util.UUID; +// Simple record representing a Sample entity record Sample(UUID sampleId, Long sampleKey, String status) {} \ No newline at end of file diff --git a/hello-sample-app/src/main/java/cane/brothers/spring/sample/SampleApi.java b/hello-sample-app/src/main/java/cane/brothers/spring/sample/SampleApi.java index 18d799a..65c8bfb 100644 --- a/hello-sample-app/src/main/java/cane/brothers/spring/sample/SampleApi.java +++ b/hello-sample-app/src/main/java/cane/brothers/spring/sample/SampleApi.java @@ -9,20 +9,22 @@ import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.http.ResponseEntity; -@Tag(name = "Sample API", description = "API для работы с Sample") +@Tag(name = "Sample API", description = "API for working with Samples") interface SampleApi { - @Operation(summary = "Получить Sample по sampleKey") + @Operation(summary = "Get Sample by sampleKey") @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Sample найден", content = @Content(schema = @Schema(implementation = Sample.class))), - @ApiResponse(responseCode = "404", description = "Sample не найден") + @ApiResponse(responseCode = "200", description = "Sample found", + content = @Content(schema = @Schema(implementation = Sample.class))), + @ApiResponse(responseCode = "404", description = "Sample not found") }) ResponseEntity getSample( - @Parameter(description = "Ключ sample", required = true) Long sampleKey); + @Parameter(description = "Sample key", required = true) Long sampleKey); - @Operation(summary = "Создать новый Sample") + @Operation(summary = "Create new Sample") @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Sample создан", content = @Content(schema = @Schema(implementation = Sample.class))) + @ApiResponse(responseCode = "200", description = "Sample created", + content = @Content(schema = @Schema(implementation = Sample.class))) }) ResponseEntity createSample( - @Parameter(description = "Sample для создания", required = true) Sample sample); -} \ No newline at end of file + @Parameter(description = "Sample to create", required = true) Sample sample); +} \ No newline at end of file diff --git a/hello-sample-app/src/main/java/cane/brothers/spring/sample/SampleController.java b/hello-sample-app/src/main/java/cane/brothers/spring/sample/SampleController.java index 34fae4c..312abd0 100644 --- a/hello-sample-app/src/main/java/cane/brothers/spring/sample/SampleController.java +++ b/hello-sample-app/src/main/java/cane/brothers/spring/sample/SampleController.java @@ -7,7 +7,7 @@ import java.util.UUID; @RestController -@RequestMapping("/api/sample") +@RequestMapping("/sample") class SampleController implements SampleApi { private final SampleRepo repo; diff --git a/hello-sample-app/src/main/java/cane/brothers/spring/security/JwtDecoderConfig.java b/hello-sample-app/src/main/java/cane/brothers/spring/security/JwtDecoderConfig.java deleted file mode 100644 index d08a239..0000000 --- a/hello-sample-app/src/main/java/cane/brothers/spring/security/JwtDecoderConfig.java +++ /dev/null @@ -1,160 +0,0 @@ -package cane.brothers.spring.security; - -import com.nimbusds.jose.jwk.source.JWKSource; -import com.nimbusds.jose.jwk.source.JWKSourceBuilder; -import com.nimbusds.jose.proc.SecurityContext; -import com.nimbusds.jose.util.DefaultResourceRetriever; -import com.nimbusds.jose.util.ResourceRetriever; -import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties; -import org.springframework.boot.web.client.RestTemplateBuilder; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.oauth2.jwt.JwtDecoder; -import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; -import org.springframework.web.client.RestTemplate; - -import java.net.MalformedURLException; -import java.time.Duration; - -@Configuration -public class JwtDecoderConfig { - - - @Bean - // RestTemplateBuilder builder - public JwtDecoder jwtDecoder(OAuth2ResourceServerProperties properties) throws MalformedURLException { -// JWKSource jwkSource = JWKSourceBuilder.create(new URL(properties.getJwt().getJwkSetUri())) -// .cache(false) -// .rateLimited(false) -// .refreshAheadCache(false) -// // .retrying(true) -// .build(); -// JWSKeySelector jwsKeySelector = -// new JWSVerificationKeySelector<>(JWS_ALGORITHMS, jwkSource); - - // Создаем источник JWK -// JwkSetSource jwkSetSource = new RefreshAheadCachingJwkSetSource<>( -// new ImmutableJwkSet<>(JwkSet.load(new URL(properties.getJwt().getJwkSetUri()))), -// Duration.ofMinutes(15) // Период обновления кэша -// ); -// RestOperations rest = builder -// .connectTimeout(Duration.ofSeconds(15)) -// .readTimeout(Duration.ofSeconds(30)) -// .build(); -// -// var jwtDecoder = NimbusJwtDecoder -//// .withIssuerLocation(properties.getJwt().getIssuerUri()) -// .withJwkSetUri(properties.getJwt().getJwkSetUri()) -// .restOperations(rest) -//// .jwtProcessorCustomizer(processor -> -//// processor.setJWSKeySelector(jwsKeySelector)) -// .build(); -// jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(properties.getJwt().getIssuerUri())); -// return jwtDecoder; - - -// URL jwkSetURL = new URL(properties.getJwt().getJwkSetUri()); -// var connectionTimeout = 20000; -// var readTimeout = 20000; -// ResourceRetriever resourceRetriever = new DefaultResourceRetriever(connectionTimeout, readTimeout); -// JWKSource jwkSource = new RemoteJWKSet<>(jwkSetURL, resourceRetriever); -// -// Set jwsAlgs = new HashSet<>(); -// jwsAlgs.addAll(JWSAlgorithm.Family.RSA); -// ConfigurableJWTProcessor jwtProcessor = new DefaultJWTProcessor<>(); -// JWSKeySelector jwsKeySelector = new JWSVerificationKeySelector<>(jwsAlgs, jwkSource); -// jwtProcessor.setJWSKeySelector(jwsKeySelector); -// // Override the default Nimbus claims set verifier as NimbusJwtDecoder handles it -// // instead -// jwtProcessor.setJWTClaimsSetVerifier((claims, context) -> { -// }); -// -// return new NimbusJwtDecoder(jwtProcessor); -// return NimbusJwtDecoder.withJwkSetUri(properties.getJwt().getJwkSetUri()) -// .restOperations(restTemplate(builder, -// Duration.ofMillis(connectionTimeout), -// Duration.ofMillis(readTimeout))) -// .build(); -// } - -// -// var jwkSetUri = properties.getJwt().getJwkSetUri(); - - // 1. Создаем основной источник JWKSet, который будет обращаться к удаленному URI -// JWKSource remoteJWKSet = new RemoteJWKSet(new URL(jwkSetUri)); - - // 2. Создаем кэширующий источник, который будет обновлять ключи заранее - // Мы используем RefreshAheadCachingJWKSetSource, который обертывает RemoteJWKSet - // и обеспечивает "опережающее" кэширование. -// JWKSource refreshAheadJWKSource = new RefreshAheadCachingJWKSetSource( -// remoteJWKSet, TIME_TO_LIVE_SECONDS, -// new DefaultJWKSetCache(TIME_TO_LIVE_SECONDS, REFRESH_AHEAD_TIME_SECONDS, TimeUnit.SECONDS) -// ); - -// JWKSetCache jwkSetCache = new JWKSetCache() { -// private final Cache cache = new ConcurrentMapCache( -// "jwkSetCache"/*, CacheBuilder.newBuilder()...build()*/, false); -// -// @Override -// public void put(JWKSet jwkSet) { -// this.cache.put(jwkSetUri, jwkSet); -// } -// -// @Override -// public JWKSet get() { -// return this.cache.get(jwkSetUri, JWKSet.class); -// } -// -// @Override -// public boolean requiresRefresh() { -// return this.cache.get(jwkSetUri) == null; -// } -// }; - -// var jwkSetCache = new ConcurrentMapCache("jwkSetCache", CacheBuilder.newBuilder() -// .expireAfterWrite(Duration.ofMinutes(30)) -// .build().asMap(), false); -// -// // 3. Создаем NimbusJwtDecoder, используя наш кастомный JWKSource -// NimbusJwtDecoder jwtDecoder = new NimbusJwtDecoder(refreshAheadJWKSource); - - - // Создаем RemoteJWKSet, который по умолчанию имеет кэширование - // Он автоматически считывает заголовки Cache-Control и Last-Modified -// JWKSource jwkSource = new RemoteJWKSet<>(new URL(properties.getJwt().getJwkSetUri())); - - // Если вы хотите настроить кэш вручную (не рекомендуется, если заголовки HTTP работают) - // Вы можете создать CachingJWKSetSource - // CachingJWKSetSource cachingJwkSource = new CachingJWKSetSource<>(jwkSource, 1, 10, TimeUnit.MINUTES); -// RefreshAheadCachingJWKSetSource - -// Cache cache = new NoOpCache("sas"); - -// JWKSourceBuilder.create(new NimbusJwtDecoder.JwkSetUriJwtDecoderBuilder.SpringJWKSource<>(this.restOperations, this.cache, jwkSetUri)) -// .refreshAheadCache(false) -// .rateLimited(false) -// .cache(this.cache instanceof NoOpCache) -// .build(); - - -// NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder -// .withJwkSetUri(jwkSetUri) -// .cache(null) -// .restOperations() -// .jwtProcessorCustomizer((processor) -> { -// JWSVerificationKeySelector selector = -// new JWSVerificationKeySelector<>(jwsAlgs, jwkSource); -// processor.setJWSKeySelector(selector); -// return processor; -// }) -// .jwsAlgorithm(SignatureAlgorithm.RS256) -// .build(); - - // настроить JWS-валидатор, чтобы он ожидал конкретный алгоритм -// jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(jwkSetUri)); - - return NimbusJwtDecoder.withIssuerLocation(properties.getJwt().getIssuerUri()) - .restOperations() - .build(); - } -} diff --git a/hello-sample-app/src/main/java/cane/brothers/spring/security/RestConfig.java b/hello-sample-app/src/main/java/cane/brothers/spring/security/RestConfig.java deleted file mode 100644 index 3e8816c..0000000 --- a/hello-sample-app/src/main/java/cane/brothers/spring/security/RestConfig.java +++ /dev/null @@ -1,81 +0,0 @@ -package cane.brothers.spring.security; - -import org.apache.hc.client5.http.classic.HttpClient; -import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; -import org.apache.hc.client5.http.impl.classic.HttpClients; -import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; -import org.apache.hc.core5.util.TimeValue; -import org.springframework.boot.web.client.RestTemplateBuilder; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.client.ClientHttpRequestFactory; -import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; -import org.springframework.web.client.RestOperations; -import org.springframework.web.client.RestTemplate; - -import javax.net.ssl.SSLContext; -import javax.net.ssl.TrustManagerFactory; -import java.security.KeyStore; -import java.security.cert.CertificateException; -import java.time.Duration; - -Configuration -public class RestConfig { - - @Bean - public RestOperations restOperations() throws CertificateException { - - // Load the truststore - KeyStore trustStore = KeyStore.getInstance("PKCS12"); - try (var trustStoreStream = getClass().getResourceAsStream("/truststore.p12")) { - if (trustStoreStream == null) { - throw new CertificateException("Truststore not found"); - } - trustStore.load(trustStoreStream, "your-truststore-password".toCharArray()); - } - - // Create SSLContext with the truststore - var trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); - trustManagerFactory.init(trustStore); - var sslContext = SSLContext.getInstance("TLS"); - sslContext.init(null, trustManagerFactory.getTrustManagers(), null); - - - return new RestTemplateBuilder() - .requestFactory(() -> new HttpComponentsClientHttpRequestFactory( - HttpClient.newBuilder() - .sslContext(sslContext) - .build())); - } - - private RestTemplate restTemplate(RestTemplateBuilder builder, ClientHttpRequestFactory clientHttpRequestFactory) { - return builder -// .connectTimeout(connectionTimeout) -// .readTimeout(readTimeout) - .requestFactory(() -> clientHttpRequestFactory) - .build(); - } - - @Bean - public CloseableHttpClient httpClient() { - // Configure connection manager with TTL - PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(TimeValue.ofMinutes(5)); - connectionManager.setMaxTotal(100); // Maximum total connections - connectionManager.setDefaultMaxPerRoute(20); // Maximum connections per route - - // Build the HttpClient - return HttpClients.custom() - .setConnectionManager(connectionManager) - .build(); - } - - @Bean - public ClientHttpRequestFactory clientHttpRequestFactory() { - var connectionTimeout = 5000; - var readTimeout = 5000; - HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(); - factory.setConnectTimeout(Duration.ofMillis(connectionTimeout)); - factory.setReadTimeout(Duration.ofMillis(readTimeout)); - return factory; - } -} diff --git a/hello-sample-app/src/main/java/cane/brothers/spring/security/SecurityConfig.java b/hello-sample-app/src/main/java/cane/brothers/spring/security/SecurityConfig.java index 481d7bc..cac1339 100644 --- a/hello-sample-app/src/main/java/cane/brothers/spring/security/SecurityConfig.java +++ b/hello-sample-app/src/main/java/cane/brothers/spring/security/SecurityConfig.java @@ -1,7 +1,6 @@ package cane.brothers.spring.security; -import com.nimbusds.jose.JWSAlgorithm; import lombok.RequiredArgsConstructor; import org.springframework.boot.actuate.autoconfigure.endpoint.web.CorsEndpointProperties; import org.springframework.context.annotation.Bean; @@ -13,31 +12,16 @@ import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; -import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.HttpStatusEntryPoint; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; -import java.util.Collections; -import java.util.Set; - @Configuration @EnableWebSecurity @RequiredArgsConstructor class SecurityConfig { - // Время жизни кэша, в секундах -// private static final long TIME_TO_LIVE_SECONDS = 3600; // 1 час - - // Настройки кэширования токенов - // Время, за которое нужно обновить кэш до его истечения, в секундах -// private static final long REFRESH_AHEAD_TIME_SECONDS = 600; // 10 минут -// private static final long CACHE_REFRESH_TIME_SECONDS = 600; // 10 минут - // Поддерживаемые алгоритмы JWS -// private static final Set JWS_ALGORITHMS = Collections.singleton(JWSAlgorithm.RS256); - // private final JwtAuthenticationConverter jwtAuthenticationConverter; - private final JwtDecoder jwtDecoder; @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { @@ -51,22 +35,22 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti http.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); http.authorizeHttpRequests(auth -> auth - // Разрешить доступ к эндпоинтам Swagger UI без авторизации + // Allow access to Swagger UI endpoints without authorization .requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll() - .requestMatchers("/api/sample/**").authenticated() + .requestMatchers("/sample/**").authenticated() .anyRequest().permitAll() ); // throw Access Denied exception only once - http.exceptionHandling(e -> e.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.FORBIDDEN))); + http.exceptionHandling(e -> + e.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.FORBIDDEN))); // disables session creation on Spring Security - http.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); + http.sessionManagement(s -> + s.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); - // authentication + // authentication. validate access tokens http.oauth2ResourceServer(c -> -// c.jwt(j -> j.jwtAuthenticationConverter(jwtAuthenticationConverter)) - c.jwt(jwt -> jwt.decoder(jwtDecoder)) -// c.jwt(Customizer.withDefaults()) + c.jwt(Customizer.withDefaults()) ); return http.build(); diff --git a/hello-sample-app/src/main/resources/application.yml b/hello-sample-app/src/main/resources/application.yml index b795b91..d53def5 100644 --- a/hello-sample-app/src/main/resources/application.yml +++ b/hello-sample-app/src/main/resources/application.yml @@ -16,11 +16,11 @@ logging: management: endpoints: web: - base-path: /management +# base-path: /management exposure: include: health, info cors: - allowed-origins: "${APP_SERVER},${SAS_SERVER}" + allowed-origins: "${APP_SERVER:https://localhost/api},${SAS_SERVER_EXTERNAL:https://localhost/auth}" allowed-methods: "OPTIONS,GET,POST,PUT,DELETE,HEAD" allowed-headers: "Accept,Authorization,Content-Type,X-Requested-With,Origin" allow-credentials: true @@ -28,32 +28,24 @@ management: server: port: ${APP_SERVER_PORT:8080} - ssl: - bundle: sas-client + servlet: + context-path: /api + forward-headers-strategy: framework spring: application: name: hello-sample-app - ssl: - bundle: - jks: - sas-client: - truststore: - location: classpath:truststore.p12 - password: "${TRUSTSTORE_PASSWORD}" - type: PKCS12 security: oauth2: resourceserver: jwt: jws-algorithms: RS256 - trusted-ssl-bundle: truststore-client - jwk-set-uri: ${SAS_SERVER}/oauth2/jwks - issuer-uri: ${SAS_SERVER} + jwk-set-uri: ${SAS_SERVER:http://hello-sample-sas:9000/auth}/oauth2/jwks + issuer-uri: ${SAS_SERVER_EXTERNAL:https://localhost/auth} # Увеличиваем таймаут на установление соединения (в миллисекундах) - jwk-set-uri-connect-timeout: 30000 + jwk-set-uri-connect-timeout: 3000 # Увеличиваем таймаут на чтение ответа - jwk-set-uri-read-timeout: 30000 + jwk-set-uri-read-timeout: 3000 # Устанавливаем таймаут для обновления кэша. # Если сервер авторизации долго генерирует ключи, # это может помочь. @@ -66,13 +58,13 @@ springdoc: path: /swagger-ui docExpansion: none oauth: - issuer-url: ${SAS_SERVER} + issuer-url: ${SAS_SERVER_EXTERNAL:https://localhost/auth} appName: hello-spring-oauth2 client-id: "swagger-ui" use-pkce-with-authorization-code-grant: true scopes: openid,api.read - oauth2-redirect-url: ${APP_SERVER}/swagger-ui/oauth2-redirect.html + oauth2-redirect-url: ${APP_SERVER:https://localhost/api}/swagger-ui/oauth2-redirect.html show-actuator: false oAuthFlow: - authorizationUrl: ${SAS_SERVER}/oauth2/authorize - tokenUrl: ${SAS_SERVER}/oauth2/token + authorizationUrl: ${SAS_SERVER_EXTERNAL:https://localhost/auth}/oauth2/authorize + tokenUrl: ${SAS_SERVER_EXTERNAL:https://localhost/auth}/oauth2/token diff --git a/hello-sample-app/src/main/resources/truststore.p12 b/hello-sample-app/src/main/resources/truststore.p12 deleted file mode 100644 index c2de2a6..0000000 Binary files a/hello-sample-app/src/main/resources/truststore.p12 and /dev/null differ diff --git a/hello-sample-sas/src/main/java/cane/brothers/spring/authserver/security/SecurityConfig.java b/hello-sample-sas/src/main/java/cane/brothers/spring/authserver/security/SecurityConfig.java index 084d96c..a993662 100644 --- a/hello-sample-sas/src/main/java/cane/brothers/spring/authserver/security/SecurityConfig.java +++ b/hello-sample-sas/src/main/java/cane/brothers/spring/authserver/security/SecurityConfig.java @@ -10,7 +10,9 @@ import com.nimbusds.jose.proc.SecurityContext; import com.nimbusds.jwt.proc.ConfigurableJWTProcessor; import com.nimbusds.jwt.proc.DefaultJWTProcessor; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.actuate.autoconfigure.endpoint.web.CorsEndpointProperties; +import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; @@ -21,11 +23,10 @@ import org.springframework.security.core.session.SessionRegistry; import org.springframework.security.core.session.SessionRegistryImpl; import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.JwtValidators; import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; -import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration; import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer; import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; -import org.springframework.security.oauth2.server.authorization.settings.TokenSettings; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; import org.springframework.security.web.session.HttpSessionEventPublisher; @@ -37,7 +38,6 @@ import java.security.KeyPairGenerator; import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPublicKey; -import java.time.Duration; import java.util.HashSet; import java.util.Set; import java.util.UUID; @@ -48,23 +48,37 @@ public class SecurityConfig { @Bean @Order(1) + // Security filter chain for the authorization server endpoints (OAuth2 and OIDC) public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = OAuth2AuthorizationServerConfigurer.authorizationServer(); + + // Apply the authorization server configuration to the HttpSecurity + // Handles: + // - /oauth2/authorize, + // - /oauth2/token, + // - /oauth2/jwks, + // - /.well-known/openid-configuration - OIDC discovery + // - /oauth2/introspect, + // - /oauth2/revoke endpoints http.securityMatcher(authorizationServerConfigurer.getEndpointsMatcher()); http.with(authorizationServerConfigurer, sas -> sas.oidc(Customizer.withDefaults()) // Enable OpenID Connect 1.0 ); + + // Require authentication for all requests to the authorization server endpoints http.authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated() ); + // Enable CORS with default settings (defined over application properties) http.cors(Customizer.withDefaults()); // Redirect to the OAuth 2.0 Login endpoint when not authenticated // from the authorization endpoint http.exceptionHandling(exceptions -> exceptions .defaultAuthenticationEntryPointFor( + // redirect to GitHub OAuth2 login page. (OAuth2 Login as Federated Identity Provider) new LoginUrlAuthenticationEntryPoint("/oauth2/authorization/github"), new MediaTypeRequestMatcher(MediaType.TEXT_HTML) ) @@ -74,6 +88,7 @@ public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity h @Bean @Order(2) + // default security filter chain for other endpoints public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests(authorize -> authorize .anyRequest().authenticated()); @@ -92,19 +107,19 @@ public AuthorizationServerSettings authorizationServerSettings() { return AuthorizationServerSettings.builder().build(); } + // to track logged in users sessions @Bean public SessionRegistry sessionRegistry() { return new SessionRegistryImpl(); } + // to publish session events (session created, destroyed) @Bean public HttpSessionEventPublisher httpSessionEventPublisher() { return new HttpSessionEventPublisher(); } - // for signing access tokens - // configure JWK Set endpoint - // required for OpenID Connect 1.0 endpoints + // for signing access tokens. (configure JWK Set endpoint. required for OpenID Connect 1.0 endpoints) @Bean public JWKSource jwkSource() { RSAKey rsaKey = generateRsaKey(); @@ -112,6 +127,7 @@ public JWKSource jwkSource() { return new ImmutableJWKSet<>(jwkSet); } + // RSA key pair setting for signing access tokens private static RSAKey generateRsaKey() { RSAKey rsaKey; try { @@ -124,25 +140,39 @@ private static RSAKey generateRsaKey() { .privateKey(privateKey) .keyID(UUID.randomUUID().toString()) .build(); - } - catch (Exception ex) { + } catch (Exception ex) { throw new IllegalStateException(ex); } return rsaKey; } - // for decoding signed access tokens + // JWT processor for processing and verifying JWT tokens @Bean - public JwtDecoder jwtDecoder(JWKSource jwkSource) { + public ConfigurableJWTProcessor jwtProcessor(JWKSource jwkSource) { Set jwsAlgs = new HashSet<>(JWSAlgorithm.Family.RSA); ConfigurableJWTProcessor jwtProcessor = new DefaultJWTProcessor<>(); JWSKeySelector jwsKeySelector = new JWSVerificationKeySelector<>(jwsAlgs, jwkSource); jwtProcessor.setJWSKeySelector(jwsKeySelector); - // Override the default Nimbus claims set verifier as NimbusJwtDecoder handles it - // instead + + // Override the default Nimbus claims set verifier as NimbusJwtDecoder handles it instead jwtProcessor.setJWTClaimsSetVerifier((claims, context) -> { }); - return new NimbusJwtDecoder(jwtProcessor); + return jwtProcessor; + } + + // for decoding signed access tokens + @Bean + public JwtDecoder jwtDecoder(ConfigurableJWTProcessor jwtProcessor, + @Value("${SAS_SERVER}") String issuerUri) { + NimbusJwtDecoder decoder = new NimbusJwtDecoder(jwtProcessor); +// NimbusJwtDecoder decoder = NimbusJwtDecoder +// .withIssuerLocation(issuerUri) +// .jwtProcessorCustomizer(proc -> proc = jwtProcessor) +// .build(); + // validate issuer, signature, expiration time +// decoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(issuerUri)); + + return decoder; } @Bean diff --git a/hello-sample-sas/src/main/java/cane/brothers/spring/authserver/web/DevToolsController.java b/hello-sample-sas/src/main/java/cane/brothers/spring/authserver/web/DevToolsController.java deleted file mode 100644 index 316e8bf..0000000 --- a/hello-sample-sas/src/main/java/cane/brothers/spring/authserver/web/DevToolsController.java +++ /dev/null @@ -1,14 +0,0 @@ -package cane.brothers.spring.authserver.web; - -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -class DevToolsController { - - @GetMapping("/.well-known/appspecific/com.chrome.devtools.json") - public ResponseEntity handleDevToolsRequest() { - return ResponseEntity.ok("{}"); - } -} diff --git a/hello-sample-sas/src/main/resources/application.yml b/hello-sample-sas/src/main/resources/application.yml index fc37ee2..18896ed 100644 --- a/hello-sample-sas/src/main/resources/application.yml +++ b/hello-sample-sas/src/main/resources/application.yml @@ -10,11 +10,11 @@ logging: management: endpoints: web: - base-path: /management +# base-path: /management exposure: include: health, info cors: - allowed-origins: "${APP_SERVER},${SAS_SERVER}" + allowed-origins: "${APP_SERVER:https://localhost/api},${SAS_SERVER_EXTERNAL:https://localhost/auth}" allowed-methods: "OPTIONS,GET,POST,PUT,DELETE,HEAD" allowed-headers: "Accept,Authorization,Content-Type,X-Requested-With,Origin" allow-credentials: true @@ -22,23 +22,13 @@ management: server: port: ${SAS_SERVER_PORT:9000} - ssl: - bundle: sas-server + servlet: + context-path: /auth + forward-headers-strategy: framework spring: application: name: hello-sample-sas - ssl: - bundle: - jks: - sas-server: - key: - alias: hello-sample-server - password: "${KEYSTORE_PASSWORD}" - keystore: - location: classpath:keystore.p12 - password: "${KEYSTORE_PASSWORD}" - type: PKCS12 security: oauth2: authorizationserver: @@ -51,9 +41,9 @@ spring: authorization-grant-types: - "authorization_code" redirect-uris: - - "${APP_SERVER}/swagger-ui/oauth2-redirect.html" + - "${APP_SERVER:https://localhost/api}/swagger-ui/oauth2-redirect.html" post-logout-redirect-uris: - - "${APP_SERVER}/" + - "${APP_SERVER:https://localhost/api}/" scopes: - "openid" - "api.read" diff --git a/hello-sample-sas/src/main/resources/keystore.p12 b/hello-sample-sas/src/main/resources/keystore.p12 deleted file mode 100644 index fa0e056..0000000 Binary files a/hello-sample-sas/src/main/resources/keystore.p12 and /dev/null differ diff --git a/nginx/README-ru.txt b/nginx/README-ru.txt new file mode 100644 index 0000000..84c0d11 --- /dev/null +++ b/nginx/README-ru.txt @@ -0,0 +1,8 @@ +# Для работы схемы: +# 1. Все внешние запросы идут через https://localhost/ (порт 443) +# 2. Для доступа к сервисам используйте: +# https://localhost/sas/ → hello-sample-sas (http внутри docker) +# https://localhost/app/ → hello-sample-app (http внутри docker) +# 3. Сертификат самоподписанный, для теста в браузере потребуется принять исключение. +# 4. Внутри docker-сети сервисы общаются по http, SSL только на nginx. + diff --git a/nginx/README.txt b/nginx/README.txt new file mode 100644 index 0000000..af0f4df --- /dev/null +++ b/nginx/README.txt @@ -0,0 +1,4 @@ +# Self-signed certificate for local development +# Run the following command to generate: +# openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout server.key -out server.crt -subj "/CN=localhost" + diff --git a/nginx/SETUP.md b/nginx/SETUP.md new file mode 100644 index 0000000..2a3cc48 --- /dev/null +++ b/nginx/SETUP.md @@ -0,0 +1,51 @@ +# SSL via Nginx - Configuration + +## Architecture + +``` +Browser (HTTPS) → Nginx (SSL termination) → Services (HTTP inside Docker) + ↓ + ┌──────┴──────┐ + ↓ ↓ + https://localhost/auth https://localhost/api + ↓ ↓ + hello-sample-sas:9000 hello-sample-app:8080 + (HTTP + context-path=/auth) (HTTP + context-path=/api) +``` + +## Key Points + +1. **SSL at Nginx level**: + - Certificates in `nginx/certs/` (server.crt, server.key) + - Port 443 is only exposed externally on nginx + +2. **Servlet Context Path**: + - hello-sample-sas: `context-path=/auth` + - hello-sample-app: `context-path=/api` + - This prevents path conflicts + +3. **URL mapping**: + - External: `https://localhost/auth/*` → Internal: `http://hello-sample-sas:9000/auth/*` + - External: `https://localhost/api/*` → Internal: `http://hello-sample-app:8080/api/*` + +4. **OAuth2 Redirect URLs**: + - Use public HTTPS URLs via nginx + - Internal calls (jwk-set-uri, issuer-uri) use HTTP inside docker + +## Service Access + +- Auth Server: https://localhost/auth/ +- Sample App: https://localhost/api/ +- Swagger UI: https://localhost/api/swagger-ui/index.html +- Health Checks: + - https://localhost/auth/management/health + - https://localhost/api/management/health + +## Startup + +```bash +docker compose up --build +``` + +On first connection the browser will ask you to accept the self-signed certificate. + diff --git a/nginx/certs/server.crt b/nginx/certs/server.crt new file mode 100644 index 0000000..f84d464 --- /dev/null +++ b/nginx/certs/server.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDCTCCAfGgAwIBAgIUSXNDtwGh8VYBsNA2q+nE7z9bTq4wDQYJKoZIhvcNAQEL +BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI1MTIyOTA3NTUwOVoXDTI2MTIy +OTA3NTUwOVowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAqcHE4Mk44Dg4vu3+ht89mMszh4yl45KFdabDUQMrEYM2 +IYmhIgp1fcCDanGdtIM3kBLLm/hRTaw6bI4hBd7XZYcaYDMVkQJbcd4KURcI8cR5 +2oHmpw59AFLV8F2YGDka234ULwFLDRP+MRCoPgB98G7V84cLANTcVY7Zddv901nu +5Scelkb1eG/2avM/wU+0J5QwKeyImUIT1+eenk2ZblXxQryo4e5aMgjmhVP+HFk7 +bvtcrnEcsWqlfDPPBdakvxtI5TE5G+yVZAB3CwoHiOmFdTk2xgLSzl2CgGfQRm9U +c0+ixJstlWsdV/Ri6c/2HTfAfuIR4rl8ax1Dwd8FXQIDAQABo1MwUTAdBgNVHQ4E +FgQUtGuAJC/ZBdj5Gn/S3+TByvxH8QgwHwYDVR0jBBgwFoAUtGuAJC/ZBdj5Gn/S +3+TByvxH8QgwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAFLIC +6sWK/xmYmVYz7du7HDpteD0lFEKqt7Nz50IjoTBEz3n0LEfE4JinG8QvkLYDZrjn +nVqFP5PiUlECqlVYrlH/pGSAJFkyTXpUHijtjAcO+0HlBVxjPFIZ3RR4TUAVp64D +SiTJgQ25oKW78dAZXiqz8uG9UwnzpD17ar/otn2UJX8k8rpx8RLEIVI0zp6hCWtp +3IhrqCBaTtJvxSFB4rRd9OmhKAzzbiL+PHYzY8vI62V1Xew1IsPleYJptQhqAgvI +hZWo6tdIAfS7/SDLXxW7tWc/RqjBgfbZX4x1ZlLBl/qHyvuNfwzJMG8MiqYmFkGz +oq7fkd6mHdmVagu/KQ== +-----END CERTIFICATE----- diff --git a/nginx/certs/server.key b/nginx/certs/server.key new file mode 100644 index 0000000..452d382 --- /dev/null +++ b/nginx/certs/server.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCpwcTgyTjgODi+ +7f6G3z2YyzOHjKXjkoV1psNRAysRgzYhiaEiCnV9wINqcZ20gzeQEsub+FFNrDps +jiEF3tdlhxpgMxWRAltx3gpRFwjxxHnageanDn0AUtXwXZgYORrbfhQvAUsNE/4x +EKg+AH3wbtXzhwsA1NxVjtl12/3TWe7lJx6WRvV4b/Zq8z/BT7QnlDAp7IiZQhPX +556eTZluVfFCvKjh7loyCOaFU/4cWTtu+1yucRyxaqV8M88F1qS/G0jlMTkb7JVk +AHcLCgeI6YV1OTbGAtLOXYKAZ9BGb1RzT6LEmy2Vax1X9GLpz/YdN8B+4hHiuXxr +HUPB3wVdAgMBAAECggEADsSuQZYP7iXF/gpLceVbAP9wmLLKPc2h8bXT2Sjq5seg +/nLwQztgtFN6u1huDWW7ADw6XXPRcu3wWUBWLCISYCFMUKExF5/6X6IfCKX1376l +kTZq4A65Hj2WoiYqVLUnGoBR9jLpGhaqrw8Ra+90BWZHE7wkX2qlToYycff1EZ1l +ip4RgpXCsQu8Q6rIuK9hAexpktLVd+Ju38SFQ4OnLExjW4p/FSl1EdJhWunk7C88 +K7l/HtADJ0zKlDE05HhUoorYwcOrJzEMAZFP8cer0yov+uwss0nmb/WbSaC8ulDa +nY4zERyLcSsXFrCueKYIlEdxQsz0jezVFcK8e6ZbawKBgQDW9kXJj8J4jGwZExAC +uMLSh9A/efwTDQys/z2Cz33locPkzvjhKKpCXwU0nb7x5sHUueCpNXPybgI8ec/2 +Zrlr1UQBkueGtI6dfiyVhhyH/XoIc5G8hTkcWmUZo8rSUeTM72/Yw4gqa5HAZdRB +lHK91irnlmNwgbu+VzO9EjboswKBgQDKKjYTdsGsxaajKByDkstaCJbQuuBwZZOA +Yrz3JSLfPhN9W4wqVygcE8kdRVh7/EqVP6MdmnqNGQB4ly1mi0IUtPixjQshvaJj +RadFk2cmvSmG4S8pRthRUi5Hcf74p5P75rVaRyLtWs/D6ZUQ89FSfEGaghElqTf6 +Wjv/UP3BrwKBgF93zJKyCBplsvSH5MpwqAW8T56BXJRRbVm/md/oqu87IrcRvLKy +zrrfXH57uHvSki8Zxk8f8Diw5slZCCVUhfEALE3OoojO06/ag458m1tCFdp/CTCC +slSHSPNULRWvTUA+7puEa4r7byXVk6j0dukcnr1vqwYid/EW5WGJH13FAoGAIJl9 +7tWPlZSpslWdg3oAYJxR9Yas+nLmviUt44yRev4/lk9U4t77EMv/+kBcbGHahQal +/vgSGv6VHN0D7S03kq88CyV7Tg2OSgPJXWbPk2edcqqNOFK8PyDJZav0OZSMQGqL +g+tErpGePzFDYGBwuKRgz9F5gmEvLaevVRRyVvECgYEAxpHV0YCouG7aOfrv8XRQ +7AEH79du84jYrltnSZBPwrMiwvKRzu21H4Cd9Is7ac0+g/CjvJq/31KBqJkZxfvh +vN/R5GT2hQudpwyYavvoUKkPPfQyFD6BBwFe7WboWvUDUhFIaRspLTzQ0U5p2n7X +7dG+5WHwnxDa1aRxDrz5u9U= +-----END PRIVATE KEY----- diff --git a/nginx/nginx.conf b/nginx/nginx.conf new file mode 100644 index 0000000..00edab5 --- /dev/null +++ b/nginx/nginx.conf @@ -0,0 +1,57 @@ +events { + worker_connections 1024; +} + +http { + server { + listen 80; + server_name localhost; + return 301 https://$host$request_uri; + } + + server { + listen 443 ssl; + server_name localhost; + + ssl_certificate /etc/nginx/certs/server.crt; + ssl_certificate_key /etc/nginx/certs/server.key; + + # use Docker's internal DNS resolver for hostname lookups. + resolver 127.0.0.11 valid=30s; + + location /.well-known/ { + default_type application/json; + return 200 '{}'; + } + + location /auth/ { + # dynamically resolve the "sas" + # resolve the hostnames at request time (runtime) rather than at configuration parsing time (startup) + set $upstream_sas sas; + proxy_pass http://$upstream_sas:9000; + # proxy_pass http://host.docker.internal:8000; # local + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port 443; + proxy_intercept_errors off; + } + + location /api/ { + # dynamically resolve the "app" + # resolve the hostnames at request time (runtime) rather than at configuration parsing time (startup) + set $upstream_app app; + proxy_pass http://$upstream_app:8080; +# proxy_pass http://host.docker.internal:8080; # local + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port 443; + proxy_intercept_errors off; + } + } +}