I'm implementing an app using Alamofire as the networking framework to communicate with the REST API services, and the authentication system we use is the Basic Auth by matching the username and password with the server. I tried to renew the session using the adapter and retrier to post the authentication request again to the server, but it doesn't work. I'm new to Swift and I have no idea since I'm asked to fit with the Basic Auth but most of the web services use the OAuth systems.
Alamofire version: 4.7
Xcode version: 10.1
Swift version: 4.2
Platform(s) running Alamofire: iOS
macOS version running Xcode: High Sierra
The helper class of the request adapter is written as follow:
class APIRequestAdapter: RequestAdapter, RequestRetrier {
private typealias RefreshCompletion = (_ succeeded: Bool, _ userId: Int?) -> Void
private let lock = NSLock()
private var username: String
private var password: String
private var isRefreshing = false
private var requestsToRetry: [RequestRetryCompletion] = []
var headers: HTTPHeaders = [:]
var userId: Int? = nil
// Session ID for user authentication - It expires for every 30 minutes inactivity.
var sessionId: String? = nil
var newSessionId: String? = nil
// MARK: - Initialization
public init(username: String, password: String) {
self.username = username
self.password = password
}
// MARK: - RequestAdapter
func adapt(_ urlRequest: URLRequest) throws -> URLRequest {
var urlRequest = urlRequest
let loginString = String(format: "%@:%@", self.username, self.password)
let loginData = loginString.data(using: String.Encoding.utf8)!
let base64LoginString = loginData.base64EncodedString()
if let urlString = urlRequest.url?.absoluteString, urlString.hasPrefix(serverConstant.BASE_URL) {
urlRequest.httpMethod = "POST"
urlRequest.setValue("Basic \(base64LoginString)", forHTTPHeaderField: "Authorization")
urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
return urlRequest
}
return urlRequest
}
// MARK: - RequestRetrier
func should(_ manager: SessionManager, retry request: Request, with error: Error, completion: @escaping RequestRetryCompletion) {
lock.lock() ; defer { lock.unlock() }
if let response = request.task?.response as? HTTPURLResponse, response.statusCode == 401 {
requestsToRetry.append(completion)
if !isRefreshing {
refreshTokens { [weak self] succeeded, userId in
guard let strongSelf = self else { return }
strongSelf.lock.lock() ; defer { strongSelf.lock.unlock() }
if let userId = userId {
strongSelf.userId = userId
}
strongSelf.requestsToRetry.forEach { $0(succeeded, 0.0) }
strongSelf.requestsToRetry.removeAll()
}
}
} else {
completion(false, 0.0)
}
}
// MARK: - Private - Refresh Tokens
private func refreshTokens(completion: @escaping RefreshCompletion) {
print("[DEBUG] Refresh Sessions.")
guard !isRefreshing else { return }
isRefreshing = true
let urlString = serverConstant.BASE_URL + "/auth"
let parameters: Parameters=["username":username,
"password":password]
if let authorizationHeader = Request.authorizationHeader(user: username, password: password) {
headers[authorizationHeader.key] = authorizationHeader.value
}
BackgroundManager.shared.manager.request(urlString, method: .post, parameters: parameters, encoding: JSONEncoding.default, headers: headers).responseJSON { [weak self] response in
guard let strongSelf = self else { return }
if
let json = response.result.value as? [String: Any],
let userId = json["id"] as? Int
{
print("[DEBUG] Renew completed for user \(userId).")
completion(true, userId)
} else {
print("[DEBUG] Error - Failed to renew session.")
completion(false, nil)
}
strongSelf.isRefreshing = false
}
}
}
The server URL is the API base URL, for example https://www.myweb.com/api/v1.
Hi @markandshare,
Could you please provide more info as to what doesn't work? Without quite a bit more information, we won't be able to help you.
Thanks @cnoon for your reply.
We are developing an app that uses API services to get and post the data to our server. Currently, we are tackling with the session timeout of our implemented authentication system (basic auth), and we think of using the adapt and retry mechanism to handle the session expiry problem for inactivity.
However, I'm not quite familiar with Swift and I tried to follow the guidelines to setup my handler function for the adapt and retry part but it doesn't work. I'm not sure where I should initialise the adapter and retrier to my session manager.
The workflow is as the following:
The problem is actually the handling of expiring a session due to user's inactivity. So we tried to use your mechanism to re-perform the requests, once the session is invalid. But somehow I can't figure out how to implement it correctly.
Many thanks if you could help me to solve the issue.
Can you post how you are setting up the session? Your adapter seems fine more or less, so I'm guessing it's how you set up the session and are using it.
I 'm not sure if in this site you could find something helpful there are examples that use basic auth but I'm not sure if is this that you want to do
btw https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md#adapting-and-retrying-requests docs is out of date.
func retry(_ request: Request, for session: Session, dueTo error: Error, completion: @escaping (RetryResult) -> Void)
@cnoon, Thanks for your reply.
Here's the sample code for initialising the session in the SignInViewController:
class SignInViewController: UIViewController, MainPageDelegate, UIScrollViewDelegate, UITextFieldDelegate {
// Session Manager
var sessionManager = Alamofire.SessionManager()
var headers: HTTPHeaders = [:]
var username = ""
var password = ""
@IBOutlet weak var textFieldUsername: UITextField!
@IBOutlet weak var textFieldPassword: UITextField!
@IBOutlet weak var loginButton: UIButton!
@IBOutlet weak var scrollView: UIScrollView!
// Login function
@IBAction func buttonLogin(_sender: UIButton) {
// Authentication API
let URL_USER_LOGIN = serverConstant.BASE_API_URL + "/loginProcessRest"
username = textFieldUsername.text!
password = textFieldPassword.text!
// API parameters for user authentication
let parameters: Parameters=["j_username": user,
"j_password": password,
"json": "true"]
if let authorizationHeader = Request.authorizationHeader(user: user, password: password){
headers[authorizationHeader.key] = authorizationHeader.value
}
if !isConnectedToInternet() {
/* Some action for no network connection */
}
else {
sessionManager = Alamofire.SessionManager(serverTrustPolicyManager: ServerTrustPolicyManager(policies: serverConstant.serverTrustPolicies))
sessionManager.request(URL_USER_LOGIN, method: .post, parameters: parameters, headers: headers).responseJSON {
//making a post request
loginResponse in
if loginResponse.response?.statusCode == nil {
/* Action of Error handling for server error */
}
if let result = loginResponse.result.value {
/* Store user's profile data to local storage */
}
// Check for user's role privilege
self.getUserPriviliege(userId: String(self.userId)) { returnUserAuthList, error in
if returnUserAuthList!.contains("<UNAUTHORIZED_ROLE_NAME>") {
/* Action of Error handling for role authentication error */
}
else {
if loginResponse.result.value != nil {
/* Some actions for Keychain services */
/* Another actions for getting user's other profile data and store locally */
}
else {
/* Action of Error handling for invalid username and password */
}
}
}
}
}
}
func getSomeUserData(completionHandler: @escaping (SomeDataType?, [SomeDataList]?, Error?) -> Void) {
let URL_GET_SOMEUSERDATA = serverConstant.BASE_API_URL + "/v1/someUserData"
/* Some variables declaration */
self.sessionManager.request(URL_GET_SOMEUSERDATA, method: .get, parameters: nil, headers: self.headers).responseJSON {
//making a get request
response in
do {
someDataList = try JSONDecoder().decode(Array<SomeDataList>.self, from: response.data!)
completionHandler(someData, someDataList, nil)
} catch {
print ("[Error] Error in decode JSON - ", error)
completionHandler(nil, nil, error)
}
}
}
}
The function getSomeUserData is a sample API request handling function for my app to get data from the server via API. These kind of requesting functions will be called when the data is needed to grab down from server.
Recently, I also had another problem of handling another type session expiry. This expiry problem is because the app was not be terminated (still working in background and using other apps) after the user successfully signed in before. I was trying to check this type of inactivity by determining whether the API returns 401 status and ask the user signing in again. May I know whether there is mechanism to achieve this by the library?
Sorry for my late reply and many thanks for your help in advance.
@SandraMarcelaHerreraArriaga, Thanks for your help.
The site seems to discuss the problem when the authentication system adopted the token approach. However, our app simply authenticates the user by the username and password only with a simple session management (handled by session IDs) without token generation.
@NikKovIos, Thanks for your help.
So does that mean the retrier support the mechanism of session ID management, for my case?
@markandshare There are several issues with the code you posted, some of which will impact your ability to retry request requiring authorization, especially if you have other requests needing to retry at the same time.
First and foremost, you're recreating your SessionManager every time you make a request. This has both a performance impact, as you're recreating quite a bit of state every time, as well as preventing you from using a common retrier for all requests. Your SessionManager should be a singleton, and the logic that builds your requests and issues them to the SessionManager instance should not be part of a view controller. This will be more performant and allow you to take advantage of Alamofire's retry features.
Second, you're not adding a retrier to the SessionManager when you initialize it, so there is nothing attempting to retry. You need to add the retrier when you create the SessionManager or update to Alamofire 5 and add it when you make the request.
Third, you should not be doing any sort of connection precheck when making requests. You should make the request and let it fail, using the error you receive to take any next steps, or to trigger retry. Alamofire 5's RetryPolicy type can do this sort of retry for you automatically.
If there are no additional questions, I'm closing this issue.
Most helpful comment
@markandshare There are several issues with the code you posted, some of which will impact your ability to retry request requiring authorization, especially if you have other requests needing to retry at the same time.
First and foremost, you're recreating your
SessionManagerevery time you make a request. This has both a performance impact, as you're recreating quite a bit of state every time, as well as preventing you from using a common retrier for all requests. YourSessionManagershould be a singleton, and the logic that builds your requests and issues them to theSessionManagerinstance should not be part of a view controller. This will be more performant and allow you to take advantage of Alamofire's retry features.Second, you're not adding a retrier to the
SessionManagerwhen you initialize it, so there is nothing attempting to retry. You need to add the retrier when you create theSessionManageror update to Alamofire 5 and add it when you make the request.Third, you should not be doing any sort of connection precheck when making requests. You should make the request and let it fail, using the error you receive to take any next steps, or to trigger retry. Alamofire 5's
RetryPolicytype can do this sort of retry for you automatically.