Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,12 @@ build
**/.DS_Store
*.hprof
local.properties
.kotlin
.kotlin

xcuserdata
*.xcodeproj/*
!*.xcodeproj/project.pbxproj
!*.xcodeproj/xcshareddata/
!*.xcodeproj/project.xcworkspace/
!*.xcworkspace/contents.xcworkspacedata
**/xcshareddata/WorkspaceSettings.xcsettings
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ fun KotlinMultiplatformExtension.configureWasmTarget(baseName: String? = null) {
// Serve sources to debug inside browser
add(project.projectDir.path)
add(project.projectDir.path + "/commonMain/")
add(project.projectDir.path + "/wasmJsMain/")
add(project.projectDir.path + "/webMain/")
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we need both webMain and wasmJsMain here?

}
}
}
Expand Down
6 changes: 5 additions & 1 deletion oidc-appsupport/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ multiplatformSwiftPackage {

kotlin {
jvm()
js(IR) {
browser()
binaries.library()
}
Comment on lines +28 to +31
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we have this in a "configureJsTarget" function just like the others?

configureIosTargets(baseName = "OpenIdConnectClient")
configureWasmTarget(baseName = "OpenIdConnectClient")
sourceSets {
Expand Down Expand Up @@ -55,7 +59,7 @@ kotlin {
}
}

val wasmJsMain by getting {
val webMain by getting {
dependencies {
implementation(libs.kotlinx.browser)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package org.publicvalue.multiplatform.oidc.appsupport

import io.ktor.http.Url
import kotlinx.browser.window
import kotlinx.coroutines.suspendCancellableCoroutine
import org.publicvalue.multiplatform.oidc.ExperimentalOpenIdConnect
import org.w3c.dom.MessageEvent
import org.w3c.dom.Window
import org.w3c.dom.events.Event
import kotlin.coroutines.resume

@ExperimentalOpenIdConnect
internal actual class WebPopupFlow actual constructor(
private val windowTarget: String,
private val windowFeatures: String,
private val redirectOrigin: String,
) : WebAuthenticationFlow {

actual override suspend fun startWebFlow(
requestUrl: Url,
redirectUrl: String
): WebAuthenticationFlowResult = suspendCancellableCoroutine { continuation ->
val popup: Window? = window.open(requestUrl.toString(), windowTarget, windowFeatures)
if (popup == null) {
continuation.resume(WebAuthenticationFlowResult.Cancelled)
return@suspendCancellableCoroutine
}

var intervalId: Int? = null
lateinit var messageListener: (Event) -> Unit

messageListener = messageListener@{ event: Event ->
val messageEvent = event as? MessageEvent ?: return@messageListener
if (messageEvent.origin != redirectOrigin) {
return@messageListener
}
if (messageEvent.source != popup) {
return@messageListener
}
val urlString = when (val data = messageEvent.data) {
is String -> data
else -> JSON.stringify(data)
}
window.removeEventListener("message", messageListener)
intervalId?.let { window.clearInterval(it) }
continuation.resume(WebAuthenticationFlowResult.Success(Url(urlString)))
popup.close()
}

window.addEventListener("message", messageListener)

intervalId = window.setInterval({
if (popup.closed) {
window.removeEventListener("message", messageListener)
intervalId?.let { window.clearInterval(it) }
if (continuation.isActive) {
continuation.resume(WebAuthenticationFlowResult.Cancelled)
}
}
}, 500)

continuation.invokeOnCancellation {
window.removeEventListener("message", messageListener)
intervalId.let { window.clearInterval(it) }
if (!popup.closed) {
popup.close()
}
}
}

actual companion object {
@ExperimentalOpenIdConnect
actual fun handleRedirect() {
val openerWindow = window.opener as? Window ?: return
val targetOrigin = openerWindow.location.origin
openerWindow.postMessage(window.location.toString(), targetOrigin)
window.close()
}
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
@file:OptIn(kotlin.js.ExperimentalWasmJsInterop::class)

package org.publicvalue.multiplatform.oidc.appsupport

import io.ktor.http.*
Expand All @@ -12,56 +14,56 @@ import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine

@ExperimentalOpenIdConnect
internal class WebPopupFlow(
private val windowTarget: String = "",
private val windowFeatures: String = "width=1000,height=800,resizable=yes,scrollbars=yes",
internal actual class WebPopupFlow actual constructor(
private val windowTarget: String,
private val windowFeatures: String,
private val redirectOrigin: String,
): WebAuthenticationFlow {
) : WebAuthenticationFlow {

private class WindowHolder(var window: Window?)

override suspend fun startWebFlow(requestUrl: Url, redirectUrl: String): WebAuthenticationFlowResult {
return suspendCoroutine<WebAuthenticationFlowResult> { continuation ->
actual override suspend fun startWebFlow(
requestUrl: Url,
redirectUrl: String
): WebAuthenticationFlowResult = suspendCoroutine { continuation ->

val popupHolder = WindowHolder(null)
lateinit var messageHandler: (Event) -> Unit
val popupHolder = WindowHolder(null)
lateinit var messageHandler: (Event) -> Unit

messageHandler = { event ->
if (event is MessageEvent) {
messageHandler = { event ->
if (event is MessageEvent) {

if (event.origin != redirectOrigin) {
throw TechnicalFailure("Security issue. Event was not from $redirectOrigin", null)
}
if (event.origin != redirectOrigin) {
throw TechnicalFailure("Security issue. Event was not from $redirectOrigin", null)
}

if (event.source == popupHolder.window) {
val urlString: String = Json.decodeFromString(getEventData(event))
val url = Url(urlString)
window.removeEventListener("message", messageHandler)
continuation.resume(WebAuthenticationFlowResult.Success(url))
} else {
// Log an advisory but stay registered for the true callback
println("${WebPopupFlow::class.simpleName} skipping message from unknown source: ${event.source}")
}
if (event.source == popupHolder.window) {
val urlString: String = Json.decodeFromString(getEventData(event))
val url = Url(urlString)
window.removeEventListener("message", messageHandler)
continuation.resume(WebAuthenticationFlowResult.Success(url))
} else {
// Log an advisory but stay registered for the true callback
println("${WebPopupFlow::class.simpleName} skipping message from unknown source: ${event.source}")
}
}
}

window.addEventListener("message", messageHandler)
window.addEventListener("message", messageHandler)

popupHolder.window = window.open(requestUrl.toString(), windowTarget, windowFeatures)
?: throw TechnicalFailure("Could not open popup", null)
}
popupHolder.window = window.open(requestUrl.toString(), windowTarget, windowFeatures)
?: throw TechnicalFailure("Could not open popup", null)
}

internal companion object {
actual companion object {
@ExperimentalOpenIdConnect
fun handleRedirect() {
actual fun handleRedirect() {
if (window.opener != null) {
postMessage(
url = window.location.toString(),
targetOrigin = getOpenerOrigin()
)

closeWindow(delay = 0)
closeWindow(0)
}
}
}
Expand All @@ -77,4 +79,4 @@ private fun postMessage(url: String, targetOrigin: String) {

private fun closeWindow(delay: Int = 100) {
window.setTimeout(handler = { window.close(); null }, timeout = delay)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,27 +23,31 @@ actual class PlatformCodeAuthFlow(

@ExperimentalOpenIdConnect
actual override suspend fun getAuthorizationCode(request: AuthCodeRequest): AuthCodeResponse {
val result = webFlow.startWebFlow(request.url, request.url.parameters.get("redirect_uri").orEmpty())

return if (result is WebAuthenticationFlowResult.Success) {
when (val error = getErrorResult<AuthCodeResult>(result.responseUri)) {
null -> {
val state = result.responseUri.parameters.get("state")
val code = result.responseUri.parameters.get("code")
Result.success(AuthCodeResult(code, state))
}
else -> {
return error
}
}
} else {
val result = webFlow.startWebFlow(request.url, request.url.parameters["redirect_uri"].orEmpty())

if (result !is WebAuthenticationFlowResult.Success) {
// browser closed, no redirect
Result.failure(OpenIdConnectException.AuthenticationCancelled())
return Result.failure(OpenIdConnectException.AuthenticationCancelled())
}

if (result.responseUri == null) {
return Result.failure(OpenIdConnectException.AuthenticationFailure("No Uri in callback from browser."))
}

return when (val error = getErrorResult<AuthCodeResult>(result.responseUri)) {
null -> {
val state = result.responseUri.parameters["state"]
val code = result.responseUri.parameters["code"]
Result.success(AuthCodeResult(code, state))
}
else -> {
return error
}
Comment on lines +26 to +45
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we keep this in the old-style nested form so it looks just like in iOS/Android/JVM?
I'd like to refactor out this code block anyways, until then i'd like to keep it structurally equal :)

}
}

actual override suspend fun endSession(request: EndSessionRequest): EndSessionResponse {
val redirectUrl = request.url.parameters.get("post_logout_redirect_uri").orEmpty()
val redirectUrl = request.url.parameters["post_logout_redirect_uri"].orEmpty()
webFlow.startWebFlow(request.url, redirectUrl)
return Result.success(Unit)
}
Expand All @@ -54,5 +58,4 @@ actual class PlatformCodeAuthFlow(
WebPopupFlow.handleRedirect()
}
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package org.publicvalue.multiplatform.oidc.appsupport

import kotlinx.browser.window
import org.publicvalue.multiplatform.oidc.ExperimentalOpenIdConnect
import org.publicvalue.multiplatform.oidc.OpenIdConnectClient
import org.publicvalue.multiplatform.oidc.flows.EndSessionFlow

@ExperimentalOpenIdConnect
class WebMainCodeAuthFlowFactory(
private val windowTarget: String = "",
private val windowFeatures: String = "width=1000,height=800,resizable=yes,scrollbars=yes",
private val redirectOrigin: String = window.location.origin,
) : CodeAuthFlowFactory {

override fun createAuthFlow(client: OpenIdConnectClient): PlatformCodeAuthFlow {
return PlatformCodeAuthFlow(
windowTarget = windowTarget,
windowFeatures = windowFeatures,
redirectOrigin = redirectOrigin,
client = client,
)
}

override fun createEndSessionFlow(client: OpenIdConnectClient): EndSessionFlow {
return createAuthFlow(client)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.publicvalue.multiplatform.oidc.appsupport

import io.ktor.http.Url
import org.publicvalue.multiplatform.oidc.ExperimentalOpenIdConnect

@ExperimentalOpenIdConnect
internal expect class WebPopupFlow(
windowTarget: String = "",
windowFeatures: String = "width=1000,height=800,resizable=yes,scrollbars=yes",
redirectOrigin: String,
) : WebAuthenticationFlow {
override suspend fun startWebFlow(
requestUrl: Url,
redirectUrl: String
): WebAuthenticationFlowResult

companion object {
@ExperimentalOpenIdConnect
fun handleRedirect()
}
}
4 changes: 4 additions & 0 deletions oidc-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ description = "Kotlin Multiplatform OIDC core library"

kotlin {
jvm()
js(IR) {
browser()
binaries.library()
}
configureIosTargets()
configureWasmTarget()
sourceSets {
Expand Down
6 changes: 5 additions & 1 deletion oidc-crypto/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ description = "Kotlin Multiplatform OIDC crypto library"

kotlin {
jvm()
js(IR) {
browser()
binaries.library()
}
configureIosTargets()
configureWasmTarget()
sourceSets {
Expand All @@ -34,7 +38,7 @@ kotlin {
}
}

val wasmJsMain by getting {
val webMain by getting {
dependencies {
implementation(project.dependencies.platform(libs.kotlincrypto.hash.bom))
implementation(libs.kotlincrypto.hash.sha2)
Expand Down
4 changes: 4 additions & 0 deletions oidc-ktor/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ description = "Kotlin Multiplatform OIDC support library for ktor clients"

kotlin {
jvm()
js(IR) {
browser()
binaries.library()
}
configureIosTargets()
configureWasmTarget()
sourceSets {
Expand Down
4 changes: 4 additions & 0 deletions oidc-tokenstore/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ description = "Kotlin Multiplatform OIDC tokenstore library"

kotlin {
jvm()
js(IR) {
browser()
binaries.library()
}
configureIosTargets()
configureWasmTarget()
sourceSets {
Expand Down
2 changes: 1 addition & 1 deletion sample-app/settings/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ kotlin {
}
}

wasmJsMain {
webMain {
dependencies {
implementation(libs.kotlinx.browser)
}
Expand Down
Loading
Loading