How to change access token with refresh token when it expires? - ios

I am doing an Alamofire request, and during login, it gives me access token and refresh token. After getting access token, I save it in keychain. Every 20 minutes the access token expires and I need to convert it to refresh token.
Below is the code of saving in keychain.
final class KeychainManager {
let keychain = Keychain(service: "com.app")
func saveToken(token: String) {
do {
try keychain.set(token, key: "accessToken")
} catch let error {
print(error)
}
}
func getAccessToken() -> String? {
let token = try? keychain.getString(accessTokenKey)
return token
}
}
And here is my Alamofire request
AF.upload(multipartFormData: { multiFormData in
for form in bodyKeyValue {
multiFormData.append(Data(form.sValue.utf8), withName: form.sKey)
}
}, to: url).responseData { response in
switch response.result {
case .success(_):
do {
let decodedData = try JSONDecoder().decode(LoginResponseBody.self, from: response.data!)
self.keychain.saveToken(token: decodedData.data.accessToken)
completion(.success(decodedData))
} catch {
completion(.failure(.serverError))
}
case .failure(_):
print("fail")
}
}
Now I don't know how to use , refresh token here, so when access token expires, it will be converted to refresh token. Does Alamofire have a function for that?

Generally as a good rule of thumb.
When your access token expires and you need to use the refresh token.
What you should do is:
When the app makes the API call and the token is no longer valid (IE: time to use refresh) , when the call fails here in the do block.
do {
let decodedData = try JSONDecoder().decode(LoginResponseBody.self, from: response.data!)
self.keychain.saveToken(token: decodedData.data.accessToken)
completion(.success(decodedData))
} catch {
completion(.failure(.serverError))
}
When the completionHandler(.failure(.serverError)) is triggered
You can make another call here to retrieve the refresh token/generate a new one. Either inside this function or in the viewController.
so in your app, when your function call returns completionHandler(.failure(.serverError)) , add the function call into the app(either in the failure block or inside the viewController, depending on your app and dev preference) on failure retrieve new access token/refresh token then make the same API called that failed.

Related

Refreshing an Access Token p2/OAuth2 iOS

I'm using p2/OAuth2 with Alamofire v4 as explained in documentation here
let sessionManager = SessionManager()
let retrier = OAuth2RetryHandler(oauth2: <# your OAuth2 instance #>)
sessionManager.adapter = retrier
sessionManager.retrier = retrier
self.alamofireManager = sessionManager // you must hold on to this somewhere
// Note that the `validate()` call here is important
sessionManager.request("https://api.github.com/user").validate().responseJSON { response in
debugPrint(response)
}
import Foundation
import OAuth2
import Alamofire
class OAuth2RetryHandler: RequestRetrier, RequestAdapter {
let loader: OAuth2DataLoader
init(oauth2: OAuth2) {
loader = OAuth2DataLoader(oauth2: oauth2)
}
/// Intercept 401 and do an OAuth2 authorization.
public func should(_ manager: SessionManager, retry request: Request, with error: Error, completion: #escaping RequestRetryCompletion) {
if let response = request.task?.response as? HTTPURLResponse, 401 == response.statusCode, let req = request.request {
var dataRequest = OAuth2DataRequest(request: req, callback: { _ in })
dataRequest.context = completion
loader.enqueue(request: dataRequest)
loader.attemptToAuthorize() { authParams, error in
guard error?.asOAuth2Error != .alreadyAuthorizing else {
// Don't dequeue requests if we are waiting for other authorization request
return
}
self.loader.dequeueAndApply() { req in
if let comp = req.context as? RequestRetryCompletion {
comp(nil != authParams, 0.0)
}
}
}
}
else {
completion(false, 0.0) // not a 401, not our problem
}
}
/// Sign the request with the access token.
public func adapt(_ urlRequest: URLRequest) throws -> URLRequest {
guard nil != loader.oauth2.accessToken else {
return urlRequest
}
return try urlRequest.signed(with: loader.oauth2) // "try" added in 3.0.2
}
}
Everything is working fine, but what I want to achieve is to avoid 401 errors by fetching an access token if expired before submitting a request.
Is it possible to achieve this approach ?
Thanks in advance,
Regards,
Well the standard behaviour in a mobile app should be as follows:
Login and get access token + refresh token
Optionally store tokens in secure storage so that logins on every app restart are avoided
Use access token to call the API and handle 401 responses via a token renewal
Use the refresh token to renew the access token when needed
Eventually the refresh token expires and the user has to login again
You can never completely avoid 401 responses from the API, and UIs need to handle this response specially. It is possible as an optimisation to refresh tokens silently in the background, before the 'exp' claim from the current access token is reached.
Out of interest there is an iOS Code Sample of mine that is easy to run, and which allows a kind of testing of the expiry events. This may give you some ideas on how to adapt your own solution.

Swift - Locksmith loadDataForUserAccount fails sometimes?

I have a strange bug that is occurring only on few user iPhones, details below -
The app consumes a universal framework (developed by ourself) to save accessToken and refreshToken after successful login to the Keychain. We are using Locksmith to achieve the functionality - Save, load data and delete when the user is logged out.
Everytime when the app is killed and launched or applicationWillEnterForeground, the tokens are refreshed with the help of a service call and are saved to keychain again. When the refreshToken expires (this token is valid for one month), user is notified that the app has not been used for a long time and he is logged out.
The actual problem is here that for only few users, the refresh mechanism fails even when they are using the app daily (i.e. not before completion of one month of the refreshToken). After verification with backend team, the refresh service is always up so I suspect the Locksmith loadDataForUserAccount but unable to reproduce the issue. Also, may users do NOT face the problem. Everything works normally as expected.
Can someone help me move further how to identify the cause?
Below is the code to refresh the accessToken and refreshToken
** Refresh token call From the App when app enters foreground or killed and launched**
if let mySession = ServiceLayer.sharedInstance.session {
mySession.refresh { result in
switch result {
case .failure(.authenticationFailure):
if isBackgroundFetch {
print("👤⚠️ Session refresh failed, user is now logged out.")
self.myService.logoutCurrentUser()
// Logout Current user
mySession.invalidate()
self.showLoginUI()
}
else {
// user accessToken is invalid but provide access to QR
// on the home screen. disable all other actions except logout button
self.showHomeScreen()
}
default:
mySession.getAccessToken { result in
switch result {
case let .success(value):
print("Access Token from App Delegate \(value)")
myAccessToken = value
case let .failure(error):
print("❌ Failed to fetch AccessToken: \(error)")
}
}
}
}
}
From the framework where the refresh mechanism is implemented
public func refresh(_ completion: #escaping (MyResult<String, MyError>) -> (Void)) {
guard isValid else {
completion(.failure(.invalidSession))
return
}
getRefreshToken { result in
switch result {
case let .success(refreshToken):
// Get new tokens.
ServiceClient.requestJSON(ServiceRequest.refreshToken(refreshToken: refreshToken)) { result in
switch result {
case let .success(dictionary):
var newAccessToken: String?
var newRefreshToken: String?
for (key, value) in dictionary {
if key as! String == "access_token" {
newAccessToken = value as? String
}
if key as! String == "refresh_token" {
newRefreshToken = value as? String
}
}
guard newAccessToken != nil && newRefreshToken != nil else {
completion(.failure(.general))
return
}
print("Renewed session tokens.")
do {
try Locksmith.updateData(data: [MySession.accessTokenKeychainKey: newAccessToken!, MySession.refreshTokenKeychainKey: newRefreshToken!],
forUserAccount: MySession.myKeychainAccount)
}
catch {
completion(.failure(.general))
}
completion(.success(newAccessToken!))
case let .failure(error):
if error == MyError.authenticationFailure {
print(“Session refresh failed due to authentication error; invalidating session.")
self.invalidate()
}
completion(.failure(error))
}
}
case let .failure(error):
completion(.failure(error))
}
}
}
The app is likely being launched in the background while the device is locked (for app refresh or other background mode you've configured). Protected data (including Keychain) is not necessarily available at that time. You can check UIApplication.isProtectedDataAvailable to check if it's available, and you can reduce the protection of the item to kSecAttrAccessibleAfterFirstUnlock in order to have background access more reliably (though not 100% promised, even in that mode). Locksmith calls this AfterFirstUnlock.

Moya rxswift : Refresh token and restart request

I'm using Moya Rx swift and i want to catch the response if the status code is 401 or 403 then call refresh token request then recall/retry the original request again and to do so i followed this Link but i tweaked it a bit to suit my needs
public extension ObservableType where E == Response {
/// Tries to refresh auth token on 401 errors and retry the request.
/// If the refresh fails, the signal errors.
public func retryWithAuthIfNeeded(sessionServiceDelegate : SessionProtocol) -> Observable<E> {
return self.retryWhen { (e: Observable<Error>) in
return Observable
.zip(e, Observable.range(start: 1, count: 3),resultSelector: { $1 })
.flatMap { i in
return sessionServiceDelegate
.getTokenObservable()?
.filterSuccessfulStatusAndRedirectCodes()
.mapString()
.catchError {
error in
log.debug("ReAuth error: \(error)")
if case Error.StatusCode(let response) = error {
if response.statusCode == 401 || response.statusCode == 403 {
// Force logout after failed attempt
sessionServiceDelegate.doLogOut()
}
}
return Observable.error(error)
}
.flatMapLatest({ responseString in
sessionServiceDelegate.refreshToken(responseString: responseString)
return Observable.just(responseString)
})
}}
}
}
And my Protocol :
import RxSwift
public protocol SessionProtocol {
func doLogOut()
func refreshToken(responseString : String)
func getTokenObservable() -> Observable<Response>?
}
But it is not working and the code is not compiling, i get the following :
'Observable' is not convertible to 'Observable<_>'
I'm just talking my first steps to RX-swift so it may be simple but i can not figure out what is wrong except that i have to return a type other than the one I'm returning but i do not know how and where to do so.
Your help is much appreciated and if you have a better idea to achieve what I'm trying to do, you are welcome to suggest it.
Thanks in advance for your help.
You can enumerate on error and return the String type from your flatMap. If the request succeeded then it will return string else will return error observable
public func retryWithAuthIfNeeded(sessionServiceDelegate: SessionProtocol) -> Observable<E> {
return self.retryWhen { (error: Observable<Error>) -> Observable<String> in
return error.enumerated().flatMap { (index, error) -> Observable<String> in
guard let moyaError = error as? MoyaError, let response = moyaError.response, index <= 3 else {
throw error
}
if response.statusCode == 401 || response.statusCode == 403 {
// Force logout after failed attempt
sessionServiceDelegate.doLogOut()
return Observable.error(error)
} else {
return sessionServiceDelegate
.getTokenObservable()!
.filterSuccessfulStatusAndRedirectCodes()
.mapString()
.flatMapLatest { (responseString: String) -> Observable<String> in
sessionServiceDelegate.refreshToken(responseString: responseString)
return Observable.just(responseString)
}
}
}
}
Finally i was able to solve this by doing the following :
First create a protocol like so ( Those functions are mandatory and not optional ).
import RxSwift
public protocol SessionProtocol {
func getTokenRefreshService() -> Single<Response>
func didFailedToRefreshToken()
func tokenDidRefresh (response : String)
}
It is very very important to conform to the protocol SessionProtocol in the class that you write your network request(s) in like so :
import RxSwift
class API_Connector : SessionProtocol {
//
private final var apiProvider : APIsProvider<APIs>!
required override init() {
super.init()
apiProvider = APIsProvider<APIs>()
}
// Very very important
func getTokenRefreshService() -> Single<Response> {
return apiProvider.rx.request(.doRefreshToken())
}
// Parse and save your token locally or do any thing with the new token here
func tokenDidRefresh(response: String) {}
// Log the user out or do anything related here
public func didFailedToRefreshToken() {}
func getUsers (page : Int, completion: #escaping completionHandler<Page>) {
let _ = apiProvider.rx
.request(.getUsers(page: String(page)))
.filterSuccessfulStatusAndRedirectCodes()
.refreshAuthenticationTokenIfNeeded(sessionServiceDelegate: self)
.map(Page.self)
.subscribe { event in
switch event {
case .success(let page) :
completion(.success(page))
case .error(let error):
completion(.failure(error.localizedDescription))
}
}
}
}
Then, I created a function that returns a Single<Response>.
import RxSwift
extension PrimitiveSequence where TraitType == SingleTrait, ElementType == Response {
// Tries to refresh auth token on 401 error and retry the request.
// If the refresh fails it returns an error .
public func refreshAuthenticationTokenIfNeeded(sessionServiceDelegate : SessionProtocol) -> Single<Response> {
return
// Retry and process the request if any error occurred
self.retryWhen { responseFromFirstRequest in
responseFromFirstRequest.flatMap { originalRequestResponseError -> PrimitiveSequence<SingleTrait, ElementType> in
if let lucidErrorOfOriginalRequest : LucidMoyaError = originalRequestResponseError as? LucidMoyaError {
let statusCode = lucidErrorOfOriginalRequest.statusCode!
if statusCode == 401 {
// Token expired >> Call refresh token request
return sessionServiceDelegate
.getTokenRefreshService()
.filterSuccessfulStatusCodesAndProcessErrors()
.catchError { tokeRefreshRequestError -> Single<Response> in
// Failed to refresh token
if let lucidErrorOfTokenRefreshRequest : LucidMoyaError = tokeRefreshRequestError as? LucidMoyaError {
//
// Logout or do any thing related
sessionServiceDelegate.didFailedToRefreshToken()
//
return Single.error(lucidErrorOfTokenRefreshRequest)
}
return Single.error(tokeRefreshRequestError)
}
.flatMap { tokenRefreshResponseString -> Single<Response> in
// Refresh token response string
// Save new token locally to use with any request from now on
sessionServiceDelegate.tokenDidRefresh(response: try! tokenRefreshResponseString.mapString())
// Retry the original request one more time
return self.retry(1)
}
}
else {
// Retuen errors other than 401 & 403 of the original request
return Single.error(lucidErrorOfOriginalRequest)
}
}
// Return any other error
return Single.error(originalRequestResponseError)
}
}
}
}
What this function do is that it catches the error from the response then check for the status code, If it is any thing other than 401 then it will return that error to the original request's onError block but if it is 401 (You can change it to fulfill your needs but this is the standard) then it is going to do the refresh token request.
After doing the refresh token request, it checks for the response.
=> If the status code is in bigger than or equal 400 then this means that the refresh token request failed too so return the result of that request to the original request OnError block.
=> If the status code in the 200..300 range then this means that refresh token request succeeded hence it will retry the original request one more time, if the original request fails again then the failure will go to OnError block as normal.
Notes:
=> It is very important to parse & save the new token after the refresh token request is successful and a new token is returned, so when repeating the original request it will do it with the new token & not with the old one.
The token response is returned at this callback right before repeating the original request.
func tokenDidRefresh (response : String)
=> In case the refresh token request fails then it may that the token is expired so in addition that the failure is redirected to the original request's onError, you also get this failure callback
func didFailedToRefreshToken(), you can use it to notify the user that his session is lost or log him out or anything.
=> It is very important to return the function that do the token request because it is the only way the refreshAuthenticationTokenIfNeeded function knows which request to call in order to do the refresh token.
func getTokenRefreshService() -> Single<Response> {
return apiProvider.rx.request(.doRefreshToken())
}
Instead of writing an extension on Observable there's another solution. It's written on pure RxSwift and returns a classic error in case of fail.
The easy way to refresh session token of Auth0 with RxSwift and Moya
The main advantage of the solution is that it can be easily applicable for different services similar to Auth0 allowing to authenticate users in mobile apps.

Getting an AuthToken using Siesta via Password Grant

new to both Swift and Siesta... Trying to make a "password" grant type request. I used the code located here (the block at the very bottom). My code is:
var authToken: String??
var tokenCreationResource: Resource { return resource("oauth/v2/token") }
func refreshTokenOnAuthFailure(request: Request) -> Request {
return request.chained {
guard case .failure(let error) = $0.response, // Did request fail…
error.httpStatusCode == 401 else { // …because of expired token?
return .useThisResponse // If not, use the response we got.
}
return .passTo(
self.createAuthToken().chained { // If so, first request a new token, then:
if case .failure = $0.response { // If token request failed…
return .useThisResponse // …report that error.
} else {
//print($0.response)
return .passTo(request.repeated()) // We have a new token! Repeat the original request.
}
}
)
}
}
func userAuthData() -> [String: String] {
return [
"username": "username",
"password": "password",
"grant_type": "password",
"client_id": "abc1234567",
"client_secret": "1234567abc"
]
}
func createAuthToken() -> Request {
print("requestingToken")
return tokenCreationResource
.request(.post, urlEncoded: userAuthData())
.onSuccess {
self.authToken = $0.jsonDict["access_token"] as? String // Store the new token, then…
print($0.jsonDict) //*****SEE MY NOTE BELOW ABOUT THIS LINE
self.invalidateConfiguration() // …make future requests use it
}
}
The problem is that it doesn't seem to set the authToken variable... In troubleshooting the $0.jsonDict variable in the createAuthToken() function seems to be empty. The line noted prints [:]
If I change the print($0.jsonDict) to print($0) I see the full response including the "content" section which displays the results I would have expected to be in the jsonDict
If it matters, my server implementation is Symfony using FOSOauthServerBundle. All this works fine if I just manually do a request in the browser and like I said the "content" of the response shows my token, I just can't seem to access it via the .jsonDict["access_token"]
Had this exact same issue with the example code - you need to remove
standardTransformers: [.text, .image]
from the Service constructor (or include .json).

Authentication Token

I'm trying to setup a simple iOS example to better understand Siesta. My REST api requires an access token to accompany each request. So (1) at the start of the app and (2) anytime I retrieve a HTTP 401 I need to request an access token and then put that in all future Authorization headers.
Working off this example from the documentation, I assume the line containing showLoginScreen is where I need to make a call to my authenticationResource to retrieve the token BUT how do I make the failed call immediately after (and not infinite loop of course)? Thank you.
let authURL = authenticationResource.url
configure({ url in url != authURL }, description: "catch auth failures") {
$0.config.beforeStartingRequest { _, req in // For all resources except auth:
req.onFailure { error in // If a request fails...
if error.httpStatusCode == 401 { // ...with a 401...
showLoginScreen() // ...then prompt the user to log in
}
}
}
}
Since you asked the question, the docs have been updated with an example that answers it.
The crux of it is to use decorateRequests(…) and Request.chained(…) to wrap all your service’s requests so that they automatically attempt to refresh the token before returning a response.
Here is the code from that example:
authToken: String??
init() {
...
configure("**", description: "auth token") {
if let authToken = self.authToken {
$0.headers["X-Auth-Token"] = authToken // Set the token header from a var that we can update
}
$0.decorateRequests {
self.refreshTokenOnAuthFailure(request: $1)
}
}
}
// Refactor away this pyramid of doom however you see fit
func refreshTokenOnAuthFailure(request: Request) -> Request {
return request.chained {
guard case .failure(let error) = $0.response, // Did request fail…
error.httpStatusCode == 401 else { // …because of expired token?
return .useThisResponse // If not, use the response we got.
}
return .passTo(
self.createAuthToken().chained { // If so, 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.
}
}
)
}
}
func createAuthToken() -> Request {
return tokenCreationResource
.request(.post, json: userAuthData())
.onSuccess {
self.authToken = $0.jsonDict["token"] as? String // Store the new token, then…
self.invalidateConfiguration() // …make future requests use it
}
}
}

Resources