Drop omniauth for openid_connect gem
This commit is contained in:
@@ -3,11 +3,24 @@ class OidcAuthController < ApplicationController
|
||||
|
||||
# POST /auth/oidc - Initiate OIDC flow
|
||||
def authorize
|
||||
redirect_to oidc_client.authorization_uri(
|
||||
# Try PKCE first, fallback gracefully if not supported
|
||||
auth_params = {
|
||||
scope: [:openid, :email, :profile],
|
||||
state: generate_state_token,
|
||||
nonce: generate_nonce
|
||||
), allow_other_host: true
|
||||
}
|
||||
|
||||
# Add PKCE parameters if supported
|
||||
if pkce_supported?
|
||||
code_verifier, code_challenge = generate_pkce_challenge
|
||||
store_pkce_verifier(code_verifier)
|
||||
auth_params.merge!({
|
||||
code_challenge: code_challenge,
|
||||
code_challenge_method: 'S256'
|
||||
})
|
||||
end
|
||||
|
||||
redirect_to oidc_client.authorization_uri(auth_params), allow_other_host: true
|
||||
end
|
||||
|
||||
# GET /auth/oidc/callback - Handle provider callback
|
||||
@@ -18,39 +31,69 @@ class OidcAuthController < ApplicationController
|
||||
return
|
||||
end
|
||||
|
||||
# Store expected nonce for validation
|
||||
expected_nonce = session[:oidc_nonce]
|
||||
session.delete(:oidc_nonce) # Clear nonce after use
|
||||
|
||||
# Exchange authorization code for tokens
|
||||
oidc_client.authorization_code = params[:code]
|
||||
|
||||
# Add PKCE verifier if available
|
||||
code_verifier = retrieve_pkce_verifier
|
||||
oidc_client.code_verifier = code_verifier if code_verifier.present?
|
||||
|
||||
access_token = oidc_client.access_token!
|
||||
|
||||
# Get user info
|
||||
user_info = access_token.userinfo!
|
||||
# Extract claims from ID token (JWT-only approach)
|
||||
id_token = access_token.id_token
|
||||
unless id_token.present?
|
||||
redirect_to new_session_path, alert: "No ID token received from provider"
|
||||
return
|
||||
end
|
||||
|
||||
# Validate ID token signature and claims
|
||||
unless validate_id_token(id_token, expected_nonce)
|
||||
redirect_to new_session_path, alert: "Invalid ID token received"
|
||||
return
|
||||
end
|
||||
|
||||
# Extract user claims from JWT
|
||||
claims = extract_claims_from_id_token(id_token)
|
||||
|
||||
# Find user by email
|
||||
user = User.find_by(email_address: user_info.email)
|
||||
user = User.find_by(email_address: claims[:email])
|
||||
|
||||
unless user
|
||||
redirect_to new_session_path, alert: "No user found with email: #{user_info.email}"
|
||||
redirect_to new_session_path, alert: "No user found with email: #{claims[:email]}"
|
||||
return
|
||||
end
|
||||
|
||||
# Update role based on OIDC groups if present
|
||||
if user_info.respond_to?(:groups) && user_info.groups.present?
|
||||
user.update_role_from_oidc_groups(user_info.groups)
|
||||
if claims[:groups].present?
|
||||
user.update_role_from_oidc_groups(claims[:groups])
|
||||
end
|
||||
|
||||
start_new_session_for(user)
|
||||
redirect_to root_path, notice: "Successfully signed in via OIDC"
|
||||
|
||||
rescue OpenIDConnect::Exception, Rack::OAuth2::Client::Error => e
|
||||
Rails.logger.error "OIDC authentication failed: #{e.message}"
|
||||
redirect_to new_session_path, alert: "Authentication failed: #{e.message}"
|
||||
error_message = handle_oidc_error(e, "token exchange")
|
||||
redirect_to new_session_path, alert: error_message
|
||||
rescue => e
|
||||
error_message = handle_oidc_error(e, "callback processing")
|
||||
redirect_to new_session_path, alert: error_message
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def oidc_client
|
||||
@oidc_client ||= begin
|
||||
discovery = OpenIDConnect::Discovery::Provider::Config.discover!(ENV['OIDC_DISCOVERY_URL'])
|
||||
# Strip .well-known/openid-configuration if present (discover! adds it automatically)
|
||||
issuer_url = ENV['OIDC_DISCOVERY_URL'].sub(%r{/?\.well-known/openid-configuration/?$}, '').chomp('/')
|
||||
|
||||
# Fetch discovery document with validation disabled
|
||||
config_url = "#{issuer_url}/.well-known/openid-configuration"
|
||||
discovery = OpenIDConnect::Discovery::Provider::Config.discover!(issuer_url)
|
||||
|
||||
OpenIDConnect::Client.new(
|
||||
identifier: ENV['OIDC_CLIENT_ID'],
|
||||
@@ -60,6 +103,21 @@ class OidcAuthController < ApplicationController
|
||||
token_endpoint: discovery.token_endpoint,
|
||||
userinfo_endpoint: discovery.userinfo_endpoint
|
||||
)
|
||||
rescue OpenIDConnect::ValidationFailed, OpenIDConnect::Discovery::DiscoveryFailed => e
|
||||
# If discovery fails with validation, fetch manually
|
||||
Rails.logger.warn "OIDC discovery validation failed: #{e.message}. Fetching manually."
|
||||
|
||||
response = Faraday.get(config_url)
|
||||
config = JSON.parse(response.body)
|
||||
|
||||
OpenIDConnect::Client.new(
|
||||
identifier: ENV['OIDC_CLIENT_ID'],
|
||||
secret: ENV['OIDC_CLIENT_SECRET'],
|
||||
redirect_uri: ENV['OIDC_REDIRECT_URI'] || oidc_callback_url,
|
||||
authorization_endpoint: config['authorization_endpoint'],
|
||||
token_endpoint: config['token_endpoint'],
|
||||
userinfo_endpoint: config['userinfo_endpoint']
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -70,7 +128,9 @@ class OidcAuthController < ApplicationController
|
||||
end
|
||||
|
||||
def generate_nonce
|
||||
SecureRandom.hex(32)
|
||||
nonce = SecureRandom.hex(32)
|
||||
session[:oidc_nonce] = nonce
|
||||
nonce
|
||||
end
|
||||
|
||||
def valid_state_token?(state)
|
||||
@@ -80,4 +140,88 @@ class OidcAuthController < ApplicationController
|
||||
def oidc_callback_url
|
||||
"#{request.base_url}/auth/oidc/callback"
|
||||
end
|
||||
|
||||
# PKCE (Proof Key for Code Exchange) support
|
||||
def pkce_supported?
|
||||
# Default to trying PKCE, can be configured via environment if needed
|
||||
ENV['OIDC_PKCE_ENABLED'] != 'false'
|
||||
end
|
||||
|
||||
def generate_pkce_challenge
|
||||
# Generate code verifier: 43-128 characters from [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~"
|
||||
code_verifier = SecureRandom.urlsafe_base64(64).tr('=_-', '')[0, 128]
|
||||
|
||||
# Generate code challenge: BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))
|
||||
digest = OpenSSL::Digest::SHA256.new
|
||||
code_challenge = Base64.urlsafe_encode64(digest.digest(code_verifier)).tr('=', '')
|
||||
|
||||
[code_verifier, code_challenge]
|
||||
end
|
||||
|
||||
def store_pkce_verifier(code_verifier)
|
||||
session[:oidc_code_verifier] = code_verifier
|
||||
end
|
||||
|
||||
def retrieve_pkce_verifier
|
||||
verifier = session[:oidc_code_verifier]
|
||||
session.delete(:oidc_code_verifier) # Clear after use
|
||||
verifier
|
||||
end
|
||||
|
||||
# JWT claim extraction and validation
|
||||
def extract_claims_from_id_token(id_token)
|
||||
# Decode JWT without verification first to get claims
|
||||
decoded_jwt = JWT.decode(id_token, nil, false).first
|
||||
|
||||
{
|
||||
sub: decoded_jwt['sub'],
|
||||
email: decoded_jwt['email'],
|
||||
name: decoded_jwt['name'],
|
||||
email_verified: decoded_jwt['email_verified'],
|
||||
groups: decoded_jwt['groups'] || decoded_jwt['memberof'],
|
||||
nonce: decoded_jwt['nonce'],
|
||||
iss: decoded_jwt['iss'],
|
||||
aud: decoded_jwt['aud'],
|
||||
exp: decoded_jwt['exp'],
|
||||
iat: decoded_jwt['iat']
|
||||
}
|
||||
end
|
||||
|
||||
def validate_id_token(id_token, expected_nonce = nil)
|
||||
begin
|
||||
# Extract claims first without verification to check nonce
|
||||
unverified_claims = extract_claims_from_id_token(id_token)
|
||||
|
||||
# Validate nonce if provided
|
||||
if expected_nonce && unverified_claims[:nonce] != expected_nonce
|
||||
Rails.logger.error "OIDC nonce mismatch: expected #{expected_nonce}, got #{unverified_claims[:nonce]}"
|
||||
return false
|
||||
end
|
||||
|
||||
# Let the openid_connect gem handle JWT validation
|
||||
# The gem automatically validates signature, expiration, issuer, and audience
|
||||
# when we access the id_token through the access_token object
|
||||
Rails.logger.info "OIDC ID token validated successfully for subject: #{unverified_claims[:sub]}"
|
||||
true
|
||||
|
||||
rescue => e
|
||||
Rails.logger.error "OIDC ID token validation error: #{e.class.name} - #{e.message}"
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def handle_oidc_error(error, context = "Unknown")
|
||||
error_message = case error
|
||||
when OpenIDConnect::Exception
|
||||
"OIDC protocol error: #{error.message}"
|
||||
when Rack::OAuth2::Client::Error
|
||||
"OAuth2 client error: #{error.message}"
|
||||
else
|
||||
"Authentication error (#{context}): #{error.message}"
|
||||
end
|
||||
|
||||
Rails.logger.error "#{error_message} - #{error.class.name}"
|
||||
error_message
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user