📜 ⬆️ ⬇️

Authorization using Facebook and Vkontakte in a one-page application on Backbonejs + Express

Hi Habr! In this article I want to talk about how to implement authorization using social networks in a one-page application using the example of Backbonejs + Express.

Backbone.js


If you haven’t installed Node.js, you can download it from off.site . To install Express, use the Express application generator.
')
npm install express-generator -g express habr cd habr && npm install 

We created a new Express application called habr. Remove the views directory, since we don’t need it, rename images to img, javascripts to js, ​​stylesheets to style and add the public / tpl folder in which the templates will lie. Now the structure of our project looks like this:

 . ├── app.js ├── bin │ └── www ├── package.json ├── public │ ├── img │ ├── js │ ├── tpl │ └── style │ └── style.css ├── routes │ ├── index.js │ └── users.js 

To download components, we will use RequireJS and RequireJS / textjs to load templates. The application will be initialized in the init.js file.

Add the RequireJs configuration.

public / js / init.js:

 requirejs.config({ baseUrl: "js/", paths: { jquery: 'lib/jquery.min', backbone: 'lib/backbone.min', underscore: 'lib/underscore.min', fb: 'https://connect.facebook.net/ru_RU/all', //Facebook api vk: 'https://vk.com/js/api/openapi', //Vk API text: 'lib/text', tpl: '../tpl' }, shim: { 'underscore': { exports: '_' }, 'vk': { exports: 'VK' }, 'fb': { exports: 'FB' }, 'backbone': { deps: ['underscore', 'jquery'], exports: 'Backbone' } } }); 

I immediately added a library to work with Vk and Facebook API.

Backbonejs does not have the functionality to call Middleware before the route, therefore, using the example , I added 2 methods: before and after, which will be called before and after each route. We need this to verify authorization before calling routes to which an unauthorized user should not access.

public / js / baseRouter.js:

baseRouter.js
 define([ 'underscore', 'backbone' ], function(_, Backbone){ var BaseRouter = Backbone.Router.extend({ before: function(){}, after: function(){}, route : function(route, name, callback){ if (!_.isRegExp(route)) route = this._routeToRegExp(route); if (_.isFunction(name)) { callback = name; name = ''; } if (!callback) callback = this[name]; var router = this; Backbone.history.route(route, function(fragment) { var args = router._extractParameters(route, fragment); var next = function(){ callback && callback.apply(router, args); router.trigger.apply(router, ['route:' + name].concat(args)); router.trigger('route', name, args); Backbone.history.trigger('route', router, name, args); router.after.apply(router, args); } router.before.apply(router, [args, next]); }); return this; } }); return BaseRouter; }); 

Now we define our routes:

public / js / router.js:

 define([ 'baseRouter', ], function(BaseRouter){ return BaseRouter.extend({ routes: { "secure": "secure", "login" : "login" }, //        secure_pages: [ '#secure' ], before : function(params, next){ next(); }, secure: function(){ console.log('This is secure page'); }, login: function(){ console.log('This is login page'); } }); }); 

Create a file public / tpl / index.html, connect bootstrap.css so that it would have an acceptable form:

 <!DOCTYPE html> <html> <head> <title></title> <script data-main="/js/init" src="js/lib/require.js"></script> <link rel="stylesheet" href="/style/bootstrap.min.css"/> </head> <body> <div class="container"> <nav class="navbar navbar-default"> <div class="container-fluid"> <ul class="nav navbar-nav"> <li><a href="#">Home</a></li> <li><a href="#secure">Secure</a></li> </ul> <ul class="nav navbar-nav navbar-right"> <li><p class="navbar-text">   </p></li> <li><a href="#login">Login</a></li> </ul> </div> </nav> <div id="main"></div> </div> </body> </html> 

Fix the app.js file I deleted the code that was not necessary for my example so as not to clutter the file with unnecessary functionality. Now app.js looks like this:

app.js
 var express = require('express'); var path = require('path'); var favicon = require('serve-favicon'); var logger = require('morgan'); var cookieParser = require('cookie-parser'); var bodyParser = require('body-parser'); var app = express(); app.use(logger('dev')); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: false })); app.use(cookieParser()); app.use(express.static(path.join(__dirname, 'public'))); //Routes app.get('/', function(req, res, next) { res.sendFile(path.join(__dirname, 'public/tpl/index.html')); }); // catch 404 and forward to error handler app.use(function(req, res, next) { var err = new Error('Not Found'); err.status = 404; next(err); }); // development error handler if (app.get('env') === 'development') { app.use(function(err, req, res, next) { res.status(err.status || 500); res.json({ message: err.message, error: err }); }); } // production error handler app.use(function(err, req, res, next) { res.status(err.status || 500); res.json({ message: err.message, error: err }); }); module.exports = app; 


And add the application load in init.js:

 require([ 'backbone', 'router', ], function(Backbone, Route){ //      var appRoute = new Route(); Backbone.history.start(); }); 

Run our application, and see what happened. Create a view for our login page.

public / js / login_view.js

 define([ 'backbone', 'text!tpl/login.html', //   'vk', //Vk Api 'fb' //Fb Api ], function(Backbone, Tpl, VK, FB){ return Backbone.View.extend({ initialize: function () { this.render(); }, events: { 'click #fb_login' : 'fb_login', 'click #vk_login' : 'vk_login' }, fb_login: function(e){ e.preventDefault(); }, vk_login: function(e){ e.preventDefault(); }, render: function(){ this.$el.html(Tpl); } }); }); 

Add a template for the login page:

 <h3>Login</h3> <a href="" id="fb_login">   Facebook</a> <br> <a href="" id="vk_login">   Vkontakte</a> 

Log in with Facebook Api




For authorization through Facebook api we need to create an application. I have already created it, and you can do this by following the link by following simple instructions.

We initialize connection to API.

public / js / login_view.js:

 initialize: function () { FB.init({ appId: ID , cookie: true, oauth: true}, function(err){ console.log(err); }); this.render(); }); 

We update the page in the browser and see the error in the console:

URL blocked: We cannot redirect you, the URI is not in the white list of the client settings application. Make sure that the client and Web OAuth Login are enabled and add your applications as domains with valid OAuth URI redirection.

This is because we have not added our domain to the application settings. Let's add localhost: 3000 / to the list of valid URLs. To do this, go to the settings of our application, then "Login through Facebook", and add localhost: 3000 / in the field "Valid URLs for OAuth redirection" and click save.

Now you need to log in to the Facebook API. To do this, call the login method, which takes the calback function as the first argument and the rights object. Request basic information + user email.

public / js / login_view.js:

 fb_login: function(e){ e.preventDefault(); FB.login(function(res) { console.log(res); }, { scope: 'public_profile,email'} ); }, 

Now, refreshing the page and clicking on “Sign in with Facebook” we will have a window in which Facebook will ask to confirm the login to our application. After confirmation, you can see in the browser console a response from the API. We are interested in the status parameter and authResponse.accessToken.

Status - the status of the current user. Possible values:


accessToken is an access token that we will use in the future.

Let's add a status handler and get the information we need about the current user:

 fb_login: function(e){ e.preventDefault(); FB.login(function(res) { if (res.status === 'connected') { var fields = ['id', 'first_name', 'last_name', 'link', 'gender', 'picture', 'email']; FB.api('/me?fields=' + fields.join(','), function(res) { console.log(res); }); } }, { scope: 'public_profile,email'} ); }, 

Now logging in to the console, we will see the data object that we requested. Read more about the information you can get here .

Fine. We received user information from facebook, but on the client side it is not particularly useful. I would like to authorize the user on the server side and write data about him to the database.

To send a request from the server, we need an access_token, which we received a little earlier. Let's send it to the server:

 fb_login: function(e){ e.preventDefault(); FB.login(function(res) { if (res.status === 'connected') { $.ajax({ url: '/auth/facebook', method: 'POST', data: { accessToken: res.authResponse.accessToken }, dataType: 'JSON', success: function(res){ console.log(res); } }); } }, { scope: 'public_profile,email'} ); }, 

And on the server we will request information from Facebook:

app.js:

 app.post('/auth/facebook', function(req, res, next){ var accessToken = req.body.accessToken; var profileFields = ['id', 'first_name', 'last_name', 'link', 'gender', 'picture', 'email']; var request = require('request'); request({ url: 'https://graph.facebook.com/me?access_token=' + accessToken + '&fields=' + profileFields.join(','), method: 'GET', json: true },function (error, response, body) { /** *      */ res.cookie.login = 'test'; res.cookie.hash = 'test'; res.json(body); }); }); 

I saved login and hash for further demonstration of authorization. When sending a request, it is necessary to specify json: true in order to get a javascript object, and not a json string. Restart the application, log in, and see the answer in the browser console. Fine. Everything works as it should.

Authorization via Vkontakte Api.




Authorization through Vkontakte is not much different from Facebook, so I will describe in less detail. Create an application for authorization here . We initiate connection to VK API:

 VK.init( { apiId: ID  },function(res) { console.log('success'); }, function(res) { console.log('error'); }, '5.53'); 

Log in. (The second parameter to the login method is the number that indicates the rights that we want to receive).

 vk_login: function(e){ e.preventDefault(); VK.Auth.login(function(res){ console.log(res); }, 4194304 ); }, 

We look into the console and see the answer. We also have a status parameter and a sig (access_token) + user object containing some information about the user.

Then everything goes not as smoothly as with Facebook.



Problem 1


The resulting token (sig) is bound to the ip-address, and when you try to use it on the server, you will get the error: "User authorization failed: access_token was given to another ip addres" . and when we receive a token on the client side, we will not be able to use it on the server.

The most interesting thing in the current situation is that it is not so easy to detect if you develop and test on one ip. The problem can emerge only on the combat server.

On the Internet, there is a myth that in the scope you need to specify the permission “offline”, then the token will be “eternal” and not bound to IP. But this method does not remove the binding to the ip-address.

offline (+65536)Access to the API at any time (when using this option, the expires_in parameter returned with the access_token contains 0 - perpetual token).

Problem 2


With this method of authorization, it is not possible to receive the user's email, even if you request the necessary rights and the user agrees - you will not receive an email in the reply.

When server authorization is described in the documentation vk.com/dev/authcode_flow_user , if you specify email in the scope, it will be returned along with the token. When using open api, the email address does not come with a token. Turning to technical support, I received the answer:

Support Agent # 1605
Currently, the possibility of receiving e-mail is provided only when using OAuth authentication, using the Open API, this will not work.

How to be?


The token received by the client, we can not use on the server, and accordingly we can not request information about the user from the server, but we can check the token for validity and find out the ID of the user who owns this token.

From the documentation we can find out that the sig parameter is equal to md5 from the concatenation of the following lines:


Let's get information about the user through open api, transfer it to the server, check the token, and if we write everything to the database:

 vk_login: function(e){ e.preventDefault(); VK.Auth.login(function(res){ if (res.status === 'connected') { var data = {}; data = res.session; var user = {}; user = res.session.user; VK.Api.call('users.get', { fields: 'sex,photo_50' }, function(res) { if(res.response){ user.photo = res.response[0].photo_50; user.gender = res.response[0].sex; data.user = user; $.ajax({ url: '/auth/vk', method: 'POST', data: data, dataType: 'JSON', success: function(res){ console.log(res); } }); } }); } }, 4194304 ); }, 

To create an md5 hash, use crypto:

 npm install crypto 

app.js:

 app.post('/auth/vk', function(req, res, next) { var secretKey = '( . )( . )'; //   var sig = req.body.sig, expire = req.body.expire, mid = req.body.mid, secret = req.body.secret, sid = req.body.sid, user = req.body.user; var str = "expire=" + expire + "mid=" + mid + "secret=" + secret + "sid=" + sid + secretKey; var hash = crypto.createHash('md5').update(str).digest('hex'); //  if(hash == sig){ /** *     ,  ,   . */ res.cookie.login = 'test'; res.cookie.hash = 'test'; res.json({ success: true }); } else { res.json({ success: false }); } }); 

Now our application checks the token and user id that came and we can authorize the user on the server based on this data.

Authorization check


Let's create a model that will contain user information:

public / js / models / user.js:

 define([ 'backbone' ], function(Backbone){ var User = Backbone.Model.extend({ url: '/auth/getUser', initialize: function(){ console.log('user model was loaded'); //  .  -  -  auth this.on('change', function(){ if(this.has('login')){ this.set('auth', true); } }); }, defaults: { auth: false }, isAuth: function(){ return this.get('auth'); }, logout: function(){ //   this.clear(); //    $.post( "/auth/logout" ); } }); return new User(); }); 

Now let's load the user model before launching our application:

public / js / init.js:

 require([ 'backbone', 'router', 'models/user' ], function(Backbone, Route, User){ //      User.fetch().done(function(){ var appRoute = new Route(); Backbone.history.start(); }); }); 

And add the check to router.js:

router.js
 define([ 'baseRouter', 'views/login_view', 'models/user' ], function(BaseRouter, LoginView, User){ return BaseRouter.extend({ initialize: function(){ //  this.model = User; //   auth,      this.listenTo(this.model, 'change:auth', function(){ Backbone.history.loadUrl(); }); }, routes: { "" : "index", "#" : "index", "secure": "secure", "login" : "login", "logout": "logoute" }, //     secure_pages: [ '#secure' ], before : function(params, next){ //  var path = Backbone.history.location.hash; //       ? var needAuth = _.contains(this.secure_pages, path); if(path == '#login' && User.isAuth()){ this.navigate("/", true); }else if(!User.isAuth() && needAuth){ this.navigate("login", true); } else { next(); } }, index: function(){ $('#main').html('Index page'); }, secure: function(){ $('#main').html('Secure page'); }, login: function(){ $('#main').html( new LoginView().el ); }, logoute: function(){ this.navigate("/", true); this.model.logout(); } }); }); 


Add a route for receiving user information on the server:

 app.get('/auth/getUser', function(req, res, next){ /** *     */ if(res.cookie.login == 'test' && res.cookie.hash == 'test'){ res.json({ login: 'text', hash: 'text' }); } else { res.send({}); } }); 

and logout routing:

 app.post('/auth/logout', function(req, res, next){ res.cookie.login = ''; res.cookie.hash = ''; }); 

The final touch is to add user_view, in which we will display information about the user in the header:

public / js / views / user_view.js:

 define([ 'backbone', 'text!tpl/user.html' ], function(Backbone, Tpl){ return Backbone.View.extend({ tpl: _.template(Tpl), initialize: function(){ this.render(); //  ,  -  -  this.listenTo(this.model, 'change:auth', function(){ this.render(); }); }, events: { //    'click #logout':'logout' }, logout: function(e){ e.preventDefault(); //  this.model.logout(); }, render: function(){ this.$el.html( this.tpl({ user:this.model.toJSON() })); } }); }); 

Template for user_view:

public / tpl / user.html:

 <ul class="nav navbar-nav navbar-right"> <li> <p class="navbar-text">   <%= user.auth ? user.login.toUpperCase() : '' %></p> </li> <li> <% if(user.auth){ %> <a href="" id="logout">Logout</a> <% } else {%> <a href="#login">Login</a> <% } %> </li> </ul> 

And change the index.html:

 <!DOCTYPE html> <html> <head> <title></title> <script data-main="/js/init" src="js/lib/require.js"></script> <link rel="stylesheet" href="/style/bootstrap.min.css"/> </head> <body> <div class="container"> <nav class="navbar navbar-default"> <div class="container-fluid"> <ul class="nav navbar-nav"> <li><a href="#">Home</a></li> <li><a href="#secure">Secure</a></li> </ul> <div id="user-info"></div> </div> </nav> <div id="main"></div> </div> </body> </html> 

Run our application and enjoy.

»Sources on Github .

Source: https://habr.com/ru/post/310594/


All Articles