📜 ⬆️ ⬇️

Single sign-on on omniauth and rails


User authentication in ecosystems like Google or Envato is implemented as separate services ( accounts.google.com , account.envato.com ) that provide the necessary data and tokens to client sites. During the development of some projects on Ruby on Rails, I had to face a similar task. Scientifically - single sign-on or single sign-on technology .

We needed (1) a common service for all sites of the ecosystem, with (2) predominantly social authorization, for the sake of logging in using the “login + password” combination.
Service, (3) accumulating in itself data from those social services with the help of which the user logs into the system, and (4) providing this data to client sites.

The task turned out to be as interesting as it was non-standard. It all started with a useful, but already slightly outdated article - the author suggested using the omniauth gem and custom strategy on client sites, and on the provider site - using the same omniauth in conjunction with devise for authentication via social. Services.
')
In my case, Devise didn’t fit a little (set on login + password), so omniauth was completely preferred. This was the beginning of my little adventure, the course of which I invite you to read in this article.

General scheme


Three projects will be considered: client site, provider site and omniauth custom strategy . The links are all available on github and ready to use. The article will be raised only the key points.

Client site

We will run on localhost: 4000 .
The structure is standard for any sites using omniauth:

gem 'omniauth' gem 'omniauth-accounts' 


 Rails.application.config.middleware.use OmniAuth::Builder do provider :accounts, ENV['ACCOUNTS_API_ID'], ENV['ACCOUNTS_API_SECRET'], client_options: { site: ENV['ACCOUNTS_API_SITE'] } end 


 match '/auth/:provider/callback', :to => 'auth#callback' 


 rails g controller auth --skip-assets # auth_controller.rb class AuthController < ApplicationController def callback auth_hash = request.env['omniauth.auth'] render json: auth_hash end end 

That's all, minimal.

Strategy

A derivative of the standard oauth 2.0 strategy, omniauth-oauth2 is indicated in the Gemspec. There is very little code, and besides, it does not make sense to adjust it for yourself; all the necessary strategies are transmitted in the initialization parameters (in the example, as environment variables). It:

Having received this data, the strategy takes up all further work. Because of this, however, in the course of development, unpleasant incidents may occur, when in a certain situation the strategy is “lost” - it cannot continue its implementation further as planned. I had to face such problems, and the solution to each was found - which will be discussed further in the article.

Site provider

We will run on localhost: 3000 .
Combines the two halves:


Authentication on the site provider occurs using standard omniauth strategies.
Authentication on the client site - using a custom strategy.

Common link - account:

ISP authentication and account management


When registering on the client site, it is pleasant to fill in most of the required fields automatically from your Facebook or Twitter profile. Our site provider will play the role of an aggregator - let it aggregate all the data from the social. services in a single profile, which can be supplemented manually, and client sites will take information from there.

This topic has previously skipped on the pages of Habr. Unfortunately, I can’t find this article in any way, but there, in particular, the question was raised about typical problems with social authentication on the site:

All this is typical requirements for a system of this type, as well as validation with sending a letter to email - a traditional requirement that has emerged in authentication by login + password. A brief look at these requirements.

Merge accounts

Signed in through the gmail-box - the system created one account, with data from gmail. The next time you went through facebook, and the system again created a new account. We look and understand that the last time we created an account for ourselves through ... we remember ... gmail! We click on the button, we go through gmail this time and our accounts merge into one - just like two pennies! .. or not - there is one problem. Merge data.

In gmail, we are Alexander Polovin, and on Facebook, Alex Polovin. And what data to leave in your account?

Immediately when you merge, ask the user what to leave? No, this is a very unsuccessful undertaking in terms of usability - the user merges accounts so that he can quickly go back using the account to the site he had visited before, he doesn’t have time to be distracted by the “Replace” and “Replace all” dialogs.

My decision was to add new data “as a reserve”, as additional values ​​of the account fields. In fact, all account data is stored in a hash, and this hash can take the following form after the merger (we’ll also add the data from the conditional twitter - Half of Alex):
 { name: [' ', 'Alex Polovin', ' '], first_name: ['', 'Alex', ''], sir_name: ['', 'Polovin'], ... } 

As you can see, the values ​​are simply added to the array for each field. However, they are not duplicated - “Half” of Twitter has not been preserved as a duplicate in the “last name”.

Client sites will always receive the very first value from the arrays, if desired, the user can put any of the values ​​in the first place.

Account Update

Among all the data available omniauth from social. services, the most frequently updated avatar user. Slightly less often - links to pages (the urls parameter), nickname and description on Twitter. In any case, there is a desire to update them in your account with a single click ... or leave the former ones - the situations are different. Our algorithm is great for this — it writes new values ​​to the end of the arrays without saving duplicates.

Linking different services to one account

Analogue of the key for the habre - the system creates an entry in the authentication table and binds to the current account. It is used later as a key and as a data source.

Manual editing of account fields

Not all fields are filled from social. services. The user should be able to fill in the missing data on their own, on the website of the provider. And also - swap the values ​​in the arrays, which was mentioned a couple of paragraphs above.

Implementation

Models


To make Rails understand info as a hash: the type of the text field is indicated in the migration, and the code is added to the model:
 serialize :info, Hash 

Between models - one-to-many relationship:
 # /app/models/account.rb has_many :authentications # /app/models/authentication.rb belongs_to :account 


Controllers

AuthenticationsController covers all authentication needs, includes the following actions:

When choosing one of the authentication services, standard omniauth operations are performed — the crown of which, in case of successful authentication, is the callback method call. Depending on the situation, it performs the following actions:

The data hash is formed in a separate private method get_data_hash (), depending on the selected social. service.

To add data to the end of arrays without duplicates, the add_info model method is used (based on the operation of combining arrays):
 def add_info(info) self.info.merge!(info){|key, oldval, newval| [*oldval].to_a | [*newval].to_a} end 

And to bind authentication, add_authentications:
 def add_authentications(authentications) self.authentications << authentications end 

As a result, the session of the account id for which the login was made is saved in the session - session [: account_id].

AccountsController at this stage contains the following actions:

And also the filter is a mandatory check for the presence of a user in the network (with a redirect to the login page).

I really wanted to achieve the possibility of really convenient and flexible data changes. And such a task is still standing and will be worked out in the future. For now - editing is done in two ways:



Creating a connection between the client site and the provider site


The standard practice in this case is to create an “application” on the provider’s website. Specify the name and address of the client site (or rather, the address for the callback redirect) - and we get two keys - id and secret. We specify them in the parameters of the social authentication system - whether it is any plug-in to cms, or hem for Rails. In our case - the keys are used omniauth - ACCOUNTS_API_ID and ACCOUNTS_API_SECRET.

Implement application support in the site provider is easy:
 rails g scaffold Application name:string uid:string secret:string redirect_uri:string account_id:integer rake db:migrate # account.rb has_many :applications 

When creating a new record, the model should generate keys for it:
 before_create :default_values def default_values self.uid = SecureRandom.hex(16) self.secret = SecureRandom.hex(16) end 

And - in all actions on the application should be filtered by the current user. For example, instead of:
 @applications = Application.all 

used by:
 @applications = Account.find(session[:account_id]).applications 

And - be sure to ensure that the user is online - put a filter:
 before_filter :check_authentication def check_authentication if !session[:account_id] redirect_to auth_path, notice: '    ,    .' end end 


Process flow

Authentication is based on oauth 2.0 - the principles of this protocol can be found in this article on Habré, or clearly here.

The starting point is client-site.com/auth/accounts. It is picked up by omniauth and, using the omniauth-accounts strategy, sends a request to the server of the site provider.

At the same time, omniauth generates the state parameter, which helps the provider not to confuse the request from one client site and user with other requests.

The site provider accepts the request (according to the standard - at provider-site.com/authorize), and performs certain actions. The goal of the provider at this stage is to authorize the user and grant him a grant for authentication on the client’s site.

If the goal is achieved, a redirect from the site provider is redirected to the callback method of the site client, in which through request.env ['omniauth.auth'] we get a hash with tokens and data from the site provider.

Authorization

The authorize method - the darkest place in the process scheme - there are a lot of nuances to consider before issuing a grant to the user.

Ideally (when re-authorization) - the following conditions are met:

In this case, the user is authorized immediately and redirects to the callback method of the client site. The parameters are the grant code and the state.

If at least one of these conditions is not met, you must first resolve the problems:

These actions involve navigating the site provider and even the social sites. services (if the user needs to log in). The latter turned out to be not without purpose — it is in this place that the omniauth shows its unpleasant sides.

The fact is that omniauth, when going to authorize, passes several parameters to the url, and also writes several parameters to the session of the site provider. This is necessary for him to correctly redirect to the callback method. But if we suddenly want to use omniauth on the provider site (for example, when trying to log in through a social service), omniauth will erase its data from the session. And the redirect will end with the error OmniAuth :: Strategies :: OAuth2 :: CallbackError - invalid_credentials.

Therefore, in order to avoid such situations, all omniauth parameters are clearly fixed in the session and are restored just before the redirect.



orders # register

If all parameters are passed correctly (that is, the request came exactly from omniauth) - create a record in the current session - “order for the grant” and save all the parameters in it:
 session[:grants_orders] = Hash.new if !session[:grants_orders] session[:grants_orders].merge!( params[:client_id] => { redirect_uri: params[:redirect_uri], state: params[:state], response_type: params[:response_type], 'omniauth.params' => session['omniauth.params'], 'omniauth.origin' => session['omniauth.origin'], 'omniauth.state' => session['omniauth.state'] } ) 


orders # show

Perform all checks here. Whether the user is online, whether the application that the request came from is registered on the site, is there an old grant, is it overdue.



orders # accept

It is carried out if the grant was right away and was suitable for the requirements, or when you click on the “Allow” button on the order page of the grant.


orders # deny

Cancel the application, simply remove it from the session.

grants # token

According to the parameters passed, we find the application and the grant. If everything is in order, we issue grant tokens in json format.

accounts # get_info

We return the hash in json format, as it was agreed - only the first values ​​of the parameters, if they are represented by an array.
 data_hash = grant.account.info hash = Hash.new hash['id'] = grant.account.id data_hash.each do |key, value| if value.kind_of?(Array) hash[key] = value[0] else hash[key] = value end end render :json => hash.to_json 


Conclusion


The solution turned out to be ingenious - and in it, for sure, much can be improved and optimized. The following tasks are currently outlined:

Not every day you have to write a similar system - after all, in fact, there are not so many ecosystems on the Internet. Google, Envato, Yandex, Yahoo - and who else? Perhaps - your project? And this is not the only way to introduce authentication into related projects - there is CAS technology (a couple of useful links), there is OpenID (and as an option, the same Loginz). On our own homepage and other TM projects, there is generally a separate authentication system on each site, plus the proprietary "Key House".

Why did my choice fall on SSO? Perhaps the key “for” is the atmosphere. These are the feelings that the user experiences when he enters not the site, but the System - with a capital "C". Into a powerful, advanced, developed System - this is truly an amazing feeling, colleagues.

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


All Articles