This commit is contained in:
Dan Milne
2025-11-12 15:50:04 +11:00
parent 11ec753c68
commit 33ad956508

View File

@@ -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