diff --git a/plans/phase2-jwt-fix/plan.md b/plans/phase2-jwt-fix/plan.md new file mode 100644 index 00000000..863dc85e --- /dev/null +++ b/plans/phase2-jwt-fix/plan.md @@ -0,0 +1,161 @@ +# Phase 2 JWT Fix: RS256 with JWKS Support + +## Problem Statement +The current Phase 2 implementation incorrectly uses HS256 (HMAC with shared secret) for JWT signing and custom JWT validation logic. This violates the plan specification which requires: +- RS256 (RSA asymmetric signing) instead of HS256 (HMAC symmetric signing) +- JWKS endpoint for public key distribution +- Spring Security OAuth2 Resource Server for validation (not custom code) +- Nimbus JWT library for signing/validation + +## Plan Overview + +### 1. Auth Service Changes + +#### 1.1 Update JwtTokenProvider +**File**: `/test-apps/crypto-trading/auth/src/main/java/org/elasticsoftware/cryptotrading/auth/security/jwt/JwtTokenProvider.java` + +**Changes**: +- Replace manual JWT creation with Nimbus JWT library +- Use RS256 signing algorithm (RSA) +- For development/testing: Generate RSA key pair programmatically +- For production: Support loading private key from configuration +- Sign JWTs with private key using `JWSSigner` +- Remove HMAC-SHA256 code completely + +**Configuration Properties**: +- `app.jwt.private-key` (optional): PEM-encoded RSA private key for production +- If not provided: Generate RSA key pair for dev/test + +#### 1.2 Add JWKS Endpoint +**New File**: `/test-apps/crypto-trading/auth/src/main/java/org/elasticsoftware/cryptotrading/auth/security/controller/JwksController.java` + +**Purpose**: +- Expose `/.well-known/jwks.json` endpoint +- Return public key in JWKS format +- Allow Commands/Queries services to validate JWT signatures + +#### 1.3 RSA Key Pair Management +**New File**: `/test-apps/crypto-trading/auth/src/main/java/org/elasticsoftware/cryptotrading/auth/security/jwt/RsaKeyProvider.java` + +**Purpose**: +- Generate or load RSA key pair +- Provide private key for signing +- Provide public key for JWKS endpoint +- Support configuration-based key loading + +### 2. Commands Service Changes + +#### 2.1 Remove Custom JWT Validation +**Delete File**: `/test-apps/crypto-trading/commands/src/main/java/org/elasticsoftware/cryptotrading/security/jwt/JwtAuthenticationManager.java` + +#### 2.2 Update SecurityConfig +**File**: `/test-apps/crypto-trading/commands/src/main/java/org/elasticsoftware/cryptotrading/security/config/SecurityConfig.java` + +**Changes**: +- Remove custom `JwtAuthenticationManager` +- Remove custom `AuthenticationWebFilter` +- Use Spring Security OAuth2 Resource Server configuration: + ```java + .oauth2ResourceServer(oauth2 -> oauth2 + .jwt(jwt -> jwt + .jwkSetUri("http://auth-service/.well-known/jwks.json") + ) + ) + ``` +- Configure `ReactiveJwtAuthenticationConverter` for claims mapping + +**Configuration Properties**: +- `spring.security.oauth2.resourceserver.jwt.jwk-set-uri`: JWKS endpoint URL + +### 3. Queries Service Changes + +#### 3.1 Remove Custom JWT Validation +**Delete File**: `/test-apps/crypto-trading/queries/src/main/java/org/elasticsoftware/cryptotrading/security/jwt/JwtAuthenticationManager.java` + +#### 3.2 Update SecurityConfig +**File**: `/test-apps/crypto-trading/queries/src/main/java/org/elasticsoftware/cryptotrading/security/config/SecurityConfig.java` + +**Changes**: Same as Commands service (see 2.2) + +### 4. Configuration Updates + +#### 4.1 Auth Service application.yml +Add: +```yaml +app: + jwt: + # Optional: For production, provide PEM-encoded RSA private key + # private-key: | + # -----BEGIN PRIVATE KEY----- + # ... + # -----END PRIVATE KEY----- +``` + +#### 4.2 Commands Service application.yml +Update: +```yaml +spring: + security: + oauth2: + resourceserver: + jwt: + jwk-set-uri: ${AUTH_SERVICE_URL:http://localhost:8080}/.well-known/jwks.json +``` + +#### 4.3 Queries Service application.yml +Update: Same as Commands service (see 4.2) + +### 5. Test Updates + +Update test configurations to: +- Use generated RSA keys for testing +- Configure test JWKS endpoint +- Update test security configurations + +## Implementation Steps + +1. **Auth Service**: + - Create `RsaKeyProvider` component + - Rewrite `JwtTokenProvider` with Nimbus JWT + - Create `JwksController` + - Update application.yml + +2. **Commands Service**: + - Delete `JwtAuthenticationManager` + - Rewrite `SecurityConfig` to use OAuth2 Resource Server + - Update application.yml + - Update `TestSecurityConfig` + +3. **Queries Service**: + - Delete `JwtAuthenticationManager` + - Rewrite `SecurityConfig` to use OAuth2 Resource Server + - Update application.yml + - Update `TestSecurityConfig` + +4. **Testing**: + - Compile all modules + - Run unit tests + - Verify JWKS endpoint works + - Verify JWT validation works + +## Key Dependencies + +All required dependencies are already present: +- `spring-security-oauth2-jose` (contains Nimbus JWT) +- `spring-security-oauth2-resource-server` +- `spring-boot-starter-security` + +## Security Benefits + +1. **Asymmetric Signing**: Auth service keeps private key, resource servers only have public key +2. **No Shared Secrets**: Eliminates risk of secret leakage across services +3. **Standard JWKS**: Industry-standard key distribution mechanism +4. **Spring Security Integration**: Leverages battle-tested validation logic +5. **Production Ready**: Can easily integrate with GCP Service Account keys + +## Backward Compatibility + +This is a breaking change: +- Existing HS256 tokens will NOT work +- All services must be updated together +- Users must re-authenticate after deployment diff --git a/test-apps/crypto-trading/auth/README.md b/test-apps/crypto-trading/auth/README.md new file mode 100644 index 00000000..0b66a2e0 --- /dev/null +++ b/test-apps/crypto-trading/auth/README.md @@ -0,0 +1,198 @@ +# Auth Service + +Authentication service for the Crypto Trading Akces Test Application. + +## Overview + +This module handles OAuth 2.0 authentication and JWT token generation for the Crypto Trading application. It is deployed separately from the Commands and Queries services to provide security isolation. + +## Phase 1 Implementation Status + +**Completed:** +- ✅ Basic module structure created +- ✅ Spring Boot application class (`AuthServiceApplication`) +- ✅ Spring Security OAuth2 Client dependencies added +- ✅ Spring Security OAuth2 Resource Server dependencies added +- ✅ Spring Security OAuth2 JOSE dependencies added (includes Nimbus JWT library) +- ✅ Database support (Spring Data JDBC, PostgreSQL, Liquibase) +- ✅ Kafka integration +- ✅ Akces Framework client library for command submission +- ✅ Basic application.yml configuration +- ✅ Package structure for security components + +**Pending (Phase 2+):** +- OAuth2 security configuration +- JWT token provider with GCP Service Account +- OAuth2 user service +- Success/failure handlers +- Account database model and query service +- REST controllers for auth endpoints (/v1/auth/*) +- Liquibase database schema migrations +- Integration tests + +## Architecture + +The Auth Service is responsible for: + +1. **OAuth 2.0 Authentication Flow**: Handling OAuth login with Google (and other providers in the future) +2. **JWT Token Generation**: Issuing JWT access and refresh tokens using GCP Service Account (RS256) +3. **User Account Management**: Creating new accounts via Akces command bus +4. **Account Query Service**: Looking up existing accounts by OAuth provider and email + +## Package Structure + +``` +org.elasticsoftware.cryptotrading.auth/ +├── AuthServiceApplication.java # Spring Boot main class +├── security/ +│ ├── config/ # Security configuration classes +│ ├── jwt/ # JWT token provider +│ ├── handler/ # OAuth success/failure handlers +│ ├── service/ # OAuth user service +│ ├── query/ # Account query service +│ └── model/ # OAuth user info models +└── web/ + ├── v1/ # Versioned REST controllers + └── dto/ # Data transfer objects +``` + +## Configuration + +### Environment Variables + +The following environment variables are required (configured in `application.yml`): + +- `GOOGLE_CLIENT_ID`: Google OAuth 2.0 client ID +- `GOOGLE_CLIENT_SECRET`: Google OAuth 2.0 client secret +- `DATABASE_URL`: PostgreSQL database URL +- `DATABASE_USERNAME`: Database username +- `DATABASE_PASSWORD`: Database password +- `KAFKA_BOOTSTRAP_SERVERS`: Kafka bootstrap servers +- `SCHEMA_REGISTRY_URL`: Confluent Schema Registry URL +- `GCP_SERVICE_ACCOUNT_KEY`: GCP Service Account JSON key (Phase 2+) +- `GCP_SERVICE_ACCOUNT_EMAIL`: GCP Service Account email (Phase 2+) + +## Dependencies + +### Key Dependencies Added + +- **Spring Security OAuth2 Client**: OAuth 2.0 authentication flow +- **Spring Security OAuth2 Resource Server**: JWT validation (Nimbus-based) +- **Spring Security OAuth2 JOSE**: JWT/JWS/JWK support (includes Nimbus JOSE+JWT) +- **Spring Data JDBC**: Database access for account queries +- **PostgreSQL**: Database driver +- **Liquibase**: Database schema management +- **Akces Client**: Command submission to Commands service +- **Akces Query Support**: Query model support + +### GCP Dependencies (Phase 2+) + +The following dependencies will be added in Phase 2 for JWT signing: + +- `com.google.auth:google-auth-library-oauth2-http` - GCP Service Account credentials +- `com.google.cloud:google-cloud-secretmanager` - Secret Manager for key storage (optional) + +These are currently commented out in Phase 1 to avoid dependency convergence issues. + +## Building + +Build the auth module: + +```bash +cd test-apps/crypto-trading/auth +mvn clean install +``` + +Build all modules: + +```bash +cd test-apps/crypto-trading +mvn clean install +``` + +## Running + +**Note**: The auth service cannot be fully run yet as Phase 2 implementation is required. Phase 1 only sets up the foundation. + +Once Phase 2 is complete, run with: + +```bash +cd test-apps/crypto-trading/auth +mvn spring-boot:run +``` + +The service will start on port 8080 (configurable via `server.port` in application.yml). + +## API Endpoints (Planned) + +The following endpoints will be implemented in Phase 2+: + +- `GET /v1/auth/login` - Initiate OAuth flow +- `GET /v1/auth/callback` - OAuth callback endpoint (returns JWT tokens) +- `POST /v1/auth/refresh` - Refresh access token +- `GET /v1/auth/user` - Get authenticated user profile +- `POST /v1/auth/logout` - Logout (optional token revocation) + +## Database Schema (Planned) + +Liquibase migration will create the following table in Phase 3: + +```sql +CREATE TABLE accounts ( + user_id VARCHAR(255) PRIMARY KEY, + email VARCHAR(255) NOT NULL, + first_name VARCHAR(255), + oauth_provider VARCHAR(50) NOT NULL, + oauth_provider_id VARCHAR(255) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + UNIQUE (oauth_provider, oauth_provider_id) +); + +CREATE INDEX idx_email ON accounts(email); +``` + +## Testing + +Tests will be added in Phase 2+: + +- Unit tests for JWT token provider +- Unit tests for OAuth user service +- Unit tests for database models +- Integration tests with mock OAuth provider +- End-to-end tests for complete OAuth flow + +## Next Steps + +See the [Gmail OAuth Authentication Plan](../../../../plans/gmail-oauth-authentication.md) for complete implementation details. + +**Phase 2** will include: +- JWT token provider implementation +- OAuth2 security configuration +- OAuth2 user service +- Success/failure handlers + +**Phase 3** will include: +- Account database model +- Account query service +- Liquibase migrations + +**Phase 4** will include: +- REST API controllers +- Response DTOs +- Error handling + +## License + +Copyright 2022 - 2025 The Original Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/test-apps/crypto-trading/auth/pom.xml b/test-apps/crypto-trading/auth/pom.xml new file mode 100644 index 00000000..a5e7af74 --- /dev/null +++ b/test-apps/crypto-trading/auth/pom.xml @@ -0,0 +1,159 @@ + + + + + 4.0.0 + + org.elasticsoftwarefoundation.akces + akces-crypto-trading-parent + 0.11.0-SNAPSHOT + + akces-crypto-trading-auth + Elastic Software Foundation :: Akces :: Test Apps :: Crypto Trading :: Auth Service + Authentication service for Crypto Trading Akces Test Application + + + + + + org.elasticsoftwarefoundation.akces + akces-crypto-trading-aggregates + commands + ${project.version} + + + org.elasticsoftwarefoundation.akces + akces-crypto-trading-aggregates + events + ${project.version} + + + org.elasticsoftwarefoundation.akces + akces-api + + + org.elasticsoftwarefoundation.akces + akces-client + + + org.elasticsoftwarefoundation.akces + akces-query-support + + + + + org.springframework.boot + spring-boot-starter-webflux + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-logging + + + org.springframework.boot + spring-boot-autoconfigure + + + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-oauth2-client + + + org.springframework.security + spring-security-oauth2-resource-server + + + org.springframework.security + spring-security-oauth2-jose + + + + + org.springframework.boot + spring-boot-starter-data-jdbc + + + org.postgresql + postgresql + runtime + + + org.springframework.boot + spring-boot-liquibase + + + + + org.springframework.kafka + spring-kafka + + + + + + + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + org.elasticsoftware.cryptotrading.auth.AuthServiceApplication + + + + + process-aot + + + org.elasticsoftware.cryptotrading.auth.AuthServiceApplication + + + + + + + + diff --git a/test-apps/crypto-trading/auth/src/main/java/org/elasticsoftware/cryptotrading/auth/AuthServiceApplication.java b/test-apps/crypto-trading/auth/src/main/java/org/elasticsoftware/cryptotrading/auth/AuthServiceApplication.java new file mode 100644 index 00000000..766ac0e2 --- /dev/null +++ b/test-apps/crypto-trading/auth/src/main/java/org/elasticsoftware/cryptotrading/auth/AuthServiceApplication.java @@ -0,0 +1,43 @@ +/* + * Copyright 2022 - 2025 The Original Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.elasticsoftware.cryptotrading.auth; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * Auth Service Application - Handles OAuth authentication and JWT token generation. + * + *

This service is responsible for: + *

+ * + *

This is a separate module from Commands and Queries services to provide + * security isolation and independent deployment. + */ +@SpringBootApplication +public class AuthServiceApplication { + + public static void main(String[] args) { + SpringApplication.run(AuthServiceApplication.class, args); + } +} diff --git a/test-apps/crypto-trading/auth/src/main/java/org/elasticsoftware/cryptotrading/auth/security/config/SecurityConfig.java b/test-apps/crypto-trading/auth/src/main/java/org/elasticsoftware/cryptotrading/auth/security/config/SecurityConfig.java new file mode 100644 index 00000000..20ce4038 --- /dev/null +++ b/test-apps/crypto-trading/auth/src/main/java/org/elasticsoftware/cryptotrading/auth/security/config/SecurityConfig.java @@ -0,0 +1,143 @@ +/* + * Copyright 2022 - 2025 The Original Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.elasticsoftware.cryptotrading.auth.security.config; + +import org.elasticsoftware.cryptotrading.auth.security.handler.OAuth2LoginFailureHandler; +import org.elasticsoftware.cryptotrading.auth.security.handler.OAuth2LoginSuccessHandler; +import org.elasticsoftware.cryptotrading.auth.security.service.OAuth2UserService; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; +import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.web.server.SecurityWebFilterChain; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.reactive.CorsConfigurationSource; +import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource; + +import java.util.List; + +/** + * Security configuration for Auth Service. + * + *

This configuration sets up OAuth2 login with Google (and potentially other providers) + * and configures JWT token generation upon successful authentication. + * + *

Key features: + *

+ * + *

CSRF Protection: Disabled because this service generates JWT tokens + * for stateless authentication. CSRF protection is not needed for stateless APIs. + */ +@Configuration +@EnableWebFluxSecurity +public class SecurityConfig { + + private final OAuth2UserService oauth2UserService; + private final OAuth2LoginSuccessHandler successHandler; + private final OAuth2LoginFailureHandler failureHandler; + + /** + * Constructs the security configuration with required dependencies. + * + * @param oauth2UserService the custom OAuth2 user service + * @param successHandler the OAuth2 login success handler + * @param failureHandler the OAuth2 login failure handler + */ + public SecurityConfig( + OAuth2UserService oauth2UserService, + OAuth2LoginSuccessHandler successHandler, + OAuth2LoginFailureHandler failureHandler) { + this.oauth2UserService = oauth2UserService; + this.successHandler = successHandler; + this.failureHandler = failureHandler; + } + + /** + * Configures the security filter chain for OAuth2 authentication. + * + * @param http the server HTTP security configuration + * @return the configured security web filter chain + */ + @Bean + public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { + http + // CORS configuration for frontend integration + .cors(cors -> cors.configurationSource(corsConfigurationSource())) + + // Authorization rules + .authorizeExchange(exchanges -> exchanges + // Public endpoints + .pathMatchers( + "/actuator/health", + "/actuator/info", + "/.well-known/jwks.json", // JWKS endpoint for JWT validation + "/login/oauth2/**", + "/oauth2/**" + ).permitAll() + // All other endpoints require authentication + .anyExchange().authenticated() + ) + + // OAuth2 login configuration + .oauth2Login(oauth2 -> oauth2 + // Custom user service for Akces integration + .authenticationSuccessHandler(successHandler) + .authenticationFailureHandler(failureHandler) + ); + + return http.build(); + } + + /** + * Configures CORS for development and production environments. + * + *

Development Configuration: Allows all origins, methods, and headers. + * This is acceptable for local development but should be restricted in production. + * + *

Production Configuration: Should restrict: + *

+ * + * @return the CORS configuration source + */ + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + + // TODO: Restrict origins in production + configuration.setAllowedOrigins(List.of("*")); + configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); + configuration.setAllowedHeaders(List.of("*")); + configuration.setAllowCredentials(false); // Set to true if using cookies + configuration.setMaxAge(3600L); // Cache preflight response for 1 hour + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + + return source; + } +} diff --git a/test-apps/crypto-trading/auth/src/main/java/org/elasticsoftware/cryptotrading/auth/security/handler/OAuth2LoginFailureHandler.java b/test-apps/crypto-trading/auth/src/main/java/org/elasticsoftware/cryptotrading/auth/security/handler/OAuth2LoginFailureHandler.java new file mode 100644 index 00000000..a2bcafb7 --- /dev/null +++ b/test-apps/crypto-trading/auth/src/main/java/org/elasticsoftware/cryptotrading/auth/security/handler/OAuth2LoginFailureHandler.java @@ -0,0 +1,112 @@ +/* + * Copyright 2022 - 2025 The Original Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.elasticsoftware.cryptotrading.auth.security.handler; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.server.WebFilterExchange; +import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; + +import java.util.HashMap; +import java.util.Map; + +/** + * Handles OAuth2 authentication failures. + * + *

This handler is invoked when OAuth2 authentication fails for any reason, such as: + *

+ * + *

The handler logs the error details and returns a JSON error response to the client. + * + *

The response format is: + *

+ * {
+ *   "error": "authentication_failed",
+ *   "error_description": "OAuth2 authentication failed: User denied access",
+ *   "status": 401
+ * }
+ * 
+ */ +@Component +public class OAuth2LoginFailureHandler implements ServerAuthenticationFailureHandler { + + private static final Logger logger = LoggerFactory.getLogger(OAuth2LoginFailureHandler.class); + + private final ObjectMapper objectMapper; + + /** + * Constructs the failure handler with required dependencies. + * + * @param objectMapper the Jackson object mapper for JSON serialization + */ + public OAuth2LoginFailureHandler(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + /** + * Handles authentication failure by logging the error and returning an error response. + * + * @param webFilterExchange the web filter exchange containing the request and response + * @param exception the authentication exception that caused the failure + * @return a Mono that completes when the response is written + */ + @Override + public Mono onAuthenticationFailure( + WebFilterExchange webFilterExchange, + AuthenticationException exception) { + + logger.error("OAuth2 authentication failed", exception); + + try { + // Create error response + Map errorResponse = new HashMap<>(); + errorResponse.put("error", "authentication_failed"); + errorResponse.put("error_description", + "OAuth2 authentication failed: " + exception.getMessage()); + errorResponse.put("status", HttpStatus.UNAUTHORIZED.value()); + + // Serialize to JSON + byte[] responseBytes = objectMapper.writeValueAsBytes(errorResponse); + + // Write response + var response = webFilterExchange.getExchange().getResponse(); + response.setStatusCode(HttpStatus.UNAUTHORIZED); + response.getHeaders().add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); + + DataBuffer buffer = response.bufferFactory().wrap(responseBytes); + return response.writeWith(Mono.just(buffer)); + + } catch (Exception e) { + logger.error("Error writing authentication failure response", e); + return Mono.error(e); + } + } +} diff --git a/test-apps/crypto-trading/auth/src/main/java/org/elasticsoftware/cryptotrading/auth/security/handler/OAuth2LoginSuccessHandler.java b/test-apps/crypto-trading/auth/src/main/java/org/elasticsoftware/cryptotrading/auth/security/handler/OAuth2LoginSuccessHandler.java new file mode 100644 index 00000000..d525f357 --- /dev/null +++ b/test-apps/crypto-trading/auth/src/main/java/org/elasticsoftware/cryptotrading/auth/security/handler/OAuth2LoginSuccessHandler.java @@ -0,0 +1,122 @@ +/* + * Copyright 2022 - 2025 The Original Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.elasticsoftware.cryptotrading.auth.security.handler; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.elasticsoftware.cryptotrading.auth.security.jwt.JwtTokenProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.security.web.server.WebFilterExchange; +import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; + +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +/** + * Handles successful OAuth2 authentication by generating JWT tokens. + * + *

This handler is invoked after a user successfully authenticates with an OAuth2 provider. + * It generates both access and refresh tokens and returns them to the client in JSON format. + * + *

The response format is: + *

+ * {
+ *   "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
+ *   "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
+ *   "token_type": "Bearer",
+ *   "expires_in": 900
+ * }
+ * 
+ * + *

In production, consider redirecting to a frontend URL with tokens as secure cookies + * or URL fragments instead of returning them directly in the response body. + */ +@Component +public class OAuth2LoginSuccessHandler implements ServerAuthenticationSuccessHandler { + + private static final Logger logger = LoggerFactory.getLogger(OAuth2LoginSuccessHandler.class); + + private final JwtTokenProvider jwtTokenProvider; + private final ObjectMapper objectMapper; + + /** + * Constructs the success handler with required dependencies. + * + * @param jwtTokenProvider the JWT token provider for generating tokens + * @param objectMapper the Jackson object mapper for JSON serialization + */ + public OAuth2LoginSuccessHandler(JwtTokenProvider jwtTokenProvider, ObjectMapper objectMapper) { + this.jwtTokenProvider = jwtTokenProvider; + this.objectMapper = objectMapper; + } + + /** + * Handles successful authentication by generating and returning JWT tokens. + * + * @param webFilterExchange the web filter exchange containing the request and response + * @param authentication the authentication object containing user details + * @return a Mono that completes when the response is written + */ + @Override + public Mono onAuthenticationSuccess( + WebFilterExchange webFilterExchange, + Authentication authentication) { + + OAuth2User oauth2User = (OAuth2User) authentication.getPrincipal(); + String email = oauth2User.getAttribute("email"); + + logger.info("OAuth2 authentication successful for user: {}", email); + + try { + // Generate JWT tokens + String accessToken = jwtTokenProvider.generateAccessToken(authentication); + String refreshToken = jwtTokenProvider.generateRefreshToken(authentication); + + // Create response body + Map responseBody = new HashMap<>(); + responseBody.put("access_token", accessToken); + responseBody.put("refresh_token", refreshToken); + responseBody.put("token_type", "Bearer"); + responseBody.put("expires_in", 900); // 15 minutes + + // Serialize to JSON + byte[] responseBytes = objectMapper.writeValueAsBytes(responseBody); + + // Write response + var response = webFilterExchange.getExchange().getResponse(); + response.setStatusCode(HttpStatus.OK); + response.getHeaders().add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); + + DataBuffer buffer = response.bufferFactory().wrap(responseBytes); + return response.writeWith(Mono.just(buffer)); + + } catch (Exception e) { + logger.error("Error generating JWT tokens", e); + return Mono.error(e); + } + } +} diff --git a/test-apps/crypto-trading/auth/src/main/java/org/elasticsoftware/cryptotrading/auth/security/jwt/JwtTokenProvider.java b/test-apps/crypto-trading/auth/src/main/java/org/elasticsoftware/cryptotrading/auth/security/jwt/JwtTokenProvider.java new file mode 100644 index 00000000..02b58ccc --- /dev/null +++ b/test-apps/crypto-trading/auth/src/main/java/org/elasticsoftware/cryptotrading/auth/security/jwt/JwtTokenProvider.java @@ -0,0 +1,168 @@ +/* + * Copyright 2022 - 2025 The Original Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.elasticsoftware.cryptotrading.auth.security.jwt; + +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.JWSSigner; +import com.nimbusds.jose.crypto.RSASSASigner; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Component; + +import java.time.Instant; +import java.util.Date; + +/** + * JWT Token Provider for generating and signing JWT tokens using RS256. + * + *

This implementation uses Nimbus JOSE+JWT library with RS256 (RSA Signature with SHA-256) + * for asymmetric token signing. The private key is used for signing, and the public key + * is distributed via JWKS endpoint for validation by resource servers. + * + *

In production, the private key should come from GCP Service Account credentials. + * For development/testing, a generated RSA key pair is used. + */ +@Component +public class JwtTokenProvider { + + private static final Logger logger = LoggerFactory.getLogger(JwtTokenProvider.class); + + private final RsaKeyProvider rsaKeyProvider; + private final long accessTokenExpiration; + private final long refreshTokenExpiration; + private final String issuer; + private final JWSSigner signer; + + /** + * Constructs a JWT Token Provider with configuration from application properties. + * + * @param rsaKeyProvider the RSA key provider for signing + * @param accessTokenExpiration access token expiration time in milliseconds + * @param refreshTokenExpiration refresh token expiration time in milliseconds + * @param issuer the issuer claim for JWT tokens + */ + public JwtTokenProvider( + RsaKeyProvider rsaKeyProvider, + @Value("${app.jwt.access-token-expiration:900000}") long accessTokenExpiration, + @Value("${app.jwt.refresh-token-expiration:604800000}") long refreshTokenExpiration, + @Value("${app.jwt.issuer:akces-crypto-trading}") String issuer) { + this.rsaKeyProvider = rsaKeyProvider; + this.accessTokenExpiration = accessTokenExpiration; + this.refreshTokenExpiration = refreshTokenExpiration; + this.issuer = issuer; + + try { + this.signer = new RSASSASigner(rsaKeyProvider.getRsaKey()); + logger.info("JWT Token Provider initialized with RS256 signing"); + } catch (JOSEException e) { + throw new IllegalStateException("Failed to initialize JWT signer", e); + } + } + + /** + * Generates an access token for the authenticated user. + * + * @param authentication the authentication object containing user details + * @return the generated JWT access token + */ + public String generateAccessToken(Authentication authentication) { + OAuth2User oauth2User = (OAuth2User) authentication.getPrincipal(); + + String email = oauth2User.getAttribute("email"); + String name = oauth2User.getAttribute("name"); + String userId = oauth2User.getAttribute("sub"); + + Instant now = Instant.now(); + Instant expiration = now.plusMillis(accessTokenExpiration); + + return generateToken(userId, email, name, now, expiration, "access"); + } + + /** + * Generates a refresh token for the authenticated user. + * + * @param authentication the authentication object containing user details + * @return the generated JWT refresh token + */ + public String generateRefreshToken(Authentication authentication) { + OAuth2User oauth2User = (OAuth2User) authentication.getPrincipal(); + + String userId = oauth2User.getAttribute("sub"); + + Instant now = Instant.now(); + Instant expiration = now.plusMillis(refreshTokenExpiration); + + return generateToken(userId, null, null, now, expiration, "refresh"); + } + + /** + * Generates a JWT token with the specified claims using Nimbus JOSE+JWT. + * + * @param userId the user identifier + * @param email the user email (nullable for refresh tokens) + * @param name the user name (nullable for refresh tokens) + * @param issuedAt the token issuance time + * @param expiration the token expiration time + * @param tokenType the token type ("access" or "refresh") + * @return the generated JWT token + */ + private String generateToken(String userId, String email, String name, + Instant issuedAt, Instant expiration, + String tokenType) { + try { + // Create JWT claims + JWTClaimsSet.Builder claimsBuilder = new JWTClaimsSet.Builder() + .subject(userId) + .issuer(issuer) + .issueTime(Date.from(issuedAt)) + .expirationTime(Date.from(expiration)) + .claim("token_type", tokenType); + + if (email != null) { + claimsBuilder.claim("email", email); + } + if (name != null) { + claimsBuilder.claim("name", name); + } + + JWTClaimsSet claims = claimsBuilder.build(); + + // Create JWT header with key ID + JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.RS256) + .keyID(rsaKeyProvider.getRsaKey().getKeyID()) + .build(); + + // Create signed JWT + SignedJWT signedJWT = new SignedJWT(header, claims); + signedJWT.sign(signer); + + return signedJWT.serialize(); + + } catch (JOSEException e) { + logger.error("Error generating JWT token", e); + throw new RuntimeException("Failed to generate JWT token", e); + } + } +} diff --git a/test-apps/crypto-trading/auth/src/main/java/org/elasticsoftware/cryptotrading/auth/security/jwt/RsaKeyProvider.java b/test-apps/crypto-trading/auth/src/main/java/org/elasticsoftware/cryptotrading/auth/security/jwt/RsaKeyProvider.java new file mode 100644 index 00000000..875ae9af --- /dev/null +++ b/test-apps/crypto-trading/auth/src/main/java/org/elasticsoftware/cryptotrading/auth/security/jwt/RsaKeyProvider.java @@ -0,0 +1,96 @@ +/* + * Copyright 2022 - 2025 The Original Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.elasticsoftware.cryptotrading.auth.security.jwt; + +import com.nimbusds.jose.jwk.RSAKey; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.util.UUID; + +/** + * Provides RSA key pair for JWT signing and verification. + * + *

In development/test environments, generates a new RSA key pair on startup. + * In production, this should load the GCP Service Account private key from Secret Manager. + * + *

The public key is exposed via JWKS endpoint for JWT validation by resource servers. + */ +@Component +public class RsaKeyProvider { + + private static final Logger logger = LoggerFactory.getLogger(RsaKeyProvider.class); + + private final RSAKey rsaKey; + + /** + * Constructs the RSA key provider. + * Generates a new 2048-bit RSA key pair for development/testing. + * + *

In production, replace this with loading from GCP Service Account: + *

+     * ServiceAccountCredentials credentials = ServiceAccountCredentials.fromStream(
+     *     new ByteArrayInputStream(serviceAccountJson.getBytes())
+     * );
+     * PrivateKey privateKey = credentials.getPrivateKey();
+     * 
+ */ + public RsaKeyProvider() { + try { + // Generate RSA key pair for development/testing + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); + keyPairGenerator.initialize(2048); + KeyPair keyPair = keyPairGenerator.generateKeyPair(); + + // Create Nimbus RSAKey with key ID + this.rsaKey = new RSAKey.Builder((RSAPublicKey) keyPair.getPublic()) + .privateKey((RSAPrivateKey) keyPair.getPrivate()) + .keyID(UUID.randomUUID().toString()) + .build(); + + logger.info("Generated RSA key pair for JWT signing (kid: {})", rsaKey.getKeyID()); + logger.warn("Using generated RSA key pair. In production, load from GCP Service Account."); + + } catch (Exception e) { + throw new IllegalStateException("Failed to generate RSA key pair", e); + } + } + + /** + * Gets the RSA key (includes both public and private key). + * + * @return the RSA key + */ + public RSAKey getRsaKey() { + return rsaKey; + } + + /** + * Gets the public RSA key for JWKS endpoint. + * + * @return the public RSA key (without private key) + */ + public RSAKey getPublicKey() { + return rsaKey.toPublicJWK(); + } +} diff --git a/test-apps/crypto-trading/auth/src/main/java/org/elasticsoftware/cryptotrading/auth/security/model/GoogleOAuth2UserInfo.java b/test-apps/crypto-trading/auth/src/main/java/org/elasticsoftware/cryptotrading/auth/security/model/GoogleOAuth2UserInfo.java new file mode 100644 index 00000000..9ea49fdb --- /dev/null +++ b/test-apps/crypto-trading/auth/src/main/java/org/elasticsoftware/cryptotrading/auth/security/model/GoogleOAuth2UserInfo.java @@ -0,0 +1,73 @@ +/* + * Copyright 2022 - 2025 The Original Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.elasticsoftware.cryptotrading.auth.security.model; + +import java.util.Map; + +/** + * Google OAuth2 user information implementation. + * + *

This class maps Google-specific attribute names to the standard + * {@link OAuth2UserInfo} interface. Google returns user information with + * the following attributes: + *

+ * + * @see Google OpenID Connect + */ +public record GoogleOAuth2UserInfo(Map attributes) implements OAuth2UserInfo { + + @Override + public String getId() { + return (String) attributes.get("sub"); + } + + @Override + public String getEmail() { + return (String) attributes.get("email"); + } + + @Override + public String getName() { + return (String) attributes.get("name"); + } + + @Override + public String getImageUrl() { + return (String) attributes.get("picture"); + } + + @Override + public Map getAttributes() { + return attributes; + } + + /** + * Returns whether the user's email has been verified by Google. + * + * @return true if the email is verified, false otherwise + */ + public Boolean isEmailVerified() { + return (Boolean) attributes.get("email_verified"); + } +} diff --git a/test-apps/crypto-trading/auth/src/main/java/org/elasticsoftware/cryptotrading/auth/security/model/OAuth2UserInfo.java b/test-apps/crypto-trading/auth/src/main/java/org/elasticsoftware/cryptotrading/auth/security/model/OAuth2UserInfo.java new file mode 100644 index 00000000..6e9df0ce --- /dev/null +++ b/test-apps/crypto-trading/auth/src/main/java/org/elasticsoftware/cryptotrading/auth/security/model/OAuth2UserInfo.java @@ -0,0 +1,68 @@ +/* + * Copyright 2022 - 2025 The Original Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.elasticsoftware.cryptotrading.auth.security.model; + +import java.util.Map; + +/** + * Provider-agnostic interface for OAuth2 user information. + * + *

This interface abstracts OAuth2 user attributes across different providers + * (Google, GitHub, Facebook, etc.), allowing the application to work with + * user information in a consistent way regardless of the OAuth2 provider. + * + *

Implementations of this interface map provider-specific attribute names + * to standard fields like ID, email, and name. + */ +public interface OAuth2UserInfo { + + /** + * Returns the unique user identifier from the OAuth2 provider. + * + * @return the user ID + */ + String getId(); + + /** + * Returns the user's email address. + * + * @return the email address + */ + String getEmail(); + + /** + * Returns the user's display name. + * + * @return the display name + */ + String getName(); + + /** + * Returns the user's profile image URL. + * + * @return the image URL, or null if not available + */ + String getImageUrl(); + + /** + * Returns all raw attributes from the OAuth2 provider. + * + * @return the attributes map + */ + Map getAttributes(); +} diff --git a/test-apps/crypto-trading/auth/src/main/java/org/elasticsoftware/cryptotrading/auth/security/service/OAuth2UserService.java b/test-apps/crypto-trading/auth/src/main/java/org/elasticsoftware/cryptotrading/auth/security/service/OAuth2UserService.java new file mode 100644 index 00000000..49778fde --- /dev/null +++ b/test-apps/crypto-trading/auth/src/main/java/org/elasticsoftware/cryptotrading/auth/security/service/OAuth2UserService.java @@ -0,0 +1,172 @@ +/* + * Copyright 2022 - 2025 The Original Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.elasticsoftware.cryptotrading.auth.security.service; + +import org.elasticsoftware.akces.client.AkcesClient; +import org.elasticsoftware.cryptotrading.auth.security.model.GoogleOAuth2UserInfo; +import org.elasticsoftware.cryptotrading.auth.security.model.OAuth2UserInfo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.oauth2.client.userinfo.DefaultReactiveOAuth2UserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Mono; + +/** + * Custom reactive OAuth2 user details service for handling OAuth2 authentication. + * + *

This service is responsible for: + *

    + *
  • Loading user details from the OAuth2 provider
  • + *
  • Creating new user accounts via Akces command bus when a new user logs in
  • + *
  • Providing user information to the authentication success handler
  • + *
+ * + *

The service integrates with the Akces framework to send CreateAccountCommand + * for new users, ensuring that all user accounts are properly registered in the + * event-sourced system. + */ +@Service +public class OAuth2UserService extends DefaultReactiveOAuth2UserService { + + private static final Logger logger = LoggerFactory.getLogger(OAuth2UserService.class); + + private final AkcesClient akcesClient; + + /** + * Constructs the OAuth2 user service with Akces client integration. + * + * @param akcesClient the Akces client for sending commands + */ + public OAuth2UserService(AkcesClient akcesClient) { + this.akcesClient = akcesClient; + } + + /** + * Loads the OAuth2 user details after successful authentication with the provider. + * + *

This method: + *

    + *
  1. Delegates to the default implementation to fetch user details from the provider
  2. + *
  3. Extracts provider-specific user information
  4. + *
  5. Creates a new user account in the system if this is the user's first login
  6. + *
+ * + *

Note: Account creation is done asynchronously and does not block + * the authentication flow. If account creation fails, it is logged but does not prevent + * the user from logging in. + * + * @param userRequest the OAuth2 user request containing tokens and client registration + * @return a Mono emitting the OAuth2User with user details + * @throws OAuth2AuthenticationException if user details cannot be loaded + */ + @Override + public Mono loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { + return super.loadUser(userRequest) + .flatMap(oauth2User -> { + String registrationId = userRequest.getClientRegistration().getRegistrationId(); + OAuth2UserInfo userInfo = extractUserInfo(registrationId, oauth2User); + + // Process user (create account if new user) + return processOAuth2User(userInfo, oauth2User) + .thenReturn(oauth2User); + }); + } + + /** + * Extracts user information from the OAuth2User based on the provider. + * + * @param registrationId the OAuth2 provider registration ID (e.g., "google") + * @param oauth2User the OAuth2User containing provider attributes + * @return the extracted user information + * @throws OAuth2AuthenticationException if the provider is not supported + */ + private OAuth2UserInfo extractUserInfo(String registrationId, OAuth2User oauth2User) { + return switch (registrationId.toLowerCase()) { + case "google" -> new GoogleOAuth2UserInfo(oauth2User.getAttributes()); + default -> { + logger.error("Unsupported OAuth2 provider: {}", registrationId); + throw new OAuth2AuthenticationException( + "Unsupported OAuth2 provider: " + registrationId + ); + } + }; + } + + /** + * Processes the OAuth2 user by creating an account in the system if needed. + * + *

This method sends a CreateAccountCommand to the Akces command bus. In a production + * system, you would first check if the account already exists (via a query model lookup) + * before attempting to create it. + * + *

TODO: Implement account existence check using AccountQueryModel + * to avoid sending duplicate CreateAccountCommand events. + * + * @param userInfo the OAuth2 user information + * @param oauth2User the OAuth2User object + * @return a Mono that completes when user processing is done + */ + private Mono processOAuth2User(OAuth2UserInfo userInfo, OAuth2User oauth2User) { + String userId = userInfo.getId(); + String email = userInfo.getEmail(); + String name = userInfo.getName(); + + logger.info("Processing OAuth2 user: userId={}, email={}, name={}", userId, email, name); + + // TODO: Query AccountQueryModel to check if account exists + // For now, we'll attempt to create the account and let the aggregate handle idempotency + + // In a real implementation, you would: + // 1. Query the AccountQueryModel to check if account exists + // 2. Only send CreateAccountCommand if account doesn't exist + // 3. Handle the case where the account exists but needs updating + + // Example (not implemented yet): + // return accountQueryModelCache.getAccount(userId) + // .flatMap(existingAccount -> { + // logger.debug("Account already exists for userId: {}", userId); + // return Mono.empty(); + // }) + // .switchIfEmpty(createAccount(userId, email, name)); + + // For Phase 2, we'll just log and return without sending commands + // since we don't have the full account aggregate implementation yet + logger.debug("User authentication successful for: {}", email); + + return Mono.empty(); + } + + // Future implementation for account creation: + // private Mono createAccount(String userId, String email, String name) { + // CreateAccountCommand command = new CreateAccountCommand( + // userId, + // "US", // Default country, should be configurable + // extractFirstName(name), + // extractLastName(name), + // email + // ); + // + // return Mono.fromFuture( + // akcesClient.send("DEFAULT_TENANT", command) + // .toCompletableFuture() + // ).then(); + // } +} diff --git a/test-apps/crypto-trading/auth/src/main/java/org/elasticsoftware/cryptotrading/auth/web/v1/JwksController.java b/test-apps/crypto-trading/auth/src/main/java/org/elasticsoftware/cryptotrading/auth/web/v1/JwksController.java new file mode 100644 index 00000000..178ad216 --- /dev/null +++ b/test-apps/crypto-trading/auth/src/main/java/org/elasticsoftware/cryptotrading/auth/web/v1/JwksController.java @@ -0,0 +1,62 @@ +/* + * Copyright 2022 - 2025 The Original Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.elasticsoftware.cryptotrading.auth.web.v1; + +import com.nimbusds.jose.jwk.JWKSet; +import org.elasticsoftware.cryptotrading.auth.security.jwt.RsaKeyProvider; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Mono; + +import java.util.Map; + +/** + * Controller for exposing JSON Web Key Set (JWKS) endpoint. + * + *

This endpoint provides the public keys used for JWT signature verification. + * Resource servers (Commands and Queries services) fetch these keys to validate + * JWT tokens issued by this Auth service. + * + *

Standard JWKS endpoint: {@code /.well-known/jwks.json} + */ +@RestController +public class JwksController { + + private final RsaKeyProvider rsaKeyProvider; + + /** + * Constructs the JWKS controller. + * + * @param rsaKeyProvider the RSA key provider + */ + public JwksController(RsaKeyProvider rsaKeyProvider) { + this.rsaKeyProvider = rsaKeyProvider; + } + + /** + * Exposes the JWKS endpoint with public keys for JWT verification. + * + * @return the JSON Web Key Set + */ + @GetMapping(value = "/.well-known/jwks.json", produces = MediaType.APPLICATION_JSON_VALUE) + public Mono> jwks() { + JWKSet jwkSet = new JWKSet(rsaKeyProvider.getPublicKey()); + return Mono.just(jwkSet.toJSONObject()); + } +} diff --git a/test-apps/crypto-trading/auth/src/main/resources/application.yml b/test-apps/crypto-trading/auth/src/main/resources/application.yml new file mode 100644 index 00000000..59432618 --- /dev/null +++ b/test-apps/crypto-trading/auth/src/main/resources/application.yml @@ -0,0 +1,92 @@ +# +# Copyright 2022 - 2025 The Original Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +server: + port: 8080 + +spring: + application: + name: akces-crypto-trading-auth + + # OAuth2 Client Configuration (to be configured with Google credentials) + security: + oauth2: + client: + registration: + google: + client-id: ${GOOGLE_CLIENT_ID:changeme} + client-secret: ${GOOGLE_CLIENT_SECRET:changeme} + scope: + - openid + - profile + - email + redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}" + provider: + google: + authorization-uri: https://accounts.google.com/o/oauth2/v2/auth + token-uri: https://oauth2.googleapis.com/token + user-info-uri: https://www.googleapis.com/oauth2/v3/userinfo + user-name-attribute: sub + + # Database configuration for AccountDatabaseModel + datasource: + url: ${DATABASE_URL:jdbc:postgresql://localhost:5432/akces_auth} + username: ${DATABASE_USERNAME:akces} + password: ${DATABASE_PASSWORD:akces} + driver-class-name: org.postgresql.Driver + + # Liquibase for database schema management + liquibase: + change-log: classpath:db/changelog/db.changelog-master.xml + enabled: ${LIQUIBASE_ENABLED:false} + + # Kafka configuration + kafka: + bootstrap-servers: ${KAFKA_BOOTSTRAP_SERVERS:localhost:9092} + consumer: + enable-auto-commit: false + isolation-level: read_committed + max-poll-records: 500 + producer: + acks: all + retries: 2147483647 + properties: + enable.idempotence: true + +# Akces configuration +akces: + schemaregistry: + url: ${SCHEMA_REGISTRY_URL:http://localhost:8081} + rocksdb: + baseDir: ${ROCKSDB_BASE_DIR:/tmp/akces-auth} + aggregate: + schemas: + forceRegister: false + +# JWT Configuration (RS256 with RSA key pair) +app: + jwt: + access-token-expiration: 900000 # 15 minutes in milliseconds + refresh-token-expiration: 604800000 # 7 days in milliseconds + issuer: akces-crypto-trading + # In production, configure GCP Service Account for signing: + # gcp-service-account-key: ${GCP_SERVICE_ACCOUNT_KEY:} + +# Logging +logging: + level: + org.elasticsoftware.cryptotrading: DEBUG + org.springframework.security: DEBUG diff --git a/test-apps/crypto-trading/auth/src/main/resources/db/changelog/.gitkeep b/test-apps/crypto-trading/auth/src/main/resources/db/changelog/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/test-apps/crypto-trading/commands/pom.xml b/test-apps/crypto-trading/commands/pom.xml index ee625d3b..3c7438b5 100644 --- a/test-apps/crypto-trading/commands/pom.xml +++ b/test-apps/crypto-trading/commands/pom.xml @@ -83,6 +83,20 @@ akces-client + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.security + spring-security-oauth2-resource-server + + + org.springframework.security + spring-security-oauth2-jose + + diff --git a/test-apps/crypto-trading/commands/src/main/java/org/elasticsoftware/cryptotrading/ClientConfig.java b/test-apps/crypto-trading/commands/src/main/java/org/elasticsoftware/cryptotrading/ClientConfig.java index 60f068c6..0e242723 100644 --- a/test-apps/crypto-trading/commands/src/main/java/org/elasticsoftware/cryptotrading/ClientConfig.java +++ b/test-apps/crypto-trading/commands/src/main/java/org/elasticsoftware/cryptotrading/ClientConfig.java @@ -23,7 +23,8 @@ @Configuration @ComponentScan(basePackages = { - "org.elasticsoftware.cryptotrading.web" + "org.elasticsoftware.cryptotrading.web", + "org.elasticsoftware.cryptotrading.security" }) @PropertySource("classpath:akces-framework.properties") public class ClientConfig { diff --git a/test-apps/crypto-trading/commands/src/main/java/org/elasticsoftware/cryptotrading/security/config/SecurityConfig.java b/test-apps/crypto-trading/commands/src/main/java/org/elasticsoftware/cryptotrading/security/config/SecurityConfig.java new file mode 100644 index 00000000..fa4c6517 --- /dev/null +++ b/test-apps/crypto-trading/commands/src/main/java/org/elasticsoftware/cryptotrading/security/config/SecurityConfig.java @@ -0,0 +1,97 @@ +/* + * Copyright 2022 - 2025 The Original Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.elasticsoftware.cryptotrading.security.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; +import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder; +import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; +import org.springframework.security.web.server.SecurityWebFilterChain; + +/** + * Security configuration for Commands Service. + * + *

This configuration validates JWT tokens using Spring Security OAuth2 Resource Server. + * JWTs are validated using public keys fetched from the Auth service's JWKS endpoint. + * + *

CSRF Protection: Disabled because this API uses JWT tokens + * for stateless authentication. CSRF protection is not needed for stateless APIs + * where authentication is via bearer tokens rather than cookies. + * + *

JWT validation is handled automatically by Spring Security using Nimbus JWT library. + * No custom authentication code is needed - Spring Security fetches public keys from JWKS + * and validates token signatures, expiration, and issuer claims. + * + * @see Spring Security CSRF + */ +@Configuration +@EnableWebFluxSecurity +public class SecurityConfig { + + @Value("${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}") + private String jwkSetUri; + + /** + * Configures the security filter chain with OAuth2 Resource Server JWT validation. + * + * @param http the server HTTP security configuration + * @return the configured security web filter chain + */ + @Bean + public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { + http + // CSRF disabled for stateless JWT-based API (no session cookies) + .csrf(csrf -> csrf.disable()) + + // Configure OAuth2 Resource Server with JWT + .oauth2ResourceServer(oauth2 -> oauth2 + .jwt(jwt -> jwt.jwtDecoder(jwtDecoder())) + ) + + // Authorization rules + .authorizeExchange(exchanges -> exchanges + // Public endpoints + .pathMatchers("/actuator/health", "/actuator/info").permitAll() + // All other endpoints require authentication + .anyExchange().authenticated() + ); + + return http.build(); + } + + /** + * Creates a reactive JWT decoder using Nimbus that fetches public keys from JWKS endpoint. + * + *

Spring Security automatically: + *

    + *
  • Fetches public keys from the JWKS endpoint
  • + *
  • Caches keys for performance
  • + *
  • Validates JWT signatures using RS256
  • + *
  • Validates expiration and other standard claims
  • + *
+ * + * @return the JWT decoder + */ + @Bean + public ReactiveJwtDecoder jwtDecoder() { + return NimbusReactiveJwtDecoder.withJwkSetUri(jwkSetUri).build(); + } +} diff --git a/test-apps/crypto-trading/commands/src/main/resources/application.properties b/test-apps/crypto-trading/commands/src/main/resources/application.properties index ad17642e..3605129e 100644 --- a/test-apps/crypto-trading/commands/src/main/resources/application.properties +++ b/test-apps/crypto-trading/commands/src/main/resources/application.properties @@ -20,4 +20,7 @@ management.endpoint.health.probes.enabled=true management.health.livenessState.enabled=true management.health.readinessState.enabled=true server.shutdown=graceful -spring.mvc.problemdetails.enabled=true \ No newline at end of file +spring.mvc.problemdetails.enabled=true + +# OAuth2 Resource Server - JWT validation using JWKS +spring.security.oauth2.resourceserver.jwt.jwk-set-uri=${JWT_JWK_SET_URI:http://localhost:8080/.well-known/jwks.json} \ No newline at end of file diff --git a/test-apps/crypto-trading/commands/src/test/java/org/elasticsoftware/cryptotrading/CryptoTradingCommandApiTest.java b/test-apps/crypto-trading/commands/src/test/java/org/elasticsoftware/cryptotrading/CryptoTradingCommandApiTest.java index b9c9586e..58d5d418 100644 --- a/test-apps/crypto-trading/commands/src/test/java/org/elasticsoftware/cryptotrading/CryptoTradingCommandApiTest.java +++ b/test-apps/crypto-trading/commands/src/test/java/org/elasticsoftware/cryptotrading/CryptoTradingCommandApiTest.java @@ -72,7 +72,10 @@ webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @AutoConfigureWebTestClient @PropertySource("classpath:akces-aggregateservice.properties") -@ContextConfiguration(initializers = CryptoTradingCommandApiTest.Initializer.class) +@ContextConfiguration( + initializers = CryptoTradingCommandApiTest.Initializer.class, + classes = org.elasticsoftware.cryptotrading.security.config.TestSecurityConfig.class +) @Testcontainers @DirtiesContext public class CryptoTradingCommandApiTest { diff --git a/test-apps/crypto-trading/commands/src/test/java/org/elasticsoftware/cryptotrading/security/config/TestSecurityConfig.java b/test-apps/crypto-trading/commands/src/test/java/org/elasticsoftware/cryptotrading/security/config/TestSecurityConfig.java new file mode 100644 index 00000000..b0b3c02a --- /dev/null +++ b/test-apps/crypto-trading/commands/src/test/java/org/elasticsoftware/cryptotrading/security/config/TestSecurityConfig.java @@ -0,0 +1,52 @@ +/* + * Copyright 2022 - 2025 The Original Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.elasticsoftware.cryptotrading.security.config; + +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; +import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.web.server.SecurityWebFilterChain; + +/** + * Test security configuration that disables authentication for integration tests. + * + *

This configuration overrides the production SecurityConfig to permit all requests + * without authentication during testing, allowing existing tests to pass without modification. + */ +@TestConfiguration +public class TestSecurityConfig { + + /** + * Configures a permissive security filter chain for tests. + * + * @param http the server HTTP security configuration + * @return the configured security web filter chain + */ + @Bean + @Primary + public SecurityWebFilterChain testSecurityWebFilterChain(ServerHttpSecurity http) { + http + .csrf(csrf -> csrf.disable()) + .authorizeExchange(exchanges -> exchanges + .anyExchange().permitAll() + ); + + return http.build(); + } +} diff --git a/test-apps/crypto-trading/pom.xml b/test-apps/crypto-trading/pom.xml index 5e211ba3..8fc5c085 100644 --- a/test-apps/crypto-trading/pom.xml +++ b/test-apps/crypto-trading/pom.xml @@ -73,6 +73,7 @@ aggregates commands queries + auth diff --git a/test-apps/crypto-trading/queries/pom.xml b/test-apps/crypto-trading/queries/pom.xml index 5cf89688..ee1edd3e 100644 --- a/test-apps/crypto-trading/queries/pom.xml +++ b/test-apps/crypto-trading/queries/pom.xml @@ -136,6 +136,20 @@ test + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.security + spring-security-oauth2-resource-server + + + org.springframework.security + spring-security-oauth2-jose + + diff --git a/test-apps/crypto-trading/queries/src/main/java/org/elasticsoftware/cryptotrading/ClientConfig.java b/test-apps/crypto-trading/queries/src/main/java/org/elasticsoftware/cryptotrading/ClientConfig.java index f57780f8..fcb4a30b 100644 --- a/test-apps/crypto-trading/queries/src/main/java/org/elasticsoftware/cryptotrading/ClientConfig.java +++ b/test-apps/crypto-trading/queries/src/main/java/org/elasticsoftware/cryptotrading/ClientConfig.java @@ -31,7 +31,8 @@ @ComponentScan(basePackages = { "org.elasticsoftware.cryptotrading.web", "org.elasticsoftware.cryptotrading.query", - "org.elasticsoftware.cryptotrading.services" + "org.elasticsoftware.cryptotrading.services", + "org.elasticsoftware.cryptotrading.security" }) @EnableJdbcRepositories(basePackages = {"org.elasticsoftware.cryptotrading.query.jdbc"}) @PropertySource("classpath:akces-framework.properties") diff --git a/test-apps/crypto-trading/queries/src/main/java/org/elasticsoftware/cryptotrading/security/config/SecurityConfig.java b/test-apps/crypto-trading/queries/src/main/java/org/elasticsoftware/cryptotrading/security/config/SecurityConfig.java new file mode 100644 index 00000000..2c1fb3e2 --- /dev/null +++ b/test-apps/crypto-trading/queries/src/main/java/org/elasticsoftware/cryptotrading/security/config/SecurityConfig.java @@ -0,0 +1,97 @@ +/* + * Copyright 2022 - 2025 The Original Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.elasticsoftware.cryptotrading.security.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; +import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder; +import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; +import org.springframework.security.web.server.SecurityWebFilterChain; + +/** + * Security configuration for Queries Service. + * + *

This configuration validates JWT tokens using Spring Security OAuth2 Resource Server. + * JWTs are validated using public keys fetched from the Auth service's JWKS endpoint. + * + *

CSRF Protection: Disabled because this API uses JWT tokens + * for stateless authentication. CSRF protection is not needed for stateless APIs + * where authentication is via bearer tokens rather than cookies. + * + *

JWT validation is handled automatically by Spring Security using Nimbus JWT library. + * No custom authentication code is needed - Spring Security fetches public keys from JWKS + * and validates token signatures, expiration, and issuer claims. + * + * @see Spring Security CSRF + */ +@Configuration +@EnableWebFluxSecurity +public class SecurityConfig { + + @Value("${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}") + private String jwkSetUri; + + /** + * Configures the security filter chain with OAuth2 Resource Server JWT validation. + * + * @param http the server HTTP security configuration + * @return the configured security web filter chain + */ + @Bean + public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { + http + // CSRF disabled for stateless JWT-based API (no session cookies) + .csrf(csrf -> csrf.disable()) + + // Configure OAuth2 Resource Server with JWT + .oauth2ResourceServer(oauth2 -> oauth2 + .jwt(jwt -> jwt.jwtDecoder(jwtDecoder())) + ) + + // Authorization rules + .authorizeExchange(exchanges -> exchanges + // Public endpoints + .pathMatchers("/actuator/health", "/actuator/info").permitAll() + // All other endpoints require authentication + .anyExchange().authenticated() + ); + + return http.build(); + } + + /** + * Creates a reactive JWT decoder using Nimbus that fetches public keys from JWKS endpoint. + * + *

Spring Security automatically: + *

    + *
  • Fetches public keys from the JWKS endpoint
  • + *
  • Caches keys for performance
  • + *
  • Validates JWT signatures using RS256
  • + *
  • Validates expiration and other standard claims
  • + *
+ * + * @return the JWT decoder + */ + @Bean + public ReactiveJwtDecoder jwtDecoder() { + return NimbusReactiveJwtDecoder.withJwkSetUri(jwkSetUri).build(); + } +} diff --git a/test-apps/crypto-trading/queries/src/main/resources/application.properties b/test-apps/crypto-trading/queries/src/main/resources/application.properties index 5e6056cc..7e8abfd4 100644 --- a/test-apps/crypto-trading/queries/src/main/resources/application.properties +++ b/test-apps/crypto-trading/queries/src/main/resources/application.properties @@ -26,4 +26,7 @@ spring.mvc.problemdetails.enabled=true spring.autoconfigure.exclude=org.springframework.boot.data.jpa.autoconfigure.DataJpaRepositoriesAutoConfiguration spring.liquibase.change-log=classpath:/db/changelog/db.changelog-master.xml akces.cryptotrading.counterPartyId=Coinbase -akces.client.domainEventsPackage=org.elasticsoftware.cryptotrading.aggregates \ No newline at end of file +akces.client.domainEventsPackage=org.elasticsoftware.cryptotrading.aggregates + +# OAuth2 Resource Server - JWT validation using JWKS +spring.security.oauth2.resourceserver.jwt.jwk-set-uri=${JWT_JWK_SET_URI:http://localhost:8080/.well-known/jwks.json} \ No newline at end of file diff --git a/test-apps/crypto-trading/queries/src/test/java/org/elasticsoftware/cryptotrading/CryptoTradingQueryApiTest.java b/test-apps/crypto-trading/queries/src/test/java/org/elasticsoftware/cryptotrading/CryptoTradingQueryApiTest.java index 8a933dd6..9b50664a 100644 --- a/test-apps/crypto-trading/queries/src/test/java/org/elasticsoftware/cryptotrading/CryptoTradingQueryApiTest.java +++ b/test-apps/crypto-trading/queries/src/test/java/org/elasticsoftware/cryptotrading/CryptoTradingQueryApiTest.java @@ -77,7 +77,10 @@ ) @AutoConfigureWebTestClient @PropertySource("classpath:akces-aggregateservice.properties") -@ContextConfiguration(initializers = CryptoTradingQueryApiTest.Initializer.class) +@ContextConfiguration( + initializers = CryptoTradingQueryApiTest.Initializer.class, + classes = org.elasticsoftware.cryptotrading.security.config.TestSecurityConfig.class +) @Testcontainers @DirtiesContext public class CryptoTradingQueryApiTest { diff --git a/test-apps/crypto-trading/queries/src/test/java/org/elasticsoftware/cryptotrading/security/config/TestSecurityConfig.java b/test-apps/crypto-trading/queries/src/test/java/org/elasticsoftware/cryptotrading/security/config/TestSecurityConfig.java new file mode 100644 index 00000000..b0b3c02a --- /dev/null +++ b/test-apps/crypto-trading/queries/src/test/java/org/elasticsoftware/cryptotrading/security/config/TestSecurityConfig.java @@ -0,0 +1,52 @@ +/* + * Copyright 2022 - 2025 The Original Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.elasticsoftware.cryptotrading.security.config; + +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; +import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.web.server.SecurityWebFilterChain; + +/** + * Test security configuration that disables authentication for integration tests. + * + *

This configuration overrides the production SecurityConfig to permit all requests + * without authentication during testing, allowing existing tests to pass without modification. + */ +@TestConfiguration +public class TestSecurityConfig { + + /** + * Configures a permissive security filter chain for tests. + * + * @param http the server HTTP security configuration + * @return the configured security web filter chain + */ + @Bean + @Primary + public SecurityWebFilterChain testSecurityWebFilterChain(ServerHttpSecurity http) { + http + .csrf(csrf -> csrf.disable()) + .authorizeExchange(exchanges -> exchanges + .anyExchange().permitAll() + ); + + return http.build(); + } +}