Include auth_time in ID token. Switch from upsert -> find_and_create_by so we actually get sid values for consent on the creation of the record
This commit is contained in:
@@ -49,6 +49,9 @@ 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)
|
||||||
|
|
||||||
|
|||||||
@@ -245,15 +245,10 @@ class OidcController < ApplicationController
|
|||||||
|
|
||||||
# Record user consent
|
# Record user consent
|
||||||
requested_scopes = oauth_params['scope'].split(' ')
|
requested_scopes = oauth_params['scope'].split(' ')
|
||||||
OidcUserConsent.upsert(
|
consent = OidcUserConsent.find_or_initialize_by(user: user, application: application)
|
||||||
{
|
consent.scopes_granted = requested_scopes.join(' ')
|
||||||
user_id: user.id,
|
consent.granted_at = Time.current
|
||||||
application_id: application.id,
|
consent.save!
|
||||||
scopes_granted: requested_scopes.join(' '),
|
|
||||||
granted_at: Time.current
|
|
||||||
},
|
|
||||||
unique_by: [:user_id, :application_id]
|
|
||||||
)
|
|
||||||
|
|
||||||
# Generate authorization code
|
# Generate authorization code
|
||||||
auth_code = OidcAuthorizationCode.create!(
|
auth_code = OidcAuthorizationCode.create!(
|
||||||
@@ -416,13 +411,14 @@ class OidcController < ApplicationController
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
# Generate ID token (JWT) with pairwise SID and at_hash
|
# Generate ID token (JWT) with pairwise SID, at_hash, and auth_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: session[:auth_time]
|
||||||
)
|
)
|
||||||
|
|
||||||
# Return tokens
|
# Return tokens
|
||||||
@@ -539,12 +535,13 @@ class OidcController < ApplicationController
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
# Generate new ID token (JWT with pairwise SID and at_hash, no nonce for refresh grants)
|
# Generate new ID token (JWT with pairwise SID, at_hash, and auth_time; no nonce for refresh grants)
|
||||||
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]
|
||||||
)
|
)
|
||||||
|
|
||||||
# Return new tokens
|
# Return new tokens
|
||||||
|
|||||||
@@ -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, access_token: nil)
|
def generate_id_token(user, application, consent: nil, nonce: nil, access_token: nil, auth_time: 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,9 @@ 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 auth_time if provided (OIDC Core §2 - required when max_age is used)
|
||||||
|
payload[:auth_time] = auth_time if auth_time.present?
|
||||||
|
|
||||||
# Add at_hash if access token is provided (OIDC Core spec §3.1.3.6)
|
# 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
|
# at_hash = left-most 128 bits of SHA-256 hash of access token, base64url encoded
|
||||||
if access_token.present?
|
if access_token.present?
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ class OidcAuthorizationCodeSecurityTest < ActionDispatch::IntegrationTest
|
|||||||
auth_code = OidcAuthorizationCode.create!(
|
auth_code = OidcAuthorizationCode.create!(
|
||||||
application: @application,
|
application: @application,
|
||||||
user: @user,
|
user: @user,
|
||||||
code: SecureRandom.urlsafe_base64(32),
|
|
||||||
redirect_uri: "http://localhost:4000/callback",
|
redirect_uri: "http://localhost:4000/callback",
|
||||||
scope: "openid profile",
|
scope: "openid profile",
|
||||||
expires_at: 10.minutes.from_now
|
expires_at: 10.minutes.from_now
|
||||||
@@ -55,7 +54,7 @@ class OidcAuthorizationCodeSecurityTest < ActionDispatch::IntegrationTest
|
|||||||
|
|
||||||
token_params = {
|
token_params = {
|
||||||
grant_type: "authorization_code",
|
grant_type: "authorization_code",
|
||||||
code: auth_code.code,
|
code: auth_code.plaintext_code,
|
||||||
redirect_uri: "http://localhost:4000/callback"
|
redirect_uri: "http://localhost:4000/callback"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,7 +93,6 @@ class OidcAuthorizationCodeSecurityTest < ActionDispatch::IntegrationTest
|
|||||||
auth_code = OidcAuthorizationCode.create!(
|
auth_code = OidcAuthorizationCode.create!(
|
||||||
application: @application,
|
application: @application,
|
||||||
user: @user,
|
user: @user,
|
||||||
code: SecureRandom.urlsafe_base64(32),
|
|
||||||
redirect_uri: "http://localhost:4000/callback",
|
redirect_uri: "http://localhost:4000/callback",
|
||||||
scope: "openid profile",
|
scope: "openid profile",
|
||||||
expires_at: 10.minutes.from_now
|
expires_at: 10.minutes.from_now
|
||||||
@@ -102,7 +100,7 @@ class OidcAuthorizationCodeSecurityTest < ActionDispatch::IntegrationTest
|
|||||||
|
|
||||||
token_params = {
|
token_params = {
|
||||||
grant_type: "authorization_code",
|
grant_type: "authorization_code",
|
||||||
code: auth_code.code,
|
code: auth_code.plaintext_code,
|
||||||
redirect_uri: "http://localhost:4000/callback"
|
redirect_uri: "http://localhost:4000/callback"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,7 +147,6 @@ class OidcAuthorizationCodeSecurityTest < ActionDispatch::IntegrationTest
|
|||||||
auth_code = OidcAuthorizationCode.create!(
|
auth_code = OidcAuthorizationCode.create!(
|
||||||
application: @application,
|
application: @application,
|
||||||
user: @user,
|
user: @user,
|
||||||
code: SecureRandom.urlsafe_base64(32),
|
|
||||||
redirect_uri: "http://localhost:4000/callback",
|
redirect_uri: "http://localhost:4000/callback",
|
||||||
scope: "openid profile",
|
scope: "openid profile",
|
||||||
used: true,
|
used: true,
|
||||||
@@ -158,7 +155,7 @@ class OidcAuthorizationCodeSecurityTest < ActionDispatch::IntegrationTest
|
|||||||
|
|
||||||
token_params = {
|
token_params = {
|
||||||
grant_type: "authorization_code",
|
grant_type: "authorization_code",
|
||||||
code: auth_code.code,
|
code: auth_code.plaintext_code,
|
||||||
redirect_uri: "http://localhost:4000/callback"
|
redirect_uri: "http://localhost:4000/callback"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,7 +183,6 @@ class OidcAuthorizationCodeSecurityTest < ActionDispatch::IntegrationTest
|
|||||||
auth_code = OidcAuthorizationCode.create!(
|
auth_code = OidcAuthorizationCode.create!(
|
||||||
application: @application,
|
application: @application,
|
||||||
user: @user,
|
user: @user,
|
||||||
code: SecureRandom.urlsafe_base64(32),
|
|
||||||
redirect_uri: "http://localhost:4000/callback",
|
redirect_uri: "http://localhost:4000/callback",
|
||||||
scope: "openid profile",
|
scope: "openid profile",
|
||||||
expires_at: 5.minutes.ago
|
expires_at: 5.minutes.ago
|
||||||
@@ -194,7 +190,7 @@ class OidcAuthorizationCodeSecurityTest < ActionDispatch::IntegrationTest
|
|||||||
|
|
||||||
token_params = {
|
token_params = {
|
||||||
grant_type: "authorization_code",
|
grant_type: "authorization_code",
|
||||||
code: auth_code.code,
|
code: auth_code.plaintext_code,
|
||||||
redirect_uri: "http://localhost:4000/callback"
|
redirect_uri: "http://localhost:4000/callback"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,7 +217,6 @@ class OidcAuthorizationCodeSecurityTest < ActionDispatch::IntegrationTest
|
|||||||
auth_code = OidcAuthorizationCode.create!(
|
auth_code = OidcAuthorizationCode.create!(
|
||||||
application: @application,
|
application: @application,
|
||||||
user: @user,
|
user: @user,
|
||||||
code: SecureRandom.urlsafe_base64(32),
|
|
||||||
redirect_uri: "http://localhost:4000/callback",
|
redirect_uri: "http://localhost:4000/callback",
|
||||||
scope: "openid profile",
|
scope: "openid profile",
|
||||||
expires_at: 10.minutes.from_now
|
expires_at: 10.minutes.from_now
|
||||||
@@ -229,7 +224,7 @@ class OidcAuthorizationCodeSecurityTest < ActionDispatch::IntegrationTest
|
|||||||
|
|
||||||
token_params = {
|
token_params = {
|
||||||
grant_type: "authorization_code",
|
grant_type: "authorization_code",
|
||||||
code: auth_code.code,
|
code: auth_code.plaintext_code,
|
||||||
redirect_uri: "http://evil.com/callback" # Wrong redirect URI
|
redirect_uri: "http://evil.com/callback" # Wrong redirect URI
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,7 +279,6 @@ class OidcAuthorizationCodeSecurityTest < ActionDispatch::IntegrationTest
|
|||||||
auth_code = OidcAuthorizationCode.create!(
|
auth_code = OidcAuthorizationCode.create!(
|
||||||
application: @application,
|
application: @application,
|
||||||
user: @user,
|
user: @user,
|
||||||
code: SecureRandom.urlsafe_base64(32),
|
|
||||||
redirect_uri: "http://localhost:4000/callback",
|
redirect_uri: "http://localhost:4000/callback",
|
||||||
scope: "openid profile",
|
scope: "openid profile",
|
||||||
expires_at: 10.minutes.from_now
|
expires_at: 10.minutes.from_now
|
||||||
@@ -293,7 +287,7 @@ class OidcAuthorizationCodeSecurityTest < ActionDispatch::IntegrationTest
|
|||||||
# Try to use it with different application credentials
|
# Try to use it with different application credentials
|
||||||
token_params = {
|
token_params = {
|
||||||
grant_type: "authorization_code",
|
grant_type: "authorization_code",
|
||||||
code: auth_code.code,
|
code: auth_code.plaintext_code,
|
||||||
redirect_uri: "http://localhost:4000/callback"
|
redirect_uri: "http://localhost:4000/callback"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,7 +319,6 @@ class OidcAuthorizationCodeSecurityTest < ActionDispatch::IntegrationTest
|
|||||||
auth_code = OidcAuthorizationCode.create!(
|
auth_code = OidcAuthorizationCode.create!(
|
||||||
application: @application,
|
application: @application,
|
||||||
user: @user,
|
user: @user,
|
||||||
code: SecureRandom.urlsafe_base64(32),
|
|
||||||
redirect_uri: "http://localhost:4000/callback",
|
redirect_uri: "http://localhost:4000/callback",
|
||||||
scope: "openid profile",
|
scope: "openid profile",
|
||||||
expires_at: 10.minutes.from_now
|
expires_at: 10.minutes.from_now
|
||||||
@@ -333,7 +326,7 @@ class OidcAuthorizationCodeSecurityTest < ActionDispatch::IntegrationTest
|
|||||||
|
|
||||||
token_params = {
|
token_params = {
|
||||||
grant_type: "authorization_code",
|
grant_type: "authorization_code",
|
||||||
code: auth_code.code,
|
code: auth_code.plaintext_code,
|
||||||
redirect_uri: "http://localhost:4000/callback"
|
redirect_uri: "http://localhost:4000/callback"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -359,7 +352,6 @@ class OidcAuthorizationCodeSecurityTest < ActionDispatch::IntegrationTest
|
|||||||
auth_code = OidcAuthorizationCode.create!(
|
auth_code = OidcAuthorizationCode.create!(
|
||||||
application: @application,
|
application: @application,
|
||||||
user: @user,
|
user: @user,
|
||||||
code: SecureRandom.urlsafe_base64(32),
|
|
||||||
redirect_uri: "http://localhost:4000/callback",
|
redirect_uri: "http://localhost:4000/callback",
|
||||||
scope: "openid profile",
|
scope: "openid profile",
|
||||||
expires_at: 10.minutes.from_now
|
expires_at: 10.minutes.from_now
|
||||||
@@ -367,7 +359,7 @@ class OidcAuthorizationCodeSecurityTest < ActionDispatch::IntegrationTest
|
|||||||
|
|
||||||
token_params = {
|
token_params = {
|
||||||
grant_type: "authorization_code",
|
grant_type: "authorization_code",
|
||||||
code: auth_code.code,
|
code: auth_code.plaintext_code,
|
||||||
redirect_uri: "http://localhost:4000/callback"
|
redirect_uri: "http://localhost:4000/callback"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -393,7 +385,6 @@ class OidcAuthorizationCodeSecurityTest < ActionDispatch::IntegrationTest
|
|||||||
auth_code = OidcAuthorizationCode.create!(
|
auth_code = OidcAuthorizationCode.create!(
|
||||||
application: @application,
|
application: @application,
|
||||||
user: @user,
|
user: @user,
|
||||||
code: SecureRandom.urlsafe_base64(32),
|
|
||||||
redirect_uri: "http://localhost:4000/callback",
|
redirect_uri: "http://localhost:4000/callback",
|
||||||
scope: "openid profile",
|
scope: "openid profile",
|
||||||
expires_at: 10.minutes.from_now
|
expires_at: 10.minutes.from_now
|
||||||
@@ -401,7 +392,7 @@ class OidcAuthorizationCodeSecurityTest < ActionDispatch::IntegrationTest
|
|||||||
|
|
||||||
token_params = {
|
token_params = {
|
||||||
grant_type: "authorization_code",
|
grant_type: "authorization_code",
|
||||||
code: auth_code.code,
|
code: auth_code.plaintext_code,
|
||||||
redirect_uri: "http://localhost:4000/callback",
|
redirect_uri: "http://localhost:4000/callback",
|
||||||
client_id: @application.client_id,
|
client_id: @application.client_id,
|
||||||
client_secret: @plain_client_secret
|
client_secret: @plain_client_secret
|
||||||
@@ -428,7 +419,6 @@ class OidcAuthorizationCodeSecurityTest < ActionDispatch::IntegrationTest
|
|||||||
auth_code = OidcAuthorizationCode.create!(
|
auth_code = OidcAuthorizationCode.create!(
|
||||||
application: @application,
|
application: @application,
|
||||||
user: @user,
|
user: @user,
|
||||||
code: SecureRandom.urlsafe_base64(32),
|
|
||||||
redirect_uri: "http://localhost:4000/callback",
|
redirect_uri: "http://localhost:4000/callback",
|
||||||
scope: "openid profile",
|
scope: "openid profile",
|
||||||
expires_at: 10.minutes.from_now
|
expires_at: 10.minutes.from_now
|
||||||
@@ -436,7 +426,7 @@ class OidcAuthorizationCodeSecurityTest < ActionDispatch::IntegrationTest
|
|||||||
|
|
||||||
token_params = {
|
token_params = {
|
||||||
grant_type: "authorization_code",
|
grant_type: "authorization_code",
|
||||||
code: auth_code.code,
|
code: auth_code.plaintext_code,
|
||||||
redirect_uri: "http://localhost:4000/callback"
|
redirect_uri: "http://localhost:4000/callback"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -495,7 +485,6 @@ class OidcAuthorizationCodeSecurityTest < ActionDispatch::IntegrationTest
|
|||||||
auth_code = OidcAuthorizationCode.create!(
|
auth_code = OidcAuthorizationCode.create!(
|
||||||
application: @application,
|
application: @application,
|
||||||
user: @user,
|
user: @user,
|
||||||
code: SecureRandom.urlsafe_base64(32),
|
|
||||||
redirect_uri: "http://localhost:4000/callback",
|
redirect_uri: "http://localhost:4000/callback",
|
||||||
scope: "openid profile",
|
scope: "openid profile",
|
||||||
expires_at: 10.minutes.from_now
|
expires_at: 10.minutes.from_now
|
||||||
@@ -503,7 +492,7 @@ class OidcAuthorizationCodeSecurityTest < ActionDispatch::IntegrationTest
|
|||||||
|
|
||||||
token_params = {
|
token_params = {
|
||||||
grant_type: "authorization_code",
|
grant_type: "authorization_code",
|
||||||
code: auth_code.code,
|
code: auth_code.plaintext_code,
|
||||||
redirect_uri: "http://localhost:4000/callback"
|
redirect_uri: "http://localhost:4000/callback"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -616,7 +605,6 @@ class OidcAuthorizationCodeSecurityTest < ActionDispatch::IntegrationTest
|
|||||||
auth_code = OidcAuthorizationCode.create!(
|
auth_code = OidcAuthorizationCode.create!(
|
||||||
application: @application,
|
application: @application,
|
||||||
user: @user,
|
user: @user,
|
||||||
code: SecureRandom.urlsafe_base64(32),
|
|
||||||
redirect_uri: "http://localhost:4000/callback",
|
redirect_uri: "http://localhost:4000/callback",
|
||||||
scope: "openid profile",
|
scope: "openid profile",
|
||||||
nonce: "test_nonce_123",
|
nonce: "test_nonce_123",
|
||||||
@@ -626,7 +614,7 @@ class OidcAuthorizationCodeSecurityTest < ActionDispatch::IntegrationTest
|
|||||||
# Exchange code for tokens
|
# Exchange code for tokens
|
||||||
post "/oauth/token", params: {
|
post "/oauth/token", params: {
|
||||||
grant_type: "authorization_code",
|
grant_type: "authorization_code",
|
||||||
code: auth_code.code,
|
code: auth_code.plaintext_code,
|
||||||
redirect_uri: "http://localhost:4000/callback"
|
redirect_uri: "http://localhost:4000/callback"
|
||||||
}, headers: {
|
}, headers: {
|
||||||
"Authorization" => "Basic " + Base64.strict_encode64("#{@application.client_id}:#{@plain_client_secret}")
|
"Authorization" => "Basic " + Base64.strict_encode64("#{@application.client_id}:#{@plain_client_secret}")
|
||||||
@@ -660,7 +648,6 @@ class OidcAuthorizationCodeSecurityTest < ActionDispatch::IntegrationTest
|
|||||||
auth_code = OidcAuthorizationCode.create!(
|
auth_code = OidcAuthorizationCode.create!(
|
||||||
application: @application,
|
application: @application,
|
||||||
user: @user,
|
user: @user,
|
||||||
code: SecureRandom.urlsafe_base64(32),
|
|
||||||
redirect_uri: "http://localhost:4000/callback",
|
redirect_uri: "http://localhost:4000/callback",
|
||||||
scope: "openid profile",
|
scope: "openid profile",
|
||||||
expires_at: 10.minutes.from_now
|
expires_at: 10.minutes.from_now
|
||||||
@@ -669,7 +656,7 @@ class OidcAuthorizationCodeSecurityTest < ActionDispatch::IntegrationTest
|
|||||||
# Exchange code for tokens
|
# Exchange code for tokens
|
||||||
post "/oauth/token", params: {
|
post "/oauth/token", params: {
|
||||||
grant_type: "authorization_code",
|
grant_type: "authorization_code",
|
||||||
code: auth_code.code,
|
code: auth_code.plaintext_code,
|
||||||
redirect_uri: "http://localhost:4000/callback"
|
redirect_uri: "http://localhost:4000/callback"
|
||||||
}, headers: {
|
}, headers: {
|
||||||
"Authorization" => "Basic " + Base64.strict_encode64("#{@application.client_id}:#{@plain_client_secret}")
|
"Authorization" => "Basic " + Base64.strict_encode64("#{@application.client_id}:#{@plain_client_secret}")
|
||||||
@@ -705,7 +692,6 @@ class OidcAuthorizationCodeSecurityTest < ActionDispatch::IntegrationTest
|
|||||||
auth_code = OidcAuthorizationCode.create!(
|
auth_code = OidcAuthorizationCode.create!(
|
||||||
application: @application,
|
application: @application,
|
||||||
user: @user,
|
user: @user,
|
||||||
code: SecureRandom.urlsafe_base64(32),
|
|
||||||
redirect_uri: "http://localhost:4000/callback",
|
redirect_uri: "http://localhost:4000/callback",
|
||||||
scope: "openid profile",
|
scope: "openid profile",
|
||||||
code_challenge: code_challenge,
|
code_challenge: code_challenge,
|
||||||
@@ -716,7 +702,7 @@ class OidcAuthorizationCodeSecurityTest < ActionDispatch::IntegrationTest
|
|||||||
# Try to exchange code without code_verifier
|
# Try to exchange code without code_verifier
|
||||||
post "/oauth/token", params: {
|
post "/oauth/token", params: {
|
||||||
grant_type: "authorization_code",
|
grant_type: "authorization_code",
|
||||||
code: auth_code.code,
|
code: auth_code.plaintext_code,
|
||||||
redirect_uri: "http://localhost:4000/callback"
|
redirect_uri: "http://localhost:4000/callback"
|
||||||
}, headers: {
|
}, headers: {
|
||||||
"Authorization" => "Basic " + Base64.strict_encode64("#{@application.client_id}:#{@plain_client_secret}")
|
"Authorization" => "Basic " + Base64.strict_encode64("#{@application.client_id}:#{@plain_client_secret}")
|
||||||
@@ -745,7 +731,6 @@ class OidcAuthorizationCodeSecurityTest < ActionDispatch::IntegrationTest
|
|||||||
auth_code = OidcAuthorizationCode.create!(
|
auth_code = OidcAuthorizationCode.create!(
|
||||||
application: @application,
|
application: @application,
|
||||||
user: @user,
|
user: @user,
|
||||||
code: SecureRandom.urlsafe_base64(32),
|
|
||||||
redirect_uri: "http://localhost:4000/callback",
|
redirect_uri: "http://localhost:4000/callback",
|
||||||
scope: "openid profile",
|
scope: "openid profile",
|
||||||
code_challenge: code_challenge,
|
code_challenge: code_challenge,
|
||||||
@@ -756,7 +741,7 @@ class OidcAuthorizationCodeSecurityTest < ActionDispatch::IntegrationTest
|
|||||||
# Exchange code with correct code_verifier
|
# Exchange code with correct code_verifier
|
||||||
post "/oauth/token", params: {
|
post "/oauth/token", params: {
|
||||||
grant_type: "authorization_code",
|
grant_type: "authorization_code",
|
||||||
code: auth_code.code,
|
code: auth_code.plaintext_code,
|
||||||
redirect_uri: "http://localhost:4000/callback",
|
redirect_uri: "http://localhost:4000/callback",
|
||||||
code_verifier: code_verifier
|
code_verifier: code_verifier
|
||||||
}, headers: {
|
}, headers: {
|
||||||
@@ -785,7 +770,6 @@ class OidcAuthorizationCodeSecurityTest < ActionDispatch::IntegrationTest
|
|||||||
auth_code = OidcAuthorizationCode.create!(
|
auth_code = OidcAuthorizationCode.create!(
|
||||||
application: @application,
|
application: @application,
|
||||||
user: @user,
|
user: @user,
|
||||||
code: SecureRandom.urlsafe_base64(32),
|
|
||||||
redirect_uri: "http://localhost:4000/callback",
|
redirect_uri: "http://localhost:4000/callback",
|
||||||
scope: "openid profile",
|
scope: "openid profile",
|
||||||
code_challenge: code_challenge,
|
code_challenge: code_challenge,
|
||||||
@@ -796,7 +780,7 @@ class OidcAuthorizationCodeSecurityTest < ActionDispatch::IntegrationTest
|
|||||||
# Try with wrong code_verifier
|
# Try with wrong code_verifier
|
||||||
post "/oauth/token", params: {
|
post "/oauth/token", params: {
|
||||||
grant_type: "authorization_code",
|
grant_type: "authorization_code",
|
||||||
code: auth_code.code,
|
code: auth_code.plaintext_code,
|
||||||
redirect_uri: "http://localhost:4000/callback",
|
redirect_uri: "http://localhost:4000/callback",
|
||||||
code_verifier: "wrong_code_verifier_12345678901234567890"
|
code_verifier: "wrong_code_verifier_12345678901234567890"
|
||||||
}, headers: {
|
}, headers: {
|
||||||
@@ -855,9 +839,7 @@ class OidcAuthorizationCodeSecurityTest < ActionDispatch::IntegrationTest
|
|||||||
assert_not_equal old_refresh_token, new_refresh_token
|
assert_not_equal old_refresh_token, new_refresh_token
|
||||||
|
|
||||||
# Verify token family is preserved
|
# Verify token family is preserved
|
||||||
new_token_record = OidcRefreshToken.where(application: @application).find do |rt|
|
new_token_record = OidcRefreshToken.find_by_token(new_refresh_token)
|
||||||
rt.token_matches?(new_refresh_token)
|
|
||||||
end
|
|
||||||
assert_equal original_token_family_id, new_token_record.token_family_id
|
assert_equal original_token_family_id, new_token_record.token_family_id
|
||||||
|
|
||||||
# Old refresh token should be revoked
|
# Old refresh token should be revoked
|
||||||
|
|||||||
@@ -127,7 +127,6 @@ class OidcPkceControllerTest < ActionDispatch::IntegrationTest
|
|||||||
auth_code = OidcAuthorizationCode.create!(
|
auth_code = OidcAuthorizationCode.create!(
|
||||||
application: @application,
|
application: @application,
|
||||||
user: @user,
|
user: @user,
|
||||||
code: SecureRandom.urlsafe_base64(32),
|
|
||||||
redirect_uri: "http://localhost:4000/callback",
|
redirect_uri: "http://localhost:4000/callback",
|
||||||
scope: "openid profile",
|
scope: "openid profile",
|
||||||
code_challenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
|
code_challenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
|
||||||
@@ -137,7 +136,7 @@ class OidcPkceControllerTest < ActionDispatch::IntegrationTest
|
|||||||
|
|
||||||
token_params = {
|
token_params = {
|
||||||
grant_type: "authorization_code",
|
grant_type: "authorization_code",
|
||||||
code: auth_code.code,
|
code: auth_code.plaintext_code,
|
||||||
redirect_uri: "http://localhost:4000/callback"
|
redirect_uri: "http://localhost:4000/callback"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,7 +164,6 @@ class OidcPkceControllerTest < ActionDispatch::IntegrationTest
|
|||||||
auth_code = OidcAuthorizationCode.create!(
|
auth_code = OidcAuthorizationCode.create!(
|
||||||
application: @application,
|
application: @application,
|
||||||
user: @user,
|
user: @user,
|
||||||
code: SecureRandom.urlsafe_base64(32),
|
|
||||||
redirect_uri: "http://localhost:4000/callback",
|
redirect_uri: "http://localhost:4000/callback",
|
||||||
scope: "openid profile",
|
scope: "openid profile",
|
||||||
code_challenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
|
code_challenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
|
||||||
@@ -175,7 +173,7 @@ class OidcPkceControllerTest < ActionDispatch::IntegrationTest
|
|||||||
|
|
||||||
token_params = {
|
token_params = {
|
||||||
grant_type: "authorization_code",
|
grant_type: "authorization_code",
|
||||||
code: auth_code.code,
|
code: auth_code.plaintext_code,
|
||||||
redirect_uri: "http://localhost:4000/callback"
|
redirect_uri: "http://localhost:4000/callback"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,7 +201,6 @@ class OidcPkceControllerTest < ActionDispatch::IntegrationTest
|
|||||||
auth_code = OidcAuthorizationCode.create!(
|
auth_code = OidcAuthorizationCode.create!(
|
||||||
application: @application,
|
application: @application,
|
||||||
user: @user,
|
user: @user,
|
||||||
code: SecureRandom.urlsafe_base64(32),
|
|
||||||
redirect_uri: "http://localhost:4000/callback",
|
redirect_uri: "http://localhost:4000/callback",
|
||||||
scope: "openid profile",
|
scope: "openid profile",
|
||||||
code_challenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
|
code_challenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
|
||||||
@@ -213,7 +210,7 @@ class OidcPkceControllerTest < ActionDispatch::IntegrationTest
|
|||||||
|
|
||||||
token_params = {
|
token_params = {
|
||||||
grant_type: "authorization_code",
|
grant_type: "authorization_code",
|
||||||
code: auth_code.code,
|
code: auth_code.plaintext_code,
|
||||||
redirect_uri: "http://localhost:4000/callback",
|
redirect_uri: "http://localhost:4000/callback",
|
||||||
# Use a properly formatted but wrong verifier (43+ chars, base64url)
|
# Use a properly formatted but wrong verifier (43+ chars, base64url)
|
||||||
code_verifier: "wrongverifier_with_enough_characters_base64url"
|
code_verifier: "wrongverifier_with_enough_characters_base64url"
|
||||||
@@ -249,7 +246,6 @@ class OidcPkceControllerTest < ActionDispatch::IntegrationTest
|
|||||||
auth_code = OidcAuthorizationCode.create!(
|
auth_code = OidcAuthorizationCode.create!(
|
||||||
application: @application,
|
application: @application,
|
||||||
user: @user,
|
user: @user,
|
||||||
code: SecureRandom.urlsafe_base64(32),
|
|
||||||
redirect_uri: "http://localhost:4000/callback",
|
redirect_uri: "http://localhost:4000/callback",
|
||||||
scope: "openid profile",
|
scope: "openid profile",
|
||||||
code_challenge: code_challenge,
|
code_challenge: code_challenge,
|
||||||
@@ -259,7 +255,7 @@ class OidcPkceControllerTest < ActionDispatch::IntegrationTest
|
|||||||
|
|
||||||
token_params = {
|
token_params = {
|
||||||
grant_type: "authorization_code",
|
grant_type: "authorization_code",
|
||||||
code: auth_code.code,
|
code: auth_code.plaintext_code,
|
||||||
redirect_uri: "http://localhost:4000/callback",
|
redirect_uri: "http://localhost:4000/callback",
|
||||||
code_verifier: code_verifier
|
code_verifier: code_verifier
|
||||||
}
|
}
|
||||||
@@ -291,7 +287,6 @@ class OidcPkceControllerTest < ActionDispatch::IntegrationTest
|
|||||||
auth_code = OidcAuthorizationCode.create!(
|
auth_code = OidcAuthorizationCode.create!(
|
||||||
application: @application,
|
application: @application,
|
||||||
user: @user,
|
user: @user,
|
||||||
code: SecureRandom.urlsafe_base64(32),
|
|
||||||
redirect_uri: "http://localhost:4000/callback",
|
redirect_uri: "http://localhost:4000/callback",
|
||||||
scope: "openid profile",
|
scope: "openid profile",
|
||||||
code_challenge: code_verifier, # Same as verifier for plain method
|
code_challenge: code_verifier, # Same as verifier for plain method
|
||||||
@@ -301,7 +296,7 @@ class OidcPkceControllerTest < ActionDispatch::IntegrationTest
|
|||||||
|
|
||||||
token_params = {
|
token_params = {
|
||||||
grant_type: "authorization_code",
|
grant_type: "authorization_code",
|
||||||
code: auth_code.code,
|
code: auth_code.plaintext_code,
|
||||||
redirect_uri: "http://localhost:4000/callback",
|
redirect_uri: "http://localhost:4000/callback",
|
||||||
code_verifier: code_verifier
|
code_verifier: code_verifier
|
||||||
}
|
}
|
||||||
@@ -342,7 +337,6 @@ class OidcPkceControllerTest < ActionDispatch::IntegrationTest
|
|||||||
auth_code = OidcAuthorizationCode.create!(
|
auth_code = OidcAuthorizationCode.create!(
|
||||||
application: legacy_app,
|
application: legacy_app,
|
||||||
user: @user,
|
user: @user,
|
||||||
code: SecureRandom.urlsafe_base64(32),
|
|
||||||
redirect_uri: "http://localhost:5000/callback",
|
redirect_uri: "http://localhost:5000/callback",
|
||||||
scope: "openid profile",
|
scope: "openid profile",
|
||||||
expires_at: 10.minutes.from_now
|
expires_at: 10.minutes.from_now
|
||||||
@@ -350,7 +344,7 @@ class OidcPkceControllerTest < ActionDispatch::IntegrationTest
|
|||||||
|
|
||||||
token_params = {
|
token_params = {
|
||||||
grant_type: "authorization_code",
|
grant_type: "authorization_code",
|
||||||
code: auth_code.code,
|
code: auth_code.plaintext_code,
|
||||||
redirect_uri: "http://localhost:5000/callback"
|
redirect_uri: "http://localhost:5000/callback"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -408,7 +402,6 @@ class OidcPkceControllerTest < ActionDispatch::IntegrationTest
|
|||||||
auth_code = OidcAuthorizationCode.create!(
|
auth_code = OidcAuthorizationCode.create!(
|
||||||
application: public_app,
|
application: public_app,
|
||||||
user: @user,
|
user: @user,
|
||||||
code: SecureRandom.urlsafe_base64(32),
|
|
||||||
redirect_uri: "http://localhost:6000/callback",
|
redirect_uri: "http://localhost:6000/callback",
|
||||||
scope: "openid profile",
|
scope: "openid profile",
|
||||||
expires_at: 10.minutes.from_now,
|
expires_at: 10.minutes.from_now,
|
||||||
@@ -419,7 +412,7 @@ class OidcPkceControllerTest < ActionDispatch::IntegrationTest
|
|||||||
# Token request with PKCE but no client_secret
|
# Token request with PKCE but no client_secret
|
||||||
token_params = {
|
token_params = {
|
||||||
grant_type: "authorization_code",
|
grant_type: "authorization_code",
|
||||||
code: auth_code.code,
|
code: auth_code.plaintext_code,
|
||||||
redirect_uri: "http://localhost:6000/callback",
|
redirect_uri: "http://localhost:6000/callback",
|
||||||
client_id: public_app.client_id,
|
client_id: public_app.client_id,
|
||||||
code_verifier: code_verifier
|
code_verifier: code_verifier
|
||||||
@@ -467,7 +460,6 @@ class OidcPkceControllerTest < ActionDispatch::IntegrationTest
|
|||||||
auth_code = OidcAuthorizationCode.create!(
|
auth_code = OidcAuthorizationCode.create!(
|
||||||
application: public_app,
|
application: public_app,
|
||||||
user: @user,
|
user: @user,
|
||||||
code: SecureRandom.urlsafe_base64(32),
|
|
||||||
redirect_uri: "http://localhost:7000/callback",
|
redirect_uri: "http://localhost:7000/callback",
|
||||||
scope: "openid profile",
|
scope: "openid profile",
|
||||||
expires_at: 10.minutes.from_now
|
expires_at: 10.minutes.from_now
|
||||||
@@ -476,7 +468,7 @@ class OidcPkceControllerTest < ActionDispatch::IntegrationTest
|
|||||||
# Token request without PKCE should fail
|
# Token request without PKCE should fail
|
||||||
token_params = {
|
token_params = {
|
||||||
grant_type: "authorization_code",
|
grant_type: "authorization_code",
|
||||||
code: auth_code.code,
|
code: auth_code.plaintext_code,
|
||||||
redirect_uri: "http://localhost:7000/callback",
|
redirect_uri: "http://localhost:7000/callback",
|
||||||
client_id: public_app.client_id
|
client_id: public_app.client_id
|
||||||
}
|
}
|
||||||
@@ -514,7 +506,6 @@ class OidcPkceControllerTest < ActionDispatch::IntegrationTest
|
|||||||
auth_code = OidcAuthorizationCode.create!(
|
auth_code = OidcAuthorizationCode.create!(
|
||||||
application: @application,
|
application: @application,
|
||||||
user: @user,
|
user: @user,
|
||||||
code: SecureRandom.urlsafe_base64(32),
|
|
||||||
redirect_uri: "http://localhost:4000/callback",
|
redirect_uri: "http://localhost:4000/callback",
|
||||||
scope: "openid profile",
|
scope: "openid profile",
|
||||||
expires_at: 10.minutes.from_now
|
expires_at: 10.minutes.from_now
|
||||||
@@ -523,7 +514,7 @@ class OidcPkceControllerTest < ActionDispatch::IntegrationTest
|
|||||||
# Token request without PKCE should fail
|
# Token request without PKCE should fail
|
||||||
token_params = {
|
token_params = {
|
||||||
grant_type: "authorization_code",
|
grant_type: "authorization_code",
|
||||||
code: auth_code.code,
|
code: auth_code.plaintext_code,
|
||||||
redirect_uri: "http://localhost:4000/callback"
|
redirect_uri: "http://localhost:4000/callback"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -536,4 +527,172 @@ class OidcPkceControllerTest < ActionDispatch::IntegrationTest
|
|||||||
assert_equal "invalid_request", error["error"]
|
assert_equal "invalid_request", error["error"]
|
||||||
assert_match /PKCE is required/, error["error_description"]
|
assert_match /PKCE is required/, error["error_description"]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# ====================
|
||||||
|
# AUTH_TIME CLAIM TESTS
|
||||||
|
# ====================
|
||||||
|
|
||||||
|
test "ID token includes auth_time claim from session" do
|
||||||
|
# Create consent
|
||||||
|
OidcUserConsent.create!(
|
||||||
|
user: @user,
|
||||||
|
application: @application,
|
||||||
|
scopes_granted: "openid profile",
|
||||||
|
granted_at: Time.current,
|
||||||
|
sid: "test-sid-auth-time"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Generate valid PKCE pair
|
||||||
|
code_verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
|
||||||
|
code_challenge = Digest::SHA256.base64digest(code_verifier)
|
||||||
|
.tr("+/", "-_")
|
||||||
|
.tr("=", "")
|
||||||
|
|
||||||
|
# Set auth_time in session (simulating user login)
|
||||||
|
session[:auth_time] = Time.now.to_i - 300 # 5 minutes ago
|
||||||
|
|
||||||
|
# Create authorization code
|
||||||
|
auth_code = OidcAuthorizationCode.create!(
|
||||||
|
application: @application,
|
||||||
|
user: @user,
|
||||||
|
redirect_uri: "http://localhost:4000/callback",
|
||||||
|
scope: "openid profile",
|
||||||
|
code_challenge: code_challenge,
|
||||||
|
code_challenge_method: "S256",
|
||||||
|
expires_at: 10.minutes.from_now
|
||||||
|
)
|
||||||
|
|
||||||
|
token_params = {
|
||||||
|
grant_type: "authorization_code",
|
||||||
|
code: auth_code.plaintext_code,
|
||||||
|
redirect_uri: "http://localhost:4000/callback",
|
||||||
|
code_verifier: code_verifier
|
||||||
|
}
|
||||||
|
|
||||||
|
post "/oauth/token", params: token_params, headers: {
|
||||||
|
"Authorization" => "Basic " + Base64.strict_encode64("#{@application.client_id}:#{@application.client_secret}")
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_response :success
|
||||||
|
tokens = JSON.parse(@response.body)
|
||||||
|
assert tokens.key?("id_token")
|
||||||
|
|
||||||
|
# Decode and verify auth_time is present
|
||||||
|
decoded = JWT.decode(tokens["id_token"], nil, false).first
|
||||||
|
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"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "ID token includes auth_time in refresh token flow" do
|
||||||
|
# Create consent
|
||||||
|
OidcUserConsent.create!(
|
||||||
|
user: @user,
|
||||||
|
application: @application,
|
||||||
|
scopes_granted: "openid profile offline_access",
|
||||||
|
granted_at: Time.current,
|
||||||
|
sid: "test-sid-refresh-auth-time"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set auth_time in session
|
||||||
|
session[:auth_time] = Time.now.to_i - 600 # 10 minutes ago
|
||||||
|
|
||||||
|
# Create initial access and refresh tokens (bypass PKCE for this test)
|
||||||
|
auth_code = OidcAuthorizationCode.create!(
|
||||||
|
application: @application,
|
||||||
|
user: @user,
|
||||||
|
redirect_uri: "http://localhost:4000/callback",
|
||||||
|
scope: "openid profile offline_access",
|
||||||
|
code_challenge: nil,
|
||||||
|
code_challenge_method: nil,
|
||||||
|
expires_at: 10.minutes.from_now
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update application to not require PKCE for testing
|
||||||
|
@application.update!(require_pkce: false)
|
||||||
|
|
||||||
|
token_params = {
|
||||||
|
grant_type: "authorization_code",
|
||||||
|
code: auth_code.plaintext_code,
|
||||||
|
redirect_uri: "http://localhost:4000/callback"
|
||||||
|
}
|
||||||
|
|
||||||
|
post "/oauth/token", params: token_params, headers: {
|
||||||
|
"Authorization" => "Basic " + Base64.strict_encode64("#{@application.client_id}:#{@application.client_secret}")
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_response :success
|
||||||
|
tokens = JSON.parse(@response.body)
|
||||||
|
refresh_token = tokens["refresh_token"]
|
||||||
|
|
||||||
|
# Now use the refresh token
|
||||||
|
refresh_params = {
|
||||||
|
grant_type: "refresh_token",
|
||||||
|
refresh_token: refresh_token
|
||||||
|
}
|
||||||
|
|
||||||
|
post "/oauth/token", params: refresh_params, headers: {
|
||||||
|
"Authorization" => "Basic " + Base64.strict_encode64("#{@application.client_id}:#{@application.client_secret}")
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_response :success
|
||||||
|
new_tokens = JSON.parse(@response.body)
|
||||||
|
assert new_tokens.key?("id_token")
|
||||||
|
|
||||||
|
# Decode and verify auth_time is still present from refresh
|
||||||
|
decoded = JWT.decode(new_tokens["id_token"], nil, false).first
|
||||||
|
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"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "at_hash is correctly computed and included in ID token" do
|
||||||
|
# Create consent
|
||||||
|
OidcUserConsent.create!(
|
||||||
|
user: @user,
|
||||||
|
application: @application,
|
||||||
|
scopes_granted: "openid profile",
|
||||||
|
granted_at: Time.current,
|
||||||
|
sid: "test-sid-at-hash"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Generate valid PKCE pair
|
||||||
|
code_verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
|
||||||
|
code_challenge = Digest::SHA256.base64digest(code_verifier)
|
||||||
|
.tr("+/", "-_")
|
||||||
|
.tr("=", "")
|
||||||
|
|
||||||
|
# Create authorization code
|
||||||
|
auth_code = OidcAuthorizationCode.create!(
|
||||||
|
application: @application,
|
||||||
|
user: @user,
|
||||||
|
redirect_uri: "http://localhost:4000/callback",
|
||||||
|
scope: "openid profile",
|
||||||
|
code_challenge: code_challenge,
|
||||||
|
code_challenge_method: "S256",
|
||||||
|
expires_at: 10.minutes.from_now
|
||||||
|
)
|
||||||
|
|
||||||
|
token_params = {
|
||||||
|
grant_type: "authorization_code",
|
||||||
|
code: auth_code.plaintext_code,
|
||||||
|
redirect_uri: "http://localhost:4000/callback",
|
||||||
|
code_verifier: code_verifier
|
||||||
|
}
|
||||||
|
|
||||||
|
post "/oauth/token", params: token_params, headers: {
|
||||||
|
"Authorization" => "Basic " + Base64.strict_encode64("#{@application.client_id}:#{@application.client_secret}")
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_response :success
|
||||||
|
tokens = JSON.parse(@response.body)
|
||||||
|
access_token = tokens["access_token"]
|
||||||
|
id_token = tokens["id_token"]
|
||||||
|
|
||||||
|
# Decode ID token
|
||||||
|
decoded = JWT.decode(id_token, nil, false).first
|
||||||
|
assert_includes decoded.keys, "at_hash", "ID token should include at_hash"
|
||||||
|
|
||||||
|
# Verify at_hash matches the access token hash
|
||||||
|
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
|
||||||
end
|
end
|
||||||
@@ -15,7 +15,6 @@ class OidcRefreshTokenControllerTest < ActionDispatch::IntegrationTest
|
|||||||
auth_code = OidcAuthorizationCode.create!(
|
auth_code = OidcAuthorizationCode.create!(
|
||||||
application: @application,
|
application: @application,
|
||||||
user: @user,
|
user: @user,
|
||||||
code: SecureRandom.urlsafe_base64(32),
|
|
||||||
redirect_uri: @application.parsed_redirect_uris.first,
|
redirect_uri: @application.parsed_redirect_uris.first,
|
||||||
scope: "openid profile email",
|
scope: "openid profile email",
|
||||||
expires_at: 10.minutes.from_now
|
expires_at: 10.minutes.from_now
|
||||||
@@ -24,7 +23,7 @@ class OidcRefreshTokenControllerTest < ActionDispatch::IntegrationTest
|
|||||||
# Exchange authorization code for tokens
|
# Exchange authorization code for tokens
|
||||||
post "/oauth/token", params: {
|
post "/oauth/token", params: {
|
||||||
grant_type: "authorization_code",
|
grant_type: "authorization_code",
|
||||||
code: auth_code.code,
|
code: auth_code.plaintext_code,
|
||||||
redirect_uri: @application.parsed_redirect_uris.first,
|
redirect_uri: @application.parsed_redirect_uris.first,
|
||||||
client_id: @application.client_id,
|
client_id: @application.client_id,
|
||||||
client_secret: @client_secret
|
client_secret: @client_secret
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ class PkceAuthorizationCodeTest < ActiveSupport::TestCase
|
|||||||
auth_code = OidcAuthorizationCode.create!(
|
auth_code = OidcAuthorizationCode.create!(
|
||||||
application: @application,
|
application: @application,
|
||||||
user: @user,
|
user: @user,
|
||||||
code: SecureRandom.urlsafe_base64(32),
|
|
||||||
redirect_uri: "http://localhost:4000/callback",
|
redirect_uri: "http://localhost:4000/callback",
|
||||||
scope: "openid profile",
|
scope: "openid profile",
|
||||||
code_challenge: code_challenge,
|
code_challenge: code_challenge,
|
||||||
@@ -46,7 +45,6 @@ class PkceAuthorizationCodeTest < ActiveSupport::TestCase
|
|||||||
auth_code = OidcAuthorizationCode.create!(
|
auth_code = OidcAuthorizationCode.create!(
|
||||||
application: @application,
|
application: @application,
|
||||||
user: @user,
|
user: @user,
|
||||||
code: SecureRandom.urlsafe_base64(32),
|
|
||||||
redirect_uri: "http://localhost:4000/callback",
|
redirect_uri: "http://localhost:4000/callback",
|
||||||
scope: "openid profile",
|
scope: "openid profile",
|
||||||
code_challenge: code_challenge,
|
code_challenge: code_challenge,
|
||||||
@@ -63,7 +61,6 @@ class PkceAuthorizationCodeTest < ActiveSupport::TestCase
|
|||||||
auth_code = OidcAuthorizationCode.create!(
|
auth_code = OidcAuthorizationCode.create!(
|
||||||
application: @application,
|
application: @application,
|
||||||
user: @user,
|
user: @user,
|
||||||
code: SecureRandom.urlsafe_base64(32),
|
|
||||||
redirect_uri: "http://localhost:4000/callback",
|
redirect_uri: "http://localhost:4000/callback",
|
||||||
scope: "openid profile",
|
scope: "openid profile",
|
||||||
expires_at: 10.minutes.from_now
|
expires_at: 10.minutes.from_now
|
||||||
@@ -78,7 +75,6 @@ class PkceAuthorizationCodeTest < ActiveSupport::TestCase
|
|||||||
auth_code = OidcAuthorizationCode.new(
|
auth_code = OidcAuthorizationCode.new(
|
||||||
application: @application,
|
application: @application,
|
||||||
user: @user,
|
user: @user,
|
||||||
code: SecureRandom.urlsafe_base64(32),
|
|
||||||
redirect_uri: "http://localhost:4000/callback",
|
redirect_uri: "http://localhost:4000/callback",
|
||||||
scope: "openid profile",
|
scope: "openid profile",
|
||||||
code_challenge: "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk",
|
code_challenge: "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk",
|
||||||
@@ -93,7 +89,6 @@ class PkceAuthorizationCodeTest < ActiveSupport::TestCase
|
|||||||
auth_code = OidcAuthorizationCode.new(
|
auth_code = OidcAuthorizationCode.new(
|
||||||
application: @application,
|
application: @application,
|
||||||
user: @user,
|
user: @user,
|
||||||
code: SecureRandom.urlsafe_base64(32),
|
|
||||||
redirect_uri: "http://localhost:4000/callback",
|
redirect_uri: "http://localhost:4000/callback",
|
||||||
scope: "openid profile",
|
scope: "openid profile",
|
||||||
code_challenge: "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk",
|
code_challenge: "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk",
|
||||||
@@ -112,7 +107,6 @@ class PkceAuthorizationCodeTest < ActiveSupport::TestCase
|
|||||||
auth_code = OidcAuthorizationCode.new(
|
auth_code = OidcAuthorizationCode.new(
|
||||||
application: @application,
|
application: @application,
|
||||||
user: @user,
|
user: @user,
|
||||||
code: SecureRandom.urlsafe_base64(32),
|
|
||||||
redirect_uri: "http://localhost:4000/callback",
|
redirect_uri: "http://localhost:4000/callback",
|
||||||
scope: "openid profile",
|
scope: "openid profile",
|
||||||
code_challenge: valid_challenge,
|
code_challenge: valid_challenge,
|
||||||
@@ -130,7 +124,6 @@ class PkceAuthorizationCodeTest < ActiveSupport::TestCase
|
|||||||
auth_code = OidcAuthorizationCode.new(
|
auth_code = OidcAuthorizationCode.new(
|
||||||
application: @application,
|
application: @application,
|
||||||
user: @user,
|
user: @user,
|
||||||
code: SecureRandom.urlsafe_base64(32),
|
|
||||||
redirect_uri: "http://localhost:4000/callback",
|
redirect_uri: "http://localhost:4000/callback",
|
||||||
scope: "openid profile",
|
scope: "openid profile",
|
||||||
code_challenge: invalid_challenge,
|
code_challenge: invalid_challenge,
|
||||||
@@ -149,7 +142,6 @@ class PkceAuthorizationCodeTest < ActiveSupport::TestCase
|
|||||||
auth_code = OidcAuthorizationCode.new(
|
auth_code = OidcAuthorizationCode.new(
|
||||||
application: @application,
|
application: @application,
|
||||||
user: @user,
|
user: @user,
|
||||||
code: SecureRandom.urlsafe_base64(32),
|
|
||||||
redirect_uri: "http://localhost:4000/callback",
|
redirect_uri: "http://localhost:4000/callback",
|
||||||
scope: "openid profile",
|
scope: "openid profile",
|
||||||
code_challenge: short_challenge,
|
code_challenge: short_challenge,
|
||||||
@@ -165,7 +157,6 @@ class PkceAuthorizationCodeTest < ActiveSupport::TestCase
|
|||||||
auth_code = OidcAuthorizationCode.new(
|
auth_code = OidcAuthorizationCode.new(
|
||||||
application: @application,
|
application: @application,
|
||||||
user: @user,
|
user: @user,
|
||||||
code: SecureRandom.urlsafe_base64(32),
|
|
||||||
redirect_uri: "http://localhost:4000/callback",
|
redirect_uri: "http://localhost:4000/callback",
|
||||||
scope: "openid profile",
|
scope: "openid profile",
|
||||||
expires_at: 10.minutes.from_now
|
expires_at: 10.minutes.from_now
|
||||||
|
|||||||
@@ -495,4 +495,48 @@ class OidcJwtServiceTest < ActiveSupport::TestCase
|
|||||||
decoded = JWT.decode(token, nil, false).first
|
decoded = JWT.decode(token, nil, false).first
|
||||||
refute_includes decoded.keys, "at_hash", "Should not include at_hash when no access token"
|
refute_includes decoded.keys, "at_hash", "Should not include at_hash when no access token"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "should include auth_time when provided" do
|
||||||
|
auth_time = Time.now.to_i - 300 # 5 minutes ago
|
||||||
|
token = @service.generate_id_token(@user, @application, auth_time: auth_time)
|
||||||
|
|
||||||
|
decoded = JWT.decode(token, nil, false).first
|
||||||
|
assert_includes decoded.keys, "auth_time", "Should include auth_time claim"
|
||||||
|
assert_equal auth_time, decoded["auth_time"], "auth_time should match provided value"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should not include auth_time when not provided" do
|
||||||
|
token = @service.generate_id_token(@user, @application)
|
||||||
|
|
||||||
|
decoded = JWT.decode(token, nil, false).first
|
||||||
|
refute_includes decoded.keys, "auth_time", "Should not include auth_time when not provided"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "auth_time should be included in both authorization code and refresh token flows" do
|
||||||
|
auth_time = Time.now.to_i - 600 # 10 minutes ago
|
||||||
|
access_token = "test-access-token"
|
||||||
|
|
||||||
|
# Authorization code flow (with nonce)
|
||||||
|
token_with_auth_code = @service.generate_id_token(
|
||||||
|
@user,
|
||||||
|
@application,
|
||||||
|
nonce: "test-nonce",
|
||||||
|
access_token: access_token,
|
||||||
|
auth_time: auth_time
|
||||||
|
)
|
||||||
|
|
||||||
|
# Refresh token flow (no nonce)
|
||||||
|
token_with_refresh = @service.generate_id_token(
|
||||||
|
@user,
|
||||||
|
@application,
|
||||||
|
access_token: access_token,
|
||||||
|
auth_time: auth_time
|
||||||
|
)
|
||||||
|
|
||||||
|
decoded_auth_code = JWT.decode(token_with_auth_code, nil, false).first
|
||||||
|
decoded_refresh = JWT.decode(token_with_refresh, nil, false).first
|
||||||
|
|
||||||
|
assert_equal auth_time, decoded_auth_code["auth_time"], "auth_time should be in authorization code flow"
|
||||||
|
assert_equal auth_time, decoded_refresh["auth_time"], "auth_time should be in refresh token flow"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
Reference in New Issue
Block a user