AWS Cognito Google login overwrites mapped attribute values in user pool - amazon-web-services

Question specific to SocialIDP (google signin):
I give my users the option to change their 'preferred_username'. When they do, the preferred_username attribute is updated in my cognito user pool.
However, when they sign in again (google signin), Cognito does not keep the value it has stored in the pool for 'preferred_username'. It gets overwritten by whatever Google has for that mapped attribute.
Can anyone help? Here's my function that updates the user pool attributes
export const updateCognitoUserAttributes = async (user, attributes) => {
return await Auth.updateUserAttributes(user, {
...attributes.appSpecificCode
? { 'custom:appSpecificCode': attributes.appSpecificCode}
: { 'email': attributes.email,
'family_name': attributes.family_name,
'given_name': attributes.given_name,
'preferred_username': attributes.preferred_username
}
})
.then(res => {
return res;
})
.catch(err => {
return err;
});
}
Here are 3 pics showing preferred_username before change, after change, and then after I log out and log back in, notice it's changed back to what it was before. I can see why this would happen give that it is mapped to Google's "name" attribute, but the amplify docs indicate this value is changeable (and is in fact, as shown, changed in the pool), but that change should be permanent? What's the point in allowing these attributes to be changed if they just get overwritten every time a user logs in? (it's not just preferred_username that gets overwritten. Any attribute that is mapped to a google attribute gets overwritten). Any help appreciated.
Before change
After change
After logout/login

I guess there's nothing that can be done. Just found this same issue on the amplify github. In short, from amplify documentation:
Amazon Cognito must be able to update your mapped user pool attributes when users sign in to your application. When a user signs in through an identity provider, Amazon Cognito updates the mapped attributes with the latest information from the identity provider. Amazon Cognito updates each mapped attribute, even if its current value already matches the latest information. If Amazon Cognito can't update the attribute, it throws an error. To ensure that Amazon Cognito can update the attributes, check the following requirements:
https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-specifying-attribute-mapping.html.
https://github.com/aws-amplify/amplify-js/issues/7300
sigh.

You cannot turn the overwriting off.
However, you can set up an attribute mapping of Google's preferred_username to a custom Cognito attribute like custom:preferred_username.
Then update Cognito's preferred_username using custom:preferred_username in your application code, but only if preferred_username is not set yet.
custom:preferred_username will be overwritten on each login, but after the first login you will ignore it.

Related

AWS Amplify Auth.updateUserAttributes() clear localStorage/sessionStorage

I am writing a web-based app that uses AWS Cognito as the authentication service. I use 'aws-amplify' to implement the client-app.
I am using Auth.updateUserAttributes() to update a custom attribute of users on Cognito. However, I found that the call of this function would clear all the Cognito-related items, including idToken, refreshToken, and accessToken stored in localStorage. As a result, the web app behaves like sign-out.
Here is the code about Auth's configuration
Amplify.configure({
Auth: {
userPoolId: process.env.REACT_APP_AWS_COGNITO_USER_POOL_ID,
region: process.env.REACT_APP_AWS_COGNITO_REGION,
userPoolWebClientId: process.env.REACT_APP_AWS_COGNITO_APP_CLIENT_ID,
storage: window.localStorage,
authenticationFlowType: 'CUSTOM_AUTH',
},
});
and the code I wrote to update the user's attribute. (I followed the example code from the amplify docs https://docs.amplify.aws/lib/auth/manageusers/q/platform/js/#managing-user-attributes)
let user = await Auth.currentAuthenticatedUser();
console.log(JSON.stringify(localStorage)); // it outputs the localstorage with idToken,
// refreshToken, accessToken and other items
// start with 'CognitoIdentityServiceProvider'
const result = await Auth.updateUserAttributes(user, {
'custom:attributes_1': '123456789',
});
console.log(result); // OUTPUT: SUCCESS
console.log(JSON.stringify(localStorage)); // Only 'amplify-signin-with-hostedUI'.
// idToken, refreshToken, accessToken and
// other items were gone. No key, no value.
After the last line, I could not interact with the web page anymore. If I refreshed the web page, I found I had signed out and had to sign in again.
It was still the same if I changed the storage for Auth from localStorage to sessionStorage.
Here are my questions:
Is this kind of behavior normal? Does Auth.updateUserAttributes() leads to a force sign-out?
If it's true, is there any way to avoid a mandatory sign-out?
If it's not, what's wrong with my code or configuration? Or should I do some specific configuration for the Cognito service?
Thanks a lot!
Well, I figured it out after reading the source code of aws-amplify.
Behind the call of Auth.userUpdateAttributes, amplify will finally call CognitoUser.refreshSession(refreshToken, callback, clientMetadata) (https://github.com/aws-amplify/amplify-js/blob/f28918b1ca1111f98c231c8ed6bccace9ad9e607/packages/amazon-cognito-identity-js/src/CognitoUser.js#L1446). Inside this function, amplify sends an 'InitiateAuth' request to Coginito. If an error of 'NotAuthorizedException' happens, amplify calls clearCachedUser() that delete everything I mentioned in my question from the localStorage.
There was an error of 'NotAuthorizedException' happening and reported by the network work monitor of Chrome Browser'. I thought it was generated after the sign-out-like behavior. However, it turned out to be triggered because no deviceKey was passed to the request's parameters.
So the whole story was:
I set remember device options in Cognito;
I used 'CUSTOM_AUTH' as the authentication flow type;
When a user successfully signed in to my application, Cognito didn't give the client the deviceKey because of the 'CUSTOM_AUTH' authentication flow type.
When Auth.userUpdateAttributes() was called, CognitoUser.refreshSession() was called behind it. It attached no deviceKey to Cognito when it sent ana request to asked Cognito to refresh the token. The Cognito rejected the request with an error of 'NotAuthorizedException'. The CognitoUser.refreshSession() handled the error and called clearCachedUser() to delete the stored tokens and other info from the localStorage.
My final solution is to turn off the remember device option since I have to use 'CUSTOM_AUTH' as the authentication flow type according to my application's functional requirements.
According to https://aws.amazon.com/premiumsupport/knowledge-center/cognito-user-pool-remembered-devices/, the remember device function only works when the authentication flow type is set as 'USER_SRP_AUTH'.

Can't use Auth.userAttributes with federated sign in

In my app, users are only allowed to sign in using google. However, I need to keep track of which users are considered "admins" in my system, and allow admins to promote other users to also be admins. For this, I have a custom attribute in my Cognito user pool called 'admin'.
However, when I tried using Auth.userAttributes, I ran into the error that it expects a CognitoUser object, and Auth.currentAuthenticatedUser isn't returning a CognitoUser object. After some researching, it looks like it just returns whatever it gets from the federated sign in process. Looking at my user pool, it would seem users also aren't being assigned anything at all for my custom attribute.
I'm thinking I may need to use lambda triggers? But which triggers do I use, and how do I get Auth.currentAuthenticatedUser to return a CognitoUser object? I'm thinking I'll at least need a trigger to check if a user is signing in for the first time, and to set the admin attribute to 0. But then do I use Auth.signIn? Also, from https://github.com/aws-amplify/amplify-js/blob/a047ce73/packages/auth/src/Auth.ts#L1191, the source code for Auth.currentAuthenticatedUser, it looks like it only looks for the user in the user pool if the cache doesn't have any entry for 'aws-amplify-federatedInfo', which is added when I use Auth.federatedSignIn. So do I need to clear that?
Thanks in advance. Any info or advice is appreciated.
Tricky! I agree this should be easier. I'd leave the cache alone. Try bypassing the cache.
import { Auth } from 'aws-amplify';
Auth.currentAuthenticatedUser({
bypassCache: true // If set to true, this call will send a request to Cognito to get the latest user data
}).then(user => console.log(user))
.catch(err => console.log(err));
Why? currentAuthenticatedUser() is returning a FederatedUser when this is somewhat helpful because the ideal response would be a CognitoUser. bypassCache alters the response of currentAuthenticatedUser().
May your app bring great felicity!

Unable to confirm the user registration via AWS SDK

The AWS documentation for JS SDK says:
Force Change Password
The user account is confirmed and the user can sign in using a temporary password, but on first sign-in, the user must change his or her password to a new value before doing anything else.
User accounts that are created by an administrator or developer start in this state.
But if for such a user I try to call forgotPassword method of SDK, it errors saying something like: Password cannot be reset in the current state.
SO how can I complete the registration of a user (created by admin in IAM) from my website. Which is the SDK method that should be called ?
Setting up an Auto Verify Lambda Trigger on the Pre Sign Up Trigger will allow for the user to be a confirmed state, which may get you to the point you are looking for?
Lambda -> Node.js
Give it an appropriate Title
Place the below value in the code:
exports.handler = (event, context, callback) => {
// Confirm the user
event.response.autoConfirmUser = true;
// Set the email as verified if it is in the request
if (event.request.userAttributes.hasOwnProperty("email")) {
event.response.autoVerifyEmail = true;
}
// Return to Amazon Cognito
callback(null, event);
};
Save
Then Select newly created trigger in General Settings -> Triggers -> Pre sign-up
We can do this,
I previously answered mongodb to aws cogniton migration question.
Go through step by step. I explained that the user's created by admin need to change the password(forgot password) but there's still another way to do it. Checkout my answer,
Some content from my answer,
AdminCreateUser:
Create a new user profile by using the AWS Management Console or by calling the AdminCreateUser API. Specify the temporary password or allow Amazon Cognito to automatically generate one.
Specify whether provided email addresses and phone numbers are marked as verified for new users. Specify custom SMS and email invitation messages for new users via the AWS Management Console.
Specify whether invitation messages are sent via SMS, email, or both.
After successful user creation,
authenticate user using same user credentials Use: SDK calls InitiateAuth(Username, USER_SRP_AUTH)
After success of initateAuth, amazon Cognito returns the PASSWORD_VERIFIER challenge with Salt & Secret block.
Use RespondToAuthChallenge(Username, , PASSWORD_VERIFIER)
Amazon Cognito returns the NEW_PASSWORD_REQUIRED challenge along with the current and required attributes.
The user is prompted and enters a new password and any missing values for required attributes.
Call RespondToAuthChallenge(Username, , ).
After successful password change user can be able to login using same credentials which admin created.
Refer: Unable to confirm the user registration via aws

Are there universal Cognito Ids for AWS regardless of sign in?

I've been wrapping my head around AWS Cognito, and can't seem to find a clear answer on this one.
Here is our situation: I have a react-native mobile application (using aws-amplify and it uses three different identity providers:
AWS User Pools
Facebook
Google
I know that user pools are a method to manage, create, and sync users across platforms. Within those user pools, each user has GUID assigned to them called sub, short for subject.
I also know that if I use a third party, say Facebook, to sign in, the identity pool will get the third party user, or the user pool, temp access to the AWS services (say dynamo db).
We have another legacy database of previous info, and we want to have a matching process to assign the user to. So, if user1 logs in, they correspond to our old website with user_one in the legacy db, and we want to link up their data and save it for reference later.
My question is, in the identity pool that all 3 methods access, is there a universal GUID or ID that I can associate with each user regardless of sign in type, and record in a DB? And how do I get it using react-native?
Here is what I have tried:
Auth.signIn(username, password)
.then(user => {
// etc
User returns the user_poolId, clientId, and no identity pool info.
With a federated (FB, Google) sign in:
Auth.federatedSignIn('facebook', {
token: data.accessToken.toString(),
expires_at: data.expirationTime,
}, result)
.then(data => {
let user = {
id: data._identityId,
};
This gives me the _identityId. I'm hoping to get a value similar to this for the user pool signin.

Save AWS Cognito Users in DynamoDB

I recently started experimenting with AWS AppSync but I had some questions around AWS Cognito.
I would like for users to be able to authenticate with Facebook but I need their profile picture, name and email as data for my public user profiles in my app. So far, I noticed Cognito integrates with Facebook Auth but it does not allow access to the user information and this info does not get saved in a DynamoDB table.
My question is, how can I create a new User in DynamoDB when Cognito receives a new sign in, or return an existing user/id when the user already exists in the db.
I was trying to achieve the same a few weeks ago.
After reading the docs for hours, I realised that Cognito may not help us in regards to the data that comes back from FB or how to save it.
I ended up doing the following:
(1) Using FB-SDK, pulled in the user data.
(2) Invoked a Lambda function that saved this data (like FB_id,etc) to DynamoDB.
(3) If user logged in again, their FB_id (or email) was used to check against DynamoDB entries to retrieve their data.
If Cognito is able to help us and I missed it somehow, I would love to know.
Happy Coding!
You could use custom attributes and federating user from Facebook in your user pool to achieve this. Here are the steps at high level to do this.
You will first have to define custom attributes for the profile information you want to save in each user profile.
Define attribute mapping to link the custom attributes to Facebook attributes you want to save.
Build you application using Cognito hosted pages and federation to allow your users to log in using Facebook.
After this, on each new user log in in your app a new user is created in your user pool with all the attributes that were defined in attribute mapping and values which Cognito gets in the Facebook token. Your app will get these attribute values in the IDToken issued after authentication and you app can use these.
Additionally, if you want to store these attribute values outside of Cognito user pools profile, like your own DynamoDB table, you can configure a PreSignUp trigger in the pool which will be invoked on all new user creations. You can export the user attributes from this trigger to any database of your choice.
Hope this helps.
AWS AppSync allows you to access information in the GraphQL resolver which you can choose to store in a DynamoDB table. In your case for data coming from a Facebook profile you could pass this as arguments to a GraphQL mutation or in a header to AppSync which you can then access in the resolver via $ctx.request.headers.NAME where NAME is your header name. Then you could simply choose which attributes you want to write to DynamoDB for that user as part of the mutation. More information is in the reference guide here: https://docs.aws.amazon.com/appsync/latest/devguide/resolver-context-reference.html
Since you also asked that you'd like to do a check first to see if the user is already in the DDB first you could just do an existence check first:
{
"version": "2017-02-28",
"operation": "PutItem",
"key": {
"userId": $util.dynamodb.toDynamoDBJson($ctx.identity.username),
},
"attributeValues": $util.dynamodb.toMapValuesJson($ctx.args.input),
"condition": {
"expression": "attribute_not_exists(userId)"
},
}
This checks against the username from Cognito User Pools. If you were using the Cognito Federated Identities feature it would be ctx.identity.cognitoIdentityId. If the record is already there the response that comes back will tell you which means the user is already present. You could also transform the returned message in the response mapping template by looking at $ctx.result with a conditional statement and either building the JSON response by scratch or using one of the $util.error() methods in the guide above.
Finally as you mentioned that you'll have public profile data, you might want to mark this on certain records for control. In AWS AppSync you can filter GraphQL responses on authorization metadata such as this. You would just have an attribute (aka column) on the DynamoDB record marked 'public' or 'private. Then your response template would look like so:
#if($context.result.public == 'yes')
$utils.toJson($context.result)
#else
$utils.unauthorized()
#end
You can see more examples of this here: https://docs.aws.amazon.com/appsync/latest/devguide/security-authorization-use-cases.html#public-and-private-records