fix: address OAuth review findings (redirect_uri, binding, timeout, CRLF, DRY)

This commit is contained in:
2026-07-08 17:46:40 +02:00
parent 5dc206ae72
commit 00e7a7456f
4 changed files with 97 additions and 69 deletions
@@ -1,6 +1,9 @@
package ch.parano.myice.auth
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withTimeout
import platform.AuthenticationServices.ASWebAuthenticationPresentationContextProvidingProtocol
import platform.AuthenticationServices.ASWebAuthenticationSession
import platform.Foundation.NSError
@@ -10,53 +13,54 @@ import platform.UIKit.UIWindow
import platform.UIKit.UIWindowScene
import platform.darwin.NSObject
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
@OptIn(ExperimentalForeignApi::class)
class IosOAuthClient : OAuthClient {
override fun redirectUri(): String = "myice://callback"
override suspend fun authenticate(loginUrl: String): String? = suspendCoroutine { continuation ->
val nsUrl = NSURL.URLWithString(loginUrl) ?: run {
continuation.resume(null)
return@suspendCoroutine
}
val session = ASWebAuthenticationSession(
nsUrl,
"myice",
completionHandler = { callbackURL: NSURL?, error: NSError? ->
if (error != null || callbackURL == null) {
override suspend fun authenticate(loginUrl: String): String? = try {
withTimeout(TIMEOUT_MS) {
suspendCancellableCoroutine { continuation ->
val nsUrl = NSURL.URLWithString(loginUrl) ?: run {
continuation.resume(null)
} else {
val urlStr = callbackURL.absoluteString ?: ""
val fragmentIndex = urlStr.indexOf("#")
if (fragmentIndex < 0) {
continuation.resume(null)
} else {
val fragment = urlStr.substring(fragmentIndex + 1)
val params = fragment.split("&").associate { pair ->
val idx = pair.indexOf("=")
if (idx >= 0) pair.substring(0, idx) to pair.substring(idx + 1)
else pair to ""
return@suspendCancellableCoroutine
}
val session = ASWebAuthenticationSession(
nsUrl,
"myice",
completionHandler = { callbackURL: NSURL?, error: NSError? ->
if (error != null || callbackURL == null) {
continuation.resume(null)
} else {
val urlStr = callbackURL.absoluteString ?: ""
val fragmentIndex = urlStr.indexOf("#")
if (fragmentIndex < 0) {
continuation.resume(null)
} else {
val fragment = urlStr.substring(fragmentIndex + 1)
continuation.resume(parseAccessTokenFromFragment(fragment))
}
}
continuation.resume(params["access_token"])
}
)
session.presentationContextProvider = object : NSObject(),
ASWebAuthenticationPresentationContextProvidingProtocol {
override fun presentationAnchorForWebAuthenticationSession(
session: ASWebAuthenticationSession
): UIWindow {
return keyWindow()
}
}
}
)
session.presentationContextProvider = object : NSObject(),
ASWebAuthenticationPresentationContextProvidingProtocol {
override fun presentationAnchorForWebAuthenticationSession(
session: ASWebAuthenticationSession
): UIWindow {
return keyWindow()
continuation.invokeOnCancellation { session.cancel() }
session.start()
}
}
session.start()
} catch (e: TimeoutCancellationException) {
null
}
private fun keyWindow(): UIWindow {
@@ -64,4 +68,8 @@ class IosOAuthClient : OAuthClient {
val windowScene = scenes.firstOrNull { it is UIWindowScene } as? UIWindowScene
return (windowScene?.windows?.firstOrNull() as? UIWindow) ?: UIWindow()
}
companion object {
private const val TIMEOUT_MS = 5L * 60 * 1000
}
}