Introduction
In this series of articles, I’ll cover different ways for a user authentification in systems with separated frontend and backend. As an example, I took my lovely programming language Ruby with RoR, with which I’ve been working already four years, for API and React application, based on CRA template, for separated frontend.
Source code for SPA you can find here. For API — here.
Problem
Imagine that some people request to develop a system for storing the most valuable thing for them — their names. Besides, users love to admire their treasure only personally. For it, they wish that the system must show name only after logging in and must not ask it for one week. Moreover, they are planning to develop GUI and API by different teams, so these parts must be independent applications.
Design — API
A core entity of the API has a model User that contains only three fields:
- login — string which users do not scare to show;
- password — stored as a password digest;
- name — sacred for every user information that we only show when they are authorized.
Design — SPA
The page has only one block, that is show login form if user not authorized and not blank field “Name” above in case of successful authentication.
Let’s go further and consider how to authenticate our users by cookies.
Authentication by cookies
The most common and obvious approach is to use HTTP cookies for storing auth information. Ruby on Rails has two similar mechanisms for working with cookies, it’s cookies themselves and sessions. For cookies, we can set an httponly flag, that protects from xss attacks, domain, and expiration date. Sessions are stored in cookies inside an encrypted string, where an httponly flag is set by default. For this example, I took sessions because the SPA does not read from cookies.
How it works:
- SPA sends a POST request with login and password
- API write user.id in the session cookie
- Component try to get the name of the user sending a request with the session
- API find a user by user id and if all right return name of this user
- Component is updated
Let’s dive deeper.
Usually, SPA and API are deployed on different hosts, hence there appears the next problem — how to pass and modify cookies. By default browser does not set cookies from another origin by javascript. But we can easily enable it.
SPA side.
For communicating with a server SPA uses the Fetch API that is provided in a global window scope. For enabling a possibility to send and receive cookies from resources with a different origin. We must set the next option:
- credentials: ‘include’ — it enables sending cookies for cross-origin requests by default it is set for the same origin;
- mode: ‘cors’ — allows to work with all headers related to CORS. By default, it allows only for same-origin requests.
Examples you will find further.
Server side.
To enable the supporting of cross-origin requests in RoR, you must add gem rack-cors that provides support for CORS for a Rack middleware. When you create rails application from a generator with API you need only uncomment string “gem ‘rack-cors’” in Gemfile and content of the config file config/initializers/cors.rb
. For setting cookies you must set parameter credentials as true. Important notice, it works only if the origin is not a wildcard. For security reason and flexibility I usually set it from environment variables like there:
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins ENV['SPA_ORIGIN']
resource '*',
headers: :any,
methods: [:get, :post, :put, :patch, :delete, :options, :head],
credentials: true
end
end
Sending and handling requests
After the setting our projects for work with cookies let’s look at how are requests handled.
Post request contains data and cors friendly settings, about I mentioned above.
const authUrl = apiUrl + 'login'
let payload = {
'data': {
'login': this.state.login,
'password': this.state.password
}
}
let headers = {
'Content-Type': 'application/json'
};
fetch(authUrl, {
method: 'POST',
mode: 'cors',
cache: 'no-cache',
headers: headers,
redirect: 'follow',
referrer: 'no-referrer',
body: JSON.stringify(payload),
credentials: 'include'
});
Request handled by standard Rails controller. API finds a user and if all right writes user’s id in a session.
class AuthController < ApplicationController
include ::ActionController::Cookies
def login
if params['data']['login'] && params['data']['password']
user = User.find_by(login: params['data']['login'])
if user && user.authenticate(params['data']['password'])
session[:user_id] = user.id
else
render json: {message: 'Wrong login or password'}, status: 403
end
else
render json: {}, status: 401
end
end
end
Next requests for getting the name send this session and controller just read it and sends name.
let username_url = apiUrl + "name";
let headers = new Headers({
'Content-Type': 'application/json'
});
if(this.state.name === null) {
fetch(username_url, {
method: 'GET',
mode: 'cors',
headers: headers,
cache: 'no-cache',
redirect: 'follow',
referrer: 'no-referrer',
credentials: 'include'
})
.then(function (response) {
return response.json();
})
.then(myJson => {
this.setState({name: myJson['name']});
});
};
..and related controller:
class UsersController < ApplicationController
include ::ActionController::Cookies
before_action :find_user
def name
if @current_user.present? && @current_user.is_a?(User)
render json: {name: @current_user.name}
else
render json: {message: 'Bad user'}, status: 401
end
end
private
def find_user
user_id = session[:user_id]
@current_user = User.find_by(id: user_id)
end
end
Pretty simple!
Pros
Security — httponly flag prevents cookies from stealing your auth data by XSS attacks. (I hope that you use https by default).
Simplicity — mechanisms for working with cookies and sessions are proven and exist almost in all frameworks.
Cons
Works only inside with web-browsers.
Top comments (0)