auth/API_INTEGRATION.md

14 KiB

Farm Auth Service - API Integration Guide

Quick reference for integrating the Farm Auth Service into Kotlin mobile applications.

Base URL

http://localhost:3000  (development)
https://your-domain.com (production)

Authentication Flow

  1. Request OTP → User enters phone number
  2. Verify OTP → User enters code, receives tokens
  3. Use Access Token → Include in Authorization header for protected endpoints
  4. Refresh Token → When access token expires, get new tokens
  5. Logout → Revoke refresh token

API Endpoints

1. Request OTP

Endpoint: POST /auth/request-otp

Request:

{
  "phone_number": "+919876543210"
}

Response (200):

{
  "ok": true
}

Kotlin Example:

data class RequestOtpRequest(val phone_number: String)
data class RequestOtpResponse(val ok: Boolean)

suspend fun requestOtp(phoneNumber: String): Result<RequestOtpResponse> {
    val request = RequestOtpRequest(phoneNumber)
    return apiClient.post("/auth/request-otp", request)
}

Note: Phone numbers are auto-normalized:

  • 9876543210+919876543210 (10-digit assumed as Indian)
  • +919876543210+919876543210 (already formatted)

2. Verify OTP

Endpoint: POST /auth/verify-otp

Request:

{
  "phone_number": "+919876543210",
  "code": "123456",
  "device_id": "android-installation-id-123",
  "device_info": {
    "platform": "android",
    "model": "Samsung SM-M326B",
    "os_version": "Android 14",
    "app_version": "1.0.0",
    "language_code": "en-IN",
    "timezone": "Asia/Kolkata"
  }
}

Response (200):

{
  "user": {
    "id": "uuid-here",
    "phone_number": "+919876543210",
    "name": null,
    "role": "user",
    "user_type": null
  },
  "access_token": "eyJhbGc...",
  "refresh_token": "eyJhbGc...",
  "needs_profile": true
}

Kotlin Example:

data class DeviceInfo(
    val platform: String,
    val model: String? = null,
    val os_version: String? = null,
    val app_version: String? = null,
    val language_code: String? = null,
    val timezone: String? = null
)

data class VerifyOtpRequest(
    val phone_number: String,
    val code: String,
    val device_id: String,
    val device_info: DeviceInfo? = null
)

data class User(
    val id: String,
    val phone_number: String,
    val name: String?,
    val role: String,
    val user_type: String?
)

data class VerifyOtpResponse(
    val user: User,
    val access_token: String,
    val refresh_token: String,
    val needs_profile: Boolean
)

suspend fun verifyOtp(
    phoneNumber: String,
    code: String,
    deviceId: String,
    deviceInfo: DeviceInfo? = null
): Result<VerifyOtpResponse> {
    val request = VerifyOtpRequest(phoneNumber, code, deviceId, deviceInfo)
    return apiClient.post("/auth/verify-otp", request)
}

Error (400):

{
  "error": "Invalid or expired OTP"
}

3. Refresh Token

Endpoint: POST /auth/refresh

Request:

{
  "refresh_token": "eyJhbGc..."
}

Response (200):

{
  "access_token": "eyJhbGc...",
  "refresh_token": "eyJhbGc..."
}

Kotlin Example:

data class RefreshRequest(val refresh_token: String)
data class RefreshResponse(val access_token: String, val refresh_token: String)

suspend fun refreshToken(refreshToken: String): Result<RefreshResponse> {
    val request = RefreshRequest(refreshToken)
    return apiClient.post("/auth/refresh", request)
}

Note: Refresh tokens rotate on each use. Always save the new refresh_token.


4. Update Profile

Endpoint: PUT /users/me

Headers:

Authorization: Bearer <access_token>

Request:

{
  "name": "John Doe",
  "user_type": "seller"
}

Response (200):

{
  "id": "uuid-here",
  "phone_number": "+919876543210",
  "name": "John Doe",
  "role": "user",
  "user_type": "seller"
}

user_type values: seller, buyer, service_provider

Kotlin Example:

data class UpdateProfileRequest(val name: String, val user_type: String)
data class UpdateProfileResponse(
    val id: String,
    val phone_number: String,
    val name: String?,
    val role: String,
    val user_type: String?
)

suspend fun updateProfile(
    name: String,
    userType: String,
    accessToken: String
): Result<UpdateProfileResponse> {
    val request = UpdateProfileRequest(name, userType)
    return apiClient.put("/users/me", request, accessToken)
}

5. Logout

Endpoint: POST /auth/logout

Request:

{
  "refresh_token": "eyJhbGc..."
}

Response (200):

{
  "ok": true
}

Kotlin Example:

suspend fun logout(refreshToken: String): Result<Unit> {
    val request = RefreshRequest(refreshToken)
    return apiClient.post("/auth/logout", request)
}

Complete Kotlin Integration Example

1. API Client Setup

import kotlinx.coroutines.*
import kotlinx.serialization.*
import kotlinx.serialization.json.*
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*

class AuthApiClient(private val baseUrl: String) {
    private val client = HttpClient {
        install(JsonFeature) {
            serializer = KotlinxSerializationJson()
        }
    }

    suspend fun requestOtp(phoneNumber: String): Result<RequestOtpResponse> {
        return try {
            val response = client.post("$baseUrl/auth/request-otp") {
                contentType(ContentType.Application.Json)
                setBody(JsonObject(mapOf("phone_number" to JsonPrimitive(phoneNumber))))
            }
            Result.success(response.body())
        } catch (e: Exception) {
            Result.failure(e)
        }
    }

    suspend fun verifyOtp(
        phoneNumber: String,
        code: String,
        deviceId: String
    ): Result<VerifyOtpResponse> {
        return try {
            val request = VerifyOtpRequest(phoneNumber, code, deviceId, getDeviceInfo())
            val response = client.post("$baseUrl/auth/verify-otp") {
                contentType(ContentType.Application.Json)
                setBody(request)
            }
            Result.success(response.body())
        } catch (e: Exception) {
            Result.failure(e)
        }
    }

    suspend fun refreshToken(refreshToken: String): Result<RefreshResponse> {
        return try {
            val request = RefreshRequest(refreshToken)
            val response = client.post("$baseUrl/auth/refresh") {
                contentType(ContentType.Application.Json)
                setBody(request)
            }
            Result.success(response.body())
        } catch (e: Exception) {
            Result.failure(e)
        }
    }

    suspend fun updateProfile(
        name: String,
        userType: String,
        accessToken: String
    ): Result<UpdateProfileResponse> {
        return try {
            val request = UpdateProfileRequest(name, userType)
            val response = client.put("$baseUrl/users/me") {
                contentType(ContentType.Application.Json)
                header("Authorization", "Bearer $accessToken")
                setBody(request)
            }
            Result.success(response.body())
        } catch (e: Exception) {
            Result.failure(e)
        }
    }

    private fun getDeviceInfo(): DeviceInfo {
        return DeviceInfo(
            platform = "android",
            model = Build.MODEL,
            os_version = Build.VERSION.RELEASE,
            app_version = BuildConfig.VERSION_NAME,
            language_code = Locale.getDefault().toString(),
            timezone = TimeZone.getDefault().id
        )
    }
}

2. Token Storage (Secure SharedPreferences)

import android.content.Context
import android.content.SharedPreferences
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey

class TokenManager(context: Context) {
    private val masterKey = MasterKey.Builder(context)
        .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
        .build()

    private val prefs: SharedPreferences = EncryptedSharedPreferences.create(
        context,
        "auth_tokens",
        masterKey,
        EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
        EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
    )

    fun saveTokens(accessToken: String, refreshToken: String) {
        prefs.edit().apply {
            putString("access_token", accessToken)
            putString("refresh_token", refreshToken)
            apply()
        }
    }

    fun getAccessToken(): String? = prefs.getString("access_token", null)
    fun getRefreshToken(): String? = prefs.getString("refresh_token", null)

    fun clearTokens() {
        prefs.edit().clear().apply()
    }
}

3. Authentication Manager

class AuthManager(
    private val apiClient: AuthApiClient,
    private val tokenManager: TokenManager
) {
    suspend fun login(phoneNumber: String, code: String): Result<User> {
        val deviceId = getDeviceId()
        return apiClient.verifyOtp(phoneNumber, code, deviceId)
            .onSuccess { response ->
                tokenManager.saveTokens(response.access_token, response.refresh_token)
            }
            .map { it.user }
    }

    suspend fun refreshTokens(): Result<Pair<String, String>> {
        val refreshToken = tokenManager.getRefreshToken()
            ?: return Result.failure(Exception("No refresh token"))

        return apiClient.refreshToken(refreshToken)
            .onSuccess { response ->
                tokenManager.saveTokens(response.access_token, response.refresh_token)
            }
            .map { it.access_token to it.refresh_token }
    }

    fun getAccessToken(): String? = tokenManager.getAccessToken()

    suspend fun logout() {
        tokenManager.getRefreshToken()?.let { refreshToken ->
            apiClient.logout(refreshToken)
        }
        tokenManager.clearTokens()
    }

    private fun getDeviceId(): String {
        // Use Android ID or Installation ID
        return Settings.Secure.getString(
            context.contentResolver,
            Settings.Secure.ANDROID_ID
        )
    }
}

4. Usage in Activity/Fragment

class LoginActivity : AppCompatActivity() {
    private val authManager by lazy {
        val apiClient = AuthApiClient("http://your-api-url")
        val tokenManager = TokenManager(this)
        AuthManager(apiClient, tokenManager)
    }

    private fun requestOtp() {
        lifecycleScope.launch {
            val phoneNumber = phoneInput.text.toString()
            authManager.requestOtp(phoneNumber)
                .onSuccess { showToast("OTP sent!") }
                .onFailure { showError(it.message) }
        }
    }

    private fun verifyOtp() {
        lifecycleScope.launch {
            val phoneNumber = phoneInput.text.toString()
            val code = otpInput.text.toString()
            
            authManager.login(phoneNumber, code)
                .onSuccess { user ->
                    if (user.needs_profile) {
                        startActivity(Intent(this, ProfileSetupActivity::class.java))
                    } else {
                        startActivity(Intent(this, MainActivity::class.java))
                    }
                    finish()
                }
                .onFailure { showError("Invalid OTP") }
        }
    }
}

Error Handling

Common Error Codes

Status Error Description
400 phone_number is required Missing phone number
400 Invalid or expired OTP Wrong code or OTP expired (10 min)
401 Invalid refresh token Token expired or revoked
401 Missing Authorization header Access token not provided
403 Origin not allowed CORS restriction (production)
500 Internal server error Server issue

Error Response Format

{
  "error": "Error message here"
}

Security Best Practices

  1. Store tokens securely - Use EncryptedSharedPreferences (Android) or Keychain (iOS)
  2. Handle token expiration - Automatically refresh when access token expires (401)
  3. Rotate refresh tokens - Always save the new refresh_token after refresh
  4. Validate device_id - Use consistent device identifier (Android ID, Installation ID)
  5. Handle reuse detection - If refresh returns 401, force re-login (token compromised)

Token Expiration

  • Access Token: 15 minutes (default, configurable via JWT_ACCESS_TTL)
  • Refresh Token: 7 days (default, configurable via JWT_REFRESH_TTL)
  • OTP: 10 minutes (fixed)

Example: Auto-refresh on 401

suspend fun <T> callWithAuth(block: suspend (String) -> Result<T>): Result<T> {
    val token = tokenManager.getAccessToken() ?: return Result.failure(Exception("Not logged in"))
    
    return block(token).recoverCatching { error ->
        if (error is HttpException && error.response.status == HttpStatusCode.Unauthorized) {
            // Token expired, refresh and retry
            refreshTokens()
                .getOrNull()
                ?.let { (newAccess, _) -> block(newAccess) }
                ?: Result.failure(Exception("Failed to refresh token"))
        } else {
            Result.failure(error)
        }
    }
}

Health Check

Endpoint: GET /health

Response (200):

{
  "ok": true
}

Use this to verify the service is running before attempting authentication.


Notes

  • Phone numbers must be in E.164 format (+ prefix with country code)
  • Device ID should be 4-128 alphanumeric characters, or it will be hashed
  • Refresh tokens rotate on each use - always update stored token
  • If needs_profile: true, prompt user to complete profile before accessing app