AWS Amplify Auth.updateUserAttributes() clear localStorage/sessionStorage - amazon-web-services

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'.

Related

Amplify Auth.signIn() ClientMetadata not sent to Lambda Trigger

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.

AWS Cognito/Amplify returning empty refresh token

I have a userpool in cognito which uses Google as the identity provider.
Now, using Amplify, we do a FederatedSign with provider as 'Google' as shown below.
Auth.federatedSignIn({ provider: "Google" });.
This gives me back the access token, id token. But the refresh token is empty.
This is for the oauth responseType:'token' configuration.
I have seen elsewhere that we need to change the grant type to 'code' i.e responseType: 'code' in order to get the refresh token.
But in this scenario, I am getting 'code = some-value' in the callback url and not the access token and refresh token.
What am I missing here?
My aim is to be able to get the refresh token - and using this Amplify would refresh the session once the access token in invalid.
You need to change oauth.responseType in your config to 'code' instead of 'token'. I'm getting an error when I do that and I'm not sure why, but this is what I found you need to do.
I am using parseCognitoWebResponse and had the same problem.
Within your User Pool go to App Clients. Check your Cognito App Client and make sure no client secret is generated. If it is filled in recreate an App Client without generating a Client Secret
Change the response_type to code
window.location.href = `https://${yourCognitoDomain}?response_type=code&client_id=${yourClientId}&redirect_uri=${cognitoRedirectUrl}`

How Do I Use Login With Amazon to Link A User Account to My Skill?

I am having a hard time getting this to work by following along with Amazon's Alexa documentation. I'm running aground on Account Linking because I can't figure out how to get Login with Amazon (LWA) to ask for alexa::skills:account_linking scope.
I've included the Amazon API library in my application and set that all up correctly and I'm invoking the process using the (globally available) amazon object as follows (typescript):
const options: any = {};
options.scope = ['profile', 'alexa::skills:account_linking'];
options.scope_data = {
profile : {essential: false}
};
options.response_type = 'code';
const self = this;
amazon.Login.authorize(options, (response) => {
if (!response || !response.code) {
throw { error: response };
}
// ... send the response code to my server
// ... to be exchanged for bearer and refresh tokens
});
What I would expect to happen from that is a popup Amazon login process to be spawned which (1) has the user log in to Amazon, and (2) collects the user's consent to link their Amazon account to my Alexa skill (i.e. linked to my credentialed hosted service), so that we get back (in the browser) an authorization code that we can exchange (on our server) for bearer and refresh tokens to act on behalf of the user.
The problem is, that code above immediately fails and never pops up a process. The message that is thrown says: "An unknown scope was requested". If I remove the 'alexa::skills:account_linking' string from the options.scope array, I get to an Amazon login screen, and if I log in to Amazon, my server does get an authorization code, etc. But no Account Linking has taken place, so I'm stuck.
I've tried to reconcile this documentation (which also talks about including a Skill ID somehow), with this documentation but I'm just not seeing how to make it work. Can anyone please help point me in the right direction about what I'm doing wrong here? It must be something pretty fundamental.
If your goal is to use Login with Amazon for account linking only for the skill and to not store the tokens on your own server, you can set up the skill and Login with Amazon with the below configurations. The advantage of this approach is that you don't need to stand up your own web server to just handle the LwA flow. This approach also handles all the flow out of the box, including refreshing tokens.
If you're using these tokens for another purpose, you may want to look into something like AWS Cognito to simplify the process.
Skill Account Linking Configuration
Replace Your Client ID with the LwA Client ID, replace Your Secret with the LwA Client Secret, and copy your redirect URIs
LwA Configuration
Paste your Alexa redirect URLs here. These will be specific to your vendor account so it's important to have the right ones.
Source: This is what I do for my Aberto Sonorus skill: https://www.amazon.com/WBPhoto-Aberto-Sonorus/dp/B078W199Z3 (edited screenshots attached)

Django REST framework - prevent data access for user view?

In my api, I have a /users endpoint which currently shows (eg address) details of all users currently registered. This needs to be accessed by the (Ember) application (eg to view a user shipping address) but for obvious reasons I can't allow anyone to be able to view the data (whether that be via the browsable api or as plain JSON if we restrict a view to just use the JSONRenderer). I don't think I can use authentication and permissions, since the application needs to log a user in from the front end app (I am using token based authentication) in the first instance. If I use authentication on the user view in Django for instance, I am unable to login from Ember.
Am I missing something?
UPDATE
Hi, I wanted to come back on this.
For authentication on the Ember side I'm using Ember Simple Auth and token based authentication in Django. All is working fine - I can log into the Ember app, and have access to the token.
What I need to be able to do is to access the user; for this I followed the code sample here https://github.com/simplabs/ember-simple-auth/blob/master/guides/managing-current-user.md
I have tested the token based authentication in Postman, using the token for my logged in user - and can access the /users endpoint. (This is returning all users - what I want is for only the user for whom I have the token to be returned but that's for later!).
The question is how to do I pass the (token) header in any Ember requests, eg
this.store.findAll('user') .... etc
This is clearly not happening currently, and I'm not sure how to fix this.
UPDATE
Fixed it. Turns out that the authorize function in my application adapter was not setting the headers, so have changed the code to set the headers explicitly:
authorize(xhr) {
let { access_token } = this.get('session.data.authenticated');
if (isPresent(access_token)) {
xhr.setRequestHeader('Authorization', `Token ${access_token}`);
}
},
headers: computed('session.data.authenticated.token', function () {
const headers = {};
if (this.session.isAuthenticated) {
headers['Authorization'] = `Token ${this.session.data.authenticated.token}`
}
return headers;
})
Ember is framework for creating SPAs. These run in the browser. So for Ember to get the data, you have to send the data to the browser.
The browser is completely under the control of the user. The browser is software that works for them, not for the owner of the website.
Any data you send to the browser, the user can access. Full stop.
If you want to limit which bits of the data the user can read from the API, then you need to write the logic to apply those limits server-side and not depend on the client-side Ember code to filter out the bits you don't want the user to see.
I don't think I can use authentication and permissions, since the application needs to log a user in from the front end app (I am using token based authentication) in the first instance. If I use authentication on the user view in Django for instance, I am unable to login from Ember.
This doesn't really make sense.
Generally, this should happen:
The user enters some credentials into the Ember app
The ember app sends them to an authentication endpoint on the server
The server returns a token
The ember app stores the token
The ember app sends the token when it makes the request for data from the API
The server uses the token to determine which data to send back from the API

WSO2 Implicit Flow not returning id_token

I am trying to authenticate a user from a custom web app with an OpenID Connect Service Provider within WSO2. I am following an answer on this article and added the Nuget package Thinktecture.IdentityModel.Client. My code is very similar to the linked article:
var client = new OAuth2Client(new Uri(serviceProviderAuthorizeUrl));
var url = client.CreateImplicitFlowUrl(
clientId,
redirectUri: redirectUrl,
scope: scope,
nonce: Guid.NewGuid().ToString());
Response.Redirect(url);
The url comes out to be: https://{wso2_url}/oauth2/authorize?client_id={my_client_id}&response_type=token&scope=openid&redirect_uri=https%3A%2F%2F{mydomain}%2F{my_app}%2FCallback.aspx&nonce=f0db4eac-18df-46f6-92f1-c28ba621596d
Now this does work and returns an access_token: https://{my_domain}/{my_app}/Callback.aspx#token_type=Bearer&expires_in=970&access_token=067e3366217798986912326a86abd92f
My issue is that I have no idea who the user is. Further more, this WSO2 article shows that if I pass a response_type:id_token I should be able to decode the response and find out who the user is by using the "sub" attribute but I am not getting the id_token response. The code above creates a url with a response_type of token instead. Simply changing the response_type gives me an error. How can I use implicit flow in WSO2 and get the id_token response?
I followed this article for the configuration of WSO2. I currently have Implicit and Client Credential checked.
Make sure you have these as query parameters.
response_type=id_token
client_id=xxxxx
redirect_uri=http://xx.com/xx/x
nonce=xxxx
scope=openid
Make sure that you pass on the scope intended to use for the Idp (WSO2) to know what data it needs to return. Make sure your scop
scope=openid.