mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-08-03 04:25:21 +02:00
Add secure OAuth2-based mobile authentication
- Replace API keys with OAuth2 tokens for mobile apps - Add device tracking and management for mobile sessions - Implement 30-day token expiration with refresh tokens - Add MFA/2FA support for mobile login - Create dedicated auth endpoints (signup/login/refresh) - Skip CSRF protection for API endpoints - Return plaintext tokens (not hashed) in responses - Track devices with unique IDs and metadata - Enable seamless native mobile experience without OAuth redirects This provides enterprise-grade security for the iOS/Android apps while maintaining a completely native authentication flow. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
cba0bdf0e2
commit
9336719242
15 changed files with 761 additions and 6 deletions
|
@ -4,12 +4,16 @@ class ApiKey < ApplicationRecord
|
|||
# Use Rails built-in encryption for secure storage
|
||||
encrypts :display_key, deterministic: true
|
||||
|
||||
# Constants
|
||||
SOURCES = [ "web", "mobile" ].freeze
|
||||
|
||||
# Validations
|
||||
validates :display_key, presence: true, uniqueness: true
|
||||
validates :name, presence: true
|
||||
validates :scopes, presence: true
|
||||
validates :source, presence: true, inclusion: { in: SOURCES }
|
||||
validate :scopes_not_empty
|
||||
validate :one_active_key_per_user, on: :create
|
||||
validate :one_active_key_per_user_per_source, on: :create
|
||||
|
||||
# Callbacks
|
||||
before_validation :set_display_key
|
||||
|
@ -82,9 +86,9 @@ class ApiKey < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
def one_active_key_per_user
|
||||
if user&.api_keys&.active&.where&.not(id: id)&.exists?
|
||||
errors.add(:user, "can only have one active API key")
|
||||
def one_active_key_per_user_per_source
|
||||
if user&.api_keys&.active&.where(source: source)&.where&.not(id: id)&.exists?
|
||||
errors.add(:user, "can only have one active API key per source (#{source})")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
55
app/models/mobile_device.rb
Normal file
55
app/models/mobile_device.rb
Normal file
|
@ -0,0 +1,55 @@
|
|||
class MobileDevice < ApplicationRecord
|
||||
belongs_to :user
|
||||
belongs_to :oauth_application, class_name: "Doorkeeper::Application", optional: true
|
||||
|
||||
validates :device_id, presence: true, uniqueness: { scope: :user_id }
|
||||
validates :device_name, presence: true
|
||||
validates :device_type, presence: true, inclusion: { in: %w[ios android] }
|
||||
|
||||
before_validation :set_last_seen_at, on: :create
|
||||
|
||||
scope :active, -> { where("last_seen_at > ?", 90.days.ago) }
|
||||
|
||||
def active?
|
||||
last_seen_at > 90.days.ago
|
||||
end
|
||||
|
||||
def update_last_seen!
|
||||
update_column(:last_seen_at, Time.current)
|
||||
end
|
||||
|
||||
def create_oauth_application!
|
||||
return oauth_application if oauth_application.present?
|
||||
|
||||
app = Doorkeeper::Application.create!(
|
||||
name: "Mobile App - #{device_id}",
|
||||
redirect_uri: "maybe://oauth/callback", # Custom scheme for mobile
|
||||
scopes: "read_write", # Use the configured scope
|
||||
confidential: false # Public client for mobile
|
||||
)
|
||||
|
||||
# Store the association
|
||||
update!(oauth_application: app)
|
||||
app
|
||||
end
|
||||
|
||||
def active_tokens
|
||||
return Doorkeeper::AccessToken.none unless oauth_application
|
||||
|
||||
Doorkeeper::AccessToken
|
||||
.where(application: oauth_application)
|
||||
.where(resource_owner_id: user_id)
|
||||
.where(revoked_at: nil)
|
||||
.where("expires_in IS NULL OR created_at + expires_in * interval '1 second' > ?", Time.current)
|
||||
end
|
||||
|
||||
def revoke_all_tokens!
|
||||
active_tokens.update_all(revoked_at: Time.current)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_last_seen_at
|
||||
self.last_seen_at ||= Time.current
|
||||
end
|
||||
end
|
|
@ -6,6 +6,7 @@ class User < ApplicationRecord
|
|||
has_many :sessions, dependent: :destroy
|
||||
has_many :chats, dependent: :destroy
|
||||
has_many :api_keys, dependent: :destroy
|
||||
has_many :mobile_devices, dependent: :destroy
|
||||
has_many :invitations, foreign_key: :inviter_id, dependent: :destroy
|
||||
has_many :impersonator_support_sessions, class_name: "ImpersonationSession", foreign_key: :impersonator_id, dependent: :destroy
|
||||
has_many :impersonated_support_sessions, class_name: "ImpersonationSession", foreign_key: :impersonated_id, dependent: :destroy
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue