diff --git a/src/main/kotlin/io/visus/demos/kotlinapi/config/ApiVersionConfig.kt b/src/main/kotlin/io/visus/demos/kotlinapi/config/ApiVersionConfig.kt index f4646c1..7514e3f 100644 --- a/src/main/kotlin/io/visus/demos/kotlinapi/config/ApiVersionConfig.kt +++ b/src/main/kotlin/io/visus/demos/kotlinapi/config/ApiVersionConfig.kt @@ -10,7 +10,7 @@ import org.springframework.web.reactive.config.WebFluxConfigurer @Configuration class ApiVersionConfig : WebFluxConfigurer { override fun configurePathMatching(configurer: PathMatchConfigurer) { - configurer.addPathPrefix("/api/v1") { controller -> + configurer.addPathPrefix("/v1") { controller -> val apiVersion = AnnotatedElementUtils.findMergedAnnotation(controller, ApiVersion::class.java) AnnotatedElementUtils.hasAnnotation(controller, RestController::class.java) && apiVersion?.version == 1 } diff --git a/src/main/kotlin/io/visus/demos/kotlinapi/config/OpenApiConfig.kt b/src/main/kotlin/io/visus/demos/kotlinapi/config/OpenApiConfig.kt index bd43117..257f354 100644 --- a/src/main/kotlin/io/visus/demos/kotlinapi/config/OpenApiConfig.kt +++ b/src/main/kotlin/io/visus/demos/kotlinapi/config/OpenApiConfig.kt @@ -36,6 +36,6 @@ class OpenApiConfig { GroupedOpenApi .builder() .group("v1") - .pathsToMatch("/api/v1/**") + .pathsToMatch("/v1/**") .build() } diff --git a/src/main/kotlin/io/visus/demos/kotlinapi/config/SecurityConfig.kt b/src/main/kotlin/io/visus/demos/kotlinapi/config/SecurityConfig.kt index 5fcf61c..363eb2b 100644 --- a/src/main/kotlin/io/visus/demos/kotlinapi/config/SecurityConfig.kt +++ b/src/main/kotlin/io/visus/demos/kotlinapi/config/SecurityConfig.kt @@ -1,6 +1,8 @@ package io.visus.demos.kotlinapi.config import io.visus.demos.kotlinapi.domain.service.UserService +import io.visus.demos.kotlinapi.infrastructure.security.CustomAccessDeniedHandler +import io.visus.demos.kotlinapi.infrastructure.security.CustomAuthenticationEntryPoint import io.visus.demos.kotlinapi.infrastructure.security.JwtAuthenticationFilter import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration @@ -24,6 +26,8 @@ import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource class SecurityConfig( private val jwtAuthenticationFilter: JwtAuthenticationFilter, private val userDetailsService: UserService, + private val customAuthenticationEntryPoint: CustomAuthenticationEntryPoint, + private val customAccessDeniedHandler: CustomAccessDeniedHandler, ) { @Bean fun corsConfigurationSource(): CorsConfigurationSource { @@ -56,16 +60,20 @@ class SecurityConfig( .csrf { it.disable() } .cors { it.configurationSource(corsConfigurationSource()) } .securityContextRepository(NoOpServerSecurityContextRepository.getInstance()) - .authorizeExchange { authorize -> + .exceptionHandling { handlers -> + handlers + .authenticationEntryPoint(customAuthenticationEntryPoint) + .accessDeniedHandler(customAccessDeniedHandler) + }.authorizeExchange { authorize -> authorize .pathMatchers( "/swagger-ui.html", "/swagger-ui/**", "/api-docs/**", - "/api/v1/health", - "/api/v1/auth/login", - "/api/v1/auth/refresh", - "/api/v1/auth/register", + "/v1/health", + "/v1/auth/login", + "/v1/auth/refresh", + "/v1/auth/register", ).permitAll() .anyExchange() .authenticated() diff --git a/src/main/kotlin/io/visus/demos/kotlinapi/infrastructure/security/CustomAccessDeniedHandler.kt b/src/main/kotlin/io/visus/demos/kotlinapi/infrastructure/security/CustomAccessDeniedHandler.kt new file mode 100644 index 0000000..df70f53 --- /dev/null +++ b/src/main/kotlin/io/visus/demos/kotlinapi/infrastructure/security/CustomAccessDeniedHandler.kt @@ -0,0 +1,30 @@ +package io.visus.demos.kotlinapi.infrastructure.security + +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.security.access.AccessDeniedException +import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler +import org.springframework.stereotype.Component +import org.springframework.web.server.ServerWebExchange +import reactor.core.publisher.Mono + +@Component +class CustomAccessDeniedHandler : ServerAccessDeniedHandler { + override fun handle( + exchange: ServerWebExchange, + denied: AccessDeniedException, + ): Mono = + Mono + .fromRunnable { + exchange.response.statusCode = HttpStatus.FORBIDDEN + exchange.response.headers.contentType = MediaType.APPLICATION_JSON + }.then( + exchange.response.writeWith( + Mono.just( + exchange.response.bufferFactory().wrap( + """{"error":"Forbidden","message":"Access denied"}""".toByteArray(), + ), + ), + ), + ) +} diff --git a/src/main/kotlin/io/visus/demos/kotlinapi/infrastructure/security/CustomAuthenticationEntryPoint.kt b/src/main/kotlin/io/visus/demos/kotlinapi/infrastructure/security/CustomAuthenticationEntryPoint.kt new file mode 100644 index 0000000..7e45532 --- /dev/null +++ b/src/main/kotlin/io/visus/demos/kotlinapi/infrastructure/security/CustomAuthenticationEntryPoint.kt @@ -0,0 +1,30 @@ +package io.visus.demos.kotlinapi.infrastructure.security + +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.security.core.AuthenticationException +import org.springframework.security.web.server.ServerAuthenticationEntryPoint +import org.springframework.stereotype.Component +import org.springframework.web.server.ServerWebExchange +import reactor.core.publisher.Mono + +@Component +class CustomAuthenticationEntryPoint : ServerAuthenticationEntryPoint { + override fun commence( + exchange: ServerWebExchange, + ex: AuthenticationException, + ): Mono = + Mono + .fromRunnable { + exchange.response.statusCode = HttpStatus.UNAUTHORIZED + exchange.response.headers.contentType = MediaType.APPLICATION_JSON + }.then( + exchange.response.writeWith( + Mono.just( + exchange.response.bufferFactory().wrap( + """{"error":"Unauthorized","message":"Authentication required"}""".toByteArray(), + ), + ), + ), + ) +}