Checking if Auth0 idToken has expired - ios

I have a Swift App, which implements the web login for Auth0. On successful login, i receive an access token and and idToken, which i both store locally in my Keychain. Upon next login, i first check, if the access token (stored in keychain) is still valid by
Auth0
.authentication()
.userInfo(withAccessToken: accessToken)
.start { result in
switch result {
case .success(result: let profile):
print("access token still valid")
On success does not have to login again. The issue i'm having however, is, that my idToken might be expired already, even though my access token is still valid, which leads to errors when i request my (GraphQL) backend with this idToken. So how do i solve this? Is there a way to check, if my idToken has expired in Swift? Because if there is no way to check, i would have to ask for login, even with potentially not expired tokens. That wouldn't make sense.

An idToken is a JSON Web Token (JWT), so the metadata is readable. To see what this looks like, paste your token into jwt.io and see what the format is.
For example, take this JWT:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiZXhwIjoiMTUwNDc1MzQ0NSIsImFkbWluIjp0cnVlfQ.ggeW2vGcdWKNWmICRfTZ8qcBOQlu38DzaO8t_6aNuHQ
It is broken into 3 parts: the header, the payload, and the signature. The expiration is in the payload section.
We just need to base64 decode this.
let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvbiBTbm93IiwiZXhwIjoiMTUwNDc1MzQ0NSIsImFkbWluIjp0cnVlfQ.aCiqyVAAmHizPshrcdy8jwgHvBg4Diz2YY2e1INjoPg"
// get the payload part of it
var payload64 = jwt.components(separatedBy: ".")[1]
// need to pad the string with = to make it divisible by 4,
// otherwise Data won't be able to decode it
while payload64.count % 4 != 0 {
payload64 += "="
}
print("base64 encoded payload: \(payload64)")
let payloadData = Data(base64Encoded: payload64,
options:.ignoreUnknownCharacters)!
let payload = String(data: payloadData, encoding: .utf8)!
print(payload)
This prints out:
{"sub":"1234567890","name":"Jon Snow","exp":"1504753445","admin":true}
The exp is your expiration date. You can pass this off to a JSON serializer to get the date:
let json = try! JSONSerialization.jsonObject(with: payloadData, options: []) as! [String:Any]
let exp = json["exp"] as! Int
let expDate = Date(timeIntervalSince1970: TimeInterval(exp))
let isValid = expDate.compare(Date()) == .orderedDescending

Related

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

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.

Swift Keychain - Storing OAuth Credentials: "The specified item already exists in the keychain"

I'm building an iOS app for my website and I'm attempting to use OAuth2 to manage login credentials. On user login, I'm successfully hitting my authentication endpoint with the provided username and password and I'm attempting to store both the Access Token and the Refresh Token in the keychain, so the user doesn't have to provide credentials moving forward.
I'm having trouble storing both refresh token and access token in my keychain, following instructions from these sources:
Adding a Password to the Keychain
Searching for Keychain Items
Updating and Deleting Keychain Items
I'm able to successfully store either the Access Token or the Refresh Token, but no matter which one I store first, when attempting to store the other, I receive the following error message: "The specified item already exists in the keychain."
I added a CheckForExisting function to delete any existing items with the same specifications, but when I attempt to delete the existing keychain item using the same query, I receive a errSecItemNotFound status. So, frustratingly enough, I'm being told that I can't create my item because it already exists, but I can't delete the existing item because no existing item exists.
My hypothesis is that the creation of the Access Token item blocks the creation of the Refresh Token item, so I'm hoping someone can shed some light on the following:
Why is the second item creation being blocked? Does the Keychain have some built in primary key checks that I'm hitting (like can't store more than one kSecClassInternetPassword)?
What's the proper way to differentiate between the two tokens. Right now I'm using kSecAttrLabel, but that's a shot in the dark.
Please note that I'm hoping for an explanation of why my current approach is failing. I absolutely welcome alternative implementations, but I really want to understand what is going on behind the scenes here, so if possible please include an explanation of where an alternative implementation avoids the pitfalls that I seem to have fallen prey to.
Swift4 Code to Store the Tokens:
func StoreTokens(username: String, access_token: String, refresh_token: String) throws {
func CheckForExisting(query: [String: Any]) throws {
let status = SecItemDelete(query as CFDictionary)
guard status == errSecSuccess || status == errSecItemNotFound else {
let error_message = SecCopyErrorMessageString(status, nil)!
throw KeychainError.unhandledError(status: error_message)
}
}
let configuration = ConfigurationDetails()
let server = configuration.server
let access_token = access_token.data(using: String.Encoding.utf8)!
let refresh_token = refresh_token.data(using: String.Encoding.utf8)!
let access_token_query: [String: Any] = [
kSecClass as String: kSecClassInternetPassword,
kSecAttrAccount as String: username,
kSecAttrServer as String: server,
kSecAttrLabel as String: "AccessToken",
kSecValueData as String: access_token
]
let refresh_token_query: [String: Any] = [
kSecClass as String: kSecClassInternetPassword,
kSecAttrAccount as String: username,
kSecAttrServer as String: server,
kSecAttrLabel as String: "RefreshToken",
kSecValueData as String: refresh_token
]
try CheckForExisting(query: access_token_query)
let access_status = SecItemAdd(access_token_query as CFDictionary, nil)
guard access_status == errSecSuccess else {
let error_message = SecCopyErrorMessageString(access_status, nil)!
throw KeychainError.unhandledError(status: error_message)
}
try CheckForExisting(query: refresh_token_query)
let refresh_status = SecItemAdd(refresh_token_query as CFDictionary, nil)
guard refresh_status == errSecSuccess else {
let error_message = SecCopyErrorMessageString(refresh_status, nil)!
throw KeychainError.unhandledError(status: error_message)
}
}
According this https://developer.apple.com/documentation/security/errsecduplicateitem looks like the unique key for class kSecClassInternetPassword contains only these properties:
kSecAttrAccount, kSecAttrSecurityDomain, kSecAttrServer, kSecAttrProtocol, kSecAttrAuthenticationType, kSecAttrPort, and kSecAttrPath.
So, kSecAttrLabel is not in the list, and your refresh_token_query duplicates access_token_query.

Siesta REST login

How to translate my login user URLSession code into Siesta framework code? My current attempt isn't working.
I've looked at the example in the GithubBrowser but the API I have doesn't work like that.
The issue is that the user structure is kind of split by how the endpoint in the API I'm consuming works. The endpoint is http://server.com/api/key. Yes, it really is called key and not user or login. Its called that by the authors because you post a user/pass pair and get a key back. So it takes in (via post) a json struct like:
{"name": "bob", "password": "s3krit"}
and returns as a response:
{"token":"AEWasBDasd...AAsdga"}
I have a SessionUser struct:
struct SessionUser: Codable
{
let name: String
let password: String
let token: String
}
...which encapsulates the state (the "S" in REST) for the user. The trouble is name & password get posted and token is the response.
When this state changes I do my:
service.invalidateConfiguration() // So requests get config change
service.wipeResources() // Scrub all unauthenticated data
An instance is stored in a singleton, which is picked up by the configure block so that the token from the API is put in the header for all other API requests:
configure("**") {
// This block ^ is run AGAIN when the configuration is invalidated
// even if loadManifest is not called again.
if let haveToken = SessionManager.shared.currentUser?.token
{
$0.headers["Authorization"] = haveToken
}
}
That token injection part is already working well, by the way. Yay, Siesta!
URLSession version
This is bloated compared to Siesta, and I'm now not using this but here is what it used to be:
func login(user: SessionUser, endpoint: URL)
{
DDLogInfo("Logging in: \(user.name) with \(user.password)")
let json: [String: Any] = ["name": user.name, "password": user.password]
let jsonData = try? JSONSerialization.data(withJSONObject: json)
var request = URLRequest(url: endpoint)
request.httpMethod = "POST"
request.httpBody = jsonData
_currentStatus = .Unknown
weak var welf = self
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data else {
handleLogin(error: error, message: "No data from login attempt")
return
}
let jsonData:Any
do {
jsonData = try JSONSerialization.jsonObject(with: data, options: [])
}
catch let jsonDecodeError {
handleLogin(error: jsonDecodeError, message: "Could not get JSON from login response data")
return
}
guard let jsonDecoded = jsonData as? [String: Any] else {
handleLogin(error: error, message: "Could not decode JSON as dictionary")
return
}
guard let token = jsonDecoded["token"] as? String else {
handleLogin(error: error, message: "No auth token in login response")
return
}
let newUser = SessionUser(name: user.name, password: "", token: token)
welf?.currentUser = newUser
welf?.saveCurrentSession()
welf?._currentStatus = .LoggedIn
DDLogInfo("User \(newUser.name) logged in")
loginUpdate(user: newUser, status: .LoggedIn, message: nil, error: nil)
}
task.resume()
}
Siesta Version
Here is my attempt right now:
func login(user: String, pass: String, status: #escaping (String?) -> ())
{
let json = [ "name": user, "password": pass]
let req = ManifestCloud.shared.keys.request(.post, json: json)
req.onSuccess { (tokenInfo) in
if let token = tokenInfo.jsonDict["token"] as? String
{
let newUser = SessionUser(name: user, password: pass, token: token)
self.currentUser = newUser
}
status("success")
}
req.onFailure { (error) in
status(error.userMessage)
}
req.onCompletion { (response) in
status(nil)
}
}
Its sort of working, but the log in credentials are not saved by Siesta and I've had to rig up a new notification system for login state which I'd hoped Siesta would do for me.
I want to use Siesta's caching so that the SessionUser object is cached locally and I can use it to get a new token, if required, using the cached credentials. At the moment I have a jury-rigged system using UserDefaults.
Any help appreciated!
The basic problem here is that you are requesting but not loading the resource. Siesta draws a distinction between those two things: the first is essentially a fancied-up URLSession request; the second means that Siesta hangs on to some state and notifies observers about it.
Funny thing, I just answered a different but related question about this a few minutes ago! You might find that answer a helpful starting point.
In your case, the problem is here:
let req = ManifestCloud.shared.keys.request(.post, json: json)
That .request(…) means that only your request hooks (onSuccess etc.) receive a notification when your POST request finishes, and Siesta doesn’t keep the state around for others to observe.
You would normally accomplish that by using .load(); however, that creates a GET request and you need a POST. You probably want to promote your POST to be a full-fledge load request like this:
let keysResource = ManifestCloud.shared.keys
let req = keysResource.load(using:
keysResource.request(.post, json: json))
This will take whatever that POST request returns and make it the (observable) latestData of ManifestCloud.shared.keys, which should give you the “notification system for login state” that you’re looking for.

AppAuth iOS. How to save the token info after user registers an account in my own OAuth2 server

I have an iOS app and a backend written in Spring with OAuth2 mechanizm implemented.
I have a signup endpoint in my backend, which accepts some user data and returns an OAuth2 response, e.g.
{
"access_token":"9154140d-b621-4391-9fdd-fbba9e5e4188",
"token_type":"bearer",
"refresh_token":"dd612036-7cc6-4858-a30f-c548fc2a823c",
"expires_in":89,
"scope":"write"
}
After that I want to save OIDAuthState in my keychain, so I do the following:
func saveTokenResponse(responseDict: [String: NSCopying & NSObjectProtocol]) {
let redirectURI = URL(string: kRedirectURI)
let configuration = OIDServiceConfiguration(authorizationEndpoint: URL(string: kAuthorizationEndpoint)!, tokenEndpoint: URL(string: kTokenEndpoint)!)
let request = OIDAuthorizationRequest(configuration: configuration, clientId: self.kClientID, scopes: ["write"], redirectURL: redirectURI!, responseType: OIDResponseTypeCode, additionalParameters: nil)
let accessToken = responseDict["access_token"] ?? nil
let tokenType = responseDict["token_type"] ?? nil
let refreshToken = responseDict["refresh_token"] ?? nil
let expiresIn = responseDict["expires_in"] ?? nil
let scope = responseDict["scope"] ?? nil
let response = OIDAuthorizationResponse(request: request, parameters: [
"access_token": accessToken!,
"token_type": tokenType!,
"refresh_token": refreshToken!,
"expires_in": expiresIn!,
"scope": scope!
]
)
let authState = OIDAuthState(authorizationResponse: response) //here authState.refreshToken is nil, so it won't be serialized.
updateAuthState(authState: authState) // it just saves it in keychain
}
Everything works well, but I have an issue when the token expires.
The app makes a call to the backend and AppAuth is not able to refresh the token:
I can see, that the refresh token is not present in the OIDAuthState object. I have checked the initialization of the OIDAuthState from OIDAuthorizationResponse and found out that token is not assigned in that case.
Can anybody help how I can save OIDAuthState from the OAuth response I receive from my backend? Or am I doing something in a wrong way?
Thanks,
Osman
If your token is expires then you move on login screen (call sign out function) like all app (Banking system, Amazon, Paytm and Facebook). So user when login with your credential then getting same responses like you want and need refresh token. You have get samilar information in login web service like below response. I hope it's work fine
let accessToken = responseDict["access_token"] ?? nil
let tokenType = responseDict["token_type"] ?? nil
let refreshToken = responseDict["refresh_token"] ?? nil
let expiresIn = responseDict["expires_in"] ?? nil
let scope = responseDict["scope"] ?? nil
let response = OIDAuthorizationResponse(request: request, parameters: [
"access_token": accessToken!,
"token_type": tokenType!,
"refresh_token": refreshToken!,
"expires_in": expiresIn!,
"scope": scope!]
)
I don't know what exactly you're doing but I would suggest you to use a different mechanism which is widely used.
Everytime a user logs in with Google, a temporary access token is sent by Google which expires in a short while.
The access token you get on logging in, should be sent to your server just to see and check if the user is authentic and all his personal details can be fetched along with it too (via GoogleAPI call).
Send this access token to your server as soon as the user logs in via Google. Autheticate this token on server-side.
After the access token is authenticated, send your own unique token from your server (with an expiry time or not) to the user.
The user continues app usage with this unique token you've provided.
I ran into this same issue using AppAuth for iOS (refreshToken was null). I was able to get it working by adding adding "offline_access" to my scopes.
let request = OIDAuthorizationRequest(configuration: configuration!, clientId: self.kClientID, scopes: [OIDScopeOpenID, OIDScopeProfile, "api", "offline_access"], redirectURL: redirectURI! as URL, responseType: OIDResponseTypeCode, additionalParameters: nil)
Change the let redirectURI = URL(string: kRedirectURI) string to lowercase, oAuth for ios is picky.
Could also try to use Okta as a middleman service that plugs right in with oAuth.

Creating AccessToken from tokenString with Uber iOS SDK failing

I'm using custom authorisation with the Uber iOS SDK, and having trouble creating the AccessToken in my iOS code. This is the response I get from my server with what appears to be a valid access token:
{
"access_token":"token here",
"expires_in":2592000,
"token_type":"Bearer",
"scope":"all_trips request",
"refresh_token":"refresh token here",
"last_authenticated":0
}
I then pass this to the AccessToken initialiser, like so:
let jsonString = //response from server as above
let accessToken = AccessToken(tokenString: jsonString)
My access token is created (ie. non-nil), but none of the relevant property are populated.
accessToken //non-nil
accessToken.expirationDate //nil
accessToken.refreshToken //nil
Strangely, accessToken.tokenString contains the original jsonString from above.
Am I doing something wrong?
Edit
Digging through the AccessToken.swift source file of the Uber project, I find this:
#objc public init(tokenString: String) {
super.init()
self.tokenString = tokenString
}
It seems like it never actually creates the refreshToken etc.
The tokenString is meant to be just the access token itself, as you observed. If you want to parse the JSON itself, I would suggest using the fact that the model conforms to the Decodable protocol, and pass in your JSON through that method.
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .secondsSince1970
let accessToken = try? decoder.decode(AccessToken.self, from: jsonData)
// If you need to convert a string to data, use String.data(using: .utf8)!

Resources