🔐 Autoryzacja

KNET obsługuje Bearer Token, Basic Auth, OAuth i automatyczny refresh tokenów.

Bearer Token

// Per request
val response = client.get(
    url = "https://api.example.com/users",
    headers = mapOf("Authorization" to "Bearer $token")
)

// Interceptor - wszystkie requesty
val client = KNETClient.builder()
    .addInterceptor(KNETHeaderInterceptor.bearer(token))
    .build()

// Dynamiczny token
val client = KNETClient.builder()
    .addInterceptor(KNETHeaderInterceptor {
        mapOf("Authorization" to "Bearer ${tokenProvider.getToken()}")
    })
    .build()

Basic Auth

// Per request
val credentials = Base64.encodeToString(
    "$username:$password".toByteArray(),
    Base64.NO_WRAP
)
val response = client.get(url, mapOf(
    "Authorization" to "Basic $credentials"
))

// Interceptor
val client = KNETClient.builder()
    .addInterceptor(KNETHeaderInterceptor.basic(username, password))
    .build()

Auth Interceptor z refresh

val authInterceptor = KNETAuthInterceptor(
    // Dostawca aktualnego tokena
    tokenProvider = {
        authRepository.getAccessToken()
    },

    // Logika refresh tokena
    refreshToken = suspend {
        val refreshToken = authRepository.getRefreshToken()
        val response = client.post("$baseUrl/auth/refresh", mapOf(
            "refresh_token" to refreshToken
        ))

        val newTokens = response.jsonObject()
        authRepository.saveTokens(
            accessToken = newTokens["access_token"] as String,
            refreshToken = newTokens["refresh_token"] as String
        )

        newTokens["access_token"] as String
    },

    // Kiedy refreshować
    shouldRefresh = { response ->
        response.statusCode == 401
    },

    // Max prób refresh
    maxRefreshAttempts = 1
)

val client = KNETClient.builder()
    .addInterceptor(authInterceptor)
    .build()

OAuth 2.0

class OAuth2Client(
    private val clientId: String,
    private val clientSecret: String,
    private val tokenUrl: String
) {
    private val client = KNETClient()

    // Authorization Code Flow
    suspend fun exchangeCode(code: String, redirectUri: String): TokenResponse {
        val response = client.post(tokenUrl, mapOf(
            "grant_type" to "authorization_code",
            "code" to code,
            "redirect_uri" to redirectUri,
            "client_id" to clientId,
            "client_secret" to clientSecret
        ))
        return response.json<TokenResponse>()
    }

    // Client Credentials Flow
    suspend fun getClientToken(): TokenResponse {
        val response = client.post(tokenUrl, mapOf(
            "grant_type" to "client_credentials",
            "client_id" to clientId,
            "client_secret" to clientSecret
        ))
        return response.json<TokenResponse>()
    }

    // Refresh Token
    suspend fun refresh(refreshToken: String): TokenResponse {
        val response = client.post(tokenUrl, mapOf(
            "grant_type" to "refresh_token",
            "refresh_token" to refreshToken,
            "client_id" to clientId
        ))
        return response.json<TokenResponse>()
    }
}

data class TokenResponse(
    val access_token: String,
    val refresh_token: String?,
    val expires_in: Int,
    val token_type: String
)

Secure Token Storage

class SecureTokenStorage(context: Context) {

    private val prefs = EncryptedSharedPreferences.create(
        "auth_prefs",
        MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC),
        context,
        EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
        EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
    )

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

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

    fun isTokenExpired(expiresInSeconds: Int): Boolean {
        val savedAt = prefs.getLong("token_saved_at", 0)
        val expiresAt = savedAt + (expiresInSeconds * 1000)
        return System.currentTimeMillis() >= expiresAt - 60_000 // 1 min buffer
    }

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

API Key Auth

// W header
val client = KNETClient.builder()
    .addInterceptor(KNETHeaderInterceptor(mapOf(
        "X-API-Key" to apiKey
    )))
    .build()

// W query
val response = client.get(
    url = "https://api.example.com/data",
    query = mapOf("api_key" to apiKey)
)

Multi-tenant auth

class MultiTenantAuthInterceptor(
    private val tenantResolver: () -> String,
    private val tokenProvider: (String) -> String?
) : KNETInterceptor {

    override suspend fun intercept(
        request: KNETRequest,
        chain: suspend (KNETRequest) -> KNETResponse
    ): KNETResponse {
        val tenant = tenantResolver()
        val token = tokenProvider(tenant)

        val authRequest = if (token != null) {
            request.copy(
                headers = request.headers + mapOf(
                    "Authorization" to "Bearer $token",
                    "X-Tenant-ID" to tenant
                )
            )
        } else {
            request
        }

        return chain(authRequest)
    }
}

Przykład: Auth Repository

class AuthRepository(
    private val client: KNETClient,
    private val storage: SecureTokenStorage
) {
    private val _authState = MutableStateFlow<AuthState>(AuthState.Unknown)
    val authState: StateFlow<AuthState> = _authState

    suspend fun login(email: String, password: String): Result<User> {
        return try {
            val response = client.post("$baseUrl/auth/login", mapOf(
                "email" to email,
                "password" to password
            ))

            val data = response.jsonObject()
            storage.saveTokens(
                accessToken = data["access_token"] as String,
                refreshToken = data["refresh_token"] as String
            )

            val user = (data["user"] as Map<*, *>).let { User.fromMap(it) }
            _authState.value = AuthState.Authenticated(user)

            Result.success(user)
        } catch (e: Exception) {
            Result.failure(e)
        }
    }

    suspend fun logout() {
        try {
            client.post("$baseUrl/auth/logout", emptyMap<String, Any>())
        } finally {
            storage.clearTokens()
            _authState.value = AuthState.NotAuthenticated
        }
    }

    fun getAccessToken() = storage.getAccessToken()
    fun getRefreshToken() = storage.getRefreshToken()
}

sealed class AuthState {
    object Unknown : AuthState()
    object NotAuthenticated : AuthState()
    data class Authenticated(val user: User) : AuthState()
}

📚 Zobacz też