I'm trying to link Ember-Data with a JSON-API backed by Express.JS.
I know these ...
models
adapters
serializers
... but how do they work together? And how do they fit into the whole picture of Ember.JS?
How can I set a secure connection between JSON-API and Ember.JS?
This is an extremely broad question but having recently gone through all of this I believe i can provide a detailed response for how I have implemented it. This is key because there is a very large amount of options, and if you look up most of the tutorials, they mostly focus around using rails
as the back end instead of node
or express.js
. I will be answering this question based on you using express.js
.
I'll preface this with remembering that ember-data
is a completely different offshoot of ember that you can bypass and entirely not use if you feel your project is not going to need the features with it and just use AJAX requests instead. ember-data
adds a lot of complexity and overhead to the initial start of the project. Additionally TLS/SSL is the most important security you can have and without it, any amount of attempted security outside of this is invalid without it. Now that that's out of the way, lets get to the gritty part of setting it up.
By default ember-data
uses the JSONAPIAdapter
which is based on the JSON API specification. Your Express.js
API server is going to have to be able to function to this specification if you use the default Adapter
with no Serializer
changes
Breaking the project out into the core components and what they need to do, and the options available is the following (with what I did in bold):
express.js
ember-simple-auth
works wellember-simple-auth-token
gives you a prebuilt authorizer using token based authenticationThe basic flow is as follows:
Here's how i set it up
** Setting Ember.js
up to use Express.js
API Server **
Install the following items for ember-cli:
ember install ember-simple-auth
- For authentication
ember install ember-simple-auth-token
- For token-based authentication
in app/adapters/application.js
:
import DS from 'ember-data';
import DataAdapterMixin from 'ember-simple-auth/mixins/data-adapter-mixin'; // Authenticating data from the API server
import Ember from 'ember';
import ENV from '../config/environment';
export default DS.JSONAPIAdapter.extend(DataAdapterMixin,{
authManager: Ember.inject.service('session'),
host: ENV.apihost, // location of the API server
namespace: ENV.apinamespace, // Namespace of API server ie: 'api/v1'
authorizer: 'authorizer:token', // Authorizer to use for authentication
ajax: function(url, method, hash) {
hash = hash || {}; // hash may be undefined
hash.crossDomain = true; // Needed for CORS
return this._super(url, method, hash);
}
});
In config/environment.js
:
ENV.host = 'http://localhost:4000'; /* this assumes the express.js server
is running on port 4000 locally, in a production environment it would point
to https://domainname.com/ */
ENV['ember-simple-auth'] = {
authorizer: 'authorizer:token', //uses ember-simple-auth-token authorizer
crossOriginWhitelist: ['http://localhost:4000'], // for CORS
baseURL: '/',
authenticationRoute: 'login', // Ember.js route that does authentication
routeAfterAuthentication: 'profile', // Ember.js route to transition to after authentication
routeIfAlreadyAuthenticated: 'profile' // Ember.js route to transition to if already authenticated
};
ENV['ember-simple-auth-token'] = {
serverTokenEndpoint: 'http://localhost:4000/auth/token', // Where to get JWT from
identificationField: 'email', // identification field that is sent to Express.js server
passwordField: 'password', // password field sent to Express.js server
tokenPropertyName: 'token', // expected response key from Express.js server
authorizationPrefix: 'Bearer ', // header value prefix
authorizationHeaderName: 'Authorization', // header key
headers: {},
};
ENV['apihost'] = "http://localhost:4000" // Host of the API server passed to `app/adapters/application.js`
ENV['apinamespace'] = ""; // Namespace of API server passed to `app/adapters/application.js`
** Setting up Express.js
Server **
Required packages:
express
: Self explanatory
body-parser
: for parsing JSON from ember.js
site
cors
: for CORS support
ejwt
: for requiring JWT on most routes to your API server
passport
: for authenticating users
passport-json
: for authenticating users
bcrypt
: for hashing/salting user passwords
sequelize
: for data modeling
** Setting up server.js
**
var express = require('express'); // App is built on express framework
var bodyParser = require('body-parser'); // For parsing JSON passed to use through the front end app
var cors = require('cors'); // For CORS support
var ejwt = require('express-jwt');
var passport = require('passport');
// Load Configuration files
var Config = require('./config/environment'),
config = new Config // Load our Environment configuration based on NODE_ENV environmental variable. Default is test.
var corsOptions = {
origin: config.cors
};
var app = express(); // Define our app object using express
app.use(bodyParser.urlencoded({extended: true})); // use x-www-form-urlencoded used for processing submitted forms from the front end app
app.use(bodyParser.json()); // parse json bodies that come in from the front end app
app.use(bodyParser.json({ type: 'application/vnd.api+json' })); // THIS ALLOWS ACCEPTING EMBER DATA BECAUSE JSON API FORMAT
app.use(cors(corsOptions)); // Cross-Origin Resource Sharing support
app.use(passport.initialize()); // initialize passport
app.use(ejwt({ secret: config.secret}).unless({path: ['/auth/token', { url : '/users', methods: ['POST']}]}));
require('./app/routes')(app); // Load our routes file that handles all the API call routing
app.listen(config.port); // Start our server on the configured port. Default is 4000
console.log('listening on port : ' + config.port);
in config/passport.js
// config/passport.js
// Configure Passport for local logins
// Required Modules
var JsonStrategy = require('passport-json').Strategy;
//
var User = require('../app/models/users'); // load user model
// Function
module.exports = function (passport) {
// serialize the user for the session
passport.serializeUser(function (user, done) {
done(null, user.id);
});
// deserialize the user
passport.deserializeUser(function (id, done) {
User.findById(id).then(function (user) {
done(null, user);
});
});
// LOCAL LOGIN ==========================================================
passport.use('json', new JsonStrategy({
usernameProp : 'email',
passwordProp : 'password',
passReqToCallback : true
},
function (req, email, password, done) {
User.findOne({where : {'email' : email }}).then(function (user) { // check against email
if (!user) {
User.findOne({where : {'displayName' : email}}).then(function(user){ //check against displayName
if (!user) return done(null, false);
else if (User.validatePassword(password,user.password)) return done(null, user);
else return done(null, false);
});
}
else if (User.validatePassword(password,user.password)) return done(null, user);
else return done(null, false);
});
}));
};
Example app/models/users.js
user sequelize model
// Load required Packages
var Sequelize = require('sequelize');
var bcrypt = require('bcrypt-node')
// Load required helpers
var sequelize = require('../helpers/sequelizeconnect');
var config = new require('../../config/environment'); // Load our Environment configuration based on NODE_ENV environmental variable. Default is test.
// Load other models
// Define model
var Users = sequelize.define('users', {
"email": { type: Sequelize.STRING}, // user email
"password": { type: Sequelize.STRING} // user password
});
// Methods =======================================================
// Hash a password before storing
Users.generateHash = function(password) {
return bcrypt.hashSync(password, bcrypt.genSaltSync(8), null);
};
// Compare a password from the DB
Users.validatePassword = function(password, dbpassword) {
return bcrypt.compareSync(password, dbpassword);
}
module.exports = Users
At this point your express.js
server will just need your routes.js
set up with routes for what your API server needs, at a minimum of /auth/token
in order to perform the authentication. An example of a successful response the Ember.js
JSON API adapter expects is:
var jsonObject = { // create json response object
"data": {
"type": "users", // ember.js model
"id": 1, // id of the model
"attributes": {
"email" : "example@example.com",
}
}
}
res.status(201).json(jsonObject); // send new data object with 201/OK as a response
There is a lot more complexities to setting up the JSON API server to respond to Delete requests, Validation errors, etc.