I'm building an iOS (Swift) app using AWS as the backend with Developer Authenticated Identities. Everything works fine until I close the app, leave it for a while and then relaunch. In this scenario I often, but not always, receive ExpiredTokenException errors when trying to retrieve data from AWS.
Here is my code:
class DeveloperAuthenticatedIdentityProvider: AWSAbstractCognitoIdentityProvider {
var _token: String!
var _logins: [ NSObject : AnyObject ]!
override var token: String {
get {
return _token
}
}
override var logins: [ NSObject : AnyObject ]! {
get {
return _logins
}
set {
_logins = newValue
}
}
override func getIdentityId() -> AWSTask! {
if self.identityId != nil {
return AWSTask(result: self.identityId)
} else {
return AWSTask(result: nil).continueWithBlock({ (task) -> AnyObject! in
if self.identityId == nil {
return self.refresh()
}
return AWSTask(result: self.identityId)
})
}
}
override func refresh() -> AWSTask! {
let apiUrl = "https://url-goes-here" // call my server to retrieve an OpenIdToken
request.GET(apiUrl, parameters: nil, progress: nil,
success: {
(task: NSURLSessionDataTask, response: AnyObject?) -> Void in
let tmp = NSMutableDictionary()
tmp.setObject("temp", forKey: "ExampleApp")
self.logins = tmp as [ NSObject : AnyObject ]
let jsonDictionary = response as! NSDictionary
self.identityId = jsonDictionary["identityId"] as! String
self._token = jsonDictionary["token"] as! String
awstask.setResult(response)
},
failure: {
(task: NSURLSessionDataTask?, error: NSError) -> Void in
awstask.setError(error)
}
)
return awstask.task
}
}
And in the AppDelegate:
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
let identityProvider = DeveloperAuthenticatedIdentityProvider()
// set default service configuration
let credentialsProvider = AWSCognitoCredentialsProvider(regionType: cognitoRegion, identityProvider: identityProvider, unauthRoleArn: unauthRole, authRoleArn: authRole)
let configuration = AWSServiceConfiguration(region: defaultServiceRegion, credentialsProvider: credentialsProvider)
AWSServiceManager.defaultServiceManager().defaultServiceConfiguration = configuration
// set service configuration for S3 (my bucket is located in a different region to my Cognito and Lambda service)
let credentialsProviderForS3 = AWSCognitoCredentialsProvider(regionType: cognitoRegion, identityProvider: identityProvider, unauthRoleArn: unauthRole, authRoleArn: unauthRole)
let awsConfigurationForS3 = AWSServiceConfiguration(region: s3ServiceRegion, credentialsProvider: credentialsProviderForS3)
AWSS3TransferUtility.registerS3TransferUtilityWithConfiguration(awsConfigurationForS3, forKey: "S3")
return true
}
This post suggests that the Cognito token has expired and it is up to the developer to manually refresh. This seems overly complex as it would require setting a timer to refresh regularly, handling app closures and relaunches and handling AWS requests that occur while the refresh is taking place. Is there a simpler way? For example, is it possible to have the AWS SDK automatically call refresh whenever it attempts to query the server using an expired token?
Any help would be appreciated. I'm using version 2.3.5 of the AWS SDK for iOS.
The AWS Mobile SDK for iOS 2.4.x has a new protocol called AWSIdentityProviderManager. It has the following method:
/**
* Each entry in logins represents a single login with an identity provider.
* The key is the domain of the login provider (e.g. 'graph.facebook.com') and the value is the
* OAuth/OpenId Connect token that results from an authentication with that login provider.
*/
- (AWSTask<NSDictionary<NSString *, NSString *> *> *)logins;
The responsibility of an object conforming to this protocol is to return a valid logins dictionary whenever it is requested. Because this method is asynchronous, you can make networking calls in it if the cached token is expired. The implementation is up to you, but in many cases, AWSIdentityProviderManager manages multiple AWSIdentityProviders, aggregates them and return the logins dictionary.
Unfortunately developers refreshing the token is the only way.
I agree that it would be simpler for app developers if AWS SDK handled this but the way CrdentialsProvider is designed is supposed to be generic for all providers. For example, if someone wants to use Facebook as provider then AWS SDK will not be able to handle the refresh on its own and developer will have t handle that in his app. Keeping the refresh flow out of the SDK gives us the capability to keep the CredentialsProvider generic.
Related
I'm using Amazon Cognito for authentication and AWS iOS SDK v. 2.6.11 in my project. My app has the following flow on the main view: Get session, then make an API call using subclass of AWSAPIGateway class.
The issue here is that after successfully authenticating with Amazon Cognito, the API call response code is 403.
After stopping the app and then running it again (now the user is already authenticated) the response status code from the API is 200.
This is the message in responseData I get from the API call with 403 response:
"Message":"User: arn:aws:sts::############:assumed-role/####_unauth_MOBILEHUB_##########/CognitoIdentityCredentials is not authorized to perform: execute-api:Invoke on resource: arn:aws:execute-api:############:********####:##########/Development/POST/my-api-endpoint
(identifiers replaced with # characters)
It seems that the API calls are unauthorized. Is there a way to make those API calls authorized after an successful authentication?
This is the authentication code in my initial UIViewController:
let user = pool.currentUser() ?? pool.getUser()
user.getSession("myUsername", password: "myPassword", validationData: nil).continueOnSuccessWith { sessiontask -> Any? in
// i've left error handling out of this example code
let request = AWSAPIGatewayRequest(httpMethod: "POST",
urlString: "/my-api-endpoint",
queryParameters: nil,
headerParameters: nil,
httpBody: nil)
let serviceClient = AWSAPI_MY_AUTOGENERATED_Client.default()
return serviceClient.invoke(request).continueOnSuccessWith(block: { (task) -> Any? in
if let result = task.result, result.statusCode == 200 {
// A: all good - Continue
} else {
// B: Handle error (403 etc.)
}
return nil
})
This is how my AppDelegate looks like:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
let pool = AWSCognitoIdentityUserPool.default()
let credentialsProvider = AWSMobileClient.sharedInstance().getCredentialsProvider()
let configuration = AWSServiceConfiguration(
region: .EUCentral1,
credentialsProvider: credentialsProvider)
AWSServiceManager.default().defaultServiceConfiguration = configuration
// keeping reference to the pool and the credentials provider
self.pool = pool
self.credentialsProvider = credentialsProvider
window = UIWindow(frame: UIScreen.main.bounds)
let rootViewController = MyInitialViewController()
window!.rootViewController = rootViewController
window!.makeKeyAndVisible()
return AWSMobileClient.sharedInstance().interceptApplication(application, didFinishLaunchingWithOptions: launchOptions)
}
I writing an app in Swift 4 that uses the Discogs API. As such, I require a user to have access to personal data on their Discogs account, so I am authenticating against their API using OAuthSwift. Currently, I am able to kick off the auth flow, sign in and return the an oauthToken and the oauthTokenSecret
Making a subsequent request to their https://api.discogs.com/oauth/identity I am returned a user object, so I am happy at this point I can sign in and make authenticated requests.
However, I do not understand how I can check if a user is authenticated when the app first starts up. Currently, I am not storing the response, instead I am making a call to the identity endpoint in nested callback
import UIKit
import OAuthSwift
class ViewController: UIViewController {
let oauthSwift = OAuth1Swift(
consumerKey: "foo",
consumerSecret: "bar",
requestTokenUrl: "https://api.discogs.com/oauth/request_token",
authorizeUrl: "https://www.discogs.com/oauth/authorize",
accessTokenUrl: "https://api.discogs.com/oauth/access_token"
)
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
view.backgroundColor = .white
kickOffAuthFlow()
}
fileprivate func kickOffAuthFlow() {
oauthSwift.authorizeURLHandler = SafariURLHandler(viewController: self, oauthSwift: oauthSwift)
guard let callbackURL = URL(string: "foo.bar.boobaz:/oauth_callback") else { return }
oauthSwift.authorize(withCallbackURL: callbackURL, success: { (credential, response, parameters) in
_ = self.oauthSwift.client.get("https://api.discogs.com/oauth/identity", success: { (response) in
guard let dataString = response.string else { return }
print(dataString)
}, failure: { (error) in
print("error")
})
}) { (error) in
print(error.localizedDescription)
}
}
}
What is best practice in this case? How should I store these tokens and how should I ensure once the user is logged in, they aren't forced to log in the next time the app is opened (providing the token hasn't expired, however that is a separate issue I am prepared to handle at a later point)
Coming from a web development background, I was able to just store a token in session storage, on load I would then check the exp on the token and request a new one or take some other action.
I have not quite grasped how this works in iOS development yet.
You have two options to store access token in local.
UserDefault
Keychain
1. UserDefault
Use UserDefault to store token in memory. When the app gets launch, check if the token is stored in userdafault. UserDefault is used as short memory storage where you can store small data. It remains in memory if you kill the app.
let tokenIdentifier = "TokenIdentifier"
func storeAccessToken(token: String) {
UserDefaults.standard.set(token, forKey: tokenIdentifier)
}
func checkUserLogin() {
if UserDefaults.standard.value(forKey: tokenIdentifier) != nil {
print("User is Login")
}
else {
print("User need to login")
}
}
check this for learn more about userdefault
https://swift3tutorials.com/swift-3-user-defaults/
https://www.hackingwithswift.com/example-code/system/how-to-save-user-settings-using-userdefaults
2. Keychain
Userdefault is not secure. An access token is a sensitive information which should be stored in a secure place. So storing the access token in user default is not the correct choice. You must store access token in the keychain. Use SwiftKeychainWrapper pod to store token in Keychain.
let tokenIdentifier = "TokenIdentifier"
func storeAccessToken(token: String) {
KeychainWrapper.standard.set(token, forKey: tokenIdentifier)
}
func checkUserLogin() {
let token: String? = KeychainWrapper.standard.string(forKey: tokenIdentifier)
if token != nil {
print("User is Login")
}
else {
print("User need to login")
}
}
I'm using https://github.com/auth0/socketio-jwt to connect the user to my node.js/socket.io server and I'm using one round trip
My problem right now is that whenever user logs in on the IOS part, the socket.connect() is not consistent, my theory is that the token is not yet ready even before the socket.connect() gets invoked.
I'm using Singleton design for my Socket.io class as many people pointed that out.
Here's the code on the SocketManager.swift part
import SocketIO
class SocketIOManager: NSObject {
static let sharedInstance = SocketIOManager()
var socket = SocketIOClient(socketURL: URL(string: mainURL)!, config: [.log(false), .compress, .connectParams(["token": getToken()])]) // getToken() I got it from other file which is Constant.Swift
func establishConnection() {
socket.connect()
}
func closeConnection() {
socket.disconnect()
}
}
I'm using KeychainAccess to store the token and Constant.Swift file store all the global variables and functions so that I could call it on any Swift files.
Constant.Swift
import Foundation
import KeychainAccess
let keychain = Keychain(server: "www.example.com", protocolType: .https)
func getToken() -> String {
if let token = keychain["token"] {
return token
}
return ""
}
LoginViewController.swift
#IBAction func facebookButtonClicked(_ sender: UIButton) {
Alamofire.request("/login", method: .post, parameters: parameters, encoding: JSONEncoding.default)
.responseJSON { response in
if let value = response.result.value {
let json = JSON(value)
self.keychain["token"] = String(describing: json["token"])
SocketIOManager.sharedInstance.establishConnection()
self.segueToAnotherVC() // Segue to another screen, to simplify things i put it in a function
}
}
}
So technically what is happening in this controller is, when the user logs in, I will store the token into KeychainAccess (it is equivalent to NSUserDefaults), then only I will make a socket connection because the socket connection needs a token beforehand.
What should I do to make the connection consistent all the time, whenever user logs in? Any methods that I could use?
I suggest you to use keychain like this:
let keychain = KeychainSwift()
keychain.set("string", forKey: "key")
keychain.get("key")
keychain.delete("key")
keychain Usage:
let saveBool: Bool = KeychainWrapper.setString("String", forKey: "key")
let retrievedString: String? = KeychainWrapper.stringForKey("key")
let removeBool: Bool = KeychainWrapper.removeObjectForKey("key")
And make sure that your token is set when calling establish connection, if not, don't try and connect.
References:
https://github.com/socketio/socket.io-client-swift/issues/788
https://github.com/marketplacer/keychain-swift
https://github.com/jrendel/SwiftKeychainWrapper
More info:
JSON Web Token is a JSON-based open standard for creating access tokens that assert some number of claims.
I'm using Swift 3 and Xcode 8.1 and I'm using Amazon Cognito iOS SDK together with Facebook SDK to provide user authentication.
I'm facing number of issues while using the latest Amazon Cognito SDK (2.4.11).
Main issues are:
warning "logins is deprecated: Use AWSIdentityProviderManager". There's a workaround here, but I'd like to have a normal, official Amazon way to do it.
Nevertheless, the Amazon official docs seems to be 6 month old and describes how to use an old SDK (see "iOS - Swift" section)
Whenever I try to retrieve a current user cognitoId - I get a new one. I use syntax credentialsProvider.getIdentityId().continue. But I expect to get an old one. UPD: I'm not authenticated with FB in this case.
I'd really appreciate an official Amazon SDK developers comment here.
I find it very frustrating that I have to hunt after a solution in the internet and not being able to just follow official Amazon documentation, because it's outdated.
I would recommend against following the push paradigm that is suggested above and switch to the pull paradigm. The purpose of AWSIdentityProviderManager is to prompt you for a token only when the SDK needs it, not for you to set it externally periodically whether the SDK needs it or not. This way you don't have to manage token expiry yourself, just make sure your token is valid when logins is called and if it isn't you can use an AWSCompletionSource to get a fresh one.
Assuming you have integrated Facebook login, your IdentityProviderManager should look something like this:
import Foundation
import AWSCore
import FacebookLogin
import FacebookCore
class FacebookProvider: NSObject, AWSIdentityProviderManager {
func logins() -> AWSTask<NSDictionary> {
if let token = AccessToken.current?.authenticationToken {
return AWSTask(result: [AWSIdentityProviderFacebook:token])
}
return AWSTask(error:NSError(domain: "Facebook Login", code: -1 , userInfo: ["Facebook" : "No current Facebook access token"]))
}
}
To use it:
let credentialsProvider = AWSCognitoCredentialsProvider(regionType: AWSRegionType.YOUR_REGION, identityPoolId: "YOUR_IDENTITY_POOL_ID", identityProviderManager: FacebookProvider())
let configuration = AWSServiceConfiguration(region: AWSRegionType.usEast1, credentialsProvider: credentialsProvider)
AWSServiceManager.default().defaultServiceConfiguration = configuration
And then to test getting credentials:
AWSServiceManager.default().defaultServiceConfiguration.credentialsProvider.credentials().continue(with: AWSExecutor.default(), with: { (task) -> Any? in
print(task.result ?? "nil")
return task
})
BTW, I needed to add this to my app delegate to get Facebook Login to work with Swift which is not mentioned in the instructions here https://developers.facebook.com/docs/swift/login :
func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {
return FBSDKApplicationDelegate.sharedInstance().application(app, open: url, sourceApplication: options[UIApplicationOpenURLOptionsKey.sourceApplication] as! String, annotation: options[UIApplicationOpenURLOptionsKey.annotation])
}
Yes in latest SDK logins property get deprecated, so we need to assign IdentityProvider to logins by using AWSIdentityProviderManager delegate. So do as follows,
Create one custom class which adopt AWSIdentityProviderManager delegate.
import UIKit
import AWSCognitoIdentityProvider
import AWSCore
import Foundation
class DVCustomIdentityProvider: NSObject, AWSIdentityProviderManager {
var tokens: NSDictionary = [String : String]() as NSDictionary
init(tokens: [String : String]) {
self.tokens = tokens as NSDictionary
}
func logins() -> AWSTask<NSDictionary> { // AWSIdentityProviderManager delegate method
return AWSTask(result: tokens)
}
}
Add following code in view controller you want.
#IBAction func loginButtonPressed(_ sender: UIButton) {
if (phoneNumberTextField.text != nil) && (passwordTextField.text != nil) {
// Change userName.getSession.... with your Facebook method to get authenticate from Facebook, in success block add what I added in my success block.
userName.getSession(phoneNumberTextField.text!, password: passwordTextField.text!, validationData: nil).continue(with: AWSExecutor.mainThread(), withSuccessBlock: { (task: AWSTask<AWSCognitoIdentityUserSession>) -> Any? in // Your Facebook call will go here
if task.error != nil {
// Error
} else {
// SUCCESS BLOCK
self.updateCredentials()
}
return nil
})
} else {
// Credential empty
}
}
func updateCredentials() {
let customcedentialProvider = DVCustomIdentityProvider(tokens: ["graph.facebook.com" : token]))
let credentialsProvider = AWSCognitoCredentialsProvider(regionType: "Your region", identityPoolId: "Your pool id", unauthRoleArn: "Your unearth role name", authRoleArn: "Your auth role name", identityProviderManager: customcedentialProvider)
let configuration = AWSServiceConfiguration(region: "Your region", credentialsProvider:credentialsProvider)
AWSServiceManager.default().defaultServiceConfiguration = configuration
credentialsProvider.getIdentityId().continue(with: AWSExecutor.mainThread(), withSuccessBlock: { (taskTask: AWSTask<NSString>) -> Any? in
if taskTask.error == nil && taskTask.exception == nil {
kUserIdentityID = taskTask.result as String? // Im saving user identity id in constant variable called "kUserIdentityID"
} else {
// Do Nothing
}
return nil
})
}
import following in your view controller
import AWSCognitoIdentityProvider
import AWSCore
import AWSCognito
Note: This code is written in swift 3
(Note: this is related to this question I posted, but since my original question was answered and I am now encountering a different issue, I am posting this as a new question.)
I am setting up registration and login for an iOS app which uses DynamoDB and AWS Cognito. I eventually got the registration login process to work, but I've noticed that whenever I log out and then immediately try to log back in, the app fails to do so and I get the error message Invalid login token. Can't pass in a Cognito token. Only after I close and relaunch the app can I successfully log in again.
I primarily used this example project to set up registration, but when I was implementing the sign-in method, I had some trouble converting from Objective-C to Swift. I wasn't able to get the login process from the example to work, so I instead set up an explicit login method:
if locked { return }
trimRegistrationValues()
let name = usernameField.text!
let user = pool!.getUser(name)
lock()
user.getSession(name, password: passwordField.text!, validationData: nil, scopes: nil).continueWithExecutor(AWSExecutor.mainThreadExecutor(), withBlock: {
(task:AWSTask!) -> AnyObject! in
if task.error != nil {
self.sendErrorPopup("ERROR: Unable to sign in. Error description: " + task.error!.description)
} else {
print("Successful Login")
let loginKey = "cognito-idp.us-east-1.amazonaws.com/" + USER_POOL_ID
var logins = [NSString : NSString]()
self.credentialsProvider!.identityProvider.logins().continueWithBlock { (task: AWSTask!) -> AnyObject! in
if (task.error != nil) {
print("ERROR: Unable to get logins. Description: " + task.error!.description)
} else {
if task.result != nil{
let prevLogins = task.result as! [NSString:NSString]
print("Previous logins: " + String(prevLogins))
logins = prevLogins
}
logins[loginKey] = name
let manager = IdentityProviderManager(tokens: logins)
self.credentialsProvider!.setIdentityProviderManagerOnce(manager)
self.credentialsProvider!.getIdentityId().continueWithBlock { (task: AWSTask!) -> AnyObject! in
if (task.error != nil) {
print("ERROR: Unable to get ID. Error description: " + task.error!.description)
} else {
print("Signed in user with the following ID:")
print(task.result)
dispatch_async(dispatch_get_main_queue()){
self.performSegueWithIdentifier("mainViewControllerSegue", sender: self)
}
}
return nil
}
}
return nil
}
}
self.unlock()
return nil
})
Currently, the code in my AppDelegate class for setting up Cognito looks like this:
let userPoolConfiguration = AWSCognitoIdentityUserPoolConfiguration(clientId:APP_CLIENT_ID, clientSecret: APP_CLIENT_SECRET, poolId: USER_POOL_ID)
let pool = AWSCognitoIdentityUserPool(forKey:USER_POOL_NAME)
pool.delegate = self
self.storyboard = UIStoryboard(name: "Main", bundle: nil)
self.credentialsProvider = AWSCognitoCredentialsProvider(regionType: .USEast1, identityPoolId: IDENTITY_POOL_ID, identityProviderManager:pool)
let serviceConfiguration = AWSServiceConfiguration(region:.USEast1, credentialsProvider:credentialsProvider!)
AWSCognitoIdentityUserPool.registerCognitoIdentityUserPoolWithConfiguration(serviceConfiguration, userPoolConfiguration: userPoolConfiguration, forKey: USER_POOL_NAME)
let manager = IdentityProviderManager(tokens: [NSString:NSString]())
self.credentialsProvider = AWSCognitoCredentialsProvider(regionType: .USEast1, identityPoolId: IDENTITY_POOL_ID, identityProviderManager: manager)
AWSServiceManager.defaultServiceManager().defaultServiceConfiguration = serviceConfiguration
startPasswordAuthentication()
The viewDidLoad() method in the login ViewController only contains this line regarding the Cognito values I use for logging in:
if pool == nil{
pool = AWSCognitoIdentityUserPool(forKey:USER_POOL)
}
In the prepareForSegue() case from the login ViewController to the first view the user sees after logging in, I set the user by calling:
destination.user = pool!.getUser(usernameField.text!)
In the method for signing out from this view, I call user!.signOut().
I've noticed that many Cognito example projects call credentialsProvider.clearKeychain() after signing out, but this did not solve the issue for me. I've been having trouble finding many examples showing specifically how to log out through Cognito. I've also heard that AWS credentials expire after an hour after signing into an app like this. What is the proper way to handle credentials if I want to solve this problem and avoid other situations that might force my users to relaunch the app in order to sign in?
On Log out in additions to user.signOut() getDetails needs to be called again! Not quite sure why this would be the case after signOut, but definitely fixed it for me.
self.user?.getDetails().continueOnSuccessWith { (task) -> AnyObject? in
return nil
}