Reputation: 169
I am trying to wrap my head around implementing proper authentication in an SPA with a backend.
I am looking at 2 different frontends: 1. SPA with Ember.js 2. Mobile application
I am trying to design a backend that serves both (either in Rails or in Python, have not decided yet). I want the authentication to take place either via Google or Facebook, i.e. I do not need to maintain separate user registrations but I still need to maintain users as I want to be able to 'merge' authentications from Google and Facebook at the end of the day. I also want the backend to be fully stateless for scalability reasons. Also I would prefer to do as much as possible on the client side to spare the load from the backend.
My questions are as follows:
I will request an authentication token in the SPA or in the mobile app. Should I convert this to an access token on the server side or on the client side? I need to authorize backend requests and I need to do that statelessly. My idea was to do everything on the frontend side and pass along the access token to each and every backend request, to be validated on the server side as well but I am not sure if this is an efficient approach.
I want to be able to merge Google and Facebook authentications, what are the best practices for that? I was planning to maintain an user registry on the server side and check the email addresses coming from the authorized backend requests and merge the users if there is a match in email addresses. What are the best practices and are there any libraries supporting this in Python/Flask or in Ruby or Rails?
Thanks!
Upvotes: 1
Views: 181
Reputation: 6555
I'm not really sure what do you mean by 'stateless'. You obviously need some database to store user's data (otherwise you don't need backend at all). So the database is your state. HTTP protocol is stateless by definition, so you can't really be very stateful by other means than storing data in DB.
I will request an authentication token in the SPA or in the mobile app. Should I convert this to an access token on the server side or on the client side?
If you don't need to use Google/Facebook on behalf of your users (and your wording suggests that you don't), you don't need to convert auth_token to server_token at all.
You just need to call Google/Facebook API with that (Ruby has libraries for both, so it's basically a one line of code) and get social network's user ID and user email.
Then you save that ID+email in your database and give your internal server token (just random string) to your user. You generate that string yourself and give it to the client.
If user logs in from another device (i.e. it gives you auth_token
with which you find out that user's email belongs to one of already-registered users), you either return existing internal token, or generate new one and bind it to the existing user (depends on what you prioritize – high security of simplicity of implementation/maintenance).
I want to be able to merge Google and Facebook authentications, what are the best practices for that?
Facebook guarantees that if it gives you user email, then it's ensured that that email belongs to the given user. Google, obviously, does the same. So you just merge them by emails.
You don't need some special libraries for that, as it is simple operation with you code on the language of your choice.
I'd organize all the things in database in the following manner:
Users table
id
email
Authentications table
user_id
email
social_uid # facebook number or google email
social_network # string, 'facebook' or 'google'
device # user agent, e.g. 'android'
ip # last login IP address
token # internal token
When user logs in, Authentication object is created. If there is no user with such email, the user is created. If there is a user, it's bind to the authentication object (both via user_id
field).
Notes on access tokens
If you do plan to interact with social network (in other ways than just authenticating user), you should exchange auth_token
for server_token
. server_token
is a 'permanent' (well, kind of) authorization token for accessing APIs of social network, whereas auth_token
has a very limited lifespan (and some API calls may be restricted if you didn't obtain server_token
).
Still, server_token
can expire (or user can recall their access for your application), so you should plan ahead to detect that situation and re-acquire token/authorization if needed.
Key points when building Rails app
In Rails, in order to create tables, you need to write migrations:
gem install rails
rails new my_project
cd my_project
rails generate migration create_users
rails generate migration create_authentications
This will generate project folder structure and two migration files, which you need to fill in:
# db/migrate/xxx_create_users.rb
def change
create_table :users do |t|
t.string :email
end
end
# db/migrate/xxx_create_authentications.rb
def change
create_table :authentications do |t|
t.integer :user_id
t.index :user_id
t.string :social_uid
# etc., all other fields
# ...
end
end
Then you generate 'models' to handle database-related manipulations:
# app/models/user.rb
class User < ActiveRecord::Base
has_many :authentications
end
# app/models/authentication.rb
class Authentication < ActiveRecord::Base
belongs_to :user
before_create :set_token
after_commit :create_user_if_needed
private
def set_token
self.token = SecureRandom.urlsafe_base64(20)
end
def create_user_if_needed
unless self.user.present?
self.user.create(email: self.email)
end
end
end
And write 'controller' to handle request from user with a single method inside of it:
# app/controllers/login_controller.rb
class LoginController < ActionController
# Login via Facebook method
def facebook
token = params.require(:auth_token)
# we will use Koala gem
profile = Koala::Facebook::API.new(token).get_object('me', fields: 'email')
# user is created automatically by model
authentication = Authentication.where(social_network: 'facebook', social_uid: profile['id'], email: profile['email']).first_or_create
authentication.update(...) # set ip and device
render json: {token: authentication.token}
end
# This one you'll have to write yourself as an exercise :)
def google
end
end
Of course, you will need to setup routes for your action:
# config/routes.rb
post "login/:action", controller: 'login'
And add Koala (or whatever else you'll use to manage external APIs for which good Ruby packages already exist) to Gemfile:
# Gemfile
gem 'koala'
Then in your terminal run:
bundle install
rails server
And your app is up and running. Well, you'll need to setup your Facebook and Google applications first, get developer keys, and authorize localhost
to accept auth_tokens
.
Basically, that's it.
Upvotes: 1