# 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:** ```json { "phone_number": "+919876543210" } ``` **Response (200):** ```json { "ok": true } ``` **Kotlin Example:** ```kotlin data class RequestOtpRequest(val phone_number: String) data class RequestOtpResponse(val ok: Boolean) suspend fun requestOtp(phoneNumber: String): Result { 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:** ```json { "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):** ```json { "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:** ```kotlin 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 { val request = VerifyOtpRequest(phoneNumber, code, deviceId, deviceInfo) return apiClient.post("/auth/verify-otp", request) } ``` **Error (400):** ```json { "error": "Invalid or expired OTP" } ``` --- ### 3. Refresh Token **Endpoint:** `POST /auth/refresh` **Request:** ```json { "refresh_token": "eyJhbGc..." } ``` **Response (200):** ```json { "access_token": "eyJhbGc...", "refresh_token": "eyJhbGc..." } ``` **Kotlin Example:** ```kotlin data class RefreshRequest(val refresh_token: String) data class RefreshResponse(val access_token: String, val refresh_token: String) suspend fun refreshToken(refreshToken: String): Result { 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 ``` **Request:** ```json { "name": "John Doe", "user_type": "seller" } ``` **Response (200):** ```json { "id": "uuid-here", "phone_number": "+919876543210", "name": "John Doe", "role": "user", "user_type": "seller" } ``` **user_type values:** `seller`, `buyer`, `service_provider` **Kotlin Example:** ```kotlin 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 { val request = UpdateProfileRequest(name, userType) return apiClient.put("/users/me", request, accessToken) } ``` --- ### 5. Logout **Endpoint:** `POST /auth/logout` **Request:** ```json { "refresh_token": "eyJhbGc..." } ``` **Response (200):** ```json { "ok": true } ``` **Kotlin Example:** ```kotlin suspend fun logout(refreshToken: String): Result { val request = RefreshRequest(refreshToken) return apiClient.post("/auth/logout", request) } ``` --- ## Complete Kotlin Integration Example ### 1. API Client Setup ```kotlin 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 { 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 { 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 { 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 { 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) ```kotlin 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 ```kotlin class AuthManager( private val apiClient: AuthApiClient, private val tokenManager: TokenManager ) { suspend fun login(phoneNumber: String, code: String): Result { 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> { 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 ```kotlin 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 ```json { "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 ```kotlin suspend fun callWithAuth(block: suspend (String) -> Result): Result { 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):** ```json { "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