Rails 8’s built-in authentication generator doesn’t support SSO itself, so the usual approach is to keep its session handling and add OmniAuth with the Entra ID strategy. After Entra verifies the person, you log them in through the same start_new_session_for that the generator created.
1. Register the app in Entra
In the Azure portal, go to Entra ID → App registrations → New registration:
- Account type: usually “Single tenant” (your organisation only)
- Redirect URI (type “Web”):
https://your-host/auth/entra_id/callback. For local development,http://localhost:3000/auth/entra_id/callbackis allowed. - Under Certificates & secrets, create a client secret and copy its value.
- Note the Application (client) ID and Directory (tenant) ID.
You may need your IT department to do this step if you lack the rights to register apps.
2. Add the gems
gem "omniauth-entra-id"
gem "omniauth-rails_csrf_protection"
Then run bundle install. Check the gem’s README for the current version and options, since the Microsoft strategies have been renamed over the years.
3. Configure OmniAuth
config/initializers/omniauth.rb:
Rails.application.config.middleware.use OmniAuth::Builder do
provider :entra_id,
client_id: Rails.application.credentials.dig(:entra, :client_id),
client_secret: Rails.application.credentials.dig(:entra, :client_secret),
tenant_id: Rails.application.credentials.dig(:entra, :tenant_id)
end
OmniAuth.config.allowed_request_methods = [:post]
Store the values with bin/rails credentials:edit.
Alternative: use ENV.fetch("ENTRA_CLIENT_ID") etc and store the IDs in environment variables.
4. Link users to Entra identities
bin/rails g migration AddEntraToUsers provider:string uid:string
Add an index on [provider, uid]. The generated users table requires a password_digest, so SSO users get a random one.
5. Routes and controller
# config/routes.rb
get "/auth/:provider/callback", to: "omniauth_sessions#create"
get "/auth/failure", to: redirect("/session/new")
# app/controllers/omniauth_sessions_controller.rb
class OmniauthSessionsController < ApplicationController
allow_unauthenticated_access only: :create
def create
auth = request.env["omniauth.auth"]
user = User.find_or_create_by!(provider: auth.provider, uid: auth.uid) do |u|
u.email_address = auth.info.email
u.password = SecureRandom.base58(32)
end
start_new_session_for user
redirect_to after_authentication_url
end
end
If you already have users with password logins, match on email instead, or restrict access to existing users only (User.find_by!(...) instead of find_or_create_by!).
6. Add the login button
It must be a POST, which is what omniauth-rails_csrf_protection enforces:
<%= button_to "Sign in with Microsoft", "/auth/entra_id", data: { turbo: false } %>
Things to watch for
- Turbo:
turbo: falseis needed because the request redirects off-site. - Single tenant: setting
tenant_idrestricts login to your organisation’s accounts. - Callback URL: it must exactly match the redirect URI registered in Entra, including http vs https.
- Email claim:
auth.info.emailcan occasionally be empty depending on the tenant’s configuration. If so, use theupnorpreferred_usernamefromauth.extra.raw_info.
Do you want to keep password login alongside Entra, or make Entra the only way in? That changes how to handle the user model and the sessions controller.
🤖 created with help of ai