Updates for oidc conformance
This commit is contained in:
@@ -1,7 +1,8 @@
|
|||||||
class OidcController < ApplicationController
|
class OidcController < ApplicationController
|
||||||
# Discovery and JWKS endpoints are public
|
# Discovery and JWKS endpoints are public
|
||||||
allow_unauthenticated_access only: [:discovery, :jwks, :token, :revoke, :userinfo, :logout]
|
# authorize is also unauthenticated to handle prompt=none and prompt=login specially
|
||||||
skip_before_action :verify_authenticity_token, only: [:token, :revoke, :userinfo, :logout]
|
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 limiting to prevent brute force and abuse
|
||||||
rate_limit to: 60, within: 1.minute, only: [:token, :revoke], with: -> {
|
rate_limit to: 60, within: 1.minute, only: [:token, :revoke], with: -> {
|
||||||
@@ -43,7 +44,8 @@ class OidcController < ApplicationController
|
|||||||
],
|
],
|
||||||
code_challenge_methods_supported: ["plain", "S256"],
|
code_challenge_methods_supported: ["plain", "S256"],
|
||||||
backchannel_logout_supported: true,
|
backchannel_logout_supported: true,
|
||||||
backchannel_logout_session_supported: true
|
backchannel_logout_session_supported: true,
|
||||||
|
request_parameter_supported: false
|
||||||
}
|
}
|
||||||
|
|
||||||
render json: config
|
render json: config
|
||||||
@@ -119,6 +121,18 @@ class OidcController < ApplicationController
|
|||||||
# per OAuth2 RFC 6749 Section 4.1.2.1
|
# 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)
|
# Validate response_type (now we can safely redirect with error)
|
||||||
unless response_type == "code"
|
unless response_type == "code"
|
||||||
Rails.logger.error "OAuth: Invalid response_type: #{response_type}"
|
Rails.logger.error "OAuth: Invalid response_type: #{response_type}"
|
||||||
@@ -162,7 +176,17 @@ class OidcController < ApplicationController
|
|||||||
|
|
||||||
# Check if user is authenticated
|
# Check if user is authenticated
|
||||||
unless 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] = {
|
session[:oauth_params] = {
|
||||||
client_id: client_id,
|
client_id: client_id,
|
||||||
redirect_uri: redirect_uri,
|
redirect_uri: redirect_uri,
|
||||||
@@ -172,10 +196,54 @@ class OidcController < ApplicationController
|
|||||||
code_challenge: code_challenge,
|
code_challenge: code_challenge,
|
||||||
code_challenge_method: code_challenge_method
|
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"
|
redirect_to signin_path, alert: "Please sign in to continue"
|
||||||
return
|
return
|
||||||
end
|
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
|
# Get the authenticated user
|
||||||
user = Current.session.user
|
user = Current.session.user
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,20 @@ class SessionsController < ApplicationController
|
|||||||
return
|
return
|
||||||
end
|
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|
|
respond_to do |format|
|
||||||
format.html # render HTML login page
|
format.html # render HTML login page
|
||||||
format.json { render json: {error: "Authentication required"}, status: :unauthorized }
|
format.json { render json: {error: "Authentication required"}, status: :unauthorized }
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
autofocus: true,
|
autofocus: true,
|
||||||
autocomplete: "username",
|
autocomplete: "username",
|
||||||
placeholder: "your@email.com",
|
placeholder: "your@email.com",
|
||||||
value: params[:email_address],
|
value: @login_hint || params[:email_address],
|
||||||
data: { action: "blur->webauthn#checkWebAuthnSupport change->webauthn#checkWebAuthnSupport" },
|
data: { action: "blur->webauthn#checkWebAuthnSupport change->webauthn#checkWebAuthnSupport" },
|
||||||
class: "block shadow-sm rounded-md border border-gray-400 focus:outline-blue-600 px-3 py-2 mt-2 w-full" %>
|
class: "block shadow-sm rounded-md border border-gray-400 focus:outline-blue-600 px-3 py-2 mt-2 w-full" %>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ Rails.application.routes.draw do
|
|||||||
# OIDC (OpenID Connect) routes
|
# OIDC (OpenID Connect) routes
|
||||||
get "/.well-known/openid-configuration", to: "oidc#discovery"
|
get "/.well-known/openid-configuration", to: "oidc#discovery"
|
||||||
get "/.well-known/jwks.json", to: "oidc#jwks"
|
get "/.well-known/jwks.json", to: "oidc#jwks"
|
||||||
get "/oauth/authorize", to: "oidc#authorize"
|
match "/oauth/authorize", to: "oidc#authorize", via: [:get, :post]
|
||||||
post "/oauth/authorize/consent", to: "oidc#consent", as: :oauth_consent
|
post "/oauth/authorize/consent", to: "oidc#consent", as: :oauth_consent
|
||||||
post "/oauth/token", to: "oidc#token"
|
post "/oauth/token", to: "oidc#token"
|
||||||
post "/oauth/revoke", to: "oidc#revoke"
|
post "/oauth/revoke", to: "oidc#revoke"
|
||||||
|
|||||||
Reference in New Issue
Block a user