From 0bc7c54ebb79ad8165f7ca5cf81e9e7b9a3c608a Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 6 Dec 2025 16:08:11 +0000
Subject: [PATCH 01/11] Initial plan
From 69eb30003acca7bdafc43260f17d8a7cd9a7d463 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 6 Dec 2025 16:15:58 +0000
Subject: [PATCH 02/11] Phase 1: Add Spring Security OAuth2 dependencies and
create auth module structure
Co-authored-by: jwijgerd <914840+jwijgerd@users.noreply.github.com>
---
test-apps/crypto-trading/auth/pom.xml | 159 ++++++++++++++++++
.../auth/AuthServiceApplication.java | 43 +++++
.../auth/src/main/resources/application.yml | 92 ++++++++++
.../src/main/resources/db/changelog/.gitkeep | 0
test-apps/crypto-trading/commands/pom.xml | 14 ++
test-apps/crypto-trading/pom.xml | 1 +
test-apps/crypto-trading/queries/pom.xml | 14 ++
7 files changed, 323 insertions(+)
create mode 100644 test-apps/crypto-trading/auth/pom.xml
create mode 100644 test-apps/crypto-trading/auth/src/main/java/org/elasticsoftware/cryptotrading/auth/AuthServiceApplication.java
create mode 100644 test-apps/crypto-trading/auth/src/main/resources/application.yml
create mode 100644 test-apps/crypto-trading/auth/src/main/resources/db/changelog/.gitkeep
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:
+ *
+ * - OAuth 2.0 authentication flow with Google (and other providers)
+ * - JWT token generation using GCP Service Account
+ * - User account creation via Akces command bus
+ * - Account query services for OAuth user lookup
+ *
+ *
+ * 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/resources/application.yml b/test-apps/crypto-trading/auth/src/main/resources/application.yml
new file mode 100644
index 00000000..10920855
--- /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
+app:
+ jwt:
+ gcp-service-account-key: ${GCP_SERVICE_ACCOUNT_KEY:}
+ access-token-expiration: 900000 # 15 minutes in milliseconds
+ refresh-token-expiration: 604800000 # 7 days in milliseconds
+ issuer: akces-crypto-trading
+ service-account-email: ${GCP_SERVICE_ACCOUNT_EMAIL:}
+
+# 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/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
+
+
From 2dd5624c691049e1d9305b414d32e6c1432f91f4 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 6 Dec 2025 16:17:42 +0000
Subject: [PATCH 03/11] Add comprehensive README for auth module documenting
Phase 1 completion
Co-authored-by: jwijgerd <914840+jwijgerd@users.noreply.github.com>
---
test-apps/crypto-trading/auth/README.md | 198 ++++++++++++++++++++++++
1 file changed, 198 insertions(+)
create mode 100644 test-apps/crypto-trading/auth/README.md
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.
From 2b24b1d52ddca68e8372f6cfa8cf901711a7fc3a Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 6 Dec 2025 16:50:14 +0000
Subject: [PATCH 04/11] Fix test failures by adding SecurityConfig to permit
all requests in commands and queries modules
Co-authored-by: jwijgerd <914840+jwijgerd@users.noreply.github.com>
---
.../cryptotrading/ClientConfig.java | 3 +-
.../security/config/SecurityConfig.java | 46 +++++++++++++++++++
.../cryptotrading/ClientConfig.java | 3 +-
.../security/config/SecurityConfig.java | 46 +++++++++++++++++++
4 files changed, 96 insertions(+), 2 deletions(-)
create mode 100644 test-apps/crypto-trading/commands/src/main/java/org/elasticsoftware/cryptotrading/security/config/SecurityConfig.java
create mode 100644 test-apps/crypto-trading/queries/src/main/java/org/elasticsoftware/cryptotrading/security/config/SecurityConfig.java
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..b57328d0
--- /dev/null
+++ b/test-apps/crypto-trading/commands/src/main/java/org/elasticsoftware/cryptotrading/security/config/SecurityConfig.java
@@ -0,0 +1,46 @@
+/*
+ * 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.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;
+
+/**
+ * Security configuration for Commands Service.
+ *
+ * Phase 1: Permits all requests (no authentication required yet).
+ * Phase 2 will add JWT validation using Spring Security OAuth2 Resource Server.
+ */
+@Configuration
+@EnableWebFluxSecurity
+public class SecurityConfig {
+
+ @Bean
+ public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
+ http
+ .csrf(csrf -> csrf.disable())
+ .authorizeExchange(exchanges -> exchanges
+ .anyExchange().permitAll()
+ );
+
+ return http.build();
+ }
+}
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..6041a770
--- /dev/null
+++ b/test-apps/crypto-trading/queries/src/main/java/org/elasticsoftware/cryptotrading/security/config/SecurityConfig.java
@@ -0,0 +1,46 @@
+/*
+ * 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.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;
+
+/**
+ * Security configuration for Queries Service.
+ *
+ *
Phase 1: Permits all requests (no authentication required yet).
+ * Phase 2 will add JWT validation using Spring Security OAuth2 Resource Server.
+ */
+@Configuration
+@EnableWebFluxSecurity
+public class SecurityConfig {
+
+ @Bean
+ public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
+ http
+ .csrf(csrf -> csrf.disable())
+ .authorizeExchange(exchanges -> exchanges
+ .anyExchange().permitAll()
+ );
+
+ return http.build();
+ }
+}
From 34c6b2766b3b6d691e119bd0f56a248c8ac64227 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 6 Dec 2025 17:18:33 +0000
Subject: [PATCH 05/11] Add comprehensive security documentation and warnings
to SecurityConfig classes
Co-authored-by: jwijgerd <914840+jwijgerd@users.noreply.github.com>
---
.../security/config/SecurityConfig.java | 39 +++++++++++++++++--
.../security/config/SecurityConfig.java | 39 +++++++++++++++++--
2 files changed, 72 insertions(+), 6 deletions(-)
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
index b57328d0..b5bfe853 100644
--- 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
@@ -26,19 +26,52 @@
/**
* Security configuration for Commands Service.
*
- *
Phase 1: Permits all requests (no authentication required yet).
- * Phase 2 will add JWT validation using Spring Security OAuth2 Resource Server.
+ *
WARNING: This is a Phase 1 foundation setup configuration.
+ *
Current configuration permits all requests without authentication to allow tests to pass
+ * during the foundation setup phase. This is intentionally insecure and must be replaced in Phase 2.
+ *
+ *
Phase 2 TODO:
+ *
+ * - Enable JWT validation using Spring Security OAuth2 Resource Server
+ * - Configure proper authorization rules for endpoints
+ * - Keep CSRF disabled (appropriate for stateless JWT-based APIs)
+ * - Add security headers (X-Frame-Options, X-Content-Type-Options, etc.)
+ *
+ *
+ * CSRF Protection: Disabled because this API will use JWT tokens
+ * for stateless authentication. CSRF protection is not needed for stateless APIs
+ * where authentication is via bearer tokens rather than cookies.
+ *
+ * @see Spring Security CSRF
*/
@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {
+ /**
+ * Configures the security filter chain.
+ *
+ *
SECURITY WARNING: This configuration permits all requests without authentication.
+ * This is only acceptable for Phase 1 foundation setup and must be replaced with proper
+ * JWT validation in Phase 2.
+ *
+ * @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())
+ // TODO Phase 2: Replace with proper JWT validation
+ // Example Phase 2 configuration:
+ // .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
+ // .authorizeExchange(exchanges -> exchanges
+ // .pathMatchers("/actuator/health", "/actuator/info").permitAll()
+ // .anyExchange().authenticated()
+ // )
.authorizeExchange(exchanges -> exchanges
- .anyExchange().permitAll()
+ .anyExchange().permitAll() // WARNING: Permits all requests without authentication
);
return http.build();
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
index 6041a770..d0ce22a4 100644
--- 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
@@ -26,19 +26,52 @@
/**
* Security configuration for Queries Service.
*
- *
Phase 1: Permits all requests (no authentication required yet).
- * Phase 2 will add JWT validation using Spring Security OAuth2 Resource Server.
+ *
WARNING: This is a Phase 1 foundation setup configuration.
+ *
Current configuration permits all requests without authentication to allow tests to pass
+ * during the foundation setup phase. This is intentionally insecure and must be replaced in Phase 2.
+ *
+ *
Phase 2 TODO:
+ *
+ * - Enable JWT validation using Spring Security OAuth2 Resource Server
+ * - Configure proper authorization rules for endpoints
+ * - Keep CSRF disabled (appropriate for stateless JWT-based APIs)
+ * - Add security headers (X-Frame-Options, X-Content-Type-Options, etc.)
+ *
+ *
+ * CSRF Protection: Disabled because this API will use JWT tokens
+ * for stateless authentication. CSRF protection is not needed for stateless APIs
+ * where authentication is via bearer tokens rather than cookies.
+ *
+ * @see Spring Security CSRF
*/
@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {
+ /**
+ * Configures the security filter chain.
+ *
+ *
SECURITY WARNING: This configuration permits all requests without authentication.
+ * This is only acceptable for Phase 1 foundation setup and must be replaced with proper
+ * JWT validation in Phase 2.
+ *
+ * @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())
+ // TODO Phase 2: Replace with proper JWT validation
+ // Example Phase 2 configuration:
+ // .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
+ // .authorizeExchange(exchanges -> exchanges
+ // .pathMatchers("/actuator/health", "/actuator/info").permitAll()
+ // .anyExchange().authenticated()
+ // )
.authorizeExchange(exchanges -> exchanges
- .anyExchange().permitAll()
+ .anyExchange().permitAll() // WARNING: Permits all requests without authentication
);
return http.build();
From e6ed6a4e72fec46283eb0f9f84585da07d03f186 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 6 Dec 2025 19:05:11 +0000
Subject: [PATCH 06/11] Implement Phase 2: Core security components with JWT
token provider and OAuth2 handlers
Co-authored-by: jwijgerd <914840+jwijgerd@users.noreply.github.com>
---
.../auth/security/config/SecurityConfig.java | 145 ++++++++++++
.../handler/OAuth2LoginFailureHandler.java | 112 +++++++++
.../handler/OAuth2LoginSuccessHandler.java | 122 ++++++++++
.../auth/security/jwt/JwtTokenProvider.java | 221 ++++++++++++++++++
.../security/model/GoogleOAuth2UserInfo.java | 73 ++++++
.../auth/security/model/OAuth2UserInfo.java | 68 ++++++
.../security/service/OAuth2UserService.java | 172 ++++++++++++++
.../auth/src/main/resources/application.yml | 3 +-
.../security/config/SecurityConfig.java | 76 ++++--
.../jwt/JwtAuthenticationManager.java | 210 +++++++++++++++++
.../src/main/resources/application.properties | 6 +-
.../CryptoTradingCommandApiTest.java | 5 +-
.../security/config/TestSecurityConfig.java | 52 +++++
.../security/config/SecurityConfig.java | 76 ++++--
.../jwt/JwtAuthenticationManager.java | 210 +++++++++++++++++
.../src/main/resources/application.properties | 6 +-
.../CryptoTradingQueryApiTest.java | 5 +-
.../security/config/TestSecurityConfig.java | 52 +++++
18 files changed, 1560 insertions(+), 54 deletions(-)
create mode 100644 test-apps/crypto-trading/auth/src/main/java/org/elasticsoftware/cryptotrading/auth/security/config/SecurityConfig.java
create mode 100644 test-apps/crypto-trading/auth/src/main/java/org/elasticsoftware/cryptotrading/auth/security/handler/OAuth2LoginFailureHandler.java
create mode 100644 test-apps/crypto-trading/auth/src/main/java/org/elasticsoftware/cryptotrading/auth/security/handler/OAuth2LoginSuccessHandler.java
create mode 100644 test-apps/crypto-trading/auth/src/main/java/org/elasticsoftware/cryptotrading/auth/security/jwt/JwtTokenProvider.java
create mode 100644 test-apps/crypto-trading/auth/src/main/java/org/elasticsoftware/cryptotrading/auth/security/model/GoogleOAuth2UserInfo.java
create mode 100644 test-apps/crypto-trading/auth/src/main/java/org/elasticsoftware/cryptotrading/auth/security/model/OAuth2UserInfo.java
create mode 100644 test-apps/crypto-trading/auth/src/main/java/org/elasticsoftware/cryptotrading/auth/security/service/OAuth2UserService.java
create mode 100644 test-apps/crypto-trading/commands/src/main/java/org/elasticsoftware/cryptotrading/security/jwt/JwtAuthenticationManager.java
create mode 100644 test-apps/crypto-trading/commands/src/test/java/org/elasticsoftware/cryptotrading/security/config/TestSecurityConfig.java
create mode 100644 test-apps/crypto-trading/queries/src/main/java/org/elasticsoftware/cryptotrading/security/jwt/JwtAuthenticationManager.java
create mode 100644 test-apps/crypto-trading/queries/src/test/java/org/elasticsoftware/cryptotrading/security/config/TestSecurityConfig.java
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..50932328
--- /dev/null
+++ b/test-apps/crypto-trading/auth/src/main/java/org/elasticsoftware/cryptotrading/auth/security/config/SecurityConfig.java
@@ -0,0 +1,145 @@
+/*
+ * 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:
+ *
+ * - OAuth2 login with custom success/failure handlers
+ * - Custom user service for Akces integration
+ * - CORS configuration for frontend integration
+ * - Public actuator endpoints for health checks
+ *
+ *
+ * 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
+ // CSRF disabled for stateless JWT-based API
+ .csrf(csrf -> csrf.disable())
+
+ // CORS configuration for frontend integration
+ .cors(cors -> cors.configurationSource(corsConfigurationSource()))
+
+ // Authorization rules
+ .authorizeExchange(exchanges -> exchanges
+ // Public endpoints
+ .pathMatchers(
+ "/actuator/health",
+ "/actuator/info",
+ "/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:
+ *
+ * - Allowed origins to specific frontend domains
+ * - Allowed methods to only required HTTP methods
+ * - Allowed headers to only required headers
+ * - Credentials based on authentication requirements
+ *
+ *
+ * @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:
+ *
+ * - User denies authorization
+ * - OAuth2 provider returns an error
+ * - Network errors during token exchange
+ * - Invalid OAuth2 configuration
+ *
+ *
+ * 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..69635703
--- /dev/null
+++ b/test-apps/crypto-trading/auth/src/main/java/org/elasticsoftware/cryptotrading/auth/security/jwt/JwtTokenProvider.java
@@ -0,0 +1,221 @@
+/*
+ * 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 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 javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.charset.StandardCharsets;
+import java.time.Instant;
+import java.util.Base64;
+import java.util.Map;
+
+/**
+ * JWT Token Provider for generating and signing JWT tokens.
+ *
+ * This implementation uses HS256 (HMAC with SHA-256) for token signing with a configurable secret.
+ * In production environments, this should be replaced with RS256 using GCP Service Account keys
+ * for enhanced security.
+ *
+ *
Security Note: This implementation is simplified for development and testing.
+ * Production deployments should use asymmetric signing (RS256) with proper key management through
+ * GCP Secret Manager or similar services.
+ */
+@Component
+public class JwtTokenProvider {
+
+ private static final Logger logger = LoggerFactory.getLogger(JwtTokenProvider.class);
+
+ private static final String HMAC_SHA256 = "HmacSHA256";
+
+ private final String jwtSecret;
+ private final long accessTokenExpiration;
+ private final long refreshTokenExpiration;
+ private final String issuer;
+
+ /**
+ * Constructs a JWT Token Provider with configuration from application properties.
+ *
+ * @param jwtSecret the secret key for signing tokens
+ * @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(
+ @Value("${app.jwt.secret:default-secret-change-in-production}") String jwtSecret,
+ @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.jwtSecret = jwtSecret;
+ this.accessTokenExpiration = accessTokenExpiration;
+ this.refreshTokenExpiration = refreshTokenExpiration;
+ this.issuer = issuer;
+
+ if ("default-secret-change-in-production".equals(jwtSecret)) {
+ logger.warn("Using default JWT secret. Please configure app.jwt.secret for production use.");
+ }
+ }
+
+ /**
+ * 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();
+
+ long nowMillis = System.currentTimeMillis();
+ long expMillis = nowMillis + accessTokenExpiration;
+
+ String email = oauth2User.getAttribute("email");
+ String name = oauth2User.getAttribute("name");
+ String userId = oauth2User.getAttribute("sub");
+
+ return generateToken(userId, email, name, nowMillis, expMillis, "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();
+
+ long nowMillis = System.currentTimeMillis();
+ long expMillis = nowMillis + refreshTokenExpiration;
+
+ String userId = oauth2User.getAttribute("sub");
+
+ return generateToken(userId, null, null, nowMillis, expMillis, "refresh");
+ }
+
+ /**
+ * Generates a JWT token with the specified claims.
+ *
+ * @param userId the user identifier
+ * @param email the user email (nullable for refresh tokens)
+ * @param name the user name (nullable for refresh tokens)
+ * @param issuedAtMillis the token issuance time in milliseconds
+ * @param expirationMillis the token expiration time in milliseconds
+ * @param tokenType the token type ("access" or "refresh")
+ * @return the generated JWT token
+ */
+ private String generateToken(String userId, String email, String name,
+ long issuedAtMillis, long expirationMillis,
+ String tokenType) {
+ try {
+ // Create JWT header
+ String header = createBase64UrlEncodedJson(Map.of(
+ "alg", "HS256",
+ "typ", "JWT"
+ ));
+
+ // Create JWT payload
+ var claimsBuilder = new java.util.HashMap();
+ claimsBuilder.put("sub", userId);
+ claimsBuilder.put("iss", issuer);
+ claimsBuilder.put("iat", issuedAtMillis / 1000);
+ claimsBuilder.put("exp", expirationMillis / 1000);
+ claimsBuilder.put("token_type", tokenType);
+
+ if (email != null) {
+ claimsBuilder.put("email", email);
+ }
+ if (name != null) {
+ claimsBuilder.put("name", name);
+ }
+
+ String payload = createBase64UrlEncodedJson(claimsBuilder);
+
+ // Create signature
+ String headerAndPayload = header + "." + payload;
+ String signature = signHmacSha256(headerAndPayload);
+
+ return headerAndPayload + "." + signature;
+
+ } catch (Exception e) {
+ logger.error("Error generating JWT token", e);
+ throw new RuntimeException("Failed to generate JWT token", e);
+ }
+ }
+
+ /**
+ * Creates a Base64 URL-encoded JSON string from a map.
+ *
+ * @param data the data to encode
+ * @return the Base64 URL-encoded JSON string
+ */
+ private String createBase64UrlEncodedJson(Map data) {
+ StringBuilder json = new StringBuilder("{");
+ boolean first = true;
+ for (Map.Entry entry : data.entrySet()) {
+ if (!first) {
+ json.append(",");
+ }
+ first = false;
+ json.append("\"").append(entry.getKey()).append("\":");
+ Object value = entry.getValue();
+ if (value instanceof String) {
+ json.append("\"").append(value).append("\"");
+ } else {
+ json.append(value);
+ }
+ }
+ json.append("}");
+
+ return base64UrlEncode(json.toString().getBytes(StandardCharsets.UTF_8));
+ }
+
+ /**
+ * Signs data using HMAC-SHA256.
+ *
+ * @param data the data to sign
+ * @return the Base64 URL-encoded signature
+ */
+ private String signHmacSha256(String data) throws Exception {
+ Mac mac = Mac.getInstance(HMAC_SHA256);
+ SecretKeySpec secretKeySpec = new SecretKeySpec(
+ jwtSecret.getBytes(StandardCharsets.UTF_8),
+ HMAC_SHA256
+ );
+ mac.init(secretKeySpec);
+ byte[] signature = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
+ return base64UrlEncode(signature);
+ }
+
+ /**
+ * Base64 URL-encodes the given bytes.
+ *
+ * @param bytes the bytes to encode
+ * @return the Base64 URL-encoded string
+ */
+ private String base64UrlEncode(byte[] bytes) {
+ return Base64.getUrlEncoder()
+ .withoutPadding()
+ .encodeToString(bytes);
+ }
+}
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:
+ *
+ * - sub - The unique Google user ID
+ * - email - The user's email address
+ * - name - The user's full name
+ * - picture - The user's profile picture URL
+ * - email_verified - Whether the email has been verified
+ *
+ *
+ * @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:
+ *
+ * - Delegates to the default implementation to fetch user details from the provider
+ * - Extracts provider-specific user information
+ * - Creates a new user account in the system if this is the user's first login
+ *
+ *
+ * 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/resources/application.yml b/test-apps/crypto-trading/auth/src/main/resources/application.yml
index 10920855..65981a5d 100644
--- a/test-apps/crypto-trading/auth/src/main/resources/application.yml
+++ b/test-apps/crypto-trading/auth/src/main/resources/application.yml
@@ -79,11 +79,10 @@ akces:
# JWT Configuration
app:
jwt:
- gcp-service-account-key: ${GCP_SERVICE_ACCOUNT_KEY:}
+ secret: ${JWT_SECRET:default-secret-change-in-production}
access-token-expiration: 900000 # 15 minutes in milliseconds
refresh-token-expiration: 604800000 # 7 days in milliseconds
issuer: akces-crypto-trading
- service-account-email: ${GCP_SERVICE_ACCOUNT_EMAIL:}
# Logging
logging:
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
index b5bfe853..6bf25b3f 100644
--- 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
@@ -17,28 +17,26 @@
package org.elasticsoftware.cryptotrading.security.config;
+import org.elasticsoftware.cryptotrading.security.jwt.JwtAuthenticationManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
+import org.springframework.http.HttpHeaders;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
+import org.springframework.security.config.web.server.SecurityWebFiltersOrder;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;
+import org.springframework.security.web.server.authentication.AuthenticationWebFilter;
+import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
+import reactor.core.publisher.Mono;
/**
* Security configuration for Commands Service.
*
- * WARNING: This is a Phase 1 foundation setup configuration.
- *
Current configuration permits all requests without authentication to allow tests to pass
- * during the foundation setup phase. This is intentionally insecure and must be replaced in Phase 2.
+ *
This configuration validates JWT tokens from the Authorization header
+ * and requires authentication for all endpoints except actuator health checks.
*
- *
Phase 2 TODO:
- *
- * - Enable JWT validation using Spring Security OAuth2 Resource Server
- * - Configure proper authorization rules for endpoints
- * - Keep CSRF disabled (appropriate for stateless JWT-based APIs)
- * - Add security headers (X-Frame-Options, X-Content-Type-Options, etc.)
- *
- *
- * CSRF Protection: Disabled because this API will use JWT tokens
+ *
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.
*
@@ -48,32 +46,62 @@
@EnableWebFluxSecurity
public class SecurityConfig {
+ private final JwtAuthenticationManager jwtAuthenticationManager;
+
/**
- * Configures the security filter chain.
+ * Constructs the security configuration with JWT authentication manager.
*
- *
SECURITY WARNING: This configuration permits all requests without authentication.
- * This is only acceptable for Phase 1 foundation setup and must be replaced with proper
- * JWT validation in Phase 2.
+ * @param jwtAuthenticationManager the JWT authentication manager
+ */
+ public SecurityConfig(JwtAuthenticationManager jwtAuthenticationManager) {
+ this.jwtAuthenticationManager = jwtAuthenticationManager;
+ }
+
+ /**
+ * Configures the security filter chain with JWT validation.
*
* @param http the server HTTP security configuration
* @return the configured security web filter chain
*/
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
+ // Create JWT authentication filter
+ AuthenticationWebFilter jwtFilter = new AuthenticationWebFilter(jwtAuthenticationManager);
+ jwtFilter.setServerAuthenticationConverter(jwtAuthenticationConverter());
+
http
// CSRF disabled for stateless JWT-based API (no session cookies)
.csrf(csrf -> csrf.disable())
- // TODO Phase 2: Replace with proper JWT validation
- // Example Phase 2 configuration:
- // .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
- // .authorizeExchange(exchanges -> exchanges
- // .pathMatchers("/actuator/health", "/actuator/info").permitAll()
- // .anyExchange().authenticated()
- // )
+
+ // Add JWT authentication filter
+ .addFilterAt(jwtFilter, SecurityWebFiltersOrder.AUTHENTICATION)
+
+ // Authorization rules
.authorizeExchange(exchanges -> exchanges
- .anyExchange().permitAll() // WARNING: Permits all requests without authentication
+ // Public endpoints
+ .pathMatchers("/actuator/health", "/actuator/info").permitAll()
+ // All other endpoints require authentication
+ .anyExchange().authenticated()
);
return http.build();
}
+
+ /**
+ * Converts the JWT token from the Authorization header to an Authentication object.
+ *
+ * @return the server authentication converter
+ */
+ private ServerAuthenticationConverter jwtAuthenticationConverter() {
+ return exchange -> {
+ String authHeader = exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
+
+ if (authHeader != null && authHeader.startsWith("Bearer ")) {
+ String token = authHeader.substring(7);
+ return Mono.just(new UsernamePasswordAuthenticationToken(token, token));
+ }
+
+ return Mono.empty();
+ };
+ }
}
diff --git a/test-apps/crypto-trading/commands/src/main/java/org/elasticsoftware/cryptotrading/security/jwt/JwtAuthenticationManager.java b/test-apps/crypto-trading/commands/src/main/java/org/elasticsoftware/cryptotrading/security/jwt/JwtAuthenticationManager.java
new file mode 100644
index 00000000..08d0bc3e
--- /dev/null
+++ b/test-apps/crypto-trading/commands/src/main/java/org/elasticsoftware/cryptotrading/security/jwt/JwtAuthenticationManager.java
@@ -0,0 +1,210 @@
+/*
+ * 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.jwt;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.security.authentication.ReactiveAuthenticationManager;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.stereotype.Component;
+import reactor.core.publisher.Mono;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.Map;
+
+/**
+ * JWT Authentication Manager for validating JWT tokens.
+ *
+ *
This manager validates JWT tokens signed with HS256 (HMAC with SHA-256).
+ * It verifies the token signature and checks expiration, then creates an
+ * Authentication object for authorized requests.
+ *
+ *
Security Note: This implementation uses symmetric key signing (HS256).
+ * In production environments with multiple services, consider using asymmetric signing (RS256)
+ * where the auth service signs with a private key and resource servers validate with a public key.
+ */
+@Component
+public class JwtAuthenticationManager implements ReactiveAuthenticationManager {
+
+ private static final String HMAC_SHA256 = "HmacSHA256";
+
+ private final String jwtSecret;
+ private final String issuer;
+
+ /**
+ * Constructs the JWT authentication manager with configuration.
+ *
+ * @param jwtSecret the secret key for validating tokens
+ * @param issuer the expected issuer claim
+ */
+ public JwtAuthenticationManager(
+ @Value("${app.jwt.secret:default-secret-change-in-production}") String jwtSecret,
+ @Value("${app.jwt.issuer:akces-crypto-trading}") String issuer) {
+ this.jwtSecret = jwtSecret;
+ this.issuer = issuer;
+ }
+
+ /**
+ * Authenticates the JWT token.
+ *
+ * @param authentication the authentication object containing the JWT token
+ * @return a Mono emitting the authenticated principal
+ */
+ @Override
+ public Mono authenticate(Authentication authentication) {
+ String token = authentication.getCredentials().toString();
+
+ return Mono.fromCallable(() -> validateToken(token))
+ .flatMap(claims -> {
+ String userId = (String) claims.get("sub");
+ String email = (String) claims.get("email");
+
+ // Create authenticated token with user details
+ UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
+ userId,
+ null,
+ Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER"))
+ );
+
+ // Add additional details
+ auth.setDetails(Map.of(
+ "email", email != null ? email : "",
+ "sub", userId
+ ));
+
+ return Mono.just(auth);
+ })
+ .onErrorResume(e -> Mono.empty());
+ }
+
+ /**
+ * Validates the JWT token and extracts claims.
+ *
+ * @param token the JWT token to validate
+ * @return the claims map
+ * @throws Exception if validation fails
+ */
+ private Map validateToken(String token) throws Exception {
+ String[] parts = token.split("\\.");
+ if (parts.length != 3) {
+ throw new IllegalArgumentException("Invalid JWT token format");
+ }
+
+ String headerAndPayload = parts[0] + "." + parts[1];
+ String signature = parts[2];
+
+ // Verify signature
+ String expectedSignature = signHmacSha256(headerAndPayload);
+ if (!signature.equals(expectedSignature)) {
+ throw new SecurityException("Invalid JWT signature");
+ }
+
+ // Decode and parse payload
+ byte[] payloadBytes = Base64.getUrlDecoder().decode(parts[1]);
+ String payloadJson = new String(payloadBytes, StandardCharsets.UTF_8);
+
+ // Simple JSON parsing for claims
+ Map claims = parseJsonClaims(payloadJson);
+
+ // Verify issuer
+ String tokenIssuer = (String) claims.get("iss");
+ if (!issuer.equals(tokenIssuer)) {
+ throw new SecurityException("Invalid issuer");
+ }
+
+ // Verify expiration
+ Number exp = (Number) claims.get("exp");
+ if (exp != null) {
+ long expirationTime = exp.longValue();
+ long currentTime = System.currentTimeMillis() / 1000;
+ if (currentTime > expirationTime) {
+ throw new SecurityException("Token expired");
+ }
+ }
+
+ return claims;
+ }
+
+ /**
+ * Signs data using HMAC-SHA256.
+ *
+ * @param data the data to sign
+ * @return the Base64 URL-encoded signature
+ */
+ private String signHmacSha256(String data) throws Exception {
+ Mac mac = Mac.getInstance(HMAC_SHA256);
+ SecretKeySpec secretKeySpec = new SecretKeySpec(
+ jwtSecret.getBytes(StandardCharsets.UTF_8),
+ HMAC_SHA256
+ );
+ mac.init(secretKeySpec);
+ byte[] signature = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
+ return Base64.getUrlEncoder().withoutPadding().encodeToString(signature);
+ }
+
+ /**
+ * Simple JSON parser for JWT claims.
+ *
+ * @param json the JSON string
+ * @return the parsed claims map
+ */
+ private Map parseJsonClaims(String json) {
+ Map claims = new java.util.HashMap<>();
+
+ // Remove braces
+ json = json.trim().substring(1, json.length() - 1);
+
+ // Split by comma (simple parser, assumes no nested objects)
+ String[] pairs = json.split(",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)");
+
+ for (String pair : pairs) {
+ String[] keyValue = pair.split(":", 2);
+ if (keyValue.length == 2) {
+ String key = keyValue[0].trim().replaceAll("\"", "");
+ String value = keyValue[1].trim();
+
+ // Parse value type
+ if (value.startsWith("\"")) {
+ // String value
+ claims.put(key, value.replaceAll("\"", ""));
+ } else if (value.equals("true") || value.equals("false")) {
+ // Boolean value
+ claims.put(key, Boolean.parseBoolean(value));
+ } else {
+ // Number value
+ try {
+ if (value.contains(".")) {
+ claims.put(key, Double.parseDouble(value));
+ } else {
+ claims.put(key, Long.parseLong(value));
+ }
+ } catch (NumberFormatException e) {
+ claims.put(key, value);
+ }
+ }
+ }
+ }
+
+ return claims;
+ }
+}
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..6a6a7e8a 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,8 @@ 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
+
+# JWT Configuration for validation
+app.jwt.secret=${JWT_SECRET:default-secret-change-in-production}
+app.jwt.issuer=akces-crypto-trading
\ 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/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
index d0ce22a4..bd0d3f06 100644
--- 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
@@ -17,28 +17,26 @@
package org.elasticsoftware.cryptotrading.security.config;
+import org.elasticsoftware.cryptotrading.security.jwt.JwtAuthenticationManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
+import org.springframework.http.HttpHeaders;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
+import org.springframework.security.config.web.server.SecurityWebFiltersOrder;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;
+import org.springframework.security.web.server.authentication.AuthenticationWebFilter;
+import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
+import reactor.core.publisher.Mono;
/**
* Security configuration for Queries Service.
*
- *
WARNING: This is a Phase 1 foundation setup configuration.
- *
Current configuration permits all requests without authentication to allow tests to pass
- * during the foundation setup phase. This is intentionally insecure and must be replaced in Phase 2.
+ *
This configuration validates JWT tokens from the Authorization header
+ * and requires authentication for all endpoints except actuator health checks.
*
- *
Phase 2 TODO:
- *
- * - Enable JWT validation using Spring Security OAuth2 Resource Server
- * - Configure proper authorization rules for endpoints
- * - Keep CSRF disabled (appropriate for stateless JWT-based APIs)
- * - Add security headers (X-Frame-Options, X-Content-Type-Options, etc.)
- *
- *
- * CSRF Protection: Disabled because this API will use JWT tokens
+ *
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.
*
@@ -48,32 +46,62 @@
@EnableWebFluxSecurity
public class SecurityConfig {
+ private final JwtAuthenticationManager jwtAuthenticationManager;
+
/**
- * Configures the security filter chain.
+ * Constructs the security configuration with JWT authentication manager.
*
- *
SECURITY WARNING: This configuration permits all requests without authentication.
- * This is only acceptable for Phase 1 foundation setup and must be replaced with proper
- * JWT validation in Phase 2.
+ * @param jwtAuthenticationManager the JWT authentication manager
+ */
+ public SecurityConfig(JwtAuthenticationManager jwtAuthenticationManager) {
+ this.jwtAuthenticationManager = jwtAuthenticationManager;
+ }
+
+ /**
+ * Configures the security filter chain with JWT validation.
*
* @param http the server HTTP security configuration
* @return the configured security web filter chain
*/
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
+ // Create JWT authentication filter
+ AuthenticationWebFilter jwtFilter = new AuthenticationWebFilter(jwtAuthenticationManager);
+ jwtFilter.setServerAuthenticationConverter(jwtAuthenticationConverter());
+
http
// CSRF disabled for stateless JWT-based API (no session cookies)
.csrf(csrf -> csrf.disable())
- // TODO Phase 2: Replace with proper JWT validation
- // Example Phase 2 configuration:
- // .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
- // .authorizeExchange(exchanges -> exchanges
- // .pathMatchers("/actuator/health", "/actuator/info").permitAll()
- // .anyExchange().authenticated()
- // )
+
+ // Add JWT authentication filter
+ .addFilterAt(jwtFilter, SecurityWebFiltersOrder.AUTHENTICATION)
+
+ // Authorization rules
.authorizeExchange(exchanges -> exchanges
- .anyExchange().permitAll() // WARNING: Permits all requests without authentication
+ // Public endpoints
+ .pathMatchers("/actuator/health", "/actuator/info").permitAll()
+ // All other endpoints require authentication
+ .anyExchange().authenticated()
);
return http.build();
}
+
+ /**
+ * Converts the JWT token from the Authorization header to an Authentication object.
+ *
+ * @return the server authentication converter
+ */
+ private ServerAuthenticationConverter jwtAuthenticationConverter() {
+ return exchange -> {
+ String authHeader = exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
+
+ if (authHeader != null && authHeader.startsWith("Bearer ")) {
+ String token = authHeader.substring(7);
+ return Mono.just(new UsernamePasswordAuthenticationToken(token, token));
+ }
+
+ return Mono.empty();
+ };
+ }
}
diff --git a/test-apps/crypto-trading/queries/src/main/java/org/elasticsoftware/cryptotrading/security/jwt/JwtAuthenticationManager.java b/test-apps/crypto-trading/queries/src/main/java/org/elasticsoftware/cryptotrading/security/jwt/JwtAuthenticationManager.java
new file mode 100644
index 00000000..08d0bc3e
--- /dev/null
+++ b/test-apps/crypto-trading/queries/src/main/java/org/elasticsoftware/cryptotrading/security/jwt/JwtAuthenticationManager.java
@@ -0,0 +1,210 @@
+/*
+ * 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.jwt;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.security.authentication.ReactiveAuthenticationManager;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.stereotype.Component;
+import reactor.core.publisher.Mono;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.Map;
+
+/**
+ * JWT Authentication Manager for validating JWT tokens.
+ *
+ *
This manager validates JWT tokens signed with HS256 (HMAC with SHA-256).
+ * It verifies the token signature and checks expiration, then creates an
+ * Authentication object for authorized requests.
+ *
+ *
Security Note: This implementation uses symmetric key signing (HS256).
+ * In production environments with multiple services, consider using asymmetric signing (RS256)
+ * where the auth service signs with a private key and resource servers validate with a public key.
+ */
+@Component
+public class JwtAuthenticationManager implements ReactiveAuthenticationManager {
+
+ private static final String HMAC_SHA256 = "HmacSHA256";
+
+ private final String jwtSecret;
+ private final String issuer;
+
+ /**
+ * Constructs the JWT authentication manager with configuration.
+ *
+ * @param jwtSecret the secret key for validating tokens
+ * @param issuer the expected issuer claim
+ */
+ public JwtAuthenticationManager(
+ @Value("${app.jwt.secret:default-secret-change-in-production}") String jwtSecret,
+ @Value("${app.jwt.issuer:akces-crypto-trading}") String issuer) {
+ this.jwtSecret = jwtSecret;
+ this.issuer = issuer;
+ }
+
+ /**
+ * Authenticates the JWT token.
+ *
+ * @param authentication the authentication object containing the JWT token
+ * @return a Mono emitting the authenticated principal
+ */
+ @Override
+ public Mono authenticate(Authentication authentication) {
+ String token = authentication.getCredentials().toString();
+
+ return Mono.fromCallable(() -> validateToken(token))
+ .flatMap(claims -> {
+ String userId = (String) claims.get("sub");
+ String email = (String) claims.get("email");
+
+ // Create authenticated token with user details
+ UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
+ userId,
+ null,
+ Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER"))
+ );
+
+ // Add additional details
+ auth.setDetails(Map.of(
+ "email", email != null ? email : "",
+ "sub", userId
+ ));
+
+ return Mono.just(auth);
+ })
+ .onErrorResume(e -> Mono.empty());
+ }
+
+ /**
+ * Validates the JWT token and extracts claims.
+ *
+ * @param token the JWT token to validate
+ * @return the claims map
+ * @throws Exception if validation fails
+ */
+ private Map validateToken(String token) throws Exception {
+ String[] parts = token.split("\\.");
+ if (parts.length != 3) {
+ throw new IllegalArgumentException("Invalid JWT token format");
+ }
+
+ String headerAndPayload = parts[0] + "." + parts[1];
+ String signature = parts[2];
+
+ // Verify signature
+ String expectedSignature = signHmacSha256(headerAndPayload);
+ if (!signature.equals(expectedSignature)) {
+ throw new SecurityException("Invalid JWT signature");
+ }
+
+ // Decode and parse payload
+ byte[] payloadBytes = Base64.getUrlDecoder().decode(parts[1]);
+ String payloadJson = new String(payloadBytes, StandardCharsets.UTF_8);
+
+ // Simple JSON parsing for claims
+ Map claims = parseJsonClaims(payloadJson);
+
+ // Verify issuer
+ String tokenIssuer = (String) claims.get("iss");
+ if (!issuer.equals(tokenIssuer)) {
+ throw new SecurityException("Invalid issuer");
+ }
+
+ // Verify expiration
+ Number exp = (Number) claims.get("exp");
+ if (exp != null) {
+ long expirationTime = exp.longValue();
+ long currentTime = System.currentTimeMillis() / 1000;
+ if (currentTime > expirationTime) {
+ throw new SecurityException("Token expired");
+ }
+ }
+
+ return claims;
+ }
+
+ /**
+ * Signs data using HMAC-SHA256.
+ *
+ * @param data the data to sign
+ * @return the Base64 URL-encoded signature
+ */
+ private String signHmacSha256(String data) throws Exception {
+ Mac mac = Mac.getInstance(HMAC_SHA256);
+ SecretKeySpec secretKeySpec = new SecretKeySpec(
+ jwtSecret.getBytes(StandardCharsets.UTF_8),
+ HMAC_SHA256
+ );
+ mac.init(secretKeySpec);
+ byte[] signature = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
+ return Base64.getUrlEncoder().withoutPadding().encodeToString(signature);
+ }
+
+ /**
+ * Simple JSON parser for JWT claims.
+ *
+ * @param json the JSON string
+ * @return the parsed claims map
+ */
+ private Map parseJsonClaims(String json) {
+ Map claims = new java.util.HashMap<>();
+
+ // Remove braces
+ json = json.trim().substring(1, json.length() - 1);
+
+ // Split by comma (simple parser, assumes no nested objects)
+ String[] pairs = json.split(",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)");
+
+ for (String pair : pairs) {
+ String[] keyValue = pair.split(":", 2);
+ if (keyValue.length == 2) {
+ String key = keyValue[0].trim().replaceAll("\"", "");
+ String value = keyValue[1].trim();
+
+ // Parse value type
+ if (value.startsWith("\"")) {
+ // String value
+ claims.put(key, value.replaceAll("\"", ""));
+ } else if (value.equals("true") || value.equals("false")) {
+ // Boolean value
+ claims.put(key, Boolean.parseBoolean(value));
+ } else {
+ // Number value
+ try {
+ if (value.contains(".")) {
+ claims.put(key, Double.parseDouble(value));
+ } else {
+ claims.put(key, Long.parseLong(value));
+ }
+ } catch (NumberFormatException e) {
+ claims.put(key, value);
+ }
+ }
+ }
+ }
+
+ return claims;
+ }
+}
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..f0d1f86f 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,8 @@ 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
+
+# JWT Configuration for validation
+app.jwt.secret=${JWT_SECRET:default-secret-change-in-production}
+app.jwt.issuer=akces-crypto-trading
\ 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();
+ }
+}
From 09125964359c8b3b3b91c83caf8b319f4a3df986 Mon Sep 17 00:00:00 2001
From: Joost van de Wijgerd
Date: Sat, 6 Dec 2025 20:25:52 +0100
Subject: [PATCH 07/11] Potential fix for code scanning alert no. 7: Disabled
Spring CSRF protection
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
---
.../cryptotrading/auth/security/config/SecurityConfig.java | 3 ---
1 file changed, 3 deletions(-)
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
index 50932328..b3335640 100644
--- 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
@@ -81,9 +81,6 @@ public SecurityConfig(
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
http
- // CSRF disabled for stateless JWT-based API
- .csrf(csrf -> csrf.disable())
-
// CORS configuration for frontend integration
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
From 1f6360c428ad342f964e0de96010ace15abbc03d Mon Sep 17 00:00:00 2001
From: jwijgerd
Date: Sat, 6 Dec 2025 21:04:23 +0100
Subject: [PATCH 08/11] fix CodeQL security alerts
---
plans/gmail-oauth-authentication.md | 2 --
.../auth/security/config/SecurityConfig.java | 4 +---
.../cryptotrading/security/config/SecurityConfig.java | 8 +-------
.../cryptotrading/security/config/TestSecurityConfig.java | 1 -
.../cryptotrading/security/config/SecurityConfig.java | 8 +-------
.../cryptotrading/security/config/TestSecurityConfig.java | 1 -
6 files changed, 3 insertions(+), 21 deletions(-)
diff --git a/plans/gmail-oauth-authentication.md b/plans/gmail-oauth-authentication.md
index aa1b0f9b..a02a6aa2 100644
--- a/plans/gmail-oauth-authentication.md
+++ b/plans/gmail-oauth-authentication.md
@@ -594,7 +594,6 @@ public class SecurityConfig {
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
http
- .csrf(csrf -> csrf.disable())
.authorizeExchange(exchanges -> exchanges
.pathMatchers("/actuator/health").permitAll()
.anyExchange().authenticated()
@@ -659,7 +658,6 @@ public class JwtAuthenticationWebFilter implements WebFilter {
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
http
- .csrf(csrf -> csrf.disable()) // Disable for stateless JWT
.authorizeExchange(exchanges -> exchanges
// Public endpoints
.pathMatchers("/v1/auth/login", "/v1/auth/callback", "/v1/auth/refresh").permitAll()
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
index b3335640..2a470643 100644
--- 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
@@ -44,9 +44,7 @@
* CORS configuration for frontend integration
* Public actuator endpoints for health checks
*
- *
- * CSRF Protection: Disabled because this service generates JWT tokens
- * for stateless authentication. CSRF protection is not needed for stateless APIs.
+ *
*/
@Configuration
@EnableWebFluxSecurity
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
index 6bf25b3f..f0842026 100644
--- 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
@@ -35,10 +35,7 @@
*
*
This configuration validates JWT tokens from the Authorization header
* and requires authentication for all endpoints except actuator health checks.
- *
- *
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.
+ *
*
* @see Spring Security CSRF
*/
@@ -70,9 +67,6 @@ public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
jwtFilter.setServerAuthenticationConverter(jwtAuthenticationConverter());
http
- // CSRF disabled for stateless JWT-based API (no session cookies)
- .csrf(csrf -> csrf.disable())
-
// Add JWT authentication filter
.addFilterAt(jwtFilter, SecurityWebFiltersOrder.AUTHENTICATION)
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
index b0b3c02a..d6ee39ca 100644
--- 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
@@ -42,7 +42,6 @@ public class TestSecurityConfig {
@Primary
public SecurityWebFilterChain testSecurityWebFilterChain(ServerHttpSecurity http) {
http
- .csrf(csrf -> csrf.disable())
.authorizeExchange(exchanges -> exchanges
.anyExchange().permitAll()
);
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
index bd0d3f06..83e72f18 100644
--- 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
@@ -35,10 +35,7 @@
*
*
This configuration validates JWT tokens from the Authorization header
* and requires authentication for all endpoints except actuator health checks.
- *
- *
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.
+ *
*
* @see Spring Security CSRF
*/
@@ -70,9 +67,6 @@ public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
jwtFilter.setServerAuthenticationConverter(jwtAuthenticationConverter());
http
- // CSRF disabled for stateless JWT-based API (no session cookies)
- .csrf(csrf -> csrf.disable())
-
// Add JWT authentication filter
.addFilterAt(jwtFilter, SecurityWebFiltersOrder.AUTHENTICATION)
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
index b0b3c02a..d6ee39ca 100644
--- 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
@@ -42,7 +42,6 @@ public class TestSecurityConfig {
@Primary
public SecurityWebFilterChain testSecurityWebFilterChain(ServerHttpSecurity http) {
http
- .csrf(csrf -> csrf.disable())
.authorizeExchange(exchanges -> exchanges
.anyExchange().permitAll()
);
From 1a74267afd7f9f980774034f6be2baa25b1dd46e Mon Sep 17 00:00:00 2001
From: jwijgerd
Date: Sat, 6 Dec 2025 21:31:28 +0100
Subject: [PATCH 09/11] Revert "fix CodeQL security alerts"
This reverts commit 1f6360c428ad342f964e0de96010ace15abbc03d.
---
plans/gmail-oauth-authentication.md | 2 ++
.../auth/security/config/SecurityConfig.java | 4 +++-
.../cryptotrading/security/config/SecurityConfig.java | 8 +++++++-
.../cryptotrading/security/config/TestSecurityConfig.java | 1 +
.../cryptotrading/security/config/SecurityConfig.java | 8 +++++++-
.../cryptotrading/security/config/TestSecurityConfig.java | 1 +
6 files changed, 21 insertions(+), 3 deletions(-)
diff --git a/plans/gmail-oauth-authentication.md b/plans/gmail-oauth-authentication.md
index a02a6aa2..aa1b0f9b 100644
--- a/plans/gmail-oauth-authentication.md
+++ b/plans/gmail-oauth-authentication.md
@@ -594,6 +594,7 @@ public class SecurityConfig {
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
http
+ .csrf(csrf -> csrf.disable())
.authorizeExchange(exchanges -> exchanges
.pathMatchers("/actuator/health").permitAll()
.anyExchange().authenticated()
@@ -658,6 +659,7 @@ public class JwtAuthenticationWebFilter implements WebFilter {
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
http
+ .csrf(csrf -> csrf.disable()) // Disable for stateless JWT
.authorizeExchange(exchanges -> exchanges
// Public endpoints
.pathMatchers("/v1/auth/login", "/v1/auth/callback", "/v1/auth/refresh").permitAll()
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
index 2a470643..b3335640 100644
--- 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
@@ -44,7 +44,9 @@
* CORS configuration for frontend integration
* Public actuator endpoints for health checks
*
- *
+ *
+ * CSRF Protection: Disabled because this service generates JWT tokens
+ * for stateless authentication. CSRF protection is not needed for stateless APIs.
*/
@Configuration
@EnableWebFluxSecurity
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
index f0842026..6bf25b3f 100644
--- 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
@@ -35,7 +35,10 @@
*
*
This configuration validates JWT tokens from the Authorization header
* and requires authentication for all endpoints except actuator health checks.
- *
+ *
+ *
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.
*
* @see Spring Security CSRF
*/
@@ -67,6 +70,9 @@ public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
jwtFilter.setServerAuthenticationConverter(jwtAuthenticationConverter());
http
+ // CSRF disabled for stateless JWT-based API (no session cookies)
+ .csrf(csrf -> csrf.disable())
+
// Add JWT authentication filter
.addFilterAt(jwtFilter, SecurityWebFiltersOrder.AUTHENTICATION)
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
index d6ee39ca..b0b3c02a 100644
--- 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
@@ -42,6 +42,7 @@ public class TestSecurityConfig {
@Primary
public SecurityWebFilterChain testSecurityWebFilterChain(ServerHttpSecurity http) {
http
+ .csrf(csrf -> csrf.disable())
.authorizeExchange(exchanges -> exchanges
.anyExchange().permitAll()
);
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
index 83e72f18..bd0d3f06 100644
--- 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
@@ -35,7 +35,10 @@
*
*
This configuration validates JWT tokens from the Authorization header
* and requires authentication for all endpoints except actuator health checks.
- *
+ *
+ *
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.
*
* @see Spring Security CSRF
*/
@@ -67,6 +70,9 @@ public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
jwtFilter.setServerAuthenticationConverter(jwtAuthenticationConverter());
http
+ // CSRF disabled for stateless JWT-based API (no session cookies)
+ .csrf(csrf -> csrf.disable())
+
// Add JWT authentication filter
.addFilterAt(jwtFilter, SecurityWebFiltersOrder.AUTHENTICATION)
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
index d6ee39ca..b0b3c02a 100644
--- 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
@@ -42,6 +42,7 @@ public class TestSecurityConfig {
@Primary
public SecurityWebFilterChain testSecurityWebFilterChain(ServerHttpSecurity http) {
http
+ .csrf(csrf -> csrf.disable())
.authorizeExchange(exchanges -> exchanges
.anyExchange().permitAll()
);
From bb93579ce2246b1210b945cb3d7290e20ff7938d Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 6 Dec 2025 20:42:42 +0000
Subject: [PATCH 10/11] Use JwtAuthenticationToken instead of
UsernamePasswordAuthenticationToken for JWT authentication
Co-authored-by: jwijgerd <914840+jwijgerd@users.noreply.github.com>
---
.../security/config/SecurityConfig.java | 6 ++--
.../jwt/JwtAuthenticationManager.java | 32 ++++++++++++-------
.../security/config/SecurityConfig.java | 6 ++--
.../jwt/JwtAuthenticationManager.java | 32 ++++++++++++-------
4 files changed, 46 insertions(+), 30 deletions(-)
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
index 6bf25b3f..87c96035 100644
--- 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
@@ -21,10 +21,10 @@
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
-import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.SecurityWebFiltersOrder;
import org.springframework.security.config.web.server.ServerHttpSecurity;
+import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthenticationToken;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.authentication.AuthenticationWebFilter;
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
@@ -88,7 +88,7 @@ public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
}
/**
- * Converts the JWT token from the Authorization header to an Authentication object.
+ * Converts the JWT token from the Authorization header to a BearerTokenAuthenticationToken.
*
* @return the server authentication converter
*/
@@ -98,7 +98,7 @@ private ServerAuthenticationConverter jwtAuthenticationConverter() {
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String token = authHeader.substring(7);
- return Mono.just(new UsernamePasswordAuthenticationToken(token, token));
+ return Mono.just(new BearerTokenAuthenticationToken(token));
}
return Mono.empty();
diff --git a/test-apps/crypto-trading/commands/src/main/java/org/elasticsoftware/cryptotrading/security/jwt/JwtAuthenticationManager.java b/test-apps/crypto-trading/commands/src/main/java/org/elasticsoftware/cryptotrading/security/jwt/JwtAuthenticationManager.java
index 08d0bc3e..ad31933d 100644
--- a/test-apps/crypto-trading/commands/src/main/java/org/elasticsoftware/cryptotrading/security/jwt/JwtAuthenticationManager.java
+++ b/test-apps/crypto-trading/commands/src/main/java/org/elasticsoftware/cryptotrading/security/jwt/JwtAuthenticationManager.java
@@ -19,15 +19,17 @@
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
-import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
+import java.time.Instant;
import java.util.Base64;
import java.util.Collections;
import java.util.Map;
@@ -77,21 +79,27 @@ public Mono authenticate(Authentication authentication) {
return Mono.fromCallable(() -> validateToken(token))
.flatMap(claims -> {
String userId = (String) claims.get("sub");
- String email = (String) claims.get("email");
+ String issuerClaim = (String) claims.get("iss");
+ Number iat = (Number) claims.get("iat");
+ Number exp = (Number) claims.get("exp");
- // Create authenticated token with user details
- UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
- userId,
- null,
+ // Create JWT object
+ Jwt jwt = Jwt.withTokenValue(token)
+ .header("alg", "HS256")
+ .header("typ", "JWT")
+ .claims(c -> c.putAll(claims))
+ .subject(userId)
+ .issuer(issuerClaim)
+ .issuedAt(iat != null ? Instant.ofEpochSecond(iat.longValue()) : null)
+ .expiresAt(exp != null ? Instant.ofEpochSecond(exp.longValue()) : null)
+ .build();
+
+ // Create JwtAuthenticationToken with user details and authorities
+ JwtAuthenticationToken auth = new JwtAuthenticationToken(
+ jwt,
Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER"))
);
- // Add additional details
- auth.setDetails(Map.of(
- "email", email != null ? email : "",
- "sub", userId
- ));
-
return Mono.just(auth);
})
.onErrorResume(e -> Mono.empty());
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
index bd0d3f06..7167354e 100644
--- 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
@@ -21,10 +21,10 @@
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
-import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.SecurityWebFiltersOrder;
import org.springframework.security.config.web.server.ServerHttpSecurity;
+import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthenticationToken;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.authentication.AuthenticationWebFilter;
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
@@ -88,7 +88,7 @@ public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
}
/**
- * Converts the JWT token from the Authorization header to an Authentication object.
+ * Converts the JWT token from the Authorization header to a BearerTokenAuthenticationToken.
*
* @return the server authentication converter
*/
@@ -98,7 +98,7 @@ private ServerAuthenticationConverter jwtAuthenticationConverter() {
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String token = authHeader.substring(7);
- return Mono.just(new UsernamePasswordAuthenticationToken(token, token));
+ return Mono.just(new BearerTokenAuthenticationToken(token));
}
return Mono.empty();
diff --git a/test-apps/crypto-trading/queries/src/main/java/org/elasticsoftware/cryptotrading/security/jwt/JwtAuthenticationManager.java b/test-apps/crypto-trading/queries/src/main/java/org/elasticsoftware/cryptotrading/security/jwt/JwtAuthenticationManager.java
index 08d0bc3e..ad31933d 100644
--- a/test-apps/crypto-trading/queries/src/main/java/org/elasticsoftware/cryptotrading/security/jwt/JwtAuthenticationManager.java
+++ b/test-apps/crypto-trading/queries/src/main/java/org/elasticsoftware/cryptotrading/security/jwt/JwtAuthenticationManager.java
@@ -19,15 +19,17 @@
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
-import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
+import java.time.Instant;
import java.util.Base64;
import java.util.Collections;
import java.util.Map;
@@ -77,21 +79,27 @@ public Mono authenticate(Authentication authentication) {
return Mono.fromCallable(() -> validateToken(token))
.flatMap(claims -> {
String userId = (String) claims.get("sub");
- String email = (String) claims.get("email");
+ String issuerClaim = (String) claims.get("iss");
+ Number iat = (Number) claims.get("iat");
+ Number exp = (Number) claims.get("exp");
- // Create authenticated token with user details
- UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
- userId,
- null,
+ // Create JWT object
+ Jwt jwt = Jwt.withTokenValue(token)
+ .header("alg", "HS256")
+ .header("typ", "JWT")
+ .claims(c -> c.putAll(claims))
+ .subject(userId)
+ .issuer(issuerClaim)
+ .issuedAt(iat != null ? Instant.ofEpochSecond(iat.longValue()) : null)
+ .expiresAt(exp != null ? Instant.ofEpochSecond(exp.longValue()) : null)
+ .build();
+
+ // Create JwtAuthenticationToken with user details and authorities
+ JwtAuthenticationToken auth = new JwtAuthenticationToken(
+ jwt,
Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER"))
);
- // Add additional details
- auth.setDetails(Map.of(
- "email", email != null ? email : "",
- "sub", userId
- ));
-
return Mono.just(auth);
})
.onErrorResume(e -> Mono.empty());
From ac1e15134e8f782e6f628e6eeed68e391ce75431 Mon Sep 17 00:00:00 2001
From: GitHub Actions
Date: Sat, 6 Dec 2025 22:31:41 +0000
Subject: [PATCH 11/11] Fix Phase 2: Use RS256 with Nimbus JWT and JWKS instead
of HS256 shared secret
Co-authored-by: jwijgerd <914840+jwijgerd@users.noreply.github.com>
---
plans/phase2-jwt-fix/plan.md | 161 +++++++++++++
.../auth/security/config/SecurityConfig.java | 1 +
.../auth/security/jwt/JwtTokenProvider.java | 165 +++++--------
.../auth/security/jwt/RsaKeyProvider.java | 96 ++++++++
.../auth/web/v1/JwksController.java | 62 +++++
.../auth/src/main/resources/application.yml | 5 +-
.../security/config/SecurityConfig.java | 68 +++---
.../jwt/JwtAuthenticationManager.java | 218 ------------------
.../src/main/resources/application.properties | 5 +-
.../security/config/SecurityConfig.java | 68 +++---
.../jwt/JwtAuthenticationManager.java | 218 ------------------
.../src/main/resources/application.properties | 5 +-
12 files changed, 441 insertions(+), 631 deletions(-)
create mode 100644 plans/phase2-jwt-fix/plan.md
create mode 100644 test-apps/crypto-trading/auth/src/main/java/org/elasticsoftware/cryptotrading/auth/security/jwt/RsaKeyProvider.java
create mode 100644 test-apps/crypto-trading/auth/src/main/java/org/elasticsoftware/cryptotrading/auth/web/v1/JwksController.java
delete mode 100644 test-apps/crypto-trading/commands/src/main/java/org/elasticsoftware/cryptotrading/security/jwt/JwtAuthenticationManager.java
delete mode 100644 test-apps/crypto-trading/queries/src/main/java/org/elasticsoftware/cryptotrading/security/jwt/JwtAuthenticationManager.java
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/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
index b3335640..20ce4038 100644
--- 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
@@ -90,6 +90,7 @@ public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
.pathMatchers(
"/actuator/health",
"/actuator/info",
+ "/.well-known/jwks.json", // JWKS endpoint for JWT validation
"/login/oauth2/**",
"/oauth2/**"
).permitAll()
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
index 69635703..02b58ccc 100644
--- 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
@@ -17,6 +17,13 @@
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;
@@ -24,56 +31,53 @@
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Component;
-import javax.crypto.Mac;
-import javax.crypto.spec.SecretKeySpec;
-import java.nio.charset.StandardCharsets;
import java.time.Instant;
-import java.util.Base64;
-import java.util.Map;
+import java.util.Date;
/**
- * JWT Token Provider for generating and signing JWT tokens.
+ * JWT Token Provider for generating and signing JWT tokens using RS256.
*
- * This implementation uses HS256 (HMAC with SHA-256) for token signing with a configurable secret.
- * In production environments, this should be replaced with RS256 using GCP Service Account keys
- * for enhanced security.
+ *
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.
*
- *
Security Note: This implementation is simplified for development and testing.
- * Production deployments should use asymmetric signing (RS256) with proper key management through
- * GCP Secret Manager or similar services.
+ *
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 static final String HMAC_SHA256 = "HmacSHA256";
-
- private final String jwtSecret;
+ 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 jwtSecret the secret key for signing tokens
+ * @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(
- @Value("${app.jwt.secret:default-secret-change-in-production}") String jwtSecret,
+ 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.jwtSecret = jwtSecret;
+ this.rsaKeyProvider = rsaKeyProvider;
this.accessTokenExpiration = accessTokenExpiration;
this.refreshTokenExpiration = refreshTokenExpiration;
this.issuer = issuer;
- if ("default-secret-change-in-production".equals(jwtSecret)) {
- logger.warn("Using default JWT secret. Please configure app.jwt.secret for production use.");
+ 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);
}
}
@@ -86,14 +90,14 @@ public JwtTokenProvider(
public String generateAccessToken(Authentication authentication) {
OAuth2User oauth2User = (OAuth2User) authentication.getPrincipal();
- long nowMillis = System.currentTimeMillis();
- long expMillis = nowMillis + accessTokenExpiration;
-
String email = oauth2User.getAttribute("email");
String name = oauth2User.getAttribute("name");
String userId = oauth2User.getAttribute("sub");
- return generateToken(userId, email, name, nowMillis, expMillis, "access");
+ Instant now = Instant.now();
+ Instant expiration = now.plusMillis(accessTokenExpiration);
+
+ return generateToken(userId, email, name, now, expiration, "access");
}
/**
@@ -105,117 +109,60 @@ public String generateAccessToken(Authentication authentication) {
public String generateRefreshToken(Authentication authentication) {
OAuth2User oauth2User = (OAuth2User) authentication.getPrincipal();
- long nowMillis = System.currentTimeMillis();
- long expMillis = nowMillis + refreshTokenExpiration;
-
String userId = oauth2User.getAttribute("sub");
- return generateToken(userId, null, null, nowMillis, expMillis, "refresh");
+ 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.
+ * 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 issuedAtMillis the token issuance time in milliseconds
- * @param expirationMillis the token expiration time in milliseconds
+ * @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,
- long issuedAtMillis, long expirationMillis,
+ Instant issuedAt, Instant expiration,
String tokenType) {
try {
- // Create JWT header
- String header = createBase64UrlEncodedJson(Map.of(
- "alg", "HS256",
- "typ", "JWT"
- ));
-
- // Create JWT payload
- var claimsBuilder = new java.util.HashMap();
- claimsBuilder.put("sub", userId);
- claimsBuilder.put("iss", issuer);
- claimsBuilder.put("iat", issuedAtMillis / 1000);
- claimsBuilder.put("exp", expirationMillis / 1000);
- claimsBuilder.put("token_type", tokenType);
+ // 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.put("email", email);
+ claimsBuilder.claim("email", email);
}
if (name != null) {
- claimsBuilder.put("name", name);
+ claimsBuilder.claim("name", name);
}
- String payload = createBase64UrlEncodedJson(claimsBuilder);
+ JWTClaimsSet claims = claimsBuilder.build();
- // Create signature
- String headerAndPayload = header + "." + payload;
- String signature = signHmacSha256(headerAndPayload);
+ // Create JWT header with key ID
+ JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.RS256)
+ .keyID(rsaKeyProvider.getRsaKey().getKeyID())
+ .build();
- return headerAndPayload + "." + signature;
+ // Create signed JWT
+ SignedJWT signedJWT = new SignedJWT(header, claims);
+ signedJWT.sign(signer);
- } catch (Exception e) {
+ return signedJWT.serialize();
+
+ } catch (JOSEException e) {
logger.error("Error generating JWT token", e);
throw new RuntimeException("Failed to generate JWT token", e);
}
}
-
- /**
- * Creates a Base64 URL-encoded JSON string from a map.
- *
- * @param data the data to encode
- * @return the Base64 URL-encoded JSON string
- */
- private String createBase64UrlEncodedJson(Map data) {
- StringBuilder json = new StringBuilder("{");
- boolean first = true;
- for (Map.Entry entry : data.entrySet()) {
- if (!first) {
- json.append(",");
- }
- first = false;
- json.append("\"").append(entry.getKey()).append("\":");
- Object value = entry.getValue();
- if (value instanceof String) {
- json.append("\"").append(value).append("\"");
- } else {
- json.append(value);
- }
- }
- json.append("}");
-
- return base64UrlEncode(json.toString().getBytes(StandardCharsets.UTF_8));
- }
-
- /**
- * Signs data using HMAC-SHA256.
- *
- * @param data the data to sign
- * @return the Base64 URL-encoded signature
- */
- private String signHmacSha256(String data) throws Exception {
- Mac mac = Mac.getInstance(HMAC_SHA256);
- SecretKeySpec secretKeySpec = new SecretKeySpec(
- jwtSecret.getBytes(StandardCharsets.UTF_8),
- HMAC_SHA256
- );
- mac.init(secretKeySpec);
- byte[] signature = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
- return base64UrlEncode(signature);
- }
-
- /**
- * Base64 URL-encodes the given bytes.
- *
- * @param bytes the bytes to encode
- * @return the Base64 URL-encoded string
- */
- private String base64UrlEncode(byte[] bytes) {
- return Base64.getUrlEncoder()
- .withoutPadding()
- .encodeToString(bytes);
- }
}
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/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