diff --git a/auth/.gitignore b/auth/.gitignore new file mode 100644 index 00000000000..5a979af6fff --- /dev/null +++ b/auth/.gitignore @@ -0,0 +1,40 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Kotlin ### +.kotlin diff --git a/auth/build.gradle.kts b/auth/build.gradle.kts new file mode 100644 index 00000000000..f04d2619d62 --- /dev/null +++ b/auth/build.gradle.kts @@ -0,0 +1,58 @@ +apply(from = rootProject.file("buildSrc/shared.gradle.kts")) + +plugins { + id("org.springframework.boot") version "3.3.2" + id("io.spring.dependency-management") version "1.1.6" + id("reportstream.project-conventions") + kotlin("plugin.spring") version "2.0.0" +} + +group = "gov.cdc.prime" +version = "0.0.1-SNAPSHOT" + +dependencies { + implementation(project(":shared")) + + implementation("org.jetbrains.kotlin:kotlin-reflect") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:1.8.1") + + /** + * Spring WebFlux was chosen for this project to be able to better handle periods of high traffic + */ + implementation("org.springframework.boot:spring-boot-starter-webflux") + implementation("org.springframework.cloud:spring-cloud-gateway-webflux") + implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server") + + runtimeOnly("com.nimbusds:oauth2-oidc-sdk:11.18") + + testImplementation("org.springframework.boot:spring-boot-starter-test") + testImplementation("org.springframework.security:spring-security-test") + testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") + testImplementation("org.mockito.kotlin:mockito-kotlin:5.4.0") + testImplementation("com.squareup.okhttp3:mockwebserver:4.12.0") + + testRuntimeOnly("org.junit.platform:junit-platform-launcher") + + compileOnly("org.springframework.boot:spring-boot-devtools") +} + +// There is a conflict in logging implementations. Excluded these in favor of using log4j-slf4j2-impl +configurations.all { + exclude(group = "org.apache.logging.log4j", module = "log4j-to-slf4j") + exclude(group = "ch.qos.logback") +} + +dependencyManagement { + imports { + mavenBom("com.azure.spring:spring-cloud-azure-dependencies:5.14.0") + mavenBom("org.springframework.cloud:spring-cloud-dependencies:2023.0.3") + } +} + +kotlin { + compilerOptions { + // https://docs.spring.io/spring-boot/docs/2.0.x/reference/html/boot-features-kotlin.html#boot-features-kotlin-null-safety + freeCompilerArgs.addAll("-Xjsr305=strict") + } +} \ No newline at end of file diff --git a/auth/gradle/wrapper/gradle-wrapper.jar b/auth/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000000..e6441136f3d Binary files /dev/null and b/auth/gradle/wrapper/gradle-wrapper.jar differ diff --git a/auth/gradle/wrapper/gradle-wrapper.properties b/auth/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000000..a4413138c96 --- /dev/null +++ b/auth/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/AuthApplication.kt b/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/AuthApplication.kt new file mode 100644 index 00000000000..249ef82f081 --- /dev/null +++ b/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/AuthApplication.kt @@ -0,0 +1,11 @@ +package gov.cdc.prime.reportstream.auth + +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.runApplication + +@SpringBootApplication +class AuthApplication + +fun main(args: Array) { + runApplication(*args) +} \ No newline at end of file diff --git a/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/AuthApplicationConstants.kt b/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/AuthApplicationConstants.kt new file mode 100644 index 00000000000..2c2909dd275 --- /dev/null +++ b/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/AuthApplicationConstants.kt @@ -0,0 +1,14 @@ +package gov.cdc.prime.reportstream.auth + +/** + * File used for application-wide constants + */ +object AuthApplicationConstants { + + /** + * All endpoints defined here + */ + object Endpoints { + const val HEALTHCHECK_ENDPOINT_V1 = "/api/v1/healthcheck" + } +} \ No newline at end of file diff --git a/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/config/ApplicationConfig.kt b/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/config/ApplicationConfig.kt new file mode 100644 index 00000000000..c0aeb78fdbe --- /dev/null +++ b/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/config/ApplicationConfig.kt @@ -0,0 +1,32 @@ +package gov.cdc.prime.reportstream.auth.config + +import org.springframework.boot.context.properties.ConfigurationProperties +import org.springframework.boot.context.properties.EnableConfigurationProperties +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import kotlin.time.TimeSource + +/** + * Simple class to automatically read configuration from application.yml (or environment variable overrides) + */ +@Configuration +@EnableConfigurationProperties(ProxyConfigurationProperties::class) +class ApplicationConfig( + val proxyConfig: ProxyConfigurationProperties, +) { + + @Bean + fun timeSource(): TimeSource { + return TimeSource.Monotonic + } +} + +@ConfigurationProperties("proxy") +data class ProxyConfigurationProperties( + val pathMappings: List, +) + +data class ProxyPathMapping( + val baseUrl: String, + val pathPrefix: String, +) \ No newline at end of file diff --git a/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/config/SecurityConfig.kt b/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/config/SecurityConfig.kt new file mode 100644 index 00000000000..004493a1646 --- /dev/null +++ b/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/config/SecurityConfig.kt @@ -0,0 +1,35 @@ +package gov.cdc.prime.reportstream.auth.config + +import gov.cdc.prime.reportstream.auth.AuthApplicationConstants +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 setup + * + * All incoming requests will require authentication via opaque token check + */ +@Configuration +@EnableWebFluxSecurity +class SecurityConfig { + + @Bean + fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + http + .authorizeExchange { authorize -> + authorize + // allow health endpoint without authentication + .pathMatchers(AuthApplicationConstants.Endpoints.HEALTHCHECK_ENDPOINT_V1).permitAll() + // all other requests must be authenticated + .anyExchange().authenticated() + } + .oauth2ResourceServer { + it.opaqueToken { } + } + + return http.build() + } +} \ No newline at end of file diff --git a/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/controller/AuthController.kt b/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/controller/AuthController.kt new file mode 100644 index 00000000000..e62df018405 --- /dev/null +++ b/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/controller/AuthController.kt @@ -0,0 +1,47 @@ +package gov.cdc.prime.reportstream.auth.controller + +import gov.cdc.prime.reportstream.auth.service.ProxyURIStrategy +import kotlinx.coroutines.reactive.awaitSingle +import org.apache.logging.log4j.kotlin.Logging +import org.springframework.cloud.gateway.webflux.ProxyExchange +import org.springframework.http.ResponseEntity +import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthentication +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import org.springframework.web.server.ServerWebExchange + +@RestController +class AuthController( + private val proxyURIStrategy: ProxyURIStrategy, +) : Logging { + + /** + * Main workhorse of the application. Handles all incoming requests and properly forwards them given successful + * authentication. Missing or invalid bearer tokens will result in a 401 unauthorized response. + * + * Authentication will be handled by the OAuth 2.0 resource server opaque token configuration + * @see https://docs.spring.io/spring-security/reference/servlet/oauth2/resource-server/opaque-token.html + * + * Proxying will be handled by the Spring Cloud Gateway library from which the ProxyExchange object is injected + */ + @RequestMapping("**") + suspend fun proxy( + exchange: ServerWebExchange, + proxy: ProxyExchange, + auth: BearerTokenAuthentication, + ): ResponseEntity { + val sub = auth.tokenAttributes["sub"] + val scopes = auth.tokenAttributes["scope"] + + logger.info("Token with sub=$sub and scopes=$scopes is authenticated with Okta") + + val uri = proxyURIStrategy.getTargetURI(exchange.request.uri) + proxy.uri(uri.toString()) + + logger.info("Proxying request to ${exchange.request.method} $uri") + val response = proxy.forward().awaitSingle() + logger.info("Proxy response from ${exchange.request.method} $uri status=${response.statusCode}") + + return response + } +} \ No newline at end of file diff --git a/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/controller/HealthController.kt b/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/controller/HealthController.kt new file mode 100644 index 00000000000..f90ee051982 --- /dev/null +++ b/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/controller/HealthController.kt @@ -0,0 +1,25 @@ +package gov.cdc.prime.reportstream.auth.controller + +import gov.cdc.prime.reportstream.auth.AuthApplicationConstants +import gov.cdc.prime.reportstream.auth.model.ApplicationStatus +import org.springframework.http.MediaType +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RestController +import kotlin.time.TimeSource + +@RestController +class HealthController( + timeSource: TimeSource, +) { + + private val applicationStart = timeSource.markNow() + + @GetMapping( + AuthApplicationConstants.Endpoints.HEALTHCHECK_ENDPOINT_V1, + produces = [MediaType.APPLICATION_JSON_VALUE] + ) + suspend fun health(): ApplicationStatus { + val uptime = applicationStart.elapsedNow().toString() + return ApplicationStatus("auth", "ok", uptime) + } +} \ No newline at end of file diff --git a/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/model/ApplicationStatus.kt b/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/model/ApplicationStatus.kt new file mode 100644 index 00000000000..da9a90b2fa0 --- /dev/null +++ b/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/model/ApplicationStatus.kt @@ -0,0 +1,10 @@ +package gov.cdc.prime.reportstream.auth.model + +/** + * Simple json response model for application status + */ +data class ApplicationStatus( + val application: String, + val status: String, + val uptime: String, +) \ No newline at end of file diff --git a/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/service/ProxyURIStrategy.kt b/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/service/ProxyURIStrategy.kt new file mode 100644 index 00000000000..38686400a11 --- /dev/null +++ b/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/service/ProxyURIStrategy.kt @@ -0,0 +1,55 @@ +package gov.cdc.prime.reportstream.auth.service + +import gov.cdc.prime.reportstream.auth.config.ApplicationConfig +import org.springframework.context.annotation.Profile +import org.springframework.stereotype.Component +import java.net.URI + +/** + * Implementations are ways to decide the ultimate destination of an incoming request + */ +interface ProxyURIStrategy { + fun getTargetURI(incomingUri: URI): URI +} + +/** + * This implementation decides via the path prefix. Currently used locally for when all services are + * running on different ports of localhost. + * + * Configured under proxyConfig.pathMappings + * + * http://localhost:9000/submissions/health -> http://localhost:8880/health + */ +@Component +@Profile("local") +class PathPrefixProxyURIStrategy( + private val applicationConfig: ApplicationConfig, +) : ProxyURIStrategy { + override fun getTargetURI(incomingUri: URI): URI { + val proxyPathMappings = applicationConfig.proxyConfig.pathMappings + val maybePathMapping = proxyPathMappings.find { incomingUri.path.startsWith(it.pathPrefix) } + return if (maybePathMapping != null) { + val baseUri = URI(maybePathMapping.baseUrl) + val path = incomingUri.path.removePrefix(maybePathMapping.pathPrefix) + URI( + baseUri.scheme, + baseUri.userInfo, + baseUri.host, + baseUri.port, + path, + incomingUri.query, + incomingUri.fragment + ) + } else { + throw IllegalStateException("no configured proxy target in path mappings for path=${incomingUri.path}") + } + } +} + +@Component +@Profile("deployed") +class HostProxyPathURIStrategy : ProxyURIStrategy { + override fun getTargetURI(incomingUri: URI): URI { + TODO("Not yet implemented") + } +} \ No newline at end of file diff --git a/auth/src/main/resources/application.yml b/auth/src/main/resources/application.yml new file mode 100644 index 00000000000..6a085c848dd --- /dev/null +++ b/auth/src/main/resources/application.yml @@ -0,0 +1,29 @@ +spring: + application: + name: "auth" + profiles: + active: local + security: + oauth2: + resourceserver: + opaquetoken: # Set client secret in SPRING_SECURITY_OAUTH2_RESOURCESERVER_OPAQUETOKEN_CLIENT_SECRET env variable + client-id: 0oaek8tip2lhrhHce1d7 + introspection-uri: https://reportstream.oktapreview.com/oauth2/ausekaai7gUuUtHda1d7/v1/introspect + cloud: + gateway: + proxy: + sensitive: [] # pass authorization and cookie headers downstream (filtered by default) + +server.port: 9000 + +proxy.pathMappings: + - pathPrefix: /reportstream + baseUrl: http://localhost:7071 + - pathPrefix: /submissions + baseUrl: http://localhost:8880 + +#Uncomment for verbose logging +#logging: +# level: +# web: debug +# org.springframework.web: debug diff --git a/auth/src/test/kotlin/gov/cdc/prime/reportstream/auth/controller/AuthControllerTest.kt b/auth/src/test/kotlin/gov/cdc/prime/reportstream/auth/controller/AuthControllerTest.kt new file mode 100644 index 00000000000..8242e7a3780 --- /dev/null +++ b/auth/src/test/kotlin/gov/cdc/prime/reportstream/auth/controller/AuthControllerTest.kt @@ -0,0 +1,146 @@ +package gov.cdc.prime.reportstream.auth.controller + +import gov.cdc.prime.reportstream.auth.service.ProxyURIStrategy +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.kotlin.any +import org.mockito.kotlin.given +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.mock.mockito.MockBean +import org.springframework.http.HttpHeaders +import org.springframework.http.MediaType +import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.csrf +import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockOpaqueToken +import org.springframework.test.context.junit.jupiter.SpringExtension +import org.springframework.test.web.reactive.server.WebTestClient +import java.net.URI +import java.nio.charset.Charset +import kotlin.test.Test +import kotlin.test.assertEquals + +@ExtendWith(SpringExtension::class) +@SpringBootTest +@AutoConfigureWebTestClient +class AuthControllerTest @Autowired constructor( + private val webTestClient: WebTestClient, + @MockBean private val mockedUriStrategy: ProxyURIStrategy, +) { + + private val server: MockWebServer = MockWebServer() + + @BeforeEach + fun setUp() { + server.start() + } + + @AfterEach + fun tearDown() { + server.shutdown() + } + + @Test + fun `successful proxy`() { + server.enqueue( + MockResponse() + .setHeader(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN) + .setBody("hello world!") + ) + + val incomingUri = URI("/service/path") + val outgoingUri = URI(server.url("/path").toString()) + given(mockedUriStrategy.getTargetURI(incomingUri)).willReturn(outgoingUri) + + webTestClient + .mutateWith(csrf()) + .mutateWith( + mockOpaqueToken() + .attributes { map -> + map["sub"] = "sub" + map["scope"] = listOf("scope1", "scope2") + } + ) + .post() + .uri("/service/path") + .accept(MediaType.TEXT_PLAIN) + .headers { headers -> + headers.add("x-test-header", "Pass this along") + } + .bodyValue("body") + .exchange() + // assertions on the response received from the mock server + .expectStatus().isOk + .expectHeader().contentType(MediaType.TEXT_PLAIN) + .expectBody(String::class.java).isEqualTo("hello world!") + + // assertions on recorded request to proxy + val recordedRequest = server.takeRequest() + assertEquals( + recordedRequest.headers.get("x-test-header"), + "Pass this along" + ) + assertEquals( + recordedRequest.body.readString(Charset.defaultCharset()), + "body" + ) + } + + @Test + fun `authorization fails in proxied server`() { + server.enqueue(MockResponse().setResponseCode(403)) + + given(mockedUriStrategy.getTargetURI(any())) + .willReturn(URI(server.url("/").toString())) + + webTestClient + .mutateWith(csrf()) + .mutateWith( + mockOpaqueToken() + .attributes { map -> + map["sub"] = "sub" + map["scope"] = listOf("scope1", "scope2") + } + ) + .post() + .uri("/random") + .accept(MediaType.TEXT_PLAIN) + .headers { headers -> + headers.add("x-test-header", "Pass this along") + } + .bodyValue("body") + .exchange() + // assertions on the response received from the mock server + .expectStatus().isForbidden + + // assertions on recorded request to proxy + val recordedRequest = server.takeRequest() + assertEquals( + recordedRequest.headers.get("x-test-header"), + "Pass this along" + ) + assertEquals( + recordedRequest.body.readString(Charset.defaultCharset()), + "body" + ) + } + + @Test + fun `authentication fails`() { + given(mockedUriStrategy.getTargetURI(any())) + .willReturn(URI(server.url("/").toString())) + + webTestClient + .mutateWith(csrf()) + .post() + .uri("/random") + .exchange() + .expectStatus().isUnauthorized + + // no request should be made to server + assertEquals(server.requestCount, 0) + } +} \ No newline at end of file diff --git a/auth/src/test/resources/application.yml b/auth/src/test/resources/application.yml new file mode 100644 index 00000000000..2925d96b9de --- /dev/null +++ b/auth/src/test/resources/application.yml @@ -0,0 +1,24 @@ +spring: + application: + name: "auth" + profiles: + active: test + security: + oauth2: + resourceserver: + opaquetoken: + client-id: mockClient + client-secret: mockSecret + introspection-uri: https://localhost:9999/oauth2/default/v1/introspect # should never be hit + cloud: + gateway: + proxy: + sensitive: [] # pass authorization and cookie headers downstream (filtered by default) + +server.port: 9000 + +proxy.pathMappings: + - pathPrefix: /reportstream + baseUrl: http://localhost:7071 + - pathPrefix: /submissions + baseUrl: http://localhost:8880 diff --git a/settings.gradle.kts b/settings.gradle.kts index 8ae87b5128c..2a0e2ecd57d 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -13,4 +13,4 @@ sourceControl { } } rootProject.name = "prime-reportstream" -include("shared", "submissions", "prime-router") +include("shared", "submissions", "prime-router", "auth") diff --git a/submissions/build.gradle.kts b/submissions/build.gradle.kts index 4f7c29c7d79..4f3f4c9294e 100644 --- a/submissions/build.gradle.kts +++ b/submissions/build.gradle.kts @@ -14,6 +14,10 @@ extra["springCloudAzureVersion"] = "5.14.0" dependencies { implementation("org.springframework.boot:spring-boot-starter-web") + implementation("org.springframework.boot:spring-boot-starter-security") + implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server") + implementation("org.springframework.security:spring-security-oauth2-jose:6.3.3") + implementation("com.azure.spring:spring-cloud-azure-starter-storage") implementation("com.microsoft.azure:applicationinsights-runtime-attach:3.5.4") implementation("com.microsoft.azure:applicationinsights-web:3.5.4") diff --git a/submissions/src/main/kotlin/gov/cdc/prime/reportstream/submissions/config/SecurityConfig.kt b/submissions/src/main/kotlin/gov/cdc/prime/reportstream/submissions/config/SecurityConfig.kt new file mode 100644 index 00000000000..cdfa64f27d4 --- /dev/null +++ b/submissions/src/main/kotlin/gov/cdc/prime/reportstream/submissions/config/SecurityConfig.kt @@ -0,0 +1,31 @@ +package gov.cdc.prime.reportstream.submissions.config + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.web.SecurityFilterChain + +/** + * Allow all requests sans any authn/authz checks. + */ +@Configuration +@EnableWebSecurity +@EnableMethodSecurity +class SecurityConfig { + @Bean + fun filterChain(http: HttpSecurity): SecurityFilterChain { + http + .authorizeHttpRequests { authorize -> + authorize + // TODO: add routes which require authentication here when required + .anyRequest().permitAll() // currently allow all requests unauthenticated + } + .oauth2ResourceServer { + it.jwt { } + } + + return http.build() + } +} \ No newline at end of file diff --git a/submissions/src/main/kotlin/gov/cdc/prime/reportstream/submissions/controllers/SubmissionController.kt b/submissions/src/main/kotlin/gov/cdc/prime/reportstream/submissions/controllers/SubmissionController.kt index 1e7612810d3..2de41c47a74 100644 --- a/submissions/src/main/kotlin/gov/cdc/prime/reportstream/submissions/controllers/SubmissionController.kt +++ b/submissions/src/main/kotlin/gov/cdc/prime/reportstream/submissions/controllers/SubmissionController.kt @@ -13,6 +13,9 @@ import gov.cdc.prime.reportstream.submissions.TelemetryService import org.slf4j.LoggerFactory import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity +import org.springframework.security.authorization.AuthorizationDeniedException +import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthentication +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken import org.springframework.web.bind.MissingRequestHeaderException import org.springframework.web.bind.annotation.ControllerAdvice import org.springframework.web.bind.annotation.ExceptionHandler @@ -173,6 +176,16 @@ class SubmissionController( return ResponseEntity("Internal Server Error: ${e.message}", HttpStatus.INTERNAL_SERVER_ERROR) } + @ExceptionHandler(AuthorizationDeniedException::class) + fun handleAuthorizationException( + e: AuthorizationDeniedException, + auth: JwtAuthenticationToken + ): ResponseEntity { + logger.warn("Authorization denied for token attributes: ${auth.tokenAttributes}", e) + + return ResponseEntity.status(HttpStatus.FORBIDDEN).build() + } + /** * Handles exceptions of type IllegalArgumentException. * diff --git a/submissions/src/main/resources/application.properties b/submissions/src/main/resources/application.properties index 14a3fe573d2..a8750014f5d 100644 --- a/submissions/src/main/resources/application.properties +++ b/submissions/src/main/resources/application.properties @@ -3,4 +3,5 @@ server.port=8880 azure.storage.connection-string=${AZURE_STORAGE_CONNECTION_STRING:DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://localhost:10000/devstoreaccount1;QueueEndpoint=http://localhost:10001/devstoreaccount1;TableEndpoint=http://127.0.0.1:10002/devstoreaccount1;} azure.storage.container-name=${AZURE_STORAGE_CONTAINER_NAME:reports} azure.storage.queue-name=${AZURE_STORAGE_QUEUE_NAME:elr-fhir-receive} -azure.storage.table-name=${AZURE_STORAGE_TABLE_NAME:submission} \ No newline at end of file +azure.storage.table-name=${AZURE_STORAGE_TABLE_NAME:submission} +spring.security.oauth2.resourceserver.jwt.issuer-uri=https://reportstream.oktapreview.com/oauth2/ausekaai7gUuUtHda1d7 \ No newline at end of file