I am working a an app which uses the Alexa Voice Service and maintains different users, so the users needs to login with Amazon (LWA). I have implemented it like it is written in the docs and it works flawlessly.
LWA docs: https://developer.amazon.com/de/docs/login-with-amazon/use-sdk-ios.html
AMZNAuthorizationManager.shared().authorize(request, withHandler: {(result : AMZNAuthorizeResult?, userDidCancel : Bool, error : Error?) -> () in
if error != nil {
// Handle errors from the SDK or authorization server.
}
else if userDidCancel {
// Handle errors caused when user cancels login.
}
else {
// Authentication was successful.
// Obtain the access token and user profile data.
self.accessToken = result!.token
self.user = result!.user!
}
})
Furthermore I need to retrieve information from DynamoDB, which uses Cognito for authentification. As stated in the docs, there should be a way pass the access token form LWA to Cognito, but I can't find the proper place to do it. They say LWA provides an AMZNAccessTokenDelegate, which it does not. The delegate method provides an API result which Cognito needs. The link in the Cognito docs below refers to the same exact link from the LWA docs I posted above.
Cognito docs: https://docs.aws.amazon.com/cognito/latest/developerguide/amazon.html
func requestDidSucceed(apiResult: APIResult!) {
if apiResult.api == API.AuthorizeUser {
AIMobileLib.getAccessTokenForScopes(["profile"], withOverrideParams: nil, delegate: self)
} else if apiResult.api == API.GetAccessToken {
credentialsProvider.logins = [AWSCognitoLoginProviderKey.LoginWithAmazon.rawValue: apiResult.result]
}
}
What am I missing?
[EDIT]
I crawled through the LWA sources today until I finally found the correct delegate method.
Use AIAuthenticationDelegate instead of AMZNAccessTokenDelegate
But that lets me sit in front of the next two problems:
I.
Value of type 'AWSCognitoCredentialsProvider' has no member 'logins'
Maybe I have to use the following?
.setValue([AWSCognitoLoginProviderKey.LoginWithAmazon.rawValue: apiResult.result], forKey: "logins")
II.
Use of unresolved identifier 'AWSCognitoLoginProviderKey'
What do I put here? Maybe the API key I got from LWA?
[EDIT2]
I wanted to try it out, but requestDidSucceed never gets called, even through I successfully logged in.
class CustomIdentityProvider: NSObject, AWSIdentityProviderManager {
func logins() -> AWSTask<NSDictionary> {
return AWSTask(result: loginTokens)
}
var loginTokens : NSDictionary
init(tokens: [String : String]) {
self.loginTokens = tokens as NSDictionary
}
}
in the Authorization code at this below in successsful
AMZNAuthorizationManager.shared().authorize(request) { (result, userDidCancel, error) in
if ((error) != nil) {
// Handle errors from the SDK or authorization server.
} else if (userDidCancel) {
// Handle errors caused when user cancels login.
} else {
let logins = [IdentityProvider.amazon.rawValue: result!.token]
let customProviderManager = CustomIdentityProvider(tokens: logins)
guard let apiGatewayEndpoint = AWSEndpoint(url: URL(string: "APIGATEWAYURL")) else {
fatalError("Error creating API Gateway endpoint url")
}
let credentialsProvider = AWSCognitoCredentialsProvider(regionType: .USWest2, identityPoolId: "IDENTITY_ID", identityProviderManager:customProviderManager)
let configuration = AWSServiceConfiguration(region: .USWest2, endpoint: apiGatewayEndpoint, credentialsProvider: credentialsProvider)
}
Related
I have started integrating AWS Cognito User Pools into my app and the signup + login works (I have followed this tutorial: https://docs.aws.amazon.com/cognito/latest/developerguide/tutorial-integrating-user-pools-ios.html)
Now I'm struggling to properly integrate a Facebook login. This is what I do:
After the user has successfully signed in with facebook (using the facebook SDK), I'm getting the token and calling this function:
func signInFacebook(){
let customProviderManager = CustomIdentityProvider(tokens: nil)
self.credentialsProvider = AWSCognitoCredentialsProvider(
regionType:CognitoIdentityUserPoolRegion,
identityPoolId: CognitoIdentityPoolId,
identityProviderManager: customProviderManager)
let configuration = AWSServiceConfiguration(region:CognitoIdentityUserPoolRegion, credentialsProvider: self.credentialsProvider!)
AWSServiceManager.default().defaultServiceConfiguration = configuration
}
My CustomIdentityProvider class looks like this:
class CustomIdentityProvider: NSObject, AWSIdentityProviderManager {
var tokens : NSDictionary?
init(tokens: NSDictionary?) {
self.tokens = tokens
}
func logins() -> AWSTask<NSDictionary> {
if let fbToken = AccessToken.current?.authenticationToken {
return AWSTask(result: [AWSIdentityProviderFacebook: fbToken])
} else if let googleToken = GIDSignIn.sharedInstance().currentUser.authentication.idToken {
return AWSTask(result: [AWSIdentityProviderGoogle: googleToken])
}
return AWSTask(error:NSError(domain: "Social login", code: -1 , userInfo: ["Social login" : "No current social access token"]))
}
}
After signInFacebook() is called, I also call
self.credentialsProvider?.credentials().continueOnSuccessWith { (task:AWSTask<AWSCredentials>) -> Any? in
print("credentials: \(task.result!)")
return nil
}
and it prints some data in the log which looks fine.
But for some reason it doesn't seem to link properly everything together.
When I'm calling my backend to fetch some data, I usually do it like this:
I call self.user?.getSession().continueOnSuccessWith (user is an instance of AWSCognitoIdentityUser) and inside the closure I build the request where I put the token in the header. But if there are no tokens, the SDK shows my login screen. And this is what happens all the time. I would expect the user object to be updated with the correct tokens after the social login with Facebook has succeeded. What am I doing wrong?
I'm using the following code, to detect auth provider and log out properly
static func logOut() {
let auth = FIRAuth.auth()!
let provider = auth.currentUser?.providerID
switch provider! {
case "Facebook": FBSDKLoginManager().logOut()
case "Google": GIDSignIn.sharedInstance().signOut()
case "Twitter": Twitter.sharedInstance().sessionStore.logOutUserID(TWTRAPIClient.withCurrentUser().userID!)
default:
print("Unknown provider ID: \(provider!)")
return
}
try! auth.signOut()
}
But the provider is always "Firebase". What am I doing wrong? 0_o Once that code throw "Facebook" when I was logged in twitter. Thanks in advance
UPD: Yeah, I actually can store auth provider in UserDefaults, but maybe it's Firebase bug. I'm using Firebase SDK 3.5.2
Since a user can sign into their Firebase Authentication account with multiple providers, the top-level provider ID will now (usually) be Firebase.
But the currentUser has a providerData property that provides information on the speciic providers. Looping over FIRAuth.auth()!.currentUser.providerData will give you the FIRUserInfo.providerID you're looking for.
See also this question about UIDs, which are in a similar situation: Firebase returns multiple IDs, Which is unique one?
Swift 4 solution:
if let providerData = Auth.auth().currentUser?.providerData {
for userInfo in providerData {
switch userInfo.providerID {
case "facebook.com":
print("user is signed in with facebook")
case "google.com":
print("user is signed in with google")
default:
print("user is signed in with \(userInfo.providerID)")
}
}
}
I had to JSON.stringify(currentUser.providerData)
in order to see how it's organized:
Stringify result
And i finally found the Auth Provider like this:
currentUser.providerData[0].providerId
Cheers, gl with your code : )
Using currentUser.providerData gives you the array of providers with each provider having its own uid. The list is sorted by the most recent provider used to sign in. So the first element in currentUser.providerData is the method that the user used to sign in.
So currentUser.providerData[0].providerId will give you the method that the user used to sign in.
// Provider Type
struct AuthProviders {
static let phone = "phone"
static let facebook = "facebook.com"
static let google = "google.com"
static let apple = "apple.com"
}
let providerIds = auth.currentUser?.providerData.map { $0.providerID }
To logout there is a simpler method:
let authUI = FUIAuth.defaultAuthUI()
do {
try authUI?.signOut()
} catch let err {
print(err);
}
On the other hand, if you want to find the provider AND determine if the user is logged in via that provider, check the accessToken. To get the accessToken you need the specific provider instance you provided to providers.
I find this is best achieved by first declaring your providers in your class this way:
lazy var facebookProvider = FUIFacebookAuth()
lazy var googleProvider = FUIGoogleAuth()
Then when you provide the providers:
let providers: [FUIAuthProvider] = [ facebookProvider, googleProvider ]
When you want the specific provider data:
if let providerData = Auth.auth().currentUser?.providerData {
for userInfo in providerData {
switch userInfo.providerID {
case "facebook.com":
if !facebookProvider.accessToken.isEmpty {
print("user is signed in with facebook")
}
case "google.com":
if !googleProvider.accessToken.isEmpty {
print("user is signed in with google")
}
default:
print("user is signed in with \(userInfo.providerID)")
}
}
}
Otherwise you will get info on each provider regardless of whether the user is actually logged in.
2022 Simple Javascript Solution (Firebase v8)
let signInMethod =
firebase.auth().currentUser?.providerData[0]?.providerId;
I have the following code for getting the Cognito ID via unauthenticated user in swift for an IOS app:-
import Foundation
import AWSCore
import AWSCognito
class CognitoInit {
func getCognitoIdentityId() -> String {
let credentialsProvider = AWSCognitoCredentialsProvider(regionType:.USEast1,
identityPoolId:"My_idenitity_pool_id_in_aws")
let configuration = AWSServiceConfiguration(region:.USEast1, credentialsProvider:credentialsProvider)
AWSServiceManager.defaultServiceManager().defaultServiceConfiguration = configuration
credentialsProvider.getIdentityId().continueWithBlock { (task: AWSTask!) -> AnyObject! in
if (task.error != nil) {
return task.error?.localizedDescription
}
else {
// the task result will contain the identity id
return task.result
}
return nil
}
return "DEFAULT_COGNITO_ID"
}
}
when getCognitoIdentityId() in a stub ViewController is called it always returns "DEFAULT_COGNITO_ID"(I am running it in an emulator). Stepping into the code via a debugger reveals that the async task does not run inside (the entire code block from if (task.error != nil) { to return nil is bypassed.
Am I missing something. Things that I can think of
Are there permissions needed for network/async calls that must be separately enabled?
Are there configurations needed in Cognito Identity Pool that I must do.
Are async tasks blocked in emulators?
Please help. I am new to swift development.
It sounds like it might just be the asynchronous nature of that call in play. Can you try the example exactly as described in the documentation, wait a few seconds, then try to get the identity id via
credentialsProvider.identityId
and see what happens? Since it is asynchronous, having it setup in this way won't work the way you're hoping, I think. You'll need to initiate the async call beforehand.
I am in the process of implementing registration and login for my iOS app, using this project as an example:
https://github.com/awslabs/aws-sdk-ios-samples/tree/75ada5b6283b7c04c1214b2e1e0a6394377e3f2b/CognitoYourUserPools-Sample/Objective-C/CognitoYourUserPoolsSample
Previously, my app was able to access DynamoDB resources by using a credentials provider set up in my AppDelegate's didFinishLaunchingWithOptions method. However, after changing my project to include logging in and function like the example, I see the error:
"__type":"NotAuthorizedException","message":"Token is not from a supported provider of this identity pool."
The code setting the credentialsProviderin AppDelegate currently looks like this:
let serviceConfiguration = AWSServiceConfiguration(region: .USEast1, credentialsProvider: nil)
let userPoolConfiguration = AWSCognitoIdentityUserPoolConfiguration(clientId:APP_CLIENT_ID, clientSecret: APP_CLIENT_SECRET, poolId: USER_POOL_ID)
AWSCognitoIdentityUserPool.registerCognitoIdentityUserPoolWithConfiguration(serviceConfiguration, userPoolConfiguration: userPoolConfiguration, forKey: USER_POOL_NAME)
let pool = AWSCognitoIdentityUserPool(forKey:USER_POOL_NAME)
pool.delegate = self
self.storyboard = UIStoryboard(name: "Main", bundle: nil)
let credentialsProvider = AWSCognitoCredentialsProvider(regionType: .USEast1, identityPoolId: IDENTITY_POOL_ID, identityProviderManager:pool)
let configuration = AWSServiceConfiguration(region:.USEast1, credentialsProvider:credentialsProvider)
I also cannot access any DynamoDB data through my app.
Based on the console output, the registration process seems to work correctly, although I'm unsure about the sign-in process. It occurred to me that I had changed the region from EU-West-1, where the DynamoDB resources were stored, to US-East-1. In order to account for this change, I repeated the same steps I had intially taken to allow my app to access DynamoDB:
I created Auth and Unauth roles, both with access to the same actions as the role which had previously worked, but for the EU-West-1 resources instead.
I set these roles to the user pool I created when setting up registration under "unauthenticated role" and "authenticated role".
In case it makes a difference, I should note that I did not use the exact same sign-in process outlined in the example project I linked. Instead, I used the explicit sign in process, like so:
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")
dispatch_async(dispatch_get_main_queue()){
self.performSegueWithIdentifier("mainViewControllerSegue", sender: self)
}
}
self.unlock()
return nil
})
The methods lock(), unlock(), and sendErrorPopup() are strictly UI-related methods that I made so that the beginning and end of the sign-in process would be more visually clear. The console output always reads "successful login", but I am wondering if this code actually signs the user in correctly, since the error message makes it sound like the user might not be properly authorized.
It occurred to me that the US-West tables might not have been set up correctly, but I experience the same problem even when trying to create new tables, so I don't think that's the issue. Are there steps I might have missed as far as giving the user access to DynamoDB? Has the process changed with AWS Cognito's new beta user pool system?
EDIT 2:
I fixed the previous issue, and for a while, my app was working fine. However, it has suddenly stopped loading DynamoDB data when I sign in, and shows the error message: invalid login token. Can't pass in a Cognito token. Currently, my AppData code looks like this:
let serviceConfiguration = AWSServiceConfiguration(region: .USEast1, credentialsProvider: nil)
let userPoolConfiguration = AWSCognitoIdentityUserPoolConfiguration(clientId:APP_CLIENT_ID, clientSecret: APP_CLIENT_SECRET, poolId: USER_POOL_ID)
AWSCognitoIdentityUserPool.registerCognitoIdentityUserPoolWithConfiguration(serviceConfiguration, userPoolConfiguration: userPoolConfiguration, forKey: USER_POOL_NAME)
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 manager = IdentityProviderManager(tokens: [NSString:NSString]())
self.credentialsProvider = AWSCognitoCredentialsProvider(regionType: .USEast1, identityPoolId: IDENTITY_POOL_ID, identityProviderManager: manager)
let configuration = AWSServiceConfiguration(region:.USEast1, credentialsProvider:credentialsProvider!)
AWSServiceManager.defaultServiceManager().defaultServiceConfiguration = configuration
...and my sign-in code looks like this:
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
})
I haven't changed anything between my app working and not working. I did cause a "too many password resets" error while testing the password reset functionality, but the issue persisted even when I created a new user account on my app, so I don't think that's the cause. Am I handling login correctly? If so, where should I look for other possible causes to this issue?
That exception is usually thrown if you've given Cognito a login but have not enabled your identity pool to consume that login provider. If you haven't, go to the Cognito Federated Identities console and turn on whichever provider you are trying to use (looks like User Pools), and this error should go away.
If you're certain you have set that up, can you give a code snippet of how you're setting the logins?
The key that you set the ID token against in logins should be of the format cognito-idp.<region>.amazonaws.com/<YOUR_USER_POOL_ID> not your USER_POOL_NAME. This blog along with the link in your post for our dev guide should explain the steps and code you need.
As for the solution to deprecated logins dictionary, you need to use this constructor to create the credentials provider. The identityProviderManager here should be an implementation of AWSIdentityProviderManager Protocol and the logins method should return the dictionary mapping for your provider name to the token. The credentials provider will call this method every time it needs the identity provider token. Check this answer for more details.
My google backend APIs are using Oauth 2.0, when i call one of my api with that code :
func userService() -> GTLServiceUserController{
var service : GTLServiceUserController? = nil
if service == nil {
service = GTLServiceUserController()
service?.retryEnabled = true
}
return service!
}
And
func callApi() {
let service = userService()
let query = GTLQueryUserController.queryForGetAllUsers()
service.executeQuery(query) { (ticket: GTLServiceTicket!, object: AnyObject!, error: NSError!) -> Void in
if(error != nil){
print("Error :\(error.localizedDescription)")
return
}
let resp = object as? GTLUserControllerOperationResponse
if(resp != nil){
print(resp)
}
}
}
I'm getting The operation couldn’t be completed. (com.google.appengine.api.oauth.OAuthRequestException: unauthorized)
I already read this article, but i don't want my users to log in, instead i want my app to present its credentials to the service for authentication without user interaction.
I want to use this approach, but they are not telling how to do it with iOS apps.
So basically i want to be able to call my APIs successfully without any user interaction or sign-in dialog.
Please note that i have those classes on my project :
GTMOAuth2Authentication
GTMOAuth2SignIn
GTMOAuth2ViewControllerTouch
Thank's in advance
If you don't need to authenticate individual users (via OAuth2 login), then you do not need to do anything else and your App can directly call the API (as described in the "Calling the Endpoint API (unauthenticated)" section).
Service accounts are for server-to-server interactions and cannot be used to authenticate individual users; if you embed a service account & key within your iOS App, anyone can extract it from the App and start calling your API, hence it does not add any security.