14 KiB
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
- Request OTP → User enters phone number
- Verify OTP → User enters code, receives tokens
- Use Access Token → Include in
Authorizationheader for protected endpoints - Refresh Token → When access token expires, get new tokens
- 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
- Store tokens securely - Use
EncryptedSharedPreferences(Android) or Keychain (iOS) - Handle token expiration - Automatically refresh when access token expires (401)
- Rotate refresh tokens - Always save the new
refresh_tokenafter refresh - Validate device_id - Use consistent device identifier (Android ID, Installation ID)
- 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