Using refresh token to get new authorization token and repeat failed api call - swift

I have a general question about using tokens in swift to make api calls. The authorization token that is needed to make api calls expires every hour so I need a way to handle this in a generalized way for multiple api calls.
I'm facing an issue where if I get a 401 error I call a function to use the refresh token to get a new authorization token and I would like to re call the original function that gotten 401 error.
For example:
If I get a 401 error when I call getDetails() I want to call the getNewAuthToken() and after I get a new refresh token I want to call getDetails() again.
I want to do this in a way so that if I call any function getX() and I get a 401 error it calls getNewAuthToken() and then it calls the original function again getX()
What would be the best way to approach this without using any external libraries etc. Would the best way be using a sort of callback function ?
I have provided general code I've been implementing but as you can see when I get a 401 error it calls the getNewAuthToken() function but the original function is not called again. How can this code be modified to behave as needed?
import UIKit
import Combine
#Published var details: Details = nil
func getNewAuthToken(){
// here I request a token using the refresh token
}
func getDetails(){
self.getCurrentDetails{ details in
DispatchQueue.main.async {
self.details = details
}
}
}
func getCurrentDetails(_ completionHandler: #escaping (Details) -> ()) {
let url = "https://api.xxx.com/details"
guard let detailsURL = URL(string: url) else{
fatalError("URL not valid")
}
let authtoken = keychain.get("authtoken") ?? ""
var request = URLRequest(url: detailsURL)
request.httpMethod = "GET"
request.addValue("Bearer \(authtoken)", forHTTPHeaderField: "Authorization")
let session = URLSession.shared
let task = session.dataTask(with: request){
data, response, error in
let httpResponse = response as? HTTPURLResponse
// I request a new token
if(httpResponse?.statusCode == 401){
print("401")
self.getNewAuthToken()
return
}
do {
if(httpResponse?.statusCode != 200){
return
}
let decoder = JSONDecoder()
let details = try decoder.decode(Details.self, from:
data!)
completionHandler(details)
} catch let error2{
print(error2)
}
}
task.resume()
}

I don't think you need to separate function like "getX()" to handle this. All you need is a completion handler.
func getNewAuthToken(completionHandler: () -> Void) {
// here I request a token using the refresh token
completionHandler()
}
func someNetworkCall() {
getNewAuthToken {
someNetworkCall()
}
}

Related

Correct Alamofire retry for JWT if status 401?

I am trying to make a retry for my Alamofire Interceptor because I work with JSON Web Token. Adapt works great. But the server updates the Access token every 10 minutes after user registration or authorization. After 10 mins Access token doesn't work anymore, and the server response is 401. So I need to Refresh the token when the status is 401. As I mentioned above, adapt works great. But I need help understanding how to deal with retry. Below is my Interceptor:
class RequestInterceptor: Alamofire.RequestInterceptor {
func adapt( _ urlRequest: URLRequest, for session: Session, completion: #escaping (Result<URLRequest, Error>) -> Void) {
var urlRequest = urlRequest
urlRequest.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
completion(.success(urlRequest))
}
func retry( _ request: Request, for session: Session, dueTo error: Error, completion: #escaping (RetryResult) -> Void) {
guard let response = request.task?.response as? HTTPURLResponse, response.statusCode == 401 else {
completion(.doNotRetryWithError(error))
return
}
}
}
My View Model:
func refreshTokenFunc() {
AF.request(TabBarModel.Request.refreshTokenUrl, method: .post, parameters: parameters, encoder: JSONParameterEncoder.default, interceptor: RequestInterceptor()).response { response in
...
And usage (I work with SwiftUI):
.task {
tabBarViewModel.refreshTokenFunc()
}
I was trying with some examples from the Internet. But it doesn't work for me.
In you retry you need to call the completion handler on both sides of the guard, not just in the else side. completion(.retry) is common but you could also track a delay to make sure you don't overload the backend.
Additionally, you should be validating response and checking the error, not reaching directly into request.task.
AF.request(...).validate()... // Ensure the response code is within range.
// In retry
guard let error = error.asAFError, error.responseCode == 401 else { ... }

Returning parsed JSON data using Alamofire?

Hello new to Swift and Alamofire,
The issue i'm having is when I call this fetchAllUsers() the code will return the empty users array and after it's done executing it will go inside the AF.request closure and execute the rest.
I've done some research and I was wondering is this is caused by Alamofire being an Async function.
Any suggestions?
func fetchAllUsers() -> [User] {
var users = [User]()
let allUsersUrl = baseUrl + "users/"
if let url = URL(string: allUsersUrl) {
AF.request(url).response { response in
if let data = response.data {
users = self.parse(json: data)
}
}
}
return users
}
You need to handle the asynchrony in some way. This this means passing a completion handler for the types you need. Other times it means you wrap it in other async structures, like promises or a publisher (which Alamofire also provides).
In you case, I'd suggest making your User type Decodable and allow Alamofire to do the decoding for you.
func fetchAllUsers(completionHandler: #escaping ([User]) -> Void) {
let allUsersUrl = baseUrl + "users/"
if let url = URL(string: allUsersUrl) {
AF.request(url).responseDecodable(of: [User].self) { response in
if let users = response.value {
completionHandler(users)
}
}
}
}
However, I would suggest returning the full Result from the response rather than just the [User] value, otherwise you'll miss any errors that occur.

Removing Swift RxAlamofire dependency

I'm trying to remove my dependency on RxAlamofire.
I currently have this function:
func requestData(_ urlRequest: URLRequestConvertible) -> Observable<(HTTPURLResponse, Data)> {
RxAlamofire.request(urlRequest).responseData()
}
How can I refactor this and use Alamofire directly to build and return an RxSwift Observable?
I suggest you look at the way the library wraps URLRequest to get an idea on how to do it...
Below is an abbreviated example from the library. In essence, you need to use Observable.create, make the network call passing in a closure that knows how to use the observer that create gives you.
Make sure you send a completed when done and make sure the disposable knows how to cancel the request.
Your Base will be something in Alamofire (I don't use Alamofire so I'm not sure what that might be.)
extension Reactive where Base: URLSession {
/**
Observable sequence of responses for URL request.
Performing of request starts after observer is subscribed and not after invoking this method.
**URL requests will be performed per subscribed observer.**
Any error during fetching of the response will cause observed sequence to terminate with error.
- parameter request: URL request.
- returns: Observable sequence of URL responses.
*/
public func response(request: URLRequest) -> Observable<(response: HTTPURLResponse, data: Data)> {
return Observable.create { observer in
let task = self.base.dataTask(with: request) { data, response, error in
guard let response = response, let data = data else {
observer.on(.error(error ?? RxCocoaURLError.unknown))
return
}
guard let httpResponse = response as? HTTPURLResponse else {
observer.on(.error(RxCocoaURLError.nonHTTPResponse(response: response)))
return
}
observer.on(.next((httpResponse, data)))
observer.on(.completed)
}
task.resume()
return Disposables.create(with: task.cancel)
}
}
}

Refresh token with SwiftUI Combine

I'm trying to implement a refresh token strategy in Swift 5 and the Combine Framework for iOS.
I don't plan on using any third party package, just using what is provided by the framework, `URLSession.dataTaskPublisher`, so mu goal is to :
Make a request
If the request fails with 401, refresh the auth token (which is another request)
After the refresh token is done, retry the first request
If it fails throw the error to be handled by the caller
​
This is a very trivial use case, but seems to be very hard to implement in Combine, that makes it really hard to use in any real life scenario.
Any help would be welcome !
​
This is my try, which unfortonately doesn't work
private func dataTaskPublisherWithAuth(for request: URLRequest) -> URLSession.DataTaskPublisher {
return session.dataTaskPublisher(for: request)
.tryCatch { error -> URLSession.DataTaskPublisher in
guard error.errorCode == 401 else {
throw error
}
var components = URLComponents(url: self.baseUrl, resolvingAgainstBaseURL: true)
components?.path += "/refresh"
components?.queryItems = [
URLQueryItem(name: "refresh_token", value: KeychainHelper.RefreshToken),
]
let url = components?.url
var loginRequest = URLRequest(url: url!)
loginRequest.httpMethod = "GET"
loginRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
return session.dataTaskPublisher(for: loginRequest)
.decode(type: LoginResponse.self, decoder: JSONDecoder())
.map { result in
if result.accessToken != nil {
// Save access token
KeychainHelper.AccessToken = result.accessToken!
KeychainHelper.RefreshToken = result.refreshToken!
KeychainHelper.ExpDate = Date(timeIntervalSinceNow: TimeInterval(result.expiresIn!))
}
return result
}
.flatMap { data -> URLSession.DataTaskPublisher in
session.dataTaskPublisher(for: request)
}
}.eraseToAnyPublsiher()
}
You should use the .tryCatch method on Publisher here. This lets you replace an error with another publisher (such as replacing error 401 with a refresh request followed by a map switchToLastest auth request) or with another error (in this case if its not a 401 then just throw the original error).
Note that you probably shouldn't be using flatMap here because its not the same as .flatMapLatest in Rx or .flatmap(.latest) in Reactive Swift. You want to get into the habit of using .map and switchToLatest in Combine (ie apple decided the flattening and the mapping are two separate operators). If you don't do this you will get into trouble in some places that generate more than one inner publisher, such as search while you type, because instead of getting the latests inner value you will get ALL of them, in arbitrary order since the network requests complete in indeterminate time.

How to handle waiting for a nested URLSession to complete

I have a function that provides a layer over URLSession. When this function is called I would like to check if the current access token as expired, if it has, I would to pause the current call, make a call to request a new token, replace the existing entry in the Keychain, then continue with the call.
func profile(with endpoint: ProfilesEndpoint, method: HTTPMethod, body: String?, headers: [String: String]?, useAuthToken: Bool = true, completion: #escaping (Either<ProfileResponse>) -> Void) {
var request = endpoint.request
request.httpMethod = method.rawValue
if let body = body {
request.httpBody = body.data(using: .utf8)
}
if useAuthToken {
if !AuthService.shared.isTokenValid {
let group = DispatchGroup()
group.enter()
OAuthService.shared.requestRefreshToken()
group.leave()
}
let (header, token) = AuthService.shared.createAuthHeaderForNetworkRequest()
request.addValue(token, forHTTPHeaderField: header)
}
if let headers = headers {
for (key, value) in headers {
request.addValue(value, forHTTPHeaderField: key)
}
}
execute(with: request, completion: completion)
}
A mechanism existing for handling the Keychain so please assume this is in place.
The function to request a new token looks like
func requestRefreshToken() -> Void {
if let refreshToken = KeychainWrapper.standard.string(forKey: "RefreshToken") {
var postBody = "grant_type=\(refreshTokenGrantType)&"
postBody += "client_id=\(clientId)&"
postBody += "refresh_token=\(refreshToken)&"
let additionalHeaders = [
"Content-Type": "application/x-www-form-urlencoded;"
]
APIClient.shared.identity(with: .token, method: .post, body: postBody, headers: additionalHeaders, useAuthToken: false) { either in
switch either {
case .success(let results):
guard let accessToken = results.accessToken, let refreshToken = results.refreshToken else { return }
AuthService.shared.addTokensToKeyChain(tokens: ["AccessToken": accessToken, "RefreshToken": refreshToken])
case .error(let error):
print("Error:", error)
}
}
}
}
I was expecting the executing to pause here
group.enter()
OAuthService.shared.requestRefreshToken()
group.leave()
However it does not.
How I can await this call to complete before completing the rest of the function?
Add to your requestRefreshToken method completion handler which will get executed when your request for token is completed
func requestRefreshToken(_ completion: #escaping () -> Void) {
if let refreshToken = KeychainWrapper.standard.string(forKey: "RefreshToken") {
var postBody = "grant_type=\(refreshTokenGrantType)&"
postBody += "client_id=\(clientId)&"
postBody += "refresh_token=\(refreshToken)&"
let additionalHeaders = [
"Content-Type": "application/x-www-form-urlencoded;"
]
APIClient.shared.identity(with: .token, method: .post, body: postBody, headers: additionalHeaders, useAuthToken: false) { either in
switch either {
case .success(let results):
guard let accessToken = results.accessToken, let refreshToken = results.refreshToken else {
completion()
return
}
AuthService.shared.addTokensToKeyChain(tokens: ["AccessToken": accessToken, "RefreshToken": refreshToken])
case .error(let error):
print("Error:", error)
}
completion()
}
}
}
then leave dispatchGroup in closure and also add group.wait() (after calling request method) for pausing current thread until group's task has completed
group.enter()
OAuthService.shared.requestRefreshToken {
group.leave()
}
group.wait()
Note: you can add boolean parameter to completion to check if request for token was successful or not
Why not do this underneath instead of on top? This seems like an ideal use for NSURLProtocol. Basically:
The URL protocol snags creation of the request in its init method and saves off all the provided parameters.
The URL protocol allocates a private NSURLSession instance and stores it in a property. That session should not be configured to use the protocol, or else you'll get an infinite loop.
When the protocol gets a startLoading() call, it checks the validity of the token, then:
Fires off a request for a new token if needed, in that private session.
Upon response, or if the token is still valid, fires off the real request — again, in that private session.
With that approach, the entire authentication process becomes basically transparent to the app except for the need to add the protocol into protocolClasses on the session configuration when creating a new session.
(There are a number of websites, including developer.apple.com, that provide examples of custom NSURLProtocol subclasses; if you decide to go with this approach, you should probably use one of those sample code projects as a starting point.)
Alternatively, if you want to stick with the layer-on-top approach, you need to stop thinking about "stopping" the method execution and start thinking about it as "doing the last part of the method later". It's all about asynchronous thinking.
Basically:
Factor out the last part of the method (the code that performs the actual request) into a new method (or a block).
If the token is valid, call that method immediately.
If the token is invalid, asynchronously fetch the new token.
In the token fetch call's completion callback, call the method to perform the actual request.