From 6851470388f45778cc2465774df3dc709071b92c Mon Sep 17 00:00:00 2001 From: John Melati Date: Mon, 10 Feb 2025 22:29:14 +0100 Subject: [PATCH] feat: implement configurable CORS (#61) * feat: implement configurable CORS * fix: js trustchain verification * fix: allow all CORS origins by default --- .env | 5 ++ build.gradle.kts | 2 +- .../{security => }/config/SecurityConfig.kt | 32 ++++++++++- .../src/main/resources/application.properties | 5 +- modules/federation-server/build.gradle.kts | 2 + .../federation/config/SecurityConfig.kt | 56 +++++++++++++++++++ .../src/main/resources/application.properties | 6 ++ .../oid/fed/client/FederationClient.kt | 2 +- .../trustChainService/TrustChainService.kt | 12 ++-- .../client/types/TrustChainResolveResponse.kt | 27 ++++++++- .../TrustChainServiceTest.kt | 4 +- .../oid/fed/client/FederationClient.js.kt | 2 +- .../oid/fed/services/ResolveService.kt | 2 +- 13 files changed, 141 insertions(+), 16 deletions(-) rename modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/{security => }/config/SecurityConfig.kt (65%) create mode 100644 modules/federation-server/src/main/kotlin/com/sphereon/oid/fed/server/federation/config/SecurityConfig.kt diff --git a/.env b/.env index bf0eb7dc..e9408120 100644 --- a/.env +++ b/.env @@ -22,3 +22,8 @@ OAUTH2_RESOURCE_SERVER_JWT_ISSUER_URI=http://keycloak:8080/realms/openid-federat DEV_MODE=false LOGGER_SEVERITY=Verbose + +CORS_ALLOWED_ORIGINS=* +CORS_ALLOWED_METHODS=GET,POST,PUT,DELETE,OPTIONS +CORS_ALLOWED_HEADERS=Authorization,Content-Type,X-Account-Username +CORS_MAX_AGE=3600 \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index dfb37035..1e5a41bd 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -92,7 +92,7 @@ fun getNpmVersion(): String { allprojects { group = "com.sphereon.oid.fed" - version = "0.4.10-SNAPSHOT" + version = "0.4.12-SNAPSHOT" val npmVersion by extra { getNpmVersion() } // Common repository configuration for all projects diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/security/config/SecurityConfig.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/config/SecurityConfig.kt similarity index 65% rename from modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/security/config/SecurityConfig.kt rename to modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/config/SecurityConfig.kt index cbfcda5f..7e2e2ff7 100644 --- a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/security/config/SecurityConfig.kt +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/config/SecurityConfig.kt @@ -1,4 +1,4 @@ -package com.sphereon.oid.fed.server.admin.security.config +package com.sphereon.oid.fed.server.admin.config import org.springframework.beans.factory.annotation.Value import org.springframework.context.annotation.Bean @@ -9,6 +9,9 @@ import org.springframework.security.config.annotation.web.invoke import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter import org.springframework.security.web.SecurityFilterChain +import org.springframework.web.cors.CorsConfiguration +import org.springframework.web.cors.CorsConfigurationSource +import org.springframework.web.cors.UrlBasedCorsConfigurationSource @Configuration @EnableWebSecurity @@ -16,6 +19,18 @@ class SecurityConfig { @Value("\${app.dev-mode:false}") private var devMode: Boolean = false + @Value("\${app.cors.allowed-origins}") + private lateinit var allowedOrigins: String + + @Value("\${app.cors.allowed-methods}") + private lateinit var allowedMethods: String + + @Value("\${app.cors.allowed-headers}") + private lateinit var allowedHeaders: String + + @Value("\${app.cors.max-age:3600}") + private var maxAge: Long = 3600 + @Bean fun filterChain(http: HttpSecurity): SecurityFilterChain { if (devMode) { @@ -25,6 +40,7 @@ class SecurityConfig { } .csrf { it.disable() } .oauth2ResourceServer { it.disable() } + .cors { } .build() } @@ -39,6 +55,7 @@ class SecurityConfig { } } csrf { disable() } + cors { } } return http.build() @@ -55,4 +72,17 @@ class SecurityConfig { } return jwtAuthenticationConverter } + + @Bean + fun corsConfigurationSource(): CorsConfigurationSource { + val configuration = CorsConfiguration() + configuration.allowedOrigins = allowedOrigins.split(",") + configuration.allowedMethods = allowedMethods.split(",") + configuration.allowedHeaders = allowedHeaders.split(",") + configuration.maxAge = maxAge + + val source = UrlBasedCorsConfigurationSource() + source.registerCorsConfiguration("/**", configuration) + return source + } } \ No newline at end of file diff --git a/modules/admin-server/src/main/resources/application.properties b/modules/admin-server/src/main/resources/application.properties index 7791b145..47b1d5e9 100644 --- a/modules/admin-server/src/main/resources/application.properties +++ b/modules/admin-server/src/main/resources/application.properties @@ -15,4 +15,7 @@ monitoring.health.interval=60000 monitoring.load.interval=300000 # Federation Configuration sphereon.federation.root-identifier=${ROOT_IDENTIFIER:http://localhost:8080} - +app.cors.allowed-origins=${CORS_ALLOWED_ORIGINS:*} +app.cors.allowed-methods=${CORS_ALLOWED_METHODS:GET,POST,PUT,DELETE,OPTIONS} +app.cors.allowed-headers=${CORS_ALLOWED_HEADERS:Authorization,Content-Type,X-Account-Username} +app.cors.max-age=${CORS_MAX_AGE:3600} diff --git a/modules/federation-server/build.gradle.kts b/modules/federation-server/build.gradle.kts index b2387ab1..dfbc34da 100644 --- a/modules/federation-server/build.gradle.kts +++ b/modules/federation-server/build.gradle.kts @@ -23,6 +23,8 @@ dependencies { implementation(libs.springboot.actuator) implementation(libs.springboot.web) implementation(libs.springboot.data.jdbc) + implementation("org.springframework.boot:spring-boot-starter-security") + implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server") implementation(libs.kotlin.reflect) testImplementation(libs.springboot.test) testImplementation(libs.testcontainer.junit) diff --git a/modules/federation-server/src/main/kotlin/com/sphereon/oid/fed/server/federation/config/SecurityConfig.kt b/modules/federation-server/src/main/kotlin/com/sphereon/oid/fed/server/federation/config/SecurityConfig.kt new file mode 100644 index 00000000..11d0c781 --- /dev/null +++ b/modules/federation-server/src/main/kotlin/com/sphereon/oid/fed/server/federation/config/SecurityConfig.kt @@ -0,0 +1,56 @@ +package com.sphereon.oid.fed.server.federation.config + +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.annotation.web.invoke +import org.springframework.security.web.SecurityFilterChain +import org.springframework.web.cors.CorsConfiguration +import org.springframework.web.cors.CorsConfigurationSource +import org.springframework.web.cors.UrlBasedCorsConfigurationSource + +@Configuration +@EnableWebSecurity +class SecurityConfig { + @Value("\${app.dev-mode:false}") + private var devMode: Boolean = false + + @Value("\${app.cors.allowed-origins}") + private lateinit var allowedOrigins: String + + @Value("\${app.cors.allowed-methods}") + private lateinit var allowedMethods: String + + @Value("\${app.cors.allowed-headers}") + private lateinit var allowedHeaders: String + + @Value("\${app.cors.max-age:3600}") + private var maxAge: Long = 3600 + + @Bean + fun filterChain(http: HttpSecurity): SecurityFilterChain { + http { + authorizeRequests { + authorize("/**", permitAll) + } + csrf { disable() } + cors { } + } + return http.build() + } + + @Bean + fun corsConfigurationSource(): CorsConfigurationSource { + val configuration = CorsConfiguration() + configuration.allowedOrigins = allowedOrigins.split(",") + configuration.allowedMethods = allowedMethods.split(",") + configuration.allowedHeaders = allowedHeaders.split(",") + configuration.maxAge = maxAge + + val source = UrlBasedCorsConfigurationSource() + source.registerCorsConfiguration("/**", configuration) + return source + } +} \ No newline at end of file diff --git a/modules/federation-server/src/main/resources/application.properties b/modules/federation-server/src/main/resources/application.properties index fc7c1502..9fabf8d5 100644 --- a/modules/federation-server/src/main/resources/application.properties +++ b/modules/federation-server/src/main/resources/application.properties @@ -5,3 +5,9 @@ management.endpoints.web.base-path=/ management.endpoints.web.path-mapping.health=status server.port=8080 spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration +# CORS Configuration +app.cors.allowed-origins=${CORS_ALLOWED_ORIGINS:*} +app.cors.allowed-methods=${CORS_ALLOWED_METHODS:GET,POST,PUT,DELETE,OPTIONS} +app.cors.allowed-headers=${CORS_ALLOWED_HEADERS:Authorization,Content-Type} +app.cors.max-age=${CORS_MAX_AGE:3600} +app.dev-mode=${DEV_MODE:false} \ No newline at end of file diff --git a/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/FederationClient.kt b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/FederationClient.kt index bafd00e4..ad9cd890 100644 --- a/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/FederationClient.kt +++ b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/FederationClient.kt @@ -60,7 +60,7 @@ class FederationClient( * @return A [VerifyTrustChainResponse] object containing the verification result. */ suspend fun trustChainVerify( - trustChain: List, + trustChain: Array, trustAnchor: String?, currentTime: Long? ): VerifyTrustChainResponse { diff --git a/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/services/trustChainService/TrustChainService.kt b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/services/trustChainService/TrustChainService.kt index 918879b4..10b0cac0 100644 --- a/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/services/trustChainService/TrustChainService.kt +++ b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/services/trustChainService/TrustChainService.kt @@ -37,7 +37,7 @@ class TrustChainService( * @see OpenID Federation 1.0 - 10.2. Validating a Trust Chain */ suspend fun verify( - chain: List, + chain: Array, trustAnchor: String?, currentTime: Long? = null ): VerifyTrustChainResponse { @@ -70,6 +70,7 @@ class TrustChainService( // 2. Verify iat is in the past val iat = statement.payload["iat"]?.jsonPrimitive?.content?.toLongOrNull() logger.debug("Statement $j - Issued at (iat): $iat") + logger.debug("Time considered: $timeToUse") if (iat == null || iat > timeToUse) { logger.error("Statement $j has invalid iat: $iat") return VerifyTrustChainResponse(false, "Statement at position $j has invalid iat") @@ -207,8 +208,7 @@ class TrustChainService( context = mapOf("trustChain" to trustChain.toString()) ) - // calculate trust chain exp - + // @TODO calculate trust chain exp TrustChainResolveResponse(trustChain, error = false, errorMessage = null) } else { @@ -228,7 +228,7 @@ class TrustChainService( cache: SimpleCache, depth: Int, maxDepth: Int - ): MutableList? { + ): Array? { logger.debug("Building trust chain for entity: $entityIdentifier at depth: $depth") if (depth == maxDepth) { logger.debug("Maximum depth reached: $maxDepth") @@ -291,7 +291,7 @@ class TrustChainService( if (result != null) { logger.debug("Successfully built trust chain through authority: $authority") - return result + return result.toTypedArray() } logger.debug("Failed to build trust chain through authority: $authority, trying next authority") } @@ -457,7 +457,7 @@ class TrustChainService( if (authorityEntityConfiguration.authorityHints?.isNotEmpty() == true) { chain.add(subordinateStatementJwt) val result = buildTrustChain(authority, trustAnchors, chain, cache, depth, maxDepth) - if (result != null) return result + if (result != null) return result.toMutableList() chain.removeLast() } return null diff --git a/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/types/TrustChainResolveResponse.kt b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/types/TrustChainResolveResponse.kt index fb5129b2..854a4313 100644 --- a/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/types/TrustChainResolveResponse.kt +++ b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/types/TrustChainResolveResponse.kt @@ -13,7 +13,7 @@ data class TrustChainResolveResponse( * A list of strings representing the resolved trust chain. * Each string contains a JWT. */ - val trustChain: List? = null, + val trustChain: Array? = null, /** * Indicates whether the resolve operation was successful. @@ -24,4 +24,27 @@ data class TrustChainResolveResponse( * Error message in case of a failure, if any. */ val errorMessage: String? = null -) \ No newline at end of file +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as TrustChainResolveResponse + + if (error != other.error) return false + if (trustChain != null) { + if (other.trustChain == null) return false + if (!trustChain.contentEquals(other.trustChain)) return false + } else if (other.trustChain != null) return false + if (errorMessage != other.errorMessage) return false + + return true + } + + override fun hashCode(): Int { + var result = error.hashCode() + result = 31 * result + (trustChain?.contentHashCode() ?: 0) + result = 31 * result + (errorMessage?.hashCode() ?: 0) + return result + } +} \ No newline at end of file diff --git a/modules/openid-federation-client/src/commonTest/kotlin/com/sphereon/oid/fed/client/services/trustChainService/TrustChainServiceTest.kt b/modules/openid-federation-client/src/commonTest/kotlin/com/sphereon/oid/fed/client/services/trustChainService/TrustChainServiceTest.kt index f6bb09db..c6902942 100644 --- a/modules/openid-federation-client/src/commonTest/kotlin/com/sphereon/oid/fed/client/services/trustChainService/TrustChainServiceTest.kt +++ b/modules/openid-federation-client/src/commonTest/kotlin/com/sphereon/oid/fed/client/services/trustChainService/TrustChainServiceTest.kt @@ -130,7 +130,7 @@ class TrustChainServiceTest { // Test with empty chain val emptyChainResponse = client.trustChainVerify( - emptyList(), + emptyArray(), "https://oidc.registry.servizicie.interno.gov.it", 1728346615 ) @@ -140,7 +140,7 @@ class TrustChainServiceTest { // Test with wrong trust anchor val wrongAnchorResponse = client.trustChainVerify( - resolveResponse.trustChain ?: emptyList(), + resolveResponse.trustChain ?: emptyArray(), "https://wrong.trust.anchor", 1728346615 ) diff --git a/modules/openid-federation-client/src/jsMain/kotlin/com/sphereon/oid/fed/client/FederationClient.js.kt b/modules/openid-federation-client/src/jsMain/kotlin/com/sphereon/oid/fed/client/FederationClient.js.kt index e8013e64..3ca5ae8d 100644 --- a/modules/openid-federation-client/src/jsMain/kotlin/com/sphereon/oid/fed/client/FederationClient.js.kt +++ b/modules/openid-federation-client/src/jsMain/kotlin/com/sphereon/oid/fed/client/FederationClient.js.kt @@ -75,7 +75,7 @@ class FederationClientJS( ): Promise { return scope.promise { trustChainService.verify( - trustChain.toList(), + trustChain, trustAnchor, currentTime?.toLong() ) diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/ResolveService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/ResolveService.kt index f018f2df..ba9d4bb9 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/ResolveService.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/ResolveService.kt @@ -60,7 +60,7 @@ class ResolveService( exp = (currentTime + 3600 * 24).toString(), // 24 hours expiration metadata = filteredMetadata, trustMarks = trustMarks, - trustChain = trustChainResolution.trustChain!!.toTypedArray() + trustChain = trustChainResolution.trustChain ) logger.debug("Successfully built resolve response") return response