We already have a login_time stored - the time stamp of the Session instance creation ( created after successful login ).

This commit is contained in:
Dan Milne
2025-12-31 16:45:45 +11:00
parent 4b4afe277e
commit 3939ea773f
3 changed files with 13 additions and 14 deletions

View File

@@ -49,9 +49,6 @@ module Authentication
user.sessions.create!(user_agent: request.user_agent, ip_address: request.remote_ip).tap do |session| user.sessions.create!(user_agent: request.user_agent, ip_address: request.remote_ip).tap do |session|
Current.session = session Current.session = session
# Store auth_time in session for OIDC max_age support
session[:auth_time] = Time.now.to_i
# Extract root domain for cross-subdomain cookies (required for forward auth) # Extract root domain for cross-subdomain cookies (required for forward auth)
domain = extract_root_domain(request.host) domain = extract_root_domain(request.host)

View File

@@ -412,13 +412,14 @@ class OidcController < ApplicationController
end end
# Generate ID token (JWT) with pairwise SID, at_hash, and auth_time # Generate ID token (JWT) with pairwise SID, at_hash, and auth_time
# auth_time comes from the Session model's created_at (when user logged in)
id_token = OidcJwtService.generate_id_token( id_token = OidcJwtService.generate_id_token(
user, user,
application, application,
consent: consent, consent: consent,
nonce: auth_code.nonce, nonce: auth_code.nonce,
access_token: access_token_record.plaintext_token, access_token: access_token_record.plaintext_token,
auth_time: session[:auth_time] auth_time: Current.session.created_at.to_i
) )
# Return tokens # Return tokens
@@ -536,12 +537,13 @@ class OidcController < ApplicationController
end end
# Generate new ID token (JWT with pairwise SID, at_hash, and auth_time; no nonce for refresh grants) # Generate new ID token (JWT with pairwise SID, at_hash, and auth_time; no nonce for refresh grants)
# auth_time comes from the Session model's created_at (when user logged in)
id_token = OidcJwtService.generate_id_token( id_token = OidcJwtService.generate_id_token(
user, user,
application, application,
consent: consent, consent: consent,
access_token: new_access_token.plaintext_token, access_token: new_access_token.plaintext_token,
auth_time: session[:auth_time] auth_time: Current.session.created_at.to_i
) )
# Return new tokens # Return new tokens

View File

@@ -532,7 +532,7 @@ class OidcPkceControllerTest < ActionDispatch::IntegrationTest
# AUTH_TIME CLAIM TESTS # AUTH_TIME CLAIM TESTS
# ==================== # ====================
test "ID token includes auth_time claim from session" do test "ID token includes auth_time claim from session created_at" do
# Create consent # Create consent
OidcUserConsent.create!( OidcUserConsent.create!(
user: @user, user: @user,
@@ -548,8 +548,8 @@ class OidcPkceControllerTest < ActionDispatch::IntegrationTest
.tr("+/", "-_") .tr("+/", "-_")
.tr("=", "") .tr("=", "")
# Set auth_time in session (simulating user login) # Get the expected auth_time from the session's created_at
session[:auth_time] = Time.now.to_i - 300 # 5 minutes ago expected_auth_time = Current.session.created_at.to_i
# Create authorization code # Create authorization code
auth_code = OidcAuthorizationCode.create!( auth_code = OidcAuthorizationCode.create!(
@@ -577,10 +577,10 @@ class OidcPkceControllerTest < ActionDispatch::IntegrationTest
tokens = JSON.parse(@response.body) tokens = JSON.parse(@response.body)
assert tokens.key?("id_token") assert tokens.key?("id_token")
# Decode and verify auth_time is present # Decode and verify auth_time is present and matches session created_at
decoded = JWT.decode(tokens["id_token"], nil, false).first decoded = JWT.decode(tokens["id_token"], nil, false).first
assert_includes decoded.keys, "auth_time", "ID token should include auth_time" assert_includes decoded.keys, "auth_time", "ID token should include auth_time"
assert_equal session[:auth_time], decoded["auth_time"], "auth_time should match session value" assert_equal expected_auth_time, decoded["auth_time"], "auth_time should match session created_at"
end end
test "ID token includes auth_time in refresh token flow" do test "ID token includes auth_time in refresh token flow" do
@@ -593,8 +593,8 @@ class OidcPkceControllerTest < ActionDispatch::IntegrationTest
sid: "test-sid-refresh-auth-time" sid: "test-sid-refresh-auth-time"
) )
# Set auth_time in session # Get the expected auth_time from the session's created_at
session[:auth_time] = Time.now.to_i - 600 # 10 minutes ago expected_auth_time = Current.session.created_at.to_i
# Create initial access and refresh tokens (bypass PKCE for this test) # Create initial access and refresh tokens (bypass PKCE for this test)
auth_code = OidcAuthorizationCode.create!( auth_code = OidcAuthorizationCode.create!(
@@ -638,10 +638,10 @@ class OidcPkceControllerTest < ActionDispatch::IntegrationTest
new_tokens = JSON.parse(@response.body) new_tokens = JSON.parse(@response.body)
assert new_tokens.key?("id_token") assert new_tokens.key?("id_token")
# Decode and verify auth_time is still present from refresh # Decode and verify auth_time is still present from session created_at
decoded = JWT.decode(new_tokens["id_token"], nil, false).first decoded = JWT.decode(new_tokens["id_token"], nil, false).first
assert_includes decoded.keys, "auth_time", "Refreshed ID token should include auth_time" assert_includes decoded.keys, "auth_time", "Refreshed ID token should include auth_time"
assert_equal session[:auth_time], decoded["auth_time"], "auth_time should persist from original session" assert_equal expected_auth_time, decoded["auth_time"], "auth_time should match session created_at"
end end
test "at_hash is correctly computed and included in ID token" do test "at_hash is correctly computed and included in ID token" do