Autenticación, autorización y gestión segura de sesiones en Android
JWT, tokens, DataStore, Android Keystore, OkHttp y renovación de sesiones
Introducción
Una aplicación que identifica a sus usuarios necesita decidir cómo comprobar su identidad, cómo conservar una sesión y cómo limitar el acceso a determinadas operaciones. Estas decisiones afectan a la interfaz, la comunicación con el servidor, el almacenamiento local y el ciclo de vida de la aplicación.
En Android, las sesiones suelen integrarse con una API mediante tokens. El cliente puede conservar credenciales temporales para evitar que el usuario escriba su contraseña en cada operación, pero debe considerar su expiración, revocación y posible exposición. La seguridad también depende del servidor: ocultar un botón no impide que alguien construya una petición HTTP.
Esta guía estudia los conceptos fundamentales y los conecta con implementaciones en Kotlin, Jetpack Compose, Retrofit, OkHttp, DataStore y Android Keystore. Los fragmentos son ejemplos de estudio, no una implementación completa de autenticación lista para producción.
Resultados de aprendizaje
Al terminar, podrás explicar las diferencias entre autenticación y autorización; interpretar el contenido y las limitaciones de un JWT; justificar el uso de tokens de acceso y refresco; representar la sesión como estado observable; reconocer las responsabilidades de DataStore, Keystore, interceptores y autenticadores; y diagnosticar fallos de expiración, permisos o cierre de sesión.
Lo imprescindible para el examen
Diferenciar autenticación y autorización; explicar access/refresh token y JWT; identificar riesgos de exposición; interpretar expiración, renovación y revocación sin asumir garantías inexistentes.
Se evalúa la capacidad de explicar, interpretar fragmentos y justificar decisiones. Las versiones puntuales de librerías, URLs, nombres de proyecto y comandos accesorios quedan como contexto de consulta.
Criterios transversales para interpretar esta guía
Principio transferible. Conviene distinguir la responsabilidad de una tecnología de la forma concreta en que se utilizó en la práctica. Un ejemplo de ViewModel, Retrofit, Hilt o WebSocket ilustra una solución posible; su configuración exacta no es una regla universal.
Decisión del proyecto. Los nombres, rutas, versiones, intervalos, tamaños y elecciones de librerías del caso original sirven para interpretar el código. Salvo indicación expresa, no se exige memorizarlos para el examen.
Alcance del código. Los fragmentos son recursos didácticos; pueden omitir imports, dependencias, permisos, detalles de manejo de errores o infraestructura. Comprender un fragmento no equivale a comprobar que compila como proyecto independiente.
Referencias cruzadas. La síntesis transversal de la colección reúne las distinciones entre estado y evento, persistencia, reintentos, idempotencia y límites de las pruebas.
1. Identidad, permisos y modelo de amenazas
1.1. Autenticación y autorización
Autenticación es el proceso de comprobar quién realiza una petición. Autorización es la decisión sobre qué operaciones puede ejecutar esa identidad.
Supongamos una aplicación académica con usuarios de tipo estudiante y docente. Ambos pueden consultar comunicados. Solo quienes tienen permiso de publicación pueden crear o eliminar comunicados. El servidor comprueba la sesión y los permisos en cada operación protegida.
| Pregunta | Concepto | Ejemplo |
|---|---|---|
| ¿Quién solicita acceso? | Autenticación | Comprobar usuario y contraseña o validar un token |
| ¿Puede realizar esta operación? | Autorización | Impedir que un estudiante publique un comunicado restringido |
| ¿La solicitud está correctamente formada? | Validación | Comprobar que el título cumple la longitud permitida |
Los códigos HTTP ayudan a distinguir casos, aunque el comportamiento concreto depende del contrato de la API.
| Código | Significado habitual | Respuesta posible de la app |
|---|---|---|
200 / 201 |
Operación exitosa | Procesar y mostrar el resultado |
400 / 422 |
Solicitud o datos inválidos | Mostrar qué debe corregirse |
401 Unauthorized |
Falta autenticación válida | Pedir credenciales o renovar el acceso según la causa |
403 Forbidden |
La identidad no tiene permiso suficiente | Informar que la operación está restringida |
409 Conflict |
Conflicto con el estado del recurso | Informar del conflicto y ofrecer una acción adecuada |
500 / 503 |
Problema del servidor | Permitir reintentar si corresponde |
Importante: un 401 no siempre significa «contraseña equivocada», ni un 403 garantiza por sí solo que el cliente esté correctamente autenticado. La interpretación final depende del endpoint, las cabeceras y el cuerpo de la respuesta.
1.2. Pensar en amenazas antes que en herramientas
El modelo de amenazas identifica qué información se protege, quién podría obtenerla y qué consecuencias tendría.
| Activo | Riesgo | Medida de mitigación |
|---|---|---|
| Contraseña | Su exposición permite intentar entrar a la cuenta | No persistirla; transmitirla mediante HTTPS; protegerla adecuadamente en el servidor |
| Token de acceso | Su robo permite actuar como el usuario mientras sea aceptado | Vigencia breve, TLS, almacenamiento controlado, evitar registros |
| Token de refresco | Su robo permite conseguir nuevos accesos mientras siga vigente | Almacenamiento protegido, rotación y revocación en servidor |
| Perfil y rol local | Manipulación de la interfaz | Verificación de autorización en el servidor |
| Datos de sesión | Exposición mediante copia de seguridad o registros | Reglas de respaldo, minimización y saneamiento de logs |
Android proporciona aislamiento entre aplicaciones y mecanismos de cifrado del almacenamiento, pero estas protecciones no cubren por sí solas un proceso comprometido, un dispositivo alterado, registros inseguros ni errores en la API.
Principio general: conservar la menor cantidad de información sensible durante el menor tiempo necesario y restringir quién puede utilizarla.
1.3. Qué guardar y qué descartar
Una contraseña se necesita para el inicio de sesión, pero normalmente no debe persistirse en el dispositivo para automatizar futuros ingresos. Una sesión basada en tokens elimina esa necesidad: el usuario envía credenciales inicialmente y después utiliza tokens de duración limitada.
El identificador de usuario, el rol y la expiración pueden ser metadatos útiles para la interfaz. Sin embargo, su copia local no autoriza operaciones: el servidor debe verificar las credenciales y permisos recibidos.
2. Tokens de acceso, refresco y JWT
2.1. Qué representa un token
Un token de acceso es una credencial que presenta el cliente para realizar solicitudes protegidas. Habitualmente viaja así:
GET /api/comunicados HTTP/1.1
Host: ejemplo.edu
Authorization: Bearer TOKEN_DE_ACCESO
El encabezado Authorization transporta la credencial. Colocarla en parámetros de consulta, por ejemplo ?token=..., aumenta el riesgo de exposición mediante historiales, registros y herramientas de observación.
2.2. Estructura de un JWT
Un JWT compacto firmado suele tener tres segmentos separados por puntos:
HEADER.PAYLOAD.SIGNATURE
- Header: describe el tipo de token y el algoritmo declarado.
- Payload: contiene claims, como
sub(identidad),role(rol) yexp(expiración). - Signature: permite detectar modificaciones no autorizadas cuando el servidor verifica la firma correctamente.
Los segmentos de contenido están codificados en Base64URL, no cifrados. Cualquiera que posea el JWT puede decodificarlos. La firma protege su integridad y autenticidad bajo las condiciones de verificación, pero no su confidencialidad.
Si alguien cambia manualmente role de student a teacher, el servidor debe rechazar el token porque la firma ya no corresponde. En cambio, si alguien roba un JWT válido, puede presentarlo sin modificarlo: la firma no impide su uso indebido. Por eso un token Bearer debe tratarse como información sensible.
2.3. Acceso y refresco
| Característica | Token de acceso | Token de refresco |
|---|---|---|
| Propósito | Autorizar peticiones habituales | Obtener nuevos tokens de acceso |
| Frecuencia de envío | En peticiones protegidas | Solo al endpoint de renovación |
| Vigencia | Normalmente corta | Habitualmente mayor |
| Formato | Puede ser JWT u opaco | Puede ser opaco u otro formato definido por el servidor |
| Revocación | Depende de la arquitectura del servidor | Suele permitir revocación y rotación |
En la práctica de referencia, el token de acceso era un JWT de cinco minutos y el de refresco una cadena aleatoria con vigencia de siete días. Son parámetros de ese servidor, no valores obligatorios de JWT o Android.
2.4. Rotación y revocación
En la rotación del refresh token, el servidor entrega uno nuevo y deja de aceptar el anterior. Esto reduce el tiempo en que una copia antigua puede seguir utilizándose y permite detectar determinados intentos de reutilización, si el servidor aplica controles adicionales.
Revocar una sesión significa impedir que sus credenciales sigan siendo aceptadas. Un JWT validado únicamente mediante firma y fecha puede seguir utilizándose hasta su expiración, aunque el refresh token haya sido revocado. No obstante, no todos los JWT son irrevocables: una arquitectura puede consultar una lista de revocación, usar versiones de sesión o introspección.
Caso para razonar
Una sesión ha sido revocada en el servidor, pero el dispositivo conserva un token de acceso que aún no expira. Si el servidor comprueba únicamente la firma y el tiempo, algunas peticiones pueden seguir funcionando hasta que ese token caduque. Al tratar de renovarlo, el servidor rechazará el refresh token y el cliente deberá pasar al estado sin sesión.
3. Representar la sesión como un estado observable
3.1. La sesión como dato compartido
Una aplicación necesita que el inicio de sesión, el cierre y la expiración se reflejen en varias pantallas. Resulta más consistente centralizar la sesión en un repositorio y exponer sus cambios mediante un Flow o StateFlow.
Una representación posible es:
sealed interface SessionState {
data object Loading : SessionState
data object SignedOut : SessionState
data class SignedIn(val user: UserSession) : SessionState
}
data class UserSession(
val userId: String,
val role: String,
val expiresAtEpochSeconds: Long
)
En este ejemplo UserSession expone únicamente datos que necesita la UI. El almacenamiento de credenciales puede permanecer encapsulado en la capa de datos.
Loadingindica que todavía se está recuperando la sesión guardada.SignedOutindica que no hay una sesión local utilizable.SignedInindica que existe una sesión local, aunque el servidor todavía puede rechazarla o exigir renovación.
Sesión almacenada no equivale a sesión validada por el servidor. La app debe tener una política explícita para gestionar expiración, falta de conectividad y revocación.
3.2. Selección del árbol de pantallas en Compose
@Composable
fun RootApp(sessionState: SessionState) {
when (sessionState) {
SessionState.Loading -> LoadingScreen()
SessionState.SignedOut -> LoginRoute()
is SessionState.SignedIn -> ProtectedNavHost()
}
}
Con este diseño, el inicio y cierre de sesión actualizan un estado compartido. Compose reevalúa qué contenido muestra y elimina el árbol protegido cuando deja de existir una sesión activa.
Este patrón es una opción de arquitectura, no la única. Otra aplicación puede incluir login dentro de un grafo de navegación y controlar transiciones de forma explícita. Lo esencial es mantener la relación entre estado de sesión y destinos accesibles, evitando pantallas protegidas visibles después del cierre.
3.3. Flujo de inicio de sesión
Usuario introduce credenciales
|
v
LoginViewModel valida entrada y solicita login
|
v
Repositorio llama a la API
|
v
Servidor responde con tokens o error
|
v
Repositorio protege y guarda la sesión
|
v
Flow emite nuevo estado
|
v
Compose muestra el contenido correspondiente
La navegación no tiene por qué ejecutarse inmediatamente después de pulsar Entrar. Puede depender de que el repositorio confirme y publique el nuevo estado de sesión.
3.4. Estados del formulario
Además del estado global de sesión, el formulario necesita su propio estado: usuario, contraseña temporal, indicador de envío y mensaje de error.
data class LoginUiState(
val username: String = "",
val password: String = "",
val submitting: Boolean = false,
val errorMessage: String? = null
) {
val canSubmit: Boolean
get() = username.isNotBlank() && password.isNotBlank() && !submitting
}
La contraseña permanece en memoria durante la interacción. Después de un inicio correcto se debe descartar del estado, reconociendo que asignar una cadena vacía no garantiza borrar copias ya existentes de una String de la memoria administrada.
4. Acceso a la API mediante Retrofit y OkHttp
4.1. Definir el contrato remoto
interface AuthApi {
@POST("auth/login")
suspend fun login(@Body body: LoginRequest): TokenResponse
@POST("auth/refresh")
suspend fun refresh(@Body body: RefreshRequest): TokenResponse
@GET("auth/me")
suspend fun getProfile(): ProfileDto
}
LoginRequest, RefreshRequest, TokenResponse y ProfileDto son DTO que representan el contrato de intercambio con el servidor. Su estructura específica depende de la API. Los modelos de dominio permanecen separados si esta distinción resulta útil para la arquitectura.
4.2. Un interceptor para adjuntar credenciales
En OkHttp, un interceptor puede añadir el token a las solicitudes:
class BearerInterceptor(
private val currentAccessToken: () -> String?
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val token = currentAccessToken()
val request = chain.request().newBuilder().apply {
if (token != null) {
header("Authorization", "Bearer $token")
}
}.build()
return chain.proceed(request)
}
}
La función se ejecuta cuando se procesa la petición, por lo que puede consultar el token vigente en lugar de utilizar una cadena capturada al iniciar la aplicación.
Precaución: no se debe añadir automáticamente el mismo token a cualquier destino externo. Hay que limitar el envío a los hosts y endpoints previstos por la arquitectura. Tampoco conviene mezclar llamadas públicas de login o refresh con mecanismos que produzcan ciclos de autenticación.
4.3. El problema de obtener el token desde código síncrono
La función intercept no es suspend. DataStore, en cambio, expone lecturas asíncronas mediante Flow. Una implementación didáctica puede utilizar runBlocking desde un hilo de OkHttp, como hacía la práctica, pero introduce bloqueo durante la lectura.
Para una aplicación real conviene considerar una instantánea de token en memoria actualizada desde el repositorio, inicializada antes de las peticiones protegidas, y mecanismos de sincronización cuando se renueva. Este diseño reduce lecturas de disco por petición y evita depender de una operación bloqueante repetida.
El hecho de ejecutar código fuera del hilo principal no convierte cualquier bloqueo en una buena decisión: también importa el consumo de hilos, los posibles interbloqueos y la concurrencia.
5. Persistencia de la sesión con DataStore
5.1. Para qué sirve
Preferences DataStore permite guardar pares clave-valor de manera persistente y observar sus cambios mediante Flow. Es apropiado para pequeños conjuntos de preferencias o metadatos. No proporciona consultas SQL como Room.
private val Context.sessionDataStore by preferencesDataStore(name = "session")
class SessionPreferences(private val context: Context) {
private val USER_ID = stringPreferencesKey("user_id")
val userId: Flow<String?> = context.sessionDataStore.data
.map { preferences -> preferences[USER_ID] }
suspend fun saveUserId(value: String) {
context.sessionDataStore.edit { preferences ->
preferences[USER_ID] = value
}
}
}
Este código guarda solo un identificador, no credenciales sensibles. Una instancia de DataStore debe administrarse de forma única por archivo y proceso según el mecanismo utilizado.
5.2. DataStore no cifra automáticamente el contenido de la aplicación
El aislamiento de Android y el cifrado del almacenamiento del dispositivo son protecciones de plataforma. No obstante, Preferences DataStore no cifra individualmente los valores que guarda. Un lector que consiga acceso legítimo o indebido al archivo puede encontrar los valores de las preferencias.
Para guardar tokens sensibles, el diseño del curso combinó DataStore con cifrado aplicado antes de persistirlos. El cifrado y descifrado se encapsulan en una clase que utiliza una clave del Android Keystore.
5.3. Almacenamiento y respaldo
Una sesión cifrada puede resultar imposible de restaurar en otro teléfono si la clave del Keystore original no se transfiere. Por ello es importante excluir los archivos de sesión sensible de los mecanismos de respaldo y transferencia correspondientes, y manejar fallos de descifrado como una condición recuperable, generalmente solicitando un nuevo inicio de sesión.
En versiones modernas de Android, las reglas de extracción y respaldo se configuran de manera específica. android:allowBackup="false" por sí solo no debe presentarse como garantía universal frente a todas las modalidades de transferencia entre dispositivos.
6. Android Keystore y cifrado de tokens
6.1. Qué protege el Keystore
El Android Keystore permite gestionar claves criptográficas de manera que el material de clave sea no exportable mediante las API normales. Dependiendo del dispositivo y de la configuración, las operaciones pueden contar también con protección respaldada por hardware.
La aplicación puede solicitar operaciones criptográficas sin recibir los bytes de la clave. Esto dificulta trasladar la clave a otro entorno, pero no impide que código malicioso que controla el proceso intente utilizar las operaciones autorizadas.
6.2. Cifrado autenticado con AES-GCM
Un esquema habitual para proteger tokens utiliza AES-GCM:
Token en memoria
|
v
Cifrado AES-GCM con clave gestionada por Keystore
|
v
IV/nonce + texto cifrado + etiqueta de autenticación
|
v
Almacenamiento en DataStore
Para descifrar, se recuperan el IV y el contenido cifrado y se solicita al Keystore la operación correspondiente. El IV debe generarse correctamente y no reutilizarse con la misma clave. El modo GCM permite comprobar integridad además de confidencialidad.
La implementación completa requiere manejar generación de claves, configuración criptográfica, codificación del resultado y excepciones. No es apropiado copiar fragmentos incompletos como si constituyeran una solución segura de producción.
6.3. Qué datos proteger
Los tokens de acceso y refresco son credenciales Bearer y requieren controles de exposición. Metadatos como el identificador de usuario o el tiempo de expiración pueden conservarse sin cifrado adicional si la evaluación de amenazas y privacidad así lo permite. El rol mostrado por la UI jamás reemplaza la autorización del servidor.
Error habitual: considerar que un archivo cifrado elimina los demás riesgos de filtración. Los tokens pueden seguir expuestos en memoria, registros, herramientas de diagnóstico, capturas, reportes de error o tráfico incorrectamente protegido.
7. Autorización y control por roles
El cliente puede utilizar el rol conocido para decidir qué controles presenta. Por ejemplo:
@Composable
fun ContentActions(
canPublish: Boolean,
onPublish: () -> Unit
) {
if (canPublish) {
Button(onClick = onPublish) {
Text("Publicar")
}
}
}
La condición mejora la experiencia, pero no protege el endpoint. Una persona puede realizar la misma petición con otra aplicación, una herramienta HTTP o un cliente modificado.
En el servidor deben verificarse el token, su validez, la identidad y los permisos asociados a la operación. Si un usuario sin privilegios solicita publicar, la API puede responder 403. La app debe presentar el resultado como acceso denegado, sin cerrar inesperadamente la sesión ni fallar.
También conviene distinguir rol de permiso. Una aplicación simple puede derivar canPublish de role == "teacher"; sistemas más complejos suelen trabajar con permisos específicos, políticas o atributos del recurso.
8. Renovación automática de credenciales
8.1. Qué ocurre al vencer el acceso
Petición protegida con token de acceso
|
v
Servidor responde 401 por token vencido
|
v
Cliente solicita renovación con refresh token
|
+-----+------+
| |
Éxito Rechazo
| |
Nuevo acceso y Invalidación de
refresh, si rota sesión según contrato
| |
Reintento de la Estado sin sesión
petición original
El reintento debe hacerse con cuidado. No todas las fallas 401 son expiraciones y algunas operaciones tienen efectos secundarios. La lógica debe evitar reintentos infinitos, preservar la semántica del endpoint y distinguir errores de conectividad frente a revocación real.
8.2. Interceptor y Authenticator
Un interceptor participa en el procesamiento de peticiones y respuestas según su tipo y posición en la cadena. Un Authenticator de OkHttp se utiliza para responder a desafíos de autenticación, particularmente respuestas 401 de origen o 407 de proxy, produciendo una nueva petición o devolviendo null.
La distinción habitual en este escenario es:
| Componente | Responsabilidad |
|---|---|
BearerInterceptor |
Agregar el token actual a solicitudes protegidas |
Authenticator |
Intentar recuperar credenciales tras un desafío de autenticación |
SessionRepository |
Coordinar almacenamiento, actualización y eliminación de la sesión |
| API de autenticación | Aceptar o rechazar login, refresh y logout |
Es preferible mantener una vía de renovación que no entre recursivamente en el mismo Authenticator.
8.3. Prevenir ciclos
Para analizar un autenticador que responde a un 401, conviene comprobar:
- Si la petición original disponía de credencial y era candidata a renovación.
- Si la petición corresponde al propio endpoint de refresh y debe excluirse.
- Si ya se intentó una renovación para evitar repetir indefinidamente.
- Si otra petición concurrente ya obtuvo un token más reciente.
- Si el servidor rechazó de manera definitiva el refresh token.
Cuando dos llamadas reciben 401 casi simultáneamente, ambas pueden intentar renovar con el mismo refresh token. Si el servidor lo rota, la segunda renovación puede fallar porque el primer intento ya invalidó el token anterior. Se necesita sincronizar la renovación y reutilizar la credencial nueva cuando otra petición la haya obtenido.
8.4. Manejar los resultados
| Situación | Política razonable |
|---|---|
| Renovación exitosa | Guardar la nueva sesión de forma consistente y reintentar la petición permitida |
| Refresh rechazado por expiración/revocación | Invalidar la sesión local y solicitar nuevo login |
IOException durante refresh |
Conservar la sesión local mientras se decide si se puede reintentar |
| Servidor temporalmente no disponible | Mostrar fallo temporal, sin interpretar automáticamente revocación |
403 por permisos insuficientes |
Informar del permiso faltante; renovar tokens no suele resolverlo |
Un catch (e: Exception) indiscriminado también puede capturar CancellationException en código suspendido. La cancelación debe propagarse correctamente.
9. Cierre de sesión
Salir de una cuenta suele involucrar dos responsabilidades:
Local: eliminar las credenciales y cambiar el estado observable a SignedOut para retirar las pantallas protegidas.
Remota: informar al servidor para revocar el refresh token o la sesión, según su API.
El cierre local debería funcionar incluso sin conectividad. Un fallo de red puede impedir la revocación inmediata del refresh token remoto; por eso la interfaz no debe prometer que se revocó globalmente si no existe confirmación. Para operaciones de mayor riesgo pueden requerirse políticas adicionales de revocación diferida o cierre de todas las sesiones.
Ejemplo conceptual de un repositorio:
suspend fun signOut() {
val previousRefreshToken = tokenStorage.getRefreshToken()
tokenStorage.clear()
if (previousRefreshToken != null) {
try {
authenticationApi.logout(RefreshRequest(previousRefreshToken))
} catch (e: IOException) {
// Cierre local completado; revocación remota pendiente/no confirmada.
} catch (e: HttpException) {
// Registrar o manejar según el contrato del servidor.
}
}
}
Las interfaces tokenStorage y authenticationApi son ilustrativas. En un sistema completo también se gestiona la cancelación de tareas, la limpieza de cachés con datos de otra cuenta y la protección frente a respuestas tardías que vuelvan a escribir una sesión después del cierre.
10. Evitar la exposición accidental de credenciales
10.1. Registros
Los interceptores de registro pueden incluir encabezados y cuerpos completos. El encabezado Authorization debe ocultarse y no deben registrarse contraseñas ni refresh tokens.
val logging = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BASIC
redactHeader("Authorization")
}
BASIC evita mostrar cuerpos y encabezados de forma detallada, pero la configuración de logs completa debe revisarse antes de publicar. Si se usa HEADERS o BODY durante depuración, la redacción de encabezados es esencial y también deben protegerse los campos sensibles de los cuerpos.
10.2. Otros canales
- Copias de seguridad: excluir los archivos sensibles de las reglas aplicables de respaldo y transferencia.
- URLs: evitar tokens en rutas y parámetros de consulta.
- Pantallas: evaluar controles como
FLAG_SECUREen vistas especialmente sensibles, según la experiencia y requisitos. - Reportes y analítica: filtrar credenciales de excepciones, eventos y trazas.
- Red: utilizar HTTPS y configurar cuidadosamente cualquier excepción para desarrollo local.
Cifrar el almacenamiento local no impide que un token aparezca por estos canales.
11. Errores habituales y su diagnóstico
| Síntoma | Posible causa | Qué revisar |
|---|---|---|
| Login correcto, pero sigue mostrándose login | Sesión no publicada u observada | Que repositorio y ViewModels compartan fuente de estado |
| Tras reiniciar se pierde el login | Credenciales solo en memoria | Persistencia local y restauración del estado |
| Los tokens aparecen en archivos legibles | Se guardaron directamente en Preferences DataStore | Cifrado de aplicación y modelo de amenazas |
401 en peticiones protegidas |
Token ausente, expirado o inválido | Interceptor, contrato de API y respuesta del servidor |
403 al publicar |
Identidad sin permiso suficiente | Autorización real en servidor, no solo controles de UI |
| Se repite refresh indefinidamente | Lógica recursiva o reintentos sin límite | Exclusiones, conteo de respuestas y cliente de renovación |
| Dos peticiones simultáneas invalidan la sesión | Refresh token rotado usado varias veces | Exclusión mutua y detección de token ya actualizado |
| Logout funciona visualmente, pero el token continúa válido | Revocación remota fallida | Política de servidor, conectividad y vigencia de credenciales |
| Falla al descifrar después de restaurar respaldo | Archivo restaurado sin clave del dispositivo original | Reglas de respaldo y nuevo login |
La interfaz muestra rol de docente, pero el servidor responde 403 |
Rol local desactualizado o alterado | No confiar en datos locales para permisos |
| Error de lectura de DataStore | Datos corruptos o error de almacenamiento | Tratamiento explícito de excepciones y corrupción |
Conexión transversal: alcance y decisiones
Sesiones, errores y recursos protegidos
La decisión de refrescar ante 401 depende del endpoint y del código de error del servidor: fallar al iniciar sesión no es equivalente a que expire un access token. Si el refresh también es inválido, corresponde invalidar la sesión local según el contrato. Un error temporal de red no demuestra por sí mismo revocación.
Ruta de un recurso privado: sesión → petición con Authorization → imagen o dato protegido → posible caché local → cierre/cambio de cuenta. Cifrar el token no protege automáticamente una imagen privada que quedó cacheada. Se deben definir políticas de cacheo y separación/limpieza por usuario según el riesgo; véase la guía 10. Una URL recibida por QR tampoco es fiable solo por tener sintaxis válida (guía 12).
12. Vocabulario fundamental
| Término | Definición |
|---|---|
| Autenticación | Comprobación de la identidad que solicita acceso |
| Autorización | Evaluación de permisos para una operación |
| Sesión | Estado que permite mantener la identidad del usuario entre solicitudes |
| JWT | Formato de token que encapsula claims; puede estar firmado o cifrado según su tipo |
| Claim | Afirmación incluida en el contenido de un token |
| Bearer token | Credencial que puede utilizar quien la posea |
| Access token | Credencial para acceder a recursos protegidos |
| Refresh token | Credencial para solicitar nuevos accesos |
| Rotación | Sustitución de credenciales antiguas por nuevas |
| Revocación | Invalidación de credenciales antes de su expiración normal |
| DataStore | Biblioteca de Android para persistencia de preferencias u objetos tipados |
| Android Keystore | Sistema para gestionar claves criptográficas no exportables |
| AES-GCM | Modo de cifrado autenticado que protege contenido e integridad |
| Interceptor | Componente que procesa solicitudes y respuestas HTTP en OkHttp |
| Authenticator | Componente que puede producir una nueva petición ante desafíos de autenticación |
| Modelo de amenazas | Identificación de activos, atacantes, riesgos y mitigaciones |
13. Preguntas de autoevaluación
Comprensión conceptual
- Explica la diferencia entre autenticación y autorización. ¿Por qué un
403no se resuelve normalmente mostrando de nuevo el formulario de login? - ¿Qué partes de un JWT firmado pueden leer terceros? ¿Qué garantiza la firma y qué no garantiza?
- ¿Por qué un JWT firmado y válido sigue siendo peligroso cuando se filtra?
- ¿Qué problema resuelve la separación entre token de acceso y token de refresco?
- ¿Qué significa rotar un refresh token y qué riesgo aparece si dos solicitudes intentan renovarlo simultáneamente?
- ¿Por qué la presencia de una sesión guardada localmente no garantiza que siga autorizada en el servidor?
- ¿Qué diferencia existe entre DataStore, almacenamiento cifrado y Android Keystore?
- ¿Por qué el rol mostrado por la UI no constituye evidencia suficiente para autorizar una operación?
Interpretación de código
- ¿Qué tres casos representa
SessionStatey qué problemas evita el casoLoading? - En
BearerInterceptor, ¿qué ventaja tiene recibir() -> String?en lugar de una cadena inmutable creada al iniciar la app? - Si un desarrollador registra las peticiones con nivel
BODY, ¿qué credenciales podrían filtrarse, incluso al usarredactHeader("Authorization")? - Un
Authenticatorresponde a todos los401llamando al endpoint de refresh, que utiliza el mismo cliente. ¿Qué comportamiento puede aparecer y cómo se evita? - ¿Qué ocurre si una petición a la API responde
401, el refresh respondeIOExceptiony la app elimina igualmente la sesión? - Examina el cierre de sesión del apartado 9. ¿Qué consigue borrar primero lo local y qué limitación permanece si falla la petición remota?
Aplicación y diagnóstico
- Un estudiante altera en DataStore su rol local para obtener un botón de publicación. Explica qué debe ocurrir en la UI y en el servidor.
- Una aplicación conserva un refresh token cifrado en DataStore. Se restaura el archivo en otro dispositivo sin transferir la clave del Keystore. ¿Qué problema habrá y cómo debería responder el cliente?
- Dos solicitudes protegidas reciben
401por expiración en el mismo instante. Diseña, a nivel conceptual, el flujo que evita dos refresh simultáneos. - El servidor revoca un refresh token, pero el access token sigue vigente durante algunos minutos. Explica qué depende de las decisiones de validación del servidor.
- Una app utiliza HTTPS, pero registra el encabezado
Authorizationy las contraseñas de los formularios. ¿Qué amenazas permanecen y cómo se reducen? - Una aplicación permite consultar documentos a todos los usuarios, pero eliminarlos solo al propietario. ¿Qué verificaciones deben hacerse en el cliente y en el servidor?
14. Respuestas posibles
- Autenticar confirma la identidad; autorizar verifica permisos. En un
403, la identidad puede estar comprobada pero carecer del permiso necesario. El cliente debe informar y no asumir que nuevas credenciales resuelven el problema. - Header y payload de un JWT firmado no cifrado son legibles tras decodificarlos. La firma permite comprobar integridad y procedencia bajo validación correcta. No oculta los datos ni impide que se use un token robado.
- Un token Bearer válido es suficiente para presentar solicitudes en nombre del usuario mientras el servidor lo acepte. Un atacante no necesita alterar su contenido.
- Permite usar una credencial de acceso de vida corta y otra destinada a renovar el acceso, con políticas diferentes de almacenamiento, rotación y revocación.
- Rotar significa reemplazar el token usado. Si dos solicitudes presentan el mismo refresh y la primera lo invalida, la segunda puede ser rechazada. Se necesita coordinar la operación.
- El servidor puede haber revocado la sesión, cambiado permisos o rechazado el token por expiración. El estado local representa lo almacenado, no una garantía remota.
- DataStore persiste valores; cifrar los valores requiere un mecanismo adicional; Keystore protege las claves empleadas en operaciones criptográficas. Cumplen funciones diferentes y complementarias.
- La UI puede modificarse, tener datos atrasados o ser sustituida por otro cliente. El servidor tiene que tomar la decisión definitiva sobre cada acción protegida.
Loading,SignedOutySignedIn.Loadingevita mostrar prematuramente login mientras se restaura una sesión desde almacenamiento.- Permite leer la credencial más reciente en cada petición. Una cadena fijada al inicio seguiría usando el token viejo después de una renovación.
- Podrían registrarse contraseñas, refresh tokens y otros secretos enviados en el cuerpo; redactar un encabezado no elimina los campos sensibles del body.
- Puede aparecer recursión o una sucesión indefinida de refresh. Se debe excluir la propia renovación, limitar intentos y, cuando sea oportuno, utilizar un cliente separado.
- Expulsaría al usuario por una falla de conectividad, aunque el servidor no haya revocado el refresh token. Conviene conservar la sesión local y manejar la falta de red de forma distinta.
- El usuario pierde acceso local de inmediato, incluso sin red. La revocación remota puede no ocurrir, por lo que el token anterior podría seguir siendo válido hasta la expiración o una revocación posterior.
- El botón podría mostrarse incorrectamente si se basa en ese dato; al recibir la petición, el servidor debe verificar identidad y permisos reales y responder
403cuando no corresponda. - El archivo cifrado podría ser ilegible sin la clave original. La app debe detectar el fallo, descartar la sesión irrecuperable y solicitar un nuevo login sin cerrarse inesperadamente.
- Una solicitud inicia la renovación bajo sincronización. La otra espera; al continuar comprueba si ya existe un access token distinto del utilizado originalmente y lo reutiliza. Se almacena la pareja de tokens de manera consistente.
- Si solo se comprueba firma y fecha del access token, puede continuar aceptándose hasta expirar. Si hay comprobaciones adicionales de revocación, el servidor puede denegarlo antes.
- HTTPS protege el tránsito, pero no los registros locales o remotos. Deben evitarse logs con cuerpos sensibles, ocultar encabezados y revisar herramientas de diagnóstico y reportes de error.
- El cliente puede ocultar o deshabilitar el botón para usuarios que no sean propietarios. El servidor debe validar el token, identificar al solicitante, consultar la propiedad del recurso y rechazar operaciones no autorizadas, independientemente de la interfaz.
15. Relaciones con las guías anteriores
| Guía | Conocimientos utilizados aquí |
|---|---|
| 02. Arquitectura y estado | MVVM, estado observable, composables, navegación y alcance |
| 03. Corrutinas | suspend, viewModelScope, excepciones y cancelación |
| 04. API REST | Retrofit, OkHttp, interceptores, DTO y códigos HTTP |
| 05. Persistencia local | DataStore como alternativa a Room para preferencias, Flow y stateIn |
Referencias de consulta
- Android Developers. Android Keystore system.
- Android Developers. DataStore.
- OkHttp. Documentación y APIs de interceptores y autenticación.
- IETF. RFC 7519, JSON Web Token (JWT).
Alcance del material. La práctica original utilizó un servidor académico con tiempos de expiración, roles y respuestas particulares. Los ejemplos de esta guía generalizan sus conceptos; para construir una aplicación de producción deben verificarse los contratos de API, los requisitos de seguridad y las versiones de las bibliotecas efectivamente utilizadas.