Skip to content

Commit 5669868

Browse files
MitchellGaledrstrangelooker
authored andcommitted
feat(Kotlin SDK): Iap kotlin sdk support (#1684)
This PR adds support for routing API requests through Identity-Aware Proxy. This is done by putting an OIDC token in the header as `Proxy-Authorization: Bearer <token>`. The OIDC is generated without IamCredentialsClient library To use, users need to provide their `iap_client_id` and `iap_service_account_email` to the SDK's configuration payload. ``` Map<String, String> lookerConfig = new HashMap<>(); lookerConfig.put("base_url", "<Base_Url>"); lookerConfig.put("kotlin_http_transport", "JAVA_NET"); lookerConfig.put("client_id", "<Client_ID>"); lookerConfig.put("client_secret", "<Client_Secret>"); lookerConfig.put("iap_client_id", "<IAP_Client_ID>"); lookerConfig.put("iap_service_account_email", "<IAP_Service_Account_Email>); ConfigurationProvider settings = ApiSettings.fromMap(lookerConfig); Transport transport = new Transport(settings); AuthSession session = new AuthSession(settings, transport); LookerSDK sdk = new LookerSDK(session); ``` `iap_client_id` is the OAuth client ID that was set-up when configuring the the identity-aware proxy. `iap_service_account_email` is the service account that is authorized to bypass IAP. --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Release-As: 26.6.3
1 parent ccd21d1 commit 5669868

2 files changed

Lines changed: 59 additions & 26 deletions

File tree

kotlin/build.gradle.kts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,17 +36,14 @@ dependencies {
3636
implementation("org.ini4j:ini4j:0.5.4")
3737

3838
implementation("commons-configuration:commons-configuration:1.10")
39-
39+
implementation("com.google.auth:google-auth-library-oauth2-http:1.23.0")
4040
implementation(platform("com.google.http-client:google-http-client-bom:$googleHttpVersion"))
4141
implementation("com.google.http-client:google-http-client")
4242
implementation("com.google.http-client:google-http-client-apache-v2")
4343
implementation("com.google.http-client:google-http-client-gson")
4444

45-
implementation(platform("com.google.cloud:libraries-bom:26.54.0"))
46-
implementation("com.google.cloud:google-cloud-iamcredentials")
47-
4845
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")
49-
implementation("com.google.code.gson:gson:2.8.5")
46+
implementation("com.google.code.gson:gson:2.8.9")
5047

5148
testImplementation("org.junit.jupiter:junit-jupiter-api:5.3.1")
5249
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.3.1")

kotlin/src/main/com/looker/rtl/AuthSession.kt

Lines changed: 57 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,13 @@
2525
package com.looker.rtl
2626

2727
import com.google.api.client.http.UrlEncodedContent
28-
import com.google.cloud.iam.credentials.v1.GenerateIdTokenRequest
29-
import com.google.cloud.iam.credentials.v1.IamCredentialsClient
30-
import com.google.cloud.iam.credentials.v1.IamCredentialsSettings
31-
import com.google.cloud.iam.credentials.v1.ServiceAccountName
28+
import com.google.auth.oauth2.GoogleCredentials
29+
import com.google.gson.JsonParser
30+
import java.net.URI
31+
import java.net.http.HttpClient
32+
import java.net.http.HttpRequest
33+
import java.net.http.HttpResponse
34+
import java.time.Duration
3235
import java.time.LocalDateTime
3336

3437
open class AuthSession(
@@ -45,6 +48,7 @@ open class AuthSession(
4548

4649
private var cachedIapToken: String? = null
4750
private var iapTokenExpiration: LocalDateTime? = null
51+
private var isIapConfigured: Boolean? = null
4852

4953
/** Abstraction of AuthToken retrieval to support sudo mode */
5054
fun activeToken(): AuthToken {
@@ -85,9 +89,21 @@ open class AuthSession(
8589
return init.copy(headers = headers)
8690
}
8791

92+
private val httpClient: HttpClient = HttpClient.newBuilder()
93+
.connectTimeout(Duration.ofSeconds(5))
94+
.build()
95+
96+
private val googleCreds by lazy {
97+
GoogleCredentials.getApplicationDefault()
98+
.createScoped(listOf("https://www.googleapis.com/auth/cloud-platform"))
99+
}
100+
101+
@Synchronized
88102
fun fetchIapToken(): String? {
103+
if (isIapConfigured == false) return null
104+
89105
if (cachedIapToken != null && iapTokenExpiration != null) {
90-
if (LocalDateTime.now().isBefore(iapTokenExpiration)) {
106+
if (LocalDateTime.now().isBefore(iapTokenExpiration!!.minusMinutes(5))) {
91107
return cachedIapToken
92108
}
93109
}
@@ -96,31 +112,51 @@ open class AuthSession(
96112
val audience = config["iap_client_id"]
97113
val serviceAccount = config["iap_service_account_email"]
98114

99-
if (audience.isNullOrBlank() || serviceAccount.isNullOrBlank()) return null
115+
if (audience.isNullOrBlank() || serviceAccount.isNullOrBlank()) {
116+
isIapConfigured = false
117+
return null
118+
}
119+
120+
isIapConfigured = true
100121

101122
return try {
102-
val settings = IamCredentialsSettings.newBuilder()
103-
.setTransportChannelProvider(
104-
IamCredentialsSettings.defaultHttpJsonTransportProviderBuilder().build(),
105-
)
123+
googleCreds.refreshIfExpired()
124+
val accessToken = googleCreds.accessToken?.tokenValue ?: throw RuntimeException("Failed to obtain Google access token")
125+
126+
val encodedServiceAccount = java.net.URLEncoder.encode(serviceAccount, java.nio.charset.StandardCharsets.UTF_8)
127+
val apiUrl = "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/$encodedServiceAccount:generateIdToken"
128+
129+
val jsonBody = com.google.gson.JsonObject().apply {
130+
addProperty("audience", audience)
131+
addProperty("includeEmail", true)
132+
}.toString()
133+
134+
val iapRequest = HttpRequest.newBuilder()
135+
.uri(URI.create(apiUrl))
136+
.header("Authorization", "Bearer $accessToken")
137+
.header("Content-Type", "application/json")
138+
.timeout(Duration.ofSeconds(5))
139+
.POST(HttpRequest.BodyPublishers.ofString(jsonBody))
106140
.build()
107141

108-
IamCredentialsClient.create(settings).use { client ->
109-
val request = GenerateIdTokenRequest.newBuilder()
110-
.setName(ServiceAccountName.of("-", serviceAccount).toString())
111-
.setAudience(audience)
112-
.setIncludeEmail(true)
113-
.build()
114-
val token = client.generateIdToken(request).token
115-
cachedIapToken = token
116-
iapTokenExpiration = LocalDateTime.now().plusMinutes(IAP_TOKEN_CACHE_MINUTES)
117-
token
142+
val iapResponse = httpClient.send(iapRequest, HttpResponse.BodyHandlers.ofString())
143+
144+
if (iapResponse.statusCode() != 200) {
145+
throw RuntimeException("IAM API Error: ${iapResponse.statusCode()} - ${iapResponse.body()}")
118146
}
147+
148+
val iapJsonObject = JsonParser.parseString(iapResponse.body()).asJsonObject
149+
val token = iapJsonObject.get("token")?.asString
150+
?: throw RuntimeException("Could not find token in IAM JSON response")
151+
152+
cachedIapToken = token
153+
iapTokenExpiration = LocalDateTime.now().plusMinutes(IAP_TOKEN_CACHE_MINUTES)
154+
token
119155
} catch (e: Exception) {
120156
cachedIapToken = null
121157
iapTokenExpiration = null
122158
throw RuntimeException(
123-
"OIDC Token failed for IAP. Please check your IAP Client ID and IAP Service Account Email. Underlying Google Cloud error: ${e.message}",
159+
"OIDC Token failed for IAP. Ensure your Google credentials are authenticated. Error: ${e.message}",
124160
e,
125161
)
126162
}

0 commit comments

Comments
 (0)