Do you want to request a feature or report a bug?
both
What is the current behavior?
I am attempting to implement a basic cross-site cookie implementation. We have two applications. The first an Ember app which uses Ember-Simple-Auth to manage internal session state which is stored separately from the tokens. Tokens are managed by the amazon-cognito-identity-js package. Initial implementation was using the previous standalone library. Now using the package as included in the amplify library.
When everything was stored via local-storage all worked well. Initial transition to cookie storage also worked as expected.
The second site uses React and Amplify and is leveraging Amplify to handle the application authenticated-state management. Login and logout are handled by the Amplify-provided High-Order Component. No custom implementation there.
Presently, when I log in via the React/Amplify app, and then refresh the Ember app, I am able to take the tokens in cookie storage and successfully construct enough of a session and user credentials object to create a logged-in state on the Ember app and query data. However, signing in to the Ember app and then refreshing the React app from a logged-out state does not see what it needs in cookie storage to then see itself as logged in after the refresh.
Also, when logging out from the Ember app using the cognitoUser.signOut method, the cookies are not removed from storage and thus when attempting to redirect to the landing page just constructs a new session instead. When attempting to use cognitoUser.globalSignOut which should invalidate and remove the tokens complains in the isFailure block that the access token has been revoked.
If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem. Your bug will get fixed much faster if we can run your code and it doesn't have dependencies other than AWS Amplify.
What is the expected behavior?
Ideally, I should be able to log in to one application and be seen by the other as being logged in. Logging out should prevent any attempt in the other application to redirect back to the login page per each application's unauthenticated behavior.
Which versions of Amplify, and which browser / OS are affected by this issue? Did this work in previous versions?
0.4.4 in Firefox / Linux. I have not had any success with previous versions.
You can turn on the debug mode to provide more info for us by setting window.LOG_LEVEL = 'DEBUG'; in your app.
A few supporting code snippets:
In my Ember app, before loading the landing page, I check to see if the Ember-Simple-Auth tool has an existing session, and if not, whether there are any cookies in the CookieStorage.
beforeModel() {
this._super(...arguments);
if (
!this.get('session.isAuthenticated') &&
Object.entries(this.get('aws.loginCookies')).find(([cookieName]) =>
cookieName.includes('accessToken')
)
) {
this.get('session')
.authenticate('authenticator:cognito', {})
.then(() => {
this.transitionTo('index');
});
}
}
Because we don't expect any authentication credentials, we send an empty object to the cognito authenticator (Ember parlance for the Ember-Simple-Auth abstraction that uses the cognito-identity library to stand up the Ember-layer's session). The this.get('aws') points to an Ember service where the actual cognito-identity library and other aspects of the AWS SDK's methods are abstracted.
Inside that authenticate hook, we check and see if there is a LastAuthUser cookie in CookieStorage, and check to see if the authenticate request has a username and password in place. If so, we proceed to the usual authentication logic, otherwise we jump to the restore hook (which Ember-Simple-Auth uses to refresh it's session)
authenticate(authenticationData) {
if (
!authenticationData.Username &&
!authenticationData.Password
) {
const username = Object.entries(this.get('aws.loginCookies')).find(
([cookieName]) => cookieName.includes('LastAuthUser')
)[1];
return this.restore({ username });
}
[...]
}
Inside the restore hook we look for whether the hook is provided with a current session object (from Ember Simple Auth) or just a username from the authenticate hook.
Once we have a username established, we use the username to assemble the cognitoUser object, configure a credentials object, and then return the session to the Ember Simple Auth service.
restore(data) {
if (!data) {
return;
}
const username = data.username || data.accessToken.payload.username;
const cognitoUser = this.get('aws').getCognitoUser(username);
const region = get(this, 'aws.region');
return new RSVP.Promise((resolve, reject) => {
cognitoUser.getSession((err, session) => {
if (!AWS.config.region) AWS.config.region = region;
if (!AWS.config.credentials) {
AWS.config.credentials = this.get('aws.getCognitoCredentials');
}
if (AWS.config.credentials.needsRefresh()) {
AWS.config.credentials.params.Logins = {
[session.idToken.payload.iss.replace('https://', '')]: session
.idToken.jwtToken
};
AWS.config.credentials.refresh(err => {
if (err) {
console.log('inside credentials refresh error block: \n', err);
reject(err);
}
});
}
if (!cognitoUser.signInUserSession) {
cognitoUser.setSignInUserSession(session);
}
resolve(session);
});
});
This gets me into the application, able to make requests, and function in my application. I can sign in from our React app using Amplify and without having to re-authenticate stand up an otherwise functional session in the Ember app.
A quick update from the previous post. Setting the setSignInUserSession prior to logout now _does_ allow the cognitoUser.signout() method to work. cognitoUser.globalSignOut() still fails with the access token error described above. At this point, I'm not sure I understand the difference between the two methods, or what the different use cases would be.
One thing I think would be very helpful for the library, which might mitigate the kinds of issues I'm having would be to have a 30,000ft view of what the Cognito authenticated session looks like; what it contains, and what needs to be in place for all methods to function. The step-by-step instructions in the amazon-cognito-identity-js package README.md is useful for the majority of scenarios, but as soon as someone needs to operate out of the primary use-cases handled by the 32 documented steps things tend to go immediately off the rails as there's no documentation of the what and why, only the how.
@dehuszar thanks for you feedback!
signOut and globalSignOut is that globalSignOut will send a http request to Cognito service which _'signs out users from all devices'_ I am not familiar with this api call but I think as far as it cleans those cached tokens, your apps should be signed out since nowuserpool.getCurrentUser() will return null object.That's very helpful, thank you! I'll report back after some testing
Okay. So, I've done a few tests and have some thoughts. One thing that might be worth doing is differentiating the var names for different implementations of cognitoUser in the docs. One cognitoUser is the result of the new AmazonCognitoIdentity.CognitoUser(userData) statement, and then again cognitoUser is created later in step 17 cognitoUser is defined from userPool.getCurrentUser(). Because the docs presume the steps before the one being discussed, if I'm jumping around I can pretty easily forget which method I should be using, or if there is any meaningful difference.
At least few sentences about the lifecycle of each variable being set would help quite a bit. Especially as the examples seem to imply that all the AWS stuff (like AWS.config.credentials) is being stored as a global var, whereas most implementations --at least mine do-- eschew any global scope definitions as much as possible and try to rely on local scope, services, or passed state. Assuming or checking for a global value from inside of a locally scoped component === code smell.
An example of how this can get confusing is, when authenticating I'm setting vars up in the scope of my login component or on the page that contains it. But in the other instance where I'm logging out, the code snippet assumes I've already set up my userPool instance and can just reference it. The docs seem to indicate that I should have to reconstruct the variables every time, but that makes me wonder where the values are being stored when I do things like cognitoUser.setSignInUserSession(). I would presume that when I create the var from the new AmazonCognitoIdentity.CognitoUser(userData) method and run setSignInUserSession that the resulting user session data would then be available when I view the output of userpool.getCurrentUser(). Instead that cognitoUser object's signInUserSession is null.
Consider:
new AWSCognito.CognitoIdentityServiceProvider.CognitoUser(userData)
.authenticateUser(authenticationDetails, {
onSuccess: function(result) {
AWS.config.region = region;
AWS.config.credentials = credentials;
cognitoUser.setSignInUserSession(result);
console.log('currentUser: ', cognitoUser);
console.log(new AWSCognito.CognitoIdentityServiceProvider.CognitoUserPool(
get(this, 'poolData')
).getCurrentUser());
The result of the first console.log has my signInUserSession, whereas the 2nd doesn't. This kind of disconnectedness between processes makes the whole tool chain difficult to reason about.
Turning back to the originating issue, I have a non-null value being returned from getCurrentUser, but the React app in the next tab is still not seeing the contents of the CookieStorage has enough of an authenticated state, which makes me wonder if I need to do something else to set the signInUserSession value in the userPool and that's why the React app isn't loading, or if there's another piece missing.
@dehuszar yeah I think the readme is quite old which those code samples are following ES5 standard. If you are coding in ES6, you can do like:
import { CognitoUserPool, CongitoUser } from 'amazon-cognito-identity-js';
const userPool = new CognitoUserPool(data);
// when you sign in
const user = new CognitoUser(userData);
user.authenticateUser(authenticationDetails, {
onSuccess: (session) => {
....
}
});
// when you want to get a user object when reloading or refreshing:
userPool.getCurrentUser().then(user => {
// After get the user from storage, retrieve the session
user.getSession();
// or you have session then manually set it into the user object
user.setSignInSession(session);
});
You can have a look at how Amplify starts the authentication process: https://github.com/aws/aws-amplify/blob/master/packages/aws-amplify/src/Auth/Auth.ts#L371
and how it get the current authenticated user: https://github.com/aws/aws-amplify/blob/master/packages/aws-amplify/src/Auth/Auth.ts#L794
Okay. I've made it a quick step forward, but hit another hurdle. As before I can sign in from the React / Amplify app, refresh my Ember app (using just the amazon-cognito-identity-js package) and it will properly restore it session. I can now also sign _out_ from either app and have the cookies properly invalidated / removed.
I am still unable to sign _in_ from the Ember app and have the React app pull the session. When tracing through the getCurrentUser() during the Amplify-Auth authenticate process, I am able to complete the call, whereas when I have the Ember app create the cookies, the cookies are not seen by the this.storage.getItem(lastUserKey) call in the getCurrentUser method. The cookies domain is set to localhost, with no path set in either app. I am wondering if the Amplify-powered process is more particular about the difference in ports? From my local-dev environment the Ember app runs on port 4200, whereas the React app runs on port 3000.
@dehuszar in your ember app, did you pass your cookieStorage when initializing the user pool object? like:
const userPoolData: ICognitoUserPoolData = {
UserPoolId: userPoolId,
ClientId: userPoolWebClientId,
};
if (cookieStorage) {
userPoolData.Storage = new CookieStorage(cookieStorage);
}
this.userPool = new CognitoUserPool(userPoolData);
Yup, I am using the Storage property on all my invocations of the user pool
From I know Amplify doesn't do anything to that Storage part. Can you try to sign in with the ember app and then iterate all the items from the CookieStorage from both apps to see if there is any difference?
@powerful23 Other than the time-related attributes, dti is the only attribute which might be of issue (though I don't fully understand what it's purpose is).
Would ports on domains be an issue? HTTP vs HTTPS? I have secure set to off for the moment.
Sorry but what's dti? I don't know if HTTP or HTTPs matters. As long as your apps are under the same domain then they should be able to access the CookieStorage. @mlabieniec if you know about dti
By the way, if you have secure on then you need to have to use HTTPS, according to https://github.com/js-cookie/js-cookie#secure
Yeah, I'll turn secure on as we move towards production, but it's not critical while we stand things up and is set to false in the current configuration.
The jti attribute is shorthand for the JWT id. It appears to just be a token uniqueness tool, but I wasn't sure if that uniqueness might be being enforced in some way. I'm going to try getting the React server running on HTTPS and see if that makes a difference.
Just to ask again, as it hasn't been addressed yet, port differences shouldn't matter, should they? The Ember server runs on https://localhost:4200 and the React server runs on http://localhost:3000 The cookies are just set to the localhost domain.
Looks like even with secure: false, the tokens won't work across sites unless they share the same protocol. Switching the React app to use HTTPS did the trick.
Thanks for everyone else's help
Most helpful comment
One thing I think would be very helpful for the library, which might mitigate the kinds of issues I'm having would be to have a 30,000ft view of what the Cognito authenticated session looks like; what it contains, and what needs to be in place for all methods to function. The step-by-step instructions in the amazon-cognito-identity-js package README.md is useful for the majority of scenarios, but as soon as someone needs to operate out of the primary use-cases handled by the 32 documented steps things tend to go immediately off the rails as there's no documentation of the what and why, only the how.