StandardRB fixes
This commit is contained in:
@@ -19,16 +19,16 @@ class Application < ApplicationRecord
|
||||
has_many :oidc_user_consents, dependent: :destroy
|
||||
|
||||
validates :name, presence: true
|
||||
validates :slug, presence: true, uniqueness: { case_sensitive: false },
|
||||
format: { with: /\A[a-z0-9\-]+\z/, message: "only lowercase letters, numbers, and hyphens" }
|
||||
validates :slug, presence: true, uniqueness: {case_sensitive: false},
|
||||
format: {with: /\A[a-z0-9-]+\z/, message: "only lowercase letters, numbers, and hyphens"}
|
||||
validates :app_type, presence: true,
|
||||
inclusion: { in: %w[oidc forward_auth] }
|
||||
validates :client_id, uniqueness: { allow_nil: true }
|
||||
inclusion: {in: %w[oidc forward_auth]}
|
||||
validates :client_id, uniqueness: {allow_nil: true}
|
||||
validates :client_secret, presence: true, on: :create, if: -> { oidc? && confidential_client? }
|
||||
validates :domain_pattern, presence: true, uniqueness: { case_sensitive: false }, if: :forward_auth?
|
||||
validates :landing_url, format: { with: URI::regexp(%w[http https]), allow_nil: true, message: "must be a valid URL" }
|
||||
validates :domain_pattern, presence: true, uniqueness: {case_sensitive: false}, if: :forward_auth?
|
||||
validates :landing_url, format: {with: URI::RFC2396_PARSER.make_regexp(%w[http https]), allow_nil: true, message: "must be a valid URL"}
|
||||
validates :backchannel_logout_uri, format: {
|
||||
with: URI::regexp(%w[http https]),
|
||||
with: URI::RFC2396_PARSER.make_regexp(%w[http https]),
|
||||
allow_nil: true,
|
||||
message: "must be a valid HTTP or HTTPS URL"
|
||||
}
|
||||
@@ -38,9 +38,9 @@ class Application < ApplicationRecord
|
||||
validate :icon_validation, if: -> { icon.attached? }
|
||||
|
||||
# Token TTL validations (for OIDC apps)
|
||||
validates :access_token_ttl, numericality: { greater_than_or_equal_to: 300, less_than_or_equal_to: 86400 }, if: :oidc? # 5 min - 24 hours
|
||||
validates :refresh_token_ttl, numericality: { greater_than_or_equal_to: 86400, less_than_or_equal_to: 7776000 }, if: :oidc? # 1 day - 90 days
|
||||
validates :id_token_ttl, numericality: { greater_than_or_equal_to: 300, less_than_or_equal_to: 86400 }, if: :oidc? # 5 min - 24 hours
|
||||
validates :access_token_ttl, numericality: {greater_than_or_equal_to: 300, less_than_or_equal_to: 86400}, if: :oidc? # 5 min - 24 hours
|
||||
validates :refresh_token_ttl, numericality: {greater_than_or_equal_to: 86400, less_than_or_equal_to: 7776000}, if: :oidc? # 1 day - 90 days
|
||||
validates :id_token_ttl, numericality: {greater_than_or_equal_to: 300, less_than_or_equal_to: 86400}, if: :oidc? # 5 min - 24 hours
|
||||
|
||||
normalizes :slug, with: ->(slug) { slug.strip.downcase }
|
||||
normalizes :domain_pattern, with: ->(pattern) {
|
||||
@@ -56,11 +56,11 @@ class Application < ApplicationRecord
|
||||
|
||||
# Default header configuration for ForwardAuth
|
||||
DEFAULT_HEADERS = {
|
||||
user: 'X-Remote-User',
|
||||
email: 'X-Remote-Email',
|
||||
name: 'X-Remote-Name',
|
||||
groups: 'X-Remote-Groups',
|
||||
admin: 'X-Remote-Admin'
|
||||
user: "X-Remote-User",
|
||||
email: "X-Remote-Email",
|
||||
name: "X-Remote-Name",
|
||||
groups: "X-Remote-Groups",
|
||||
admin: "X-Remote-Admin"
|
||||
}.freeze
|
||||
|
||||
# Scopes
|
||||
@@ -135,8 +135,8 @@ class Application < ApplicationRecord
|
||||
def matches_domain?(domain)
|
||||
return false if domain.blank? || !forward_auth?
|
||||
|
||||
pattern = domain_pattern.gsub('.', '\.')
|
||||
pattern = pattern.gsub('*', '[^.]*')
|
||||
pattern = domain_pattern.gsub(".", '\.')
|
||||
pattern = pattern.gsub("*", "[^.]*")
|
||||
|
||||
regex = Regexp.new("^#{pattern}$", Regexp::IGNORECASE)
|
||||
regex.match?(domain.downcase)
|
||||
@@ -144,18 +144,18 @@ class Application < ApplicationRecord
|
||||
|
||||
# Policy determination based on user status (for ForwardAuth)
|
||||
def policy_for_user(user)
|
||||
return 'deny' unless active?
|
||||
return 'deny' unless user.active?
|
||||
return "deny" unless active?
|
||||
return "deny" unless user.active?
|
||||
|
||||
# If no groups specified, bypass authentication
|
||||
return 'bypass' if allowed_groups.empty?
|
||||
return "bypass" if allowed_groups.empty?
|
||||
|
||||
# If user is in allowed groups, determine auth level
|
||||
if user_allowed?(user)
|
||||
# Require 2FA if user has TOTP configured, otherwise one factor
|
||||
user.totp_enabled? ? 'two_factor' : 'one_factor'
|
||||
user.totp_enabled? ? "two_factor" : "one_factor"
|
||||
else
|
||||
'deny'
|
||||
"deny"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -197,7 +197,7 @@ class Application < ApplicationRecord
|
||||
def generate_new_client_secret!
|
||||
secret = SecureRandom.urlsafe_base64(48)
|
||||
self.client_secret = secret
|
||||
self.save!
|
||||
save!
|
||||
secret
|
||||
end
|
||||
|
||||
@@ -242,7 +242,7 @@ class Application < ApplicationRecord
|
||||
# (i.e., has valid, non-revoked tokens)
|
||||
def user_has_active_session?(user)
|
||||
oidc_access_tokens.where(user: user).valid.exists? ||
|
||||
oidc_refresh_tokens.where(user: user).valid.exists?
|
||||
oidc_refresh_tokens.where(user: user).valid.exists?
|
||||
end
|
||||
|
||||
private
|
||||
@@ -260,14 +260,14 @@ class Application < ApplicationRecord
|
||||
return unless icon.attached?
|
||||
|
||||
# Check content type
|
||||
allowed_types = ['image/png', 'image/jpg', 'image/jpeg', 'image/gif', 'image/svg+xml']
|
||||
allowed_types = ["image/png", "image/jpg", "image/jpeg", "image/gif", "image/svg+xml"]
|
||||
unless allowed_types.include?(icon.content_type)
|
||||
errors.add(:icon, 'must be a PNG, JPG, GIF, or SVG image')
|
||||
errors.add(:icon, "must be a PNG, JPG, GIF, or SVG image")
|
||||
end
|
||||
|
||||
# Check file size (2MB limit)
|
||||
if icon.blob.byte_size > 2.megabytes
|
||||
errors.add(:icon, 'must be less than 2MB')
|
||||
errors.add(:icon, "must be less than 2MB")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -302,8 +302,8 @@ class Application < ApplicationRecord
|
||||
|
||||
begin
|
||||
uri = URI.parse(backchannel_logout_uri)
|
||||
unless uri.scheme == 'https'
|
||||
errors.add(:backchannel_logout_uri, 'must use HTTPS in production')
|
||||
unless uri.scheme == "https"
|
||||
errors.add(:backchannel_logout_uri, "must use HTTPS in production")
|
||||
end
|
||||
rescue URI::InvalidURIError
|
||||
# Let the format validator handle invalid URIs
|
||||
|
||||
@@ -2,5 +2,5 @@ class ApplicationGroup < ApplicationRecord
|
||||
belongs_to :application
|
||||
belongs_to :group
|
||||
|
||||
validates :application_id, uniqueness: { scope: :group_id }
|
||||
validates :application_id, uniqueness: {scope: :group_id}
|
||||
end
|
||||
|
||||
@@ -9,7 +9,7 @@ class ApplicationUserClaim < ApplicationRecord
|
||||
groups
|
||||
].freeze
|
||||
|
||||
validates :user_id, uniqueness: { scope: :application_id }
|
||||
validates :user_id, uniqueness: {scope: :application_id}
|
||||
validate :no_reserved_claim_names
|
||||
|
||||
# Parse custom_claims JSON field
|
||||
@@ -25,7 +25,7 @@ class ApplicationUserClaim < ApplicationRecord
|
||||
|
||||
reserved_used = parsed_custom_claims.keys.map(&:to_s) & RESERVED_CLAIMS
|
||||
if reserved_used.any?
|
||||
errors.add(:custom_claims, "cannot override reserved OIDC claims: #{reserved_used.join(', ')}")
|
||||
errors.add(:custom_claims, "cannot override reserved OIDC claims: #{reserved_used.join(", ")}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -11,7 +11,7 @@ class Group < ApplicationRecord
|
||||
groups
|
||||
].freeze
|
||||
|
||||
validates :name, presence: true, uniqueness: { case_sensitive: false }
|
||||
validates :name, presence: true, uniqueness: {case_sensitive: false}
|
||||
normalizes :name, with: ->(name) { name.strip.downcase }
|
||||
validate :no_reserved_claim_names
|
||||
|
||||
@@ -28,7 +28,7 @@ class Group < ApplicationRecord
|
||||
|
||||
reserved_used = parsed_custom_claims.keys.map(&:to_s) & RESERVED_CLAIMS
|
||||
if reserved_used.any?
|
||||
errors.add(:custom_claims, "cannot override reserved OIDC claims: #{reserved_used.join(', ')}")
|
||||
errors.add(:custom_claims, "cannot override reserved OIDC claims: #{reserved_used.join(", ")}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -25,7 +25,7 @@ class OidcAccessToken < ApplicationRecord
|
||||
|
||||
# Compute HMAC for token lookup
|
||||
def self.compute_token_hmac(plaintext_token)
|
||||
OpenSSL::HMAC.hexdigest('SHA256', TokenHmac::KEY, plaintext_token)
|
||||
OpenSSL::HMAC.hexdigest("SHA256", TokenHmac::KEY, plaintext_token)
|
||||
end
|
||||
|
||||
def expired?
|
||||
|
||||
@@ -9,7 +9,7 @@ class OidcAuthorizationCode < ApplicationRecord
|
||||
|
||||
validates :code_hmac, presence: true, uniqueness: true
|
||||
validates :redirect_uri, presence: true
|
||||
validates :code_challenge_method, inclusion: { in: %w[plain S256], allow_nil: 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) }
|
||||
@@ -25,7 +25,7 @@ class OidcAuthorizationCode < ApplicationRecord
|
||||
|
||||
# Compute HMAC for code lookup
|
||||
def self.compute_code_hmac(plaintext_code)
|
||||
OpenSSL::HMAC.hexdigest('SHA256', TokenHmac::KEY, plaintext_code)
|
||||
OpenSSL::HMAC.hexdigest("SHA256", TokenHmac::KEY, plaintext_code)
|
||||
end
|
||||
|
||||
def expired?
|
||||
|
||||
@@ -29,7 +29,7 @@ class OidcRefreshToken < ApplicationRecord
|
||||
|
||||
# Compute HMAC for token lookup
|
||||
def self.compute_token_hmac(plaintext_token)
|
||||
OpenSSL::HMAC.hexdigest('SHA256', TokenHmac::KEY, plaintext_token)
|
||||
OpenSSL::HMAC.hexdigest("SHA256", TokenHmac::KEY, plaintext_token)
|
||||
end
|
||||
|
||||
def expired?
|
||||
|
||||
@@ -3,19 +3,19 @@ class OidcUserConsent < ApplicationRecord
|
||||
belongs_to :application
|
||||
|
||||
validates :user, :application, :scopes_granted, :granted_at, presence: true
|
||||
validates :user_id, uniqueness: { scope: :application_id }
|
||||
validates :user_id, uniqueness: {scope: :application_id}
|
||||
|
||||
before_validation :set_granted_at, on: :create
|
||||
before_validation :set_sid, on: :create
|
||||
|
||||
# Parse scopes_granted into an array
|
||||
def scopes
|
||||
scopes_granted.split(' ')
|
||||
scopes_granted.split(" ")
|
||||
end
|
||||
|
||||
# Set scopes from an array
|
||||
def scopes=(scope_array)
|
||||
self.scopes_granted = Array(scope_array).uniq.join(' ')
|
||||
self.scopes_granted = Array(scope_array).uniq.join(" ")
|
||||
end
|
||||
|
||||
# Check if this consent covers the requested scopes
|
||||
@@ -31,18 +31,18 @@ class OidcUserConsent < ApplicationRecord
|
||||
def formatted_scopes
|
||||
scopes.map do |scope|
|
||||
case scope
|
||||
when 'openid'
|
||||
'Basic authentication'
|
||||
when 'profile'
|
||||
'Profile information'
|
||||
when 'email'
|
||||
'Email address'
|
||||
when 'groups'
|
||||
'Group membership'
|
||||
when "openid"
|
||||
"Basic authentication"
|
||||
when "profile"
|
||||
"Profile information"
|
||||
when "email"
|
||||
"Email address"
|
||||
when "groups"
|
||||
"Group membership"
|
||||
else
|
||||
scope.humanize
|
||||
end
|
||||
end.join(', ')
|
||||
end.join(", ")
|
||||
end
|
||||
|
||||
# Find consent by SID
|
||||
|
||||
@@ -29,16 +29,16 @@ class User < ApplicationRecord
|
||||
groups
|
||||
].freeze
|
||||
|
||||
validates :email_address, presence: true, uniqueness: { case_sensitive: false },
|
||||
format: { with: URI::MailTo::EMAIL_REGEXP }
|
||||
validates :username, uniqueness: { case_sensitive: false }, allow_nil: true,
|
||||
format: { with: /\A[a-zA-Z0-9_-]+\z/, message: "can only contain letters, numbers, underscores, and hyphens" },
|
||||
length: { minimum: 2, maximum: 30 }
|
||||
validates :password, length: { minimum: 8 }, allow_nil: true
|
||||
validates :email_address, presence: true, uniqueness: {case_sensitive: false},
|
||||
format: {with: URI::MailTo::EMAIL_REGEXP}
|
||||
validates :username, uniqueness: {case_sensitive: false}, allow_nil: true,
|
||||
format: {with: /\A[a-zA-Z0-9_-]+\z/, message: "can only contain letters, numbers, underscores, and hyphens"},
|
||||
length: {minimum: 2, maximum: 30}
|
||||
validates :password, length: {minimum: 8}, allow_nil: true
|
||||
validate :no_reserved_claim_names
|
||||
|
||||
# Enum - automatically creates scopes (User.active, User.disabled, etc.)
|
||||
enum :status, { active: 0, disabled: 1, pending_invitation: 2 }
|
||||
enum :status, {active: 0, disabled: 1, pending_invitation: 2}
|
||||
|
||||
# Scopes
|
||||
scope :admins, -> { where(admin: true) }
|
||||
@@ -122,12 +122,7 @@ class User < ApplicationRecord
|
||||
cache_key = "backup_code_failed_attempts_#{id}"
|
||||
attempts = Rails.cache.read(cache_key) || 0
|
||||
|
||||
if attempts >= 5 # Allow max 5 failed attempts per hour
|
||||
true
|
||||
else
|
||||
# Don't increment here - increment only on failed attempts
|
||||
false
|
||||
end
|
||||
attempts >= 5
|
||||
end
|
||||
|
||||
# Increment failed attempt counter
|
||||
@@ -231,7 +226,7 @@ class User < ApplicationRecord
|
||||
|
||||
reserved_used = parsed_custom_claims.keys.map(&:to_s) & RESERVED_CLAIMS
|
||||
if reserved_used.any?
|
||||
errors.add(:custom_claims, "cannot override reserved OIDC claims: #{reserved_used.join(', ')}")
|
||||
errors.add(:custom_claims, "cannot override reserved OIDC claims: #{reserved_used.join(", ")}")
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@ class UserGroup < ApplicationRecord
|
||||
belongs_to :user
|
||||
belongs_to :group
|
||||
|
||||
validates :user_id, uniqueness: { scope: :group_id }
|
||||
validates :user_id, uniqueness: {scope: :group_id}
|
||||
end
|
||||
|
||||
@@ -4,9 +4,9 @@ class WebauthnCredential < ApplicationRecord
|
||||
# Validations
|
||||
validates :external_id, presence: true, uniqueness: true
|
||||
validates :public_key, presence: true
|
||||
validates :sign_count, presence: true, numericality: { greater_than_or_equal_to: 0, only_integer: true }
|
||||
validates :sign_count, presence: true, numericality: {greater_than_or_equal_to: 0, only_integer: true}
|
||||
validates :nickname, presence: true
|
||||
validates :authenticator_type, inclusion: { in: %w[platform cross-platform] }
|
||||
validates :authenticator_type, inclusion: {in: %w[platform cross-platform]}
|
||||
|
||||
# Scopes for querying
|
||||
scope :active, -> { where(nil) } # All credentials are active (we can add revoked_at later if needed)
|
||||
@@ -84,13 +84,13 @@ class WebauthnCredential < ApplicationRecord
|
||||
days = hours / 24
|
||||
|
||||
if days > 0
|
||||
"#{days.floor} day#{'s' if days > 1} ago"
|
||||
"#{days.floor} day#{"s" if days > 1} ago"
|
||||
elsif hours > 0
|
||||
"#{hours.floor} hour#{'s' if hours > 1} ago"
|
||||
"#{hours.floor} hour#{"s" if hours > 1} ago"
|
||||
elsif minutes > 0
|
||||
"#{minutes.floor} minute#{'s' if minutes > 1} ago"
|
||||
"#{minutes.floor} minute#{"s" if minutes > 1} ago"
|
||||
else
|
||||
"Just now"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user