discourse/lib/middleware/omniauth_bypass_middleware.rb
David Taylor 6b6b31a97f
FEATURE: Allow admins to opt-in to seamless redirects on /auth/* (#31235)
By default, when multiple login providers are enabled, Discourse
requires user interaction before triggering an external auth flow. This
is defense-in-depth against "Login CSRF" attacks.

This commit introduces a setting to control this behavior, so that it
can be disabled when admins fully trust the downstream systems, and need
an interaction-free login flow on a site with multiple login providers.

Default behavior remains unchanged.
2025-02-07 11:43:39 +00:00

71 lines
2.7 KiB
Ruby

# frozen_string_literal: true
require "csrf_token_verifier"
# omniauth loves spending lots cycles in its magic middleware stack
# this middleware bypasses omniauth middleware and only hits it when needed
class Middleware::OmniauthBypassMiddleware
class AuthenticatorDisabled < StandardError
end
def initialize(app, options = {})
@app = app
OmniAuth.config.before_request_phase do |env|
request = ActionDispatch::Request.new(env)
# Check for CSRF token in POST requests
CSRFTokenVerifier.new.call(env) if request.request_method.downcase.to_sym != :get
# If the user is trying to reconnect to an existing account, store in session
request.session[:auth_reconnect] = !!request.params["reconnect"]
# If the client provided an origin, store in session to redirect back
request.session[:destination_url] = request.params["origin"]
end
end
def call(env)
if env["PATH_INFO"].start_with?("/auth")
begin
# When only one provider is enabled, assume it can be completely trusted, and allow GET requests
only_one_provider =
!SiteSetting.enable_local_logins && Discourse.enabled_authenticators.length == 1
allow_get = only_one_provider || !SiteSetting.auth_require_interaction
OmniAuth.config.allowed_request_methods = allow_get ? %i[get post] : [:post]
omniauth =
OmniAuth::Builder.new(@app) do
Discourse.enabled_authenticators.each do |authenticator|
authenticator.register_middleware(self)
end
end
omniauth.call(env)
rescue AuthenticatorDisabled => e
# Authenticator is disabled, pretend it doesn't exist and pass request to app
@app.call(env)
rescue OAuth::Unauthorized => e
# OAuth1 (i.e. Twitter) makes a web request during the setup phase
# If it fails, Omniauth does not handle the error. Handle it here
env["omniauth.error.type"] ||= "request_error"
Rails.logger.error "Authentication failure! request_error: #{e.class}, #{e.message}"
OmniAuth::FailureEndpoint.call(env)
rescue JWT::InvalidIatError => e
# Happens for openid-connect (including google) providers, when the server clock is wrong
env["omniauth.error.type"] ||= "invalid_iat"
Rails.logger.error "Authentication failure! invalid_iat: #{e.class}, #{e.message}"
OmniAuth::FailureEndpoint.call(env)
rescue CSRFTokenVerifier::InvalidCSRFToken => e
# Happens when CSRF token is missing from request
env["omniauth.error.type"] ||= "csrf_detected"
OmniAuth::FailureEndpoint.call(env)
end
else
@app.call(env)
end
end
end