Include the hash of the access token in the JWT / ID Token under the key at_hash as per the requirements. Update the discovery endpoint to describe subject_type as 'pairwise', rather than 'public', since we do pairwise subject ids.
This commit is contained in:
@@ -26,7 +26,7 @@ class OidcController < ApplicationController
|
|||||||
response_types_supported: ["code"],
|
response_types_supported: ["code"],
|
||||||
response_modes_supported: ["query"],
|
response_modes_supported: ["query"],
|
||||||
grant_types_supported: ["authorization_code", "refresh_token"],
|
grant_types_supported: ["authorization_code", "refresh_token"],
|
||||||
subject_types_supported: ["public"],
|
subject_types_supported: ["pairwise"],
|
||||||
id_token_signing_alg_values_supported: ["RS256"],
|
id_token_signing_alg_values_supported: ["RS256"],
|
||||||
scopes_supported: ["openid", "profile", "email", "groups", "offline_access"],
|
scopes_supported: ["openid", "profile", "email", "groups", "offline_access"],
|
||||||
token_endpoint_auth_methods_supported: ["client_secret_post", "client_secret_basic"],
|
token_endpoint_auth_methods_supported: ["client_secret_post", "client_secret_basic"],
|
||||||
@@ -422,8 +422,14 @@ class OidcController < ApplicationController
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
# Generate ID token (JWT) with pairwise SID
|
# Generate ID token (JWT) with pairwise SID and at_hash
|
||||||
id_token = OidcJwtService.generate_id_token(user, application, consent: consent, nonce: auth_code.nonce)
|
id_token = OidcJwtService.generate_id_token(
|
||||||
|
user,
|
||||||
|
application,
|
||||||
|
consent: consent,
|
||||||
|
nonce: auth_code.nonce,
|
||||||
|
access_token: access_token_record.plaintext_token
|
||||||
|
)
|
||||||
|
|
||||||
# Return tokens
|
# Return tokens
|
||||||
render json: {
|
render json: {
|
||||||
@@ -539,8 +545,13 @@ class OidcController < ApplicationController
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
# Generate new ID token (JWT with pairwise SID, no nonce for refresh grants)
|
# Generate new ID token (JWT with pairwise SID and at_hash, no nonce for refresh grants)
|
||||||
id_token = OidcJwtService.generate_id_token(user, application, consent: consent)
|
id_token = OidcJwtService.generate_id_token(
|
||||||
|
user,
|
||||||
|
application,
|
||||||
|
consent: consent,
|
||||||
|
access_token: new_access_token.plaintext_token
|
||||||
|
)
|
||||||
|
|
||||||
# Return new tokens
|
# Return new tokens
|
||||||
render json: {
|
render json: {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ class OidcJwtService
|
|||||||
|
|
||||||
class << self
|
class << self
|
||||||
# Generate an ID token (JWT) for the user
|
# Generate an ID token (JWT) for the user
|
||||||
def generate_id_token(user, application, consent: nil, nonce: nil)
|
def generate_id_token(user, application, consent: nil, nonce: nil, access_token: nil)
|
||||||
now = Time.current.to_i
|
now = Time.current.to_i
|
||||||
# Use application's configured ID token TTL (defaults to 1 hour)
|
# Use application's configured ID token TTL (defaults to 1 hour)
|
||||||
ttl = application.id_token_expiry_seconds
|
ttl = application.id_token_expiry_seconds
|
||||||
@@ -26,6 +26,14 @@ class OidcJwtService
|
|||||||
# Add nonce if provided (OIDC requires this for implicit flow)
|
# Add nonce if provided (OIDC requires this for implicit flow)
|
||||||
payload[:nonce] = nonce if nonce.present?
|
payload[:nonce] = nonce if nonce.present?
|
||||||
|
|
||||||
|
# Add at_hash if access token is provided (OIDC Core spec §3.1.3.6)
|
||||||
|
# at_hash = left-most 128 bits of SHA-256 hash of access token, base64url encoded
|
||||||
|
if access_token.present?
|
||||||
|
sha256 = Digest::SHA256.digest(access_token)
|
||||||
|
at_hash = Base64.urlsafe_encode64(sha256[0..15], padding: false)
|
||||||
|
payload[:at_hash] = at_hash
|
||||||
|
end
|
||||||
|
|
||||||
# Add groups if user has any
|
# Add groups if user has any
|
||||||
if user.groups.any?
|
if user.groups.any?
|
||||||
payload[:groups] = user.groups.pluck(:name)
|
payload[:groups] = user.groups.pluck(:name)
|
||||||
|
|||||||
@@ -476,4 +476,23 @@ class OidcJwtServiceTest < ActiveSupport::TestCase
|
|||||||
assert_includes decoded["roles"], "moderator"
|
assert_includes decoded["roles"], "moderator"
|
||||||
assert_includes decoded["roles"], "app_admin"
|
assert_includes decoded["roles"], "app_admin"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "should include at_hash when access token is provided" do
|
||||||
|
access_token = "test-access-token-abc123xyz"
|
||||||
|
token = @service.generate_id_token(@user, @application, access_token: access_token)
|
||||||
|
|
||||||
|
decoded = JWT.decode(token, nil, false).first
|
||||||
|
assert_includes decoded.keys, "at_hash", "Should include at_hash claim"
|
||||||
|
|
||||||
|
# Verify at_hash is correctly computed: base64url(sha256(access_token)[0:16])
|
||||||
|
expected_hash = Base64.urlsafe_encode64(Digest::SHA256.digest(access_token)[0..15], padding: false)
|
||||||
|
assert_equal expected_hash, decoded["at_hash"], "at_hash should match SHA-256 hash of access token"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should not include at_hash when access token is not provided" do
|
||||||
|
token = @service.generate_id_token(@user, @application)
|
||||||
|
|
||||||
|
decoded = JWT.decode(token, nil, false).first
|
||||||
|
refute_includes decoded.keys, "at_hash", "Should not include at_hash when no access token"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
Reference in New Issue
Block a user