Add a token prefix column, generate the token_prefix and the token_digest, removing the plaintext token from use.

This commit is contained in:
Dan Milne
2025-12-30 09:45:16 +11:00
parent 0761c424c1
commit 99c3ac905f
6 changed files with 118 additions and 73 deletions

View File

@@ -1,10 +1,12 @@
class OidcRefreshToken < ApplicationRecord
include TokenPrefixable
belongs_to :application
belongs_to :user
belongs_to :oidc_access_token
has_many :oidc_access_tokens, foreign_key: :oidc_access_token_id, dependent: :nullify
before_validation :generate_token, on: :create
before_validation :generate_token_with_prefix, on: :create
before_validation :set_expiry, on: :create
before_validation :set_token_family_id, on: :create
@@ -43,37 +45,11 @@ class OidcRefreshToken < ApplicationRecord
OidcRefreshToken.in_family(token_family_id).update_all(revoked_at: Time.current)
end
# Verify a plaintext token against the stored digest
def self.find_by_token(plaintext_token)
return nil if plaintext_token.blank?
# Try to find tokens that could match (we can't search by hash directly)
# This is less efficient but necessary with BCrypt
# In production, you might want to add a token prefix or other optimization
all.find do |refresh_token|
refresh_token.token_matches?(plaintext_token)
end
end
def token_matches?(plaintext_token)
return false if plaintext_token.blank? || token_digest.blank?
BCrypt::Password.new(token_digest) == plaintext_token
rescue BCrypt::Errors::InvalidHash
false
end
# find_by_token, token_matches?, and generate_token_with_prefix
# are now provided by TokenPrefixable concern
private
def generate_token
# Generate a secure random token
plaintext = SecureRandom.urlsafe_base64(48)
self.token = plaintext # Store temporarily for returning to client
# Hash it with BCrypt for storage
self.token_digest = BCrypt::Password.create(plaintext)
end
def set_expiry
# Use application's configured refresh token TTL
self.expires_at ||= application.refresh_token_expiry