DEV Community

Alan Lamas
Alan Lamas

Posted on

How can we login through SAML 2.0 using our API REST developed in Ruby on Rails?

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.

class User < ApplicationRecord
  devise :saml_authenticatable
  include DeviseTokenAuth::Concerns::User
Enter fullscreen mode Exit fullscreen mode

At the bottom of the file, add this method and leave it empty.

  # We don't have passwords
  def remove_tokens_after_password_reset; end
Enter fullscreen mode Exit fullscreen mode

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.

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
Enter fullscreen mode Exit fullscreen mode

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.

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
Enter fullscreen mode Exit fullscreen mode

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

    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
Enter fullscreen mode Exit fullscreen mode

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.

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
Enter fullscreen mode Exit fullscreen mode

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.

  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
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)