StandardRB fixes
Some checks failed
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled

This commit is contained in:
Dan Milne
2026-01-01 13:29:44 +11:00
parent 7d3af2bcec
commit 93a0edb0a2
79 changed files with 779 additions and 786 deletions

View File

@@ -5,7 +5,7 @@ class OidcController < ApplicationController
# Rate limiting to prevent brute force and abuse
rate_limit to: 60, within: 1.minute, only: [:token, :revoke], with: -> {
render json: { error: "too_many_requests", error_description: "Rate limit exceeded. Try again later." }, status: :too_many_requests
render json: {error: "too_many_requests", error_description: "Rate limit exceeded. Try again later."}, status: :too_many_requests
}
rate_limit to: 30, within: 1.minute, only: [:authorize, :consent], with: -> {
render plain: "Too many authorization attempts. Try again later.", status: :too_many_requests
@@ -63,7 +63,7 @@ class OidcController < ApplicationController
error_details << "redirect_uri is required" unless redirect_uri.present?
error_details << "response_type must be 'code'" unless response_type == "code"
render plain: "Invalid request: #{error_details.join(', ')}", status: :bad_request
render plain: "Invalid request: #{error_details.join(", ")}", status: :bad_request
return
end
@@ -90,7 +90,7 @@ class OidcController < ApplicationController
Rails.logger.error "OAuth: Available OIDC applications: #{all_oidc_apps.pluck(:id, :client_id, :name)}"
error_msg = if Rails.env.development?
"Invalid request: Application not found for client_id '#{client_id}'. Available OIDC applications: #{all_oidc_apps.pluck(:name, :client_id).map { |name, id| "#{name} (#{id})" }.join(', ')}"
"Invalid request: Application not found for client_id '#{client_id}'. Available OIDC applications: #{all_oidc_apps.pluck(:name, :client_id).map { |name, id| "#{name} (#{id})" }.join(", ")}"
else
"Invalid request: Application not found"
end
@@ -105,7 +105,7 @@ class OidcController < ApplicationController
# For development, show detailed error
error_msg = if Rails.env.development?
"Invalid request: Redirect URI mismatch. Application is configured for: #{@application.parsed_redirect_uris.join(', ')}, but received: #{redirect_uri}"
"Invalid request: Redirect URI mismatch. Application is configured for: #{@application.parsed_redirect_uris.join(", ")}, but received: #{redirect_uri}"
else
"Invalid request: Redirect URI not registered for this application"
end
@@ -223,22 +223,22 @@ class OidcController < ApplicationController
# User denied consent
if params[:deny].present?
session.delete(:oauth_params)
error_uri = "#{oauth_params['redirect_uri']}?error=access_denied"
error_uri += "&state=#{CGI.escape(oauth_params['state'])}" if oauth_params['state']
error_uri = "#{oauth_params["redirect_uri"]}?error=access_denied"
error_uri += "&state=#{CGI.escape(oauth_params["state"])}" if oauth_params["state"]
redirect_to error_uri, allow_other_host: true
return
end
# Find the application
client_id = oauth_params['client_id']
client_id = oauth_params["client_id"]
application = Application.find_by(client_id: client_id, app_type: "oidc")
# Check if application is active (redirect with OAuth error)
unless application&.active?
Rails.logger.error "OAuth: Application is not active: #{application&.name || client_id}"
session.delete(:oauth_params)
error_uri = "#{oauth_params['redirect_uri']}?error=unauthorized_client&error_description=Application+is+not+active"
error_uri += "&state=#{CGI.escape(oauth_params['state'])}" if oauth_params['state'].present?
error_uri = "#{oauth_params["redirect_uri"]}?error=unauthorized_client&error_description=Application+is+not+active"
error_uri += "&state=#{CGI.escape(oauth_params["state"])}" if oauth_params["state"].present?
redirect_to error_uri, allow_other_host: true
return
end
@@ -246,9 +246,9 @@ class OidcController < ApplicationController
user = Current.session.user
# Record user consent
requested_scopes = oauth_params['scope'].split(' ')
requested_scopes = oauth_params["scope"].split(" ")
consent = OidcUserConsent.find_or_initialize_by(user: user, application: application)
consent.scopes_granted = requested_scopes.join(' ')
consent.scopes_granted = requested_scopes.join(" ")
consent.granted_at = Time.current
consent.save!
@@ -256,11 +256,11 @@ class OidcController < ApplicationController
auth_code = OidcAuthorizationCode.create!(
application: application,
user: user,
redirect_uri: oauth_params['redirect_uri'],
scope: oauth_params['scope'],
nonce: oauth_params['nonce'],
code_challenge: oauth_params['code_challenge'],
code_challenge_method: oauth_params['code_challenge_method'],
redirect_uri: oauth_params["redirect_uri"],
scope: oauth_params["scope"],
nonce: oauth_params["nonce"],
code_challenge: oauth_params["code_challenge"],
code_challenge_method: oauth_params["code_challenge_method"],
auth_time: Current.session.created_at.to_i,
acr: Current.session.acr,
expires_at: 10.minutes.from_now
@@ -270,8 +270,8 @@ class OidcController < ApplicationController
session.delete(:oauth_params)
# Redirect back to client with authorization code (plaintext)
redirect_uri = "#{oauth_params['redirect_uri']}?code=#{auth_code.plaintext_code}"
redirect_uri += "&state=#{CGI.escape(oauth_params['state'])}" if oauth_params['state']
redirect_uri = "#{oauth_params["redirect_uri"]}?code=#{auth_code.plaintext_code}"
redirect_uri += "&state=#{CGI.escape(oauth_params["state"])}" if oauth_params["state"]
redirect_to redirect_uri, allow_other_host: true
end
@@ -286,7 +286,7 @@ class OidcController < ApplicationController
when "refresh_token"
handle_refresh_token_grant
else
render json: { error: "unsupported_grant_type" }, status: :bad_request
render json: {error: "unsupported_grant_type"}, status: :bad_request
end
end
@@ -295,14 +295,14 @@ class OidcController < ApplicationController
client_id, client_secret = extract_client_credentials
unless client_id
render json: { error: "invalid_client", error_description: "client_id is required" }, status: :unauthorized
render json: {error: "invalid_client", error_description: "client_id is required"}, status: :unauthorized
return
end
# Find the application
application = Application.find_by(client_id: client_id)
unless application
render json: { error: "invalid_client", error_description: "Unknown client" }, status: :unauthorized
render json: {error: "invalid_client", error_description: "Unknown client"}, status: :unauthorized
return
end
@@ -313,7 +313,7 @@ class OidcController < ApplicationController
else
# Confidential clients MUST provide valid client_secret
unless client_secret.present? && application.authenticate_client_secret(client_secret)
render json: { error: "invalid_client", error_description: "Invalid client credentials" }, status: :unauthorized
render json: {error: "invalid_client", error_description: "Invalid client credentials"}, status: :unauthorized
return
end
end
@@ -321,7 +321,7 @@ class OidcController < ApplicationController
# Check if application is active
unless application.active?
Rails.logger.error "OAuth: Token request for inactive application: #{application.name}"
render json: { error: "invalid_client", error_description: "Application is not active" }, status: :forbidden
render json: {error: "invalid_client", error_description: "Application is not active"}, status: :forbidden
return
end
@@ -334,7 +334,7 @@ class OidcController < ApplicationController
auth_code = OidcAuthorizationCode.find_by_plaintext(code)
unless auth_code && auth_code.application == application
render json: { error: "invalid_grant" }, status: :bad_request
render json: {error: "invalid_grant"}, status: :bad_request
return
end
@@ -365,13 +365,13 @@ class OidcController < ApplicationController
# Check if code is expired
if auth_code.expires_at < Time.current
render json: { error: "invalid_grant", error_description: "Authorization code expired" }, status: :bad_request
render json: {error: "invalid_grant", error_description: "Authorization code expired"}, status: :bad_request
return
end
# Validate redirect URI matches
unless auth_code.redirect_uri == redirect_uri
render json: { error: "invalid_grant", error_description: "Redirect URI mismatch" }, status: :bad_request
render json: {error: "invalid_grant", error_description: "Redirect URI mismatch"}, status: :bad_request
return
end
@@ -413,7 +413,7 @@ class OidcController < ApplicationController
unless consent
Rails.logger.error "OIDC Security: Token requested without consent record (user: #{user.id}, app: #{application.id})"
render json: { error: "invalid_grant", error_description: "Authorization consent not found" }, status: :bad_request
render json: {error: "invalid_grant", error_description: "Authorization consent not found"}, status: :bad_request
return
end
@@ -440,7 +440,7 @@ class OidcController < ApplicationController
}
end
rescue ActiveRecord::RecordNotFound
render json: { error: "invalid_grant" }, status: :bad_request
render json: {error: "invalid_grant"}, status: :bad_request
end
end
@@ -449,14 +449,14 @@ class OidcController < ApplicationController
client_id, client_secret = extract_client_credentials
unless client_id
render json: { error: "invalid_client", error_description: "client_id is required" }, status: :unauthorized
render json: {error: "invalid_client", error_description: "client_id is required"}, status: :unauthorized
return
end
# Find the application
application = Application.find_by(client_id: client_id)
unless application
render json: { error: "invalid_client", error_description: "Unknown client" }, status: :unauthorized
render json: {error: "invalid_client", error_description: "Unknown client"}, status: :unauthorized
return
end
@@ -467,7 +467,7 @@ class OidcController < ApplicationController
else
# Confidential clients MUST provide valid client_secret
unless client_secret.present? && application.authenticate_client_secret(client_secret)
render json: { error: "invalid_client", error_description: "Invalid client credentials" }, status: :unauthorized
render json: {error: "invalid_client", error_description: "Invalid client credentials"}, status: :unauthorized
return
end
end
@@ -475,14 +475,14 @@ class OidcController < ApplicationController
# Check if application is active
unless application.active?
Rails.logger.error "OAuth: Refresh token request for inactive application: #{application.name}"
render json: { error: "invalid_client", error_description: "Application is not active" }, status: :forbidden
render json: {error: "invalid_client", error_description: "Application is not active"}, status: :forbidden
return
end
# Get the refresh token
refresh_token = params[:refresh_token]
unless refresh_token.present?
render json: { error: "invalid_request", error_description: "refresh_token is required" }, status: :bad_request
render json: {error: "invalid_request", error_description: "refresh_token is required"}, status: :bad_request
return
end
@@ -491,13 +491,13 @@ class OidcController < ApplicationController
# Verify the token belongs to the correct application
unless refresh_token_record && refresh_token_record.application == application
render json: { error: "invalid_grant", error_description: "Invalid refresh token" }, status: :bad_request
render json: {error: "invalid_grant", error_description: "Invalid refresh token"}, status: :bad_request
return
end
# Check if refresh token is expired
if refresh_token_record.expired?
render json: { error: "invalid_grant", error_description: "Refresh token expired" }, status: :bad_request
render json: {error: "invalid_grant", error_description: "Refresh token expired"}, status: :bad_request
return
end
@@ -508,7 +508,7 @@ class OidcController < ApplicationController
Rails.logger.warn "OAuth Security: Revoked refresh token reuse detected for token family #{refresh_token_record.token_family_id}"
refresh_token_record.revoke_family!
render json: { error: "invalid_grant", error_description: "Refresh token has been revoked" }, status: :bad_request
render json: {error: "invalid_grant", error_description: "Refresh token has been revoked"}, status: :bad_request
return
end
@@ -541,7 +541,7 @@ class OidcController < ApplicationController
unless consent
Rails.logger.error "OIDC Security: Refresh token used without consent record (user: #{user.id}, app: #{application.id})"
render json: { error: "invalid_grant", error_description: "Authorization consent not found" }, status: :bad_request
render json: {error: "invalid_grant", error_description: "Authorization consent not found"}, status: :bad_request
return
end
@@ -566,7 +566,7 @@ class OidcController < ApplicationController
scope: refresh_token_record.scope
}
rescue ActiveRecord::RecordNotFound
render json: { error: "invalid_grant" }, status: :bad_request
render json: {error: "invalid_grant"}, status: :bad_request
end
# GET /oauth/userinfo
@@ -650,7 +650,7 @@ class OidcController < ApplicationController
# Find and validate the application
application = Application.find_by(client_id: client_id)
unless application && application.authenticate_client_secret(client_secret)
unless application&.authenticate_client_secret(client_secret)
Rails.logger.warn "OAuth: Token revocation attempted for invalid application: #{client_id}"
head :ok
return
@@ -669,7 +669,7 @@ class OidcController < ApplicationController
unless token.present?
# RFC 7009: Missing token parameter is an error
render json: { error: "invalid_request", error_description: "token parameter is required" }, status: :bad_request
render json: {error: "invalid_request", error_description: "token parameter is required"}, status: :bad_request
return
end
@@ -695,7 +695,7 @@ class OidcController < ApplicationController
if access_token_record
access_token_record.revoke!
Rails.logger.info "OAuth: Access token revoked for application #{application.name}"
revoked = true
true
end
end
@@ -709,7 +709,7 @@ class OidcController < ApplicationController
# OpenID Connect RP-Initiated Logout
# Handle id_token_hint and post_logout_redirect_uri parameters
id_token_hint = params[:id_token_hint]
params[:id_token_hint]
post_logout_redirect_uri = params[:post_logout_redirect_uri]
state = params[:state]
@@ -763,7 +763,7 @@ class OidcController < ApplicationController
end
# Skip validation if no code challenge was stored (legacy clients without PKCE requirement)
return { valid: true } unless pkce_provided
return {valid: true} unless pkce_provided
# PKCE was provided during authorization but no verifier sent with token request
unless code_verifier.present?
@@ -787,18 +787,18 @@ class OidcController < ApplicationController
# Recreate code challenge based on method
expected_challenge = case auth_code.code_challenge_method
when "plain"
code_verifier
when "S256"
Base64.urlsafe_encode64(Digest::SHA256.digest(code_verifier), padding: false)
else
return {
valid: false,
error: "server_error",
error_description: "Unsupported code challenge method",
status: :internal_server_error
}
end
when "plain"
code_verifier
when "S256"
Base64.urlsafe_encode64(Digest::SHA256.digest(code_verifier), padding: false)
else
return {
valid: false,
error: "server_error",
error_description: "Unsupported code challenge method",
status: :internal_server_error
}
end
# Validate the code challenge
unless auth_code.code_challenge == expected_challenge
@@ -810,7 +810,7 @@ class OidcController < ApplicationController
}
end
{ valid: true }
{valid: true}
end
def extract_client_credentials
@@ -835,7 +835,7 @@ class OidcController < ApplicationController
return nil unless parsed_uri.is_a?(URI::HTTP) || parsed_uri.is_a?(URI::HTTPS)
# Only allow HTTPS in production
return nil if Rails.env.production? && parsed_uri.scheme != 'https'
return nil if Rails.env.production? && parsed_uri.scheme != "https"
# Check if URI matches any registered OIDC application's redirect URIs
# According to OIDC spec, post_logout_redirect_uri should be pre-registered