
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.
We will run on
localhost: 4000 .
The structure is standard for any sites using omniauth:
- We include omniauth and our omniauth-accounts strategy in Gemfile:
gem 'omniauth' gem 'omniauth-accounts'
- bundle install
- In config / initializers / omniauth.rb insert the initialization code:
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
- In router.rb, add a route for the callback method:
match '/auth/:provider/callback', :to => 'auth#callback'
- We create a controller and a callback method in it, in which we receive through request.env ['omniauth.auth'] the final hash with the data and tokens
rails g controller auth --skip-assets
That's all, minimal.
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:
- Credentials keys (ACCOUNTS_API_ID and ACCOUNTS_API_SECRET) for connecting to the site-provider of the client site
- The address of the site provider ACCOUNTS_API_SITE
- Address for authentication on the provider site (default: / authorize) ..
- ... and to get a token (/ token)
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.
We will run on
localhost: 3000 .
Combines the two halves:
- One - to communicate with the client site
- Another - to communicate with social services

Authentication on the site provider occurs using standard omniauth strategies.
Authentication on the client site - using a custom strategy.
Common link - account:
- Ways to log on to the provider site are tied to it (as a key keeper on a habr)
- Applications and grants for client sites are tied to it.
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:
- Ability to merge accounts
- Update account data when updating services
- Ability to bind different services to one account
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
- Account - stores the data hash (info)
- Authentication - accomplished via omniauth authentication on facebook, twitter, or other service; stores the provider name and uid of the user;
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:
Controllers
AuthenticationsController covers all authentication needs, includes the following actions:
- auth (/ auth) - the page for selecting a service to enter, or update the data of the questionnaire
- logout (/ logout) - logout
- callback (/ auth /: provider / callback) - in this method, the main work is performed on entering, updating data, authenticating bindings, etc.
- failure (/ auth / failure) - performed if an error occurred at the “end of the end” when entering
- detach (/ auth / detach) - disconnects authentication from current account
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:
- Updates the questionnaire data
- Merges two different accounts together
- Binds a new service to the current account.
- Performs a second or initial login.
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:
- index - the output of the user profile in the form of
- edit - edit account profile data
- update - update the questionnaire (POST request from edit)
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:
- If js is disabled, there is a text zone with a YAML-formatted hash
- If enabled, the visual editor of json-structures jsoneditor is loaded.

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
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:
- The user has already logged in to the provider site
- The user has already been granted a grant for this application.
- This grant has not expired.
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:
- If the user has not logged in to the site provider - allow him to do it
- If the grant is not issued - create it
- If the grant has expired - re-create the grant
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.
- If everything is in order, immediately call the method of issuing a grant
- If something is wrong, we’ll show a page that is typical for such authentications (“The application requests access to the account”, “Allow”, “Deny”)

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.
- All omniauth parameters saved in the session are restored so that they are adequately processed by omniauth when redirected to the callback method
- Create a grant and perform a redirect
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:
- To give the possibility, when creating an application, to specify which parameters it will require - is mandatory and optional. And, if the necessary parameters are not in the user profile - to give him the opportunity to enter them directly on the page for receiving the grant
- Provide login login + password - using the omniauth-identity strategy
- Add a logout action to the client site, leaving the system not only on the client site, but also on the provider site
- Solve the problem with the missing session in the token and get_info json requests (this seems to be somehow related to the Rails security system, protect_from_forgery and verify_authenticity_token)
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.
