Android Coden
Android 7 min lesen

Authentication Tokens in Android-Apps

Tokens halten Sitzungen nutzbar. Du lernst, wie Bearer Tokens, Refresh und Storage in Android sauber zusammenspielen.

Authentication Tokens sind kleine, aber kritische Bausteine in fast jeder App mit Login. Sie entscheiden, ob ein Request als berechtigt gilt, wie lange eine Sitzung lebt und was beim Logout wirklich verschwindet. Wenn du Android-Apps mit Kotlin, Coroutines, Flow und einer sauberen Daten-Schicht baust, behandelst du Tokens nicht als zufälligen String, sondern als Teil deiner Architektur.

Was ist das?

Ein Authentication Token ist ein Nachweis, den deine App nach einem erfolgreichen Login vom Server bekommt. Statt bei jedem Request Benutzername und Passwort zu senden, hängt die App ein Token an den Request. Häufig ist das ein Bearer Token: Der Server akzeptiert den Request, wenn der Header etwa Authorization: Bearer <token> enthält und das Token gültig ist.

Das mentale Modell ist einfach genug, um damit zu starten: Ein Access Token ist wie ein zeitlich begrenzter Ausweis für API-Requests. Ein Refresh Token ist ein länger gültiger Schlüssel, mit dem die App ein neues Access Token holen kann, wenn das alte abgelaufen ist. Beide Werte sind sensibel. Wer sie auslesen kann, kann im Namen des Nutzers handeln, solange der Server sie akzeptiert.

Im Android-Kontext betrifft das mehrere Schichten. Die UI in Jetpack Compose sollte nur wissen, ob der Nutzer angemeldet ist und welche Aktion gerade läuft. Das ViewModel koordiniert Zustände. Die Daten-Schicht kümmert sich um Login, Request-Autorisierung, Token-Refresh, persistente Speicherung und Logout. Genau dort gehört Token-Logik hin, weil sie direkt mit Netzwerk, Storage und Fehlerbehandlung zusammenhängt.

Wichtig ist die Abgrenzung: Authentication Tokens sind kein allgemeines Session-Management-Tutorial und auch kein vollständiges Kryptografie-Thema. Für deinen Alltag als Android-Entwickler zählt zuerst, dass Tokens nicht verstreut im Code liegen, nicht in Logs auftauchen, korrekt erneuert werden und beim Logout zuverlässig gelöscht sind.

Wie funktioniert es?

Der typische Ablauf beginnt mit einem Login-Request. Der Server prüft die Zugangsdaten und liefert ein Access Token, oft zusätzlich ein Refresh Token. Die App speichert diese Werte über eine eigene Komponente, zum Beispiel ein TokenStore. Danach fügt ein Netzwerk-Layer das Access Token automatisch an geschützte Requests an.

Wenn der Server einen Request wegen eines abgelaufenen Access Tokens ablehnt, meist mit HTTP 401, startet die App einen Refresh. Dafür sendet sie das Refresh Token an einen speziellen Endpoint. Bei Erfolg erhält sie ein neues Access Token, speichert es und wiederholt den ursprünglichen Request. Bei Fehlern, etwa wenn auch das Refresh Token ungültig ist, muss die App den Nutzer ausloggen oder in einen klaren Anmeldezustand bringen.

Coroutines passen gut zu dieser Aufgabe, weil Netzwerk und Storage asynchron sind. Eine Repository- oder Data-Source-Funktion kann suspend sein und dadurch lesbar bleiben. Gleichzeitig musst du Nebenläufigkeit ernst nehmen: Wenn fünf Requests parallel 401 zurückbekommen, sollten sie nicht alle gleichzeitig einen Refresh starten. Sonst erzeugst du doppelte Serverlast, überschreibst Tokens in ungünstiger Reihenfolge oder bekommst schwer nachvollziehbare Fehler.

Flow ist nützlich, um Auth-Zustand zu beobachten. Ein Flow<AuthState> kann der App mitteilen, ob Tokens vorhanden sind, ob ein Refresh läuft oder ob ein Logout erfolgt ist. Compose sammelt diesen Zustand über das ViewModel ein und zeigt die passende Oberfläche. Die UI entscheidet aber nicht selbst, wie ein Token erneuert wird.

Storage ist der dritte Kernpunkt. Tokens sollten nicht in normalen Logs, Debug-Ausgaben oder ungeschützten Dateien landen. In realen Apps kapselst du Speicherung hinter einer Schnittstelle. Ob du dafür DataStore, verschlüsselte Speicherung oder eine Plattformlösung nutzt, hängt von Sicherheitsanforderungen, App-Typ und Backend-Vorgaben ab. Die wichtige Regel lautet: Der Rest der App sollte keine Details kennen. Er fragt den TokenStore, nicht die konkrete Datei oder Implementierung.

Offline-First-Architektur ändert die Verantwortung nicht, macht sie aber sichtbarer. Eine App kann lokale Daten anzeigen, obwohl der Nutzer gerade offline ist. Trotzdem darf sie geschützte Serveraktionen nicht so tun, als wären sie autorisiert, wenn Tokens fehlen oder ungültig sind. Du trennst also lokalen Lesestatus von Server-Berechtigung. Das hilft bei stabilen Apps, die auch bei schlechter Verbindung verständlich reagieren.

In der Praxis

Eine robuste Struktur beginnt mit klaren Rollen. Dein AuthRepository spricht mit der API und kennt Login, Refresh und Logout. Dein TokenStore speichert und liefert Tokens. Dein Netzwerk-Client fragt das Access Token ab und reagiert auf 401. Das ViewModel ruft nur fachliche Aktionen auf, etwa login() oder logout().

Ein vereinfachtes Beispiel zeigt das Prinzip. Es ist bewusst kompakt und lässt konkrete HTTP-Client-Details weg:

data class AuthTokens(
    val accessToken: String,
    val refreshToken: String
)

interface TokenStore {
    val tokens: Flow<AuthTokens?>
    suspend fun readTokens(): AuthTokens?
    suspend fun saveTokens(tokens: AuthTokens)
    suspend fun clearTokens()
}

class AuthRepository(
    private val api: AuthApi,
    private val tokenStore: TokenStore
) {
    private val refreshMutex = Mutex()

    suspend fun login(email: String, password: String) {
        val tokens = api.login(email, password)
        tokenStore.saveTokens(tokens)
    }

    suspend fun refreshAccessToken(): AuthTokens? = refreshMutex.withLock {
        val current = tokenStore.readTokens() ?: return null

        val refreshed = api.refresh(current.refreshToken)
        tokenStore.saveTokens(refreshed)
        refreshed
    }

    suspend fun logout() {
        tokenStore.clearTokens()
    }
}

Der wichtige Teil ist der Mutex. Er verhindert, dass mehrere Coroutines gleichzeitig den Refresh-Code ausführen. In einer echten App würdest du zusätzlich prüfen, ob ein anderer Request das Token während des Wartens bereits erneuert hat. Dann muss nicht jeder wartende Request erneut refreshen. Diese Optimierung ist besonders relevant, wenn du viele API-Aufrufe beim App-Start hast.

Ein Request-Ablauf kann dann so aussehen:

class AuthorizedApiClient(
    private val rawClient: RawApiClient,
    private val tokenStore: TokenStore,
    private val authRepository: AuthRepository
) {
    suspend fun getProfile(): Profile {
        val token = tokenStore.readTokens()?.accessToken
            ?: throw NotLoggedInException()

        val firstResult = rawClient.getProfile(
            authorization = "Bearer $token"
        )

        if (firstResult !is ApiResult.Unauthorized) {
            return firstResult.requireBody()
        }

        val newTokens = authRepository.refreshAccessToken()
            ?: throw NotLoggedInException()

        val retryResult = rawClient.getProfile(
            authorization = "Bearer ${newTokens.accessToken}"
        )

        return retryResult.requireBody()
    }
}

Das Beispiel zeigt eine konkrete Entscheidungsregel: Ein Request darf genau einmal nach einem erfolgreichen Refresh wiederholt werden. Wenn auch der zweite Versuch 401 liefert, behandelst du das als Auth-Fehler. Wiederholst du endlos, baust du Schleifen, die Akku, Datenvolumen und Serverkapazität verschwenden.

Eine typische Stolperfalle ist Token-Logik in der Compose-Oberfläche. Wenn ein Button im Composable direkt Tokens liest, Header baut oder Refresh auslöst, wird dein Code schwer testbar und fehleranfällig. Compose sollte Zustände darstellen und Ereignisse melden. Die Entscheidung, wann ein Token gültig ist, liegt in der Daten-Schicht.

Eine zweite Stolperfalle ist unvollständiger Logout. Es reicht nicht, nur den Bildschirm zur Login-Seite zu wechseln. Beim Logout löschst du Tokens, brichst laufende geschützte Aktionen ab oder lässt sie kontrolliert fehlschlagen, räumst sensible gecachte Daten auf, falls sie nicht mehr sichtbar sein dürfen, und setzt den Auth-Zustand für die UI. Wenn du Push-Tokens, Geräte-Registrierungen oder serverseitige Sessions nutzt, kann zusätzlich ein Server-Logout nötig sein.

Auch Logging ist kritisch. Ein HttpLoggingInterceptor oder eigene Debug-Ausgaben dürfen den Authorization-Header nicht vollständig ausgeben. In Entwicklung wirkt das praktisch, in echten Builds ist es ein Risiko. Eine gute Code-Review-Frage lautet: Kann ein Token in Logcat, Crash-Reports, Analytics-Events oder Fehlermeldungen landen? Wenn ja, ändere die Stelle.

Zum Testen brauchst du keine riesige Infrastruktur. Schreibe Unit-Tests für drei Fälle: Ein gültiges Token wird an Requests gehängt. Ein abgelaufenes Access Token führt zu genau einem Refresh und danach zu einem Retry. Ein ungültiges Refresh Token führt zu einem leeren TokenStore und einem ausgeloggten Zustand. Für Nebenläufigkeit testest du mehrere parallele Aufrufe und prüfst, dass nur ein Refresh-Request ausgelöst wird.

Im Debugger kannst du zusätzlich den Ablauf Schritt für Schritt verfolgen. Starte mit einem gespeicherten Access Token, simuliere eine 401-Antwort, beobachte den Refresh und prüfe danach den neuen Header. Danach prüfst du den Logout: Nach clearTokens() darf kein geschützter Request mehr mit altem Header gesendet werden. Genau diese einfachen Prüfungen finden viele echte Fehler.

Fazit

Authentication Tokens sind kein Detail, das du nebenbei in irgendeinem Screen unterbringst. Behandle Bearer Tokens, Refresh und Storage als zusammenhängenden Teil deiner Daten-Schicht: klar gekapselt, asynchron sauber umgesetzt, nebenläufigkeitssicher und beim Logout konsequent aufgeräumt. Prüfe dein Verständnis aktiv, indem du einen kleinen TokenStore baust, 401-Antworten in Tests simulierst, parallele Requests laufen lässt und im Code-Review gezielt nach Token-Leaks, Endlosschleifen und UI-Logik suchst.

Quellen (5)
Redaktion

Geschrieben von

Redaktion

Das Redaktionsteam recherchiert und schreibt Artikel zu aktuellen Themen rund um Tech, Lifestyle und Ratgeber.