Spotify-Integration: Client-Credentials-Flow und Token-Cache (Teil 11)

EchoPlay kann Hörspielserien direkt aus dem Spotify-Katalog importieren. Dafür muss sich die Anwendung bei der Spotify Web API authentifizieren — und zwar ohne dass der Nutzer sein Spotify-Konto verknüpfen muss. Wie das funktioniert, wie der Access Token automatisch jedem Request beigelegt wird und warum ein DelegatingHandler dabei die zentrale Rolle spielt, zeigt dieser Artikel.

Client-Credentials-Flow: Anmeldung ohne Nutzer

Spotify bietet mehrere Authentifizierungsverfahren. EchoPlay nutzt den Client-Credentials-Flow: Die App meldet sich mit ihrer eigenen Identität an — mit ClientId und ClientSecret — und bekommt einen zeitlich begrenzten Access Token zurück. Das Verfahren funktioniert ohne Nutzer-Login, ohne Browser-Weiterleitung und ohne Spotify-Konto des Nutzers. Es ist für reine Lesezugriffe auf den öffentlichen Katalog gedacht — genau das, was EchoPlay für die Suche nach Hörspielserien braucht.

Der Ablauf ist simpel: Der SpotifyTokenClient sendet einen POST /api/token mit Base64-codierten Credentials. Spotify antwortet mit einem access_token und einer Gültigkeitsdauer in Sekunden (expires_in). Der Token wird gecacht und bei jedem API-Request im Authorization-Header mitgesendet. Läuft er ab, wird automatisch ein neuer geholt.

SpotifyTokenClient: Token holen und cachen

public async Task<string> GetAccessTokenAsync()
{
    // Gecachten Token zurückgeben, wenn er noch mindestens 60 Sekunden gültig ist
    if (_cachedToken is not null && DateTime.UtcNow < _tokenExpiresAt - TimeSpan.FromSeconds(60))
    {
        return _cachedToken;
    }

    // Neue Anfrage – HttpRequestMessage nicht wiederverwenden (einmalig verwendbar)
    HttpRequestMessage CreateRequest()
    {
        string credentials = Convert.ToBase64String(
            Encoding.UTF8.GetBytes($"{_options.ClientId}:{_options.ClientSecret}"));

        HttpRequestMessage request = new(HttpMethod.Post, "api/token");
        request.Headers.Authorization = new("Basic", credentials);
        request.Content = new FormUrlEncodedContent(new[]
        {
            new KeyValuePair<string, string>("grant_type", "client_credentials")
        });

        return request;
    }

    HttpResponseMessage response = await _httpClient.SendAsync(CreateRequest());
    response.EnsureSuccessStatusCode();

    // JSON parsen und Token speichern
    // ...

    _tokenExpiresAt = DateTime.UtcNow.AddSeconds(tokenDto.ExpiresIn);
    _cachedToken = tokenDto.AccessToken;
    return _cachedToken;
}

Der 60-Sekunden-Puffer ist ein bewusstes Design-Detail: Der Token wird bereits 60 Sekunden vor dem tatsächlichen Ablauf als ungültig behandelt. Das verhindert eine Race Condition — also den Fall, dass ein Request mit einem Token gesendet wird, der in der Millisekunde zwischen Prüfung und HTTP-Übertragung abläuft.

Wichtig ist auch, dass HttpRequestMessage in .NET nicht wiederverwendbar ist. Ein einmal gesendetes Request-Objekt wird intern als „consumed“ markiert. Deshalb ist CreateRequest() eine Factory-Funktion, die bei jedem Aufruf ein neues Objekt erstellt. Das Base64-Encoding der Credentials folgt dem HTTP-Basic-Authentication-Format: ClientId:ClientSecret, Base64-kodiert, im Authorization: Basic-Header — so erwartet es die Spotify-API.

DelegatingHandler: Automatische Token-Injektion

Jeder API-Request an Spotify braucht Authorization: Bearer <token>. Man könnte das in jedem API-Aufruf separat tun — aber das wäre Duplikation und fehleranfällig. Ein DelegatingHandler ist eine Middleware in der HttpClient-Pipeline. Er wird für jede ausgehende Anfrage aufgerufen, bevor sie den Netzwerk-Stack erreicht:

public sealed class SpotifyAuthMessageHandler(SpotifyTokenClient tokenClient) : DelegatingHandler
{
    private readonly SpotifyTokenClient _tokenClient = tokenClient;

    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        // Token pro Request abfragen – TokenClient liefert gecachten Token oder holt neuen
        string accessToken = await _tokenClient.GetAccessTokenAsync();

        request.Headers.Authorization = new("Bearer", accessToken);

        return await base.SendAsync(request, cancellationToken);
    }
}

Der SpotifyApiClient muss sich um kein Token kümmern. Er sendet einen normalen Request, und der Handler injiziert den Token automatisch. Die Konfiguration in der Dependency Injection sieht so aus:

builder.Services.AddHttpClient<SpotifyApiClient>((services, client) =>
{
    SpotifyOptions options = services.GetRequiredService<SpotifyOptions>();
    client.BaseAddress = new(options.ApiBaseUrl);
    client.Timeout = TimeSpan.FromSeconds(15);
})
.AddHttpMessageHandler<SpotifyAuthMessageHandler>();

.AddHttpMessageHandler<SpotifyAuthMessageHandler>() hängt den Handler in die Pipeline des SpotifyApiClient-HttpClients ein. Jeder Request, der über diesen Client gesendet wird, durchläuft automatisch den Handler — ohne dass der aufrufende Code etwas davon mitbekommt.

SpotifyApiClient: Suche und Episodendaten

public async Task<IReadOnlyList<SpotifyArtistDto>> SearchArtistsAsync(string query, int limit)
{
    // Factory-Funktion, weil HttpRequestMessage nicht wiederverwendbar ist
    HttpResponseMessage response = await SpotifyHttpRetry.SendWithRetryAsync(
        () => _httpClient.GetAsync($"v1/search?q={Uri.EscapeDataString(query)}&type=artist&limit={limit}"));

    response.EnsureSuccessStatusCode();

    string json = await response.Content.ReadAsStringAsync();
    // JSON parsen...
}

Uri.EscapeDataString ist wichtig: Suchanfragen wie „Die drei ???“ enthalten Sonderzeichen, die in URLs kodiert werden müssen. Ohne Encoding wäre die URL kaputt. Die Retry-Logik über SpotifyHttpRetry.SendWithRetryAsync wird im nächsten Artikel ausführlich erklärt.

DTOs: Nur holen, was gebraucht wird

Die Spotify-API liefert deutlich mehr Daten als EchoPlay benötigt. DTOs (Data Transfer Objects) sind einfache C#-Klassen, die nur die relevanten Felder enthalten:

public record SpotifyArtistDto(
    string SpotifyArtistId,
    string Name,
    int Popularity,
    IReadOnlyList<string> Genres,
    string? ImageUrl
);

Das Schlüsselwort record erzeugt in C# einen Datentyp mit Wert-Semantik und eingebautem ToString, Equals und GetHashCode. Für reine Datenbehälter wie DTOs ist das die richtige Wahl — weniger Boilerplate-Code, mehr Klarheit. IReadOnlyList<string> für die Genres stellt sicher, dass die Liste von außen nicht verändert werden kann.

Was nicht gemacht werden sollte

// FALSCH: Credentials im Code
string credentials = Convert.ToBase64String(
    Encoding.UTF8.GetBytes("meine-client-id:mein-secret"));

Credentials gehören nie in den Code — nicht einmal als Placeholder. In EchoPlay kommen sie aus appsettings.json bzw. appsettings.Development.json, die nicht in Git eingecheckt werden. Für lokale Entwicklung nutzt man idealerweise User Secrets — ein Mechanismus in .NET, der sensible Werte außerhalb des Projektordners speichert.

// FALSCH: Token ohne Caching – jeder Request holt neuen Token
public async Task<string> GetAccessTokenAsync()
{
    // POST /api/token bei JEDEM Aufruf
}

Ohne Token-Caching würde jede API-Anfrage mit zwei HTTP-Requests enden: erst Token holen, dann eigentliche Anfrage. Das ist doppelter Netzwerk-Overhead und kann schnell zum Rate-Limiting führen — Spotify erlaubt keine unbegrenzte Anzahl von Token-Anfragen. Der SpotifyTokenClient von EchoPlay vermeidet das, indem er den Token im Speicher hält und erst bei drohendem Ablauf einen neuen anfordert.

Die gezeigten Code-Beispiele dienen zur Veranschaulichung. Nutzung auf eigene Verantwortung. Mehr dazu