Skip to content

Commit

Permalink
feat: implement configurable CORS (#61)
Browse files Browse the repository at this point in the history
* feat: implement configurable CORS

* fix: js trustchain verification

* fix: allow all CORS origins by default
  • Loading branch information
jcmelati committed Feb 10, 2025
1 parent abe1e8b commit 6851470
Show file tree
Hide file tree
Showing 13 changed files with 141 additions and 16 deletions.
5 changes: 5 additions & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -9,13 +9,28 @@ 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
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) {
Expand All @@ -25,6 +40,7 @@ class SecurityConfig {
}
.csrf { it.disable() }
.oauth2ResourceServer { it.disable() }
.cors { }
.build()
}

Expand All @@ -39,6 +55,7 @@ class SecurityConfig {
}
}
csrf { disable() }
cors { }
}

return http.build()
Expand All @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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}
2 changes: 2 additions & 0 deletions modules/federation-server/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ class FederationClient(
* @return A [VerifyTrustChainResponse] object containing the verification result.
*/
suspend fun trustChainVerify(
trustChain: List<String>,
trustChain: Array<String>,
trustAnchor: String?,
currentTime: Long?
): VerifyTrustChainResponse {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class TrustChainService(
* @see <a href="https://openid.net/specs/openid-federation-1_0.html#section-10.2">OpenID Federation 1.0 - 10.2. Validating a Trust Chain</a>
*/
suspend fun verify(
chain: List<String>,
chain: Array<String>,
trustAnchor: String?,
currentTime: Long? = null
): VerifyTrustChainResponse {
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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 {
Expand All @@ -228,7 +228,7 @@ class TrustChainService(
cache: SimpleCache<String, String>,
depth: Int,
maxDepth: Int
): MutableList<String>? {
): Array<String>? {
logger.debug("Building trust chain for entity: $entityIdentifier at depth: $depth")
if (depth == maxDepth) {
logger.debug("Maximum depth reached: $maxDepth")
Expand Down Expand Up @@ -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")
}
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ data class TrustChainResolveResponse(
* A list of strings representing the resolved trust chain.
* Each string contains a JWT.
*/
val trustChain: List<String>? = null,
val trustChain: Array<String>? = null,

/**
* Indicates whether the resolve operation was successful.
Expand All @@ -24,4 +24,27 @@ data class TrustChainResolveResponse(
* Error message in case of a failure, if any.
*/
val errorMessage: String? = null
)
) {
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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ class TrustChainServiceTest {

// Test with empty chain
val emptyChainResponse = client.trustChainVerify(
emptyList(),
emptyArray(),
"https://oidc.registry.servizicie.interno.gov.it",
1728346615
)
Expand All @@ -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
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ class FederationClientJS(
): Promise<VerifyTrustChainResponse> {
return scope.promise {
trustChainService.verify(
trustChain.toList(),
trustChain,
trustAnchor,
currentTime?.toLong()
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 6851470

Please sign in to comment.