Moya: Refresh Token and retry old request

Created on 19 Jul 2017  ·  15Comments  ·  Source: Moya/Moya

Hello, i want to implement refresh token with Moya and RxSwift but i don't solve my issue

but i don't understand, when my response is 401, i want to make another request ( refresh token ) and after that i want to retry my old request but i don't understand how to do that, someone can help me

i implement this code in this issue https://github.com/Moya/Moya/issues/744 but i don't know how and when i make my refresh request before to execut my old request

question

Most helpful comment

@rlam3
I dont know what's your scenario, so I will say my first:
I don't know when access token will be expired, backend judge it ,so when some network request found that access token is expired, I should request backend to refresh access token. and if other request found access token is expired at the same time, they should wait, and all request should retry when the new access token come back. the refresh token request should be only invoked once.
here is my solution :

```swift
static func request(target: API) -> Observable {
return self.provider.request(target).retry(1)
.observeOn(ConcurrentDispatchQueueScheduler.init(qos: .default))
.flatMap { (response) -> Observable in

            if (the access token expired) {
                throw TokenError.TokenExpired
            } else{
                return Observable.just(response)
            }

        }.retryWhen({ (error: Observable<TokenError>)  in
            error.flatMap{ error -> Observable<()> in
                switch error {
                case .TokenExpired:

                    return RefreshTokenObservable.shareReplay(1).flatMap({ (result) -> Observable<()> in
                        switch result {

                        case .RefreshSuccess:
                            return Observable.just("").asObservable().map{_ in return () }

                        case .RefreshFailure:
                            DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: {
                                // here is logout 
                            })
                            throw error
                        }

                    })
                }
            }
        })

```
I don't know if this is all right for you, but works for me. and I know this is not the elegant code, anyone can review?
@pietbrauer @sunshinejr @ashfurrow 🙏

All 15 comments

Hey @mrachid. This is not the easiest thing to do, so I understand your pain.

Could you please share what have you tried? The best would be code that you wrote with comments around places you had problems with. This way we could figure out the easiest way to help you in your specific scenario (configuration, abstraction layers etc.)

Thanks for your help @sunshinejr !!
MY CONTROLLER

//Get User info action button

    @IBAction func requestUser(_ sender: UIButton) {
        getUser().subscribe(onNext: { (user) in
            print(user)
        }, onError: { (error) in
            print("ERROR SECOND TRY ", error)
        }, onCompleted: { 
            print("Completed")
        }) { 
            print("Disposed")
        }.addDisposableTo(disposeBag)
    }


//Get user with fail token for simulate error request for to do my refresh token request and retry my old request

    func getUser() -> Observable<User?>  {
        let providerUser = myRxProvider<UserEndPoints>(plugins: [AccessTokenPlugin(token: "FailToken")])

        //I don't understand why i need to implemente the callback
        providerUser.authenticationBlock = { (_ done: () -> Void) -> Void in
            print("DONE")
            done()
        }

        let response = providerUser.request(.ReadCurrent()).mapObjectOptional(type: User.self)
        return response
    }

/* AUTHENTIFICATION PROVIDER #744 
I implemente the issue #744 i don't understand the callback first and how and where i need to make my refresh request for after execute my old request */

    public typealias AuthenticationBlock = (_ done: () -> Void) -> Void

    public enum Error: Swift.Error {
        case missingAuthenticationBlock
        case invalidCredentials
    }


    public class myRxProvider<Target>: RxMoyaProvider<Target> where Target: TargetType {

        private let disposeBag = DisposeBag()
        public var authenticationBlock: AuthenticationBlock?

        public override init(endpointClosure: @escaping EndpointClosure = MoyaProvider.defaultEndpointMapping,
                         requestClosure: @escaping RequestClosure = MoyaProvider.defaultRequestMapping,
                         stubClosure: @escaping StubClosure = MoyaProvider.neverStub,
                         manager: Manager = MoyaProvider<Target>.defaultAlamofireManager(),
                         plugins: [PluginType] = [],
                         trackInflights: Bool = false) {

            super.init(endpointClosure: endpointClosure, requestClosure: requestClosure, stubClosure: stubClosure, manager: manager, plugins: plugins, trackInflights: trackInflights)
        }

        public override func request(_ token: Target) -> Observable<Response> {
            return _request(token)
        }

        private func _request(_ token: Target, isSecondTryAfterAuth: Bool = false) -> Observable<Response> {
            return super.request(token)
                .flatMap { [unowned self] response -> Observable<Response> in

                    //check the response status code if error make a refresh token
                    if response.statusCode == 401 || response.statusCode == 403 {

                        //Check if me second try fail return error and move to login view
                        if isSecondTryAfterAuth {
                            return Observable<Response>.error(Error.invalidCredentials)
                        }

                        //Need to make a request for resfresh my token and after i have my response,
                        //change my header authorisation token to my old request and execute old request
                        self.refreshTokenAction().subscribe(onNext: { (refreshToken) in
                            print(resfreshToken)
                            //Here after i have my new token, change header autorisation and add new token
                            //into the header
                            //try the old request with new header
                        }, onError: { (error) in
                            print("ERROR REFRESH", error)
                        }, onCompleted: {
                            print("completed")
                        }, onDisposed: {
                            print("disposed")
                        })

                        //I don't understand the callback ...
                        guard let authenticationBlock = self.authenticationBlock else {
                            throw Error.missingAuthenticationBlock
                        }

                        //i think is here where my old request is execute
                        return Observable.create { observer in
                            authenticationBlock {
                                self._request(token, isSecondTryAfterAuth: true)
                                    .subscribe { event in
                                        switch event {
                                        case .next(let element):
                                            print("test element", element)
                                        case .error(let error):
                                            observer.on(event)
                                        case .completed:
                                            print("test completed")
                                        }
                                    }.addDisposableTo(self.disposeBag)
                            }
                            return Disposables.create()
                        }
                    } else {
                        return Observable.just(response)
                    }
            }
        }


        //Refresh token request
        //We suppose the params is valid
        func refreshTokenAction() -> Observable<Token?> {
            let providerToken = myRxProvider<TokenEndPoints>()
            providerToken.authenticationBlock = { (_ done: () -> Void) -> Void in
                print("TOKEN IS REFRESH")
            }

            let response = providerToken.request(.Update(refreshToken: "Riox8RWVm8lmxxxMaMLF4PMz65MMZp4_oaq5_sjEK8c=")).mapObjectOptional(type: Token.self)
            return response

        }
    }

I hope you can help me, if you don't understand something, i can tell you more for understand

@sunshinejr or someone else, if you have any exemple with refresh token and retry old request after the refresh you can show me thanks you so much

@mrachid this conversation may help https://github.com/Moya/Moya/issues/748#issuecomment-260059360

@AndrewSB
I read the issue but i don't understand all, if you have any idea for help me in my code, this will be great!!
i take look to #748 i need more time for understand, i'm just begin with RxSwift And Moya

Well i don't solve my issue @sunshinejr @AndrewSB any idea?

@mrachid have you solve the issue at last?

@mrachid @asasdasasd any luck with this?

@rlam3 retrywhen operator can handle this

@asasdasasd do you have an example? Thanks!

I manage to fetch and refresh the token using the requestClosure from MoyaProvider.
See this link for code example

@rlam3
I dont know what's your scenario, so I will say my first:
I don't know when access token will be expired, backend judge it ,so when some network request found that access token is expired, I should request backend to refresh access token. and if other request found access token is expired at the same time, they should wait, and all request should retry when the new access token come back. the refresh token request should be only invoked once.
here is my solution :

```swift
static func request(target: API) -> Observable {
return self.provider.request(target).retry(1)
.observeOn(ConcurrentDispatchQueueScheduler.init(qos: .default))
.flatMap { (response) -> Observable in

            if (the access token expired) {
                throw TokenError.TokenExpired
            } else{
                return Observable.just(response)
            }

        }.retryWhen({ (error: Observable<TokenError>)  in
            error.flatMap{ error -> Observable<()> in
                switch error {
                case .TokenExpired:

                    return RefreshTokenObservable.shareReplay(1).flatMap({ (result) -> Observable<()> in
                        switch result {

                        case .RefreshSuccess:
                            return Observable.just("").asObservable().map{_ in return () }

                        case .RefreshFailure:
                            DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: {
                                // here is logout 
                            })
                            throw error
                        }

                    })
                }
            }
        })

```
I don't know if this is all right for you, but works for me. and I know this is not the elegant code, anyone can review?
@pietbrauer @sunshinejr @ashfurrow 🙏

public class myRxMoyaProvider<Target>: RxMoyaProvider<Target> where Target: TargetType {

    private let disposeBag = DisposeBag()
    private var refreshToken = ""
    private var authenticationBlock = { (_ done: () -> Void) -> Void in
        print("Execute refresh and after retry")
        done()
    }

    public override init(endpointClosure: @escaping EndpointClosure = MoyaProvider.defaultEndpointMapping,
                         requestClosure: @escaping RequestClosure = MoyaProvider.defaultRequestMapping,
                         stubClosure: @escaping StubClosure = MoyaProvider.neverStub,
                         manager: Manager = MoyaProvider<Target>.defaultAlamofireManager(),
                         plugins: [PluginType] = [],
                         trackInflights: Bool = false) {

        if let dictionary = Locksmith.loadDataForUserAccount(userAccount: "myKeychain") {
            if let value = dictionary["refresh_token"] {
                refreshToken = value as! String
            }
        }

        super.init(endpointClosure: endpointClosure, requestClosure: requestClosure, stubClosure: stubClosure, manager: manager, plugins: plugins, trackInflights: trackInflights)
    }

    public override func request(_ token: Target) -> Observable<Response> {
        return _request(token)
    }

    private func _request(_ token: Target, isSecondTryAfterAuth: Bool = false) -> Observable<Response> {
        return super.request(token)
            .flatMap { [unowned self] response -> Observable<Response> in
                if response.statusCode == 401 || response.statusCode == 403 {
                    if isSecondTryAfterAuth {
                        try? Locksmith.deleteDataForUserAccount(userAccount: "myKeychain")
                        return Observable<Response>.error(Error.invalidCredentials)
                    }
                    if !self.refreshToken.isEmpty {
                        return Observable.create { observer in
                            self.authenticationBlock {
                                let providerToken = myRxMoyaProvider<TokenEndPoints>()
                                providerToken._request(.Update(refreshToken: self.refreshToken), isSecondTryAfterAuth: false).mapObjectOptional(type: Token.self)
                                    .bind(onNext: { (newToken) in
                                        do {
                                            let param = ["refresh_token": newToken?.refreshToken, "access_token": newToken?.accessToken, "scope": newToken?.scope, "token_type": newToken?.tokenType]
                                            try Locksmith.updateData(data: param, forUserAccount: "myKeychain")
                                        } catch let error {
                                            print("ERROR SAVE TOKEN INTO KEYCHAIN : ", error)
                                        }
                                        self._request(token, isSecondTryAfterAuth: true).subscribe{ event in
                                            observer.on(event)
                                        }
                                    })
                            }
                            return Disposables.create()
                        }
                    } else {
                        return Observable<Response>.error(Error.invalidCredentials)
                    }
                }
                else {
                    return Observable.just(response)
                }
            }.retry(1)
    }

I hope this code can help you, i don't know if is the best practice with moya and rx to or elegant code but it's resolve my issue
@rlam3

Hi there,
My teammates found one more solution to the problem of refreshing session token of Auth0 with RxSwift and Moya

We wrote it using pure RxSwift approach and we return a classic error in case of fail. Hope it will help!

@asasdasasd 请问这块到代码还有吗,万分感激🙏

Was this page helpful?
0 / 5 - 0 ratings