msuliq / omniauth_oidc

Omniauth strategy to authenticate and retrieve user data using OpenID Connect (OIDC)
MIT License
5 stars 1 forks source link

OmniAuth::Oidc

This gem provides an OmniAuth strategy for integrating OpenID Connect (OIDC) authentication into your Ruby on Rails application. It allows seamless login using various OIDC providers.

Developed with reference to omniauth-openid-connect and omniauth_openid_connect.

Article on Medium about the development of this gem.

Installation

To install the gem run the following command in the terminal:

$ bundle add omniauth_oidc

If bundler is not being used to manage dependencies, install the gem by executing:

$ gem install omniauth_oidc

Usage

To use the OmniAuth OIDC strategy, you need to configure your Rails application and set up the necessary environment variables for OIDC client credentials.

Configuration

You have to provide Client ID, Client Secret and url for the OIDC configuration endpoint as a bare minimum for the omniauth_oidc to work properly. Create an initializer file at config/initializers/omniauth.rb

# config/initializers/omniauth.rb
Rails.application.config.middleware.use OmniAuth::Builder do
  provider :oidc, {
    name: :simple_provider, # used for dynamic routing
    client_options: {
      identifier: '23575f4602bebbd9a17dbc38d85bd1a77',
      secret: ENV['SIMPLE_PROVIDER_CLIENT_SECRET'],
      config_endpoint: 'https://simpleprovider.com/cdn-cgi/access/sso/oidc/23575f4602bebbd9a17dbc38d85bd1a77/.well-known/openid-configuration'
    }
  }
end

With Devise

Devise.setup do |config|
  config.omniauth :oidc, {
    name: :simple_provider,
    scope: [:openid, :email, :profile, :address],
    response_type: :code,
    uid_field: "preferred_username",
    client_options: {
      identifier: '23575f4602bebbd9a17dbc38d85bd1a77',
      secret: ENV['SIMPLE_PROVIDER_CLIENT_SECRET'],
      config_endpoint: 'https://simpleprovider.com/cdn-cgi/access/sso/oidc/23575f4602bebbd9a17dbc38d85bd1a77/.well-known/openid-configuration'
    }
  }
end

The gem also supports a wide range of optional parameters for higher degree of configurability.

# config/initializers/omniauth.rb
Rails.application.config.middleware.use OmniAuth::Builder do
  provider :oidc, {
    name: :complex_provider, # used for dynamic routing
    issuer: 'https://complexprovider.com/cdn-cgi/access/sso/oidc/23575f4602bebbd9a17dbc38d85bd1a77',
    scope: [:openid],
    response_type: 'id_token',
    require_state: true,
    response_mode: :query,
    prompt: :login,
    send_nonce: false,
    uid_field: "sub",
    pkce: false,
    client_options: {
      identifier: '23575f4602bebbd9a17dbc38d85bd1a77',
      secret: ENV['COMPLEX_PROVIDER_CLIENT_SECRET'],
      config_endpoint: 'https://complexprovider.com/cdn-cgi/access/sso/oidc/23575f4602bebbd9a17dbc38d85bd1a77/.well-known/openid-configuration',
      host: 'complexprovider.com'
      scheme: "https",
      port: 443,
      authorization_endpoint: 'https://complexprovider.com/cdn-cgi/access/sso/oidc/23575f4602bebbd9a17dbc38d85bd1a77/authorization',
      token_endpoint: 'https://complexprovider.com/cdn-cgi/access/sso/oidc/23575f4602bebbd9a17dbc38d85bd1a77/token',
      userinfo_endpoint: 'https://complexprovider.com/cdn-cgi/access/sso/oidc/23575f4602bebbd9a17dbc38d85bd1a77/userinfo',
      jwks_uri: 'https://complexprovider.com/cdn-cgi/access/sso/oidc/23575f4602bebbd9a17dbc38d85bd1a77/jwks',
      end_session_endpoint: '/signout'
    }
  }
end

Ensure to replace identifier, secret, configuration endpoint url and others with credentials received from your OIDC provider. Please note that the gem does not accept redirect_uri as a configurable option. For details please see section Routes.

Redirecting for Authentication

Buttons and links to initialize the authentication request can be placed on relevant pages as below:

<%= button_to "Login with Simple Provider", "/auth/simple_provider" %>

Handling Callbacks

The gem uses dyanmic routes to handle different phases, and while you can use same routes in your Rails application, for better experience you should have a controller to process the authenticated user. Create a CallbacksController:

# app/controllers/callbacks_controller.rb
class CallbacksController < ApplicationController
  def omniauth
    # user info received from OIDC provider will be available in `request.env['omniauth.auth']`
    auth = request.env['omniauth.auth']

    user = User.find_or_create_by(uid: auth['uid']) do |user|
      user.name = auth['info']['name']
      user.email = auth['info']['email']
    end

    session[:user_id] = user.id
    redirect_to root_path, notice: 'Successfully logged in!'
  end
end

Routes

The gem uses dynamic routes when making requests to the OIDC provider endpoints, so called redirect_uri which is a non-configurable value that follows the naming pattern of https://your_app.com/auth/<simple_provider>/callback, where <simple_provider> is the provider name defined within the configuration of the omniauth.rb initializer. This represents the redirect_uri that will be passed with the authorization request to your OIDC provider and that has to be registered with your OIDC provider as permitted redirect_uri.

Dynamic routes are used to process responses and perform intermediary steps by the middleware, e.g. request phase, token verification. While you can define and use same routes within your Rails app, it is highly recommended to modify your routes.rb to perform a dynamic redirect to a another controller method so this does not cause any conflicts with the middleware or the authorization flow.

In an example below, auth/:provider/callback is generalized redirect_uri value that is passed in the authorization flow, while all OIDC provider responses are ultimately redirected to the omniauth method of the callbacks_controller, which could be a "Swiss army knife" method to handle authentication or user data from various omniauth providers:

# config/routes.rb
Rails.application.routes.draw do
  match 'auth/:provider/callback', via: :get, to: "callbacks#omniauth"
end

Alternatively, you can specify separate redirects for some of your OIDC providers, in case you need to handle responses differently:

# config/routes.rb
Rails.application.routes.draw do
  match 'auth/simple_provider/callback', via: :get, to: "callbacks#simple_provider"
  match 'auth/complex_provider/callback', via: :get, to: "callbacks#complex_provider"

  # you can add the line below if you would like the rest of the providers to be redirected to a universal `omniauth` method
  match 'auth/:provider/callback', via: :get, to: "callbacks#omniauth"
end

Please note that you should register https://your_app.com/auth/<simple_provider>/callback with your OIDC provider as a callback redirect url.

Using Access Token Without User Info

In case your app requries only an access token and not the user information, then you can specify an optional configuration in the omniauth initializer:

# config/initializers/omniauth.rb
Rails.application.config.middleware.use OmniAuth::Builder do
  provider :oidc, {
    name: :simple_provider_access_token_only,
    fetch_user_info: false, # if not specified, default value of true will be applied
    client_options: {
      identifier: '23575f4602bebbd9a17dbc38d85bd1a77',
      secret: ENV['SIMPLE_PROVIDER_CLIENT_SECRET'],
      config_endpoint: 'https://simpleprovider.com/cdn-cgi/access/sso/oidc/23575f4602bebbd9a17dbc38d85bd1a77/.well-known/openid-configuration'
    }
  }
end

Then the callback returned once your user authenticates with the OIDC provider will contain only access token parameters:

# app/controllers/callbacks_controller.rb
class CallbacksController < ApplicationController
  def omniauth
    # access token parameters received from OIDC provider will be available in `request.env['omniauth.auth']`
    omniauth_params = request.env['omniauth.auth']

    # omniauth_params will contain similar data as shown below
    # {"provider"=>:simple_provider_access_token_only,
    #  "credentials"=>
    #   {"id_token"=> "id token value",
    #    "token"=> "token value",
    #    "refresh_token"=>"refresh token value",
    #    "expires_in"=>300,
    #    "scope"=>nil
    #   }
    # }
  end
end

Ending Session

The gem provides two configuration options to allow ending a session simultaneously with your client application and the OIDC provider.

To use this feature, you need to provide a logout_path in the options and an end_session_endpoint in the client options. Here’s a sample setup:

  provider :oidc, {
    name: :simple_provider,
    client_options: {
      identifier: ENV['SIMPLE_PROVIDER_CLIENT_ID'],
      secret: ENV['SIMPLE_PROVIDER_SECRET'],
      config_endpoint: 'https://simpleprovider.com/1234567890/.well-known/openid-configuration',
      end_session_endpoint: 'https://simpleprovider.com/signout' # URL to end session with OIDC provider
    },
    logout_path: '/logout' # path in your application to end user session
  }

Using these two configurations, you can ensure that when a user logs out from your application, they are also logged out from the OIDC provider, providing a seamless logout across multiple services.

This works by calling other_phase on every controller request in your application. The method checks if the requested URL matches the defined logout_path. If it does (i.e. current user has requested to log out from your application) other_phase performs a redirect to the end_session_endpoint to terminate the user's session with the OIDC provider and then it returns back to your application and concludes the request to end the current user's session.

For additional details please refer to the OIDC specification.

Advanced Configuration

You can customize the OIDC strategy further by adding additional configuration options:

Field Description Required Default Value Example/Notes
name Arbitrary string to identify OIDC provider and segregate it from other OIDC providers no "oidc" :simple_provider
issuer Root url for the OIDC authorization server no retrived from config_endpoint "https://simpleprovider.com"
fetch_user_info Fetches user information from user_info_endpoint using the access token. If set to false the omniauth params will include only access token no true fetch_user_info: false
client_auth_method Authentication method to be used with the OIDC authorization server no :basic "basic", "jwks"
scope OIDC scopes to be included in the server's response [:openid] is required all scopes offered by OIDC provider [:openid, :profile, :email]
response_type OAuth2 response type expected from OIDC provider during authorization no "code" "code" or "id_token"
state Value to be used for the OAuth2 state parameter on the authorization request. Can be a proc that generates a string no Random 16 character string Proc.new { SecureRandom.hex(32) }
require_state Boolean to indicate if state param should be verified. This is a recommendation by OIDC spec no true true or false
response_mode The response mode per OIDC spec no nil :query, :fragment, :form_post or :web_message
display Specifies how OIDC authorization server should display the authentication and consent UI pages to the end user no nil :page, :popup, :touch or :wap
prompt Specifies whether the OIDC authorization server prompts the end user for reauthentication and consent no nil :none, :login, :consent or :select_account
send_scope_to_token_endpoint Should the scope parameter be sent to the authorization token endpoint no true true or false
post_logout_redirect_uri Logout redirect uri to use per the session management draft no nil "https://your_app.com/logout/callback"
uid_field Field of the user info response to be used as a unique ID no 'sub' "sub" or "preferred_username"
extra_authorize_params Hash of extra fixed parameters that will be merged to the authorization request no {} {"tenant" => "common"}
allow_authorize_params List of allowed dynamic parameters that will be merged to the authorization request no [] [:screen_name]
pkce Enable PKCE flow no false true or false
pkce_verifier Specify custom PKCE verifier code no Random 128-character string Proc.new { SecureRandom.hex(64) }
pkce_options Specify custom implementation of the PKCE code challenge/method no SHA256(code_challenge) in hex Proc to customise the code challenge generation
client_options Hash of client options detailed below in a separate table yes see below see below
jwt_secret_base64 Specify the base64-encoded secret used to sign the JWT token for HMAC with SHA2 (e.g. HS256) signing algorithms no client_options.secret "bXlzZWNyZXQ=\n"
logout_path Log out is only triggered when the request path ends on this path no '/logout' '/sign_out'
acr_values Authentication Class Reference (ACR) values to be passed to the authorize_uri to enforce a specific level, see RFC9470 no nil "c1 c2" å

Below are options for the client_options hash of the configuration:

Field Description Required Default value
identifier OAuth2 client_id yes nil
secret OAuth2 client secret yes nil
config_endpoint OIDC configuration endpoint yes nil
scheme http scheme to use no https
host host of the authorization server no nil
port port for the authorization server no 443
authorization_endpoint authorize endpoint on the authorization server no retrived from config_endpoint
token_endpoint token endpoint on the authorization server no retrived from config_endpoint
userinfo_endpoint user info endpoint on the authorization server no retrived from config_endpoint
jwks_uri jwks_uri on the authorization server no retrived from config_endpoint
end_session_endpoint url to call to log the user out at the authorization server no nil

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/msuliq/omniauth_oidc. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.

License

The gem is available as open source under the terms of the MIT License.

Code of Conduct

Everyone interacting in the OmniauthOidc project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.