> ## Documentation Index
> Fetch the complete documentation index at: https://docs.evomarketing.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Authentication

> JWT authentication and Better Auth integration

EVO Dialed uses **Better Auth** for authentication, with the Rails backend validating JWT tokens.

## Authentication flow

```
┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│   Client    │────▶│ Better Auth │────▶│   Backend   │
│ (iOS/Web)   │     │   (Next.js) │     │   (Rails)   │
└─────────────┘     └─────────────┘     └─────────────┘
      │                    │                   │
      │  1. Login request  │                   │
      │───────────────────▶│                   │
      │                    │  2. Verify creds  │
      │                    │──────────────────▶│
      │                    │  3. User data     │
      │                    │◀──────────────────│
      │  4. JWT token      │                   │
      │◀───────────────────│                   │
      │                    │                   │
      │  5. API request with JWT               │
      │───────────────────────────────────────▶│
      │  6. Response                           │
      │◀───────────────────────────────────────│
```

## Better Auth

Better Auth handles:

* Magic link email login
* Password authentication
* Google OAuth
* Apple Sign-In
* Session management
* JWT token issuance

### Sign-in methods

| Method         | Description                   |
| -------------- | ----------------------------- |
| Email/password | Traditional username/password |
| Magic link     | Passwordless email login      |
| Google OAuth   | Sign in with Google           |
| Apple OAuth    | Sign in with Apple            |

### User sync

When a user authenticates, Better Auth syncs to Rails via:

```
POST /api/auth/sync-user
{
  "user_id": "better_auth_id",
  "email": "user@example.com",
  "name": "User Name"
}
```

## JWT validation

The Rails backend validates JWT tokens in requests.

### JwtAuthenticatable concern

**File:** `app/controllers/concerns/jwt_authenticatable.rb`

```ruby theme={null}
module JwtAuthenticatable
  extend ActiveSupport::Concern

  included do
    before_action :authenticate_jwt!
  end

  def authenticate_jwt!
    token = request.headers["Authorization"]&.split(" ")&.last
    raise UnauthorizedError unless token
    
    @jwt_claims = BetterAuth::JwtVerifier.verify(token)
  end

  def current_user_id
    @jwt_claims["sub"]
  end

  def current_user
    @current_user ||= User.find_by(auth_user_id: current_user_id)
  end
end
```

### User types

| Type    | Value     | Description     |
| ------- | --------- | --------------- |
| Admin   | `evo`     | EVO team member |
| Creator | `creator` | Content creator |

### Requiring admin access

```ruby theme={null}
class Api::Internal::BaseController < ApplicationController
  include JwtAuthenticatable
  before_action :require_evo!

  def require_evo!
    raise ForbiddenError unless current_user&.evo?
  end
end
```

## User identity

<Warning>
  **Critical:** `user_id` across \~46 tables is a Better Auth ID (string, 64 chars), NOT `users.id`. Always join via `users.auth_user_id`.
</Warning>

### Correct joins

```ruby theme={null}
# Correct - join on auth_user_id
User.find_by(auth_user_id: user_id)

# Wrong - this looks up wrong user
User.find(user_id)
```

### Foreign key pattern

Tables reference users via Better Auth ID:

```ruby theme={null}
class CampaignSubmission < ApplicationRecord
  belongs_to :user, 
    foreign_key: :user_id, 
    primary_key: :auth_user_id
end
```

## iOS authentication

The iOS app uses cookies and tokens:

```swift theme={null}
final class AuthSession: ObservableObject {
    enum State { 
        case booting, signedOut, signedIn, blocked(String) 
    }
    @Published private(set) var state: State
}
```

### Token refresh

* Tokens cached in `TokenStore` (UserDefaults)
* Auto-refresh when token expires (60s margin)
* 401 responses trigger forced refresh + retry

## Session security

### Cookie configuration

Better Auth sessions use secure cookies:

* `HttpOnly` - Not accessible to JavaScript
* `Secure` - HTTPS only in production
* `SameSite=Lax` - CSRF protection

### Token expiration

* Access tokens: Short-lived (1 hour)
* Refresh tokens: Long-lived (30 days)
* Session: Extended on activity

## Error handling

| Error           | HTTP Status | Meaning                   |
| --------------- | ----------- | ------------------------- |
| Missing token   | 401         | No Authorization header   |
| Invalid token   | 401         | Token verification failed |
| Expired token   | 401         | Token has expired         |
| Wrong user type | 403         | User lacks required role  |

## Testing authentication

```ruby theme={null}
class Api::Internal::BrandsControllerTest < ActionController::TestCase
  include JwtTestStub

  setup do
    @admin = create_evo_user
    stub_jwt_for(@admin)
  end

  test "requires admin authentication" do
    get :index
    assert_response :success
  end
end
```
