Skip to content

Commit

Permalink
WIP: Fix fetching public profile
Browse files Browse the repository at this point in the history
  • Loading branch information
ILIYANGERMANOV committed Nov 30, 2024
1 parent ae41b85 commit e88f4bd
Show file tree
Hide file tree
Showing 2 changed files with 28 additions and 50 deletions.
2 changes: 1 addition & 1 deletion server/src/main/kotlin/ivy/learn/api/common/ApiUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ suspend inline fun RoutingContext.handleRequest(
handler(call)
}
} catch (e: Throwable) {
Either.Left(ServerError.Unknown("Unexpected error occurred."))
Either.Left(ServerError.Unknown("Unexpected error occurred: $e"))
}
result.onLeft { error ->
respondError(error)
Expand Down
76 changes: 27 additions & 49 deletions server/src/main/kotlin/ivy/learn/domain/auth/GoogleOAuthUseCase.kt
Original file line number Diff line number Diff line change
@@ -1,26 +1,20 @@
package ivy.learn.domain.auth

import arrow.core.Either
import arrow.core.left
import arrow.core.raise.catch
import arrow.core.raise.either
import arrow.core.raise.ensure
import arrow.core.raise.ensureNotNull
import arrow.core.right
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.request.*
import io.ktor.http.*
import ivy.learn.config.ServerConfiguration
import ivy.learn.util.Base64Util
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json

class GoogleOAuthUseCase(
private val config: ServerConfiguration,
private val httpClient: HttpClient,
private val base64: Base64Util,
) {

suspend fun verify(
Expand All @@ -30,7 +24,12 @@ class GoogleOAuthUseCase(
"Google authorization code is blank!"
}
val tokenResponse = exchangeAuthCodeForTokens(authCode).bind()
extractPublicProfile(tokenResponse.idToken).bind()
val userInfoResponse = fetchUserInfo(accessToken = tokenResponse.accessToken).bind()
GooglePublicProfile(
email = userInfoResponse.email,
names = userInfoResponse.name,
profilePictureUrl = userInfoResponse.picture,
)
}

/*
Expand All @@ -56,62 +55,41 @@ class GoogleOAuthUseCase(
)
}
ensure(response.status.isSuccess()) {
"Failed to verify Google authorization code: status - ${response.status}"
"Verify Google authorization code: status - ${response.status}"
}
response.body<GoogleTokenResponse>()
}
}) {
Either.Left("Failed to verify Google authorization code: ${code.value}")
}

private fun extractPublicProfile(idToken: String): Either<String, GooglePublicProfile> = either {
val idTokenPayload = decodeIdTokenPayload(idToken)
.mapLeft { errMsg ->
"Google ID token decode: $errMsg; idToken = $idToken"
private suspend fun fetchUserInfo(
accessToken: String
): Either<String, GoogleUserInfoResponse> = catch({
either {
val response = httpClient.get("https://www.googleapis.com/oauth2/v3/userinfo") {
header(HttpHeaders.Authorization, "Bearer $accessToken")
}
.bind()
val audience = idTokenPayload["aud"]
ensure(audience != config.googleOAuth.clientId) {
"Google ID token is not intended for our client"
}

val email = idTokenPayload["email"]
ensureNotNull(email) {
"Google ID token Email is null"
ensure(response.status.isSuccess()) {
"Fetch Google user info status code: ${response.status}"
}
response.body<GoogleUserInfoResponse>()
}
val name = idTokenPayload["name"]
val picture = idTokenPayload["picture"]


GooglePublicProfile(
email = email,
names = name,
profilePictureUrl = picture
)
}

private fun decodeIdTokenPayload(
idToken: String
): Either<String, Map<String, String>> = either {
val payloadBase64 = catch({
idToken.split(".")[1].right()
}) { e ->
"Split on '.' for \"$idToken\" because $e".left()
}.bind()
val payloadText = base64.decode(payloadBase64).bind()
val payload = catch({
Json.decodeFromString<Map<String, String>>(payloadText).right()
}) { e ->
"JSON decode of '$payloadText' because $e".left()
}.bind()
payload
}) { e ->
Either.Left("Fetch Google user info because $e")
}

@Serializable
data class GoogleUserInfoResponse(
val email: String,
val name: String?,
val picture: String?,
)

@Serializable
data class GoogleTokenResponse(
@SerialName("id_token")
val idToken: String,
@SerialName("access_token")
val accessToken: String,
)
}

Expand Down

0 comments on commit e88f4bd

Please sign in to comment.