Files

348 lines
12 KiB
Swift

//
// Copyright RevenueCat Inc. All Rights Reserved.
//
// Licensed under the MIT License (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://opensource.org/licenses/MIT
//
// TokenTokenLogInOperation.swift
//
import Foundation
final class TokenLogInOperation: CacheableNetworkOperation {
private let tokenCallbackCache: CallbackCache<TokenCallback>
private let configuration: UserSpecificConfiguration
private let token: IdentityAuthToken
private let linkToID: String?
static func createFactory(
configuration: UserSpecificConfiguration,
token: IdentityAuthToken,
linkToIDToken: String?,
tokenCallbackCache: CallbackCache<TokenCallback>
) -> CacheableNetworkOperationFactory<TokenLogInOperation> {
return .init({
.init(
configuration: configuration,
token: token,
linkToIDToken: linkToIDToken,
tokenCallbackCache: tokenCallbackCache,
cacheKey: $0
) },
individualizedCacheKeyPart: token.cacheIdentifier)
}
private init(
configuration: UserSpecificConfiguration,
token: IdentityAuthToken,
linkToIDToken: String?,
tokenCallbackCache: CallbackCache<TokenCallback>,
cacheKey: String
) {
self.configuration = configuration
self.token = token
self.linkToID = linkToIDToken
self.tokenCallbackCache = tokenCallbackCache
super.init(configuration: configuration, cacheKey: cacheKey)
}
override func begin(completion: @escaping () -> Void) {
self.logIn(completion: completion)
}
}
// Restating inherited @unchecked Sendable from Foundation's Operation
extension TokenLogInOperation: @unchecked Sendable {}
private extension TokenLogInOperation {
func logIn(completion: @escaping () -> Void) {
guard self.token.validate() else {
self.tokenCallbackCache.performOnAllItemsAndRemoveFromCache(withCacheable: self) { callback in
callback.completion(.failure(.invalidAuthorizationToken()))
}
completion()
return
}
let body: any HTTPRequestBody
switch token {
case .anonymous:
body = AnonymousBody(method: "anonymous",
scope: "openid offline_access")
case .oidc(let token):
body = StandardBody(method: "oidc",
scope: "openid offline_access",
idToken: String(bytes: token, encoding: .utf8) ?? token.base64EncodedString(),
linkToID: linkToID)
case .google(let token):
body = StandardBody(method: "google",
scope: "openid offline_access",
idToken: String(bytes: token, encoding: .utf8) ?? token.base64EncodedString(),
linkToID: linkToID)
case .signInWithApple(let token):
body = StandardBody(method: "apple",
scope: "openid offline_access",
idToken: String(bytes: token, encoding: .utf8) ?? token.base64EncodedString(),
linkToID: linkToID)
case .facebook(let token, let email):
body = FacebookBody(method: "facebook",
scope: "openid offline_access",
idToken: String(bytes: token, encoding: .utf8) ?? token.base64EncodedString(),
email: email,
linkToID: linkToID)
case .firebase(let token):
body = StandardBody(method: "firebase",
scope: "openid offline_access",
idToken: String(bytes: token, encoding: .utf8) ?? token.base64EncodedString(),
linkToID: linkToID)
}
let request = HTTPRequest(method: .post(body), path: .tokenLogin)
self.httpClient.perform(request) { (response: VerifiedHTTPResponse<TokenResponse>.Result) in
self.tokenCallbackCache.performOnAllItemsAndRemoveFromCache(withCacheable: self) { callbackObject in
self.handleLogin(response, completion: callbackObject.completion)
}
completion()
}
}
func handleLogin(_ result: VerifiedHTTPResponse<TokenResponse>.Result,
completion: TokenAPI.TokenResponseHandler) {
let finalResult: TokenAPI.TokenResult
switch result {
case .success(let response):
do {
let jwt = try JWT(from: response.body.accessToken)
guard let userID = jwt.appUserID else {
throw BackendError.unexpectedBackendResponse(.loginResponseDecoding,
extraContext: "JWT missing RC user ID")
}
finalResult = .success((response.body, userID))
Logger.user(Strings.identity.login_success)
} catch let error as BackendError {
finalResult = .failure(error)
} catch {
finalResult = .failure(BackendError.unexpectedBackendResponse(.loginResponseDecoding))
}
case .failure(let networkError):
finalResult = .failure(BackendError.networkError(networkError))
}
completion(finalResult)
}
}
final class TokenRevocationOperation: CacheableNetworkOperation, @unchecked Sendable {
private let callbackCache: CallbackCache<TokenRevokeCallback>
private let configuration: UserSpecificConfiguration
private let token: String
private let tokenTypeHint: String
private let appUserID: String
static func createFactory(
configuration: UserSpecificConfiguration,
refreshToken: String,
appUserID: String,
callbackCache: CallbackCache<TokenRevokeCallback>
) -> CacheableNetworkOperationFactory<TokenRevocationOperation> {
return .init({
.init(
configuration: configuration,
token: refreshToken,
tokenTypeHint: "refresh_token",
appUserID: appUserID,
callbackCache: callbackCache,
cacheKey: $0
) },
individualizedCacheKeyPart: appUserID)
}
private init(
configuration: UserSpecificConfiguration,
token: String,
tokenTypeHint: String,
appUserID: String,
callbackCache: CallbackCache<TokenRevokeCallback>,
cacheKey: String
) {
self.configuration = configuration
self.token = token
self.tokenTypeHint = tokenTypeHint
self.appUserID = appUserID
self.callbackCache = callbackCache
super.init(configuration: configuration, cacheKey: cacheKey)
}
override func begin(completion: @escaping () -> Void) {
guard self.token.isNotEmpty else {
self.callbackCache.performOnAllItemsAndRemoveFromCache(withCacheable: self) { callback in
callback.completion(BackendError.missingAppUserID())
}
completion()
return
}
let body = Body(token: token, tokenTypeHint: tokenTypeHint)
let request = HTTPRequest(method: .post(body), path: .tokenLogOut)
self.httpClient.perform(request) { (response: VerifiedHTTPResponse<Data>.Result) in
self.callbackCache.performOnAllItemsAndRemoveFromCache(withCacheable: self) { callbackObject in
let mappedResponse = response.mapError(BackendError.networkError)
callbackObject.completion(mappedResponse.error)
}
completion()
}
}
}
// MARK: - Request Bodies
extension TokenLogInOperation {
struct AnonymousBody: Encodable, HTTPRequestBody {
// Note: These keys need to be explicitly declared using snake_case
// because the CodingKeys are also used for request signing via `contentForSignature`.
// swiftlint:disable:next nesting
private enum CodingKeys: String, CodingKey {
case method
case scope
}
let method: String
let scope: String
var contentForSignature: [(key: String, value: String?)] {
return [
(Self.CodingKeys.method.stringValue, self.method),
(Self.CodingKeys.scope.stringValue, self.scope)
]
}
}
struct StandardBody: Encodable, HTTPRequestBody {
// Note: These keys need to be explicitly declared using snake_case
// because the CodingKeys are also used for request signing via `contentForSignature`.
// swiftlint:disable:next nesting
private enum CodingKeys: String, CodingKey {
case method = "method"
case scope = "scope"
case idToken = "id_token"
case linkToID = "link_to_id"
}
let method: String
let scope: String
let idToken: String
let linkToID: String?
var contentForSignature: [(key: String, value: String?)] {
return [
(Self.CodingKeys.method.stringValue, self.method),
(Self.CodingKeys.scope.stringValue, self.scope),
(Self.CodingKeys.idToken.stringValue, self.idToken),
(Self.CodingKeys.linkToID.stringValue, self.linkToID)
]
}
}
struct FacebookBody: Encodable, HTTPRequestBody {
// swiftlint:disable:next nesting
private enum CodingKeys: String, CodingKey {
case method = "method"
case scope = "scope"
case idToken = "id_token"
case email = "email"
case linkToID = "link_to_id"
}
let method: String
let scope: String
let idToken: String
let email: String?
let linkToID: String?
var contentForSignature: [(key: String, value: String?)] {
return [
(Self.CodingKeys.method.stringValue, self.method),
(Self.CodingKeys.scope.stringValue, self.scope),
(Self.CodingKeys.idToken.stringValue, self.idToken),
(Self.CodingKeys.email.stringValue, self.email),
(Self.CodingKeys.linkToID.stringValue, self.linkToID)
]
}
}
}
// this is an enum because "TokenRefreshOperations" don't exist like the TokenLogInOperation.
// token refreshing happens directly in the TokenManager. This enum exists for
// namespacing consistency and recognizability.
enum TokenRefreshOperation {
struct Body: Encodable, HTTPRequestBody {
// swiftlint:disable:next nesting
private enum CodingKeys: String, CodingKey {
case grantType = "grant_type"
case refreshToken = "refresh_token"
}
var grantType = "refresh_token"
let refreshToken: String
var contentForSignature: [(key: String, value: String?)] {
return [
(Self.CodingKeys.grantType.stringValue, self.grantType),
(Self.CodingKeys.refreshToken.stringValue, self.refreshToken)
]
}
}
}
extension TokenRevocationOperation {
struct Body: Encodable, HTTPRequestBody {
// swiftlint:disable:next nesting
private enum CodingKeys: String, CodingKey {
case token = "token"
case tokenTypeHint = "token_type_hint"
}
let token: String
let tokenTypeHint: String
var contentForSignature: [(key: String, value: String?)] {
return [
(Self.CodingKeys.token.stringValue, self.token),
(Self.CodingKeys.tokenTypeHint.stringValue, self.tokenTypeHint)
]
}
}
}