Skip to content

Instantly share code, notes, and snippets.

@vakhidbetrakhmadov
Last active August 19, 2019 10:14
Show Gist options
  • Save vakhidbetrakhmadov/ed9d06261c1276db8fce25f197c47bbd to your computer and use it in GitHub Desktop.
Save vakhidbetrakhmadov/ed9d06261c1276db8fce25f197c47bbd to your computer and use it in GitHub Desktop.
Utility class build on top of p2/OAuth2 to simplify user login/logout and access token refreshing.
import Foundation
import p2_OAuth2
protocol AuthManagerProtocol {
var userLoggedIn: Bool { get }
func login(username: String, password: String, completion: @escaping Completion<Void>)
func sign(request: URLRequest, completion: @escaping Completion<URLRequest>)
func logout()
}
class AuthManager: AuthManagerProtocol {
// MARK: - Inits & deinit
init(endpoint: URL, clientId: String, clientSecret: String, logLevel: OAuth2LogLevel, isMocking: Bool = false) {
self.endpoint = endpoint
self.clientId = clientId
self.clientSecret = clientSecret
self.logLevel = logLevel
self.isMocking = isMocking
if isMocking { Log.warning("AuthManager is mocking") }
}
// MARK: - Properties
var userLoggedIn: Bool {
guard !isMocking else { return mockLoginStatus }
return oauth.hasUnexpiredAccessToken() || oauth.refreshToken != nil
}
let endpoint: URL
let clientId: String
let clientSecret: String
let logLevel: OAuth2LogLevel
let isMocking: Bool
private lazy var oauth: OAuth2 = OAuth2PasswordGrant(settings: oauth2PasswordGrantSettings)
private lazy var oauth2PasswordGrantSettings: OAuth2JSON = [
"grant_type": "password",
"client_id": clientId,
"client_secret": clientSecret,
"token_uri": endpoint.absoluteString,
"secret_in_body": true,
"keychain": true
]
/// A serial `OperationQueue` to lock access to `completions` property.
private let serialAccessQueue: OperationQueue = {
let serialAccessQueue = OperationQueue()
serialAccessQueue.maxConcurrentOperationCount = 1
return serialAccessQueue
}()
private var completions: [UUID : (URLRequest, Completion<URLRequest>)] = [:]
private var mockLoginStatus = false
// MARK: - Methods
func login(username: String, password: String, completion: @escaping Completion<Void>) {
guard !isMocking else {
mockLoginStatus = true
return completion(.value(()))
}
let settings = oauth2PasswordGrantSettings.merging([
"username": username,
"password": password
], uniquingKeysWith: { _, second in second })
oauth = OAuth2PasswordGrant(settings: settings)
oauth.logger = OAuth2DebugLogger(logLevel)
authorize { result in
completion(result.map { _ in () })
}
}
func logout() {
guard !isMocking else { return (mockLoginStatus = false) }
oauth.forgetTokens()
oauth.forgetClient()
}
func sign(request: URLRequest, completion: @escaping Completion<URLRequest>) {
serialAccessQueue.addOperation { [weak self] in
let identifier = UUID()
self?.completions[identifier] = (request, completion)
self?.sign(for: identifier)
}
}
private func sign(for identifier: UUID) {
func invokeCompletionHandler(for identifier: UUID, sign: (URLRequest) -> Result<URLRequest>) {
guard let completion = completions.removeValue(forKey: identifier) else { return }
completion.1(sign(completion.0))
}
guard userLoggedIn else { return invokeCompletionHandler(for: identifier, sign: { _ in .error(Error.notLoggedIn) }) }
func sign(request: URLRequest) -> Result<URLRequest> {
do {
return .value(try request.signed(with: oauth))
} catch {
return .error(error)
}
}
guard !oauth.isAuthorizing else { return }
guard !oauth.hasUnexpiredAccessToken() else { return invokeCompletionHandler(for: identifier, sign: sign) }
authorize { [weak self] result in
guard let `self` = self else { return }
switch result {
case .value:
self.serialAccessQueue.addOperation {
self.completions.keys.forEach { invokeCompletionHandler(for: $0, sign: sign) }
}
case .error(let error):
self.serialAccessQueue.addOperation {
self.completions.keys.forEach { invokeCompletionHandler(for: $0, sign: { _ in .error(error) }) }
}
}
}
}
private func authorize(params: OAuth2StringDict? = nil, completion: @escaping Completion<OAuth2JSON>) {
oauth.authorize(params: params) { (response: OAuth2JSON?, error: OAuth2Error?) in
if let error = error {
return completion(.error(error))
}
completion(.value(response!))
}
}
}
extension AuthManager {
enum Error: Swift.Error, LocalizedError {
case notLoggedIn
var errorDescription: String? {
return "Not logged in"
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment