Last active
August 19, 2019 10:14
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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