Skip to content

Commit 7909af0

Browse files
committed
add support for using GitHub app-based authentication
1 parent 604594a commit 7909af0

File tree

7 files changed

+233
-19
lines changed

7 files changed

+233
-19
lines changed

CONTRIBUTING.md

+18
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,24 @@ If you want to use the updater tool to add entries into the database, you need t
3434

3535
The production server uses mongodb to store data, however you can also use Fongo. If you would like to install mongodb and are on mac, I used this [guide](https://zellwk.com/blog/install-mongodb/) which utilizes homebrew. You can also install `mongo` which is a command-line tool that gives you access to your mongodb, allowing you to manually search through the database.
3636

37+
### GitHub App Authentication
38+
39+
The updater can be used with a GitHub Token or GitHub App. To use a GitHub app you need to generate an app on GitHub. Once you've done that you need to convert the key to PKCS#8 format using the following command:
40+
41+
```bash
42+
openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in your-rsa-private-key.pem -out pkcs8-key.pem
43+
```
44+
45+
Once this is done you can export the following variables at runtime:
46+
47+
```bash
48+
export GITHUB_APP_ID="1234"
49+
export GITHUB_APP_INSTALLATION_ID="1234"
50+
export GITHUB_APP_PRIVATE_KEY=$'-----BEGIN PRIVATE KEY-----
51+
<key contents>
52+
-----END PRIVATE KEY-----'
53+
```
54+
3755
### Build Tool
3856

3957
[Maven](https://maven.apache.org/index.html) is used to build the project.

adoptium-updater-parent/adoptium-datasources-parent/adoptium-github-datasource/pom.xml

+22
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,23 @@
1616
<groupId>net.adoptium.api</groupId>
1717
<artifactId>adoptium-http-client-datasource</artifactId>
1818
</dependency>
19+
<dependency>
20+
<groupId>io.jsonwebtoken</groupId>
21+
<artifactId>jjwt-api</artifactId>
22+
<version>0.12.3</version>
23+
</dependency>
24+
<dependency>
25+
<groupId>io.jsonwebtoken</groupId>
26+
<artifactId>jjwt-impl</artifactId>
27+
<version>0.12.3</version>
28+
<scope>runtime</scope>
29+
</dependency>
30+
<dependency>
31+
<groupId>io.jsonwebtoken</groupId>
32+
<artifactId>jjwt-jackson</artifactId>
33+
<version>0.12.3</version>
34+
<scope>runtime</scope>
35+
</dependency>
1936
<dependency>
2037
<groupId>com.expediagroup</groupId>
2138
<artifactId>graphql-kotlin-ktor-client</artifactId>
@@ -30,6 +47,11 @@
3047
<groupId>com.expediagroup</groupId>
3148
<artifactId>graphql-kotlin-client-jackson</artifactId>
3249
</dependency>
50+
<dependency>
51+
<groupId>org.kohsuke</groupId>
52+
<artifactId>github-api</artifactId>
53+
<version>1.317</version>
54+
</dependency>
3355
<dependency>
3456
<groupId>net.adoptium.api</groupId>
3557
<artifactId>adoptium-api-v3-persistence</artifactId>

adoptium-updater-parent/adoptium-datasources-parent/adoptium-github-datasource/src/main/kotlin/net/adoptium/api/v3/dataSources/github/GitHubAuth.kt

+76-5
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,61 @@
11
package net.adoptium.api.v3.dataSources.github
22

3-
import org.slf4j.LoggerFactory
43
import java.io.File
54
import java.nio.file.Files
65
import java.util.Properties
6+
import org.slf4j.LoggerFactory
7+
import io.jsonwebtoken.Jwts
8+
import io.jsonwebtoken.SignatureAlgorithm
9+
import org.kohsuke.github.GHAppInstallation
10+
import org.kohsuke.github.GHAppInstallationToken
11+
import java.security.KeyFactory
12+
import java.security.spec.PKCS8EncodedKeySpec
13+
import java.util.Base64
14+
import java.util.Date
15+
import org.kohsuke.github.GitHub
16+
import org.kohsuke.github.GitHubBuilder
717

818
class GitHubAuth {
19+
data class AuthInfo(val token: String?, val type: AuthType)
20+
enum class AuthType {
21+
APP, TOKEN, NONE
22+
}
923

1024
companion object {
1125
@JvmStatic
1226
private val LOGGER = LoggerFactory.getLogger(this::class.java)
1327

14-
fun readToken(): String? {
15-
var token = System.getenv("GITHUB_TOKEN")
16-
if (token.isNullOrEmpty()) {
17-
token = System.getProperty("GITHUB_TOKEN")
28+
fun getAuthenticationToken(): AuthInfo {
29+
val appId = System.getenv("GITHUB_APP_ID")
30+
val privateKey = System.getenv("GITHUB_APP_PRIVATE_KEY")
31+
val installationId = System.getenv("GITHUB_APP_INSTALLATION_ID")
32+
33+
return if (!appId.isNullOrEmpty() && !privateKey.isNullOrEmpty() && !installationId.isNullOrEmpty()) {
34+
LOGGER.info("Using GitHub App for authentication")
35+
val jwtToken = authenticateAsGitHubApp(appId, privateKey)
36+
if (jwtToken != null) {
37+
val gitHubApp: GitHub = GitHubBuilder().withJwtToken(jwtToken).build()
38+
val appInstallation: GHAppInstallation = gitHubApp.getApp().getInstallationById(installationId.toLong())
39+
val appInstallationToken: GHAppInstallationToken = appInstallation.createToken().create()
40+
LOGGER.info("Using token: ${appInstallationToken.token}")
41+
AuthInfo(appInstallationToken.token, AuthType.APP)
42+
} else {
43+
LOGGER.error("Failed to authenticate as GitHub App")
44+
AuthInfo(null, AuthType.NONE)
45+
}
46+
} else {
47+
val token = readToken()
48+
if (token != null) {
49+
LOGGER.info("Using Personal Access Token for authentication")
50+
AuthInfo(token, AuthType.TOKEN)
51+
} else {
52+
AuthInfo(null, AuthType.NONE)
53+
}
1854
}
55+
}
1956

57+
fun readToken(): String? {
58+
var token = System.getenv("GITHUB_TOKEN")
2059
if (token.isNullOrEmpty()) {
2160

2261
val userHome = System.getProperty("user.home")
@@ -36,5 +75,37 @@ class GitHubAuth {
3675
}
3776
return token
3877
}
78+
79+
fun authenticateAsGitHubApp(appId: String, privateKey: String): String? {
80+
try {
81+
// Remove the first and last lines
82+
val sanitizedKey = privateKey
83+
.replace("-----BEGIN PRIVATE KEY-----", "")
84+
.replace("-----END PRIVATE KEY-----", "")
85+
.replace("\\s".toRegex(), "")
86+
87+
// Decode the Base64 encoded key
88+
val keyBytes = Base64.getDecoder().decode(sanitizedKey)
89+
90+
// Generate the private key
91+
val keySpec = PKCS8EncodedKeySpec(keyBytes)
92+
val keyFactory = KeyFactory.getInstance("RSA")
93+
val privateKey = keyFactory.generatePrivate(keySpec)
94+
95+
// Create and sign the JWT
96+
val nowMillis = System.currentTimeMillis()
97+
val jwt = Jwts.builder()
98+
.setIssuer(appId)
99+
.setIssuedAt(Date(nowMillis))
100+
.setExpiration(Date(nowMillis + 60000)) // Token valid for 1 minute
101+
.signWith(privateKey, SignatureAlgorithm.RS256)
102+
.compact()
103+
104+
return jwt
105+
} catch (e: Exception) {
106+
LOGGER.error("Error authenticating as GitHub App", e)
107+
return null
108+
}
109+
}
39110
}
40111
}

adoptium-updater-parent/adoptium-datasources-parent/adoptium-github-datasource/src/main/kotlin/net/adoptium/api/v3/dataSources/github/graphql/clients/GraphQLRequestImpl.kt

+11-6
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import io.ktor.client.*
88
import jakarta.enterprise.context.ApplicationScoped
99
import net.adoptium.api.v3.dataSources.UpdaterJsonMapper
1010
import net.adoptium.api.v3.dataSources.github.GitHubAuth
11+
import net.adoptium.api.v3.dataSources.github.GitHubAuth.AuthType
12+
import net.adoptium.api.v3.dataSources.github.GitHubAuth.AuthInfo
1113
import java.net.URL
1214

1315
@ApplicationScoped
@@ -16,14 +18,12 @@ open class GraphQLRequestImpl : GraphQLRequest {
1618
private val client: GraphQLKtorClient
1719
private val httpClient: HttpClient
1820
val BASE_URL = "https://api.github.com/graphql"
19-
private val TOKEN: String
21+
private val AUTH_INFO: AuthInfo
2022

2123
init {
22-
val token = GitHubAuth.readToken()
23-
if (token == null) {
24+
AUTH_INFO = GitHubAuth.getAuthenticationToken()
25+
if (AUTH_INFO.token == null) {
2426
throw IllegalStateException("No token provided")
25-
} else {
26-
TOKEN = token
2727
}
2828
httpClient = HttpClient()
2929
client = GraphQLKtorClient(
@@ -35,7 +35,12 @@ open class GraphQLRequestImpl : GraphQLRequest {
3535

3636
override suspend fun <T : Any> request(query: GraphQLClientRequest<T>): GraphQLClientResponse<T> {
3737
return client.execute(query) {
38-
headers.append("Authorization", "Bearer $TOKEN")
38+
val authHeader = when (AUTH_INFO.type) {
39+
AuthType.APP -> "Bearer ${AUTH_INFO.token}"
40+
AuthType.TOKEN -> "token ${AUTH_INFO.token}"
41+
else -> throw IllegalStateException("Invalid authentication type")
42+
}
43+
headers.append("Authorization", authHeader)
3944
}
4045
}
4146
}

adoptium-updater-parent/adoptium-datasources-parent/adoptium-http-client-datasource/pom.xml

+22
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,28 @@
4848
<groupId>org.slf4j</groupId>
4949
<artifactId>slf4j-api</artifactId>
5050
</dependency>
51+
<dependency>
52+
<groupId>io.jsonwebtoken</groupId>
53+
<artifactId>jjwt-api</artifactId>
54+
<version>0.12.3</version>
55+
</dependency>
56+
<dependency>
57+
<groupId>io.jsonwebtoken</groupId>
58+
<artifactId>jjwt-impl</artifactId>
59+
<version>0.12.3</version>
60+
<scope>runtime</scope>
61+
</dependency>
62+
<dependency>
63+
<groupId>io.jsonwebtoken</groupId>
64+
<artifactId>jjwt-jackson</artifactId>
65+
<version>0.12.3</version>
66+
<scope>runtime</scope>
67+
</dependency>
68+
<dependency>
69+
<groupId>org.kohsuke</groupId>
70+
<artifactId>github-api</artifactId>
71+
<version>1.317</version>
72+
</dependency>
5173
</dependencies>
5274

5375
<build>

adoptium-updater-parent/adoptium-datasources-parent/adoptium-http-client-datasource/src/main/kotlin/net/adoptium/api/v3/dataSources/DefaultUpdaterHtmlClient.kt

+10-3
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import kotlin.coroutines.Continuation
1919
import kotlin.coroutines.resume
2020
import kotlin.coroutines.resumeWithException
2121
import kotlin.coroutines.suspendCoroutine
22+
import net.adoptium.api.v3.dataSources.GitHubAuth.AuthInfo
23+
import net.adoptium.api.v3.dataSources.GitHubAuth.AuthType
2224

2325
@Default
2426
@ApplicationScoped
@@ -33,7 +35,7 @@ open class DefaultUpdaterHtmlClient @Inject constructor(
3335
companion object {
3436
@JvmStatic
3537
private val LOGGER = LoggerFactory.getLogger(this::class.java)
36-
private val TOKEN: String? = GitHubAuth.readToken()
38+
private val AUTH_INFO: AuthInfo = GitHubAuth.getAuthenticationToken()
3739
private const val REQUEST_TIMEOUT = 12_000L
3840
private val GITHUB_DOMAINS = listOf("api.github.com", "github.com")
3941

@@ -112,8 +114,13 @@ open class DefaultUpdaterHtmlClient @Inject constructor(
112114
request.addHeader("If-Modified-Since", urlRequest.lastModified)
113115
}
114116

115-
if (GITHUB_DOMAINS.contains(url.host) && TOKEN != null) {
116-
request.setHeader("Authorization", "token $TOKEN")
117+
if (GITHUB_DOMAINS.contains(url.host) && AUTH_INFO.token != null) {
118+
val authHeader = when (AUTH_INFO.type) {
119+
AuthType.APP -> "Bearer ${AUTH_INFO.token}"
120+
AuthType.TOKEN -> "token ${AUTH_INFO.token}"
121+
else -> ""
122+
}
123+
request.setHeader("Authorization", authHeader)
117124
}
118125

119126
val client =

adoptium-updater-parent/adoptium-datasources-parent/adoptium-http-client-datasource/src/main/kotlin/net/adoptium/api/v3/dataSources/GitHubAuth.kt

+74-5
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,59 @@
11
package net.adoptium.api.v3.dataSources
22

3+
import io.jsonwebtoken.Jwts
4+
import io.jsonwebtoken.SignatureAlgorithm
5+
import org.kohsuke.github.GHAppInstallation
6+
import org.kohsuke.github.GHAppInstallationToken
7+
import org.kohsuke.github.GitHub
8+
import org.kohsuke.github.GitHubBuilder
39
import org.slf4j.LoggerFactory
410
import java.io.File
511
import java.nio.file.Files
6-
import java.util.Properties
12+
import java.security.KeyFactory
13+
import java.security.spec.PKCS8EncodedKeySpec
14+
import java.util.*
715

816
class GitHubAuth {
17+
data class AuthInfo(val token: String?, val type: AuthType)
18+
enum class AuthType {
19+
APP, TOKEN, NONE
20+
}
921

1022
companion object {
1123
@JvmStatic
1224
private val LOGGER = LoggerFactory.getLogger(this::class.java)
1325

14-
fun readToken(): String? {
15-
var token = System.getenv("GITHUB_TOKEN")
16-
if (token.isNullOrEmpty()) {
17-
token = System.getProperty("GITHUB_TOKEN")
26+
fun getAuthenticationToken(): AuthInfo {
27+
val appId = System.getenv("GITHUB_APP_ID")
28+
val privateKey = System.getenv("GITHUB_APP_PRIVATE_KEY")
29+
val installationId = System.getenv("GITHUB_APP_INSTALLATION_ID")
30+
31+
return if (!appId.isNullOrEmpty() && !privateKey.isNullOrEmpty() && !installationId.isNullOrEmpty()) {
32+
LOGGER.info("Using GitHub App for authentication")
33+
val jwtToken = authenticateAsGitHubApp(appId, privateKey)
34+
if (jwtToken != null) {
35+
val gitHubApp: GitHub = GitHubBuilder().withJwtToken(jwtToken).build()
36+
val appInstallation: GHAppInstallation = gitHubApp.getApp().getInstallationById(installationId.toLong())
37+
val appInstallationToken: GHAppInstallationToken = appInstallation.createToken().create()
38+
LOGGER.info("Using token: ${appInstallationToken.token}")
39+
AuthInfo(appInstallationToken.token, AuthType.APP)
40+
} else {
41+
LOGGER.error("Failed to authenticate as GitHub App")
42+
AuthInfo(null, AuthType.NONE)
43+
}
44+
} else {
45+
val token = readToken()
46+
if (token != null) {
47+
LOGGER.info("Using Personal Access Token for authentication")
48+
AuthInfo(token, AuthType.TOKEN)
49+
} else {
50+
AuthInfo(null, AuthType.NONE)
51+
}
1852
}
53+
}
1954

55+
fun readToken(): String? {
56+
var token = System.getenv("GITHUB_TOKEN")
2057
if (token.isNullOrEmpty()) {
2158

2259
val userHome = System.getProperty("user.home")
@@ -36,5 +73,37 @@ class GitHubAuth {
3673
}
3774
return token
3875
}
76+
77+
fun authenticateAsGitHubApp(appId: String, privateKey: String): String? {
78+
try {
79+
// Remove the first and last lines
80+
val sanitizedKey = privateKey
81+
.replace("-----BEGIN PRIVATE KEY-----", "")
82+
.replace("-----END PRIVATE KEY-----", "")
83+
.replace("\\s".toRegex(), "")
84+
85+
// Decode the Base64 encoded key
86+
val keyBytes = Base64.getDecoder().decode(sanitizedKey)
87+
88+
// Generate the private key
89+
val keySpec = PKCS8EncodedKeySpec(keyBytes)
90+
val keyFactory = KeyFactory.getInstance("RSA")
91+
val privateKey = keyFactory.generatePrivate(keySpec)
92+
93+
// Create and sign the JWT
94+
val nowMillis = System.currentTimeMillis()
95+
val jwt = Jwts.builder()
96+
.setIssuer(appId)
97+
.setIssuedAt(Date(nowMillis))
98+
.setExpiration(Date(nowMillis + 60000)) // Token valid for 1 minute
99+
.signWith(privateKey, SignatureAlgorithm.RS256)
100+
.compact()
101+
102+
return jwt
103+
} catch (e: Exception) {
104+
LOGGER.error("Error authenticating as GitHub App", e)
105+
return null
106+
}
107+
}
39108
}
40109
}

0 commit comments

Comments
 (0)