-
Notifications
You must be signed in to change notification settings - Fork 40
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
Changes from 3 commits
312ffb4
b038476
6fdb013
a9cb906
1663c1a
4bbe1a1
56ffab0
b8d8bd7
56e4011
50e18b1
dbc7e1e
3264387
dbe3ff2
99b11c2
44265ca
8c65ba3
9435857
90080f4
14c26aa
b14ff38
03905f3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 |
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") | ||
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") | ||
} | ||
} |
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
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure we need this file? |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
distributionBase=GRADLE_USER_HOME | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this what the spring gateway would replace? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thats the idea. We could define it in configuration that
|
||
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") | ||
} | ||
} |
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
There was a problem hiding this comment.
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?