From fb7d23b5d6917c0fcc3b2ea00d8aef11b98092dc Mon Sep 17 00:00:00 2001 From: Justin Mazzocchi <2831158+jzzocc@users.noreply.github.com> Date: Thu, 13 Aug 2020 20:40:46 -0700 Subject: [PATCH] Revoke access token when removing account --- Metatext.xcodeproj/project.pbxproj | 6 +++ .../Endpoints/DeletionEndpoint.swift | 39 +++++++++++++++++++ Shared/Services/IdentitiesService.swift | 22 ++++++----- Shared/Services/IdentityService.swift | 2 +- Shared/Services/SecretsService.swift | 8 +++- Shared/View Models/RootViewModel.swift | 4 +- iOS/Views/IdentitiesView.swift | 2 +- 7 files changed, 68 insertions(+), 15 deletions(-) create mode 100644 Shared/Networking/Mastodon API/Endpoints/DeletionEndpoint.swift diff --git a/Metatext.xcodeproj/project.pbxproj b/Metatext.xcodeproj/project.pbxproj index 49465d1..38ff6d1 100644 --- a/Metatext.xcodeproj/project.pbxproj +++ b/Metatext.xcodeproj/project.pbxproj @@ -44,6 +44,8 @@ D019E6EE24DF7BF300697C7D /* IdentityDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = D019E6EC24DF7BF300697C7D /* IdentityDatabase.swift */; }; D019E6F024DF7C2F00697C7D /* DatabaseError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D019E6EF24DF7C2F00697C7D /* DatabaseError.swift */; }; D019E6F124DF7C2F00697C7D /* DatabaseError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D019E6EF24DF7C2F00697C7D /* DatabaseError.swift */; }; + D03DF45B24E62A68007A8CD5 /* DeletionEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03DF45A24E62A68007A8CD5 /* DeletionEndpoint.swift */; }; + D03DF45C24E62A68007A8CD5 /* DeletionEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03DF45A24E62A68007A8CD5 /* DeletionEndpoint.swift */; }; D047FAAE24C3E21200AF17C5 /* MetatextApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D047FA8524C3E21000AF17C5 /* MetatextApp.swift */; }; D047FAAF24C3E21200AF17C5 /* MetatextApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D047FA8524C3E21000AF17C5 /* MetatextApp.swift */; }; D047FAB224C3E21200AF17C5 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D047FA8724C3E21200AF17C5 /* Assets.xcassets */; }; @@ -238,6 +240,7 @@ D019E6E024DF72E700697C7D /* InstanceEndpoint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstanceEndpoint.swift; sourceTree = ""; }; D019E6EC24DF7BF300697C7D /* IdentityDatabase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IdentityDatabase.swift; sourceTree = ""; }; D019E6EF24DF7C2F00697C7D /* DatabaseError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseError.swift; sourceTree = ""; }; + D03DF45A24E62A68007A8CD5 /* DeletionEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletionEndpoint.swift; sourceTree = ""; }; D047FA8524C3E21000AF17C5 /* MetatextApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetatextApp.swift; sourceTree = ""; }; D047FA8724C3E21200AF17C5 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; D047FA8C24C3E21200AF17C5 /* Metatext.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Metatext.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -396,6 +399,7 @@ D019E6DF24DF72E700697C7D /* AccessTokenEndpoint.swift */, D019E6DE24DF72E700697C7D /* AccountEndpoint.swift */, D019E6DC24DF72E700697C7D /* AppAuthorizationEndpoint.swift */, + D03DF45A24E62A68007A8CD5 /* DeletionEndpoint.swift */, D019E6E024DF72E700697C7D /* InstanceEndpoint.swift */, D019E6DD24DF72E700697C7D /* PreferencesEndpoint.swift */, D0EC8DED24E2704D00A08489 /* PushSubscriptionEndpoint.swift */, @@ -934,6 +938,7 @@ D0091B6E24DD68090040E8D2 /* PreferencesView.swift in Sources */, D0159F8F24DE743700E78478 /* IdentitiesView.swift in Sources */, D0EC8DEB24E26F1100A08489 /* PushSubscription.swift in Sources */, + D03DF45B24E62A68007A8CD5 /* DeletionEndpoint.swift in Sources */, D0DB6EF424C5228A00D965FE /* AddIdentityView.swift in Sources */, D074577724D29006004758DB /* MockWebAuthSession.swift in Sources */, D0159FA524DE989700E78478 /* NSMutableAttributedString+Extensions.swift in Sources */, @@ -1020,6 +1025,7 @@ D074577B24D29366004758DB /* URLSessionConfiguration+Extensions.swift in Sources */, D0ED1BB824CE47F400B4899C /* WebAuthSession.swift in Sources */, D0EC8DEC24E26F1100A08489 /* PushSubscription.swift in Sources */, + D03DF45C24E62A68007A8CD5 /* DeletionEndpoint.swift in Sources */, D0A1CA7524DAC2F1003063E9 /* KingfisherOptionsInfo+Extensions.swift in Sources */, D0ED1BC524CED54D00B4899C /* HTTPTarget.swift in Sources */, D0C963FF24CC3812003BD330 /* Publisher+Extensions.swift in Sources */, diff --git a/Shared/Networking/Mastodon API/Endpoints/DeletionEndpoint.swift b/Shared/Networking/Mastodon API/Endpoints/DeletionEndpoint.swift new file mode 100644 index 0000000..b19f8a4 --- /dev/null +++ b/Shared/Networking/Mastodon API/Endpoints/DeletionEndpoint.swift @@ -0,0 +1,39 @@ +// Copyright © 2020 Metabolist. All rights reserved. + +import Foundation + +enum DeletionEndpoint { + case oauthRevoke(token: String, clientID: String, clientSecret: String) +} + +extension DeletionEndpoint: MastodonEndpoint { + typealias ResultType = [String: String] + + var context: [String] { + switch self { + case .oauthRevoke: + return [] + } + } + + var pathComponentsInContext: [String] { + switch self { + case .oauthRevoke: + return ["oauth", "revoke"] + } + } + + var method: HTTPMethod { + switch self { + case .oauthRevoke: + return .post + } + } + + var parameters: [String: Any]? { + switch self { + case let .oauthRevoke(token, clientID, clientSecret): + return ["token": token, "client_id": clientID, "client_secret": clientSecret] + } + } +} diff --git a/Shared/Services/IdentitiesService.swift b/Shared/Services/IdentitiesService.swift index 5a8c961..a04b86c 100644 --- a/Shared/Services/IdentitiesService.swift +++ b/Shared/Services/IdentitiesService.swift @@ -50,18 +50,22 @@ extension IdentitiesService { .eraseToAnyPublisher() } - func deleteIdentity(id: UUID) -> AnyPublisher { - let environment = self.environment + func deleteIdentity(_ identity: Identity) -> AnyPublisher { + let secretsService = SecretsService(identityID: identity.id, keychainService: environment.keychainServiceType) + let networkClient = MastodonClient(session: environment.session) - return identityDatabase.deleteIdentity(id: id) - .tryMap { _ -> Void in - try SecretsService( - identityID: id, - keychainService: environment.keychainServiceType) - .deleteAllItems() + networkClient.instanceURL = identity.url - return () + return identityDatabase.deleteIdentity(id: identity.id) + .tryMap { + DeletionEndpoint.oauthRevoke( + token: try secretsService.item(.accessToken), + clientID: try secretsService.item(.clientID), + clientSecret: try secretsService.item(.clientSecret)) } + .flatMap(networkClient.request) + .tryMap { _ in try secretsService.deleteAllItems() } + .print() .eraseToAnyPublisher() } diff --git a/Shared/Services/IdentityService.swift b/Shared/Services/IdentityService.swift index deca3f3..1208ae5 100644 --- a/Shared/Services/IdentityService.swift +++ b/Shared/Services/IdentityService.swift @@ -35,7 +35,7 @@ class IdentityService { keychainService: environment.keychainServiceType) networkClient = MastodonClient(session: environment.session) networkClient.instanceURL = identity.url - networkClient.accessToken = try secretsService.item(.accessToken) + networkClient.accessToken = try? secretsService.item(.accessToken) observation.catch { [weak self] error -> Empty in self?.observationErrorsInput.send(error) diff --git a/Shared/Services/SecretsService.swift b/Shared/Services/SecretsService.swift index 80e6a30..b4487c0 100644 --- a/Shared/Services/SecretsService.swift +++ b/Shared/Services/SecretsService.swift @@ -31,6 +31,10 @@ extension SecretsService { } } +enum SecretsServiceError: Error { + case itemAbsent +} + extension SecretsService.Item { enum Kind { case genericPassword @@ -53,11 +57,11 @@ extension SecretsService { service: Self.keychainServiceName) } - func item(_ item: Item) throws -> T? { + func item(_ item: Item) throws -> T { guard let data = try keychainService.getGenericPassword( account: key(item: item), service: Self.keychainServiceName) else { - return nil + throw SecretsServiceError.itemAbsent } return try T.fromDataStoredInSecrets(data) diff --git a/Shared/View Models/RootViewModel.swift b/Shared/View Models/RootViewModel.swift index 19fc040..5a95526 100644 --- a/Shared/View Models/RootViewModel.swift +++ b/Shared/View Models/RootViewModel.swift @@ -70,8 +70,8 @@ extension RootViewModel { mainNavigationViewModel = MainNavigationViewModel(identityService: identityService) } - func deleteIdentity(id: UUID) { - identitiesService.deleteIdentity(id: id) + func deleteIdentity(_ identity: Identity) { + identitiesService.deleteIdentity(identity) .sink { _ in } receiveValue: { _ in } .store(in: &cancellables) } diff --git a/iOS/Views/IdentitiesView.swift b/iOS/Views/IdentitiesView.swift index 2ec792c..e5da94e 100644 --- a/iOS/Views/IdentitiesView.swift +++ b/iOS/Views/IdentitiesView.swift @@ -53,7 +53,7 @@ struct IdentitiesView: View { .onDelete { guard let index = $0.first else { return } - rootViewModel.deleteIdentity(id: viewModel.identities[index].id) + rootViewModel.deleteIdentity(viewModel.identities[index]) } } }