Compare commits
1 Commits
2025.02
...
038801f34b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
038801f34b |
@@ -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
|
||||
177
test/models/pkce_authorization_code_test.rb
Normal file
177
test/models/pkce_authorization_code_test.rb
Normal 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
|
||||
Reference in New Issue
Block a user