Hi. I’m Matthew.
@mixonic
httP://madhatted.com
matt.beale@madhatted.com
201 Created
We build õ-age apps with Ember.js. We take
teams from £ to • in no time flat.
http://bit.ly/ember-toronto-edge
authentication
The Goal
•Auth against multiple 3rd party services
•Don’t be reloading the page
•keep the complexity off the server
One page, no reload
Sign In
Auth at Windows Live
Signed In!
OAuth2
•access to resources via tokens
•Several token types
•a different flow for each type
+----------+
| 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
+----------+
| 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
+----------+
| 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
+----------+
| 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
Implicit Grant supported by
•facebook (behind sdk)
•google
•soundcloud
•box.net
•windows live
!
•& more
Building it!
Abstractions
•session manager (controller)
•Oauth adapter
•live, FB specific adapters
•popup manager
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>
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 },
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
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
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
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
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
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
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 },
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
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 });
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 }
promises complete!
Other ideas
•localstorage for auth tokens
•how to recognize a cancelled sign in?
•we still check the token is valid
server-side
Try it
HereHere.co
Thanks!
@mixonic
httP://madhatted.com
matt.beale@madhatted.com

Client-side Auth with Ember.js

  • 1.
  • 2.
  • 3.
    201 Created We buildõ-age apps with Ember.js. We take teams from £ to • in no time flat.
  • 4.
  • 5.
  • 6.
    The Goal •Auth againstmultiple 3rd party services •Don’t be reloading the page •keep the complexity off the server
  • 7.
    One page, noreload Sign In Auth at Windows Live Signed In!
  • 8.
    OAuth2 •access to resourcesvia tokens •Several token types •a different flow for each type
  • 9.
    +----------+ | 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
  • 10.
    +----------+ | 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
  • 11.
    +----------+ | 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
  • 12.
    +----------+ | 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
  • 13.
    Implicit Grant supportedby •facebook (behind sdk) •google •soundcloud •box.net •windows live ! •& more
  • 14.
  • 15.
    Abstractions •session manager (controller) •Oauthadapter •live, FB specific adapters •popup manager
  • 16.
    6 <div {{bind-attrclass=":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>
  • 17.
    36 signIn: function(service){ 37var 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 },
  • 18.
    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
  • 19.
    9 open: function(credentials){ 10var 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
  • 20.
    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
  • 21.
    50 // returnsa 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
  • 22.
    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
  • 23.
    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
  • 24.
    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 },
  • 25.
    30 createBoundMessageHandler: function(){ 31this.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
  • 26.
    Upon load, lookfor 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 });
  • 27.
    Upon load, lookfor 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 }
  • 28.
  • 29.
    Other ideas •localstorage forauth tokens •how to recognize a cancelled sign in? •we still check the token is valid server-side
  • 30.
  • 31.