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

148 lines
5.8 KiB
Swift
Raw Normal View History

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
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 {
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)
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-13 10:03:08 +02:00
let authorization = appAuthorization(redirectURI: OAuth.authorizationCallbackURL).share()
2020-09-11 11:55:06 +02:00
return authorization
.zip(authorization.flatMap(authenticate(appAuthorization:)))
.eraseToAnyPublisher()
}
2020-10-06 00:50:05 +02:00
func register(_ registration: Registration,
id: Identity.Id) -> AnyPublisher<(AppAuthorization, AccessToken), Error> {
2020-09-13 10:03:08 +02:00
let redirectURI = OAuth.registrationCallbackURL.appendingPathComponent(id.uuidString)
let authorization = appAuthorization(redirectURI: redirectURI)
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(
2020-10-06 00:50:05 +02:00
clientId: appAuthorization.clientId,
2020-09-11 11:55:06 +02:00
clientSecret: appAuthorization.clientSecret,
grantType: OAuth.registrationGrantType,
scopes: OAuth.scopes,
code: nil,
2020-09-13 10:03:08 +02:00
redirectURI: redirectURI.absoluteString))
2020-09-11 11:55:06 +02:00
.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()
})
.eraseToAnyPublisher()
2020-08-09 07:37:04 +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"
static let callbackURLScheme = "metatext"
2020-09-13 10:03:08 +02:00
static let authorizationCallbackURL = URL(string: "\(callbackURLScheme)://oauth.callback")!
static let registrationCallbackURL = URL(string: "https://metatext.link/confirmation")!
2021-02-15 23:20:59 +01:00
static let website = URL(string: "https://metabolist.org/metatext")!
2020-08-09 07:37:04 +02:00
}
2020-08-31 21:01:37 +02:00
enum OAuthError: Error {
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-13 10:03:08 +02:00
func appAuthorization(redirectURI: URL) -> AnyPublisher<AppAuthorization, Error> {
2020-09-11 11:55:06 +02:00
mastodonAPIClient.request(
AppAuthorizationEndpoint.apps(
clientName: OAuth.clientName,
2020-09-13 10:03:08 +02:00
redirectURI: redirectURI.absoluteString,
2020-09-11 11:55:06 +02:00
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
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"),
2020-09-13 10:03:08 +02:00
.init(name: "redirect_uri", value: OAuth.authorizationCallbackURL.absoluteString)
2020-09-09 03:02:55 +02:00
]
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(
2020-10-06 00:50:05 +02:00
clientId: appAuthorization.clientId,
2020-09-09 03:02:55 +02:00
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-13 10:03:08 +02:00
redirectURI: OAuth.authorizationCallbackURL.absoluteString))
2020-09-09 03:02:55 +02:00
}
.eraseToAnyPublisher()
}
}