188 lines
4.8 KiB
Swift
188 lines
4.8 KiB
Swift
|
//
|
||
|
// OAuthAccountAuthorizationOperation.swift
|
||
|
// NetNewsWire
|
||
|
//
|
||
|
// Created by Kiel Gillard on 8/11/19.
|
||
|
// Copyright © 2019 Ranchero Software. All rights reserved.
|
||
|
//
|
||
|
|
||
|
import Foundation
|
||
|
import Account
|
||
|
import AuthenticationServices
|
||
|
|
||
|
protocol OAuthAccountAuthorizationOperationDelegate: class {
|
||
|
func oauthAccountAuthorizationOperation(_ operation: OAuthAccountAuthorizationOperation, didFailWith error: Error)
|
||
|
}
|
||
|
|
||
|
final class OAuthAccountAuthorizationOperation: Operation, ASWebAuthenticationPresentationContextProviding {
|
||
|
|
||
|
weak var presentationAnchor: ASPresentationAnchor?
|
||
|
weak var delegate: OAuthAccountAuthorizationOperationDelegate?
|
||
|
|
||
|
private let accountType: AccountType
|
||
|
private let oauthClient: OAuthAuthorizationClient
|
||
|
private var session: ASWebAuthenticationSession?
|
||
|
|
||
|
init(accountType: AccountType, oauthClient: OAuthAuthorizationClient) {
|
||
|
self.accountType = accountType
|
||
|
self.oauthClient = oauthClient
|
||
|
}
|
||
|
|
||
|
override func main() {
|
||
|
assert(Thread.isMainThread)
|
||
|
assert(presentationAnchor != nil, "\(self) outlived presentation anchor.")
|
||
|
|
||
|
guard !isCancelled else {
|
||
|
didFinish()
|
||
|
return
|
||
|
}
|
||
|
|
||
|
let request = Account.oauthAuthorizationCodeGrantRequest(for: accountType, client: oauthClient)
|
||
|
|
||
|
guard let url = request.url else {
|
||
|
return DispatchQueue.main.async {
|
||
|
self.didEndAuthentication(url: nil, error: URLError(.badURL))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
guard let redirectUri = URL(string: oauthClient.redirectUri), let scheme = redirectUri.scheme else {
|
||
|
assertionFailure("Could not get callback URL scheme from \(oauthClient.redirectUri)")
|
||
|
return DispatchQueue.main.async {
|
||
|
self.didEndAuthentication(url: nil, error: URLError(.badURL))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
let session = ASWebAuthenticationSession(url: url, callbackURLScheme: scheme) { url, error in
|
||
|
DispatchQueue.main.async { [weak self] in
|
||
|
self?.didEndAuthentication(url: url, error: error)
|
||
|
}
|
||
|
}
|
||
|
self.session = session
|
||
|
session.presentationContextProvider = self
|
||
|
|
||
|
session.start()
|
||
|
}
|
||
|
|
||
|
override func cancel() {
|
||
|
session?.cancel()
|
||
|
super.cancel()
|
||
|
}
|
||
|
|
||
|
private func didEndAuthentication(url: URL?, error: Error?) {
|
||
|
guard !isCancelled else {
|
||
|
didFinish()
|
||
|
return
|
||
|
}
|
||
|
|
||
|
do {
|
||
|
guard let url = url else {
|
||
|
if let error = error {
|
||
|
throw error
|
||
|
}
|
||
|
throw URLError(.badURL)
|
||
|
}
|
||
|
|
||
|
let response = try OAuthAuthorizationResponse(url: url, client: oauthClient)
|
||
|
|
||
|
Account.requestOAuthAccessToken(with: response, client: oauthClient, accountType: accountType, completionHandler: didEndRequestingAccessToken(_:))
|
||
|
|
||
|
} catch is ASWebAuthenticationSessionError {
|
||
|
didFinish() // Primarily, cancellation.
|
||
|
|
||
|
} catch {
|
||
|
didFinish(error)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
|
||
|
guard let anchor = presentationAnchor else {
|
||
|
fatalError("\(self) has outlived presentation anchor.")
|
||
|
}
|
||
|
return anchor
|
||
|
}
|
||
|
|
||
|
private func didEndRequestingAccessToken(_ result: Result<OAuthAuthorizationGrant, Error>) {
|
||
|
guard !isCancelled else {
|
||
|
didFinish()
|
||
|
return
|
||
|
}
|
||
|
|
||
|
switch result {
|
||
|
case .success(let tokenResponse):
|
||
|
saveAccount(for: tokenResponse)
|
||
|
case .failure(let error):
|
||
|
didFinish(error)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private func saveAccount(for grant: OAuthAuthorizationGrant) {
|
||
|
// TODO: Find an already existing account for this username?
|
||
|
let account = AccountManager.shared.createAccount(type: .feedly)
|
||
|
do {
|
||
|
|
||
|
// Store the refresh token first because it sends this token to the account delegate.
|
||
|
if let token = grant.refreshToken {
|
||
|
try account.storeCredentials(token)
|
||
|
}
|
||
|
|
||
|
// Now store the access token because we want the account delegate to use it.
|
||
|
try account.storeCredentials(grant.accessToken)
|
||
|
|
||
|
account.oauthAuthorizationClient = oauthClient
|
||
|
|
||
|
didFinish()
|
||
|
} catch {
|
||
|
didFinish(error)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// MARK: Managing Operation State
|
||
|
|
||
|
private func didFinish() {
|
||
|
assert(Thread.isMainThread)
|
||
|
assert(!isFinished, "Finished operation is attempting to finish again.")
|
||
|
self.isExecutingOperation = false
|
||
|
self.isFinishedOperation = true
|
||
|
}
|
||
|
|
||
|
private func didFinish(_ error: Error) {
|
||
|
assert(Thread.isMainThread)
|
||
|
assert(!isFinished, "Finished operation is attempting to finish again.")
|
||
|
delegate?.oauthAccountAuthorizationOperation(self, didFailWith: error)
|
||
|
didFinish()
|
||
|
}
|
||
|
|
||
|
override func start() {
|
||
|
isExecutingOperation = true
|
||
|
DispatchQueue.main.async {
|
||
|
self.main()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
override var isExecuting: Bool {
|
||
|
return isExecutingOperation
|
||
|
}
|
||
|
|
||
|
private var isExecutingOperation = false {
|
||
|
willSet {
|
||
|
willChangeValue(for: \.isExecuting)
|
||
|
}
|
||
|
didSet {
|
||
|
didChangeValue(for: \.isExecuting)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
override var isFinished: Bool {
|
||
|
return isFinishedOperation
|
||
|
}
|
||
|
|
||
|
private var isFinishedOperation = false {
|
||
|
willSet {
|
||
|
willChangeValue(for: \.isFinished)
|
||
|
}
|
||
|
didSet {
|
||
|
didChangeValue(for: \.isFinished)
|
||
|
}
|
||
|
}
|
||
|
}
|