Add OIDC fixes, add prefered_username, add application-user claims
This commit is contained in:
@@ -19,8 +19,9 @@ class OidcAuthorizationCodeSecurityTest < ActionDispatch::IntegrationTest
|
||||
end
|
||||
|
||||
def teardown
|
||||
OidcAuthorizationCode.where(application: @application).destroy_all
|
||||
OidcAccessToken.where(application: @application).destroy_all
|
||||
OidcAuthorizationCode.where(application: @application).delete_all
|
||||
# Use delete_all to avoid triggering callbacks that might have issues with the schema
|
||||
OidcAccessToken.where(application: @application).delete_all
|
||||
@user.destroy
|
||||
@application.destroy
|
||||
end
|
||||
|
||||
@@ -11,7 +11,7 @@ class PasswordsControllerTest < ActionDispatch::IntegrationTest
|
||||
test "create" do
|
||||
post passwords_path, params: { email_address: @user.email_address }
|
||||
assert_enqueued_email_with PasswordsMailer, :reset, args: [ @user ]
|
||||
assert_redirected_to new_session_path
|
||||
assert_redirected_to signin_path
|
||||
|
||||
follow_redirect!
|
||||
assert_notice "reset instructions sent"
|
||||
@@ -20,14 +20,14 @@ class PasswordsControllerTest < ActionDispatch::IntegrationTest
|
||||
test "create for an unknown user redirects but sends no mail" do
|
||||
post passwords_path, params: { email_address: "missing-user@example.com" }
|
||||
assert_enqueued_emails 0
|
||||
assert_redirected_to new_session_path
|
||||
assert_redirected_to signin_path
|
||||
|
||||
follow_redirect!
|
||||
assert_notice "reset instructions sent"
|
||||
end
|
||||
|
||||
test "edit" do
|
||||
get edit_password_path(@user.password_reset_token)
|
||||
get edit_password_path(@user.generate_token_for(:password_reset))
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
@@ -41,8 +41,8 @@ class PasswordsControllerTest < ActionDispatch::IntegrationTest
|
||||
|
||||
test "update" do
|
||||
assert_changes -> { @user.reload.password_digest } do
|
||||
put password_path(@user.password_reset_token), params: { password: "new", password_confirmation: "new" }
|
||||
assert_redirected_to new_session_path
|
||||
put password_path(@user.generate_token_for(:password_reset)), params: { password: "newpassword", password_confirmation: "newpassword" }
|
||||
assert_redirected_to signin_path
|
||||
end
|
||||
|
||||
follow_redirect!
|
||||
|
||||
@@ -18,7 +18,7 @@ class SessionsControllerTest < ActionDispatch::IntegrationTest
|
||||
test "create with invalid credentials" do
|
||||
post session_path, params: { email_address: @user.email_address, password: "wrong" }
|
||||
|
||||
assert_redirected_to new_session_path
|
||||
assert_redirected_to signin_path
|
||||
assert_nil cookies[:session_id]
|
||||
end
|
||||
|
||||
@@ -27,7 +27,7 @@ class SessionsControllerTest < ActionDispatch::IntegrationTest
|
||||
|
||||
delete session_path
|
||||
|
||||
assert_redirected_to new_session_path
|
||||
assert_redirected_to signin_path
|
||||
assert_empty cookies[:session_id]
|
||||
end
|
||||
end
|
||||
|
||||
11
test/fixtures/application_user_claims.yml
vendored
Normal file
11
test/fixtures/application_user_claims.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
|
||||
|
||||
kavita_alice_claims:
|
||||
application: kavita_app
|
||||
user: alice
|
||||
custom_claims: { "kavita_groups": ["admin"], "library_access": "all" }
|
||||
|
||||
abs_alice_claims:
|
||||
application: audiobookshelf_app
|
||||
user: alice
|
||||
custom_claims: { "abs_groups": ["user"], "abs_permissions": { "canDownload": true, "canUpload": false } }
|
||||
11
test/fixtures/applications.yml
vendored
11
test/fixtures/applications.yml
vendored
@@ -24,3 +24,14 @@ another_app:
|
||||
https://app.example.com/auth/callback
|
||||
metadata: "{}"
|
||||
active: true
|
||||
|
||||
audiobookshelf_app:
|
||||
name: Audiobookshelf
|
||||
slug: audiobookshelf
|
||||
app_type: oidc
|
||||
client_id: <%= SecureRandom.urlsafe_base64(32) %>
|
||||
client_secret_digest: <%= BCrypt::Password.create(SecureRandom.urlsafe_base64(48)) %>
|
||||
redirect_uris: |
|
||||
https://abs.example.com/auth/openid/callback
|
||||
metadata: "{}"
|
||||
active: true
|
||||
|
||||
8
test/fixtures/groups.yml
vendored
8
test/fixtures/groups.yml
vendored
@@ -1,5 +1,13 @@
|
||||
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
|
||||
|
||||
one:
|
||||
name: Group One
|
||||
description: First test group
|
||||
|
||||
two:
|
||||
name: Group Two
|
||||
description: Second test group
|
||||
|
||||
admin_group:
|
||||
name: Administrators
|
||||
description: System administrators with full access
|
||||
|
||||
12
test/fixtures/users.yml
vendored
12
test/fixtures/users.yml
vendored
@@ -1,5 +1,17 @@
|
||||
<% password_digest = BCrypt::Password.create("password") %>
|
||||
|
||||
one:
|
||||
email_address: one@example.com
|
||||
password_digest: <%= password_digest %>
|
||||
admin: false
|
||||
status: 0 # active
|
||||
|
||||
two:
|
||||
email_address: two@example.com
|
||||
password_digest: <%= password_digest %>
|
||||
admin: true
|
||||
status: 0 # active
|
||||
|
||||
alice:
|
||||
email_address: alice@example.com
|
||||
password_digest: <%= password_digest %>
|
||||
|
||||
@@ -58,8 +58,8 @@ class ForwardAuthIntegrationTest < ActionDispatch::IntegrationTest
|
||||
# Domain and Rule Integration Tests
|
||||
test "different domain patterns with same session" do
|
||||
# Create test rules
|
||||
wildcard_rule = ForwardAuthRule.create!(domain_pattern: "*.example.com", active: true)
|
||||
exact_rule = ForwardAuthRule.create!(domain_pattern: "api.example.com", active: true)
|
||||
wildcard_rule = Application.create!(domain_pattern: "*.example.com", active: true)
|
||||
exact_rule = Application.create!(domain_pattern: "api.example.com", active: true)
|
||||
|
||||
# Sign in
|
||||
post "/signin", params: { email_address: @user.email_address, password: "password" }
|
||||
@@ -82,7 +82,7 @@ class ForwardAuthIntegrationTest < ActionDispatch::IntegrationTest
|
||||
|
||||
test "group-based access control integration" do
|
||||
# Create restricted rule
|
||||
restricted_rule = ForwardAuthRule.create!(domain_pattern: "restricted.example.com", active: true)
|
||||
restricted_rule = Application.create!(domain_pattern: "restricted.example.com", active: true)
|
||||
restricted_rule.allowed_groups << @group
|
||||
|
||||
# Sign in user without group
|
||||
@@ -104,17 +104,19 @@ class ForwardAuthIntegrationTest < ActionDispatch::IntegrationTest
|
||||
|
||||
# Header Configuration Integration Tests
|
||||
test "different header configurations with same user" do
|
||||
# Create rules with different header configs
|
||||
default_rule = ForwardAuthRule.create!(domain_pattern: "default.example.com", active: true)
|
||||
custom_rule = ForwardAuthRule.create!(
|
||||
# Create applications with different configs
|
||||
default_rule = Application.create!(name: "Default App", slug: "default-app", app_type: "forward_auth", domain_pattern: "default.example.com", active: true)
|
||||
custom_rule = Application.create!(
|
||||
name: "Custom App", slug: "custom-app", app_type: "forward_auth",
|
||||
domain_pattern: "custom.example.com",
|
||||
active: true,
|
||||
headers_config: { user: "X-WEBAUTH-USER", groups: "X-WEBAUTH-ROLES" }
|
||||
metadata: { headers: { user: "X-WEBAUTH-USER", groups: "X-WEBAUTH-ROLES" } }.to_json
|
||||
)
|
||||
no_headers_rule = ForwardAuthRule.create!(
|
||||
no_headers_rule = Application.create!(
|
||||
name: "No Headers App", slug: "no-headers-app", app_type: "forward_auth",
|
||||
domain_pattern: "noheaders.example.com",
|
||||
active: true,
|
||||
headers_config: { user: "", email: "", name: "", groups: "", admin: "" }
|
||||
metadata: { headers: { user: "", email: "", name: "", groups: "", admin: "" } }.to_json
|
||||
)
|
||||
|
||||
# Add user to groups
|
||||
@@ -191,7 +193,7 @@ class ForwardAuthIntegrationTest < ActionDispatch::IntegrationTest
|
||||
admin_user = users(:two)
|
||||
|
||||
# Create restricted rule
|
||||
admin_rule = ForwardAuthRule.create!(
|
||||
admin_rule = Application.create!(
|
||||
domain_pattern: "admin.example.com",
|
||||
active: true,
|
||||
headers_config: { user: "X-Admin-User", admin: "X-Admin-Flag" }
|
||||
|
||||
@@ -25,8 +25,8 @@ class InvitationsMailerTest < ActionMailer::TestCase
|
||||
|
||||
assert_equal "You're invited to join Clinch", email.subject
|
||||
assert_equal [@user.email_address], email.to
|
||||
assert_equal [], email.cc
|
||||
assert_equal [], email.bcc
|
||||
assert_equal [], email.cc || []
|
||||
assert_equal [], email.bcc || []
|
||||
# From address is configured in ApplicationMailer
|
||||
assert_not_nil email.from
|
||||
assert email.from.is_a?(Array)
|
||||
|
||||
@@ -25,8 +25,8 @@ class PasswordsMailerTest < ActionMailer::TestCase
|
||||
|
||||
assert_equal "Reset your password", email.subject
|
||||
assert_equal [@user.email_address], email.to
|
||||
assert_equal [], email.cc
|
||||
assert_equal [], email.bcc
|
||||
assert_equal [], email.cc || []
|
||||
assert_equal [], email.bcc || []
|
||||
# From address is configured in ApplicationMailer
|
||||
assert_not_nil email.from
|
||||
assert email.from.is_a?(Array)
|
||||
|
||||
78
test/models/application_user_claim_test.rb
Normal file
78
test/models/application_user_claim_test.rb
Normal file
@@ -0,0 +1,78 @@
|
||||
require "test_helper"
|
||||
|
||||
class ApplicationUserClaimTest < ActiveSupport::TestCase
|
||||
def setup
|
||||
@user = users(:bob)
|
||||
@application = applications(:another_app)
|
||||
end
|
||||
|
||||
test "should create valid application user claim" do
|
||||
claim = ApplicationUserClaim.new(
|
||||
user: @user,
|
||||
application: @application,
|
||||
custom_claims: { "role": "admin" }
|
||||
)
|
||||
assert claim.valid?
|
||||
assert claim.save
|
||||
end
|
||||
|
||||
test "should enforce uniqueness of user per application" do
|
||||
ApplicationUserClaim.create!(
|
||||
user: @user,
|
||||
application: @application,
|
||||
custom_claims: { "role": "admin" }
|
||||
)
|
||||
|
||||
duplicate = ApplicationUserClaim.new(
|
||||
user: @user,
|
||||
application: @application,
|
||||
custom_claims: { "role": "user" }
|
||||
)
|
||||
|
||||
assert_not duplicate.valid?
|
||||
assert_includes duplicate.errors[:user_id], "has already been taken"
|
||||
end
|
||||
|
||||
test "parsed_custom_claims returns hash" do
|
||||
claim = ApplicationUserClaim.new(
|
||||
user: @user,
|
||||
application: @application,
|
||||
custom_claims: { "role": "admin", "level": 5 }
|
||||
)
|
||||
|
||||
parsed = claim.parsed_custom_claims
|
||||
assert_equal "admin", parsed["role"]
|
||||
assert_equal 5, parsed["level"]
|
||||
end
|
||||
|
||||
test "parsed_custom_claims returns empty hash when nil" do
|
||||
claim = ApplicationUserClaim.new(
|
||||
user: @user,
|
||||
application: @application,
|
||||
custom_claims: nil
|
||||
)
|
||||
|
||||
assert_equal({}, claim.parsed_custom_claims)
|
||||
end
|
||||
|
||||
test "should not allow reserved OIDC claim names" do
|
||||
claim = ApplicationUserClaim.new(
|
||||
user: @user,
|
||||
application: @application,
|
||||
custom_claims: { "groups": ["admin"], "role": "user" }
|
||||
)
|
||||
|
||||
assert_not claim.valid?
|
||||
assert_includes claim.errors[:custom_claims], "cannot override reserved OIDC claims: groups"
|
||||
end
|
||||
|
||||
test "should allow non-reserved claim names" do
|
||||
claim = ApplicationUserClaim.new(
|
||||
user: @user,
|
||||
application: @application,
|
||||
custom_claims: { "kavita_groups": ["admin"], "role": "user" }
|
||||
)
|
||||
|
||||
assert claim.valid?
|
||||
end
|
||||
end
|
||||
@@ -14,7 +14,8 @@ class OidcJwtServiceTest < ActiveSupport::TestCase
|
||||
assert token.length > 100, "Token should be substantial"
|
||||
assert token.include?('.')
|
||||
|
||||
decoded = JWT.decode(token, nil, true)
|
||||
# Decode without verification for testing the payload
|
||||
decoded = JWT.decode(token, nil, false).first
|
||||
assert_equal @application.client_id, decoded['aud'], "Should have correct audience"
|
||||
assert_equal @user.id.to_s, decoded['sub'], "Should have correct subject"
|
||||
assert_equal @user.email_address, decoded['email'], "Should have correct email"
|
||||
@@ -22,16 +23,16 @@ class OidcJwtServiceTest < ActiveSupport::TestCase
|
||||
assert_equal @user.email_address, decoded['preferred_username'], "Should have preferred username"
|
||||
assert_equal @user.email_address, decoded['name'], "Should have name"
|
||||
assert_equal "https://localhost:3000", decoded['iss'], "Should have correct issuer"
|
||||
assert_equal Time.now.to_i + 3600, decoded['exp'], "Should have correct expiration"
|
||||
assert_in_delta Time.current.to_i + 3600, decoded['exp'], 5, "Should have correct expiration"
|
||||
end
|
||||
|
||||
test "should handle nonce in id token" do
|
||||
nonce = "test-nonce-12345"
|
||||
token = @service.generate_id_token(@user, @application, nonce: nonce)
|
||||
|
||||
decoded = JWT.decode(token, nil, true)
|
||||
decoded = JWT.decode(token, nil, false).first
|
||||
assert_equal nonce, decoded['nonce'], "Should preserve nonce in token"
|
||||
assert_equal Time.now.to_i + 3600, decoded['exp'], "Should have correct expiration with nonce"
|
||||
assert_in_delta Time.current.to_i + 3600, decoded['exp'], 5, "Should have correct expiration with nonce"
|
||||
end
|
||||
|
||||
test "should include groups in token when user has groups" do
|
||||
@@ -39,17 +40,17 @@ class OidcJwtServiceTest < ActiveSupport::TestCase
|
||||
|
||||
token = @service.generate_id_token(@user, @application)
|
||||
|
||||
decoded = JWT.decode(token, nil, true)
|
||||
decoded = JWT.decode(token, nil, false).first
|
||||
assert_includes decoded['groups'], "admin", "Should include user's groups"
|
||||
end
|
||||
|
||||
test "should include admin claim for admin users" do
|
||||
test "admin claim should not be included in token" do
|
||||
@user.update!(admin: true)
|
||||
|
||||
token = @service.generate_id_token(@user, @application)
|
||||
|
||||
decoded = JWT.decode(token, nil, true)
|
||||
assert_equal true, decoded['admin'], "Admin users should have admin claim"
|
||||
decoded = JWT.decode(token, nil, false).first
|
||||
refute decoded.key?('admin'), "Admin claim should not be included in ID tokens (use groups instead)"
|
||||
end
|
||||
|
||||
test "should handle role-based claims when enabled" do
|
||||
@@ -63,7 +64,7 @@ class OidcJwtServiceTest < ActiveSupport::TestCase
|
||||
|
||||
token = @service.generate_id_token(@user, @application)
|
||||
|
||||
decoded = JWT.decode(token, nil, true)
|
||||
decoded = JWT.decode(token, nil, false).first
|
||||
assert_includes decoded['roles'], "editor", "Should include user's role"
|
||||
end
|
||||
|
||||
@@ -96,7 +97,7 @@ class OidcJwtServiceTest < ActiveSupport::TestCase
|
||||
|
||||
token = @service.generate_id_token(@user, @application)
|
||||
|
||||
decoded = JWT.decode(token, nil, true)
|
||||
decoded = JWT.decode(token, nil, false).first
|
||||
assert_equal "Content Editor", decoded['role_display_name'], "Should include role display name"
|
||||
assert_includes decoded['role_permissions'], "read", "Should include read permission"
|
||||
assert_includes decoded['role_permissions'], "write", "Should include write permission"
|
||||
@@ -107,7 +108,7 @@ class OidcJwtServiceTest < ActiveSupport::TestCase
|
||||
test "should handle missing roles gracefully" do
|
||||
token = @service.generate_id_token(@user, @application)
|
||||
|
||||
decoded = JWT.decode(token, nil, true)
|
||||
decoded = JWT.decode(token, nil, false).first
|
||||
refute_includes decoded, 'roles', "Should not have roles when not configured"
|
||||
end
|
||||
|
||||
@@ -260,7 +261,7 @@ class OidcJwtServiceTest < ActiveSupport::TestCase
|
||||
test "should handle access token generation" do
|
||||
token = @service.generate_id_token(@user, @application)
|
||||
|
||||
decoded = JWT.decode(token, nil, true)
|
||||
decoded = JWT.decode(token, nil, false).first
|
||||
refute_includes decoded.keys, 'email_verified'
|
||||
assert_equal @user.id.to_s, decoded['sub'], "Should decode subject correctly"
|
||||
assert_equal @application.client_id, decoded['aud'], "Should decode audience correctly"
|
||||
@@ -291,4 +292,215 @@ class OidcJwtServiceTest < ActiveSupport::TestCase
|
||||
end
|
||||
assert_match /no key found/, error.message, "Should warn about missing private key"
|
||||
end
|
||||
|
||||
test "should include app-specific custom claims in token" do
|
||||
# Use bob and another_app to avoid fixture conflicts
|
||||
user = users(:bob)
|
||||
app = applications(:another_app)
|
||||
|
||||
# Create app-specific claim
|
||||
ApplicationUserClaim.create!(
|
||||
user: user,
|
||||
application: app,
|
||||
custom_claims: { "app_groups": ["admin"], "library_access": "all" }
|
||||
)
|
||||
|
||||
token = @service.generate_id_token(user, app)
|
||||
decoded = JWT.decode(token, nil, false).first
|
||||
|
||||
assert_equal ["admin"], decoded["app_groups"]
|
||||
assert_equal "all", decoded["library_access"]
|
||||
end
|
||||
|
||||
test "app-specific claims should override user and group claims" do
|
||||
# Use bob and another_app to avoid fixture conflicts
|
||||
user = users(:bob)
|
||||
app = applications(:another_app)
|
||||
|
||||
# Add user to group with claims
|
||||
group = groups(:admin_group)
|
||||
group.update!(custom_claims: { "role": "viewer", "max_items": 10 })
|
||||
user.groups << group
|
||||
|
||||
# Add user custom claims
|
||||
user.update!(custom_claims: { "role": "editor", "theme": "dark" })
|
||||
|
||||
# Add app-specific claims (should override both)
|
||||
ApplicationUserClaim.create!(
|
||||
user: user,
|
||||
application: app,
|
||||
custom_claims: { "role": "admin", "app_specific": true }
|
||||
)
|
||||
|
||||
token = @service.generate_id_token(user, app)
|
||||
decoded = JWT.decode(token, nil, false).first
|
||||
|
||||
# App-specific claim should win
|
||||
assert_equal "admin", decoded["role"]
|
||||
# App-specific claim should be present
|
||||
assert_equal true, decoded["app_specific"]
|
||||
# User claim not overridden should still be present
|
||||
assert_equal "dark", decoded["theme"]
|
||||
# Group claim not overridden should still be present
|
||||
assert_equal 10, decoded["max_items"]
|
||||
end
|
||||
|
||||
test "should deep merge array claims from group and user" do
|
||||
user = users(:bob)
|
||||
app = applications(:another_app)
|
||||
|
||||
# Group has roles: ["user"]
|
||||
group = groups(:admin_group)
|
||||
group.update!(custom_claims: { "roles" => ["user"], "permissions" => ["read"] })
|
||||
user.groups << group
|
||||
|
||||
# User adds roles: ["admin"]
|
||||
user.update!(custom_claims: { "roles" => ["admin"], "permissions" => ["write"] })
|
||||
|
||||
token = @service.generate_id_token(user, app)
|
||||
decoded = JWT.decode(token, nil, false).first
|
||||
|
||||
# Roles should be combined (not overwritten)
|
||||
assert_equal 2, decoded["roles"].length
|
||||
assert_includes decoded["roles"], "user"
|
||||
assert_includes decoded["roles"], "admin"
|
||||
# Permissions should also be combined
|
||||
assert_equal 2, decoded["permissions"].length
|
||||
assert_includes decoded["permissions"], "read"
|
||||
assert_includes decoded["permissions"], "write"
|
||||
end
|
||||
|
||||
test "should deep merge array claims from multiple groups" do
|
||||
user = users(:bob)
|
||||
app = applications(:another_app)
|
||||
|
||||
# First group has roles: ["user"]
|
||||
group1 = groups(:admin_group)
|
||||
group1.update!(custom_claims: { "roles" => ["user"] })
|
||||
user.groups << group1
|
||||
|
||||
# Second group has roles: ["moderator"]
|
||||
group2 = Group.create!(name: "moderators", description: "Moderators group")
|
||||
group2.update!(custom_claims: { "roles" => ["moderator"] })
|
||||
user.groups << group2
|
||||
|
||||
# User adds roles: ["admin"]
|
||||
user.update!(custom_claims: { "roles" => ["admin"] })
|
||||
|
||||
token = @service.generate_id_token(user, app)
|
||||
decoded = JWT.decode(token, nil, false).first
|
||||
|
||||
# All roles should be combined
|
||||
assert_equal 3, decoded["roles"].length
|
||||
assert_includes decoded["roles"], "user"
|
||||
assert_includes decoded["roles"], "moderator"
|
||||
assert_includes decoded["roles"], "admin"
|
||||
end
|
||||
|
||||
test "should remove duplicate values when merging arrays" do
|
||||
user = users(:bob)
|
||||
app = applications(:another_app)
|
||||
|
||||
# Group has roles: ["user", "reader"]
|
||||
group = groups(:admin_group)
|
||||
group.update!(custom_claims: { "roles" => ["user", "reader"] })
|
||||
user.groups << group
|
||||
|
||||
# User also has "user" role (duplicate)
|
||||
user.update!(custom_claims: { "roles" => ["user", "admin"] })
|
||||
|
||||
token = @service.generate_id_token(user, app)
|
||||
decoded = JWT.decode(token, nil, false).first
|
||||
|
||||
# "user" should only appear once
|
||||
assert_equal 3, decoded["roles"].length
|
||||
assert_includes decoded["roles"], "user"
|
||||
assert_includes decoded["roles"], "reader"
|
||||
assert_includes decoded["roles"], "admin"
|
||||
end
|
||||
|
||||
test "should override non-array values while merging arrays" do
|
||||
user = users(:bob)
|
||||
app = applications(:another_app)
|
||||
|
||||
# Group has roles array and max_items scalar
|
||||
group = groups(:admin_group)
|
||||
group.update!(custom_claims: { "roles" => ["user"], "max_items" => 10, "theme" => "light" })
|
||||
user.groups << group
|
||||
|
||||
# User overrides max_items and theme, adds to roles
|
||||
user.update!(custom_claims: { "roles" => ["admin"], "max_items" => 100, "theme" => "dark" })
|
||||
|
||||
token = @service.generate_id_token(user, app)
|
||||
decoded = JWT.decode(token, nil, false).first
|
||||
|
||||
# Arrays should be combined
|
||||
assert_equal 2, decoded["roles"].length
|
||||
assert_includes decoded["roles"], "user"
|
||||
assert_includes decoded["roles"], "admin"
|
||||
# Scalar values should be overridden (user wins)
|
||||
assert_equal 100, decoded["max_items"]
|
||||
assert_equal "dark", decoded["theme"]
|
||||
end
|
||||
|
||||
test "should deep merge nested hashes in claims" do
|
||||
user = users(:bob)
|
||||
app = applications(:another_app)
|
||||
|
||||
# Group has nested config
|
||||
group = groups(:admin_group)
|
||||
group.update!(custom_claims: {
|
||||
"config" => {
|
||||
"theme" => "light",
|
||||
"notifications" => { "email" => true }
|
||||
}
|
||||
})
|
||||
user.groups << group
|
||||
|
||||
# User adds to nested config
|
||||
user.update!(custom_claims: {
|
||||
"config" => {
|
||||
"language" => "en",
|
||||
"notifications" => { "sms" => true }
|
||||
}
|
||||
})
|
||||
|
||||
token = @service.generate_id_token(user, app)
|
||||
decoded = JWT.decode(token, nil, false).first
|
||||
|
||||
# Nested hashes should be deep merged
|
||||
assert_equal "light", decoded["config"]["theme"]
|
||||
assert_equal "en", decoded["config"]["language"]
|
||||
assert_equal true, decoded["config"]["notifications"]["email"]
|
||||
assert_equal true, decoded["config"]["notifications"]["sms"]
|
||||
end
|
||||
|
||||
test "app-specific claims should combine arrays with group and user claims" do
|
||||
user = users(:bob)
|
||||
app = applications(:another_app)
|
||||
|
||||
# Group has roles: ["user"]
|
||||
group = groups(:admin_group)
|
||||
group.update!(custom_claims: { "roles" => ["user"] })
|
||||
user.groups << group
|
||||
|
||||
# User has roles: ["moderator"]
|
||||
user.update!(custom_claims: { "roles" => ["moderator"] })
|
||||
|
||||
# App-specific has roles: ["app_admin"]
|
||||
ApplicationUserClaim.create!(
|
||||
user: user,
|
||||
application: app,
|
||||
custom_claims: { "roles" => ["app_admin"] }
|
||||
)
|
||||
|
||||
token = @service.generate_id_token(user, app)
|
||||
decoded = JWT.decode(token, nil, false).first
|
||||
|
||||
# All three sources should be combined
|
||||
assert_equal 3, decoded["roles"].length
|
||||
assert_includes decoded["roles"], "user"
|
||||
assert_includes decoded["roles"], "moderator"
|
||||
assert_includes decoded["roles"], "app_admin"
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user