1 Commits

Author SHA1 Message Date
Dan Milne
038801f34b Add pkce
Some checks failed
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled
2025-11-09 10:21:29 +11:00
2 changed files with 186 additions and 0 deletions

View File

@@ -0,0 +1,9 @@
class AddPkceSupportToOidcAuthorizationCodes < ActiveRecord::Migration[8.1]
def change
add_column :oidc_authorization_codes, :code_challenge, :string
add_column :oidc_authorization_codes, :code_challenge_method, :string
# Add index for code_challenge to improve query performance
add_index :oidc_authorization_codes, :code_challenge
end
end

View File

@@ -0,0 +1,177 @@
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