2020-08-09 07:37:04 +02:00
|
|
|
// Copyright © 2020 Metabolist. All rights reserved.
|
|
|
|
|
|
|
|
import Combine
|
2020-09-05 04:31:43 +02:00
|
|
|
import Foundation
|
2020-08-31 01:33:11 +02:00
|
|
|
import Mastodon
|
2020-09-04 03:55:46 +02:00
|
|
|
import MastodonAPI
|
2020-08-09 07:37:04 +02:00
|
|
|
|
2020-09-09 03:02:55 +02:00
|
|
|
public enum AuthenticationError: Error {
|
|
|
|
case canceled
|
|
|
|
}
|
|
|
|
|
|
|
|
struct AuthenticationService {
|
2020-09-04 03:55:46 +02:00
|
|
|
private let mastodonAPIClient: MastodonAPIClient
|
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()
|
|
|
|
|
2020-09-09 03:02:55 +02:00
|
|
|
init(url: URL, environment: AppEnvironment) {
|
|
|
|
mastodonAPIClient = MastodonAPIClient(session: environment.session, instanceURL: url)
|
2020-08-09 13:27:38 +02:00
|
|
|
webAuthSessionType = environment.webAuthSessionType
|
2020-08-09 07:37:04 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-09 03:02:55 +02:00
|
|
|
extension AuthenticationService {
|
|
|
|
func authenticate() -> AnyPublisher<(AppAuthorization, AccessToken), Error> {
|
2020-09-11 11:55:06 +02:00
|
|
|
let authorization = appAuthorization().share()
|
|
|
|
|
|
|
|
return authorization
|
|
|
|
.zip(authorization.flatMap(authenticate(appAuthorization:)))
|
|
|
|
.eraseToAnyPublisher()
|
|
|
|
}
|
|
|
|
|
2020-09-12 04:50:42 +02:00
|
|
|
func register(_ registration: Registration) -> AnyPublisher<(AppAuthorization, AccessToken), Error> {
|
2020-09-11 11:55:06 +02:00
|
|
|
let authorization = appAuthorization()
|
2020-09-09 03:02:55 +02:00
|
|
|
.share()
|
2020-08-09 07:37:04 +02:00
|
|
|
|
2020-09-11 11:55:06 +02:00
|
|
|
return authorization.zip(
|
|
|
|
authorization.flatMap { appAuthorization -> AnyPublisher<AccessToken, Error> in
|
|
|
|
mastodonAPIClient.request(
|
|
|
|
AccessTokenEndpoint.oauthToken(
|
|
|
|
clientID: appAuthorization.clientId,
|
|
|
|
clientSecret: appAuthorization.clientSecret,
|
|
|
|
grantType: OAuth.registrationGrantType,
|
|
|
|
scopes: OAuth.scopes,
|
|
|
|
code: nil,
|
|
|
|
redirectURI: OAuth.callbackURL.absoluteString))
|
|
|
|
.flatMap { accessToken -> AnyPublisher<AccessToken, Error> in
|
|
|
|
mastodonAPIClient.accessToken = accessToken.accessToken
|
|
|
|
|
2020-09-12 04:50:42 +02:00
|
|
|
return mastodonAPIClient.request(AccessTokenEndpoint.accounts(registration))
|
2020-09-11 11:55:06 +02:00
|
|
|
}
|
|
|
|
.eraseToAnyPublisher()
|
|
|
|
})
|
2020-08-09 13:27:38 +02:00
|
|
|
.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"
|
2020-09-11 11:55:06 +02:00
|
|
|
static let authorizationCodeGrantType = "authorization_code"
|
|
|
|
static let registrationGrantType = "client_credentials"
|
2020-08-09 13:27:38 +02:00
|
|
|
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-31 21:01:37 +02:00
|
|
|
enum OAuthError: Error {
|
2020-08-09 13:27:38 +02:00
|
|
|
case codeNotFound
|
2020-08-09 07:37:04 +02:00
|
|
|
}
|
|
|
|
|
2020-09-09 03:02:55 +02:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2020-09-11 11:55:06 +02:00
|
|
|
func appAuthorization() -> AnyPublisher<AppAuthorization, Error> {
|
|
|
|
mastodonAPIClient.request(
|
|
|
|
AppAuthorizationEndpoint.apps(
|
|
|
|
clientName: OAuth.clientName,
|
|
|
|
redirectURI: OAuth.callbackURL.absoluteString,
|
|
|
|
scopes: OAuth.scopes,
|
|
|
|
website: OAuth.website))
|
|
|
|
}
|
|
|
|
|
2020-09-09 03:02:55 +02:00
|
|
|
func authorizationURL(appAuthorization: AppAuthorization) throws -> URL {
|
|
|
|
guard var authorizationURLComponents = URLComponents(
|
|
|
|
url: mastodonAPIClient.instanceURL,
|
|
|
|
resolvingAgainstBaseURL: true)
|
|
|
|
else { throw URLError(.badURL) }
|
2020-08-09 07:37:04 +02:00
|
|
|
|
2020-08-09 13:27:38 +02:00
|
|
|
authorizationURLComponents.path = "/oauth/authorize"
|
|
|
|
authorizationURLComponents.queryItems = [
|
2020-09-09 03:02:55 +02:00
|
|
|
.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)
|
|
|
|
}
|
2020-08-09 07:37:04 +02:00
|
|
|
|
2020-09-09 03:02:55 +02:00
|
|
|
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,
|
2020-09-11 11:55:06 +02:00
|
|
|
grantType: OAuth.authorizationCodeGrantType,
|
2020-09-09 03:02:55 +02:00
|
|
|
scopes: OAuth.scopes,
|
2020-09-11 11:55:06 +02:00
|
|
|
code: $0,
|
2020-09-09 03:02:55 +02:00
|
|
|
redirectURI: OAuth.callbackURL.absoluteString))
|
|
|
|
}
|
|
|
|
.eraseToAnyPublisher()
|
2020-08-09 13:27:38 +02:00
|
|
|
}
|
|
|
|
}
|