Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Authentication Microservice POC #15765

Merged
merged 21 commits into from
Sep 17, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions auth/.gitignore
Original file line number Diff line number Diff line change
@@ -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
59 changes: 59 additions & 0 deletions auth/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
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"

val ktorVersion: String by project.properties
val springCloudVersion: String by project.properties
val springCloudAzureVersion: String by project.properties

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")

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")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think some of these can be consolidated into the shared plugin?

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:$springCloudAzureVersion")
mavenBom("org.springframework.cloud:spring-cloud-dependencies:$springCloudVersion")
}
}

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")
}
}
3 changes: 3 additions & 0 deletions auth/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
ktorVersion=2.3.12
springCloudVersion=2023.0.3
springCloudAzureVersion=5.14.0
jalbinson marked this conversation as resolved.
Show resolved Hide resolved
Binary file added auth/gradle/wrapper/gradle-wrapper.jar
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure we need this file?

Binary file not shown.
7 changes: 7 additions & 0 deletions auth/gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here

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
Original file line number Diff line number Diff line change
@@ -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<String>) {
runApplication<AuthApplication>(*args)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package gov.cdc.prime.reportstream.auth.config

import org.springframework.beans.factory.annotation.Autowired
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

@Configuration
@EnableConfigurationProperties(ProxyConfigurationProperties::class)
class ApplicationConfig @Autowired constructor(
val proxyConfig: ProxyConfigurationProperties,
) {

@Bean
fun timeSource(): TimeSource {
return TimeSource.Monotonic
}
}

@ConfigurationProperties("proxy")
data class ProxyConfigurationProperties(
val pathMappings: List<ProxyPathMapping>,
)

data class ProxyPathMapping(
val baseUrl: String,
val pathPrefix: String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package gov.cdc.prime.reportstream.auth.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

@Configuration
@EnableWebFluxSecurity
class SecurityConfig {

@Bean
fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
http
.authorizeExchange { authorize ->
authorize
.pathMatchers("/health").permitAll()
.anyExchange().authenticated()
}
.oauth2ResourceServer {
it.opaqueToken { }
}

return http.build()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
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.beans.factory.annotation.Autowired
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 @Autowired constructor(
private val proxyURIStrategy: ProxyURIStrategy,
) : Logging {

@RequestMapping("**")
suspend fun proxy(
exchange: ServerWebExchange,
proxy: ProxyExchange<ByteArray>,
auth: BearerTokenAuthentication,
): ResponseEntity<ByteArray> {
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
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package gov.cdc.prime.reportstream.auth.controller

import gov.cdc.prime.reportstream.auth.model.ApplicationStatus
import org.springframework.beans.factory.annotation.Autowired
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 @Autowired constructor(
timeSource: TimeSource,
) {

private val applicationStart = timeSource.markNow()

@GetMapping("/health", produces = [MediaType.APPLICATION_JSON_VALUE])
jalbinson marked this conversation as resolved.
Show resolved Hide resolved
suspend fun health(): ApplicationStatus {
val uptime = applicationStart.elapsedNow().toString()
return ApplicationStatus("auth", "ok", uptime)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package gov.cdc.prime.reportstream.auth.model

data class ApplicationStatus(
val application: String,
val status: String,
val uptime: String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package gov.cdc.prime.reportstream.auth.service

import gov.cdc.prime.reportstream.auth.config.ApplicationConfig
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.Profile
import org.springframework.stereotype.Component
import java.net.URI

interface ProxyURIStrategy {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this what the spring gateway would replace?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thats the idea.

We could define it in configuration that

/reports -> http://localhost:8000/reports
/submission -> http://localhost:8888/submission
etc

fun getTargetURI(incomingUri: URI): URI
}

@Component
@Profile("local")
class PathPrefixProxyURIStrategy @Autowired constructor(
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")
}
}
}

@Component
@Profile("deployed")
class HostProxyPathURIStrategy : ProxyURIStrategy {
override fun getTargetURI(incomingUri: URI): URI {
TODO("Not yet implemented")
}
}
28 changes: 28 additions & 0 deletions auth/src/main/resources/application.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
spring:
application:
name: "auth"
profiles:
active: local
security:
oauth2:
resourceserver:
opaquetoken: # Add client-secret in env variable
client-id: 0oaek8tip2lhrhHce1d7
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we'll need to figure out how to make this per env too?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe that environment variables take precedence over configuration in application.yml so will just need to set the correct env variables per environment.

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

jalbinson marked this conversation as resolved.
Show resolved Hide resolved
logging:
jalbinson marked this conversation as resolved.
Show resolved Hide resolved
level:
web: debug
org.springframework.web: debug
Loading
Loading