177 lines
5.8 KiB
Ruby
177 lines
5.8 KiB
Ruby
require "test_helper"
|
|
|
|
class PkceAuthorizationCodeTest < ActiveSupport::TestCase
|
|
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
|
|
)
|
|
end
|
|
|
|
def teardown
|
|
# Clean up any authorization codes first to avoid foreign key constraints
|
|
OidcAuthorizationCode.where(application: @application).destroy_all
|
|
@user.destroy
|
|
@application.destroy
|
|
end
|
|
|
|
test "authorization code can store PKCE challenge with S256 method" do
|
|
code_challenge = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
|
|
code_challenge_method = "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: code_challenge_method,
|
|
expires_at: 10.minutes.from_now
|
|
)
|
|
|
|
assert_equal code_challenge, auth_code.code_challenge
|
|
assert_equal code_challenge_method, auth_code.code_challenge_method
|
|
assert auth_code.uses_pkce?
|
|
end
|
|
|
|
test "authorization code can store PKCE challenge with plain method" do
|
|
code_challenge = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"
|
|
code_challenge_method = "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_challenge,
|
|
code_challenge_method: code_challenge_method,
|
|
expires_at: 10.minutes.from_now
|
|
)
|
|
|
|
assert_equal code_challenge, auth_code.code_challenge
|
|
assert_equal code_challenge_method, auth_code.code_challenge_method
|
|
assert auth_code.uses_pkce?
|
|
end
|
|
|
|
test "authorization code works without PKCE (backward compatibility)" do
|
|
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
|
|
)
|
|
|
|
assert_nil auth_code.code_challenge
|
|
assert_nil auth_code.code_challenge_method
|
|
assert_not auth_code.uses_pkce?
|
|
end
|
|
|
|
test "code_challenge_method validation accepts valid methods" do
|
|
auth_code = OidcAuthorizationCode.new(
|
|
application: @application,
|
|
user: @user,
|
|
code: SecureRandom.urlsafe_base64(32),
|
|
redirect_uri: "http://localhost:4000/callback",
|
|
scope: "openid profile",
|
|
code_challenge: "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk",
|
|
code_challenge_method: "S256",
|
|
expires_at: 10.minutes.from_now
|
|
)
|
|
|
|
assert auth_code.valid?
|
|
end
|
|
|
|
test "code_challenge_method validation rejects invalid methods" do
|
|
auth_code = OidcAuthorizationCode.new(
|
|
application: @application,
|
|
user: @user,
|
|
code: SecureRandom.urlsafe_base64(32),
|
|
redirect_uri: "http://localhost:4000/callback",
|
|
scope: "openid profile",
|
|
code_challenge: "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk",
|
|
code_challenge_method: "invalid_method",
|
|
expires_at: 10.minutes.from_now
|
|
)
|
|
|
|
assert_not auth_code.valid?
|
|
assert_includes auth_code.errors[:code_challenge_method], "is not included in the list"
|
|
end
|
|
|
|
test "code_challenge format validation accepts valid base64url" do
|
|
# Valid base64url encoded string (43 characters, valid characters)
|
|
valid_challenge = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
|
|
|
|
auth_code = OidcAuthorizationCode.new(
|
|
application: @application,
|
|
user: @user,
|
|
code: SecureRandom.urlsafe_base64(32),
|
|
redirect_uri: "http://localhost:4000/callback",
|
|
scope: "openid profile",
|
|
code_challenge: valid_challenge,
|
|
code_challenge_method: "S256",
|
|
expires_at: 10.minutes.from_now
|
|
)
|
|
|
|
assert auth_code.valid?
|
|
end
|
|
|
|
test "code_challenge format validation rejects invalid format" do
|
|
# Invalid: contains + character (not base64url)
|
|
invalid_challenge = "dBjftJeZ4CVP+mB92K27uhbUJU1p1r/wW1gFWFOEjXk"
|
|
|
|
auth_code = OidcAuthorizationCode.new(
|
|
application: @application,
|
|
user: @user,
|
|
code: SecureRandom.urlsafe_base64(32),
|
|
redirect_uri: "http://localhost:4000/callback",
|
|
scope: "openid profile",
|
|
code_challenge: invalid_challenge,
|
|
code_challenge_method: "S256",
|
|
expires_at: 10.minutes.from_now
|
|
)
|
|
|
|
assert_not auth_code.valid?
|
|
assert_includes auth_code.errors[:code_challenge], "must be 43-128 characters of base64url encoding"
|
|
end
|
|
|
|
test "code_challenge format validation rejects wrong length" do
|
|
# Invalid: too short (42 characters)
|
|
short_challenge = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjX"
|
|
|
|
auth_code = OidcAuthorizationCode.new(
|
|
application: @application,
|
|
user: @user,
|
|
code: SecureRandom.urlsafe_base64(32),
|
|
redirect_uri: "http://localhost:4000/callback",
|
|
scope: "openid profile",
|
|
code_challenge: short_challenge,
|
|
code_challenge_method: "S256",
|
|
expires_at: 10.minutes.from_now
|
|
)
|
|
|
|
assert_not auth_code.valid?
|
|
assert_includes auth_code.errors[:code_challenge], "must be 43-128 characters of base64url encoding"
|
|
end
|
|
|
|
test "code_challenge validation is skipped when no challenge present" do
|
|
auth_code = OidcAuthorizationCode.new(
|
|
application: @application,
|
|
user: @user,
|
|
code: SecureRandom.urlsafe_base64(32),
|
|
redirect_uri: "http://localhost:4000/callback",
|
|
scope: "openid profile",
|
|
expires_at: 10.minutes.from_now
|
|
)
|
|
|
|
# Should be valid even without code_challenge
|
|
assert auth_code.valid?
|
|
end
|
|
end |