client-side auth with ember.js
DESCRIPTION
There are several platforms you can authenticate users against without using a server, among them Facebook (who provides a JavaScript SDK) and Windows Live (who provides Oauth2 and bearer tokens). With these services, we can implement authentication flows nearly entirely in Ember. With the example of a real project (http://herehere.co), let’s see how to do this using dependency injection, dependency lookup, promises, and routing hooks.TRANSCRIPT
![Page 1: Client-side Auth with Ember.js](https://reader034.vdocuments.mx/reader034/viewer/2022052618/554a27e7b4c9051b578b4ac2/html5/thumbnails/1.jpg)
Hi. I’m Matthew.
![Page 3: Client-side Auth with Ember.js](https://reader034.vdocuments.mx/reader034/viewer/2022052618/554a27e7b4c9051b578b4ac2/html5/thumbnails/3.jpg)
201 Created
We build õ-age apps with Ember.js. We take teams from £ to • in no time flat.
![Page 5: Client-side Auth with Ember.js](https://reader034.vdocuments.mx/reader034/viewer/2022052618/554a27e7b4c9051b578b4ac2/html5/thumbnails/5.jpg)
authentication
![Page 6: Client-side Auth with Ember.js](https://reader034.vdocuments.mx/reader034/viewer/2022052618/554a27e7b4c9051b578b4ac2/html5/thumbnails/6.jpg)
The Goal
• Auth against multiple 3rd party services • Don’t be reloading the page • keep the complexity off the server
![Page 7: Client-side Auth with Ember.js](https://reader034.vdocuments.mx/reader034/viewer/2022052618/554a27e7b4c9051b578b4ac2/html5/thumbnails/7.jpg)
One page, no reload
Sign In
Auth at Windows Live
Signed In!
![Page 8: Client-side Auth with Ember.js](https://reader034.vdocuments.mx/reader034/viewer/2022052618/554a27e7b4c9051b578b4ac2/html5/thumbnails/8.jpg)
OAuth2
• access to resources via tokens • Several token types • a different flow for each type
![Page 9: Client-side Auth with Ember.js](https://reader034.vdocuments.mx/reader034/viewer/2022052618/554a27e7b4c9051b578b4ac2/html5/thumbnails/9.jpg)
+----------+ | Resource | | Owner | | | +----------+ v | Resource Owner (A) Password Credentials | v +---------+ +---------------+ | |>--(B)---- Resource Owner ------->| | | | Password Credentials | Authorization | | Client | | Server | | |<--(C)---- Access Token ---------<| | | | (w/ Optional Refresh Token) | | +---------+ +---------------+
resource owner password credentials grant
Do not use this
![Page 10: Client-side Auth with Ember.js](https://reader034.vdocuments.mx/reader034/viewer/2022052618/554a27e7b4c9051b578b4ac2/html5/thumbnails/10.jpg)
+----------+ | Resource | | Owner | | | +----------+ ^ | (B) +----|-----+ Client Identifier +---------------+ | -+----(A)-- & Redirection URI ---->| | | User- | | Authorization | | Agent -+----(B)-- User authenticates --->| Server | | | | | | -+----(C)-- Authorization Code ---<| | +-|----|---+ +---------------+ | | ^ v (A) (C) | | | | | | ^ v | | +---------+ | | | |>---(D)-- Authorization Code ---------' | | Client | & Redirection URI | | | | | |<---(E)----- Access Token -------------------' +---------+ (w/ Optional Refresh Token)
Authorization code grant
![Page 11: Client-side Auth with Ember.js](https://reader034.vdocuments.mx/reader034/viewer/2022052618/554a27e7b4c9051b578b4ac2/html5/thumbnails/11.jpg)
+----------+ | Resource | | Owner | | | +----------+ ^ | (B) +----|-----+ Client Identifier +---------------+ | -+----(A)-- & Redirection URI --->| | | User- | | Authorization | | Agent -|----(B)-- User authenticates -->| Server | | | | | | |<---(C)--- Redirection URI ----<| | | | with Access Token +---------------+ | | in Fragment | | +---------------+ | |----(D)--- Redirection URI ---->| Web-Hosted | | | without Fragment | Client | | | | Resource | | (F) |<---(E)------- Script ---------<| | | | +---------------+ +-|--------+ | | (A) (G) Access Token | | ^ v +---------+ | | | Client | | | +---------+
implicit grant
![Page 12: Client-side Auth with Ember.js](https://reader034.vdocuments.mx/reader034/viewer/2022052618/554a27e7b4c9051b578b4ac2/html5/thumbnails/12.jpg)
+----------+ | Resource | | Owner | | | +----------+ ^ | (B) +----|-----+ Client Identifier +---------------+ | -+----(A)-- & Redirection URI --->| | | User- | | Authorization | | Agent -|----(B)-- User authenticates -->| Server | | | | | | |<---(C)--- Redirection URI ----<| | | | with Access Token +---------------+ | | in Fragment | | +---------------+ | |----(D)--- Redirection URI ---->| Web-Hosted | | | without Fragment | Client | | | | Resource | | (F) |<---(E)------- Script ---------<| | | | +---------------+ +-|--------+ | | (A) (G) Access Token | | ^ v +---------+ | | | Client | | | +---------+
implicit grant
browser
Facebook Auth
website server
website
User
Facebook connect
![Page 13: Client-side Auth with Ember.js](https://reader034.vdocuments.mx/reader034/viewer/2022052618/554a27e7b4c9051b578b4ac2/html5/thumbnails/13.jpg)
Implicit Grant supported by
• facebook (behind sdk) • google • soundcloud • box.net • windows live !
• & more
![Page 14: Client-side Auth with Ember.js](https://reader034.vdocuments.mx/reader034/viewer/2022052618/554a27e7b4c9051b578b4ac2/html5/thumbnails/14.jpg)
Building it!
![Page 15: Client-side Auth with Ember.js](https://reader034.vdocuments.mx/reader034/viewer/2022052618/554a27e7b4c9051b578b4ac2/html5/thumbnails/15.jpg)
Abstractions
• session manager (controller) • Oauth adapter • live, FB specific adapters • popup manager
![Page 16: Client-side Auth with Ember.js](https://reader034.vdocuments.mx/reader034/viewer/2022052618/554a27e7b4c9051b578b4ac2/html5/thumbnails/16.jpg)
6 <div {{bind-attr class=":sign-in-menu isOpen:open:closed"}}> 7 <a class="sign-in-button facebook" href="#" {{action "selectService" "facebook"}}> 8 Sign In with Facebook 9 </a> 10 <a class="sign-in-button windows" href="#" {{action "selectService" "live"}}> 11 Sign In with Windows Live 12 </a> 13 <div class="mobile-show menu-footer subheader"> 14 {{link-to 'Leaderboard' 'leaderboard'}} 15 {{link-to 'About' 'map.about'}} 16 </div> 17 </div>
![Page 17: Client-side Auth with Ember.js](https://reader034.vdocuments.mx/reader034/viewer/2022052618/554a27e7b4c9051b578b4ac2/html5/thumbnails/17.jpg)
36 signIn: function(service){ 37 var session = this.get('session'), 38 route = this; 39 session.open(service) 40 .then(function(){ 41 if (route.router.isActive('map')) { 42 route.disconnectOutlet({ 43 outlet: 'prompt', 44 parentView: 'map/neighborhood' 45 }); 46 } 47 var lastTransition = session.get('afterRedirect'); 48 if (lastTransition) { 49 lastTransition.retry(); 50 } else if (route.router.isActive('sign_in')) { 51 route.transitionTo(''); 52 } 53 }).catch(Ember.Logger.error); 54 },
![Page 18: Client-side Auth with Ember.js](https://reader034.vdocuments.mx/reader034/viewer/2022052618/554a27e7b4c9051b578b4ac2/html5/thumbnails/18.jpg)
61 Ember.Application.initializer({ 62 name: 'authentication', 63 initialize: function(container, app){ . . . 65 import Session from 'appkit/controllers/session'; 66 container.register('session:main', Session); 67 app.inject('controller', 'session', 'session:main'); 68 app.inject('route', 'session', 'session:main'); . . . 95 });
inject session
![Page 19: Client-side Auth with Ember.js](https://reader034.vdocuments.mx/reader034/viewer/2022052618/554a27e7b4c9051b578b4ac2/html5/thumbnails/19.jpg)
9 open: function(credentials){ 10 var session = this; 11 session.set('isAuthenticating', true); 12 13 return this.get('adapter').open(credentials, this) 14 .then(function(user){ 15 session.setProperties({ 16 isAuthenticated: true, 17 currentUser: user 18 }); 19 return user; 20 }).catch(function(err){ 21 if (err === 'canceled') { 22 return; // no-op 23 } else { 24 return Ember.RSVP.reject(err); 25 } 26 }).finally(function(){ 27 session.set('isAuthenticating', false); 28 }); 29 },
opening a session
![Page 20: Client-side Auth with Ember.js](https://reader034.vdocuments.mx/reader034/viewer/2022052618/554a27e7b4c9051b578b4ac2/html5/thumbnails/20.jpg)
61 Ember.Application.initializer({ 62 name: 'authentication', 63 initialize: function(container, app){ . . . 70 import Auth from 'appkit/models/auth'; 71 container.register('session:adapter', Auth); 72 app.inject('session:adapter', 'store', 'store:main'); 73 app.inject('session:main', 'adapter', 'session:adapter'); . . . 95 });
Inject Auth adapter
![Page 21: Client-side Auth with Ember.js](https://reader034.vdocuments.mx/reader034/viewer/2022052618/554a27e7b4c9051b578b4ac2/html5/thumbnails/21.jpg)
50 // returns a promise that resolves to a user 51 open: function(serviceName){ 52 var auth = this; 53 var authService = this.container.lookup('auth:' + serviceName); 54 55 if (!authService) { 56 return Ember.RSVP.reject('unrecognized service auth:' + 57 serviceName); 58 } else { 59 return authService.open() 60 .then( function(serviceData) { 61 // create a session 62 var sessionData = { 63 authData: { 64 name: serviceData.name, 65 id: serviceData.id, 66 accessToken: serviceData.accessToken 67 } 68 }; 69 var session = auth.get('store').createRecord('session', 70 sessionData); 71 return session.save(); 72 }).then( function(session){ 73 auth.set('authToken', session.get('id')); 74 75 return session.get('user'); 76 }); 77 } 78 },
open an auth attempt
![Page 22: Client-side Auth with Ember.js](https://reader034.vdocuments.mx/reader034/viewer/2022052618/554a27e7b4c9051b578b4ac2/html5/thumbnails/22.jpg)
25 var LiveAuthService = Ember.Object.extend({ 26 open: function(){ 27 return this.signIn() 28 .then( this.normalizeServiceData ); 29 }, 30 31 signIn: function(){ 32 var url = createAuthUrl(); 33 return this.get('popup'). 34 open(url, {width: 500, height: 510 }); 35 }, 36 37 normalizeServiceData: function(accessToken){ 38 return { 39 name: 'live', 40 accessToken: accessToken 41 }; 42 } 43 });
open windows live auth attempt
![Page 23: Client-side Auth with Ember.js](https://reader034.vdocuments.mx/reader034/viewer/2022052618/554a27e7b4c9051b578b4ac2/html5/thumbnails/23.jpg)
38 App.initializer({ 39 name: 'Register Services', 40 initialize: function(container, app) { 41 registerServices(container); 42 43 // force creation of FacebookAuthService (to load the FB global) 44 container.lookup('auth:facebook'); 45 46 app.inject('auth', 'popup', 'popups:authenticate'); 47 } 48 });
Inject popup service
![Page 24: Client-side Auth with Ember.js](https://reader034.vdocuments.mx/reader034/viewer/2022052618/554a27e7b4c9051b578b4ac2/html5/thumbnails/24.jpg)
65 open: function(url, options) { 66 this.closeExistingWindow(); 67 this.rejectExistingDeferred('canceled'); 68 var deferred = this.generateNewDeferred(); 69 var defaultedOptions = this.applyDefaultOptions(options); 70 this.popup = window.open( 71 url, 'authentication', parameterizeOptions(defaultedOptions) 72 ); 73 74 if (this.popup) { 75 this.popup.focus(); 76 $(window).on('message', this.boundMessageHandler); 77 } else { 78 this.rejectExistingDeferred('failed to open popup'); 79 } 80 81 return deferred.promise; 82 },
![Page 25: Client-side Auth with Ember.js](https://reader034.vdocuments.mx/reader034/viewer/2022052618/554a27e7b4c9051b578b4ac2/html5/thumbnails/25.jpg)
30 createBoundMessageHandler: function(){ 31 this.boundMessageHandler = function(event){ 32 var matches, message = event.originalEvent.data; 33 if (!message || !(matches = message.match(/^setAccessToken:(.*)/))){ 34 return; 35 } 36 37 if (matches[1]){ 38 Ember.run(this, function(){ 39 this.closeExistingWindow(); 40 this.resolveExistingDeferred(matches[1]); 41 }); 42 } 43 }.bind(this); 44 }.on('init'),
Listen for messages from the popup
![Page 26: Client-side Auth with Ember.js](https://reader034.vdocuments.mx/reader034/viewer/2022052618/554a27e7b4c9051b578b4ac2/html5/thumbnails/26.jpg)
Upon load, look for tokens
61 Ember.Application.initializer({ 62 name: 'authentication', 63 initialize: function(container, app){ . . . 75 // Kind of feels like the in-popup logic should be elsewhere 76 var auth = container.lookup('session:adapter'); 77 var token = auth.readAccessToken(); 78 if (token && window.opener) { 79 // Don't go forward, we are just a popup with an accessToken 80 app.deferReadiness(); 81 window.opener.postMessage( 82 'setAccessToken:'+token, 83 Overherd.settings.origin 84 ); 85 } . . . 94 } 95 });
![Page 27: Client-side Auth with Ember.js](https://reader034.vdocuments.mx/reader034/viewer/2022052618/554a27e7b4c9051b578b4ac2/html5/thumbnails/27.jpg)
Upon load, look for tokens
99 readAccessToken: function(){ 100 var accessToken, match, 101 regex = /access_token=([^&]*)/, 102 hash = window.location.hash; 103 104 if (window.location.hash){ 105 hash = window.location.hash; 106 if (match = hash.match(regex)) { 107 return match[1]; 108 } 109 } 110 }
![Page 28: Client-side Auth with Ember.js](https://reader034.vdocuments.mx/reader034/viewer/2022052618/554a27e7b4c9051b578b4ac2/html5/thumbnails/28.jpg)
promises complete!
![Page 29: Client-side Auth with Ember.js](https://reader034.vdocuments.mx/reader034/viewer/2022052618/554a27e7b4c9051b578b4ac2/html5/thumbnails/29.jpg)
Other ideas
• localstorage for auth tokens • how to recognize a cancelled sign in? • we still check the token is valid server-side
![Page 31: Client-side Auth with Ember.js](https://reader034.vdocuments.mx/reader034/viewer/2022052618/554a27e7b4c9051b578b4ac2/html5/thumbnails/31.jpg)
Thanks!
@mixonic
httP://madhatted.com