android: add auth sessions hardening APIs and tests
Some checks are pending
CI / test (push) Has started running
Some checks are pending
CI / test (push) Has started running
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package ru.daemonlord.messenger.data.auth.api
|
||||
|
||||
import ru.daemonlord.messenger.data.auth.dto.AuthUserDto
|
||||
import ru.daemonlord.messenger.data.auth.dto.AuthSessionDto
|
||||
import ru.daemonlord.messenger.data.auth.dto.LoginRequestDto
|
||||
import ru.daemonlord.messenger.data.auth.dto.RefreshTokenRequestDto
|
||||
import ru.daemonlord.messenger.data.auth.dto.TokenResponseDto
|
||||
@@ -8,6 +9,8 @@ import retrofit2.http.Headers
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.DELETE
|
||||
import retrofit2.http.Path
|
||||
|
||||
interface AuthApiService {
|
||||
@Headers("No-Auth: true")
|
||||
@@ -20,4 +23,13 @@ interface AuthApiService {
|
||||
|
||||
@GET("/api/v1/auth/me")
|
||||
suspend fun me(): AuthUserDto
|
||||
|
||||
@GET("/api/v1/auth/sessions")
|
||||
suspend fun sessions(): List<AuthSessionDto>
|
||||
|
||||
@DELETE("/api/v1/auth/sessions/{jti}")
|
||||
suspend fun revokeSession(@Path("jti") jti: String)
|
||||
|
||||
@DELETE("/api/v1/auth/sessions")
|
||||
suspend fun revokeAllSessions()
|
||||
}
|
||||
|
||||
@@ -37,6 +37,20 @@ data class AuthUserDto(
|
||||
val emailVerified: Boolean,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class AuthSessionDto(
|
||||
val jti: String,
|
||||
@SerialName("created_at")
|
||||
val createdAt: String,
|
||||
@SerialName("ip_address")
|
||||
val ipAddress: String? = null,
|
||||
@SerialName("user_agent")
|
||||
val userAgent: String? = null,
|
||||
val current: Boolean? = null,
|
||||
@SerialName("token_type")
|
||||
val tokenType: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ErrorResponseDto(
|
||||
val detail: String? = null,
|
||||
|
||||
@@ -7,10 +7,12 @@ import ru.daemonlord.messenger.core.token.TokenBundle
|
||||
import ru.daemonlord.messenger.core.token.TokenRepository
|
||||
import ru.daemonlord.messenger.data.auth.api.AuthApiService
|
||||
import ru.daemonlord.messenger.data.auth.dto.AuthUserDto
|
||||
import ru.daemonlord.messenger.data.auth.dto.AuthSessionDto
|
||||
import ru.daemonlord.messenger.data.auth.dto.LoginRequestDto
|
||||
import ru.daemonlord.messenger.data.auth.dto.RefreshTokenRequestDto
|
||||
import ru.daemonlord.messenger.di.IoDispatcher
|
||||
import ru.daemonlord.messenger.domain.auth.model.AuthUser
|
||||
import ru.daemonlord.messenger.domain.auth.model.AuthSession
|
||||
import ru.daemonlord.messenger.domain.auth.repository.AuthRepository
|
||||
import ru.daemonlord.messenger.domain.common.AppError
|
||||
import ru.daemonlord.messenger.domain.common.AppResult
|
||||
@@ -96,6 +98,32 @@ class NetworkAuthRepository @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun listSessions(): AppResult<List<AuthSession>> = withContext(ioDispatcher) {
|
||||
try {
|
||||
AppResult.Success(authApiService.sessions().map { it.toDomain() })
|
||||
} catch (error: Throwable) {
|
||||
AppResult.Error(error.toAppError(forLogin = false))
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun revokeSession(jti: String): AppResult<Unit> = withContext(ioDispatcher) {
|
||||
try {
|
||||
authApiService.revokeSession(jti = jti)
|
||||
AppResult.Success(Unit)
|
||||
} catch (error: Throwable) {
|
||||
AppResult.Error(error.toAppError(forLogin = false))
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun revokeAllSessions(): AppResult<Unit> = withContext(ioDispatcher) {
|
||||
try {
|
||||
authApiService.revokeAllSessions()
|
||||
AppResult.Success(Unit)
|
||||
} catch (error: Throwable) {
|
||||
AppResult.Error(error.toAppError(forLogin = false))
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun logout() {
|
||||
tokenRepository.clearTokens()
|
||||
}
|
||||
@@ -111,6 +139,17 @@ class NetworkAuthRepository @Inject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
private fun AuthSessionDto.toDomain(): AuthSession {
|
||||
return AuthSession(
|
||||
jti = jti,
|
||||
createdAt = createdAt,
|
||||
ipAddress = ipAddress,
|
||||
userAgent = userAgent,
|
||||
current = current,
|
||||
tokenType = tokenType,
|
||||
)
|
||||
}
|
||||
|
||||
private fun Throwable.toAppError(forLogin: Boolean): AppError {
|
||||
return when (this) {
|
||||
is IOException -> AppError.Network
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package ru.daemonlord.messenger.domain.auth.model
|
||||
|
||||
data class AuthSession(
|
||||
val jti: String,
|
||||
val createdAt: String,
|
||||
val ipAddress: String?,
|
||||
val userAgent: String?,
|
||||
val current: Boolean?,
|
||||
val tokenType: String?,
|
||||
)
|
||||
@@ -1,6 +1,7 @@
|
||||
package ru.daemonlord.messenger.domain.auth.repository
|
||||
|
||||
import ru.daemonlord.messenger.domain.auth.model.AuthUser
|
||||
import ru.daemonlord.messenger.domain.auth.model.AuthSession
|
||||
import ru.daemonlord.messenger.domain.common.AppResult
|
||||
|
||||
interface AuthRepository {
|
||||
@@ -8,5 +9,8 @@ interface AuthRepository {
|
||||
suspend fun refreshTokens(): AppResult<Unit>
|
||||
suspend fun getMe(): AppResult<AuthUser>
|
||||
suspend fun restoreSession(): AppResult<AuthUser>
|
||||
suspend fun listSessions(): AppResult<List<AuthSession>>
|
||||
suspend fun revokeSession(jti: String): AppResult<Unit>
|
||||
suspend fun revokeAllSessions(): AppResult<Unit>
|
||||
suspend fun logout()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package ru.daemonlord.messenger.domain.auth.usecase
|
||||
|
||||
import ru.daemonlord.messenger.domain.auth.model.AuthSession
|
||||
import ru.daemonlord.messenger.domain.auth.repository.AuthRepository
|
||||
import ru.daemonlord.messenger.domain.common.AppResult
|
||||
import javax.inject.Inject
|
||||
|
||||
class ListSessionsUseCase @Inject constructor(
|
||||
private val repository: AuthRepository,
|
||||
) {
|
||||
suspend operator fun invoke(): AppResult<List<AuthSession>> {
|
||||
return repository.listSessions()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package ru.daemonlord.messenger.domain.auth.usecase
|
||||
|
||||
import ru.daemonlord.messenger.domain.auth.repository.AuthRepository
|
||||
import ru.daemonlord.messenger.domain.common.AppResult
|
||||
import javax.inject.Inject
|
||||
|
||||
class RevokeAllSessionsUseCase @Inject constructor(
|
||||
private val repository: AuthRepository,
|
||||
) {
|
||||
suspend operator fun invoke(): AppResult<Unit> {
|
||||
return repository.revokeAllSessions()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package ru.daemonlord.messenger.domain.auth.usecase
|
||||
|
||||
import ru.daemonlord.messenger.domain.auth.repository.AuthRepository
|
||||
import ru.daemonlord.messenger.domain.common.AppResult
|
||||
import javax.inject.Inject
|
||||
|
||||
class RevokeSessionUseCase @Inject constructor(
|
||||
private val repository: AuthRepository,
|
||||
) {
|
||||
suspend operator fun invoke(jti: String): AppResult<Unit> {
|
||||
return repository.revokeSession(jti = jti)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user