Rails Quickstart
Integrate UniAuth with Rails using OmniAuth and omniauth_openid_connect. Full OIDC discovery, PKCE, JWKS verification, back-channel logout.
Discovery-first: the strategy auto-fetches every endpoint from https://uniauth.id/.well-known/openid-configuration when you set discovery: true.
1. Install
# Gemfile
gem 'omniauth'
gem 'omniauth_openid_connect'
gem 'omniauth-rails_csrf_protection' # CSRF for omniauth POST requestsbundle install2. Environment
# .env / Rails credentials
UNIAUTH_CLIENT_ID=uni_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
UNIAUTH_CLIENT_SECRET=unis_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx3. OmniAuth initializer
# config/initializers/omniauth.rb
Rails.application.config.middleware.use OmniAuth::Builder do
provider :openid_connect,
name: :uniauth,
scope: [:openid, :profile, :email, :groups],
response_type: :code,
uid_field: 'sub',
discovery: true, # ← auto-fetch all endpoints
pkce: true, # ← required by UniAuth
client_options: {
identifier: ENV['UNIAUTH_CLIENT_ID'],
secret: ENV['UNIAUTH_CLIENT_SECRET'],
redirect_uri: 'https://yourapp.com/auth/uniauth/callback',
# Only issuer needed — discovery fills in the rest
scheme: 'https',
host: 'uniauth.id',
port: 443,
},
issuer: 'https://uniauth.id'
end4. Routes
# config/routes.rb
Rails.application.routes.draw do
get '/auth/uniauth/callback', to: 'sessions#create'
post '/auth/uniauth', to: 'sessions#passthru' # login trigger (POST for CSRF)
delete '/logout', to: 'sessions#destroy'
post '/logout/backchannel', to: 'sessions#backchannel_logout'
end5. SessionsController
# app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
skip_before_action :verify_authenticity_token, only: [:backchannel_logout]
# GET /auth/uniauth/callback
def create
auth = request.env['omniauth.auth']
# omniauth_openid_connect already verified the ID token signature
# via UniAuth's JWKS, checked iss/aud/exp/nonce.
info = auth.info # email, name, image…
claims = auth.extra.raw_info # full ID token claims (groups, etc.)
user = User.find_or_create_by(uniauth_sub: auth.uid) do |u|
u.email = info.email
u.display_name = info.name
end
# Map UniAuth 'groups' claim to Rails role / admin flag
groups = Array(claims['groups'])
user.update!(
email: info.email,
is_admin: groups.include?('admin'),
role: groups.include?('admin') ? 'admin' :
groups.include?('staff') ? 'staff' : 'user',
)
session[:user_id] = user.id
session[:id_token] = auth.credentials.id_token # needed for RP-initiated logout
redirect_to root_path, notice: 'Signed in'
end
# DELETE /logout — RP-initiated logout per OIDC RP-Initiated Logout 1.0
def destroy
id_token = session[:id_token]
reset_session
redirect_to(
'https://uniauth.id/api/oauth/end-session?' +
{ id_token_hint: id_token,
post_logout_redirect_uri: 'https://yourapp.com/' }.to_query,
allow_other_host: true
)
end
# POST /logout/backchannel — receive logout tokens from UniAuth
def backchannel_logout
token = params[:logout_token].to_s
claims = decode_logout_token(token)
return head :bad_request unless claims
# Kill all sessions for this user
sub = claims['sub']
User.find_by(uniauth_sub: sub)&.sessions&.delete_all
head :ok
end
private
def decode_logout_token(token)
jwks = Rails.cache.fetch('uniauth_jwks', expires_in: 1.hour) do
JSON.parse(Net::HTTP.get(URI('https://uniauth.id/.well-known/jwks.json')))
end
payload, _header = JWT.decode(
token, nil, true,
algorithms: ['RS256'],
iss: 'https://uniauth.id', verify_iss: true,
aud: ENV['UNIAUTH_CLIENT_ID'], verify_aud: true,
jwks: { keys: jwks['keys'] },
)
# OIDC Back-Channel Logout 1.0 extra checks
return nil if payload['nonce'] # MUST NOT contain nonce
return nil unless payload.dig('events', 'http://schemas.openid.net/event/backchannel-logout')
payload
rescue JWT::DecodeError
nil
end
end6. Authorization helper
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
helper_method :current_user, :signed_in?
def current_user
@current_user ||= User.find_by(id: session[:user_id])
end
def signed_in?
current_user.present?
end
def require_login
redirect_to root_path, alert: 'Sign in required' unless signed_in?
end
def require_admin
head :forbidden unless current_user&.is_admin?
end
end7. Migration
class AddUniAuthToUsers < ActiveRecord::Migration[7.1]
def change
add_column :users, :uniauth_sub, :string, null: false
add_index :users, :uniauth_sub, unique: true
add_column :users, :display_name, :string
add_column :users, :is_admin, :boolean, default: false, null: false
add_column :users, :role, :string, default: 'user', null: false
end
endRegister https://yourapp.com/logout/backchannel as backchannel_logout_uri in the Developer Console.