67 lines
1.8 KiB
Ruby
67 lines
1.8 KiB
Ruby
class OidcAuthorizationCode < ApplicationRecord
|
|
belongs_to :application
|
|
belongs_to :user
|
|
|
|
attr_accessor :plaintext_code
|
|
|
|
before_validation :generate_code, on: :create
|
|
before_validation :set_expiry, on: :create
|
|
|
|
validates :code, presence: true, uniqueness: true
|
|
validates :redirect_uri, presence: true
|
|
validates :code_challenge_method, inclusion: { in: %w[plain S256], allow_nil: true }
|
|
validate :validate_code_challenge_format, if: -> { code_challenge.present? }
|
|
|
|
scope :valid, -> { where(used: false).where("expires_at > ?", Time.current) }
|
|
scope :expired, -> { where("expires_at <= ?", Time.current) }
|
|
|
|
# Find authorization code by plaintext code using HMAC verification
|
|
def self.find_by_plaintext(plaintext_code)
|
|
return nil if plaintext_code.blank?
|
|
|
|
code_hmac = compute_code_hmac(plaintext_code)
|
|
find_by(code: code_hmac)
|
|
end
|
|
|
|
# Compute HMAC for code lookup
|
|
def self.compute_code_hmac(plaintext_code)
|
|
OpenSSL::HMAC.hexdigest('SHA256', TokenHmac::KEY, plaintext_code)
|
|
end
|
|
|
|
def expired?
|
|
expires_at <= Time.current
|
|
end
|
|
|
|
def usable?
|
|
!used? && !expired?
|
|
end
|
|
|
|
def consume!
|
|
update!(used: true)
|
|
end
|
|
|
|
def uses_pkce?
|
|
code_challenge.present?
|
|
end
|
|
|
|
private
|
|
|
|
def generate_code
|
|
# Generate random plaintext code
|
|
self.plaintext_code ||= SecureRandom.urlsafe_base64(32)
|
|
# Store HMAC in database (not plaintext)
|
|
self.code ||= self.class.compute_code_hmac(plaintext_code)
|
|
end
|
|
|
|
def set_expiry
|
|
self.expires_at ||= 10.minutes.from_now
|
|
end
|
|
|
|
def validate_code_challenge_format
|
|
# PKCE code challenge should be base64url-encoded, 43-128 characters
|
|
unless code_challenge.match?(/\A[A-Za-z0-9\-_]{43,128}\z/)
|
|
errors.add(:code_challenge, "must be 43-128 characters of base64url encoding")
|
|
end
|
|
end
|
|
end
|