Continue adding auth_time - need it in the refresh token too, so we can accurately create new access tokens.
This commit is contained in:
@@ -162,6 +162,7 @@ class OidcController < ApplicationController
|
|||||||
nonce: nonce,
|
nonce: nonce,
|
||||||
code_challenge: code_challenge,
|
code_challenge: code_challenge,
|
||||||
code_challenge_method: code_challenge_method,
|
code_challenge_method: code_challenge_method,
|
||||||
|
auth_time: Current.session.created_at.to_i,
|
||||||
expires_at: 10.minutes.from_now
|
expires_at: 10.minutes.from_now
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -259,6 +260,7 @@ class OidcController < ApplicationController
|
|||||||
nonce: oauth_params['nonce'],
|
nonce: oauth_params['nonce'],
|
||||||
code_challenge: oauth_params['code_challenge'],
|
code_challenge: oauth_params['code_challenge'],
|
||||||
code_challenge_method: oauth_params['code_challenge_method'],
|
code_challenge_method: oauth_params['code_challenge_method'],
|
||||||
|
auth_time: Current.session.created_at.to_i,
|
||||||
expires_at: 10.minutes.from_now
|
expires_at: 10.minutes.from_now
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -399,7 +401,8 @@ class OidcController < ApplicationController
|
|||||||
application: application,
|
application: application,
|
||||||
user: user,
|
user: user,
|
||||||
oidc_access_token: access_token_record,
|
oidc_access_token: access_token_record,
|
||||||
scope: auth_code.scope
|
scope: auth_code.scope,
|
||||||
|
auth_time: auth_code.auth_time
|
||||||
)
|
)
|
||||||
|
|
||||||
# Find user consent for this application
|
# Find user consent for this application
|
||||||
@@ -412,14 +415,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)
|
# auth_time comes from the authorization code (captured at /authorize time)
|
||||||
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: Current.session.created_at.to_i
|
auth_time: auth_code.auth_time
|
||||||
)
|
)
|
||||||
|
|
||||||
# Return tokens
|
# Return tokens
|
||||||
@@ -524,7 +527,8 @@ class OidcController < ApplicationController
|
|||||||
user: user,
|
user: user,
|
||||||
oidc_access_token: new_access_token,
|
oidc_access_token: new_access_token,
|
||||||
scope: refresh_token_record.scope,
|
scope: refresh_token_record.scope,
|
||||||
token_family_id: refresh_token_record.token_family_id # Keep same family for rotation tracking
|
token_family_id: refresh_token_record.token_family_id, # Keep same family for rotation tracking
|
||||||
|
auth_time: refresh_token_record.auth_time # Carry over original auth_time
|
||||||
)
|
)
|
||||||
|
|
||||||
# Find user consent for this application
|
# Find user consent for this application
|
||||||
@@ -537,13 +541,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)
|
# auth_time comes from the original refresh token (carried over from initial auth)
|
||||||
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: Current.session.created_at.to_i
|
auth_time: refresh_token_record.auth_time
|
||||||
)
|
)
|
||||||
|
|
||||||
# Return new tokens
|
# Return new tokens
|
||||||
|
|||||||
4
db/schema.rb
generated
4
db/schema.rb
generated
@@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[8.1].define(version: 2025_12_31_043838) do
|
ActiveRecord::Schema[8.1].define(version: 2025_12_31_055350) do
|
||||||
create_table "active_storage_attachments", force: :cascade do |t|
|
create_table "active_storage_attachments", force: :cascade do |t|
|
||||||
t.bigint "blob_id", null: false
|
t.bigint "blob_id", null: false
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
@@ -114,6 +114,7 @@ ActiveRecord::Schema[8.1].define(version: 2025_12_31_043838) do
|
|||||||
|
|
||||||
create_table "oidc_authorization_codes", force: :cascade do |t|
|
create_table "oidc_authorization_codes", force: :cascade do |t|
|
||||||
t.integer "application_id", null: false
|
t.integer "application_id", null: false
|
||||||
|
t.integer "auth_time"
|
||||||
t.string "code_challenge"
|
t.string "code_challenge"
|
||||||
t.string "code_challenge_method"
|
t.string "code_challenge_method"
|
||||||
t.string "code_hmac", null: false
|
t.string "code_hmac", null: false
|
||||||
@@ -135,6 +136,7 @@ ActiveRecord::Schema[8.1].define(version: 2025_12_31_043838) do
|
|||||||
|
|
||||||
create_table "oidc_refresh_tokens", force: :cascade do |t|
|
create_table "oidc_refresh_tokens", force: :cascade do |t|
|
||||||
t.integer "application_id", null: false
|
t.integer "application_id", null: false
|
||||||
|
t.integer "auth_time"
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "expires_at", null: false
|
t.datetime "expires_at", null: false
|
||||||
t.integer "oidc_access_token_id", null: false
|
t.integer "oidc_access_token_id", null: false
|
||||||
|
|||||||
@@ -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 created_at" do
|
test "ID token includes auth_time claim from authorization code" do
|
||||||
# Create consent
|
# Create consent
|
||||||
OidcUserConsent.create!(
|
OidcUserConsent.create!(
|
||||||
user: @user,
|
user: @user,
|
||||||
@@ -551,7 +551,7 @@ class OidcPkceControllerTest < ActionDispatch::IntegrationTest
|
|||||||
# Get the expected auth_time from the session's created_at
|
# Get the expected auth_time from the session's created_at
|
||||||
expected_auth_time = Current.session.created_at.to_i
|
expected_auth_time = Current.session.created_at.to_i
|
||||||
|
|
||||||
# Create authorization code
|
# Create authorization code with auth_time
|
||||||
auth_code = OidcAuthorizationCode.create!(
|
auth_code = OidcAuthorizationCode.create!(
|
||||||
application: @application,
|
application: @application,
|
||||||
user: @user,
|
user: @user,
|
||||||
@@ -559,6 +559,7 @@ class OidcPkceControllerTest < ActionDispatch::IntegrationTest
|
|||||||
scope: "openid profile",
|
scope: "openid profile",
|
||||||
code_challenge: code_challenge,
|
code_challenge: code_challenge,
|
||||||
code_challenge_method: "S256",
|
code_challenge_method: "S256",
|
||||||
|
auth_time: expected_auth_time,
|
||||||
expires_at: 10.minutes.from_now
|
expires_at: 10.minutes.from_now
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -577,10 +578,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 and matches session created_at
|
# Decode and verify auth_time is present and matches what we stored
|
||||||
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 expected_auth_time, decoded["auth_time"], "auth_time should match session created_at"
|
assert_equal expected_auth_time, decoded["auth_time"], "auth_time should match authorization code"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "ID token includes auth_time in refresh token flow" do
|
test "ID token includes auth_time in refresh token flow" do
|
||||||
@@ -596,7 +597,7 @@ class OidcPkceControllerTest < ActionDispatch::IntegrationTest
|
|||||||
# Get the expected auth_time from the session's created_at
|
# Get the expected auth_time from the session's created_at
|
||||||
expected_auth_time = Current.session.created_at.to_i
|
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 with auth_time
|
||||||
auth_code = OidcAuthorizationCode.create!(
|
auth_code = OidcAuthorizationCode.create!(
|
||||||
application: @application,
|
application: @application,
|
||||||
user: @user,
|
user: @user,
|
||||||
@@ -604,6 +605,7 @@ class OidcPkceControllerTest < ActionDispatch::IntegrationTest
|
|||||||
scope: "openid profile offline_access",
|
scope: "openid profile offline_access",
|
||||||
code_challenge: nil,
|
code_challenge: nil,
|
||||||
code_challenge_method: nil,
|
code_challenge_method: nil,
|
||||||
|
auth_time: expected_auth_time,
|
||||||
expires_at: 10.minutes.from_now
|
expires_at: 10.minutes.from_now
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -638,10 +640,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 session created_at
|
# Decode and verify auth_time is preserved from original authorization
|
||||||
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 expected_auth_time, decoded["auth_time"], "auth_time should match session created_at"
|
assert_equal expected_auth_time, decoded["auth_time"], "auth_time should match original authorization code"
|
||||||
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
|
||||||
|
|||||||
Reference in New Issue
Block a user