84 lines
2.3 KiB
Ruby
84 lines
2.3 KiB
Ruby
class OidcAccessToken < ApplicationRecord
|
|
belongs_to :application
|
|
belongs_to :user
|
|
has_many :oidc_refresh_tokens, dependent: :destroy
|
|
|
|
before_validation :generate_token, on: :create
|
|
before_validation :set_expiry, on: :create
|
|
|
|
validates :token, uniqueness: true, presence: true
|
|
|
|
scope :valid, -> { where("expires_at > ?", Time.current).where(revoked_at: nil) }
|
|
scope :expired, -> { where("expires_at <= ?", Time.current) }
|
|
scope :revoked, -> { where.not(revoked_at: nil) }
|
|
scope :active, -> { valid }
|
|
|
|
attr_accessor :plaintext_token # Store plaintext temporarily for returning to client
|
|
|
|
def expired?
|
|
expires_at <= Time.current
|
|
end
|
|
|
|
def revoked?
|
|
revoked_at.present?
|
|
end
|
|
|
|
def active?
|
|
!expired? && !revoked?
|
|
end
|
|
|
|
def revoke!
|
|
update!(revoked_at: Time.current)
|
|
# Also revoke associated refresh tokens
|
|
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
|
|
|
|
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
|
|
end
|