I have an app which has a session mechanism. When a user creates an account in the app initially, the API returns an access token, the expiry date of that access token and a refresh token which can be used to retrieve a new access token when the existing access token expires.
I have written this singleton class to store these session related data.
import Foundation
import KeychainAccess
class SessionManager {
static let shared = SessionManager()
/// Save the access token string in the keychain.
/// Returns the access token string if available.
var accessToken: String? {
set {
let keychain = Keychain(service: Bundle.main.bundleIdentifier!)
if let token = newValue {
keychain["AccessToken"] = token
} else {
keychain["AccessToken"] = nil
}
}
get {
let keychain = Keychain(service: Bundle.main.bundleIdentifier!)
do {
if
let accessToken = try keychain.getString("AccessToken"),
let expiryDate = expiryDate {
if expiryDate < Date() {
if let refreshToken = refreshToken {
return ApiClient.shared.refreshAccessToken(refreshToken: refreshToken)?.accessToken
}
return nil
} else {
return accessToken
}
} else {
return nil
}
} catch {
return nil
}
}
}
/// Save the refresh token in the keychain.
/// Returns the refresh token string if available
var refreshToken: String? {
set {
let keychain = Keychain(service: Bundle.main.bundleIdentifier!)
if let token = newValue {
keychain["RefreshToken"] = token
} else {
keychain["RefreshToken"] = nil
}
}
get {
let keychain = Keychain(service: Bundle.main.bundleIdentifier!)
do {
if let refreshToken = try keychain.getString("RefreshToken") {
return refreshToken
} else {
return nil
}
} catch {
return nil
}
}
}
/// Save the expiry date of access token in user defaults.
/// Returns the expiry date of access token if available
var expiryDate: Date? {
set {
if let date = newValue {
UserDefaults.standard.set(date, forKey: "ExpiryDate")
} else {
UserDefaults.standard.removeObject(forKey: "ExpiryDate")
}
}
get {
if let expiryDate = UserDefaults.standard.object(forKey: "ExpiryDate") as? Date {
return expiryDate
} else {
return nil
}
}
}
}
The class has three computed properties; accessToken
, refreshToken
and expiryDate
. I want to direct your attention to the accessToken
property.
What I do here is whenever I retrieve this value in the app, I check if the access token is expired and if it is, I fire an API call with the refresh token to fetch a new access token. Now this is where things get a little hairy. I want to make the network call asynchronous. But since accessToken
is a property, I cannot. So what I have done is, I'm making that API call synchronously. I'm using Alamofire in my project and I found this project called Alamofire-Synchronous that suits my need here. This is that method where the synchronous network call happens.
func refreshAccessToken(refreshToken: String) -> (accessToken: String, expiryDate: Date)? {
let urlString = baseUrl + Endpoint.RefreshAccessToken
let headers = [
"Authorization": "Bearer \(refreshToken)"
]
let response = Alamofire
.request(urlString, method: .post, parameters: nil, encoding: JSONEncoding.default, headers: headers)
.validate()
.responseData()
if let data = response.data {
// parse dara and get retrieve accessToken and expiryDate
// save them
let sessionManager = SessionManager.shared
sessionManager.accessToken = accessToken
sessionManager.expiryDate = expiryDate
return (accessToken, expiryDate)
} else {
if let httpError = response.result.error {
print("HTTP Error: \(httpError)")
return nil
} else {
return nil
}
}
}
This gets the job done. But I feel like having a synchronous network call is not a good idea. At the same time I don't just want to make this like an async function because I want to be able to get the accessToken
value as a property anywhere from the app if needed. Is there a better, cleaner way to handle this?