Skip to content

Commit

Permalink
CID-2729: authenticate to GHE
Browse files Browse the repository at this point in the history
  • Loading branch information
mohamedlajmileanix committed Jun 25, 2024
1 parent 9f2fadb commit 70cd1ed
Show file tree
Hide file tree
Showing 12 changed files with 222 additions and 0 deletions.
16 changes: 16 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,25 @@ repositories {
mavenCentral()
}

extra["springCloudVersion"] = "2023.0.1"

dependencyManagement {
imports {
mavenBom("org.springframework.cloud:spring-cloud-dependencies:${property("springCloudVersion")}")
}
}

dependencies {
implementation("org.springframework.boot:spring-boot-starter")
implementation("org.springframework.cloud:spring-cloud-starter-openfeign")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("io.jsonwebtoken:jjwt-api:0.11.2")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.bouncycastle:bcprov-jdk18on:1.78")
implementation("org.bouncycastle:bcpkix-jdk18on:1.78")
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.11.2")
runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.11.2")

testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package net.leanix.githubagent

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.boot.runApplication
import org.springframework.cloud.openfeign.EnableFeignClients

@SpringBootApplication
@EnableFeignClients(value = ["net.leanix.githubagent.client"])
@EnableConfigurationProperties(value = [net.leanix.githubagent.config.GithubEnterpriseProperties::class])
class GithubAgentApplication

fun main() {
Expand Down
15 changes: 15 additions & 0 deletions src/main/kotlin/net/leanix/githubagent/client/GithubClient.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package net.leanix.githubagent.client

import org.springframework.cloud.openfeign.FeignClient
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestHeader

@FeignClient(name = "githubClient", url = "\${github-enterprise.baseUrl}")
interface GithubClient {

@GetMapping("/api/v3/app")
fun getApp(
@RequestHeader("Authorization") jwt: String,
@RequestHeader("Accept") accept: String = "application/vnd.github.v3+json"
): String
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package net.leanix.githubagent.config

import jakarta.annotation.PostConstruct
import net.leanix.githubagent.exceptions.GithubEnterpriseConfigurationMissingException
import org.springframework.stereotype.Component

@Component
class AgentSetupValidation(
private val githubEnterpriseProperties: GithubEnterpriseProperties
) {

@PostConstruct
fun validateConfiguration() {
val missingProperties = mutableListOf<String>()

if (githubEnterpriseProperties.baseUrl.isBlank()) {
missingProperties.add("GITHUB_ENTERPRISE_BASE_URL")
}
if (githubEnterpriseProperties.githubAppId.isBlank()) {
missingProperties.add("GITHUB_ENTERPRISE_GITHUB_APP_ID")
}
if (githubEnterpriseProperties.pemFile.isBlank()) {
missingProperties.add("GITHUB_ENTERPRISE_PEM_FILE")
}

if (missingProperties.isNotEmpty()) {
throw GithubEnterpriseConfigurationMissingException(missingProperties.joinToString(", "))
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package net.leanix.githubagent.config

import org.springframework.boot.context.properties.ConfigurationProperties

@ConfigurationProperties(prefix = "github-enterprise")
data class GithubEnterpriseProperties(
val baseUrl: String,
val githubAppId: String,
val pemFile: String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package net.leanix.githubagent.dto

import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.annotation.JsonProperty

@JsonIgnoreProperties(ignoreUnknown = true)
data class GithubAppResponse(
@JsonProperty("name") val name: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package net.leanix.githubagent.exceptions

class GithubEnterpriseConfigurationMissingException(properties: String) : RuntimeException(
"Github Enterprise properties '$properties' are not set"
)
class AuthenticationFailedException(message: String) : RuntimeException(message)
class ConnectingToGithubEnterpriseFailedException(message: String) : RuntimeException(message)
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package net.leanix.githubagent.runners

import net.leanix.githubagent.services.GithubAuthenticationService
import org.springframework.boot.ApplicationArguments
import org.springframework.boot.ApplicationRunner
import org.springframework.stereotype.Component

@Component
class PostStartupRunner(private val githubAuthenticationService: GithubAuthenticationService) : ApplicationRunner {

override fun run(args: ApplicationArguments?) {
githubAuthenticationService.generateJwtToken()
}
}
26 changes: 26 additions & 0 deletions src/main/kotlin/net/leanix/githubagent/services/CachingService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package net.leanix.githubagent.services

import jakarta.annotation.PostConstruct
import net.leanix.githubagent.config.GithubEnterpriseProperties
import org.springframework.stereotype.Service

@Service
class CachingService(
private val githubEnterpriseProperties: GithubEnterpriseProperties
) {
private val cache = HashMap<String, String?>()

@PostConstruct
private fun init() {
cache["baseUrl"] = githubEnterpriseProperties.baseUrl
cache["githubAppId"] = githubEnterpriseProperties.githubAppId
}

fun set(key: String, value: String) {
cache[key] = value
}

fun get(key: String): String? {
return cache[key]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package net.leanix.githubagent.services

import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import io.jsonwebtoken.Jwts
import io.jsonwebtoken.SignatureAlgorithm
import net.leanix.githubagent.client.GithubClient
import net.leanix.githubagent.config.GithubEnterpriseProperties
import net.leanix.githubagent.dto.GithubAppResponse
import net.leanix.githubagent.exceptions.AuthenticationFailedException
import net.leanix.githubagent.exceptions.ConnectingToGithubEnterpriseFailedException
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.slf4j.LoggerFactory
import org.springframework.core.io.ResourceLoader
import org.springframework.stereotype.Service
import java.io.File
import java.io.IOException
import java.nio.charset.Charset
import java.nio.file.Files
import java.security.KeyFactory
import java.security.PrivateKey
import java.security.Security
import java.security.spec.PKCS8EncodedKeySpec
import java.util.*

@Service
class GithubAuthenticationService(
private val cachingService: CachingService,
private val githubClient: GithubClient,
private val githubEnterpriseProperties: GithubEnterpriseProperties,
private val resourceLoader: ResourceLoader
) {

companion object {
private const val JWT_EXPIRATION_DURATION = 600000L
private val logger = LoggerFactory.getLogger(GithubAuthenticationService::class.java)
}

fun generateJwtToken() {
runCatching {
logger.info("Generating JWT token")
Security.addProvider(BouncyCastleProvider())
val rsaPrivateKey: String = readPrivateKey(loadPemFile())
val keySpec = PKCS8EncodedKeySpec(Base64.getDecoder().decode(rsaPrivateKey))
val privateKey = KeyFactory.getInstance("RSA").generatePrivate(keySpec)
createJwtToken(privateKey)?.also {
cachingService.set("jwtToken", it)
verifyJwt(it)
} ?: throw AuthenticationFailedException("Failed to generate a valid JWT token")
}
}

private fun createJwtToken(privateKey: PrivateKey): String? {
return Jwts.builder()
.setIssuedAt(Date())
.setExpiration(Date(System.currentTimeMillis() + JWT_EXPIRATION_DURATION))
.setIssuer(cachingService.get("githubAppId"))
.signWith(privateKey, SignatureAlgorithm.RS256)
.compact()
}

@Throws(IOException::class)
private fun readPrivateKey(file: File): String {
return String(Files.readAllBytes(file.toPath()), Charset.defaultCharset())
.replace("-----BEGIN RSA PRIVATE KEY-----", "")
.replace(System.lineSeparator().toRegex(), "")
.replace("-----END RSA PRIVATE KEY-----", "")
}

private fun verifyJwt(jwt: String) {
runCatching {
val githubApp = jacksonObjectMapper().readValue(
githubClient.getApp("Bearer $jwt"),
GithubAppResponse::class.java
)
logger.info("Authenticated as GitHub App: ${githubApp.name}")
}.onFailure {
throw ConnectingToGithubEnterpriseFailedException("Failed to verify JWT token")
}
}

private fun loadPemFile() =
File(resourceLoader.getResource("file:${githubEnterpriseProperties.pemFile}").uri)
}
4 changes: 4 additions & 0 deletions src/main/resources/application.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
github-enterprise:
baseUrl: ${GITHUB_ENTERPRISE_BASE_URL:}
githubAppId: ${GITHUB_APP_ID:}
pemFile: ${PEM_FILE:}
4 changes: 4 additions & 0 deletions src/test/resources/application.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
github-enterprise:
baseUrl: ${GITHUB_ENTERPRISE_BASE_URL:dummy}
githubAppId: ${GITHUB_APP_ID:dummy}
pemFile: ${PEM_FILE:dummy}

0 comments on commit 70cd1ed

Please sign in to comment.