Mobile App Authentication with JWT

Overview

Unlike web browsers that use secure HttpOnly cookies, mobile applications (iOS/Android) represent stateless clients that do not support standard cookie management. To support mobile clients, the system implements a Hybrid Auth Strategy supporting both Cookies (for web) and JWT Bearer Tokens (for mobile).

Authentication Flow

1. Sign In

Mobile clients should use the standard sign-in endpoints provided by the Better Auth service. Endpoint: POST /api/auth/sign-in-email Request Body:
{
  "email": "user@example.com",
  "password": "yourpassword"
}
Response:
{
  "user": { "id": "...", "email": "..." },
  "session": {
    "token": "header.payload.signature",
    "refreshToken": "...",
    "expiresAt": "2026-04-13T16:00:00Z"
  }
}
The session.token is the JWT you must use for all subsequent API requests.

2. Token Storage

Tokens must be stored in secure local storage provided by the operating system:

3. Authenticated Requests

Include the JWT in the Authorization header of every request.
Authorization: Bearer <your_jwt_token>

4. Token Refresh

JWT tokens typically expire in 1 hour. Use the refresh token to obtain a new JWT before it expires. Endpoint: POST /api/auth/refresh Request Body:
{
  "refreshToken": "<your_refresh_token>"
}

Implementation Examples

iOS (Swift)

let url = URL(string: "http://api.gremlin.com/api/admin/users")!
var request = URLRequest(url: url)
request.httpMethod = "GET"

// Retrieve token from Keychain
if let token = KeychainHelper.standard.read(service: "auth_token", account: "gremlin") {
    request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
}

let task = URLSession.shared.dataTask(with: request) { data, response, error in
    // Handle response
}
task.resume()

Android (Kotlin)

val client = OkHttpClient()
val request = Request.Builder()
    .url("http://api.gremlin.com/api/admin/users")
    .addHeader("Authorization", "Bearer $jwtToken")
    .build()

client.newCall(request).enqueue(object : Callback {
    override fun onFailure(call: Call, e: IOException) { /* Handle error */ }
    override fun onResponse(call: Call, response: Response) { /* Handle success */ }
})

Security Best Practices

  • Never store tokens in standard SharedPreferences or UserDefaults.
  • Never log full Authorization headers.
  • Always use HTTPS for all communication.
  • Revoke Tokens locally and on the server when the user logs out.

Error Handling

  • 401 Unauthorized: Token is missing, malformed, or expired. Trigger the refresh token flow or re-authentication.
  • 403 Forbidden: User is authenticated but inactive or lacks required roles. Check is_active status or specific role requirements.