Updates for oidc conformance

This commit is contained in:
Dan Milne
2026-01-03 10:11:10 +11:00
parent 0bb84f08d6
commit 16e34ffaf0
4 changed files with 88 additions and 6 deletions

View File

@@ -1,7 +1,8 @@
class OidcController < ApplicationController
# Discovery and JWKS endpoints are public
allow_unauthenticated_access only: [:discovery, :jwks, :token, :revoke, :userinfo, :logout]
skip_before_action :verify_authenticity_token, only: [:token, :revoke, :userinfo, :logout]
# authorize is also unauthenticated to handle prompt=none and prompt=login specially
allow_unauthenticated_access only: [:discovery, :jwks, :token, :revoke, :userinfo, :logout, :authorize]
skip_before_action :verify_authenticity_token, only: [:token, :revoke, :userinfo, :logout, :authorize, :consent]
# Rate limiting to prevent brute force and abuse
rate_limit to: 60, within: 1.minute, only: [:token, :revoke], with: -> {
@@ -43,7 +44,8 @@ class OidcController < ApplicationController
],
code_challenge_methods_supported: ["plain", "S256"],
backchannel_logout_supported: true,
backchannel_logout_session_supported: true
backchannel_logout_session_supported: true,
request_parameter_supported: false
}
render json: config
@@ -119,6 +121,18 @@ class OidcController < ApplicationController
# per OAuth2 RFC 6749 Section 4.1.2.1
# ============================================================================
# Reject request objects (JWT-encoded authorization parameters)
# Per OIDC Core §3.1.2.6: If request parameter is present and not supported,
# return request_not_supported error
if params[:request].present? || params[:request_uri].present?
Rails.logger.error "OAuth: Request object not supported"
error_uri = "#{redirect_uri}?error=request_not_supported"
error_uri += "&error_description=#{CGI.escape("Request objects are not supported")}"
error_uri += "&state=#{CGI.escape(state)}" if state.present?
redirect_to error_uri, allow_other_host: true
return
end
# Validate response_type (now we can safely redirect with error)
unless response_type == "code"
Rails.logger.error "OAuth: Invalid response_type: #{response_type}"
@@ -162,7 +176,17 @@ class OidcController < ApplicationController
# Check if user is authenticated
unless authenticated?
# Store OAuth parameters in session and redirect to sign in
# Handle prompt=none - no UI allowed, return error immediately
# Per OIDC Core spec §3.1.2.6: If prompt=none and user not authenticated,
# return login_required error without showing any UI
if params[:prompt] == "none"
error_uri = "#{redirect_uri}?error=login_required"
error_uri += "&state=#{CGI.escape(state)}" if state.present?
redirect_to error_uri, allow_other_host: true
return
end
# Normal flow: store OAuth parameters and redirect to sign in
session[:oauth_params] = {
client_id: client_id,
redirect_uri: redirect_uri,
@@ -172,10 +196,54 @@ class OidcController < ApplicationController
code_challenge: code_challenge,
code_challenge_method: code_challenge_method
}
# Store the current URL (with all OAuth params) for redirect after authentication
session[:return_to_after_authenticating] = request.url
redirect_to signin_path, alert: "Please sign in to continue"
return
end
# Handle prompt=login - force re-authentication
# Per OIDC Core spec §3.1.2.1: If prompt=login, the Authorization Server MUST prompt
# the End-User for reauthentication, even if the End-User is currently authenticated
if params[:prompt] == "login"
# Destroy current session to force re-authentication
# This creates a fresh authentication event with a new auth_time
Current.session&.destroy!
# Store the current URL (which contains all OAuth params) for redirect after login
# Remove prompt=login to prevent infinite re-auth loop
return_url = request.url.sub(/&prompt=login(?=&|$)|\?prompt=login&?/, '\1')
# Fix any resulting URL issues (like ?& or & at end)
return_url = return_url.gsub("?&", "?").gsub(/[?&]$/, "")
session[:return_to_after_authenticating] = return_url
redirect_to signin_path, alert: "Please sign in to continue"
return
end
# Handle max_age - require re-authentication if session is too old
# Per OIDC Core spec §3.1.2.1: If max_age is provided and the auth time is older,
# the Authorization Server MUST prompt for reauthentication
if params[:max_age].present?
max_age_seconds = params[:max_age].to_i
# Calculate session age
session_age_seconds = Time.current.to_i - Current.session.created_at.to_i
if session_age_seconds > max_age_seconds
# Session is too old - require re-authentication
# Store return URL in session (creates new session cookie)
# Destroy session and clear cookie to force fresh login
Current.session&.destroy!
cookies.delete(:session_id)
session[:return_to_after_authenticating] = request.url
redirect_to signin_path, alert: "Please sign in to continue"
return
end
end
# Get the authenticated user
user = Current.session.user

View File

@@ -14,6 +14,20 @@ class SessionsController < ApplicationController
return
end
# Extract login_hint from the return URL for pre-filling the email field (OIDC spec)
@login_hint = nil
if session[:return_to_after_authenticating].present?
begin
uri = URI.parse(session[:return_to_after_authenticating])
if uri.query.present?
query_params = CGI.parse(uri.query)
@login_hint = query_params["login_hint"]&.first
end
rescue URI::InvalidURIError
# Ignore parsing errors
end
end
respond_to do |format|
format.html # render HTML login page
format.json { render json: {error: "Authentication required"}, status: :unauthorized }