Files
Flaro/Flixy/Session/PlexWebAuthenticator.swift
T
GuidoandClaude Opus 5 1756ed11cc Anmeldung über die Plex-Seite statt über einen Code
Kein Code mehr zum Abtippen. Die Anmeldemaske kommt von Plex und öffnet
sich über ASWebAuthenticationSession in einem Fenster innerhalb der App;
nach dem Anmelden schickt Plex über forwardUrl auf ein eigenes Schema
zurück, das Fenster schließt sich von selbst.

Der PIN steckt weiterhin darunter — er ist der einzige Weg, auf dem Plex
einen Token herausgibt. Sichtbar ist er nicht mehr.

Was das bringt:
- Kein Fernseher-Gefühl mehr.
- Zwei-Faktor läuft mit, weil Plex die Seite selbst ausliefert.
- Das Passwort erreicht Flixy nie. Steht auch so unter dem Knopf.
- prefersEphemeralWebBrowserSession = false: wer in Safari schon bei
  Plex angemeldet ist, braucht nur zu bestätigen.

Zwei Details, die sonst still scheitern:
- Die Rückadresse wird strenger kodiert als die übrigen Werte. `:` und
  `/` lässt urlQueryAllowed durch, und für Plex endete der Wert dann
  beim ersten Schrägstrich — der Rücksprung käme nie.
- Das Schema muss in CFBundleURLTypes stehen, sonst kennt iOS es nicht
  und das Fenster bliebe offen stehen.

Als Schema die Bundle-Kennung statt eines kurzen Worts wie „flixy":
ein kurzes könnte eine andere App beanspruchen und die Anmeldung
abfangen.

132 Tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 20:22:52 +02:00

75 lines
3.1 KiB
Swift

import AuthenticationServices
import Foundation
import UIKit
/// Öffnet die Anmeldeseite von Plex in einem Fenster innerhalb der App.
///
/// `ASWebAuthenticationSession` ist dafür der vorgesehene Weg: die Seite kommt
/// von Plex, das Passwort wird dort eingegeben und erreicht die App nie. Nach
/// dem Anmelden schickt Plex den Browser auf eine Adresse mit unserem eigenen
/// Schema, das Fenster schließt sich von selbst, und die App weiß, dass sie
/// den Token abholen kann.
///
/// Ein einfacher `SFSafariViewController` täte es nicht: er meldet nicht, wann
/// die Rückadresse erreicht wurde, und müsste von Hand geschlossen werden.
@MainActor
final class PlexWebAuthenticator: NSObject {
enum Failure: Error, Equatable {
/// Der Nutzer hat das Fenster geschlossen. Kein Fehler im eigentlichen
/// Sinn — die Oberfläche soll darauf kommentarlos zurückgehen.
case cancelled
case presentationFailed(String)
}
/// Führt die Anmeldung durch und kehrt zurück, sobald Plex die App
/// zurückgeschickt hat.
func authenticate(url: URL, callbackScheme: String) async throws(Failure) {
do {
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, any Error>) in
let session = ASWebAuthenticationSession(
url: url,
callbackURLScheme: callbackScheme
) { _, error in
if let error {
continuation.resume(throwing: error)
} else {
continuation.resume()
}
}
session.presentationContextProvider = self
// `false`, damit eine bestehende Plex-Anmeldung in Safari
// mitgenutzt wird: wer dort schon angemeldet ist, braucht nur
// zu bestätigen statt das Passwort erneut einzugeben.
session.prefersEphemeralWebBrowserSession = false
guard session.start() else {
continuation.resume(throwing: Failure.presentationFailed(
String(localized: "signIn.windowFailed")
))
return
}
}
} catch let error as ASWebAuthenticationSessionError where error.code == .canceledLogin {
throw .cancelled
} catch let error as Failure {
throw error
} catch {
throw .presentationFailed(error.localizedDescription)
}
}
}
extension PlexWebAuthenticator: ASWebAuthenticationPresentationContextProviding {
func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
// Das Fenster der App. Über die verbundenen Szenen gesucht, weil eine
// SwiftUI-App keinen festen Verweis darauf hat — und ein frisch
// erzeugtes Fenster erschiene hinter dem sichtbaren.
let scene = UIApplication.shared.connectedScenes
.compactMap { $0 as? UIWindowScene }
.first { $0.activationState == .foregroundActive }
return scene?.keyWindow ?? ASPresentationAnchor()
}
}