Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Understanding AlamoFire OAuth Example

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 }
  1. 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()
            }
        }
    
  2. 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()

  1. Does NSLock just not allow self (OAuth2Handler) to be accessed by any other thread while this method is executing?
like image 707
Alan Avatar asked Feb 06 '17 23:02

Alan


1 Answers

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.

like image 125
Alessandro Ornano Avatar answered Sep 22 '22 03:09

Alessandro Ornano