How to implement OpenID authorization in Keycloak server for Rails application.

Sometimes we need to implement an authorization and an authentication system for our Ruby on Rails application. In most of the applications, it is enough to use gem Devise, but sometimes customer asks to implement different types of authorization and authentication. In this article I'm going to explain what are SSO, IdP and how to implement authorization and authentication using Keycloak service. Also, we'll consider how to up and run in local development environment.

Introduction

Let's imagine a situation, when we have some applications, where a user can have accounts. For example, a company have the main service, the seller service, the report service, etc. Yes, the user can register on all of them. But, for example, if the user forgot password on one of the services, and change the password, this password was changed only on one service, on another it will remain the same. And in the future, the user can try to log in with a new password on a service where it wasn't changed.

For this situation, there are many solutions, and one of them – use SSO (Single Sign-On). As we can read on Wikipedia:

Single sign-on (SSO) is an authentication scheme that allows a user to log in with a single ID to any of several related, yet independent, software systems.

True single sign-on allows the user to log in once and access services without re-entering authentication factors.

That means that if the user was registered on one service – he/she will be able to log in to another services connected to this company. And if the user changed the password – it not a problem to log in with the new password on all services.

In other words, Single Sign-On (SSO) is an authentication process that enables users to access multiple applications, systems, or services using a single set of login credentials, typically a username and password. SSO simplifies the user experience by eliminating the need to remember and enter multiple usernames and passwords for different applications.

SSO usually relies on trusted third-party identity providers (IdP) that manage user identities and handle the authentication process. Common SSO protocols include:

  1. Security Assertion Markup Language (SAML): An XML-based standard used for exchanging authentication and authorization data between parties, particularly between an IdP and a service provider (SP).

  2. OpenID Connect (OIDC): A simple identity layer built on top of the OAuth 2.0 protocol, which allows clients to verify the identity of users based on the authentication performed by an IdP.

  3. OAuth 2.0: Although not an SSO protocol itself, OAuth 2.0 is often used as a foundation for building SSO solutions like OpenID Connect. OAuth 2.0 is a widely-used authorization framework that enables third-party applications to obtain limited access to a protected resource on behalf of the user.

Implementation for Ruby on Rails

Keycloak service

Foremost, we need to up the Keycloak service. Keycloak is written on Java and the best way to run and up this service is to use docker compose. Here is a working docker-compose.yml config.

version: '3.5'

services:
  postgres:
    image: postgres
    volumes:
      - postgres_data:/var/lib/postgresql/data
    environment:
      POSTGRES_DB: keycloak21
      POSTGRES_USER: keycloak
      POSTGRES_PASSWORD: password
  keycloak:
    image: quay.io/keycloak/keycloak:21.0.2
    environment:
      KC_DB: postgres
      KC_DB_URL: jdbc:postgresql://postgres:5432/keycloak21
      DB_SCHEMA: keycloak
      KC_DB_USERNAME: keycloak
      KC_DB_PASSWORD: password
      KEYCLOAK_ADMIN: admin
      KEYCLOAK_ADMIN_PASSWORD: P@ssw0rd
    ports:
      - 8080:8080
    volumes:
      - keycloak_data:/keycloak
    command:
      - start-dev
      - --http-relative-path '/auth'
    depends_on:
      - postgres

volumes:
  postgres_data:
    driver: local
  keycloak_data:
    driver: local

After run command docker-compose up and waiting some time, we should open the main page of Keycloak service. I use docker on a separate server in my home network, and I open page opening address http://192.168.1.102:8080/.

For some reason main page for me looks like one line "Resource not found". I think this was done for security reasons. To open login admin form, you should add full path to address like http://192.168.1.102:8080/auth/admin/master/console/

If service works and path in address is correct, you'll see a login form.

That is awesome! It's means that our Keycloak server is running. So, let's add some special settings.

Firstly, we need to add a new realm. For this, press the button "Create Realm" in dropdown toggle, by default there is only one realm - master

I already have one realm, except master. But you should press the button "Create Realm". After that, in new realm form, need to fill only one field - "Realm name". You can fill this like for me "hashnode", but don't forget it, we will use this name in the next steps.

Next small changes I would like to suggest to change - in "Realm settings" menu, in "Login" tab add ability to register user and use email as username. In my service, these settings look like that.

Also, there is one small setting that need to change, but we'll do it later.

Ruby on Rails application

It's not a problem to integrate OpenID authorization into existing application, as into the new application. I'm going to integrate this into a new application. Let's do it step by step.

  1. Create a new application

rails new hashnode-idp -T -d=postgresql --skip-javascript && cd hashnode-idp && rails db:setup

I use ruby version 3.0.2, rails version 7.0.4.3

  1. Add gems
gem 'devise'
gem 'omniauth-keycloak'

After adding gems, need to run commands, described in post install messages or in gem's documentation. I suppose it's not a difficulty for even beginner ruby developers :) (small note: do not need to run rails g devise:views after install devise, we won't use devise views in our application.)

  1. Add User model and migration

rails generate devise User

I added to file migration four lines

t.string :first_name
t.string :last_name
t.string :provider
t.string :uid

After add migration and additional information need to run this migration, execute command rails db:migrate.

  1. Generate home controller and views

rails g controller Home index

  1. Add omniauth callbacks settings
  • in file config/routes.rb change line for users.
devise_for :users, controllers: { omniauth_callbacks: "users/omniauth_callbacks" }
  • in file config/initializers/devise.rb add settings for omniauth.
config.omniauth :keycloak_openid, 
    ENV.fetch('OMNIAUTH_CLIENT', 'account'),
    ENV.fetch('OMNIAUTH_SECRET', ''),
    client_options: {
        site: ENV.fetch('OMNIAUTH_SITE', 'http://192.168.1.102:8080'),
        realm: ENV.fetch('OMNIAUTH_REALM', 'hashnode') },
    strategy_class: OmniAuth::Strategies::KeycloakOpenId

As we can see, we need to fill values OMNIAUTH_CLIENT, OMNIAUTH_SITE, OMNIAUTH_REALM (value of OMNIAUTH_REALM we should set to value of realm, that we set up in the step about Keycloak settings). For saving these variables, you can use rails secret, your own .bashrc or .zshrc files, or some gem like dotenv-rals. It's up to you.

  • add devise :omniauthable, omniauth_providers: %i[keycloakopenid] in User model

  • create users/omniauth_callbacks.rb controller

module Users
  class OmniauthCallbacksController < Devise::OmniauthCallbacksController
    def keycloakopenid
      @user = User.from_omniauth(request.env["omniauth.auth"])

      if @user.persisted?
        sign_in_and_redirect @user, event: :authentication
      else
        flash[:error] = 'Error when trying to login with Keycloak, please try again.'
        session["devise.keycloakopenid_data"] = request.env["omniauth.auth"]
        redirect_to root_path
      end
    end
  end
end
  • add method from_omniauth to User model
  def self.from_omniauth(auth)
    auth = JSON.parse auth.to_json, object_class: OpenStruct
    where(provider: auth.provider, uid: auth.uid).first_or_initialize.tap do |user|
      user.email            = auth.info.email
      user.first_name       = auth.info.first_name
      user.last_name        = auth.info.last_name
      user.password         = Devise.friendly_token[0, 20]
      user.save
    end
  end
  • add button in app/views/home/index.html.erb for login using Keycloak
<% if user_signed_in? %>
  Hello. You are signed as <%= current_user.email %>
<% else %>
  <%= button_to user_keycloakopenid_omniauth_authorize_path, method: :post do %>
    <span> Login with Keycloak </span>
  <% end %>
<% end %>
  • So, almost everything is ready, lets, try to run and login. When we run server and open http://localhost:3000/ we see a page like this.

If we try to press the button "Login with Keycloak" – we'll get an error Could not authenticate you from Keycloakopenid because "Authenticity error". What happened? Let's see application logs.

ERROR -- omniauth: (keycloakopenid) Authentication failure! authenticity_error: OmniAuth::AuthenticityError, Forbidden

To fix it, let's add gem omniauth-rails_csrf_protection to our Gemfile. After run command bundle and restart server we again see our index page and if we press the button – we should redirect to our Keycloak service. But again, we'll get an error.

To fix it we need to log in again in admin console our IdP service, select our realm hashnode, press Clients and for client account add Valid redirect URIs with value *

We should do it only for development environment, it's not recommended to set in production. After that we again try to press the button Login with Keycloak and voilà – we see a sign-in form.

We don't have any users, for this reason we need to create one. Press register link, fill all fields and press Register. After that, the application redirects you to root path but as a logged user.

That's great. We built an application, that can authorize the user using Keycloak IdP service. And if we need to add another application where the user can be logged in – we can do it very easy.

But it's just sign-up / sign-in workflow. There are a lot of work with this application – destroy a session, more detailed setup of realm, add some design (as for a rails application, as for keycloak service), move all variables to config and so on. But the main functionality – authorization with Keycloak - we implemented well.

UPD. This repository https://github.com/Hunk13/hashnode-idp.git contains all code and docker-compose.yml file

Conclusion

Implementing OpenID Connect authorization in a Keycloak server for a Rails application provides a robust and secure solution for managing user authentication and authorization. This article has demonstrated a step-by-step process for integrating Keycloak with a Rails application, highlighting the setup of Keycloak, configuration of the Rails app, and the use of the OmniAuth OpenID Connect strategy. By following these steps, developers can harness the power of Keycloak's identity and access management capabilities, while streamlining the user experience.

Not only does this integration offer enhanced security features, but it also simplifies the process of incorporating multiple identity providers and managing user permissions. Furthermore, Keycloak's extensive documentation and active community support make it an ideal choice for organizations seeking to implement a scalable, customizable, and open-source solution. As Rails applications continue to evolve and grow, the adoption of OpenID Connect and Keycloak will undoubtedly become more widespread, paving the way for more secure and seamless user experiences across the web.

0
Subscribe to my newsletter

Read articles from Alexandr Kalinka directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Alexandr Kalinka
Alexandr Kalinka