Files
sure/app/models/mobile_device.rb
Josh Pigford 9336719242 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>
2025-06-18 08:20:22 -05:00

56 lines
1.5 KiB
Ruby

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