As I understand from the documentation a Service is basically a singleton object used to provide services to other objects through the application lifecycle. I have a user management service which I use to save an authentication token after the user logs in using the route /users/login. But transitioning to another route (/composer for instance) causes the service instance to be recreated and hence it loses all the stored data. Doesn't this contradict the fact that it should live as long as the application does or do I have a wrong of idea this whole lifecycle thingy?
I'm injecting the service in all my routes as below:
authenticationService: Ember.inject.service('authentication-service'),
The service itself is only a set of getters and setters:
import Ember from 'ember';
export default Ember.Service.extend({
currentUser: undefined,
jwtToken: undefined,
// ================================================================================================================ \\
// ================================================================================================================ \\
// ================================================================================================================ \\
setCurrentUser(user) {
this.currentUser = user ;
},
getCurrentUser() {
return this.currentUser ;
},
isLoggedIn() {
return Boolean(this.currentUser) ;
},
getJwtToken() {
return this.jwtToken ? this.jwtToken : '' ;
},
setJwtToken(jwtToken) {
this.jwtToken = jwtToken ;
}
});
Here is how the login token is handled:
actions: {
onSubmitLoginForm() {
if (!this.validateLoginForm()) {
return ;
}
var self = this ;
Ember.$.post('login/', {
'username': this.controller.get('username'),
'password': this.controller.get('password'),
'email': this.controller.get('email'),
}, function(data) {
console.log(data) ;
if (data['success'] === 'Ok') {
self.get('authenticationService').setJwtToken(data['auth']['jwt']) ;
var user = self.get('store').createRecord('user', {
username: data['auth']['user']['username'],
email : data['auth']['user']['email'],
mongoId : data['auth']['user']['id']['$oid'],
}) ;
self.get('authenticationService').setCurrentUser(user) ;
self.transitionTo('home') ;
console.log('logged in') ;
console.log(self.get('authenticationService').getJwtToken()) ;
console.log(self.get('authenticationService').getCurrentUser()) ;
console.log(self.get('authenticationService').isLoggedIn()) ;
} else {
self.transitionTo('error') ;
}
}) ;
},
}
I'm not looking for suggestions on using some other means of persistence such as IndexedDB; I'm willing to understand how this thing actually works so any explanation is appreciated.
Yes, you understand it right - service is a singletone and I can assure you that service persists its state between trasitions. But to make a transition, you must use link-to helper. If you are changing url manually, you reloading your app instead of transitioning. And app reload of course causes state reset. You should use any available kind of storage to persist state between page reloads. It may be local storage, session storage, cookies etc.
Also, in Ember we don't use such code: this.currentUser = user ; on Ember objects. We use this.set('currentUser', user); instead. Otherwise Ember would not be able to rerender templates, update computed properties and work properly.
And finally, you shouldn't build auth solution from zero. It's very hard and complex thing to do. Instead, you can use ember-simple-auth addon and build authentication process on top of it. It will be much easier and result will be more reliable.
Related
Using: ember-cli v2.5, ember-simple-auth v1.1.0
I got trouble understanding if I can properly store additional token inside a custom ember-simple-auth's cookie based session-store.
I'm trying to store a shopping-cart token to be sure it survives browser refresh.
I started to create a shopping-cart service to handle init, add, remove etc ... regarding if the session has a token.
Here is my app/session-stores/application.js
// app/session-stores/application.js
import Cookie from 'ember-simple-auth/session-stores/cookie';
export default Cookie.extend({
orderToken: null
});
Doesn't seems to be used. The session service still use the adaptive store.
Here is my shopping-cart service
// app/services/shopping-cart.js
import Ember from 'ember';
export default Ember.Service.extend({
store: Ember.inject.service('store'),
session: Ember.inject.service('session'),
basket: null,
[...]
init() {
this._super(...arguments);
let orderToken = this.get('session.orderToken'); // First try
// let orderToken = this.get('session.store.orderToken'); // Second Try
if (orderToken) {
this.get('store').findRecord('order', orderToken).then((order) => {
this.set('basket', order);
})
}
},
[...]
_createBasket() {
return this.get('store').createRecord('order', {}).save().then((order) => {
this.set('basket', order);
this.set('session.orderToken', order.token); // First try
// this.set('session.store.orderToken', order.token); // Second Try
});
}
})
Then the idea will be to inject the service wherever I need. Unfortunately, It doesn't work, and I don't really know if I can do it or if it's the right way to do it.
Any advices, answers will be much appreciate !
I am currently using ember-cookie and it's working like a charm. I am juste trying to play with ember-simple-auth and understand all my possibilities.
I have the following situation: The user enters his credentials and clicks a Login button. An API call is done in the action creator via redux-thunk. When the API call was successful, another action is dispatched containing the response from the server. After the (successful) login I want to store the users session id in a cookie (via react-cookie).
Action creator
export function initiateLoginRequest(username, password) {
return function(dispatch) {
dispatch(loginRequestStarting())
return fetch('http://path.to/api/v1/login',
{
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: username,
password: password
})
})
.then(checkStatus)
.then(parseJSON)
.then(function(data) {
dispatch(loginRequestSuccess(data))
})
.catch(function(error) {
dispatch(loginRequestError(error))
})
}
}
export function loginRequestSuccess(user) {
return {
type: ActionTypes.LOGIN_REQUEST_SUCCESS,
user
}
}
Reducer
export default function user(state = initialState, action) {
switch (action.type) {
case ActionTypes.LOGIN_REQUEST_SUCCESS:
cookie.save('sessionId', action.user.sid, { path: '/' })
return merge({}, state, {
sessionId: action.user.sid,
id: action.user.id,
firstName: action.user.first_name,
lastName: action.user.last_name,
isAuthenticated: true
})
default:
return state
}
}
Right now the reducer responsible for LOGIN_REQUEST_SUCCESS saves the cookie. I know the reducer has to be a pure function.
Is saving a cookie in the reducer violating this principle? Would it be better to save the cookie inside the action creator?
Have a look at redux-persist.
You can persist/save your reducers (or parts of them) in LocalStorage.
Concept
Initiate login.
Receive cookie from server.
Dispatch login success.
Reducer stores cookie in memory.
Persist middleware stores reducer state in LocalStorage.
Example
Install
npm install --save-dev redux-persist
Example Usage
Create a component that wraps the persistence/rehydration logic.
AppProvider.js
import React, { Component, PropTypes } from 'react';
import { Provider } from 'react-redux';
import { persistStore } from 'redux-persist';
class AppProvider extends Component {
static propTypes = {
store: PropTypes.object.isRequired,
children: PropTypes.node
}
constructor(props) {
super(props);
this.state = { rehydrated: false };
}
componentWillMount() {
const opts = {
whitelist: ['user'] // <-- Your auth/user reducer storing the cookie
};
persistStore(this.props.store, opts, () => {
this.setState({ rehydrated: true });
});
}
render() {
if (!this.state.rehydrated) {
return null;
}
return (
<Provider store={this.props.store}>
{this.props.children}
</Provider>
);
}
}
AppProvider.propTypes = {
store: PropTypes.object.isRequired,
children: PropTypes.node
}
export default AppProvider;
Then, in your index.js or file in which you set up the store, wrap the rendered components in your new AppProvider.
index.js
...
import AppProvider from 'containers/AppProvider.jsx';
...
render((
<AppProvider store={store}>
...
</AppProvider>
), document.getElementById('App'));
This will serialize your user reducer state to LocalStorage on each update of the store/state. You can open your dev tools (Chrome) and look at Resources => Local Storage.
I'm not sure if this is the "right" way, but that's how my team is persisting the logged user in the Redux app we built:
We have a very default architecture, an API ready to receive requests in one side, and a React/Redux/Single Page app that consumes this API endpoints in the other side.
When the user credentials are valid, the API's endpoint responsible for the login respond to the app with the user object, including an access token. The access token is latter used in every request made by the app to validate the user against the API.
When the app receives this user information from the API two things happen: 1) an action is dispatched to the users reducer, something like ADD_USER, to include this user in the users store and 2) the user's access token is persisted in the localStorage.
After this, any component can connect to the users reducer and use the persisted access token to know who is the logged user, and of course if you have no access token in your localStorage it means the user is not logged in.
In the top of our components hierarchy, we have one component responsible to connect to the users reducer, get the current user based on the access token persisted in the localStorage, and pass this current user in the React's context. So we avoid every component that depends on the current user to have to connect to the users reducer and read from the localStorage, we assume that this components will always receive the current user from the app's context.
There are some challenges like token expiration that adds more complexity to the solution, but basically this is how we are doing it and it's working pretty well.
I'd probably have the server-side set the cookie, personally, and make it transparent to JavaScript. But if you really want to do it client-side, I'd do it in an action helper. Something like this:
// Using redux-thunk
function login(user, password) {
return dispatch => api.auth.login(user, password)
.then(result => setCookie())
.then(() => dispatch({type: 'USER_LOGGED_IN'}))
}
Or something like that.
Action helpers don't need to be pure, but reducers should be. So, if I'm doing side-effects, I put them into action helpers.
We're working with two ember applications that each run different version of ember and ember-simple-auth, and want to get ember-simple-auth to work well with both version.
The old app
Ember 1.8.1
Ember-simple-auth 0.7.3
The new app
Ember 2.3.1
Ember-simple-auth 1.0.1
Uses cookie session store
We trying to change the session API for the older version so that it stores the access and refresh tokens correctly so the new app can use it.
So far, we’ve tried overriding the setup and updateStore methods to work with the authenticated nested object but are still running into issues.
Disclaimer - Patrick Berkeley and I work together. We found a solution after posting this question that I figured I would share.
In order for a 0.7.3 version of ember-simple-auth's cookie store to play nicely with a 1.0.0 version, we did have to normalize how the cookie was being formatted on the app with the earlier version in a few key places, mostly centered around the session object (the 0.7.3 session is an ObjectProxy that can be extended in the consuming app to create your own custom session).
The methods that we needed to override, centered around the structure of data being passed to the cookie store to persist and what was being returned when a session was being restored. The key difference is on version 0.7.3, the access_token, etc is stored top-level on the content object property of the session. With 1.0.0. this is nested inside another object inside content with the property name of authenticated. We therefore needed to ensure that everywhere we were making the assumption to set or get the access_token at the top level, we should instead retrieve one level deeper. With that in mind, we came up with these methods being overridden in our custom session object:
// alias access_token to point to new place
access_token: Ember.computed.alias('content.authenticated.access_token'),
// overridden methods to handle v2 cookie structure
restore: function() {
return new Ember.RSVP.Promise((resolve, reject) => {
const restoredContent = this.store.restore();
const authenticator = restoredContent.authenticated.authenticator;
if (!!authenticator) {
delete restoredContent.authenticated.authenticator;
this.container.lookup(authenticator).restore(restoredContent.authenticated).then(function(content) {
this.setup(authenticator, content);
resolve();
}, () => {
this.store.clear();
reject();
});
} else {
this.store.clear();
reject();
}
});
},
updateStore: function() {
let data = this.content;
if (!Ember.isEmpty(this.authenticator)) {
Ember.set(data, 'authenticated', Ember.merge({ authenticator: this.authenticator }, data.authenticated || {}));
}
if (!Ember.isEmpty(data)) {
this.store.persist(data);
}
},
setup(authenticator, authenticatedContent, trigger) {
trigger = !!trigger && !this.get('isAuthenticated');
this.beginPropertyChanges();
this.setProperties({
isAuthenticated: true,
authenticator
});
Ember.set(this, 'content.authenticated', authenticatedContent);
this.bindToAuthenticatorEvents();
this.updateStore();
this.endPropertyChanges();
if (trigger) {
this.trigger('sessionAuthenticationSucceeded');
}
},
clear: function(trigger) {
trigger = !!trigger && this.get('isAuthenticated');
this.beginPropertyChanges();
this.setProperties({
isAuthenticated: false,
authenticator: null
});
Ember.set(this.content, 'authenticated', {});
this.store.clear();
this.endPropertyChanges();
if (trigger) {
this.trigger('sessionInvalidationSucceeded');
}
},
bindToStoreEvents: function() {
this.store.on('sessionDataUpdated', (content) => {
const authenticator = content.authenticated.authenticator;
this.set('content', content);
if (!!authenticator) {
delete content.authenticated.authenticator;
this.container.lookup(authenticator).restore(content.authenticated).then((content) => {
this.setup(authenticator, content, true);
}, () => {
this.clear(true);
});
} else {
this.clear(true);
}
});
}.observes('store'),
This took us most of the way there. We just needed to ensure that the authenticator name that we use matches the name on 1.0.0. Instead of 'simple-auth-authenticator:oauth2-password-grant', we needed to rename our authenticator via an initializer to 'authenticator:oauth2'. This ensures that the apps with the newer version will be able to handle the correct authenticator events when the cookie session data changes. The initializer logic is simple enough:
import OAuth2 from 'simple-auth-oauth2/authenticators/oauth2';
export default {
name: 'oauth2',
before: 'simple-auth',
initialize: function(container) {
container.register('authenticator:oauth2', OAuth2);
}
};
The above satisfies our needs- we can sign in to an app using ember-simple-auth 0.7.3 and have the cookie session stored and formatted properly to be handled by another app on ember-simple-auth 1.0.0.
Ideally, we would just update the Ember and Ember Simple Auth versions of the app though business needs and the fact that we wanted to focus our energies on the v2 versions (which are completely fresh and new code bases) propelled us to go down this path.
I am trying to set up Torii with my own OAuth flow and Ember-Simple-Auth. I can get a successful authentication event, but immediately after I authenticate, the invalidateSession trigger is fired causing my session to end. I can see this by intercepting sessionInvalidated() in /app/routes/application.js (which has the ApplicationRouteMixin).
Have any of you come across this? Is there something peculiar that would cause an immediate session validation? Any advice would be greatly appreciated.
EDIT: I think it has to do with the torii popup code because the first return works, but the second doesn't. Any thoughts?
import OAuth2 from 'torii/providers/oauth2-code';
import {configurable} from 'torii/configuration';
export default OAuth2.extend({
name: 'api',
init() { this.set('clientID', this.get('apiKey')); },
baseUrl: configurable('baseUrl'),
redirectUri: configurable('redirectUri'),
responseParams: ['access_token', 'user_id', 'first_name'],
requiredUrlParams: ['client_id', 'redirect_uri', 'response_type'],
open() {
let name = this.get('name');
let url = this.buildUrl();
let redirectUri = this.get('redirectUri');
let responseParams = this.get('responseParams');
// this return works
return { 'yes' : 'no' }
// this return causes the immediate invalidation
return this.get('popup').open(url, responseParams).then((authData) => {
var missingResponseParams = [];
responseParams.forEach(function(param){
if (authData[param] === undefined) {
missingResponseParams.push(param);
}
});
if (missingResponseParams.length){
throw new Error("The response from the provider is missing " +
"these required response params: " + missingResponseParams.join(', '));
}
return {
access_token: authData.access_token,
first_name: authData.first_name,
user_id: authData.user_id,
provider: name,
redirectUri: redirectUri
};
});
}
});
the real answer is using this fork: https://github.com/simplabs/ember-simple-auth/pull/931 (hopefully it'll be in master soon).
You might have this.get('session').invalidate(); somewhere. Probably in one of your controllers action properties. You would usually put that in your actions for your logout button. Maybe you copy and pasted it by accident. If you post some code I might be able to look at it some more
Alright, this is my first question on SO so I'll try to make it a good one sorry ahead of time.
I've been using ember-cli to work on a product. I am using Firebase simple login for authentication and have created an initializer that adds an auth object to all my controllers and routes.
This works, but... There seems to be a delay for the redirect checking. example I go to /secret and this should redirect me back to the /login route and it does but there is a slight delay where I can see the template
I've tried to create a gist with all the necessary information. Let me know if there is anything else I can provide to help out
https://gist.github.com/mbertino/060e96e532f8ce05d2d0
You could provide a flag on your auth object, which you use in your route to conditionally display the content:
import Ember from 'ember';
var firebase = new window.Firebase( window.MyAppENV.APP.firebaseURL );
export default Ember.Object.extend({
authed: false,
init: function(route) {
var self = this;
self.authClient = new window.FirebaseSimpleLogin( firebase ), function(error, user) {
self.set( 'authed', false );
if (error) {
alert('Authentication failed: ' + error);
} else if (user) {
self.set( 'authed', true );
// if already a user redirect to the secret resource
route.transitionTo('secret')
} else if (route.routeName !== 'login'){
// if the route is anything but the login route and there is not
// a user then redirect to the login resource
route.transitionTo('login')
}
}.bind(this));
You then can use {{#if auth.authed}} in your templates to reveal private data, or display different content.
Instead of setting the flag above, I use a watcher on the authenticated status, this is provided by the API (documented here: https://www.firebase.com/docs/web/guide/user-auth.html#section-monitoring-authentication):
// also in init()
self.authRef = new window.Firebase( firebase + '/.info/authenticated' );
self.authRef.on( 'value', function( snap ) {
var oldAuthed = self.get('authed');
if ( snap.val() === true ) {
self.set( 'authed', true );
if ( ! oldAuthed ) {
// Status switched form unauthenticated to authenticated
// go to your route here
);
}
} else {
self.set( 'authed', false );
if ( oldAuthed ) {
// User was logged in, is now logged out
}
}
});
Keep in mind that it's JavaScript, and nothing you can do will be fool-proof. In other words, you can try to hide anything, but someone who really wants to can always hack into your data structures and reveal a hidden page that way. If you don't want people to see something, make sure they can't read it in the first place (by locking down access through the Firebase privilege system).