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