From 33ad9565085f9701339ab89929863027ebba0fcf Mon Sep 17 00:00:00 2001 From: Dan Milne Date: Wed, 12 Nov 2025 15:50:04 +1100 Subject: [PATCH] Add test --- test/controllers/oidc_pkce_controller_test.rb | 297 ++++++++++++++++++ 1 file changed, 297 insertions(+) create mode 100644 test/controllers/oidc_pkce_controller_test.rb diff --git a/test/controllers/oidc_pkce_controller_test.rb b/test/controllers/oidc_pkce_controller_test.rb new file mode 100644 index 0000000..b9ebaa4 --- /dev/null +++ b/test/controllers/oidc_pkce_controller_test.rb @@ -0,0 +1,297 @@ +require "test_helper" + +class OidcPkceControllerTest < ActionDispatch::IntegrationTest + def setup + @user = User.create!(email_address: "pkce_test@example.com", password: "password123") + @application = Application.create!( + name: "PKCE Test App", + slug: "pkce-test-app", + app_type: "oidc", + redirect_uris: ["http://localhost:4000/callback"].to_json, + active: true + ) + + # Sign in the user by creating a session directly + @session = Session.create!(user: @user) + cookies[:session_id] = @session.id + end + + def teardown + OidcAuthorizationCode.where(application: @application).destroy_all + @user.destroy + @application.destroy + end + + test "discovery endpoint includes PKCE support" do + get "/.well-known/openid-configuration" + + assert_response :success + config = JSON.parse(@response.body) + + assert config.key?("code_challenge_methods_supported") + assert_includes config["code_challenge_methods_supported"], "S256" + assert_includes config["code_challenge_methods_supported"], "plain" + end + + test "authorization endpoint accepts PKCE parameters (S256)" do + code_verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" + code_challenge = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM" + + auth_params = { + response_type: "code", + client_id: @application.client_id, + redirect_uri: "http://localhost:4000/callback", + scope: "openid profile", + state: "test_state", + nonce: "test_nonce", + code_challenge: code_challenge, + code_challenge_method: "S256" + } + + get "/oauth/authorize", params: auth_params + + # Should redirect to consent page (user is already authenticated) + assert_response :success + assert_template "consent" + end + + test "authorization endpoint accepts PKCE parameters (plain)" do + code_challenge = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM" + + auth_params = { + response_type: "code", + client_id: @application.client_id, + redirect_uri: "http://localhost:4000/callback", + scope: "openid profile", + state: "test_state", + nonce: "test_nonce", + code_challenge: code_challenge, + code_challenge_method: "plain" + } + + get "/oauth/authorize", params: auth_params + + # Should redirect to consent page (user is already authenticated) + assert_response :success + assert_template "consent" + end + + test "authorization endpoint rejects invalid code_challenge_method" do + auth_params = { + response_type: "code", + client_id: @application.client_id, + redirect_uri: "http://localhost:4000/callback", + scope: "openid profile", + code_challenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM", + code_challenge_method: "invalid_method" + } + + get "/oauth/authorize", params: auth_params + + assert_response :bad_request + assert_match(/Invalid code_challenge_method/, @response.body) + end + + test "authorization endpoint rejects invalid code_challenge format" do + # Contains + character which is not base64url + auth_params = { + response_type: "code", + client_id: @application.client_id, + redirect_uri: "http://localhost:4000/callback", + scope: "openid profile", + code_challenge: "invalid+challenge", + code_challenge_method: "S256" + } + + get "/oauth/authorize", params: auth_params + + assert_response :bad_request + assert_match(/Invalid code_challenge format/, @response.body) + end + + test "token endpoint requires code_verifier when PKCE was used (S256)" do + # Create authorization code with PKCE S256 + auth_code = OidcAuthorizationCode.create!( + application: @application, + user: @user, + code: SecureRandom.urlsafe_base64(32), + redirect_uri: "http://localhost:4000/callback", + scope: "openid profile", + code_challenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM", + code_challenge_method: "S256", + expires_at: 10.minutes.from_now + ) + + token_params = { + grant_type: "authorization_code", + code: auth_code.code, + redirect_uri: "http://localhost:4000/callback" + } + + post "/oauth/token", params: token_params, headers: { + "Authorization" => "Basic " + Base64.strict_encode64("#{@application.client_id}:#{@application.client_secret}") + } + + assert_response :bad_request + error = JSON.parse(@response.body) + assert_equal "invalid_request", error["error"] + assert_match(/code_verifier is required/, error["error_description"]) + end + + test "token endpoint requires code_verifier when PKCE was used (plain)" do + # Create authorization code with PKCE plain + auth_code = OidcAuthorizationCode.create!( + application: @application, + user: @user, + code: SecureRandom.urlsafe_base64(32), + redirect_uri: "http://localhost:4000/callback", + scope: "openid profile", + code_challenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM", + code_challenge_method: "plain", + expires_at: 10.minutes.from_now + ) + + token_params = { + grant_type: "authorization_code", + code: auth_code.code, + redirect_uri: "http://localhost:4000/callback" + } + + post "/oauth/token", params: token_params, headers: { + "Authorization" => "Basic " + Base64.strict_encode64("#{@application.client_id}:#{@application.client_secret}") + } + + assert_response :bad_request + error = JSON.parse(@response.body) + assert_equal "invalid_request", error["error"] + assert_match(/code_verifier is required/, error["error_description"]) + end + + test "token endpoint rejects invalid code_verifier (S256)" do + # Create authorization code with PKCE S256 + auth_code = OidcAuthorizationCode.create!( + application: @application, + user: @user, + code: SecureRandom.urlsafe_base64(32), + redirect_uri: "http://localhost:4000/callback", + scope: "openid profile", + code_challenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM", + code_challenge_method: "S256", + expires_at: 10.minutes.from_now + ) + + token_params = { + grant_type: "authorization_code", + code: auth_code.code, + redirect_uri: "http://localhost:4000/callback", + code_verifier: "invalid_verifier" + } + + post "/oauth/token", params: token_params, headers: { + "Authorization" => "Basic " + Base64.strict_encode64("#{@application.client_id}:#{@application.client_secret}") + } + + assert_response :bad_request + error = JSON.parse(@response.body) + assert_equal "invalid_grant", error["error"] + assert_match(/Invalid code verifier/, error["error_description"]) + end + + test "token endpoint accepts valid code_verifier (S256)" do + # Generate valid PKCE pair + code_verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" + code_challenge = Digest::SHA256.base64digest(code_verifier) + .tr("+/", "-_") + .tr("=", "") + + # Create authorization code with PKCE S256 + auth_code = OidcAuthorizationCode.create!( + application: @application, + user: @user, + code: SecureRandom.urlsafe_base64(32), + redirect_uri: "http://localhost:4000/callback", + scope: "openid profile", + code_challenge: code_challenge, + code_challenge_method: "S256", + expires_at: 10.minutes.from_now + ) + + token_params = { + grant_type: "authorization_code", + code: auth_code.code, + redirect_uri: "http://localhost:4000/callback", + code_verifier: code_verifier + } + + post "/oauth/token", params: token_params, headers: { + "Authorization" => "Basic " + Base64.strict_encode64("#{@application.client_id}:#{@application.client_secret}") + } + + assert_response :success + tokens = JSON.parse(@response.body) + assert tokens.key?("access_token") + assert tokens.key?("id_token") + assert_equal "Bearer", tokens["token_type"] + end + + test "token endpoint accepts valid code_verifier (plain)" do + code_verifier = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM" + + # Create authorization code with PKCE plain + auth_code = OidcAuthorizationCode.create!( + application: @application, + user: @user, + code: SecureRandom.urlsafe_base64(32), + redirect_uri: "http://localhost:4000/callback", + scope: "openid profile", + code_challenge: code_verifier, # Same as verifier for plain method + code_challenge_method: "plain", + expires_at: 10.minutes.from_now + ) + + token_params = { + grant_type: "authorization_code", + code: auth_code.code, + redirect_uri: "http://localhost:4000/callback", + code_verifier: code_verifier + } + + post "/oauth/token", params: token_params, headers: { + "Authorization" => "Basic " + Base64.strict_encode64("#{@application.client_id}:#{@application.client_secret}") + } + + assert_response :success + tokens = JSON.parse(@response.body) + assert tokens.key?("access_token") + assert tokens.key?("id_token") + assert_equal "Bearer", tokens["token_type"] + end + + test "token endpoint works without PKCE (backward compatibility)" do + # Create authorization code without PKCE + auth_code = OidcAuthorizationCode.create!( + application: @application, + user: @user, + code: SecureRandom.urlsafe_base64(32), + redirect_uri: "http://localhost:4000/callback", + scope: "openid profile", + expires_at: 10.minutes.from_now + ) + + token_params = { + grant_type: "authorization_code", + code: auth_code.code, + redirect_uri: "http://localhost:4000/callback" + } + + post "/oauth/token", params: token_params, headers: { + "Authorization" => "Basic " + Base64.strict_encode64("#{@application.client_id}:#{@application.client_secret}") + } + + assert_response :success + tokens = JSON.parse(@response.body) + assert tokens.key?("access_token") + assert tokens.key?("id_token") + assert_equal "Bearer", tokens["token_type"] + end +end \ No newline at end of file