diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..9e87004 --- /dev/null +++ b/.env.example @@ -0,0 +1,13 @@ +HEIMDALL_URL=http://localhost:8080 +HEIMDALL_SUCCESS=http://localhost:8080/success +HEIMDALL_FAILURE=http://localhost:8080/failure + +GOOGLE_AID= +GOOGLE_SECRET= + +MSID_AID= +MSID_SECRET= +MSID_TENANT=common + +KEYSTORE_PRIVATE_KEY_PASSWORD=pk-password +KEYSTORE_PASSWORD=keystore-password \ No newline at end of file diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml new file mode 100644 index 0000000..b22b642 --- /dev/null +++ b/.github/workflows/gradle.yml @@ -0,0 +1,30 @@ +name: Build + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Set up JDK 21 + uses: actions/setup-java@v3 + with: + java-version: 21 + distribution: corretto + + - name: Validate Gradle wrapper + uses: gradle/wrapper-validation-action@v1.0.4 + + - name: Build with Gradle + uses: gradle/gradle-build-action@v2.2.0 + with: + arguments: build \ No newline at end of file diff --git a/.gitignore b/.gitignore index d6dc924..9dd4b96 100644 --- a/.gitignore +++ b/.gitignore @@ -1,40 +1,20 @@ +Thumbs.db +.DS_Store .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 ### +target/ +out/ +.micronaut/ .idea -*.iws *.iml *.ipr -out/ -!**/src/main/**/out/ -!**/src/test/**/out/ - -### NetBeans ### -/nbproject/private/ -/nbbuild/ -/dist/ -/nbdist/ -/.nb-gradle/ - -### VS Code ### -.vscode/ +*.iws +.project +.settings +.classpath +.factorypath ### Project ### heimdall.jks heimdall-storage +.env \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 1c3d839..d5a15af 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,8 @@ FROM amazoncorretto:21 -ADD build/distributions/heimdall.tar ./ -RUN chmod a+rw heimdall -WORKDIR ./heimdall +ADD build/distributions/heimdall-optimized-0.1.tar ./ +RUN chmod a+rw heimdall-optimized-0.1 +WORKDIR ./heimdall-optimized-0.1 EXPOSE 8080 ENV TZ="UTC" diff --git a/README.md b/README.md index 2239ca2..9fdcd10 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,2 @@ # Heimdall -User Authentication service +User Authentication service \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 79f3c9f..c32a2cf 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,32 +1,34 @@ plugins { - application alias(libs.plugins.kotlin.jvm) - alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.kotlin.all.open) + alias(libs.plugins.ksp) + alias(libs.plugins.johnrengelman.shadow) + alias(libs.plugins.micronaut.application) + alias(libs.plugins.micronaut.aot) alias(libs.plugins.kotlinter) } +version = "0.1" group = "com.qualitive" -version = "1.0.0" - -kotlin { - jvmToolchain(21) -} - -application { - mainClass.set("io.ktor.server.netty.EngineMain") - - val isDevelopment: Boolean = project.ext.has("development") - applicationDefaultJvmArgs = listOf("-Dio.ktor.development=$isDevelopment") -} repositories { mavenCentral() } dependencies { - implementation(libs.bundles.ktor) + ksp(libs.micronaut.http.validation) - implementation(libs.logback) + compileOnly(libs.micronaut.http.client) + implementation(libs.micronaut.jackson.databind) + implementation(libs.micronaut.kotlin.runtime) + + implementation(libs.kotlin.reflect) + implementation(libs.kotlin.stdlib) + + runtimeOnly(libs.jackson.kotlin) + + runtimeOnly(libs.logback) + implementation(libs.logstash.logbackEncoder) implementation(libs.kotlinLogging) implementation(libs.bouncycastle.bcprov) @@ -35,20 +37,52 @@ dependencies { implementation(libs.scribejava.apis) implementation(libs.auth0.jwt) - implementation(libs.logstash.logbackEncoder) + testImplementation(libs.micronaut.http.client) +} - testImplementation(libs.ktor.test) - testImplementation(libs.kotlin.test) + +application { + mainClass = "com.qualitive.heimdall.ApplicationKt" +} +java { + sourceCompatibility = JavaVersion.toVersion("21") +} + + +graalvmNative.toolchainDetection = false +micronaut { + runtime("netty") + testRuntime("kotest5") + processing { + incremental(true) + annotations("com.qualitive.*") + } + aot { + // Please review carefully the optimizations enabled below + // Check https://micronaut-projects.github.io/micronaut-aot/latest/guide/ for more details + optimizeServiceLoading = false + convertYamlToJava = false + precomputeOperations = true + cacheEnvironment = true + optimizeClassLoading = true + deduceEnvironment = true + optimizeNetty = true + replaceLogbackXml = true + } } tasks.test { - ignoreFailures = false useJUnitPlatform() + ignoreFailures = false // Never use existing jks doFirst { delete("${project.projectDir}/heimdall.jks")} doLast { delete("${project.projectDir}/heimdall.jks") } } -tasks.distTar { archiveFileName.set("heimdall.tar") } -tasks.distZip { enabled = false } + +tasks.named("dockerfileNative") { + jdkVersion = "21" +} + + diff --git a/docker-compose.yaml b/docker-compose.yaml index 1ec95ac..6f99459 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,14 +1,19 @@ services: - heimdall: - build: . - container_name: heimdall - ports: - - "8080:8080" - environment: - - HEIMDALL_SUCCESS=http://localhost:8081/heimdall-success - - HEIMDALL_FAILURE=http://localhost:8080/loginerror - - HEIMDALL_URL=http://localhost:8080 - - KEYSTORE_PASS=heimdall - - PRIVATE_KEY_PASS=heimdall - volumes: - - ./heimdall-storage:/var/lib/heimdall + heimdall: + build: . + container_name: heimdall + ports: + - "8080:8080" + environment: + - HEIMDALL_URL=${HEIMDALL_URL} + - HEIMDALL_SUCCESS=${HEIMDALL_SUCCESS} + - HEIMDALL_FAILURE=${HEIMDALL_FAILURE} + - HEIMDALL_GOOGLE_AID=${GOOGLE_AID} + - HEIMDALL_GOOGLE_SECRET=${GOOGLE_SECRET} + - HEIMDALL_MSID_AID=${MSID_AID} + - HEIMDALL_MSID_SECRET=${MSID_SECRET} + - HEIMDALL_MSID_TENANT=${MSID_TENANT} + - KEYSTORE_PASSWORD=${KEYSTORE_PASSWORD} + - KEYSTORE_PRIVATE_KEY_PASSWORD=${KEYSTORE_PRIVATE_KEY_PASSWORD} + volumes: + - ./heimdall-storage:/var/lib/heimdall diff --git a/gradle.properties b/gradle.properties index 7fc6f1f..003f431 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1,3 @@ -kotlin.code.style=official +micronautVersion=4.4.3 +kotlinVersion=1.9.23 +org.gradle.jvmargs=-Xmx4096M diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2cac70c..2efe61f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,21 +1,24 @@ [versions] -ktorVersion = "3.0.0-beta-1" -kotlinVersion = "1.9.22" +kotlinVersion = "2.0.0" logbackVersion = "1.4.14" kotlinterVersion = "4.2.0" bouncyCastleVersion = "1.77" scribeVersion = "8.3.3" +micronautVersion = "4.3.0" [libraries] -kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test-junit5", version.ref = "kotlinVersion" } +micronaut-http-validation = { module = "io.micronaut:micronaut-http-validation", version.ref = "micronautVersion" } +micronaut-jackson-databind = { module = "io.micronaut:micronaut-jackson-databind", version.ref = "micronautVersion" } +micronaut-kotlin-runtime = { module = "io.micronaut.kotlin:micronaut-kotlin-runtime", version.ref = "micronautVersion" } +micronaut-http-client = { module = "io.micronaut:micronaut-http-client", version.ref = "micronautVersion" } -ktor-core = { module = "io.ktor:ktor-server-core-jvm", version.ref = "ktorVersion" } -ktor-contentNegotiation = { module = "io.ktor:ktor-server-content-negotiation-jvm", version.ref = "ktorVersion" } -ktor-serialization = { module = "io.ktor:ktor-serialization-kotlinx-json-jvm", version.ref = "ktorVersion" } -ktor-netty = { module = "io.ktor:ktor-server-netty", version.ref = "ktorVersion" } -ktor-test = { module = "io.ktor:ktor-server-tests-jvm", version.ref = "ktorVersion" } +kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlinVersion" } +kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlinVersion" } + +jackson-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin" } logback = { module = "ch.qos.logback:logback-classic", version.ref = "logbackVersion" } +logstash-logbackEncoder = { module = "net.logstash.logback:logstash-logback-encoder", version = "7.4" } kotlinLogging = { module = "io.github.oshai:kotlin-logging-jvm", version = "5.1.0" } bouncycastle-bcprov = { module = "org.bouncycastle:bcprov-jdk18on", version.ref = "bouncyCastleVersion" } @@ -25,12 +28,11 @@ scribejava-core = { module = "com.github.scribejava:scribejava-core", version.re scribejava-apis = { module = "com.github.scribejava:scribejava-apis", version.ref = "scribeVersion" } auth0-jwt = { module = "com.auth0:java-jwt", version = "4.4.0" } -logstash-logbackEncoder = { module = "net.logstash.logback:logstash-logback-encoder", version = "7.4" } - -[bundles] -ktor = ["ktor-core", "ktor-contentNegotiation", "ktor-netty", "ktor-serialization"] - [plugins] kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlinVersion" } -kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlinVersion" } +kotlin-all-open = { id = "org.jetbrains.kotlin.plugin.allopen", version.ref = "kotlinVersion" } +ksp = { id = "com.google.devtools.ksp", version = "2.0.0-1.0.22"} +johnrengelman-shadow = { id = "com.github.johnrengelman.shadow", version = "8.1.1"} kotlinter = { id = "org.jmailen.kotlinter", version.ref = "kotlinterVersion" } +micronaut-application = { id = "io.micronaut.application", version.ref = "micronautVersion"} +micronaut-aot = { id = "io.micronaut.aot", version.ref = "micronautVersion"} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7f93135..e644113 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a80b22c..b82aa23 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew.bat b/gradlew.bat index 6689b85..7101f8e 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -43,11 +43,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail diff --git a/settings.gradle.kts b/settings.gradle.kts index 4f24560..4b078a3 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1 +1 @@ -rootProject.name = "heimdall" +rootProject.name="heimdall" \ No newline at end of file diff --git a/src/main/kotlin/com/qualitive/Application.kt b/src/main/kotlin/com/qualitive/Application.kt deleted file mode 100644 index 6f70662..0000000 --- a/src/main/kotlin/com/qualitive/Application.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.qualitive - -import com.qualitive.plugins.configureRouting -import com.qualitive.plugins.configureSerialization -import io.ktor.server.application.Application -import org.bouncycastle.jce.provider.BouncyCastleProvider -import java.security.Security - -fun main(args: Array) { - io.ktor.server.netty.EngineMain.main(args) -} - -fun Application.module() { - Security.addProvider(BouncyCastleProvider()) - - configureSerialization() - configureRouting() -} diff --git a/src/main/kotlin/com/qualitive/heimdall/Application.kt b/src/main/kotlin/com/qualitive/heimdall/Application.kt new file mode 100644 index 0000000..2f21aa4 --- /dev/null +++ b/src/main/kotlin/com/qualitive/heimdall/Application.kt @@ -0,0 +1,7 @@ +package com.qualitive.heimdall + +import io.micronaut.runtime.Micronaut.run + +fun main(args: Array) { + run(*args) +} diff --git a/src/main/kotlin/com/qualitive/heimdall/conf/BeanConf.kt b/src/main/kotlin/com/qualitive/heimdall/conf/BeanConf.kt new file mode 100644 index 0000000..86c2f1c --- /dev/null +++ b/src/main/kotlin/com/qualitive/heimdall/conf/BeanConf.kt @@ -0,0 +1,62 @@ +package com.qualitive.heimdall.conf + +import com.github.scribejava.apis.GoogleApi20 +import com.github.scribejava.core.builder.ServiceBuilder +import com.qualitive.heimdall.crypto.KeyStoreKeyManager +import com.qualitive.heimdall.provider.GoogleProvider +import com.qualitive.heimdall.provider.MicrosoftIdentityProvider +import com.qualitive.heimdall.provider.api.MicrosoftIdentityApi +import com.qualitive.heimdall.util.TokenFactory +import io.micronaut.context.annotation.Context +import io.micronaut.context.annotation.Factory +import io.micronaut.context.annotation.Value +import jakarta.inject.Singleton + +@Factory +class BeanConf { + @Context + fun heimdallConf( + @Value("\${heimdall.url}") url: String, + @Value("\${heimdall.success-url}") successUrl: String, + @Value("\${heimdall.failure-url}") failureUrl: String, + ) = HeimdallConf( + url = url, + successRedirect = successUrl, + failureRedirect = failureUrl, + ) + + @Singleton + fun tokenFactory(keyStoreKeyManager: KeyStoreKeyManager) = TokenFactory(keyStoreKeyManager) + + @Context + fun googleProvider( + @Value("\${heimdall.google.app-id}") appId: String, + @Value("\${heimdall.google.secret}") secret: String, + heimdallConf: HeimdallConf, + tokenFactory: TokenFactory, + ): GoogleProvider { + val googleOauth = ServiceBuilder(appId) + .apiSecret(secret) + .callback("${heimdallConf.url}/google/callback") + .defaultScope("openid email profile") + .build(GoogleApi20.instance()) + return GoogleProvider(googleOauth, tokenFactory) + } + + @Context + fun msidProvider( + @Value("\${heimdall.msid.app-id}") appId: String, + @Value("\${heimdall.msid.secret}") secret: String, + @Value("\${heimdall.msid.tenant}") tenant: String, + heimdallConf: HeimdallConf, + tokenFactory: TokenFactory, + ): MicrosoftIdentityProvider { + val msidOauth = ServiceBuilder(appId) + .apiSecret(secret) + .callback("${heimdallConf.url}/msidentity/callback") + .responseType("id_token") + .defaultScope("openid email profile") + .build(MicrosoftIdentityApi(tenant)) + return MicrosoftIdentityProvider(msidOauth, tokenFactory) + } +} diff --git a/src/main/kotlin/com/qualitive/conf/HeimdallConf.kt b/src/main/kotlin/com/qualitive/heimdall/conf/HeimdallConf.kt similarity index 76% rename from src/main/kotlin/com/qualitive/conf/HeimdallConf.kt rename to src/main/kotlin/com/qualitive/heimdall/conf/HeimdallConf.kt index 913c443..577f2d5 100644 --- a/src/main/kotlin/com/qualitive/conf/HeimdallConf.kt +++ b/src/main/kotlin/com/qualitive/heimdall/conf/HeimdallConf.kt @@ -1,4 +1,4 @@ -package com.qualitive.conf +package com.qualitive.heimdall.conf data class HeimdallConf( val url: String, diff --git a/src/main/kotlin/com/qualitive/heimdall/controller/GoogleProviderController.kt b/src/main/kotlin/com/qualitive/heimdall/controller/GoogleProviderController.kt new file mode 100644 index 0000000..2c6f553 --- /dev/null +++ b/src/main/kotlin/com/qualitive/heimdall/controller/GoogleProviderController.kt @@ -0,0 +1,28 @@ +package com.qualitive.heimdall.controller + +import com.qualitive.heimdall.conf.HeimdallConf +import com.qualitive.heimdall.provider.GoogleProvider +import io.micronaut.http.HttpResponse +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.QueryValue +import java.net.URI + +@Controller(value = "/google") +class GoogleProviderController( + private val googleProvider: GoogleProvider, + private val heimdallConf: HeimdallConf, +) { + @Get + fun authenticate(): HttpResponse { + val redirectLocation = URI.create(googleProvider.authenticate()) + return HttpResponse.redirect(redirectLocation) + } + + @Get("/callback") + fun callback( + @QueryValue("code") code: String, + ): HttpResponse { + return HttpResponse.redirect(googleProvider.callback(code).getUrl(heimdallConf)) + } +} diff --git a/src/main/kotlin/com/qualitive/heimdall/controller/MicrosoftIdentityProviderController.kt b/src/main/kotlin/com/qualitive/heimdall/controller/MicrosoftIdentityProviderController.kt new file mode 100644 index 0000000..368ce4b --- /dev/null +++ b/src/main/kotlin/com/qualitive/heimdall/controller/MicrosoftIdentityProviderController.kt @@ -0,0 +1,28 @@ +package com.qualitive.heimdall.controller + +import com.qualitive.heimdall.conf.HeimdallConf +import com.qualitive.heimdall.provider.MicrosoftIdentityProvider +import io.micronaut.http.HttpResponse +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.QueryValue +import java.net.URI + +@Controller(value = "/msidentity") +class MicrosoftIdentityProviderController( + private val provider: MicrosoftIdentityProvider, + private val heimdallConf: HeimdallConf, +) { + @Get + fun authenticate(): HttpResponse { + val redirectLocation = URI.create(provider.authenticate()) + return HttpResponse.redirect(redirectLocation) + } + + @Get("/callback") + fun callback( + @QueryValue("code") code: String, + ): HttpResponse { + return HttpResponse.redirect(provider.callback(code).getUrl(heimdallConf)) + } +} diff --git a/src/main/kotlin/com/qualitive/heimdall/controller/PublicKeyController.kt b/src/main/kotlin/com/qualitive/heimdall/controller/PublicKeyController.kt new file mode 100644 index 0000000..f93eddb --- /dev/null +++ b/src/main/kotlin/com/qualitive/heimdall/controller/PublicKeyController.kt @@ -0,0 +1,17 @@ +package com.qualitive.heimdall.controller + +import com.qualitive.heimdall.crypto.KeyStoreKeyManager +import io.micronaut.http.MediaType +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import java.util.Base64 + +@Controller +class PublicKeyController( + private val keyStoreKeyManager: KeyStoreKeyManager, +) { + @Get("/public-key", produces = [MediaType.TEXT_PLAIN]) + fun getPublicKey(): String { + return Base64.getEncoder().withoutPadding().encodeToString(keyStoreKeyManager.publicKey.encoded) + } +} diff --git a/src/main/kotlin/com/qualitive/crypto/KeyStoreKeyManager.kt b/src/main/kotlin/com/qualitive/heimdall/crypto/KeyStoreKeyManager.kt similarity index 91% rename from src/main/kotlin/com/qualitive/crypto/KeyStoreKeyManager.kt rename to src/main/kotlin/com/qualitive/heimdall/crypto/KeyStoreKeyManager.kt index c1e2c26..722f2a5 100644 --- a/src/main/kotlin/com/qualitive/crypto/KeyStoreKeyManager.kt +++ b/src/main/kotlin/com/qualitive/heimdall/crypto/KeyStoreKeyManager.kt @@ -1,6 +1,8 @@ -package com.qualitive.crypto +package com.qualitive.heimdall.crypto import io.github.oshai.kotlinlogging.KotlinLogging +import io.micronaut.context.annotation.Value +import jakarta.inject.Singleton import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder @@ -15,6 +17,7 @@ import java.security.KeyPair import java.security.KeyPairGenerator import java.security.KeyStore import java.security.SecureRandom +import java.security.Security import java.security.cert.X509Certificate import java.security.interfaces.ECPrivateKey import java.security.interfaces.ECPublicKey @@ -24,8 +27,11 @@ import java.util.Date private val logger = KotlinLogging.logger {} +@Singleton class KeyStoreKeyManager( + @Value("\${keystore.private-key-password:test}") private val privateKeyPass: String, + @Value("\${keystore.keystore-password:test}") private val keystorePass: String, ) { private var certificate: X509Certificate @@ -45,6 +51,8 @@ class KeyStoreKeyManager( } private fun createKeyStore(): KeyStore { + Security.addProvider(BouncyCastleProvider()) + val keyPair = KeyPairGenerator.getInstance("ECDSA").apply { initialize(256, SecureRandom()) }.let(KeyPairGenerator::generateKeyPair) diff --git a/src/main/kotlin/com/qualitive/dto/AuthenticationResult.kt b/src/main/kotlin/com/qualitive/heimdall/dto/AuthenticationResult.kt similarity index 62% rename from src/main/kotlin/com/qualitive/dto/AuthenticationResult.kt rename to src/main/kotlin/com/qualitive/heimdall/dto/AuthenticationResult.kt index 9523fa4..28d77df 100644 --- a/src/main/kotlin/com/qualitive/dto/AuthenticationResult.kt +++ b/src/main/kotlin/com/qualitive/heimdall/dto/AuthenticationResult.kt @@ -1,15 +1,16 @@ -package com.qualitive.dto +package com.qualitive.heimdall.dto -import com.qualitive.conf.HeimdallConf +import com.qualitive.heimdall.conf.HeimdallConf +import java.net.URI class AuthenticationResult( private val jwt: String? = null, private val failureReason: FailureReason? = null, ) { fun getUrl(conf: HeimdallConf) = if (jwt != null) { - "${conf.successRedirect}#$jwt" + URI.create("${conf.successRedirect}#$jwt") } else { - "${conf.failureRedirect}#${failureReason?.errorCode}" + URI.create("${conf.failureRedirect}#${failureReason?.errorCode}") } companion object { diff --git a/src/main/kotlin/com/qualitive/dto/FailureReason.kt b/src/main/kotlin/com/qualitive/heimdall/dto/FailureReason.kt similarity index 75% rename from src/main/kotlin/com/qualitive/dto/FailureReason.kt rename to src/main/kotlin/com/qualitive/heimdall/dto/FailureReason.kt index 73fe2b7..83c693a 100644 --- a/src/main/kotlin/com/qualitive/dto/FailureReason.kt +++ b/src/main/kotlin/com/qualitive/heimdall/dto/FailureReason.kt @@ -1,4 +1,4 @@ -package com.qualitive.dto +package com.qualitive.heimdall.dto enum class FailureReason(val errorCode: Int) { AUTHENTICATION_FAILED(400), diff --git a/src/main/kotlin/com/qualitive/dto/HeimdallUser.kt b/src/main/kotlin/com/qualitive/heimdall/dto/HeimdallUser.kt similarity index 67% rename from src/main/kotlin/com/qualitive/dto/HeimdallUser.kt rename to src/main/kotlin/com/qualitive/heimdall/dto/HeimdallUser.kt index 2b0baad..26ee9cf 100644 --- a/src/main/kotlin/com/qualitive/dto/HeimdallUser.kt +++ b/src/main/kotlin/com/qualitive/heimdall/dto/HeimdallUser.kt @@ -1,6 +1,6 @@ -package com.qualitive.dto +package com.qualitive.heimdall.dto -import com.qualitive.provider.HeimdallProvider +import com.qualitive.heimdall.provider.HeimdallProvider data class HeimdallUser( val subject: String, diff --git a/src/main/kotlin/com/qualitive/exception/CallbackFailed.kt b/src/main/kotlin/com/qualitive/heimdall/exception/CallbackFailed.kt similarity index 62% rename from src/main/kotlin/com/qualitive/exception/CallbackFailed.kt rename to src/main/kotlin/com/qualitive/heimdall/exception/CallbackFailed.kt index 5352bbf..c578ebf 100644 --- a/src/main/kotlin/com/qualitive/exception/CallbackFailed.kt +++ b/src/main/kotlin/com/qualitive/heimdall/exception/CallbackFailed.kt @@ -1,3 +1,3 @@ -package com.qualitive.exception +package com.qualitive.heimdall.exception class CallbackFailed(message: String) : RuntimeException(message) diff --git a/src/main/kotlin/com/qualitive/provider/GoogleProvider.kt b/src/main/kotlin/com/qualitive/heimdall/provider/GoogleProvider.kt similarity index 82% rename from src/main/kotlin/com/qualitive/provider/GoogleProvider.kt rename to src/main/kotlin/com/qualitive/heimdall/provider/GoogleProvider.kt index 804da3a..a6a7b25 100644 --- a/src/main/kotlin/com/qualitive/provider/GoogleProvider.kt +++ b/src/main/kotlin/com/qualitive/heimdall/provider/GoogleProvider.kt @@ -1,13 +1,13 @@ -package com.qualitive.provider +package com.qualitive.heimdall.provider import com.auth0.jwt.JWT import com.github.scribejava.apis.openid.OpenIdOAuth2AccessToken import com.github.scribejava.core.oauth.OAuth20Service -import com.qualitive.dto.AuthenticationResult -import com.qualitive.dto.FailureReason -import com.qualitive.dto.HeimdallUser -import com.qualitive.util.TokenFactory -import com.qualitive.util.asNullableString +import com.qualitive.heimdall.dto.AuthenticationResult +import com.qualitive.heimdall.dto.FailureReason +import com.qualitive.heimdall.dto.HeimdallUser +import com.qualitive.heimdall.util.TokenFactory +import com.qualitive.heimdall.util.asNullableString import io.github.oshai.kotlinlogging.KotlinLogging private val logger = KotlinLogging.logger {} diff --git a/src/main/kotlin/com/qualitive/provider/HeimdallProvider.kt b/src/main/kotlin/com/qualitive/heimdall/provider/HeimdallProvider.kt similarity index 73% rename from src/main/kotlin/com/qualitive/provider/HeimdallProvider.kt rename to src/main/kotlin/com/qualitive/heimdall/provider/HeimdallProvider.kt index 2f38257..46263ed 100644 --- a/src/main/kotlin/com/qualitive/provider/HeimdallProvider.kt +++ b/src/main/kotlin/com/qualitive/heimdall/provider/HeimdallProvider.kt @@ -1,4 +1,4 @@ -package com.qualitive.provider +package com.qualitive.heimdall.provider enum class HeimdallProvider(val value: String) { GOOGLE("google"), diff --git a/src/main/kotlin/com/qualitive/provider/MicrosoftIdentityProvider.kt b/src/main/kotlin/com/qualitive/heimdall/provider/MicrosoftIdentityProvider.kt similarity index 86% rename from src/main/kotlin/com/qualitive/provider/MicrosoftIdentityProvider.kt rename to src/main/kotlin/com/qualitive/heimdall/provider/MicrosoftIdentityProvider.kt index 4a10416..89df29c 100644 --- a/src/main/kotlin/com/qualitive/provider/MicrosoftIdentityProvider.kt +++ b/src/main/kotlin/com/qualitive/heimdall/provider/MicrosoftIdentityProvider.kt @@ -1,11 +1,11 @@ -package com.qualitive.provider +package com.qualitive.heimdall.provider import com.auth0.jwt.JWT import com.github.scribejava.core.oauth.OAuth20Service -import com.qualitive.dto.AuthenticationResult -import com.qualitive.dto.FailureReason -import com.qualitive.dto.HeimdallUser -import com.qualitive.util.TokenFactory +import com.qualitive.heimdall.dto.AuthenticationResult +import com.qualitive.heimdall.dto.FailureReason +import com.qualitive.heimdall.dto.HeimdallUser +import com.qualitive.heimdall.util.TokenFactory import io.github.oshai.kotlinlogging.KotlinLogging import java.security.SecureRandom diff --git a/src/main/kotlin/com/qualitive/provider/RedirectingAuthenticationProvider.kt b/src/main/kotlin/com/qualitive/heimdall/provider/RedirectingAuthenticationProvider.kt similarity index 58% rename from src/main/kotlin/com/qualitive/provider/RedirectingAuthenticationProvider.kt rename to src/main/kotlin/com/qualitive/heimdall/provider/RedirectingAuthenticationProvider.kt index b50db17..77ee350 100644 --- a/src/main/kotlin/com/qualitive/provider/RedirectingAuthenticationProvider.kt +++ b/src/main/kotlin/com/qualitive/heimdall/provider/RedirectingAuthenticationProvider.kt @@ -1,6 +1,6 @@ -package com.qualitive.provider +package com.qualitive.heimdall.provider -import com.qualitive.dto.AuthenticationResult +import com.qualitive.heimdall.dto.AuthenticationResult interface RedirectingAuthenticationProvider { fun authenticate(): String diff --git a/src/main/kotlin/com/qualitive/provider/api/MicrosoftIdentityApi.kt b/src/main/kotlin/com/qualitive/heimdall/provider/api/MicrosoftIdentityApi.kt similarity index 89% rename from src/main/kotlin/com/qualitive/provider/api/MicrosoftIdentityApi.kt rename to src/main/kotlin/com/qualitive/heimdall/provider/api/MicrosoftIdentityApi.kt index cdffb3a..256a775 100644 --- a/src/main/kotlin/com/qualitive/provider/api/MicrosoftIdentityApi.kt +++ b/src/main/kotlin/com/qualitive/heimdall/provider/api/MicrosoftIdentityApi.kt @@ -1,4 +1,4 @@ -package com.qualitive.provider.api +package com.qualitive.heimdall.provider.api import com.github.scribejava.core.builder.api.DefaultApi20 diff --git a/src/main/kotlin/com/qualitive/util/JwtUtil.kt b/src/main/kotlin/com/qualitive/heimdall/util/JwtUtil.kt similarity index 74% rename from src/main/kotlin/com/qualitive/util/JwtUtil.kt rename to src/main/kotlin/com/qualitive/heimdall/util/JwtUtil.kt index e7c473a..f2bbb15 100644 --- a/src/main/kotlin/com/qualitive/util/JwtUtil.kt +++ b/src/main/kotlin/com/qualitive/heimdall/util/JwtUtil.kt @@ -1,4 +1,4 @@ -package com.qualitive.util +package com.qualitive.heimdall.util import com.auth0.jwt.interfaces.Claim diff --git a/src/main/kotlin/com/qualitive/util/TokenFactory.kt b/src/main/kotlin/com/qualitive/heimdall/util/TokenFactory.kt similarity index 83% rename from src/main/kotlin/com/qualitive/util/TokenFactory.kt rename to src/main/kotlin/com/qualitive/heimdall/util/TokenFactory.kt index cf1a5df..921cfa9 100644 --- a/src/main/kotlin/com/qualitive/util/TokenFactory.kt +++ b/src/main/kotlin/com/qualitive/heimdall/util/TokenFactory.kt @@ -1,9 +1,9 @@ -package com.qualitive.util +package com.qualitive.heimdall.util import com.auth0.jwt.JWT import com.auth0.jwt.algorithms.Algorithm -import com.qualitive.crypto.KeyStoreKeyManager -import com.qualitive.dto.HeimdallUser +import com.qualitive.heimdall.crypto.KeyStoreKeyManager +import com.qualitive.heimdall.dto.HeimdallUser import java.time.Instant class TokenFactory(private val keyManager: KeyStoreKeyManager) { diff --git a/src/main/kotlin/com/qualitive/plugins/Routing.kt b/src/main/kotlin/com/qualitive/plugins/Routing.kt deleted file mode 100644 index e76cef1..0000000 --- a/src/main/kotlin/com/qualitive/plugins/Routing.kt +++ /dev/null @@ -1,84 +0,0 @@ -package com.qualitive.plugins - -import com.github.scribejava.apis.GoogleApi20 -import com.github.scribejava.core.builder.ServiceBuilder -import com.qualitive.conf.HeimdallConf -import com.qualitive.crypto.KeyStoreKeyManager -import com.qualitive.exception.CallbackFailed -import com.qualitive.provider.GoogleProvider -import com.qualitive.provider.MicrosoftIdentityProvider -import com.qualitive.provider.api.MicrosoftIdentityApi -import com.qualitive.util.TokenFactory -import io.ktor.server.application.Application -import io.ktor.server.config.ApplicationConfig -import io.ktor.server.request.receiveParameters -import io.ktor.server.response.respondRedirect -import io.ktor.server.response.respondText -import io.ktor.server.routing.get -import io.ktor.server.routing.post -import io.ktor.server.routing.route -import io.ktor.server.routing.routing -import java.util.Base64 - -fun Application.configureRouting() { - //region configuration - val heimdallConf = HeimdallConf( - url = environment.config.stringProperty("heimdall.url"), - successRedirect = environment.config.stringProperty("heimdall.successRedirect"), - failureRedirect = environment.config.stringProperty("heimdall.failureRedirect"), - ) - - val keyStoreKeyManager = KeyStoreKeyManager( - privateKeyPass = environment.config.stringProperty("keystore.privateKeyPass"), - keystorePass = environment.config.stringProperty("keystore.keystorePass"), - ) - - val tokenFactory = TokenFactory(keyStoreKeyManager) - - val googleOauth = ServiceBuilder(environment.config.stringProperty("heimdall.google.appId")) - .apiSecret(environment.config.stringProperty("heimdall.google.secret")) - .callback("${heimdallConf.url}/google/callback") - .defaultScope("openid email profile") - .build(GoogleApi20.instance()) - - val msidOauth = ServiceBuilder(environment.config.stringProperty("heimdall.msid.appId")) - .apiSecret(environment.config.stringProperty("heimdall.msid.secret")) - .callback("${heimdallConf.url}/msidentity/callback") - .responseType("id_token") - .defaultScope("openid email profile") - .build(MicrosoftIdentityApi(environment.config.stringProperty("heimdall.msid.tenant"))) - - val googleProvider = GoogleProvider(googleOauth, tokenFactory) - val msidProvider = MicrosoftIdentityProvider(msidOauth, tokenFactory) - //endregion - - routing { - get("/public-key") { - val encodedPublicKey = Base64.getEncoder().withoutPadding().encodeToString(keyStoreKeyManager.publicKey.encoded) - call.respondText(encodedPublicKey) - } - - route("/google") { - get { - call.respondRedirect(googleProvider.authenticate()) - } - get("/callback") { - val code = call.request.queryParameters["code"] ?: throw CallbackFailed("Could not find 'code' query param") - call.respondRedirect(googleProvider.callback(code).getUrl(heimdallConf)) - } - } - - route("/msidentity") { - get { - call.respondRedirect(msidProvider.authenticate()) - } - post("/callback") { - val formParams = call.receiveParameters() - val idToken = formParams["id_token"] ?: throw CallbackFailed("Could not find 'id_token' query param") - call.respondRedirect(msidProvider.callback(idToken).getUrl(heimdallConf)) - } - } - } -} - -private fun ApplicationConfig.stringProperty(path: String) = property(path).getString() diff --git a/src/main/kotlin/com/qualitive/plugins/Serialization.kt b/src/main/kotlin/com/qualitive/plugins/Serialization.kt deleted file mode 100644 index e15c2a2..0000000 --- a/src/main/kotlin/com/qualitive/plugins/Serialization.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.qualitive.plugins - -import io.ktor.serialization.kotlinx.json.json -import io.ktor.server.application.Application -import io.ktor.server.application.install -import io.ktor.server.plugins.contentnegotiation.ContentNegotiation -import io.ktor.server.routing.get - -fun Application.configureSerialization() { - install(ContentNegotiation) { - json() - } -} diff --git a/src/main/resources/application.conf b/src/main/resources/application.conf deleted file mode 100644 index 5ee0852..0000000 --- a/src/main/resources/application.conf +++ /dev/null @@ -1,31 +0,0 @@ -ktor { - deployment { - port = 8080 - port = ${?PORT} - } - application { - modules = [ com.qualitive.ApplicationKt.module ] - } -} - -heimdall { - url = ${HEIMDALL_URL} - successRedirect = ${HEIMDALL_SUCCESS} - failureRedirect = ${HEIMDALL_FAILURE} - - google { - appId = ${GOOGLE_AID} - secret = ${GOOGLE_SECRET} - } - - msid { - appId = ${MSID_AID} - secret = ${MSID_SECRET} - tenant = ${MSID_TENANT} - } -} - -keystore { - keystorePass = ${KEYSTORE_PASS} - privateKeyPass = ${PRIVATE_KEY_PASS} -} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..217ee9f --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,16 @@ +#Fri Jun 07 20:58:35 UTC 2024 +micronaut.application.name=heimdall + +heimdall.url=${HEIMDALL_URL} +heimdall.success-url=${HEIMDALL_SUCCESS} +heimdall.failure-url=${HEIMDALL_FAILURE} + +heimdall.google.app-id=${HEIMDALL_GOOGLE_AID} +heimdall.google.secret=${HEIMDALL_GOOGLE_SECRET} + +heimdall.msid.app-id=${HEIMDALL_MSID_AID} +heimdall.msid.secret=${HEIMDALL_MSID_SECRET} +heimdall.msid.tenant=${HEIMDALL_MSID_TENANT} + +keystore.private-key-password=${KEYSTORE_PRIVATE_KEY_PASSWORD} +keystore.keystore-password=${KEYSTORE_PASSWORD} \ No newline at end of file diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index 42b41fa..177b0dd 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -5,6 +5,5 @@ - diff --git a/src/main/resources/micronaut-banner.txt b/src/main/resources/micronaut-banner.txt new file mode 100644 index 0000000..86e5cf0 --- /dev/null +++ b/src/main/resources/micronaut-banner.txt @@ -0,0 +1,6 @@ + _ _ _ _ _ _ +| | | | (_) | | | | | +| |_| | ___ _ _ __ ___ __| | __ _| | | +| _ |/ _ \ | '_ ` _ \ / _` |/ _` | | | +| | | | __/ | | | | | | (_| | (_| | | | +\_| |_/\___|_|_| |_| |_|\__,_|\__,_|_|_| \ No newline at end of file diff --git a/src/test/kotlin/com/qualitive/ApplicationTest.kt b/src/test/kotlin/com/qualitive/ApplicationTest.kt deleted file mode 100644 index cd1dcd8..0000000 --- a/src/test/kotlin/com/qualitive/ApplicationTest.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.qualitive - -import com.qualitive.plugins.configureRouting -import io.ktor.client.request.get -import io.ktor.http.HttpStatusCode -import io.ktor.server.testing.testApplication -import kotlin.test.Test -import kotlin.test.assertEquals - -class ApplicationTest { - @Test - fun testRoot() = testApplication { - application { - configureRouting() - } - client.get("/public-key").apply { - assertEquals(HttpStatusCode.OK, status) - } - } -} diff --git a/src/test/kotlin/com/qualitive/crypto/KeyStoreKeyManagerTest.kt b/src/test/kotlin/com/qualitive/crypto/KeyStoreKeyManagerTest.kt deleted file mode 100644 index 67bc7c3..0000000 --- a/src/test/kotlin/com/qualitive/crypto/KeyStoreKeyManagerTest.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.qualitive.crypto - -import org.bouncycastle.jce.provider.BouncyCastleProvider -import org.junit.jupiter.api.assertDoesNotThrow -import java.security.Security -import kotlin.test.BeforeTest -import kotlin.test.Test - -class KeyStoreKeyManagerTest { - @BeforeTest - fun setup() { - Security.addProvider(BouncyCastleProvider()) - } - - @Test - fun `can instantiate class`() { - assertDoesNotThrow { - KeyStoreKeyManager("PrivateKey", "KeyStore") - } - } -} diff --git a/src/test/kotlin/com/qualitive/heimdall/HeimdallTest.kt b/src/test/kotlin/com/qualitive/heimdall/HeimdallTest.kt new file mode 100644 index 0000000..19c4bd3 --- /dev/null +++ b/src/test/kotlin/com/qualitive/heimdall/HeimdallTest.kt @@ -0,0 +1,23 @@ +package com.qualitive.heimdall +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldNotBe +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.annotation.Client +import io.micronaut.runtime.EmbeddedApplication +import io.micronaut.test.extensions.kotest5.annotation.MicronautTest + +@MicronautTest +class HeimdallTest( + private val application: EmbeddedApplication<*>, + @Client("/") val client: HttpClient, +) : StringSpec({ + + "test the server is running" { + assert(application.isRunning) + } + + "can get public key" { + val result = client.toBlocking().retrieve("/public-key") + result shouldNotBe null + } + }) diff --git a/src/test/kotlin/io/kotest/provided/ProjectConfig.kt b/src/test/kotlin/io/kotest/provided/ProjectConfig.kt new file mode 100644 index 0000000..a328bf4 --- /dev/null +++ b/src/test/kotlin/io/kotest/provided/ProjectConfig.kt @@ -0,0 +1,8 @@ +package io.kotest.provided + +import io.kotest.core.config.AbstractProjectConfig +import io.micronaut.test.extensions.kotest5.MicronautKotest5Extension + +object ProjectConfig : AbstractProjectConfig() { + override fun extensions() = listOf(MicronautKotest5Extension) +} diff --git a/src/test/resources/application-test.properties b/src/test/resources/application-test.properties new file mode 100644 index 0000000..7094a8a --- /dev/null +++ b/src/test/resources/application-test.properties @@ -0,0 +1,15 @@ +#Fri Jun 07 20:58:35 UTC 2024 +micronaut.application.name=heimdall + +heimdall.url=localhost +heimdall.success-url=localhost/success +heimdall.failure-url=localhost/failure + +heimdall.google.app-id=123 +heimdall.google.secret=secret +heimdall.msid.app-id=123 +heimdall.msid.secret=secret +heimdall.msid.tenant=common + +keystore.private-key-password=private-key-password +keystore.keystore-password=keystore-password \ No newline at end of file diff --git a/src/test/resources/application.conf b/src/test/resources/application.conf deleted file mode 100644 index 9ca15cc..0000000 --- a/src/test/resources/application.conf +++ /dev/null @@ -1,31 +0,0 @@ -ktor { - deployment { - port = 8080 - port = ${?PORT} - } - application { - modules = [ com.qualitive.ApplicationKt.module ] - } -} - -heimdall { - url = "http://localhost:8080" - successRedirect = "http://localhost:8080/success" - failureRedirect = "http://localhost:8080/failure" - - google { - appId = test - secret = test - } - - msid { - appId = test - secret = test - tenant = test - } -} - -keystore { - keystorePass = KeyStore - privateKeyPass = PrivateKey -} diff --git a/src/test/resources/logback.xml b/src/test/resources/logback.xml deleted file mode 100644 index ac32700..0000000 --- a/src/test/resources/logback.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - %d{yyyy-MM-dd HH:mm:ss.SSS} %5p [heimdall:%thread:%X{X-B3-TraceId}:%X{X-B3-SpanId}] %logger{40} - %msg%n - - - - - - - -