I am trying to implement my OAuth2 flow using Alamofire 5.0.0-beta.3. As i can see the documentation is still for Alamofire 4 as stated in the github page as well.
I am trying to make the Oauth2 handler following the documentation for Alamofire 4. As the class names are changed, I am completely lost while making it.
This is the code that i am following:
class OAuth2Handler: RequestAdapter, RequestRetrier {
private typealias RefreshCompletion = (_ succeeded: Bool, _ accessToken: String?, _ refreshToken: String?) -> Void
private let sessionManager: SessionManager = {
let configuration = URLSessionConfiguration.default
configuration.httpAdditionalHeaders = SessionManager.defaultHTTPHeaders
return SessionManager(configuration: configuration)
}()
private let lock = NSLock()
private var clientID: String
private var baseURLString: String
private var accessToken: String
private var refreshToken: String
private var isRefreshing = false
private var requestsToRetry: [RequestRetryCompletion] = []
// MARK: - Initialization
public init(clientID: String, baseURLString: String, accessToken: String, refreshToken: String) {
self.clientID = clientID
self.baseURLString = baseURLString
self.accessToken = accessToken
self.refreshToken = refreshToken
}
// MARK: - RequestAdapter
func adapt(_ urlRequest: URLRequest) throws -> URLRequest {
if let urlString = urlRequest.url?.absoluteString, urlString.hasPrefix(baseURLString) {
var urlRequest = urlRequest
urlRequest.setValue("Bearer " + accessToken, forHTTPHeaderField: "Authorization")
return urlRequest
}
return urlRequest
}
// MARK: - RequestRetrier
func should(_ manager: SessionManager, retry request: Request, with error: Error, completion: #escaping RequestRetryCompletion) {
lock.lock() ; defer { lock.unlock() }
if let response = request.task?.response as? HTTPURLResponse, response.statusCode == 401 {
requestsToRetry.append(completion)
if !isRefreshing {
refreshTokens { [weak self] succeeded, accessToken, refreshToken in
guard let strongSelf = self else { return }
strongSelf.lock.lock() ; defer { strongSelf.lock.unlock() }
if let accessToken = accessToken, let refreshToken = refreshToken {
strongSelf.accessToken = accessToken
strongSelf.refreshToken = refreshToken
}
strongSelf.requestsToRetry.forEach { $0(succeeded, 0.0) }
strongSelf.requestsToRetry.removeAll()
}
}
} else {
completion(false, 0.0)
}
}
// MARK: - Private - Refresh Tokens
private func refreshTokens(completion: #escaping RefreshCompletion) {
guard !isRefreshing else { return }
isRefreshing = true
let urlString = "\(baseURLString)/oauth2/token"
let parameters: [String: Any] = [
"access_token": accessToken,
"refresh_token": refreshToken,
"client_id": clientID,
"grant_type": "refresh_token"
]
sessionManager.request(urlString, method: .post, parameters: parameters, encoding: JSONEncoding.default)
.responseJSON { [weak self] response in
guard let strongSelf = self else { return }
if
let json = response.result.value as? [String: Any],
let accessToken = json["access_token"] as? String,
let refreshToken = json["refresh_token"] as? String
{
completion(true, accessToken, refreshToken)
} else {
completion(false, nil, nil)
}
strongSelf.isRefreshing = false
}
}
}
This is how to use this for alamofire 4:
let baseURLString = "https://some.domain-behind-oauth2.com"
let oauthHandler = OAuth2Handler(
clientID: "12345678",
baseURLString: baseURLString,
accessToken: "abcd1234",
refreshToken: "ef56789a"
)
let sessionManager = SessionManager()
sessionManager.adapter = oauthHandler
sessionManager.retrier = oauthHandler
let urlString = "\(baseURLString)/some/endpoint"
sessionManager.request(urlString).validate().responseJSON { response in
debugPrint(response)
}
This is the link i am following to implement this.
https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md#adapting-and-retrying-requests
Look at something like this.
struct EnvironmentInterceptor: RequestInterceptor {
func adapt(_ urlRequest: URLRequest, for session: Session, completion: #escaping (Result<URLRequest>) -> Void) {
var adaptedRequest = urlRequest
guard let token = AtraqService.shared.user?.token.accessToken else {
completion(.success(adaptedRequest))
return
}
adaptedRequest.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
completion(.success(adaptedRequest))
}
func retry(_ request: Request, for session: Session, dueTo error: Error, completion: #escaping (RetryResult) -> Void) {
if let response = request.task?.response as? HTTPURLResponse, response.statusCode == 401 {
//get token
}
}
}
Then
Session(configuration: configuration, interceptor: EnvironmentInterceptor())
Finally
request().validate().response...
If Alamofire 5 is not intercepting (adapting or retrying) your requests for some reason, then just try to check the delegate signature out as described here.
func adapt(_ urlRequest: URLRequest, for session: Session, completion: #escaping (AFResult<URLRequest>) -> Void) {
var modifiedURLRequest = urlRequest
let apiToken = config.apiToken
modifiedURLRequest.setValue(apiToken, forHTTPHeaderField: Constants.apiTokenHeader)
completion(.success(modifiedURLRequest))
}
func retry(_ request: Request, for session: Session, dueTo error: Error, completion: #escaping (RetryResult) -> Void) {
completion(.doNotRetry)
}
Here's the difference (look at the signature):
func adapt(_ urlRequest: URLRequest, for session: Alamofire.Session, completion: #escaping (AFResult<URLRequest>) -> Void) {
var modifiedURLRequest = urlRequest
let apiToken = config.apiToken
modifiedURLRequest.setValue(apiToken, forHTTPHeaderField: Constants.apiTokenHeader)
completion(.success(modifiedURLRequest))
}
func retry(_ request: Alamofire.Request, for session: Alamofire.Session, dueTo error: Error, completion: #escaping (RetryResult) -> Void) {
completion(.doNotRetry)
}
Related
Here I have 3 files loginView(SwiftUI file) for UI purpose, LoginViewModel for handling the logic, ServiceManager for handling the Network call
Below code is in loginView(SwiftUI file)
Button("Login") {
loginVM.loginWebserviceCall()
}
Below code is in loginVM class
protocol LoginViewModelService: AnyObject {
func getLoginWebServiceCall(url: URL, params: [String: Any], completion: #escaping (Result<LoginRequest, APIError>) -> ())
}
class LoginViewModel: ObservableObject {
private weak var movieService: LoginViewModelService?
#Published var error: NSError?
init(movieService: LoginViewModelService = LoginStore.shared) {
self.movieService = movieService
}
fileprivate func loginWebserviceCall() {
let loginParams = ["username": "userEnteredUserName", "password": "userEnteredPassword", "request_token": "token"]
self.movieService!.getLoginWebServiceCall(url: "API_URL",
params: loginParams) { [weak self] (result) in
guard let self = self else { return }
switch result {
case .success(let response):
print(response)
case .failure(let error):
self.error = error as NSError
}
}
}
}
class LoginStore: LoginViewModelService {
static let shared = LoginStore()
private init() {}
func getLoginWebServiceCall(url: URL, params: [String: Any], completion: #escaping (Result<LoginRequest, APIError>) -> ()) {
ServiceManager.shared().requestWebServiceCall(requestType: .POST, url: url, params: params, completion: completion)
}
}
Below code is in ServiceManager class
class ServiceManager: NSObject {
private static var manager: ServiceManager? = nil
static func shared() -> ServiceManager {
if manager == nil {
manager = ServiceManager()
}
return manager!
}
func requestWebServiceCall<Response: Decodable>(requestType: HTTPMethod,
url: URL, params: [String: Any]? = nil,
completion: #escaping (Result<Response, APIError>) -> ()) {
var urlRequest = URLRequest.init(url: url)
if let params = params {
let postData = try? JSONSerialization.data(withJSONObject: params, options: .init(rawValue: 0))
urlRequest.httpBody = postData
}
urlRequest.httpMethod = requestType.rawValue
urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
URLSession.shared.dataTask(with: urlRequest) { [weak self] (data, response, error) in
guard let self = self else { return }
guard let data = data else {
self.executeCompletionHandlerInMainThread(with: .failure(.noData), completion: completion)
return
}
do {
if let str = String(data: data, encoding: .utf8) {
print(str)
// Output: {"success":true,"expires_at":"2022-06-23 08:56:52 UTC","request_token":"6587563498567begjhgf3r5387853"}
}
let decodedResponse = try JSONDecoder().decode(Response.self, from: data)
self.executeCompletionHandlerInMainThread(with: .success(decodedResponse), completion: completion)
} catch let DecodingError.keyNotFound(key, context) {
print("Key '\(key)' not found:", context.debugDescription)
print("codingPath:", context.codingPath)
} catch {
print(error)
}
}.resume()
}
private func executeCompletionHandlerInMainThread<Response: Decodable>(with result: Result<Response, APIError>,
completion: #escaping (Result<Response, APIError>) -> ()) {
DispatchQueue.main.async {
completion(result)
}
}
}
Below is the JSON we are expecting as response
{
"success": true,
"expires_at": "2018-07-24 04:10:26 UTC",
"request_token": "1531f1a558c8357ce8990cf887ff196e8f5402ec"
}
But once I get response, the decoding is getting failed and it is going inside catch block(in ServiceManager class) and it print's below error.
Key 'CodingKeys(stringValue: "username", intValue: nil)' not found: No value associated with key CodingKeys(stringValue: "username", intValue: nil) ("username").
codingPath: []
It is showing username as not found. But in my API response, I don't have username at all.
But I am passing username as httpBody to this API.
What could be the reason? Why it is throwing error?
In my App multiple Apis . i have 2 differnet server one is for HTTP and one for HTTPS
When i Run my App on Http : it works fine 1st time for each Api and 2 second time for each Api same response time .
But when i run App on https : for each APi first time taking extra time for each Api , then if i hit same Api again it is fast . Problem is why for first time for each APi is slow or taking extra time . But thing is not happened with Android App .
here is My Request builder Class : URLRequestBuilder.swift
import Foundation
import Alamofire
protocol URLRequestBuilder: URLRequestConvertible, APIRequestHandler {
var mainURL: URL { get }
var requestURL: URL { get }
var path: String { get }
var parameters: Parameters? { get }
var method: HTTPMethod { get }
var encoding: ParameterEncoding { get }
var urlRequest: URLRequest { get }
}
extension URLRequestBuilder {
var encoding: ParameterEncoding {
switch method {
case .get:
return URLEncoding.default
default:
return JSONEncoding.default
}
}
var mainURL: URL {
return URL(string: SERVER_URL)!
}
var requestURL: URL {
var fullURL = mainURL.absoluteString + path
if L102Language.currentAppleLanguage() == "ar" && path.contains("?") {
fullURL = fullURL + "&blLocaleCode=ar"
} else if L102Language.currentAppleLanguage() == "ar" {
fullURL = fullURL + "?blLocaleCode=ar"
}
let urlComponents = URLComponents(string: fullURL)!
return urlComponents.url!
}
var urlRequest: URLRequest {
var request = URLRequest(url: requestURL)
request.httpMethod = method.rawValue
if UserDefaults.standard.isUserLoggedIn() {
request.setValue(UserDefaults.standard.getAccessToken(), forHTTPHeaderField: NETWORK_ACCESS_TOKEN)
}
request.setValue(NETWORK_REQUEST_TYPE, forHTTPHeaderField: NETWORK_ACCEPT)
request.setValue(NETWORK_REQUEST_TYPE, forHTTPHeaderField: NETWORK_CONTENT_TYPE)
//request.cachePolicy = .useProtocolCachePolicy
return request
}
func asURLRequest() throws -> URLRequest {
return try encoding.encode(urlRequest, with: parameters)
}
}
final class NetworkClient {
let evaluators = [
"somehttpsURL.com": ServerTrustPolicy.pinCertificates(
certificates: [Certificates.stackExchange],
validateCertificateChain: true,
validateHost: true)
]
let session: SessionManager
// 2
private init() {
session = SessionManager(
serverTrustPolicyManager: ServerTrustPolicyManager(policies: evaluators))
}
// MARK: - Static Definitions
private static let shared = NetworkClient()
static func request(_ convertible: URLRequestConvertible) -> DataRequest {
return shared.session.request(convertible)
}
}
struct Certificates {
static let stackExchange =
Certificates.certificate(filename: "certificate")
private static func certificate(filename: String) -> SecCertificate {
let filePath = Bundle.main.path(forResource: filename, ofType: "der")!
let data = try? Data(contentsOf: URL(fileURLWithPath: filePath))
let certificate = SecCertificateCreateWithData(nil, data! as CFData)!
return certificate
}
}
And my Request Handler Class is :
extension APIRequestHandler where Self : URLRequestBuilder {
// For Response Object
func send<T: AnyObject>(modelType: T.Type, data: [UIImage]? = nil, success: #escaping ( _ servicResponse: AnyObject) -> Void, fail: #escaping ( _ error: NSError) -> Void, showHUD: Bool) where T: Mappable {
if let data = data {
uploadToServerWith(modelType: modelType, images: data, request: self, parameters: self.parameters, success: success, fail: fail)
} else {
//print(requestURL.absoluteString)
// NetworkClient.
request(self).authenticate(user: APIAuthencationUserName, password: APIAuthencationPassword).validate().responseObject { (response: DataResponse<T>) in
switch response.result {
case .success(let objectData):
success(objectData)
break
case .failure(let error):
print(error.localizedDescription)
if error.localizedDescription == RefreshTokenFailed {
self.getAccessTokenAPI(completion: { (value) in
if value == TOKEN_SAVED {
self.send(modelType: modelType, success: success, fail: fail, showHUD: showHUD)
return
}else {
fail(error as NSError)
}
})
} else {
fail(error as NSError)
}
}
}
}
}
}
There is a function getUser in RequestManager class that called in my VC.
func getUser(onCompletion: #escaping (_ result: User?, error: String?) -> Void) {
Alamofire.request(Router.getUser).responseJSON { (response) in
// here is the work with response
}
}
If this request returns 403 it means access_token is expired. I need to refresh token and repeat the request from my VC.
Now the question.
How to refresh token and repeat the request in the right way?
To handle the error and refresh token in MyViewController or getUser method is not good idea because I have a lot of VCs and request methods.
I need something like: VC calls the method and gets the User even if token is expired and refreshToken must not be in all request methods.
EDIT
refreshToken method
func refreshToken(onCompletion: #escaping (_ result: Bool?) -> Void) {
Alamofire.request(Router.refreshToken).responseJSON { (response) in
print(response)
if response.response?.statusCode == 200 {
guard let data = response.data else { return onCompletion(false) }
let token = try? JSONDecoder().decode(Token.self, from: data)
token?.setToken()
onCompletion(true)
} else {
onCompletion(false)
}
}
}
To solve this, I created a class from which we will call every API, say BaseService.swift.
BaseService.swift :
import Foundation
import Alamofire
import iComponents
struct AlamofireRequestModal {
var method: Alamofire.HTTPMethod
var path: String
var parameters: [String: AnyObject]?
var encoding: ParameterEncoding
var headers: [String: String]?
init() {
method = .get
path = ""
parameters = nil
encoding = JSONEncoding() as ParameterEncoding
headers = ["Content-Type": "application/json",
"X-Requested-With": "XMLHttpRequest",
"Cache-Control": "no-cache"]
}
}
class BaseService: NSObject {
func callWebServiceAlamofire(_ alamoReq: AlamofireRequestModal, success: #escaping ((_ responseObject: AnyObject?) -> Void), failure: #escaping ((_ error: NSError?) -> Void)) {
// Create alamofire request
// "alamoReq" is overridden in services, which will create a request here
let req = Alamofire.request(alamoReq.path, method: alamoReq.method, parameters: alamoReq.parameters, encoding: alamoReq.encoding, headers: alamoReq.headers)
// Call response handler method of alamofire
req.validate(statusCode: 200..<600).responseJSON(completionHandler: { response in
let statusCode = response.response?.statusCode
switch response.result {
case .success(let data):
if statusCode == 200 {
Logs.DLog(object: "\n Success: \(response)")
success(data as AnyObject?)
} else if statusCode == 403 {
// Access token expire
self.requestForGetNewAccessToken(alaomReq: alamoReq, success: success, failure: failure)
} else {
let errorDict: [String: Any] = ((data as? NSDictionary)! as? [String: Any])!
Logs.DLog(object: "\n \(errorDict)")
failure(errorTemp as NSError?)
}
case .failure(let error):
Logs.DLog(object: "\n Failure: \(error.localizedDescription)")
failure(error as NSError?)
}
})
}
}
extension BaseService {
func getAccessToken() -> String {
if let accessToken = UserDefaults.standard.value(forKey: UserDefault.userAccessToken) as? String {
return "Bearer " + accessToken
} else {
return ""
}
}
// MARK: - API CALL
func requestForGetNewAccessToken(alaomReq: AlamofireRequestModal, success: #escaping ((_ responseObject: AnyObject?) -> Void), failure: #escaping ((_ error: NSError?) -> Void) ) {
UserModal().getAccessToken(success: { (responseObj) in
if let accessToken = responseObj?.value(forKey: "accessToken") {
UserDefaults.standard.set(accessToken, forKey: UserDefault.userAccessToken)
}
// override existing alaomReq (updating token in header)
var request: AlamofireRequestModal = alaomReq
request.headers = ["Content-Type": "application/json",
"X-Requested-With": "XMLHttpRequest",
"Cache-Control": "no-cache",
"X-Authorization": self.getAccessToken()]
self.callWebServiceAlamofire(request, success: success, failure: failure)
}, failure: { (_) in
self.requestForGetNewAccessToken(alaomReq: alaomReq, success: success, failure: failure)
})
}
}
For calling the API from this call, we need to create a object of AlamofireRequestModal and override it with necessary parameter.
For example I created a file APIService.swift in which we have a method for getUserProfileData.
APIService.swift :
import Foundation
let GET_USER_PROFILE_METHOD = "user/profile"
struct BaseURL {
// Local Server
static let urlString: String = "http://192.168.10.236: 8084/"
// QAT Server
// static let urlString: String = "http://192.171.286.74: 8080/"
static let staging: String = BaseURL.urlString + "api/v1/"
}
class APIService: BaseService {
func getUserProfile(success: #escaping ((_ responseObject: AnyObject?) -> Void), failure: #escaping ((_ error: NSError?) -> Void)) {
var request: AlamofireRequestModal = AlamofireRequestModal()
request.method = .get
request.path = BaseURL.staging + GET_USER_PROFILE_METHOD
request.headers = ["Content-Type": "application/json",
"X-Requested-With": "XMLHttpRequest",
"Cache-Control": "no-cache",
"X-Authorization": getAccessToken()]
self.callWebServiceAlamofire(request, success: success, failure: failure)
}
}
Explanation:
In code block:
else if statusCode == 403 {
// Access token expire
self.requestForGetNewAccessToken(alaomReq: alamoReq, success: success, failure: failure)
}
I call getNewAccessToken API (say refresh-token, in your case), with the request( it could be any request based from APIService.swift).
When we get new token I save it user-defaults then I will update the request( the one I am getting as a parameter in refresh-token API call), and will pass the success and failure block as it is.
You can create generic refresher class:
protocol IRefresher {
associatedtype RefreshTarget: IRefreshing
var target: RefreshTarget? { get }
func launch(repeats: Bool, timeInterval: TimeInterval)
func invalidate()
}
class Refresher<T: IRefreshing>: IRefresher {
internal weak var target: T?
private var timer: Timer?
init(target: T?) {
self.target = target
}
public func launch(repeats: Bool, timeInterval: TimeInterval) {
timer = Timer.scheduledTimer(withTimeInterval: timeInterval, repeats: repeats) { [weak self] (timer) in
self?.target?.refresh()
}
}
public func invalidate() {
timer?.invalidate()
}
}
And the refresh target protocol:
protocol IRefreshing: class {
func refresh()
}
Define new typealias:
typealias RequestManagerRefresher = Refresher<RequestManager>
Now create refresher and store it:
class RequestManager {
let refresher: RequestManagerRefresher
init() {
refresher = Refresher(target: self)
refresher?.launch(repeats: true, timeInterval: 15*60)
}
}
And expand RequestManager:
extension RequestManager: IRefreshing {
func refresh() {
updateToken()
}
}
Every 15 minutes your RequestManager's token will be updated
UPDATE
Of course, you also can change the update time. Create a static var that storing update time you need. For example inside the RequestManager:
class RequestManager {
static var updateInterval: TimeInterval = 0
let refresher: RequestManagerRefresher
init() {
refresher = Refresher(target: self)
refresher?.launch(repeats: true, timeInterval: updateInterval)
}
}
So now you can ask the token provider server for token update interval and set this value to updateInterval static var:
backendTokenUpdateIntervalRequest() { interval in
RequestManager.updateInterval = interval
}
You can easily Refresh token and retry your previous API call using Alamofire
RequestInterceptor
NetworkManager.Swift:-
import Alamofire
class NetworkManager {
static let shared: NetworkManager = {
return NetworkManager()
}()
typealias completionHandler = ((Result<Data, CustomError>) -> Void)
var request: Alamofire.Request?
let retryLimit = 3
func request(_ url: String, method: HTTPMethod = .get, parameters: Parameters? = nil,
encoding: ParameterEncoding = URLEncoding.queryString, headers: HTTPHeaders? = nil,
interceptor: RequestInterceptor? = nil, completion: #escaping completionHandler) {
AF.request(url, method: method, parameters: parameters, encoding: encoding, headers: headers, interceptor: interceptor ?? self).validate().responseJSON { (response) in
if let data = response.data {
completion(.success(data))
} else {
completion(.failure())
}
}
}
}
RequestInterceptor.swift :-
import Alamofire
extension NetworkManager: RequestInterceptor {
func adapt(_ urlRequest: URLRequest, for session: Session, completion: #escaping (Result<URLRequest, Error>) -> Void) {
var request = urlRequest
guard let token = UserDefaultsManager.shared.getToken() else {
completion(.success(urlRequest))
return
}
let bearerToken = "Bearer \(token)"
request.setValue(bearerToken, forHTTPHeaderField: "Authorization")
print("\nadapted; token added to the header field is: \(bearerToken)\n")
completion(.success(request))
}
func retry(_ request: Request, for session: Session, dueTo error: Error,
completion: #escaping (RetryResult) -> Void) {
guard let statusCode = request.response?.statusCode else {
completion(.doNotRetry)
return
}
guard request.retryCount < retryLimit else {
completion(.doNotRetry)
return
}
print("retry statusCode....\(statusCode)")
switch statusCode {
case 200...299:
completion(.doNotRetry)
case 401:
refreshToken { isSuccess in isSuccess ? completion(.retry) : completion(.doNotRetry) }
break
default:
completion(.retry)
}
}
func refreshToken(completion: #escaping (_ isSuccess: Bool) -> Void) {
let params = [
"refresh_token": Helpers.getStringValueForKey(Constants.REFRESH_TOKEN)
]
AF.request(url, method: .post, parameters: params, encoding: JSONEncoding.default).responseJSON { response in
if let data = response.data, let token = (try? JSONSerialization.jsonObject(with: data, options: [])
as? [String: Any])?["access_token"] as? String {
UserDefaultsManager.shared.setToken(token: token)
print("\nRefresh token completed successfully. New token is: \(token)\n")
completion(true)
} else {
completion(false)
}
}
}
}
Alamofire v5 has a property named RequestInterceptor.
RequestInterceptor has two method, one is Adapt which assign
access_token to any Network call header, second one is Retry method.
In Retry method we can check response status code and call
refresh_token block to get new token and retry previous API again.
I was able to get a working implementation of OAuth example as provided by AlamoFire. However, I am looking to understand certain lines of code and how it works.
Full Example:
class OAuth2Handler: RequestAdapter, RequestRetrier {
private typealias RefreshCompletion = (_ succeeded: Bool, _ accessToken: String?, _ refreshToken: String?) -> Void
private let sessionManager: SessionManager = {
let configuration = URLSessionConfiguration.default
configuration.httpAdditionalHeaders = SessionManager.defaultHTTPHeaders
return SessionManager(configuration: configuration)
}()
private let lock = NSLock()
private var clientID: String
private var baseURLString: String
private var accessToken: String
private var refreshToken: String
private var isRefreshing = false
private var requestsToRetry: [RequestRetryCompletion] = []
// MARK: - Initialization
public init(clientID: String, baseURLString: String, accessToken: String, refreshToken: String) {
self.clientID = clientID
self.baseURLString = baseURLString
self.accessToken = accessToken
self.refreshToken = refreshToken
}
// MARK: - RequestAdapter
func adapt(_ urlRequest: URLRequest) throws -> URLRequest {
if let urlString = urlRequest.url?.absoluteString, urlString.hasPrefix(baseURLString) {
var urlRequest = urlRequest
urlRequest.setValue("Bearer " + accessToken, forHTTPHeaderField: "Authorization")
return urlRequest
}
return urlRequest
}
// MARK: - RequestRetrier
func should(_ manager: SessionManager, retry request: Request, with error: Error, completion: #escaping RequestRetryCompletion) {
lock.lock() ; defer { lock.unlock() }
if let response = request.task?.response as? HTTPURLResponse, response.statusCode == 401 {
requestsToRetry.append(completion)
if !isRefreshing {
refreshTokens { [weak self] succeeded, accessToken, refreshToken in
guard let strongSelf = self else { return }
strongSelf.lock.lock() ; defer { strongSelf.lock.unlock() }
if let accessToken = accessToken, let refreshToken = refreshToken {
strongSelf.accessToken = accessToken
strongSelf.refreshToken = refreshToken
}
strongSelf.requestsToRetry.forEach { $0(succeeded, 0.0) }
strongSelf.requestsToRetry.removeAll()
}
}
} else {
completion(false, 0.0)
}
}
// MARK: - Private - Refresh Tokens
private func refreshTokens(completion: #escaping RefreshCompletion) {
guard !isRefreshing else { return }
isRefreshing = true
let urlString = "\(baseURLString)/oauth2/token"
let parameters: [String: Any] = [
"access_token": accessToken,
"refresh_token": refreshToken,
"client_id": clientID,
"grant_type": "refresh_token"
]
sessionManager.request(urlString, method: .post, parameters: parameters, encoding: JSONEncoding.default)
.responseJSON { [weak self] response in
guard let strongSelf = self else { return }
if
let json = response.result.value as? [String: Any],
let accessToken = json["access_token"] as? String,
let refreshToken = json["refresh_token"] as? String
{
completion(true, accessToken, refreshToken)
} else {
completion(false, nil, nil)
}
strongSelf.isRefreshing = false
}
}
}
Questions:
[weak self] succeeded, accessToken, refreshToken in
guard let strongSelf = self else { return }
What is the purpose of [weak self] and the guard for strongSelf?
requestsToRetry.append(completion)
if !isRefreshing {
refreshTokens { [weak self] succeeded, accessToken, refreshToken in
guard let strongSelf = self else { return }
//Implementation
strongSelf.requestsToRetry.forEach { $0(succeeded, 0.0) }
strongSelf.requestsToRetry.removeAll()
}
}
How does this request retry work? The requestsToRetry is just an array of RequestRetryCompletion = (_ shouldRetry: Bool, _ timeDelay: TimeInterval) How does it know what requests to retry?
strongSelf.lock.lock()
Does NSLock just not allow self (OAuth2Handler) to be accessed by any other thread while this method is executing?
1) Exactly as commented by Fonix, you have a strong reference to selfto avoid that if self was nil you start to collect retain cycles..
I'm refeer to :
[weak self] ... in
guard let strongSelf = self else { return }
Since self will be captured in the block which is dispatched asynchronously, self will be implicitly retained and released again when the block has been finished, in other words self will be extended up until after the block finishes. Making this code, you avoid to extend the life-time of self and decide to don't execute the block if self is equal to nil
2) According to the lines you mentioned:
if let response = request.task?.response as? HTTPURLResponse, response.statusCode == 401 {
requestsToRetry.append(completion)
..
there is an array called requestsToRetry which contains all the request you need to relaunch. In this code you append to the array all the request that have the 401 status code (when server returns status code 401)
With the code forEach you loop through the requestToRetry array:
strongSelf.requestsToRetry.forEach { $0(succeeded, 0.0) }
strongSelf.requestsToRetry.removeAll()
and launch all items. After the cycle is concluded you remove all the items.
In fact, the sources report:
public typealias RequestRetryCompletion = (_ shouldRetry: Bool, _ timeDelay: TimeInterval) -> Void
public protocol RequestRetrier {
func should(_ manager: SessionManager, retry request: Request, with error: Error, completion: #escaping RequestRetryCompletion)
}
You can find more details here
3) Exactly as you said the frequently concurrency issues faced are the one related to accessing/modifying the shared resource from different threads.
The lock.lock() is a solution to lock the other execution blocks when items is being modified. The code in defer is called just before leaving the function to unlocking the block.
I have this class here and inside the class is a method and I am trying to do an NSURLSession on an API that requires windows authentication username and password. I have followed the tutorial here https://gist.github.com/n8armstrong/5c5c828f1b82b0315e24
and came up with this:
let webservice = "https://api.com"
let config = NSURLSessionConfiguration.defaultSessionConfiguration()
let urlSession = NSURLSession(configuration: config)
class WebService: NSObject {
func loginUser(username: String, password: String) -> Bool {
let userPasswordString = "username#domain.com:Password"
let userPasswordData = userPasswordString.dataUsingEncoding(NSUTF8StringEncoding)
let base64EncodedCredential = userPasswordData!.base64EncodedStringWithOptions([])
let authString = "Basic \(base64EncodedCredential)"
config.HTTPAdditionalHeaders = ["Authorization" : authString]
let requestString = NSString(format:"%#", webservice) as String
let url: NSURL! = NSURL(string: requestString)
let task = urlSession.dataTaskWithURL(url) {
(let data, let response, let error) in
if (response as? NSHTTPURLResponse) != nil {
let dataString = NSString(data: data!, encoding: NSUTF8StringEncoding)
print(dataString)
}
}
task.resume()
return true
}
}
but when I run this I get a 401 error: 401 - Unauthorized: Access is denied due to invalid credentials.
I have confirmed the URL to the API is correct. Same with the username and password. What am I doing wrong?
I was able to fix this by doing the following:
var credential: NSURLCredential!
func loginUser(username: String, password: String) -> Bool {
let configuration = NSURLSessionConfiguration.defaultSessionConfiguration()
let session = NSURLSession(configuration: configuration, delegate: self, delegateQueue: nil)
credential = NSURLCredential(user:username, password:password, persistence: .ForSession)
let requestString = NSString(format:"%#", webservice) as String
let url: NSURL! = NSURL(string: requestString)
let task = session.dataTaskWithURL(url, completionHandler: {
data, response, error in
dispatch_async(dispatch_get_main_queue(),
{
if(error == nil)
{
print("Yay!")
}
else
{
print("Naw!")
}
})
})
task.resume()
return true
}
and then adding NSURLSessionDelegate methods:
func URLSession(session: NSURLSession, didReceiveChallenge challenge: NSURLAuthenticationChallenge, completionHandler: (NSURLSessionAuthChallengeDisposition, NSURLCredential?) -> Void) {
if challenge.previousFailureCount > 0
{
completionHandler(NSURLSessionAuthChallengeDisposition.CancelAuthenticationChallenge, nil)
}
else
{
completionHandler(NSURLSessionAuthChallengeDisposition.UseCredential, NSURLCredential(forTrust:challenge.protectionSpace.serverTrust!))
}
}
func URLSession(session: NSURLSession, task: NSURLSessionTask, didReceiveChallenge challenge: NSURLAuthenticationChallenge, completionHandler: (NSURLSessionAuthChallengeDisposition, NSURLCredential?) -> Void) {
completionHandler(NSURLSessionAuthChallengeDisposition.UseCredential,credential)
}