Even though Ember Auth doesn't automatically inject custom objects returned upon Sign In, it allows you to grab them by inspecting the response object:
actions:
signIn: ->
#auth.signIn(
data:
user:
email: #get 'email'
password: #get 'password'
).then( (response) ->
console.log response['user_context']['roles']
)
The Sign In action is triggered by my Log In page's submit button, and, in turn, calls the authentication API service.
However, when rememberable is used and the page is refreshed, Ember Auth calls the API service with the remember token and retrieves the same response object as the Sign In action.
How can I grab the response object in this scenario? I need to be able to have access to objects such as user permissions/roles, so that I can build menu bar items based on them. This is just one example.
UPDATE:
Pretty much right after asking this question I realized EmberAuth wasn't gonna cut it for my needs. So I rolled my own Authentication solution. Wasn't as hard as I thought, and much lighter and easier for me to use/customize than the alternative - SimpleAuth.
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'.
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
Here's my workflow.
User presses a login button.
User is redirected to the API which redirects to a login page on a third party site (ADFS SAML).
User authenticates there, is returned to the API which decodes the response, creates a user and a JWT token (both stored in a datastore), and then redirects the user to Ember with a ?token= query parameter.
What I'd like to do is work this in to a custom ember-simple-auth authenticator (and authorizer?) in order to take advantage of the niceness of session and Authorization headers from there on.
Currently I've hacked an authenticator to do just the redirect to the identity provider. The rest of the API workflow is working. And I'm catching the token in a route on the way back and populating a User resource via a call to the database. But I feel that I'm heading down the wrong (and the long) path.
Any thoughts? Is this something that ember-simple-auth-token is designed for?
I solved this in last month. what i did was
click the login
redirect to the adfs
adfs login success-> redirect to my backend
backend generate a sso temp token with access token details which is
already there and only valid for 2 minutes.
backend redirect web with a GET with params of sso temp token
frontend take the params of sso temp token and make another post
ajax request to backend
Api will validate sso temp token and provide details ( access
token and refresh token ) to the user
read response data for the post request in 6 and update on ember
simple auth service using authenticate interface ( you can
customize this using custom authenticator )
here is the custom authenticator class
import Base from 'ember-simple-auth/authenticators/base';
import {isEmpty} from '#ember/utils';
export default Base.extend({
authenticate(data) {
return new Promise((resolve, reject) => {
if(data.access_token!=null){
resolve({
scope: data.scope,
access_token: data.access_token,
});
}else{
reject();
} })
},
restore(data) {
return new Promise((resolve, reject) => {
if (!isEmpty(data.access_token)) {
resolve(data);
} else {
reject();
}
});
},
});
i have created a route to handle in the frontend to get the token which is sent by backend. so the route takes sso token by a get param ( cause backend cannot do a post to emberapp) so when the route triggers i catch the param in route and do a post method again to validate the token.
we are using redis generated tokens. so since this question is asked about the emberjs i illustrated the answer to support you on frontend. i have illustrate the concept as well to show what you need to do. so its up to you to dig up and find more about it. like how to create temporary JWT token, how to validate it, how to redirect to you to your backend with claims from the adfs and etc. i think i provide answer to the initial question you asked which is
"How to implement an Ember Simple Auth using an external redirect"
The page to login to our application is a jsp hosted on another machine. I have managed to get requests firing to this machine by modifying authenticated-route-mixin by allowing window.location.replace to be called if the route start with http.
beforeModel(transition) {
if (!this.get('session.isAuthenticated')) {
Ember.assert('The route configured as Configuration.authenticationRoute cannot implement the AuthenticatedRouteMixin mixin as that leads to an infinite transitioning loop!', this.get('routeName') !== Configuration.authenticationRoute);
transition.abort();
this.set('session.attemptedTransition', transition);
debugger;
if (Configuration.authenticationRoute.startsWith('http')) {
window.location.replace(Configuration.authenticationRoute);
} else {
this.transitionTo(Configuration.authenticationRoute);
}
} else {
return this._super(...arguments);
}
}
This is working but when I am redirected back to my application, ember-simple-auth thinks I am no longer logged in and redirects be back to the remote machine, which then sends me back to the application in an infinite loop.
Obviously I need to set something to let ember-simple-auth know that it it is actually logged in. Why is it not doing this automatically? What am I doing wrong?
I am pretty new to oAuth so I could be missing some basic setting here.
Here is the URL.
ENV['ember-simple-auth'] = {
authenticationRoute: 'https://our-server.com/opensso/oauth2/authorize?client_id=test-client-1&response_type=code&redirect_uri=http%3A%2F%2Flocalhost%3A4200%2Fsecure'
};
Instead of modifying the AuthenticatedRouteMixin, I'd recommend handling your app-specific login in an Authenticator-- the key configuration primitive that Ember Simple Auth provides as part of its public API.
To the best of my understanding, on first loading the app, and checking to see if a user is authenticated, Ember Simple Auth will use the restore method, defined as part of the Authenticator API.
You can return a promise from restore that resolves or rejects to indicate whether the user is authenticated. How you check this is an implementation detail of your auth system.
I don't know how you're storing credential(s) on the client (would be great if you could provide more detail), but here's an example flow, using cookies for authentication:
Ember boots, ESA attempts to restore the session.
restore makes a simple AJAX request to a secured, "dummy" resource on your Java server-- and checks if it gets back a 200 or a 401.
We get a 401 back. The user isn't authenticated, so reject in the Promise returned from restore.
Let ESA redirect the user to your authentication route. Ideally, don't override the AuthenticatedRouteMixin-- instead, use the beforeModel hook in the authentication route to send users to your JSP login page.
The user correctly authenticates against the JSP form.
In its response, your Java server sets some kind of encrypted, signed session cookie (this is how it generally works with Rails) as a credential. In addition, it sends a redirect back to your Ember app.
Ember boots again, ESA calls restore again.
restore pings your Java server again, gets a 200 back (thanks to the cookie), and thus resolves its Promise.
ESA learns that the user's authenticated, and redirects to the 'route after authentication'.
Keep in mind that, at its core, ESA can only indicate to the client whether the backend considers it 'authenticated' or not. ESA can never be used to deny access to a resource-- only to show something different on the client, based on the last thing it heard from the backend.
Let me know if any of that was helpful.
How would I implement a solution like described below? Would it be easier to jack in something like ember-simple-auth? All examples I find implement their own login form, that's not something I'll have. My login will live on its own domain.
Scenario:
user visits https://myapp.com.
In some initializer I see that they don't have an/don't have a valid access token so I redirect them to https://login.myapp.com?redirect_uri=https://myapp.com
login.myapp.com sign the user in hand hands them an access token and redirects user to https://myapp.com?access_token={token}
Myapp boots again only this time it finds a valid access token (from the url) and sets some variable that the user is logged in. And then adds the token as header bearer onto all future xhr requests.
Is this a logical login approach? Some fundamental flaws I'm missing? Do you know any tutorials doing something like this? Thanks!
That flow would be pretty easy to implement with Ember Simple Auth. You simply need to define a custom authenticator that you authenticate the session with when the application starts up again after the redirect. The authenticator's authenticate method would look sth. like this:
authenticate: function(options) {
return new Ember.RSVP.Promise(function(resolve, reject) {
resolve({ token: /* the token you get from the query string */ });
});
}
Authenticating the session with the authenticator in an initializer then is as easy as:
this.get('session').authenticate('your-authenticator');