-
Notifications
You must be signed in to change notification settings - Fork 28
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implementing Google Sign In in Desktop (#59)
* Adding ktor * Google Authentication main logic in desktop
- Loading branch information
1 parent
9951e5d
commit f4d813a
Showing
12 changed files
with
245 additions
and
84 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
23 changes: 23 additions & 0 deletions
23
kmpauth-core/src/commonMain/kotlin/com/mmk/kmpauth/core/HttpClientFactory.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
package com.mmk.kmpauth.core | ||
|
||
import io.ktor.client.HttpClient | ||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation | ||
import io.ktor.client.plugins.defaultRequest | ||
import io.ktor.client.plugins.logging.LogLevel | ||
import io.ktor.client.plugins.logging.Logger | ||
import io.ktor.client.plugins.logging.Logging | ||
import io.ktor.client.request.header | ||
import io.ktor.http.HttpHeaders | ||
import io.ktor.serialization.kotlinx.json.json | ||
import kotlinx.serialization.json.Json | ||
|
||
internal object HttpClientFactory { | ||
internal fun default() = HttpClient { | ||
defaultRequest { | ||
header(HttpHeaders.ContentType, "application/json") | ||
} | ||
install(ContentNegotiation) { | ||
json(Json { ignoreUnknownKeys = true }) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
138 changes: 136 additions & 2 deletions
138
kmpauth-google/src/jvmMain/kotlin/com/mmk/kmpauth/google/GoogleAuthUiProviderImpl.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,142 @@ | ||
package com.mmk.kmpauth.google | ||
|
||
internal class GoogleAuthUiProviderImpl : GoogleAuthUiProvider { | ||
import com.auth0.jwt.JWT | ||
import io.ktor.http.ContentType | ||
import io.ktor.server.engine.embeddedServer | ||
import io.ktor.server.html.respondHtml | ||
import io.ktor.server.netty.Netty | ||
import io.ktor.server.response.respondText | ||
import io.ktor.server.routing.get | ||
import io.ktor.server.routing.routing | ||
import kotlinx.coroutines.CompletableDeferred | ||
import kotlinx.coroutines.Dispatchers | ||
import kotlinx.coroutines.withContext | ||
import kotlinx.html.body | ||
import kotlinx.html.script | ||
import kotlinx.html.unsafe | ||
import java.awt.Desktop | ||
import java.net.URI | ||
import java.net.URLEncoder | ||
import java.nio.charset.StandardCharsets | ||
import java.security.SecureRandom | ||
import java.util.Base64 | ||
|
||
internal class GoogleAuthUiProviderImpl(private val credentials: GoogleAuthCredentials) : | ||
GoogleAuthUiProvider { | ||
|
||
private val authUrl = "https://accounts.google.com/o/oauth2/v2/auth" | ||
|
||
override suspend fun signIn(filterByAuthorizedAccounts: Boolean): GoogleUser? { | ||
TODO("Not yet implemented") | ||
val scope = "email profile" | ||
val redirectUri = "http://localhost:8080/callback" | ||
val state: String | ||
val nonce: String | ||
val googleAuthUrl = withContext(Dispatchers.IO) { | ||
state = URLEncoder.encode(generateRandomString(), StandardCharsets.UTF_8.toString()) | ||
val encodedScope = URLEncoder.encode(scope, StandardCharsets.UTF_8.toString()) | ||
nonce = URLEncoder.encode(generateRandomString(), StandardCharsets.UTF_8.toString()) | ||
|
||
"$authUrl?" + | ||
"client_id=${credentials.serverId}" + | ||
"&redirect_uri=$redirectUri" + | ||
"&response_type=id_token" + | ||
"&scope=$encodedScope" + | ||
"&nonce=$nonce" + | ||
"&state=$state" | ||
} | ||
|
||
|
||
openUrlInBrowser(googleAuthUrl) | ||
|
||
val idToken = startHttpServerAndGetToken(state = state) | ||
if (idToken == null) { | ||
println("GoogleAuthUiProvider: idToken is null") | ||
return null | ||
} | ||
val jwt = JWT().decodeJwt(idToken) | ||
val name = jwt.getClaim("name")?.asString() // User's name | ||
val picture = jwt.getClaim("picture")?.asString() | ||
val receivedNonce = jwt.getClaim("nonce")?.asString() | ||
if (receivedNonce != nonce) { | ||
println("GoogleAuthUiProvider: Invalid nonce state: A login callback was received, but no login request was sent.") | ||
return null | ||
} | ||
|
||
return GoogleUser( | ||
idToken = idToken, | ||
accessToken = null, | ||
displayName = name ?: "", | ||
profilePicUrl = picture | ||
) | ||
} | ||
|
||
private suspend fun startHttpServerAndGetToken( | ||
redirectUriPath: String = "/callback", | ||
state: String | ||
): String? { | ||
val idTokenDeferred = CompletableDeferred<String?>() | ||
|
||
val jsCode = """ | ||
var fragment = window.location.hash; | ||
if (fragment) { | ||
var params = new URLSearchParams(fragment.substring(1)); | ||
var idToken = params.get('id_token'); | ||
var receivedState = params.get('state'); | ||
var expectedState = '${state}'; | ||
if (receivedState === expectedState) { | ||
window.location.href = '$redirectUriPath/token?id_token=' + idToken; | ||
} else { | ||
console.error('State does not match! Possible CSRF attack.'); | ||
window.location.href = '$redirectUriPath/token?id_token=null'; | ||
} | ||
} | ||
""".trimIndent() | ||
|
||
val server = embeddedServer(Netty, port = 8080) { | ||
routing { | ||
get(redirectUriPath) { | ||
call.respondHtml { | ||
body { script { unsafe { +jsCode } } } | ||
} | ||
} | ||
get("$redirectUriPath/token") { | ||
val idToken = call.request.queryParameters["id_token"] | ||
if (idToken.isNullOrEmpty().not()) { | ||
call.respondText( | ||
"Authorization is complete. You can close this window, and return to the application", | ||
contentType = ContentType.Text.Plain | ||
) | ||
idTokenDeferred.complete(idToken) | ||
} else { | ||
call.respondText( | ||
"Authorization failed", | ||
contentType = ContentType.Text.Plain | ||
) | ||
idTokenDeferred.complete(null) | ||
} | ||
} | ||
} | ||
}.start(wait = false) | ||
|
||
val idToken = idTokenDeferred.await() | ||
server.stop(1000, 1000) | ||
return idToken | ||
} | ||
|
||
private fun openUrlInBrowser(url: String) { | ||
if (Desktop.isDesktopSupported()) { | ||
Desktop.getDesktop().browse(URI(url)) | ||
} else { | ||
println("GoogleAuthUiProvider: Desktop is not supported on this platform.") | ||
} | ||
} | ||
|
||
private fun generateRandomString(length: Int = 32): String { | ||
val secureRandom = SecureRandom() | ||
val stateBytes = ByteArray(length) | ||
secureRandom.nextBytes(stateBytes) | ||
return Base64.getUrlEncoder().withoutPadding().encodeToString(stateBytes) | ||
} | ||
|
||
|
||
} |
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.