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,12 +1,15 @@
class OidcAccessToken < ApplicationRecord
include TokenPrefixable
belongs_to :application
belongs_to :user
has_many :oidc_refresh_tokens, dependent: :destroy
before_validation :generate_token, on: :create
before_validation :generate_token_with_prefix, on: :create
before_validation :set_expiry, on: :create
validates :token, uniqueness: true, presence: true
validates :token_digest, presence: true
validates :token_prefix, presence: true
scope :valid, -> { where("expires_at > ?", Time.current).where(revoked_at: nil) }
scope :expired, -> { where("expires_at <= ?", Time.current) }
@@ -33,50 +36,11 @@ class OidcAccessToken < ApplicationRecord
oidc_refresh_tokens.each(&:revoke!)
end
# Check if a plaintext token matches the hashed token
def token_matches?(plaintext_token)
return false if plaintext_token.blank?
# Use BCrypt to compare if token_digest exists
if token_digest.present?
BCrypt::Password.new(token_digest) == plaintext_token
# Fall back to direct comparison for backward compatibility
elsif token.present?
token == plaintext_token
else
false
end
end
# Find by token (validates and checks if revoked)
def self.find_by_token(plaintext_token)
return nil if plaintext_token.blank?
# Find all non-revoked, non-expired tokens
valid.find_each do |access_token|
# Use BCrypt to compare (if token_digest exists) or direct comparison
if access_token.token_digest.present?
return access_token if BCrypt::Password.new(access_token.token_digest) == plaintext_token
elsif access_token.token == plaintext_token
return access_token
end
end
nil
end
# find_by_token, token_matches?, and generate_token_with_prefix
# are now provided by TokenPrefixable concern
private
def generate_token
return if token.present?
# Generate opaque access token
plaintext = SecureRandom.urlsafe_base64(48)
self.plaintext_token = plaintext # Store temporarily for returning to client
self.token_digest = BCrypt::Password.create(plaintext)
# Keep token column for backward compatibility during migration
self.token = plaintext
end
def set_expiry
self.expires_at ||= application.access_token_expiry
end