OmniAuth strategy for authenticating PingOne users.
Mount this with your Rails app to simplify the OAuth flow with PingOne.
This is intended for apps already using OmniAuth, for apps that authenticate against more than one service (eg: PingOne and GitHub).
bundle install --binstubs
docker build . -t sdo_omniauth_ping_one
docker run --rm sdo_omniauth_ping_one
export IMAGE=omniauth-ping-one:localdev
docker build --tag $IMAGE .
docker run \
-v $PWD/spec:/usr/src/app/spec \
-v $PWD/lib:/usr/src/app/lib \
$IMAGE
PingOne supports different OAuth scopes.
Your .env
:
OAUTH_PING_ONE_CLIENT_ID="<set_client_id_here>"
OAUTH_PING_ONE_CLIENT_SECRET="<set_client_secret_here>"
OAUTH_PING_ONE_SITE="https://sso.connect.pingidentity.com"
OAUTH_PING_ONE_REDIRECT_URI="https://your.host.example.com/auth/ping_one/callback"
OAUTH_PING_ONE_ENV_PATH="/sso"
Note that not all Ping instances have a Ping environment set but use hostnames to differentiate Ping environments (not to be confused with Rails environment for example). In this case, omit OAUTH_PING_ONE_ENV_PATH
. Otherwise set the Ping ID environment ID here with a slash prepended, because the Ping environment ID is part of the endpoint URIs.
Under config/initializers/omniauth.rb
:
Rails.application.config.middleware.use OmniAuth::Builder do
provider :ping_one, ENV['OAUTH_PING_ONE_CLIENT_ID'], ENV['OAUTH_PING_ONE_CLIENT_SECRET'], {
:pkce => false,
:client_options => {
site: ENV['OAUTH_PING_ONE_SITE'],
redirect_uri: ENV['OAUTH_PING_ONE_REDIRECT_URI'],
authorize_url: "#{ENV['OAUTH_PING_ONE_ENV_PATH']}/as/authorization.oauth2",
token_url: "#{ENV['OAUTH_PING_ONE_ENV_PATH']}/as/token.oauth2",
user_url: "#{ENV['OAUTH_PING_ONE_ENV_PATH']}/idp/userinfo.openid"
},
:scopes => ["profile", "email", "openid"],
name: :ping_one,
setup: false,
}
end
Note that if you set
:pkce => true
this gem will not provide the OAuth2 secret in access token requests. In all cases I have seen so far it would clash with the PKCE credentials and Ping would reject the request with an error "invalid credentials", even though all credentials are correct.
Then add to config/routes.rb
:
# Named routes for OAuth cycle, b/c OmniAuth does not seem to provide named routes
post "/auth/:provider", to: lambda{ |env| [404, {}, ["Route not Found"]] }, as: :oauth_start
get '/auth/:provider/callback', to: 'sessions#create', as: :oauth_callback
post '/auth/failure', to: 'sessions#auth_failure'
Controller support:
# Note: this is not supposed to be a "copy&paste&forget"-kind-of example.
# It is supposed to give you an idea what you'd probably want to change in your sessions controller.
class SessionsController < ApplicationController
def new
# No need for anything in here, we are just going to render our
# new.html.erb AKA the login page
end
def create
auth_hash ? create_oauth_session : create_regular_session
end
def auth_failure
# On OAuth error (e.g. invalid secret) 'omniauth-rails_csrf_protection' intercepts this call with
# an CSRF error so it's not that easy to access the actual error here.
# Show at least a generic message. The actual error is in the logs.
render(:file => Rails.root.join('public', '401'), :formats => [:html], :status => 401, :layout => 'error')
end
private
def auth_hash
request.env['omniauth.auth']
end
# Look up User in db by the UID and email address provided by OAuth
# Note: If multiple OIDC providers are configured make sure their UIDs to not conflict.
def create_oauth_session
logger.debug "create_oauth_session:" + auth_hash.slice('provider', 'uid', 'info', 'extra').to_s # do not log the token
# Email address is still the main identifier for users, for example when invalidating accounts on DEV/STAGE.
user = User.find_by(uid: auth_hash.uid, email: auth_hash.info.email.downcase)
if user
session[:user_id] = user.id.to_s
# redirect to home
redirect_to dashboard_path, notice: t('forms.login.loggedin', user: user.name)
else
flash.now[:error] = t('forms.login.denied_ping_one')
render :new
end
end
# Look up User in db by the UID and email address provided the user via login form
def create_regular_session
# ...
end
end
The view (note that for security reasons the OAuth login cycle begins with a POST request):
<h1>Your PingOne OAuth cycle start:</h1>
<%= bootstrap_form_with method: :post, url: oauth_start_path('ping_one'), local: true do |f| %>
<%= f.submit t('forms.login.submit_sso.ping_one' ), class: "btn btn-primary btn-block" %>
<% end %>
I'd be happy to implement new features for you at a competitive hourly rate. Get in touch now: https://sdo.sh/#contact
Pull requests are welcome. Please adhere to some very basic and simple principles:
- Follow "separation of concern" on all levels: 1 issue == 1 pull request. Do not cover multiple issues in a pull request.
- Unit tests raise the chance of your pull request getting accepted.