Authorization mit NSwag Client und OpenIddict
In diesem Beitrag zeige ich wie man den von NSwag automatisch generierten Client um die Authorization mit OpenIddict erweitert. Der Client authentifiziert sich und bekommt ein Bearer Token. Damit lassen sich dann alle abgesicherten Endpoints ansteuern bzw. alle abgesicherten Ressourcen abfragen.
Authorization mit NSwag Client und OpenIddict
Vor einiger Zeig habe ich gezeigt wie man für eine ASP.NET Web API automatisch einen Client erstellt. Zum Einsatz kam dabei das NSwag NuGet beziehungsweise das NSwagStudio. Der Client ist nun soweit, dass er die Endpunkte über den automatisch erstellten Client Code abfragt und die Business Logik soweit umgesetzt ist. Für einen produktiven Einsatz möchte man üblicherweise die API vor unberechtigten Zugriff schützen. Eine Möglich das mit Hilfe eines fertigen NuGets schnell, einfach und sicher umzusetzen ist OpenIddict. Im letzten Tutorial habe ich noch gezeigt wie man die API mit OpenIddict absichert.
OpenIddict
Warum OpenIddict? Das NuGet ist frei verfügbar und das Projekt Open Source. Ursprünglich wollte ich den IdentityServer verwenden. Da die freie Variante aber nur noch begrenzte Zeit supported wird und IdentityServer 5 nun kommerziell ist habe ich mich anders entschieden. Wie baut man nun OpenIddict in den automatisch generierten Client Code ein? Meine Lösung basiert auf diesem StackOverflow Post.
Partial Client
NSwag erstellt eine Client Klasse. Diese ist mit dem Schlüsselwort partial definiert. Dadurch ist es möglich die Klasse in mehreren Code Dateien zu definieren, der Compiler baut daraus dann ein Objekt.
Dieser Ansatz ist insbesondere dann interessant, wenn eine Datei automatisch erstellt wird. Änderungen dort würden mit dem nächsten Erstellvorgang überschrieben. Schreibt man alle Erweiterungen in eine eigene Quelldatei die diese partial class erweitert, dann bleiben diese stets erhalten.
Die fertige Lösung sieht im Projekt wie folgt aus:
Zuletzt hat der automatisch erstellte Code ausgereicht um mit der Schnittstelle zu arbeiten. Die Authorisation bzw. die Authentifizierung sind damit nicht abgedeckt, NSwag kann damit nicht umgehen. Als erstes habe ich ein LoginData Model erstellt in dem die Daten definiert werden, die man für eine Authorisation benötigt. In meinem Fall sind das zwei Strings clientId und clientSecret. Die üblichen Login Daten die man im Web gewohnt ist. Jeder Benutzer bekommt ein solches Paar. Im Laufe des Projekts können sich diese Informationen möglicherweise ändern, man benötigt aber immer Login daten. Diese sind nun mit dem LoginData Model abstrahiert. Das vollständige Model (LoginData.cs):
public class LoginData { public LoginData(string clientId, string clientSecret) { ClientId = clientId; ClientSecret = clientSecret; } public string ClientId { get; set; } public string ClientSecret { get; set; } }
Die vollständige Erweiterung des Clients (Client.cs):
public partial class Client { public string JwtToken { set; get; } partial void PrepareRequest(HttpClient client, HttpRequestMessage request, string url) { request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", JwtToken); } public async Task AuthorizeAsync(HttpClient client, string url, LoginData data) { // do authorization with given login data to AuthorizationController var request = new HttpRequestMessage(HttpMethod.Post, url + "/connect/token"); request.Content = new FormUrlEncodedContent(new Dictionary<string, string> { ["grant_type"] = "client_credentials", ["client_id"] = data.ClientId, ["client_secret"] = data.ClientSecret }); var response = await client.SendAsync(request, HttpCompletionOption.ResponseContentRead); var payload = await response.Content.ReadFromJsonAsync(); if (!string.IsNullOrEmpty(payload.Error)) { return false; } // save returned token JwtToken = payload.AccessToken; return true; } }
Die Client Klasse hat nun ein neues Member JwtToken, einmal erfolgreich registriert ist dort das Token hinterlegt. Wie kommt man zu dem Token? Der Client muss einmalig die AuthorizeAsync Methode aufrufen. Übergeben werden dieser unter anderem ein LoginData Objekt. Mit diesen wird versucht sich am OpenIddict Authorization Controller (mit der Uri /connect/token) zu authorisieren. Ist der OpenIddictResponse leer, hat es nicht funktioniert. Falls schon, dann ist das AccessToken vorhandne und kann für alle weiteren Requests im Client Objekt gespeichert werden.
Die PrepareRequest Methode wird implizit vor jedem Request vor dem Client aufgerufen. Dort geben wir nun einfach immer das Bearer Token (JwtToken Variable) als Authorization Header mit. Nur wenn dort ein valides Token hinterlegt ist wird kein 401 Response geliefert.
Beispiel
Die tatsächliche Implementierung eines Clients der den automatisch generierten NSwag Client verwendet und dazu eine OpenIddict abgesicherte API sieht bei mir wie folgt aus:
Fazit
Ich habe gezeigt wie man schnell und einfach den automatisch erstellten Code von NSwag um die Authorisierung mit einem Token erweitert. Mit nur wenigen Zeilen Code wird ein erheblichen Maß an Sicherheit der Schnittstelle hinzugefügt. Unberechtigter Zugriff ist nun nicht mehr möglich, dafür ist es aber immer noch sehr einfach einen eigenen Client zu schreiben.