Skip to content

Commit

Permalink
Handle https callback url (#524)
Browse files Browse the repository at this point in the history
  • Loading branch information
etoledom authored Oct 24, 2024
1 parent 0cadaec commit c439d43
Show file tree
Hide file tree
Showing 5 changed files with 84 additions and 16 deletions.
57 changes: 50 additions & 7 deletions Sources/GravatarUI/SwiftUI/OAuthSession/OAuthSession.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import AuthenticationServices

public struct OAuthSession: Sendable {
private static let shared = OAuthSession()
static let shared = OAuthSession()
private var emailStorage = SessionEmailStorage()

private let storage: SecureStorage
private let authenticationSession: AuthenticationSession
Expand Down Expand Up @@ -54,25 +55,47 @@ public struct OAuthSession: Sendable {
try? storage.secret(with: email.rawValue)
}

@discardableResult
func retrieveAccessToken(with email: Email) async throws -> KeychainToken {
func retrieveAccessToken(with email: Email) async throws {
guard let secrets = await Configuration.shared.oauthSecrets else {
assertionFailure("Trying to retrieve access token without configuring oauth secrets.")
throw OAuthError.notConfigured
}

await emailStorage.save(email)
do {
let url = try oauthURL(with: email, secrets: secrets)
let callbackURL = try await authenticationSession.authenticate(using: url, callbackURLScheme: secrets.callbackScheme)
let tokenText = try tokenResponse(from: callbackURL).token
_ = await Self.handleCallback(callbackURL)
} catch {
throw OAuthError.from(error: error)
}
}

public static func handleCallback(_ callbackURL: URL) async -> Bool {
guard let email = await shared.emailStorage.restore() else { return false }

do {
let tokenText = try shared.tokenResponse(from: callbackURL).token
guard try await CheckTokenAuthorizationService().isToken(tokenText, authorizedFor: email) else {
throw OAuthError.loggedInWithWrongEmail(email: email.rawValue)
}
let newToken = KeychainToken(token: tokenText)
overrideToken(newToken, for: email)
return newToken
shared.overrideToken(newToken, for: email)
await shared.authenticationSession.cancel()
postNotification(.authorizationFinished)
return true
} catch OAuthError.couldNotParseAccessCode {
return false // The URL was not a Gravatar callback URL with a token.
} catch {
throw OAuthError.from(error: error)
await shared.authenticationSession.cancel()
postNotification(.authorizationError, error: error)
return true
}
}

private static func postNotification(_ name: Notification.Name, error: Error? = nil) {
Task { @MainActor in
NotificationCenter.default.post(name: name, object: error)
}
}

Expand Down Expand Up @@ -216,6 +239,26 @@ extension [URLQueryItem] {

protocol AuthenticationSession: Sendable {
func authenticate(using url: URL, callbackURLScheme: String) async throws -> URL
func cancel() async
}

extension OldAuthenticationSession: AuthenticationSession {}

// Stores the email used for the current OAuth flow
private actor SessionEmailStorage {
var current: Email?

func save(_ email: Email) {
current = email
}

func restore() -> Email? {
let currentEmail = current
return currentEmail
}
}

extension Notification.Name {
static let authorizationFinished = Notification.Name("com.GravatarSDK.AuthorizationFinished")
static let authorizationError = Notification.Name("com.GravatarSDK.AuthorizationFinishedWithError")
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import SwiftUI

private struct OAuthSessionKey: EnvironmentKey {
static let defaultValue: OAuthSession = .init()
static let defaultValue: OAuthSession = .shared
}

extension EnvironmentValues {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ final class WebAuthenticationPresentationContextProvider: NSObject, ASWebAuthent
}
}

struct OldAuthenticationSession: Sendable {
actor OldAuthenticationSession: Sendable {
let context = WebAuthenticationPresentationContextProvider()
var session: ASWebAuthenticationSession?

func authenticate(using url: URL, callbackURLScheme: String) async throws -> URL {
try await withCheckedThrowingContinuation { continuation in
let session = ASWebAuthenticationSession(url: url, callbackURLScheme: callbackURLScheme) { callbackURL, error in
session = ASWebAuthenticationSession(url: url, callbackURLScheme: callbackURLScheme) { callbackURL, error in
if let error {
continuation.resume(throwing: error)
} else if let callbackURL {
Expand All @@ -20,9 +21,16 @@ struct OldAuthenticationSession: Sendable {
}

Task { @MainActor in
session.presentationContextProvider = context
session.start()
await session?.presentationContextProvider = context
await session?.start()
}
}
}

nonisolated
func cancel() {
Task { @MainActor in
await session?.cancel()
}
}
}
23 changes: 19 additions & 4 deletions Sources/GravatarUI/SwiftUI/ProfileEditor/QuickEditor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ struct QuickEditor<ImageEditor: ImageEditorView>: View {
self.avatarUpdatedHandler = avatarUpdatedHandler
}

let authorizationFinishedNotification = NotificationCenter.default.publisher(for: .authorizationFinished)
let authorizationErrorNotification = NotificationCenter.default.publisher(for: .authorizationError)

var body: some View {
NavigationView {
if let token {
Expand All @@ -63,6 +66,12 @@ struct QuickEditor<ImageEditor: ImageEditorView>: View {
noticeView()
.accumulateIntrinsicHeight()
}
}.onReceive(authorizationFinishedNotification) { _ in
onAuthenticationFinished()
}.onReceive(authorizationErrorNotification) { notification in
guard let error = notification.object as? OAuthError else { return }
oauthError = error
onAuthenticationFinished()
}
}

Expand Down Expand Up @@ -131,8 +140,7 @@ struct QuickEditor<ImageEditor: ImageEditorView>: View {
isAuthenticating = true
if !oauthSession.hasValidSession(with: email) {
do {
_ = try await oauthSession.retrieveAccessToken(with: email)
oauthError = nil
try await oauthSession.retrieveAccessToken(with: email)
} catch OAuthError.oauthResponseError(_, let code) where code == .canceledLogin {
// ignore the error if the user has cancelled the operation.
} catch let error as OAuthError {
Expand All @@ -141,9 +149,16 @@ struct QuickEditor<ImageEditor: ImageEditorView>: View {
oauthError = nil
}
}
fetchedToken = oauthSession.sessionToken(with: email)?.token
isAuthenticating = false
onAuthenticationFinished()
}
}

func onAuthenticationFinished() {
if let fetchedToken = oauthSession.sessionToken(with: email)?.token {
self.fetchedToken = fetchedToken
oauthError = nil
}
isAuthenticating = false
}
}

Expand Down
2 changes: 2 additions & 0 deletions Tests/GravatarUITests/OAuthSessionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ final class OAuthSessionTests: XCTestCase {
}

class AuthenticationSessionMock: AuthenticationSession, @unchecked Sendable {
func cancel() async {}

let responseURL: URL

init(responseURL: URL) {
Expand Down

0 comments on commit c439d43

Please sign in to comment.