metatext-app-ios-iphone-ipad/ServiceLayer/Sources/ServiceLayer/Services/AuthenticationService.swift

117 lines
4.2 KiB
Swift

// Copyright © 2020 Metabolist. All rights reserved.
import Combine
import Foundation
import Mastodon
import MastodonAPI
public enum AuthenticationError: Error {
case canceled
}
struct AuthenticationService {
private let mastodonAPIClient: MastodonAPIClient
private let webAuthSessionType: WebAuthSession.Type
private let webAuthSessionContextProvider = WebAuthSessionContextProvider()
init(url: URL, environment: AppEnvironment) {
mastodonAPIClient = MastodonAPIClient(session: environment.session, instanceURL: url)
webAuthSessionType = environment.webAuthSessionType
}
}
extension AuthenticationService {
func authenticate() -> AnyPublisher<(AppAuthorization, AccessToken), Error> {
let appAuthorization = mastodonAPIClient.request(
AppAuthorizationEndpoint.apps(
clientName: OAuth.clientName,
redirectURI: OAuth.callbackURL.absoluteString,
scopes: OAuth.scopes,
website: OAuth.website))
.share()
return appAuthorization
.zip(appAuthorization.flatMap(authenticate(appAuthorization:)))
.eraseToAnyPublisher()
}
}
private extension AuthenticationService {
struct OAuth {
static let clientName = "Metatext"
static let scopes = "read write follow push"
static let codeCallbackQueryItemName = "code"
static let grantType = "authorization_code"
static let callbackURLScheme = "metatext"
static let callbackURL = URL(string: "\(callbackURLScheme)://oauth.callback")!
static let website = URL(string: "https://metabolist.com/metatext")!
}
enum OAuthError: Error {
case codeNotFound
}
static func extractCode(oauthCallbackURL: URL) throws -> String {
guard let queryItems = URLComponents(
url: oauthCallbackURL,
resolvingAgainstBaseURL: true)?.queryItems,
let code = queryItems.first(where: {
$0.name == OAuth.codeCallbackQueryItemName
})?.value
else { throw OAuthError.codeNotFound }
return code
}
func authorizationURL(appAuthorization: AppAuthorization) throws -> URL {
guard var authorizationURLComponents = URLComponents(
url: mastodonAPIClient.instanceURL,
resolvingAgainstBaseURL: true)
else { throw URLError(.badURL) }
authorizationURLComponents.path = "/oauth/authorize"
authorizationURLComponents.queryItems = [
.init(name: "client_id", value: appAuthorization.clientId),
.init(name: "scope", value: OAuth.scopes),
.init(name: "response_type", value: "code"),
.init(name: "redirect_uri", value: OAuth.callbackURL.absoluteString)
]
guard let authorizationURL = authorizationURLComponents.url else {
throw URLError(.badURL)
}
return authorizationURL
}
func authenticate(appAuthorization: AppAuthorization) -> AnyPublisher<AccessToken, Error> {
Just(appAuthorization)
.tryMap(authorizationURL(appAuthorization:))
.flatMap {
webAuthSessionType.publisher(
url: $0,
callbackURLScheme: OAuth.callbackURLScheme,
presentationContextProvider: webAuthSessionContextProvider)
}
.mapError { error -> Error in
if (error as? WebAuthSessionError)?.code == .canceledLogin {
return AuthenticationError.canceled as Error
}
return error
}
.tryMap(Self.extractCode(oauthCallbackURL:))
.flatMap {
mastodonAPIClient.request(
AccessTokenEndpoint.oauthToken(
clientID: appAuthorization.clientId,
clientSecret: appAuthorization.clientSecret,
code: $0,
grantType: OAuth.grantType,
scopes: OAuth.scopes,
redirectURI: OAuth.callbackURL.absoluteString))
}
.eraseToAnyPublisher()
}
}