Richard
Richard

Reputation: 99

How to handle refreshing credentials using RxSwift

I have a problem with auth flow in my app. There is some API that I use for authorisation (how it works: I am sending request and it sets auth token cookies in HTTPCookieStorage.shared with some expire time (20 min)). If user is authenticated, I have authToken cookie in HTTPCookieStorage.shared, if not, HTTPCookieStorage.shared is empty.

Now I faced problem, how to refresh expired token. For example, if I don't use app for a hour, my token is expired and I can't use other api calls before getting new authToken.

For now I have following solution: I call auth request before every request and check if I have something in HTTPCookieStorage.shared If yes, then I return .just(my_auth_token), If no, I getting new token, by triggering request for auth.

But it have one huge flaw: if I have more than one request at app startup, there will be more than one auth request that can lead to unexpected behaviour of the auth token (like it can be immediately expired, because we already get new token from another auth request)

So question is: How to make other observables wait until there will be some authToken? I was thinking about skipUntil operator, but I think its not suitable either, because once it triggered, it pass all events, what came into observable. Its bad because once we refresh token, we can't refresh it second time.

Upvotes: 1

Views: 255

Answers (1)

Daniel T.
Daniel T.

Reputation: 33967

Here is an article I wrote that solves this problem: RxSwift and Handling Invalid Tokens

The "huge flaw" you mentioned is one of the use cases I accounted for:

  1. Since multiple requests could be unauthorized while the service is waiting for a new token, I need a way to notify all the requests once the token is provided.

The solution...

Because of requirement (3) I know that there will have to be some sort of object that can bind all the requests together and this object is primarally in charge of acquiring new tokens when needed. I’ll name it’s class TokenAcquisitionService. This object will also provide the most recent token on request.

Once you have a properly constructed TokenAcquisitionService object, you will be able to handle requests as follows:

/**
 Builds and makes network requests using the token provided by the 
     service. Will request a new token and retry if the result is an 
     unauthorized (401) error.
 - parameter response: A function that sends requests to the network 
     and emits responses. Can be for example 
     `URLSession.shared.rx.response`
 - parameter tokenAcquisitionService: The object responsible for 
     tracking the auth token. All requests should use the same 
     object.
 - parameter request: A function that can build the request when 
     given a token.
 - returns: response of a guaranteed authorized network request.
 **/
typealias Response = (URLRequst) -> Observable<(response: HTTPURLResponse, data: Data)>
typealias Request = (String) -> URLRequest
func getData(response: @escaping Response, tokenAcquisitionService: TokenAcquisitionService, request: @escaping Request) -> Observable<(response: HTTPURLResponse, data: Data)> {
    return Observable
        .deferred { tokenAcquisitionService.token.take(1) }
        .map { request($0) }
        .flatMap { response($0) }
        .map { response in
            guard response.response.statusCode != 401 else { throw ResponseError.unauthorized }
            return response
        }
        .retryWhen { $0.renewToken(with: tokenAcquisitionService) }
}

The TokenAcquisionService I built is provided in the article along with an analysis of how it works and a full test harness proving it works. It has been used in several projects that I know of.

Upvotes: 1

Related Questions