128 lines
4.2 KiB
Ruby
128 lines
4.2 KiB
Ruby
require 'omniauth-oauth2'
|
|
|
|
module ::OmniAuth
|
|
module Strategies
|
|
class OpenIDConnect < OmniAuth::Strategies::OAuth2
|
|
option :scope, "openid"
|
|
option :discovery, true
|
|
option :cache, lambda { |key, &blk| blk.call } # Default no-op cache
|
|
|
|
option :client_options,
|
|
site: 'https://op.com/',
|
|
authorize_url: 'authorize',
|
|
token_url: 'token',
|
|
userinfo_endpoint: 'userinfo',
|
|
auth_scheme: :basic_auth
|
|
|
|
def discover!
|
|
discovery_document = options.cache.call("openid_discovery_#{options[:client_options][:discovery_document]}") do
|
|
client.request(:get, options[:client_options][:discovery_document], parse: :json).parsed
|
|
end
|
|
options[:client_options][:authorize_url] = discovery_document["authorization_endpoint"].to_s
|
|
options[:client_options][:token_url] = discovery_document["token_endpoint"].to_s
|
|
options[:client_options][:userinfo_endpoint] = discovery_document["userinfo_endpoint"].to_s
|
|
options[:client_options][:site] = discovery_document["issuer"].to_s
|
|
end
|
|
|
|
def request_phase
|
|
discover! if options[:discovery]
|
|
super
|
|
end
|
|
|
|
def authorize_params
|
|
super.tap do |params|
|
|
params[:scope] = options[:scope]
|
|
session['omniauth.nonce'] = params[:nonce] = SecureRandom.hex(32)
|
|
end
|
|
end
|
|
|
|
uid { id_token_info['sub'] }
|
|
|
|
info do
|
|
data_source = options.use_userinfo ? userinfo_response : id_token_info
|
|
prune!(
|
|
name: data_source['name'],
|
|
email: data_source['email'],
|
|
first_name: data_source['given_name'],
|
|
last_name: data_source['family_name'],
|
|
nickname: data_source['preferred_username'],
|
|
picture: data_source['picture']
|
|
)
|
|
end
|
|
|
|
extra do
|
|
hash = {}
|
|
hash[:raw_info] = options.use_userinfo ? userinfo_response : id_token_info
|
|
prune! hash
|
|
end
|
|
|
|
def userinfo_response
|
|
@raw_info ||= access_token.get(options[:client_options][:userinfo_endpoint]).parsed
|
|
return fail!(:csrf_detected, CallbackError.new(:csrf_detected, "CSRF detected")) unless @raw_info['sub'] == id_token_info['sub']
|
|
@raw_info
|
|
end
|
|
|
|
def id_token_info
|
|
# Verify the claims in the JWT
|
|
# The signature does not need to be verified because the
|
|
# token was acquired via a direct server-server connection to the issuer
|
|
@id_token_info ||= JWT.decode(
|
|
access_token['id_token'], nil, false,
|
|
:verify_iss => true,
|
|
'iss' => options[:client_options][:site],
|
|
:verify_aud => true,
|
|
'aud' => options.client_id,
|
|
:verify_sub => false,
|
|
:verify_expiration => true,
|
|
:verify_not_before => true,
|
|
:verify_iat => true,
|
|
:verify_jti => false
|
|
).first
|
|
end
|
|
|
|
def callback_phase
|
|
discover! if options[:discovery]
|
|
oauth2_callback_phase = super
|
|
if id_token_info["nonce"].empty? || id_token_info["nonce"] != session.delete("omniauth.nonce")
|
|
return fail!(:csrf_detected, CallbackError.new(:csrf_detected, "CSRF detected"))
|
|
end
|
|
oauth2_callback_phase
|
|
end
|
|
|
|
private
|
|
|
|
def callback_url
|
|
# return "http://localhost:8080/auth/callback/"
|
|
full_host + script_name + callback_path
|
|
end
|
|
|
|
def get_token_options
|
|
{ redirect_uri: callback_url,
|
|
grant_type: 'authorization_code',
|
|
code: request.params["code"],
|
|
client_id: options[:client_id],
|
|
client_secret: options[:client_secret]
|
|
}.merge(token_params.to_hash(symbolize_keys: true))
|
|
end
|
|
|
|
def prune!(hash)
|
|
hash.delete_if do |_, v|
|
|
prune!(v) if v.is_a?(Hash)
|
|
v.nil? || (v.respond_to?(:empty?) && v.empty?)
|
|
end
|
|
end
|
|
|
|
protected
|
|
|
|
def build_access_token
|
|
return super if options.use_userinfo
|
|
response = client.request(:get, options[:client_options][:token_url], params: get_token_options)
|
|
::OAuth2::AccessToken.from_hash(client, response.parsed)
|
|
end
|
|
|
|
end
|
|
end
|
|
end
|
|
|
|
OmniAuth.config.add_camelization 'openid_connect', 'OpenIDConnect'
|