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
Related
I went through he documentation for Google People API.
https://developers.google.com/people/v1/getting-started
I couldn't find any help/sample to implement the same for iOS platform.
Is there any help available for iOS platform?
Yes there is a People API client library for iOS. To include it you specify
pod 'GoogleAPIClientForREST/PeopleService', '~> 1.3.4'
pod 'GoogleSignIn', '~> 4.1.2'
in the Cocoapods pod file.
No, I havn't found any documentation of the Google People API client library for iOS except the source libraries themselves at https://github.com/google/google-api-objectivec-client-for-rest
Use the Google Calendar API iOS Swift sample code as guidance, setting
private let scopes = [kGTLRAuthScopePeopleServiceContactsReadonly]
private let service = GTLRPeopleServiceService()
The following code reads the contacts list for the signed in user.
// MARK: - Get Google Contacts
#objc func fetchContacts() {
let query = GTLRPeopleServiceQuery_PeopleConnectionsList.query(withResourceName: "people/me")
query.personFields = "names,emailAddresses,photos"
service2.executeQuery(
query,
delegate: self,
didFinish: #selector(getCreatorFromTicket(ticket:finishedWithObject:error:)))
}
#objc func getCreatorFromTicket(
ticket: GTLRServiceTicket,
finishedWithObject response: GTLRPeopleService_ListConnectionsResponse,
error: NSError?) {
if let error = error {
showAlert(title: "Error", message: error.localizedDescription)
return
}
if let connections = response.connections, !connections.isEmpty {
for connection in connections {
if let names = connection.names, !names.isEmpty {
for name in names {
if let _ = name.metadata?.primary {
print(name.displayName ?? "")
}
}
}
if let emailAddresses = connection.emailAddresses, !emailAddresses.isEmpty {
for email in emailAddresses {
if let _ = email.metadata?.primary {
print(email.value ?? "")
}
}
}
if let photos = connection.photos, !photos.isEmpty {
for photo in photos {
if let _ = photo.metadata?.primary {
print(photo.url ?? "")
}
}
}
}
}
}
ps To make the Google Calendar API iOS Swift sample build without errors, you probably need to add a bridging header file (File > New > File, choose header file) and add the following:
#import <GTMSessionFetcher/GTMSessionFetcher.h>
#import <GTMSessionFetcher/GTMSessionFetcherService.h>
Then in the target Build Settings under the Swift Compiler - General heading, in the ObjectiveC Bridging header row add
projectname/bridgingheaderfilename
You may also then have to clean the build folder (Product menu, hold down options key).
Adding on to Hugo's answer in Swift 4.2 you need to authenticate with the GIDSignInDelegate if you use the GoogleSignIn cocoapod, once you conform to this protocol you can add the following to your class init() along with an optional String
private var accessToken: String?
//init
GIDSignIn.sharedInstance().clientID = "Your client id via https://console.developers.google.com/apis/api/people.googleapis.com"
GIDSignIn.sharedInstance().delegate = self
func sign(_ signIn: GIDSignIn!, didSignInFor user: GIDGoogleUser!, withError error: Error!) {
if let error = error {
print("Google sign in error: \(String(describing: error.localizedDescription))")
return
}
guard let authentication = user.authentication else { return }
self.accessToken = authentication.accessToken
self.fetchContacts()
}
Now fetchContacts() looks something like this:
func fetchContacts() {
let query = GTLRPeopleServiceQuery_PeopleConnectionsList.query(withResourceName: "people/me")
let formattedToken = String(format: "Bearer %#", self.accessToken!)
let headers = ["Authorization": formattedToken, "3.0": "GData-Version"]
query.additionalHTTPHeaders = headers
query.personFields = "names,emailAddresses,photos"
query.pageSize = 2000 //max
services.shouldFetchNextPages = true
services.executeQuery(
query,
delegate: self,
didFinish: #selector(getCreatorFromTicket(ticket:finishedWithObject:error:)))
}
In AppDelegate.swift you need to call this method as well so that at the end of the authentication the url is properly handled
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
return GIDSignIn.sharedInstance().handle(url,
sourceApplication:options[UIApplication.OpenURLOptionsKey.sourceApplication] as? String,
annotation: [:])
}
I am attempting to connect Facebook login via FirebaseUI Auth however I cannot get the Facebook connect button to appear in the instanced authViewController. Platform: iOS, Language: Swift 2.0, Latest stable Xcode release
Everything is working as it should as far as pulling up an instanced authViewController. The e-mail/password option is present and actively creates a new user in my Firebase database.
The problem I am having occurs when trying to implement Facebook login functionality;
In my AppDelegate.swift file I have the following:
import Firebase
import FirebaseAuthUI
...
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
FIRApp.configure()
let authUI = FIRAuthUI.authUI()!
authUI.delegate = self
return true
}
The error I'm getting is "Cannot assign value of type 'AppDelegate' to type 'FIRAuthUIDelegate'" - basically I can't get the following line to work; authUI.delegate = self
I have read all documentation and browsed open/closed issues on implementation at the project's gitHub repo here:
https://github.com/firebase/FirebaseUI-iOS/tree/master/FirebaseUI
I have looked over a previous StackOverflow question that is similar in nature here;
How to use FirebaseUI for Google authentication on iOS in Swift?
I have attempted to copy the code from the above Stack Overflow since he states he did get more options to appear using his code (his question is about an error further down the line regarding authentication) but I still run into the same self delegation error. I have tried assigning FIRAuthUIDelegate to AppDelegate but it does not conform. What am I missing?
I'm also not familiar with FirebaseUI but here is a working example of authorizing a user with Facebook using regular Firebase and the FBSDKs
#IBAction func fbButtonTapped(sender: UIButton) {
let facebookReadPermissions = ["email", "public_profile", "user_photos"]
FBSDKLoginManager().logInWithReadPermissions(facebookReadPermissions, fromViewController: self, handler: { (result:FBSDKLoginManagerLoginResult?, error:NSError?) -> Void in
if error != nil {
Helper.displayAlert("Error Logging into Facebook", message: error!.localizedDescription, viewController: self)
} else {
let credential = FIRFacebookAuthProvider.credentialWithAccessToken(FBSDKAccessToken.currentAccessToken().tokenString)
FIRAuth.auth()?.signInWithCredential(credential) { (user, error) in
if error != nil {
Helper.displayAlert("Error Logging into Facebook", message: error!.localizedDescription, viewController: self)
} else {
let request = FBSDKGraphRequest(graphPath:"me", parameters: ["fields": "id, first_name, last_name, email, age_range, gender, verified, timezone, picture"])
request.startWithCompletionHandler {
(connection, result, error) in
if error != nil {
print (error)
} else if let userData = result as? [String : AnyObject] {
guard let userID = FIRAuth.auth()?.currentUser?.uid else { return }
let userInfo = ["firstName": userData["first_name"] as! String, "lastName": userData["last_name"] as! String, "email": userData["email"] as! String,
"gender": userData["gender"] as! String, "id": userData["id"] as! String, "verified": userData["verified"]?.description as! AnyObject, "key": userID]
FirebaseData.fbData.createFirebaseUser(userID, user: userInfo)
self.performSegueWithIdentifier(self.loginSucessIdentifier, sender: nil)
}
}
}
}
}
})
}
func createFirebaseUser(uid: String, user: [String : AnyObject]) {
REF_USERS.child(uid).setValue(user)
}
The code could be cleaned up a bit to get rid of all the if let statements but it should get you going with a working authorization for facebook login.
I haven't worked with Firebase, but it seems like you should put the code in your view controller...
import UIKit
import Firebase
import FirebaseAuthUI
class ViewController: UIViewController, FIRAuthUIDelegate {
override func viewDidLoad() {
super.viewDidLoad()
FIRApp.configure()
let authUI = FIRAuthUI.authUI()!
authUI.delegate = self
}
Again, I have not used Firebase before, but this is how delegates usually work.
(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
}
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.
Currently I'm trying to implement a twitter user login for my app in Xcode 7 beta with parse. I followed the docs at parse.com for twitter. After implementing, I am still running to use of unresolved identifier for
PFTwitterUtils.initializeWithConsumerKey("somekey", consumerSecret:"somekey")
and
#IBAction func twitterButtonTapped(sender: AnyObject) {
PFTwitterUtils.logInWithBlock {
(user: PFUser?, error: NSError?) -> Void in
if let user = user {
if user.isNew {
// process user object
self.processTwitterUser()
} else {
// process user object
self.processTwitterUser()
}
} else {
print("Uh oh. The user cancelled the Twitter login.")
}
}
}
and
func processTwitterUser()
{
// Show activity indicator
let spiningActivity = MBProgressHUD.showHUDAddedTo(self.view, animated: true)
spiningActivity.labelText = "Loading"
spiningActivity.detailsLabelText = "Please wait"
let pfTwitter = PFTwitterUtils.twitter()
let twitterUsername = pfTwitter?.screenName
var userDetailsUrl:String = "https://api.twitter.com/1.1/users/show.json?screen_name="
userDetailsUrl = userDetailsUrl + twitterUsername!
let myUrl = NSURL(string: userDetailsUrl);
let request = NSMutableURLRequest(URL:myUrl!);
request.HTTPMethod = "GET";
pfTwitter!.signRequest(request);
any common pitfalls I might have fell into?
Note: I added the correct frameworks (accounts etc.)
Here is a solution that worked for me.
I added the "ParseTwitterUtils.framework"
In my Bridging header I added "#import ParseTwitterUtils/ParseTwitterUtils.h "
In my AppDelegate.Swift I added "import ParseTwitterUtils"
I filled the consumerKey and secretKey (You'll find them on your Twitter dev account)
And now it's working
Hope it helped