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 @@
+
+
+
+
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: + *
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 MonoThis 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 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:
+ * This class maps Google-specific attribute names to the standard
+ * {@link OAuth2UserInfo} interface. Google returns user information with
+ * the following attributes:
+ * 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 This service is responsible for:
+ * 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:
+ * 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 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 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
+ * 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.
+ *
+ *
+ *
+ *
+ * @see Google OpenID Connect
+ */
+public record GoogleOAuth2UserInfo(Map
+ *
+ *
+ *
+ *
+ *
+ *