0

I'm trying to use Siesta swift package with my API on the server. We've setup a JWT authentication with Access and Refresh tokens. We can successfully authenticate and get new access tokens with refresh procedure. But the solution we've made looks a bit hacky me.

We are using request decorators like this

func init() {
       service.configure("**") {
            if let session = self.appSession {
                $0.headers["Authorization"] = "Bearer \(session.tokens.access)"
            }

            $0.decorateRequests {
                self.globalApiFailHandler(request: $1)
            }
        }

        service.configure(authRefreshResource) {
            if let session = self.appSession {
                $0.headers["Authorization-Refresh"] = "Bearer \(session.tokens.refresh)"
            }

            $0.decorateRequests {
                self.refreshTokenFailure(request: $1)
            }
        }
}

private func globalApiFailHandler(request: Siesta.Request) -> Request {

        return request.chained {                    //special case to Refresh Token On Auth Failure
            if case
                .failure(let error) = $0.response,  // Did request fail…
                error.httpStatusCode == 401,        // …because of expired token?
                self.appSession != nil {            // we have refreshToken

                log.warning("Seems like Access Token is expired, Trying to refresh it!")
                return .passTo(
                    self.refreshAuth().chained {                  // first request a new token, then:
                        if case .failure = $0.response {          // If token request failed…
                            return .useThisResponse               // …report that error.
                        } else {
                            return .passTo(request.repeated())    // We have a new token! Repeat the original request.
                        }
                    }
                )

            }

            if case
                .failure(let error) = $0.response,
                error.httpStatusCode != 409 {

                log.warning("Something went wrong during request: \(error)")
                self.retryLaterEvent() // TODO: Really need this here?
            }

            return .useThisResponse                // If not, use the response we got.
        }
    }

    private func refreshTokenFailure(request: Siesta.Request) -> Request {
        return request.chained {
            if case
                .failure(let error) = $0.response {  // Did request fail…

                log.error("Refresh token procedure failed with \(error).")

                if error.httpStatusCode == 409 {
                    log.warning("409, Resetting app session storage! Relogin or app recreation is needed!")
                    self.relogin = true //Reset saved sessions to create new app
                    self.reloginEvent()
                } else {
                    log.warning("Something went wrong during refresh token procedure. Please retry later!")
                    self.retryLaterEvent()
                }

                ///let requestError  = RequestError(userMessage: "Unable to refresh access token", cause: "")
                let response = Response.failure(error) //(requestError)
                let responseInfo = ResponseInfo(response: response)

                return .useResponse(responseInfo)                // If not, use the response we got.
            }

            return .useThisResponse        // We have new token!
        }
    }

please note 409 return code check in globalApiFailHandler. It is there because global decorator is always called for authRefreshResource. If we omit that check, an API will stuck in an infinite loop refreshing token under some server errors.

The question is how to disable global decorator for particular resource we want? Having this will elegantly resolve our problem.

Roman Savrulin
  • 183
  • 1
  • 6

1 Answers1

0

You can pass an arbitrary predicate to configure(whenURLMatches:), which lets you surgically exclude whatever you like:

service.configure(whenURLMatches: { url in url.path != "/auth" }) {
    ...
}

Or if, as in your case, you want to exclude a URL for which you have a Resource handy:

service.configure(whenURLMatches: { $0 != authRefreshResource.url }) {
    ...
}
Paul Cantrell
  • 9,175
  • 2
  • 40
  • 48