strongloop node.js api security & customization
TRANSCRIPT
API SECURITY, CUSTOMIZATION,AND MOBILE BACKENDS
Jordan Kasper | Developer Evangelist
Open source REST API framework based on Express.js
LOOPBACK FEATURESModel-driven API developmentDynamic REST API endpoint generationConnect to any datasource (SQL, NoSQL, REST, SOAP)Rich model relationsAccess controls (built in token authentication)Geolocation, push notifications, offline syncAngular, Android, and iOS SDKs
STEP 1: INSTALL STRONGLOOP TOOLS~$ npm install g strongloop
And scaffold your application:~$ slc loopback
WORKING WITH DATA MODELS
CREATING A MODEL VIA CLI~/myapp$ slc loopback:model[?] Enter the model name: CoffeeShop[?] Select the datasource to attach CoffeeShop to: (Use arrow keys)❯ mdb (mongodb)[?] Select model's base class: (Use arrow keys) Model❯ PersistedModel ACL[?] Expose CoffeeShop via the REST API? (Y/n) Y[?] Custom plural form (used to build REST URL):
CREATING A MODEL VIA CLI[?] Property name: name invoke loopback:property[?] Property type: (Use arrow keys)❯ string number boolean object array date ...[?] Required? (y/N)
When complete, simply hit "enter" with a blank property
MODEL CONFIG "name": "CoffeeShop", "base": "PersistedModel", "idInjection": true, "properties": "name": "type": "string", "required": true , "validations": [], "relations": , "acls": [], "methods": []
RUN THE APPLICATION~/myapp$ node server/server.js
Web server's listening at http://localhost:3000Browse your REST API at http://localhost:3000/explorer
COFFEESHOP ROUTEShttp://localhost:3000/api/CoffeeShops
[]
We don't have any coffee shops yet,so we get an empty array!
http://localhost:3000/explorer
RELATIONSHIP MODELING
VARIOUS RELATIONSHIP TYPESHasManyHasManyThroughHasAndBelongsToManyPolymorphicEmbedded (embedsOne and embedsMany)
We also have a special "ownership" relation:"BelongsTo"
RELATIONSHIP DEFINITION// common/models/coffeeshop.json "name": "CoffeeShop", // ..., "relations": "reviews": "type": "hasMany", "model": "Review"
BELONGSTO RELATIONSHIPS (OWNERSHIP)// common/models/review.json "name": "Review", // ..., "relations": "reviewer": "type": "belongsTo", "model": "User"
AUTHENTICATION ANDAUTHORIZATION
LOOPBACK AUTH MODELSUserPrincipalRoleRoleMappingACL
PRINCIPALSAn entity that can be identified or authenticated
A userA roleAn application
ROLES AND MAPPINGSA Role is a group of principals with the same permissions.
RoleMappings are used to map principals onto Roles.
DYNAMIC ROLESSome dynamic roles already exist for you:
$everyone (regardless of authorization status)$unauthenticated$authenticated$owner (using a belongsTo relation)
CREATING NEW ROLES// server/boot/roles.jsmodule.exports = function(app)
app.models.Role.create(
name: 'admin'
, function(err, theAdminRole) if (err) cb(err);
// Maybe add some users to it? );
;
ADDING USERS TO A CUSTOM ROLEtheAdminRole.principals.create(
principalType: app.models.RoleMapping.USER, principalId: someUser.id
, function(err, principal) // handle the error! cb(err););
CUSTOM DYNAMIC ROLESUse Role.registerResolver() to set up a custom role handler:
// server/boot/roles.jsRole.registerResolver('admin', function(role, context, cb)
// custom method you create to determine this... determineAdminStatus(function(err, isAuthorized)
if (err) // maybe handle the error here? return cb(err);
// execute callback with a boolean cb(null, isAuthorized);
););
ACCESS CONTROL LAYERSA layer defines what access a principal has for a certain
operation against a specific model.
WHITELISTINGFor example, we might create these layers:
Deny everyone to access the modelAllow '$everyone' role to read instancesAllow '$authenticated' role to create instancesAllow '$owner' to update an existing instance
This is an example of "whitelisting", and is safer thandenying specific operations.
DEFINING AN ACLWe use the slc loopback:acl subcommand:
~/myapp$ slc loopback:acl
DEFINING AN ACLThe CLI will ask you some questions...
~/myapp$ slc loopback:acl? Select the model to apply the ACL entry to: CoffeeShop? Select the ACL scope: All methods and properties? Select the access type: All (match all types)? Select the role: All users? Select the permission to apply: Explicitly deny access
DEFINING AN ACLNow allow everyone to read CoffeeShops:
~/myapp$ slc loopback:acl? Select the model to apply the ACL entry to: CoffeeShop? Select the ACL scope: All methods and properties? Select the access type: Read? Select the role: All users? Select the permission to apply: Explicitly grant access
DEFINING AN ACLHere's what this looks like in the config file:
// in common/models/coffeeshop.json"acls": [ "accessType": "*", "principalType": "ROLE", "principalId": "$everyone", "permission": "DENY" , "accessType": "READ", "principalType": "ROLE", "principalId": "$everyone", "permission": "ALLOW" ]
DEFINING ACLSAccess Control Layers execute in order, so be sure to DENY
first, then add all of your "whitelisting".
USING OWNERSBy creating a belongsTo relation, we can use $owner :
"acls": [ // ..., "accessType": "WRITE", "principalType": "ROLE", "principalId": "$owner", "permission": "ALLOW" ]
RESTRICTING SPECIFIC METHODSWe can ALLOW or DENY access to specific remote methods:"acls": [ // ..., "accessType": "EXECUTE", "principalType": "ROLE", "principalId": "admin", "permission": "ALLOW", "property": "create" ]
ADVANCING YOUR API
REMOTE METHODSA way to create new, non-CRUD methods on your model.
REMOTE METHODSFirst, define the handler function...
// common/models/coffeeshop.jsmodule.exports = function(CoffeeShop)
CoffeeShop.status = function(id, cb) CoffeeShop.findById(id, function(err, shop) if (err) return cb(err);
cb( null, determineStatus() ); ); ;
// ...;
REMOTE METHODSThen define the API spec...
// common/models/coffeeshop.jsmodule.exports = function(CoffeeShop)
CoffeeShop.status = function(id, cb) /* ... */
CoffeeShop.remoteMethod( 'status', accepts: [ arg: 'id', type: 'number', required: true, http: source: 'path' ], returns: arg: 'isOpen', type: 'boolean' , http: [ verb: 'get', path: '/:id/status' ] );;
ACCESSING REMOTE METHODSA GET request to /api/CoffeeShops/1/status might return:
isOpen: true
RESTRICTING REMOTE METHODSRemember, we can ALLOW or DENY access to specificremote methods on a model, including custom ones!
"acls": [ // ..., "accessType": "EXECUTE", "principalType": "ROLE", "principalId": "$unauthenticated", "permission": "DENY", "property": "status" ]
GETTING THE CURRENT USERAt any point in your application you can get the current
user access token, and the user ID:// common/models/coffeeshop.js
var loopback = require('loopback');
module.exports = function(CoffeeShop)
CoffeeShop.status = function(id, cb) var context = loopback.getCurrentContext(); var token = context.get('accessToken');
console.log( 'Access Token ID', token.id ); console.log( 'Current User ID', token.userId ); ;
;
CONNECTING TO THE FRONT ENDLoopBack has client SDKs for Angular, iOS, Android, and
Xamarin!
STEP ONEThe first step is to generate the client code.
Here we use the Angular generator:~/myapp$ mkdir client/js/services
~/myapp$ lbng server/server.js client/js/services/lbservices.js
ANGULAR MODELSLoopBack provides models for you to use in Angular which
have all of the remote methods ( create , find , destroy , etc).For User objects this includes the login() method!
ANGULAR MODELSWhen you create your application modules, just include the
LoopBack Services we created from the CLI:angular.module('myapp', ['ui.router', 'lbServices'])
.config( ... )
.run( ... );
CREATING AN AUTH SERVICENow we can create an Angular service for authentication:angular.module('myapp').factory( 'AuthService', ['User', '$q', '$rootScope', function(User, $q, $rootScope)
function login(email, password) return User .login( email: email, password: password ) .$promise .then(function(response) $rootScope.currentUser = id: response.user.id, tokenId: response.id ; );
return login: login ; ]);
CREATING AN AUTH SERVICEThe logout() method is easy!
function logout() return User .logout() .$promise .then(function() $rootScope.currentUser = null; ); ;
CREATING AN AUTH SERVICEWhen you use the User.login() method (and it is successful),
LoopBack will send an Authorization header with eachrequest containing the accessToken !
USING 3RD PARTY AUTHIntegrating with 3rd party authentication services is easy!
Just use the .passport LoopBack component
3RD PARTY AUTHThe basic workflow is:
1. Visitor clicks "login with X" button (i.e. Facebook (FB))2. LoopBack (LB) redirects to FB login page3. FB redirects to your LB server4. LB requests access token from FB5. LB uses token to get user info6. LB matches info to internal User model
(LB creates the user locally if it doesn't exist)7. User info is added to the LB context
3RD PARTY AUTHFirst, install the passport component:
~/myapp$ npm install save loopbackcomponentpassport
Then set up a PassportConfigurator ...(This is the piece that connects LoopBack to Passport.)
CONFIGURING PASSPORTWe need to configure Passport, a boot script works:
// server/boot/setupauth.jsvar loopback = require('loopback'), PPConfigurator = require('loopbackcomponentpassport').PassportConfigurator;
module.exports = function(app) // Enable http sessions app.use(loopback.session( secret: 'supersecretstring' ));
var configurator = new PPConfigurator(app); configurator.init(); configurator.setupModels( userModel: app.models.user, userIdentityModel: app.models.userIdentity, userCredentialModel: app.models.userCredential );
configurator.configureProvider('facebooklogin', // ... );;
CONFIGURING PASSPORTAnd here is our Facebook-specific config...
// server/boot/setupauth.js
...module.exports = function(app) // ...
configurator.configureProvider('facebooklogin', provider: 'facebook', module: 'passportfacebook', clientID: 'yourFBclientid', clientSecret: 'yourFBclientsecret', callbackURL: 'http://localhost:3000/auth/facebook/callback', authPath: '/auth/facebook', callbackPath: '/auth/facebook/callback', successRedirect: '/auth/account', scope: ['email'] );;
ADDITIONAL PASSPORT STEPSFill in your FB/Google/etc app detailsCreate a UI button linking to /auth/facebookSet up a page at /auth/account (or wherever successRedirectpoints)Add other auth providers!
See an example app here:https://github.com/strongloop/loopback-example-passport
BEING AN OAUTH PROVIDERWant to be your own OAuth provider?
(instead of using Facebook, Google, etc)
BEING AN OAUTH PROVIDERYou can use StrongLoop's !oauth2 LoopBack component~/myapp$ npm install save loopbackcoponentoath2
* Note that this component requires a license.
CONFIGURE OAUTH// server/boot/oauth.js
var oauth2 = require('loopbackcomponentoauth2');
module.exports = function(app)
oauth2.oAuth2Provider( app, dataSource': app.dataSources.mydb, authorizePath': '/oauth/authorize', loginPage': '/login', loginPath': '/login' );
;
Full options can be found on docs.strongloop.com
OAUTH LOGINDon't forget that you still need to code the login page!
And you will also need a callback URL for the authorizationaction.
OAUTH LOCK DOWNNow we configure the resource endpoints we're locking
down:// server/middleware.json
"auth": "loopbackcomponentoauth2#authenticate": "paths": [ "/api" ], "params": "session": false, "scopes": "reviews": [ "/api/Reviews" ], "user": [ "methods": ["find", "findById", "destroy", "save"], "path": "/api/Users" ]
WHAT ABOUT RATE LIMITING AND PROXY?Create more middleware at specific phases.
Capture the request and evaluate before passing it on.Centralized in front of your resource server!
DEMO PROJECThttps://github.com/jakerella/lb-central
You can use this demo project as a starting point:~$ git clone https://github.com/jakerella/lbcentral.git
~$ cd lbcentral && npm install
NOTE: This code is licensed by StrongLoop, you will need a license to use it!
RATE LIMITING YOUR APIControl how many requests a client can
make within a time period.Simply add the middleware config!
CONFIGURE RATE LIMITING"routes:after": "./middleware/ratelimiting": "params": "interval": 60000, "keys": "ip": 100, "url": "template": "url$urlPaths[0]/$urlPaths[1]", "limit": 500 , "user": "template": "user$user.id", "limit": 1500 , "app,user": "template": "app$app.iduser$user.id", "limit": 2500
REQUEST PROXIESOnce the the user is authenticated, rate limiting is
compelte, and any other centralized code, we can proxyrequests to their final destination.
PROXYING REQUESTSSend requests from this server to the resource server(s):// in middleware.json
"routes:after": "./middleware/proxy": "params": "rules": [ "/api/foo/(.*)$ https://serviceone.com:3007/api/$1 [P]", "/api/bar/(.*)$ https://servicetwo.com:3001/api/v2/$1 [P]" ]
THANK YOU!QUESTIONS?
Jordan Kasper | Developer EvangelistJoin us for more events!
strongloop.com/developers/events