How can we login through SAML 2.0 using our API REST developed in Ruby on Rails?
Tags: ruby, rails
Published: 2020-03-24 23:11:52

**Our problem**
We have an API REST developed in RoR and our client has a Single Sign On service which we need to login through SAML 2.0 standard to use our application.

**Solution**
We’ll see how integrating these 3 gems help us achieve our goal. If you want to know what these 3 gems do, just go to the Github page of each one.
- devise_saml_authenticatble, version 1.5.0 (From now, DSA)
- devise_token_auth, version 1.1.0 (From now, DTA)
- devise, version 4.6.2

**Some important concepts**
There are a large number of articles about SAML standard, but in my opinion, i didn’t found one that explains correctly these concepts.
- Identity Provider or IdP: The service in which we want to login, generally with a user and a password. This is usually provided by our client.
-  Service Provider or SP: Is our API. In my case, my RoR API.

**Installations**
Previously, we have to have the “create_users” migration and User model, after that, we need to install the 3 gems and run Devise’s generator and then, DTA’s generator setting up the class and the mount path.

**Model**
We add these two lines in model.

```ruby
class User < ApplicationRecord
  devise :saml_authenticatable
  include DeviseTokenAuth::Concerns::User
```

At the bottom of the file, add this method and leave it empty.
```ruby
  # We don't have passwords
  def remove_tokens_after_password_reset; end
```

We do this because we don’t need passwords in our database, this is work for the IdP.

**Routes**
We have to rewrite the routes after run the DTA’s generator.
```ruby
Rails.application.routes.draw do
  mount_devise_token_auth_for 'User',
    at: 'api/v1/auth',
    skip: %i[omniauth_callbacks],
    controllers: { sessions: 'api/v1/sessions' }

  devise_scope :user do
    get '/users/saml/sign_in', to: 'saml/auth#new'
    get '/users/saml/metadata', to: 'devise/saml_sessions#metadata'
    post '/users/saml/auth', to: 'dta_saml/sessions#create'
    delete '/users/saml/dta_logout', to: 'dta_saml/sessions#destroy'
  end
```

**Controllers**
For the controllers, we create two new ones: 
- saml/auth_controller.rb
- dta_saml/sessions_controller.rb

*auth_controller.rb*
This controller will create the URL and redirect us, with a request, to the IdP to login, also the controller will test out if you are logged in or not. We will rewrite the #new method of DSA to edit the authentication flow.
```ruby
module Saml
  class AuthController < Devise::SamlSessionsController
    def new
      idp_entity_id = get_idp_entity_id(params)
      request = OneLogin::RubySaml::Authrequest.new
      auth_params = { RelayState: relay_state } if relay_state
      action = request.create(saml_config(idp_entity_id), auth_params || {})
      redirect_to action
    end
```

After that, we rewrite the “ require_no_authentication” method of Devise to eliminate all we don’t need and we add a forced logout. This is related to Warden and i won’t explain it here. Keep in mind that the 'action' variable is the URL of the IdP's page
```ruby
    private

    def require_no_authentication
      assert_is_devise_resource!
      return unless is_navigational_format?

      no_input = devise_mapping.no_input_strategies
      authenticated = validate_credentials(no_input)
      logged(authenticated)
    end

    def validate_credentials(no_input)
      if no_input.present?
        args = no_input.dup.push scope: resource_name
        warden.authenticate?(*args)
      else
        warden.authenticated?(resource_name)
      end
    end

    def logged(authenticated)
      warden.logout
      resource = warden.user(resource_name)
      redirect_to after_sign_in_path_for(resource) if authenticated && resource
    end
```

*sessions_controller.rb*
Create
In this controller, we combine all we need of DSA and DTA. When we get the authentication response of the IdP, it will be decoded in the first line of the #create method and it will continue with the common process of DTA returning, in @client_id, 2 variables (client and token) and in @resource, the User object with it’s email. DTA uses these 2 variables and the email to authenticate us.
```ruby
module DtaSaml
  class SessionsController < DeviseTokenAuth::SessionsController
    def create
      # retrieves the user to authenticate based on SamlResponse
      @resource = warden.authenticate!(auth_options)
      @client_id, @token = @resource.create_token
      @resource.save
      sign_in(:user, @resource, store: false, bypass: false)
      yield @resource if block_given?

      # Here you can return a JWT token with the 3 variables
    end

    def destroy
      warden.logout
      super
    end

    protected

    def auth_options
      { scope: :user, recall: 'devise/sessions#new' }
    end
```

At this point, I suggest use JWT to create a token with those 3 variables and retrieve it to the Frontend to continue the authentication flow of DTA, but we won’t see it in this article.

Destroy
The #destroy method, destroys the DTA's session invalidating the auth tokens.  

**Configuration**
/config/initializers/devise.rb and /config/attribute-map.yml
Configure these files as DSA’s page shows. The necessary data to configure the /config/initializers/devise.rb file should be provided by the IdP.

**Migration**
Modify the DTA’s migration and migrate.
```ruby
  def change
    change_table(:users) do |t|
      t.string :provider, :null => false, :default => "email"
      t.string :uid, :null => false, :default => ""

      ## Tokens
      t.json :tokens
    end
    
    add_index :users, [:uid, :provider],     unique: true
  end
```

**Notes**
If you have some problem with URL’s creation, you can create them manually.

**Conclusion**
We're done!
Now, we can use at the same time, a SSO by SAML 2.0 with an API REST developed Ruby on Rails,
I hope you find it useful, thank you for reading,
Alan