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:
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).
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.
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.
- 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
- 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.)
- 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.
- Generate home controller and views
rails g controller Home index
- 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]
inUser
modelcreate
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
toUser
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.
Subscribe to my newsletter
Read articles from Alexandr Kalinka directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by