2020-08-09 07:37:04 +02:00
|
|
|
// Copyright © 2020 Metabolist. All rights reserved.
|
|
|
|
|
|
|
|
import Foundation
|
|
|
|
import Combine
|
|
|
|
|
|
|
|
struct AuthenticationService {
|
|
|
|
private let networkClient: MastodonClient
|
2020-08-12 11:01:21 +02:00
|
|
|
private let webAuthSessionType: WebAuthSession.Type
|
2020-08-09 07:37:04 +02:00
|
|
|
private let webAuthSessionContextProvider = WebAuthSessionContextProvider()
|
|
|
|
|
|
|
|
init(environment: AppEnvironment) {
|
2020-08-21 04:29:01 +02:00
|
|
|
networkClient = MastodonClient(environment: environment)
|
2020-08-09 13:27:38 +02:00
|
|
|
webAuthSessionType = environment.webAuthSessionType
|
2020-08-09 07:37:04 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
extension AuthenticationService {
|
2020-08-09 13:27:38 +02:00
|
|
|
func authorizeApp(instanceURL: URL) -> AnyPublisher<AppAuthorization, Error> {
|
2020-08-09 07:37:04 +02:00
|
|
|
let endpoint = AppAuthorizationEndpoint.apps(
|
2020-08-09 13:27:38 +02:00
|
|
|
clientName: OAuth.clientName,
|
|
|
|
redirectURI: OAuth.callbackURL.absoluteString,
|
|
|
|
scopes: OAuth.scopes,
|
|
|
|
website: OAuth.website)
|
2020-08-09 07:37:04 +02:00
|
|
|
let target = MastodonTarget(baseURL: instanceURL, endpoint: endpoint, accessToken: nil)
|
|
|
|
|
|
|
|
return networkClient.request(target)
|
|
|
|
}
|
|
|
|
|
2020-08-09 13:27:38 +02:00
|
|
|
func authenticate(instanceURL: URL, appAuthorization: AppAuthorization) -> AnyPublisher<AccessToken, Error> {
|
|
|
|
guard let authorizationURL = authorizationURL(
|
|
|
|
instanceURL: instanceURL,
|
|
|
|
clientID: appAuthorization.clientId) else {
|
|
|
|
return Fail(error: URLError(.badURL)).eraseToAnyPublisher()
|
|
|
|
}
|
2020-08-09 07:37:04 +02:00
|
|
|
|
2020-08-09 13:27:38 +02:00
|
|
|
return webAuthSessionType.publisher(
|
|
|
|
url: authorizationURL,
|
|
|
|
callbackURLScheme: OAuth.callbackURLScheme,
|
|
|
|
presentationContextProvider: webAuthSessionContextProvider)
|
|
|
|
.tryCatch { error -> AnyPublisher<URL?, Error> in
|
|
|
|
if (error as? WebAuthSessionError)?.code == .canceledLogin {
|
|
|
|
return Just(nil).setFailureType(to: Error.self).eraseToAnyPublisher()
|
|
|
|
}
|
2020-08-09 07:37:04 +02:00
|
|
|
|
2020-08-09 13:27:38 +02:00
|
|
|
throw error
|
2020-08-09 07:37:04 +02:00
|
|
|
}
|
2020-08-09 13:27:38 +02:00
|
|
|
.compactMap { $0 }
|
|
|
|
.tryMap { url -> String in
|
|
|
|
guard let queryItems = URLComponents(url: url, resolvingAgainstBaseURL: true)?.queryItems,
|
|
|
|
let code = queryItems.first(where: {
|
|
|
|
$0.name == OAuth.codeCallbackQueryItemName
|
|
|
|
})?.value
|
|
|
|
else { throw OAuthError.codeNotFound }
|
|
|
|
|
|
|
|
return code
|
|
|
|
}
|
|
|
|
.flatMap { code -> AnyPublisher<AccessToken, Error> in
|
|
|
|
let endpoint = AccessTokenEndpoint.oauthToken(
|
|
|
|
clientID: appAuthorization.clientId,
|
|
|
|
clientSecret: appAuthorization.clientSecret,
|
|
|
|
code: code,
|
|
|
|
grantType: OAuth.grantType,
|
|
|
|
scopes: OAuth.scopes,
|
|
|
|
redirectURI: OAuth.callbackURL.absoluteString)
|
|
|
|
let target = MastodonTarget(baseURL: instanceURL, endpoint: endpoint, accessToken: nil)
|
|
|
|
|
|
|
|
return networkClient.request(target)
|
|
|
|
}
|
|
|
|
.eraseToAnyPublisher()
|
2020-08-09 07:37:04 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-09 13:27:38 +02:00
|
|
|
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")!
|
2020-08-09 07:37:04 +02:00
|
|
|
}
|
|
|
|
|
2020-08-09 13:27:38 +02:00
|
|
|
enum OAuthError {
|
|
|
|
case codeNotFound
|
2020-08-09 07:37:04 +02:00
|
|
|
}
|
|
|
|
|
2020-08-09 13:27:38 +02:00
|
|
|
private func authorizationURL(instanceURL: URL, clientID: String) -> URL? {
|
|
|
|
guard var authorizationURLComponents = URLComponents(url: instanceURL, resolvingAgainstBaseURL: true) else {
|
|
|
|
return nil
|
2020-08-09 07:37:04 +02:00
|
|
|
}
|
|
|
|
|
2020-08-09 13:27:38 +02:00
|
|
|
authorizationURLComponents.path = "/oauth/authorize"
|
|
|
|
authorizationURLComponents.queryItems = [
|
|
|
|
"client_id": clientID,
|
|
|
|
"scope": OAuth.scopes,
|
|
|
|
"response_type": "code",
|
|
|
|
"redirect_uri": OAuth.callbackURL.absoluteString
|
|
|
|
].map { URLQueryItem(name: $0, value: $1) }
|
2020-08-09 07:37:04 +02:00
|
|
|
|
2020-08-09 13:27:38 +02:00
|
|
|
return authorizationURLComponents.url
|
|
|
|
}
|
|
|
|
}
|
2020-08-09 07:37:04 +02:00
|
|
|
|
2020-08-09 13:27:38 +02:00
|
|
|
extension AuthenticationService.OAuthError: LocalizedError {
|
|
|
|
var errorDescription: String? {
|
|
|
|
switch self {
|
|
|
|
case .codeNotFound:
|
|
|
|
return NSLocalizedString("oauth.error.code-not-found", comment: "")
|
2020-08-09 07:37:04 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|