I am looking at the AWS Amplify and AWS Cognito documentation, and I need something explained. Take the following code:
Auth.signIn({
username, // Required, the username
password, // Optional, the password
validationData, // Optional, a random key-value pair map which can contain any key and will be passed to your PreAuthentication Lambda trigger as-is. It can be used to implement additional validations around authentication
}).then(user => console.log(user))
.catch(err => console.log(err));
Here is some more code from the documentation:
Auth.signUp({
username,
password,
attributes: {
email, // optional
phone_number, // optional - E.164 number convention
// other custom attributes
},
validationData: [] //optional
})
.then(data => console.log(data))
.catch(err => console.log(err));
My question is, where do the tokens get stored? Do you store them in state? If so, how do they get refreshed when they do. Or does Auth take care of this and you can just call auth when you need to. If so, do you have to wrap your entire ap with withAuthenticator? I don't understand this. Thanks!
Also, if you want a secure endpoint with AppSync, how does this work? Does it automatically check auth? You're not sending a token so I don't understand how this works. Thanks for your help!
If you're using Amplify it will keep the controls inside the LocalStorage. This will be managed by the Amplify Library. It is also responsible to request a new token when the first one is expired. Again: You don't need to worry about this, the library will manage this for you.
The thing is: you also must use Amplify to send your request o AppSync. When you use Amplify the library will see that you are making a request to a AWS Resource and you're logged in in Cognito and it will append the needed HTTP headers to the request before send it. All this will be done for you by the Amplify library. You can just use it..
Related
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'.
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.
We are moving our auth to Cognito and need to alter the token we get from Cognito. We are using a Pre Token Generation Lambda Trigger to accomplish this. We are also using Amplify's Auth library. However, I can not access the clientMetadata we are sending with Auth.signIn().
On the front-end we simply have:
const user = await Auth.signIn(username, password, { metadataKey: metadataValue });
It appears the request is being sent properly becuase in the Request Payload on the network tab we have:
{
AuthFlow: ...,
AuthParameters: ...,
ClientId: ...,
ClientMetadata: { metadataKey: metadataValue }
}
In the lambda function I am simply logging the event to the console:
exports.handler = async (event, context, callback) => {
console.log('Event:', event)
callback(null, event)
}
In the AWS Cloundwatch logs, the event is logging each time we sign-in from the application (so everything seems to be set up properly), but the event does not include a clientMetadata property as part of the event.request.
So ultimately, everything runs right, no errors or anything like that, we get our tokens back from Cognito, but the clientMetadata is nowhere to be found in the Lambda function, preventing us from performing the necessary logic in the Lambda function to adjust our token.
Links:
From the signIn Amplify Docs it appears we are calling this properly.
From the Pre Token Generation Lambda Trigger Docs, it appears the clientMetadata property should exist at event.request.clientMetadata.
This is a related stackoverflow question, but either I am doing something incorrectly, or AWS changed this, because the individual who asked the question was able to access clientMetadata from Lambda using this same syntax I am using to send it on sign-in.
Any help with this would be tremendously appreciated.
UPDATE:
This seems to be because we are using the authentication flow "USER_PASSWORD_AUTH". This flow is required for smooth user migration which is why we are using it, but it seems to omit the clientMetadata we send.
Try setting the metadata value using Auth.configure before executing Auth.signIn. On one hand, it doesn't look like the sign-in event is a pre-token lambda trigger, which explains why the metadata isn't being passed. I appreciate that this is not especially intuitive. That said, on the other hand and assuming you will need to access the same metadata value when tokens are generated as part of a refresh, you'll likely need to cover additional non-initial-signIn events anyways. Using Auth.configure looks to do the trick for both.
When using the "USER_PASSWORD_AUTH" flow, clientMetadata is omited from the request (not sure if this is a bug or intended behavior). Changing back to "USER_SRP_AUTH" resolved this issue and clientMetadata appears in the request as expected.
I have an AWS AppSync schema with the default authorization mode set to Amazon Cognito User Pool. I make calls to this AppSync endpoint from a web app using AWS Amplify GraphQL Client and, coherently, its configuration points Cognito User Pools as authentication type, too:
aws_appsync_authenticationType: 'AMAZON_COGNITO_USER_POOLS'
It works as expected when the user is authenticated; however (although the involving Cognito Identity Pool has proper Auth and Unath roles set already), when the website runs some Amplify fetch command like for a unauthenticated(guest) user:
const item = await API.graphql(graphqlOperation(getItem, { id: 'my-id' }))
Ends up with throwing an error:
"No current user"
Well, I expected it to perform if I allow unauthenticated users, but it simply fails. Seeking for a way out, I found some discussions like:
a GitHub issue comment here,
another Github issue,
or an SO question here.
And, all of the above suggest revisiting the Amplify configs so that the AppSync authentication type is converted from AMAZON_COGNITO_USER_POOLS to AWS_IAM or API_KEY. However, for some detailed reason 1:
I want to stick with AMAZON_COGNITO_USER_POOLS authentication type,
And still be able to fetch some AppSync resources as a guest user unless they are restricted with
#aws_auth decorators or such.
Is it possible in any way?
1 I have more granular controls depending on the user's group (admin, normal etc.) with decorators such as #aws_auth(cognito_groups: ["default-user-group"]) on the AppSync schema. So, I need Cognito User Pools for that usage.
So, I just went through a similar issue and managed to get it sorted. I hope this might help you sort this out. The SO question you mentioned in your question is almost the right way to do it. However, there are one "little" tiny details that are not documented and took me a while to find out.
Apart from having to enable both authenticated and unauthenticated access by running amplify update auth (you can see how in the SO linked above) there are other tweaks you need to do.
First, in your model you need to adjust your rules to be something like:
#auth(
rules: [
# allow owners ability to update and delete their these messages (user pools)
{ allow: owner },
# allow all authenticated users to access this data
{ allow: private, provider: userPools },
# allow all guest users (not authenticated) to access this data
{ allow: public, provider: iam }
]
)
Once you set up this model to allow user pools to access all the data they "own", you can also let any "guest" user to access the data too.
In the frontend, let's get your code as an example, you need to use a bit of a different approach:
Instead of
const item = await API.graphql(graphqlOperation(getItem, { id: 'my-id' }))
try something like
// Check if the user is logged in or not
let isLoggedIn = await isLoggedIn();
const item = (await API.graphql({
query: getItem,
variables: { id: 'my-id' },
authMode: isLoggedIn ? GRAPHQL_AUTH_MODE.AMAZON_COGNITO_USER_POOLS : GRAPHQL_AUTH_MODE.AWS_IAM,
}));
by the way the "isLoggedIn" function look like this
async function isLoggedIn() {
// Another way to get if its a guest or not
//return await Auth.Credentials.getCredSource() === "guest"
try {
await Auth.currentAuthenticatedUser();
return true;
} catch {
return false;
}
}
So... this line is what it does the trick, which is not really well documented.
authMode: isLoggedIn ? GRAPHQL_AUTH_MODE.AMAZON_COGNITO_USER_POOLS : GRAPHQL_AUTH_MODE.AWS_IAM
you need to path the different methods according to the state of the user (logged in or guest), not just the AWS_IAM one.
This will only get you as far as READING the data, in order to make sure guests can do Create/Update/Delete, and separate the data ownership from each other and logged users, that's a completely different story that you will need to start digging up on resolvers to get it sorted. But the good news is, there is a way :)
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!