diff --git a/app/controllers/concerns/authentication.rb b/app/controllers/concerns/authentication.rb index c60d06f..7118f3b 100644 --- a/app/controllers/concerns/authentication.rb +++ b/app/controllers/concerns/authentication.rb @@ -52,12 +52,24 @@ module Authentication # Extract root domain for cross-subdomain cookies (required for forward auth) domain = extract_root_domain(request.host) - cookie_options = { - value: session.id, - httponly: true, - same_site: :lax, - secure: Rails.env.production? - } + # Set cookie options based on environment + # Production: Use SameSite=None to allow cross-site cookies (needed for OIDC conformance testing) + # Development: Use SameSite=Lax since HTTPS might not be available + cookie_options = if Rails.env.production? + { + value: session.id, + httponly: true, + same_site: :none, # Allow cross-site cookies for OIDC testing + secure: true # Required for SameSite=None + } + else + { + value: session.id, + httponly: true, + same_site: :lax, + secure: false + } + end # Set domain for cross-subdomain authentication if we can extract it cookie_options[:domain] = domain if domain.present? diff --git a/app/controllers/oidc_controller.rb b/app/controllers/oidc_controller.rb index 95b8714..86342b5 100644 --- a/app/controllers/oidc_controller.rb +++ b/app/controllers/oidc_controller.rb @@ -210,6 +210,9 @@ class OidcController < ApplicationController # This creates a fresh authentication event with a new auth_time Current.session&.destroy! + # Clear the session cookie so the user is truly logged out + cookies.delete(:session_id) + # 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') @@ -536,6 +539,10 @@ class OidcController < ApplicationController scopes: auth_code.scope ) + # RFC6749-5.1: Token endpoint MUST return Cache-Control: no-store + response.headers["Cache-Control"] = "no-store" + response.headers["Pragma"] = "no-cache" + # Return tokens render json: { access_token: access_token_record.plaintext_token, # Opaque token @@ -665,6 +672,10 @@ class OidcController < ApplicationController scopes: refresh_token_record.scope ) + # RFC6749-5.1: Token endpoint MUST return Cache-Control: no-store + response.headers["Cache-Control"] = "no-store" + response.headers["Pragma"] = "no-cache" + # Return new tokens render json: { access_token: new_access_token.plaintext_token, # Opaque token @@ -763,6 +774,10 @@ class OidcController < ApplicationController application = access_token.application claims.merge!(application.custom_claims_for_user(user)) + # Security: Don't cache user data responses + response.headers["Cache-Control"] = "no-store" + response.headers["Pragma"] = "no-cache" + render json: claims end @@ -907,8 +922,8 @@ class OidcController < ApplicationController } end - # Validate code verifier format (base64url-encoded, 43-128 characters) - unless code_verifier.match?(/\A[A-Za-z0-9\-_]{43,128}\z/) + # Validate code verifier format (per RFC 7636: [A-Za-z0-9\-._~], 43-128 characters) + unless code_verifier.match?(/\A[A-Za-z0-9\.\-_~]{43,128}\z/) return { valid: false, error: "invalid_request", diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index f3fd5ca..44df291 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -87,7 +87,10 @@ class SessionsController < ApplicationController # Sign in successful (password only) start_new_session_for user, acr: "1" - redirect_to after_authentication_url, notice: "Signed in successfully.", allow_other_host: true + + # Use status: :see_other to ensure browser makes a GET request + # This prevents Turbo from converting it to a TURBO_STREAM request + redirect_to after_authentication_url, notice: "Signed in successfully.", allow_other_host: true, status: :see_other end def verify_totp