From 3ab78f1134e06e62ed148f0801ed15cf34065523 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 4 Jun 2021 18:31:57 +0800 Subject: [PATCH] feat: adapt AuthenticationSession for authentication --- Mastodon.xcodeproj/project.pbxproj | 24 +-- Mastodon/Coordinator/SceneCoordinator.swift | 7 +- .../MastodonPickServerViewController.swift | 96 +++++++---- .../MastodonPickServerViewModel.swift | 158 +----------------- ...PinBasedAuthenticationViewController.swift | 73 -------- ...todonPinBasedAuthenticationViewModel.swift | 40 ----- ...ationViewModelNavigationDelegateShim.swift | 41 ----- .../Share/AuthenticationViewModel.swift | 17 +- .../MastodonAuthenticationController.swift | 75 +++++++++ .../Service/APIService/APIService+App.swift | 6 +- .../APIService+Authentication.swift | 6 +- .../MastodonSDK/API/Mastodon+API+App.swift | 2 +- .../MastodonSDK/API/Mastodon+API+OAuth.swift | 4 +- 13 files changed, 168 insertions(+), 381 deletions(-) delete mode 100644 Mastodon/Scene/Onboarding/PinBasedAuthentication/MastodonPinBasedAuthenticationViewController.swift delete mode 100644 Mastodon/Scene/Onboarding/PinBasedAuthentication/MastodonPinBasedAuthenticationViewModel.swift delete mode 100644 Mastodon/Scene/Onboarding/PinBasedAuthentication/MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift create mode 100644 Mastodon/Scene/Onboarding/Share/MastodonAuthenticationController.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 4406f593e..7d63b3036 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -181,11 +181,9 @@ 87FFDA5D898A5C42ADCB35E7 /* Pods_Mastodon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A4ABE34829701A4496C5BB64 /* Pods_Mastodon.framework */; }; B914FC6B0B8AF18573C0B291 /* Pods_NotificationService.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 374AA339A20E0FAC75BCDA6D /* Pods_NotificationService.framework */; }; DB00CA972632DDB600A54956 /* CommonOSLog in Frameworks */ = {isa = PBXBuildFile; productRef = DB00CA962632DDB600A54956 /* CommonOSLog */; }; - DB0140A125C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140A025C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift */; }; - DB0140A825C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140A725C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift */; }; - DB0140AE25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140AD25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift */; }; DB0140BD25C40D7500F9F3CF /* CommonOSLog in Frameworks */ = {isa = PBXBuildFile; productRef = DB0140BC25C40D7500F9F3CF /* CommonOSLog */; }; DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140CE25C42AEE00F9F3CF /* OSLog.swift */; }; + DB029E95266A20430062874E /* MastodonAuthenticationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB029E94266A20430062874E /* MastodonAuthenticationController.swift */; }; DB02CDAB26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02CDAA26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift */; }; DB02CDBF2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02CDBE2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift */; }; DB040ECD26526EA600BEE9D8 /* ComposeCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB040ECC26526EA600BEE9D8 /* ComposeCollectionView.swift */; }; @@ -753,10 +751,8 @@ BB482D32A7B9825BF5327C4F /* Pods-Mastodon-MastodonUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-MastodonUITests.release.xcconfig"; path = "Target Support Files/Pods-Mastodon-MastodonUITests/Pods-Mastodon-MastodonUITests.release.xcconfig"; sourceTree = ""; }; CD92E0F10BDE4FE7C4B999F2 /* Pods_MastodonTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MastodonTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D7D7CF93E262178800077512 /* Pods-Mastodon-AppShared.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-AppShared.debug.xcconfig"; path = "Target Support Files/Pods-Mastodon-AppShared/Pods-Mastodon-AppShared.debug.xcconfig"; sourceTree = ""; }; - DB0140A025C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPinBasedAuthenticationViewController.swift; sourceTree = ""; }; - DB0140A725C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift; sourceTree = ""; }; - DB0140AD25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPinBasedAuthenticationViewModel.swift; sourceTree = ""; }; DB0140CE25C42AEE00F9F3CF /* OSLog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OSLog.swift; sourceTree = ""; }; + DB029E94266A20430062874E /* MastodonAuthenticationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonAuthenticationController.swift; sourceTree = ""; }; DB02CDAA26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadReplyLoaderTableViewCell.swift; sourceTree = ""; }; DB02CDBE2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdaptiveUserInterfaceStyleBarButtonItem.swift; sourceTree = ""; }; DB040ECC26526EA600BEE9D8 /* ComposeCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeCollectionView.swift; sourceTree = ""; }; @@ -1618,7 +1614,6 @@ DB68A03825E900CC00CFDF14 /* Share */, 0FAA0FDD25E0B5700017CCDE /* Welcome */, 0FAA102525E1125D0017CCDE /* PickServer */, - DB0140A625C40C0900F9F3CF /* PinBasedAuthentication */, DBE0821A25CD382900FD6BBD /* Register */, DB72602125E36A2500235243 /* ServerRules */, 2D364F7025E66D5B00204FDC /* ResendEmail */, @@ -1627,16 +1622,6 @@ path = Onboarding; sourceTree = ""; }; - DB0140A625C40C0900F9F3CF /* PinBasedAuthentication */ = { - isa = PBXGroup; - children = ( - DB0140A025C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift */, - DB0140AD25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift */, - DB0140A725C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift */, - ); - path = PinBasedAuthentication; - sourceTree = ""; - }; DB084B5125CBC56300F898ED /* CoreDataStack */ = { isa = PBXGroup; children = ( @@ -1921,6 +1906,7 @@ children = ( 2D82B9FE25E7863200E36F0F /* OnboardingViewControllerAppearance.swift */, DB2F073325E8ECF000957B2D /* AuthenticationViewModel.swift */, + DB029E94266A20430062874E /* MastodonAuthenticationController.swift */, ); path = Share; sourceTree = ""; @@ -3043,6 +3029,7 @@ DB49A62B25FF36C700B98345 /* APIService+CustomEmoji.swift in Sources */, DBCBED1D26132E1A00B49291 /* StatusFetchedResultsController.swift in Sources */, 2D79E701261EA5550011E398 /* APIService+CoreData+Tag.swift in Sources */, + DB029E95266A20430062874E /* MastodonAuthenticationController.swift in Sources */, 5B90C461262599800002E742 /* SettingsLinkTableViewCell.swift in Sources */, DB6180DD263918E30018D199 /* MediaPreviewViewController.swift in Sources */, DBE3CDEC261C6B2900430CC6 /* FavoriteViewController.swift in Sources */, @@ -3149,7 +3136,6 @@ DBB525562611EDCA002F1F29 /* UserTimelineViewModel.swift in Sources */, 2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */, DB221B16260C395900AEFE46 /* CustomEmojiPickerInputViewModel.swift in Sources */, - DB0140A125C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift in Sources */, DB789A1C25F9F76A0071ACA0 /* ComposeStatusContentCollectionViewCell.swift in Sources */, DB1FD43625F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift in Sources */, 2D939AE825EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift in Sources */, @@ -3220,7 +3206,6 @@ 2D4AD89C263165B500613EFC /* SuggestionAccountCollectionViewCell.swift in Sources */, DB447691260B406600B66B82 /* CustomEmojiPickerItemCollectionViewCell.swift in Sources */, DB9282B225F3222800823B15 /* PickServerEmptyStateView.swift in Sources */, - DB0140A825C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift in Sources */, DB1FD45025F26FA1004CFCFC /* MastodonPickServerViewModel+Diffable.swift in Sources */, DB2B3AE925E38850007045F9 /* UIViewPreview.swift in Sources */, DB68046C2636DC9E00430867 /* MastodonNotification.swift in Sources */, @@ -3280,7 +3265,6 @@ DB8190C62601FF0400020C08 /* AttachmentContainerView.swift in Sources */, DB51D173262832380062B7A1 /* BlurHashEncode.swift in Sources */, DB5086AB25CC0BBB00C2C187 /* AvatarConfigurableView.swift in Sources */, - DB0140AE25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift in Sources */, 2D32EAAC25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift in Sources */, DB71FD2C25F86A5100512AE1 /* AvatarStackContainerButton.swift in Sources */, DB87D4512609CF1E00D12C0D /* ComposeStatusPollOptionAppendEntryCollectionViewCell.swift in Sources */, diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift index f9da785ad..11053e660 100644 --- a/Mastodon/Coordinator/SceneCoordinator.swift +++ b/Mastodon/Coordinator/SceneCoordinator.swift @@ -42,7 +42,6 @@ extension SceneCoordinator { // onboarding case welcome case mastodonPickServer(viewMode: MastodonPickServerViewModel) - case mastodonPinBasedAuthentication(viewModel: MastodonPinBasedAuthenticationViewModel) case mastodonRegister(viewModel: MastodonRegisterViewModel) case mastodonServerRules(viewModel: MastodonServerRulesViewModel) case mastodonConfirmEmail(viewModel: MastodonConfirmEmailViewModel) @@ -78,6 +77,7 @@ extension SceneCoordinator { case safari(url: URL) case alertController(alertController: UIAlertController) case activityViewController(activityViewController: UIActivityViewController, sourceView: UIView?, barButtonItem: UIBarButtonItem?) + #if DEBUG case publicTimeline #endif @@ -86,7 +86,6 @@ extension SceneCoordinator { switch self { case .welcome, .mastodonPickServer, - .mastodonPinBasedAuthentication, .mastodonRegister, .mastodonServerRules, .mastodonConfirmEmail, @@ -217,10 +216,6 @@ private extension SceneCoordinator { let _viewController = MastodonPickServerViewController() _viewController.viewModel = viewModel viewController = _viewController - case .mastodonPinBasedAuthentication(let viewModel): - let _viewController = MastodonPinBasedAuthenticationViewController() - _viewController.viewModel = viewModel - viewController = _viewController case .mastodonRegister(let viewModel): let _viewController = MastodonRegisterViewController() _viewController.viewModel = viewModel diff --git a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift index 7c7e7b0e1..2a978c691 100644 --- a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift +++ b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift @@ -9,6 +9,7 @@ import os.log import UIKit import Combine import GameController +import AuthenticationServices final class MastodonPickServerViewController: UIViewController, NeedsDependency { @@ -19,6 +20,11 @@ final class MastodonPickServerViewController: UIViewController, NeedsDependency weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } var viewModel: MastodonPickServerViewModel! + private(set) lazy var authenticationViewModel = AuthenticationViewModel( + context: context, + coordinator: coordinator, + isAuthenticationExist: false + ) private var expandServerDomainSet = Set() @@ -50,6 +56,8 @@ final class MastodonPickServerViewController: UIViewController, NeedsDependency }() var nextStepButtonBottomLayoutConstraint: NSLayoutConstraint! + var mastodonAuthenticationController: MastodonAuthenticationController? + deinit { tableViewObservation = nil os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) @@ -182,23 +190,26 @@ extension MastodonPickServerViewController { .assign(to: \.isEnabled, on: nextStepButton) .store(in: &disposeBag) - viewModel.error - .compactMap { $0 } - .receive(on: DispatchQueue.main) - .sink { [weak self] error in - guard let self = self else { return } - let alertController = UIAlertController(for: error, title: "Error", preferredStyle: .alert) - let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default, handler: nil) - alertController.addAction(okAction) - self.coordinator.present( - scene: .alertController(alertController: alertController), - from: nil, - transition: .alertController(animated: true, completion: nil) - ) - } - .store(in: &disposeBag) + Publishers.Merge( + viewModel.error, + authenticationViewModel.error + ) + .compactMap { $0 } + .receive(on: DispatchQueue.main) + .sink { [weak self] error in + guard let self = self else { return } + let alertController = UIAlertController(for: error, title: "Error", preferredStyle: .alert) + let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default, handler: nil) + alertController.addAction(okAction) + self.coordinator.present( + scene: .alertController(alertController: alertController), + from: nil, + transition: .alertController(animated: true, completion: nil) + ) + } + .store(in: &disposeBag) - viewModel + authenticationViewModel .authenticated .flatMap { [weak self] (domain, user) -> AnyPublisher, Never> in guard let self = self else { return Just(.success(false)).eraseToAnyPublisher() } @@ -217,7 +228,7 @@ extension MastodonPickServerViewController { } .store(in: &disposeBag) - viewModel.isAuthenticating + authenticationViewModel.isAuthenticating .receive(on: DispatchQueue.main) .sink { [weak self] isAuthenticating in guard let self = self else { return } @@ -273,11 +284,15 @@ extension MastodonPickServerViewController { private func doSignIn() { guard let server = viewModel.selectedServer.value else { return } - viewModel.isAuthenticating.send(true) + authenticationViewModel.isAuthenticating.send(true) context.apiService.createApplication(domain: server.domain) - .tryMap { response -> MastodonPickServerViewModel.AuthenticateInfo in + .tryMap { response -> AuthenticationViewModel.AuthenticateInfo in let application = response.value - guard let info = MastodonPickServerViewModel.AuthenticateInfo(domain: server.domain, application: application) else { + guard let info = AuthenticationViewModel.AuthenticateInfo( + domain: server.domain, + application: application, + redirectURI: response.value.redirectURI ?? MastodonAuthenticationController.callbackURL + ) else { throw APIService.APIError.explicit(.badResponse) } return info @@ -285,7 +300,7 @@ extension MastodonPickServerViewController { .receive(on: DispatchQueue.main) .sink { [weak self] completion in guard let self = self else { return } - self.viewModel.isAuthenticating.send(false) + self.authenticationViewModel.isAuthenticating.send(false) switch completion { case .failure(let error): @@ -296,15 +311,19 @@ extension MastodonPickServerViewController { } } receiveValue: { [weak self] info in guard let self = self else { return } - let mastodonPinBasedAuthenticationViewModel = MastodonPinBasedAuthenticationViewModel(authenticateURL: info.authorizeURL) - self.viewModel.authenticate( - info: info, - pinCodePublisher: mastodonPinBasedAuthenticationViewModel.pinCodePublisher + let authenticationController = MastodonAuthenticationController( + context: self.context, + authenticateURL: info.authorizeURL ) - self.viewModel.mastodonPinBasedAuthenticationViewController = self.coordinator.present( - scene: .mastodonPinBasedAuthentication(viewModel: mastodonPinBasedAuthenticationViewModel), - from: nil, - transition: .modal(animated: true, completion: nil) + + self.mastodonAuthenticationController = authenticationController + authenticationController.authenticationSession?.prefersEphemeralWebBrowserSession = true + authenticationController.authenticationSession?.presentationContextProvider = self + authenticationController.authenticationSession?.start() + + self.authenticationViewModel.authenticate( + info: info, + pinCodePublisher: authenticationController.pinCodePublisher ) } .store(in: &disposeBag) @@ -313,7 +332,7 @@ extension MastodonPickServerViewController { private func doSignUp() { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) guard let server = viewModel.selectedServer.value else { return } - viewModel.isAuthenticating.send(true) + authenticationViewModel.isAuthenticating.send(true) context.apiService.instance(domain: server.domain) .compactMap { [weak self] response -> AnyPublisher? in @@ -328,7 +347,10 @@ extension MastodonPickServerViewController { .switchToLatest() .tryMap { response -> MastodonPickServerViewModel.SignUpResponseSecond in let application = response.application.value - guard let authenticateInfo = AuthenticationViewModel.AuthenticateInfo(domain: server.domain, application: application) else { + guard let authenticateInfo = AuthenticationViewModel.AuthenticateInfo( + domain: server.domain, + application: application + ) else { throw APIService.APIError.explicit(.badResponse) } return MastodonPickServerViewModel.SignUpResponseSecond(instance: response.instance, authenticateInfo: authenticateInfo) @@ -340,7 +362,8 @@ extension MastodonPickServerViewController { return self.context.apiService.applicationAccessToken( domain: server.domain, clientID: authenticateInfo.clientID, - clientSecret: authenticateInfo.clientSecret + clientSecret: authenticateInfo.clientSecret, + redirectURI: authenticateInfo.redirectURI ) .map { MastodonPickServerViewModel.SignUpResponseThird(instance: instance, authenticateInfo: authenticateInfo, applicationToken: $0) } .eraseToAnyPublisher() @@ -349,7 +372,7 @@ extension MastodonPickServerViewController { .receive(on: DispatchQueue.main) .sink { [weak self] completion in guard let self = self else { return } - self.viewModel.isAuthenticating.send(false) + self.authenticationViewModel.isAuthenticating.send(false) switch completion { case .failure(let error): @@ -519,3 +542,10 @@ extension MastodonPickServerViewController: PickServerCellDelegate { // MARK: - OnboardingViewControllerAppearance extension MastodonPickServerViewController: OnboardingViewControllerAppearance { } + +// MARK: - ASWebAuthenticationPresentationContextProviding +extension MastodonPickServerViewController: ASWebAuthenticationPresentationContextProviding { + func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { + return view.window! + } +} diff --git a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel.swift b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel.swift index a576632a4..8348f8843 100644 --- a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel.swift +++ b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel.swift @@ -58,15 +58,11 @@ class MastodonPickServerViewModel: NSObject { let filteredIndexedServers = CurrentValueSubject<[Mastodon.Entity.Server], Never>([]) let servers = CurrentValueSubject<[Mastodon.Entity.Server], Error>([]) let selectedServer = CurrentValueSubject(nil) - let error = PassthroughSubject() - let authenticated = PassthroughSubject<(domain: String, account: Mastodon.Entity.Account), Never>() - let isAuthenticating = CurrentValueSubject(false) + let error = CurrentValueSubject(nil) let isLoadingIndexedServers = CurrentValueSubject(false) let emptyStateViewState = CurrentValueSubject(.none) - - var mastodonPinBasedAuthenticationViewController: UIViewController? - + init(context: AppContext, mode: PickServerMode) { self.context = context self.mode = mode @@ -233,156 +229,6 @@ extension MastodonPickServerViewModel { } } } - - -// MARK: - SignIn methods & structs -extension MastodonPickServerViewModel { - enum AuthenticationError: Error, LocalizedError { - case badCredentials - case registrationClosed - - var errorDescription: String? { - switch self { - case .badCredentials: return "Bad Credentials" - case .registrationClosed: return "Registration Closed" - } - } - - var failureReason: String? { - switch self { - case .badCredentials: return "Credentials invalid." - case .registrationClosed: return "Server disallow registration." - } - } - - var helpAnchor: String? { - switch self { - case .badCredentials: return "Please try again." - case .registrationClosed: return "Please try another domain." - } - } - } - - struct AuthenticateInfo { - let domain: String - let clientID: String - let clientSecret: String - let authorizeURL: URL - - init?(domain: String, application: Mastodon.Entity.Application) { - self.domain = domain - guard let clientID = application.clientID, - let clientSecret = application.clientSecret else { return nil } - self.clientID = clientID - self.clientSecret = clientSecret - self.authorizeURL = { - let query = Mastodon.API.OAuth.AuthorizeQuery(clientID: clientID) - let url = Mastodon.API.OAuth.authorizeURL(domain: domain, query: query) - return url - }() - } - } - - func authenticate(info: AuthenticateInfo, pinCodePublisher: PassthroughSubject) { - pinCodePublisher - .handleEvents(receiveOutput: { [weak self] _ in - guard let self = self else { return } -// self.isAuthenticating.value = true - self.mastodonPinBasedAuthenticationViewController?.dismiss(animated: true, completion: nil) - self.mastodonPinBasedAuthenticationViewController = nil - }) - .compactMap { [weak self] code -> AnyPublisher, Error>? in - guard let self = self else { return nil } - return self.context.apiService - .userAccessToken( - domain: info.domain, - clientID: info.clientID, - clientSecret: info.clientSecret, - code: code - ) - .flatMap { response -> AnyPublisher, Error> in - let token = response.value - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: sign in success. Token: %s", ((#file as NSString).lastPathComponent), #line, #function, token.accessToken) - return Self.verifyAndSaveAuthentication( - context: self.context, - info: info, - userToken: token - ) - } - .eraseToAnyPublisher() - } - .switchToLatest() - .receive(on: DispatchQueue.main) - .sink { [weak self] completion in - guard let self = self else { return } - switch completion { - case .failure(let error): - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: swap user access token swap fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) -// self.isAuthenticating.value = false - self.error.send(error) - case .finished: - break - } - } receiveValue: { [weak self] response in - guard let self = self else { return } - let account = response.value - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: user %s sign in success", ((#file as NSString).lastPathComponent), #line, #function, account.username) - - self.authenticated.send((domain: info.domain, account: account)) - } - .store(in: &self.disposeBag) - } - - static func verifyAndSaveAuthentication( - context: AppContext, - info: AuthenticateInfo, - userToken: Mastodon.Entity.Token - ) -> AnyPublisher, Error> { - let authorization = Mastodon.API.OAuth.Authorization(accessToken: userToken.accessToken) - let managedObjectContext = context.backgroundManagedObjectContext - - return context.apiService.accountVerifyCredentials( - domain: info.domain, - authorization: authorization - ) - .flatMap { response -> AnyPublisher, Error> in - let account = response.value - let mastodonUserRequest = MastodonUser.sortedFetchRequest - mastodonUserRequest.predicate = MastodonUser.predicate(domain: info.domain, id: account.id) - mastodonUserRequest.fetchLimit = 1 - guard let mastodonUser = try? managedObjectContext.fetch(mastodonUserRequest).first else { - return Fail(error: AuthenticationError.badCredentials).eraseToAnyPublisher() - } - - let property = MastodonAuthentication.Property( - domain: info.domain, - userID: mastodonUser.id, - username: mastodonUser.username, - appAccessToken: userToken.accessToken, // TODO: swap app token - userAccessToken: userToken.accessToken, - clientID: info.clientID, - clientSecret: info.clientSecret - ) - return managedObjectContext.performChanges { - _ = APIService.CoreData.createOrMergeMastodonAuthentication( - into: managedObjectContext, - for: mastodonUser, - in: info.domain, - property: property, - networkDate: response.networkDate - ) - } - .tryMap { result in - switch result { - case .failure(let error): throw error - case .success: return response - } - } - .eraseToAnyPublisher() - } - .eraseToAnyPublisher() - } -} // MARK: - SignUp methods & structs extension MastodonPickServerViewModel { diff --git a/Mastodon/Scene/Onboarding/PinBasedAuthentication/MastodonPinBasedAuthenticationViewController.swift b/Mastodon/Scene/Onboarding/PinBasedAuthentication/MastodonPinBasedAuthenticationViewController.swift deleted file mode 100644 index d566da4c3..000000000 --- a/Mastodon/Scene/Onboarding/PinBasedAuthentication/MastodonPinBasedAuthenticationViewController.swift +++ /dev/null @@ -1,73 +0,0 @@ -// -// MastodonPinBasedAuthenticationViewController.swift -// Mastodon -// -// Created by MainasuK Cirno on 2021/1/29. -// - -import os.log -import UIKit -import Combine -import WebKit - -final class MastodonPinBasedAuthenticationViewController: UIViewController, NeedsDependency { - - weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } - weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } - - var disposeBag = Set() - var viewModel: MastodonPinBasedAuthenticationViewModel! - - let webView: WKWebView = { - let configuration = WKWebViewConfiguration() - configuration.processPool = WKProcessPool() - let webView = WKWebView(frame: .zero, configuration: configuration) - return webView - }() - - deinit { - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - - // cleanup cookie - let httpCookieStore = webView.configuration.websiteDataStore.httpCookieStore - httpCookieStore.getAllCookies { cookies in - for cookie in cookies { - httpCookieStore.delete(cookie, completionHandler: nil) - } - } - } - -} - -extension MastodonPinBasedAuthenticationViewController { - - override func viewDidLoad() { - super.viewDidLoad() - - title = "Authentication" - navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(MastodonPinBasedAuthenticationViewController.cancelBarButtonItemPressed(_:))) - - webView.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(webView) - NSLayoutConstraint.activate([ - webView.topAnchor.constraint(equalTo: view.topAnchor), - webView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - webView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - webView.bottomAnchor.constraint(equalTo: view.bottomAnchor), - ]) - - let request = URLRequest(url: viewModel.authenticateURL) - webView.navigationDelegate = viewModel.navigationDelegate - webView.load(request) - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: authenticate via: %s", ((#file as NSString).lastPathComponent), #line, #function, viewModel.authenticateURL.debugDescription) - } - -} - -extension MastodonPinBasedAuthenticationViewController { - - @objc private func cancelBarButtonItemPressed(_ sender: UIBarButtonItem) { - dismiss(animated: true, completion: nil) - } - -} diff --git a/Mastodon/Scene/Onboarding/PinBasedAuthentication/MastodonPinBasedAuthenticationViewModel.swift b/Mastodon/Scene/Onboarding/PinBasedAuthentication/MastodonPinBasedAuthenticationViewModel.swift deleted file mode 100644 index 5eac359eb..000000000 --- a/Mastodon/Scene/Onboarding/PinBasedAuthentication/MastodonPinBasedAuthenticationViewModel.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// MastodonPinBasedAuthenticationViewModel.swift -// Mastodon -// -// Created by MainasuK Cirno on 2021/1/29. -// - -import os.log -import Foundation -import Combine -import WebKit - -final class MastodonPinBasedAuthenticationViewModel { - - // input - let authenticateURL: URL - - // output - let pinCodePublisher = PassthroughSubject() - private var navigationDelegateShim: MastodonPinBasedAuthenticationViewModelNavigationDelegateShim? - - init(authenticateURL: URL) { - self.authenticateURL = authenticateURL - } - - deinit { - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - } - -} - -extension MastodonPinBasedAuthenticationViewModel { - - var navigationDelegate: WKNavigationDelegate { - let navigationDelegateShim = MastodonPinBasedAuthenticationViewModelNavigationDelegateShim(viewModel: self) - self.navigationDelegateShim = navigationDelegateShim - return navigationDelegateShim - } - -} diff --git a/Mastodon/Scene/Onboarding/PinBasedAuthentication/MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift b/Mastodon/Scene/Onboarding/PinBasedAuthentication/MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift deleted file mode 100644 index dd8901721..000000000 --- a/Mastodon/Scene/Onboarding/PinBasedAuthentication/MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift -// Mastodon -// -// Created by Cirno MainasuK on 2021/1/29. -// - -import os.log -import Foundation -import WebKit - -final class MastodonPinBasedAuthenticationViewModelNavigationDelegateShim: NSObject { - - weak var viewModel: MastodonPinBasedAuthenticationViewModel? - - init(viewModel: MastodonPinBasedAuthenticationViewModel) { - self.viewModel = viewModel - } - - deinit { - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - } -} - - -// MARK: - WKNavigationDelegate -extension MastodonPinBasedAuthenticationViewModelNavigationDelegateShim: WKNavigationDelegate { - - func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { - guard let url = webView.url, - let components = URLComponents(url: url, resolvingAgainstBaseURL: false), - let codeQueryItem = components.queryItems?.first(where: { $0.name == "code" }), - let code = codeQueryItem.value else { - return - } - - viewModel?.pinCodePublisher.send(code) - } - -} - diff --git a/Mastodon/Scene/Onboarding/Share/AuthenticationViewModel.swift b/Mastodon/Scene/Onboarding/Share/AuthenticationViewModel.swift index 0bd1bf09b..eb3cf5721 100644 --- a/Mastodon/Scene/Onboarding/Share/AuthenticationViewModel.swift +++ b/Mastodon/Scene/Onboarding/Share/AuthenticationViewModel.swift @@ -31,9 +31,7 @@ final class AuthenticationViewModel { let isIdle = CurrentValueSubject(true) let authenticated = PassthroughSubject<(domain: String, account: Mastodon.Entity.Account), Never>() let error = CurrentValueSubject(nil) - - var mastodonPinBasedAuthenticationViewController: UIViewController? - + init(context: AppContext, coordinator: SceneCoordinator, isAuthenticationExist: Bool) { self.context = context self.coordinator = coordinator @@ -118,18 +116,24 @@ extension AuthenticationViewModel { let clientID: String let clientSecret: String let authorizeURL: URL + let redirectURI: String - init?(domain: String, application: Mastodon.Entity.Application) { + init?( + domain: String, + application: Mastodon.Entity.Application, + redirectURI: String = MastodonAuthenticationController.callbackURL + ) { self.domain = domain guard let clientID = application.clientID, let clientSecret = application.clientSecret else { return nil } self.clientID = clientID self.clientSecret = clientSecret self.authorizeURL = { - let query = Mastodon.API.OAuth.AuthorizeQuery(clientID: clientID) + let query = Mastodon.API.OAuth.AuthorizeQuery(clientID: clientID, redirectURI: redirectURI) let url = Mastodon.API.OAuth.authorizeURL(domain: domain, query: query) return url }() + self.redirectURI = redirectURI } } @@ -138,8 +142,6 @@ extension AuthenticationViewModel { .handleEvents(receiveOutput: { [weak self] _ in guard let self = self else { return } self.isAuthenticating.value = true - self.mastodonPinBasedAuthenticationViewController?.dismiss(animated: true, completion: nil) - self.mastodonPinBasedAuthenticationViewController = nil }) .compactMap { [weak self] code -> AnyPublisher, Error>? in guard let self = self else { return nil } @@ -148,6 +150,7 @@ extension AuthenticationViewModel { domain: info.domain, clientID: info.clientID, clientSecret: info.clientSecret, + redirectURI: info.redirectURI, code: code ) .flatMap { response -> AnyPublisher, Error> in diff --git a/Mastodon/Scene/Onboarding/Share/MastodonAuthenticationController.swift b/Mastodon/Scene/Onboarding/Share/MastodonAuthenticationController.swift new file mode 100644 index 000000000..c97fc1489 --- /dev/null +++ b/Mastodon/Scene/Onboarding/Share/MastodonAuthenticationController.swift @@ -0,0 +1,75 @@ +// +// MastodonAuthenticationController.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-6-4. +// + +import os.log +import UIKit +import Combine +import AuthenticationServices + +final class MastodonAuthenticationController { + + static let callbackURLScheme = "mastodon" + static let callbackURL = "mastodon://joinmastodon.org/oauth" + + var disposeBag = Set() + + // input + var context: AppContext! + let authenticateURL: URL + var authenticationSession: ASWebAuthenticationSession? + + // output + let isAuthenticating = CurrentValueSubject(false) + let error = CurrentValueSubject(nil) + let pinCodePublisher = PassthroughSubject() + + init( + context: AppContext, + authenticateURL: URL + ) { + self.context = context + self.authenticateURL = authenticateURL + + authentication() + } + +} + +extension MastodonAuthenticationController { + private func authentication() { + authenticationSession = ASWebAuthenticationSession( + url: authenticateURL, + callbackURLScheme: MastodonAuthenticationController.callbackURLScheme + ) { [weak self] callback, error in + guard let self = self else { return } + os_log("%{public}s[%{public}ld], %{public}s: callback: %s, error: %s", ((#file as NSString).lastPathComponent), #line, #function, callback?.debugDescription ?? "", error.debugDescription) + + if let error = error { + if let error = error as? ASWebAuthenticationSessionError { + if error.errorCode == ASWebAuthenticationSessionError.canceledLogin.rawValue { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: user cancel authentication", ((#file as NSString).lastPathComponent), #line, #function) + self.isAuthenticating.value = false + return + } + } + + self.isAuthenticating.value = false + self.error.value = error + return + } + + guard let url = callback, + let components = URLComponents(url: url, resolvingAgainstBaseURL: false), + let codeQueryItem = components.queryItems?.first(where: { $0.name == "code" }), + let code = codeQueryItem.value else { + return + } + + self.pinCodePublisher.send(code) + } + } +} diff --git a/Mastodon/Service/APIService/APIService+App.swift b/Mastodon/Service/APIService/APIService+App.swift index 9726a6ee6..b5b1d8686 100644 --- a/Mastodon/Service/APIService/APIService+App.swift +++ b/Mastodon/Service/APIService/APIService+App.swift @@ -20,7 +20,11 @@ extension APIService { #endif func createApplication(domain: String) -> AnyPublisher, Error> { - let query = Mastodon.API.App.CreateQuery(clientName: APIService.clientName, website: nil) + let query = Mastodon.API.App.CreateQuery( + clientName: APIService.clientName, + redirectURIs: MastodonAuthenticationController.callbackURL, + website: nil + ) return Mastodon.API.App.create( session: session, domain: domain, diff --git a/Mastodon/Service/APIService/APIService+Authentication.swift b/Mastodon/Service/APIService/APIService+Authentication.swift index 55a188a5c..ffd9afd77 100644 --- a/Mastodon/Service/APIService/APIService+Authentication.swift +++ b/Mastodon/Service/APIService/APIService+Authentication.swift @@ -17,11 +17,13 @@ extension APIService { domain: String, clientID: String, clientSecret: String, + redirectURI: String, code: String ) -> AnyPublisher, Error> { let query = Mastodon.API.OAuth.AccessTokenQuery( clientID: clientID, clientSecret: clientSecret, + redirectURI: redirectURI, code: code, grantType: "authorization_code" ) @@ -35,11 +37,13 @@ extension APIService { func applicationAccessToken( domain: String, clientID: String, - clientSecret: String + clientSecret: String, + redirectURI: String ) -> AnyPublisher, Error> { let query = Mastodon.API.OAuth.AccessTokenQuery( clientID: clientID, clientSecret: clientSecret, + redirectURI: redirectURI, code: nil, grantType: "client_credentials" ) diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+App.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+App.swift index 3993afa68..48d78d680 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+App.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+App.swift @@ -103,7 +103,7 @@ extension Mastodon.API.App { public init( clientName: String, - redirectURIs: String = "urn:ietf:wg:oauth:2.0:oob", + redirectURIs: String, scopes: String? = "read write follow push", website: String? ) { diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+OAuth.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+OAuth.swift index 332d78a38..b5451e8fa 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+OAuth.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+OAuth.swift @@ -139,7 +139,7 @@ extension Mastodon.API.OAuth { forceLogin: String? = nil, responseType: String = "code", clientID: String, - redirectURI: String = "urn:ietf:wg:oauth:2.0:oob", + redirectURI: String, scope: String? = "read write follow push" ) { self.forceLogin = forceLogin @@ -166,7 +166,7 @@ extension Mastodon.API.OAuth { public init( clientID: String, clientSecret: String, - redirectURI: String = "urn:ietf:wg:oauth:2.0:oob", + redirectURI: String, scope: String? = "read write follow push", code: String?, grantType: String