Blog

Mocken Sie Ihren OpenID Connect Provider

Kristof Riebbels

Aktualisiert Oktober 15, 2025
20 Minuten

Ein Artikel, der Ihnen zeigt, wie Sie Ihre Endpunkte mit OpenID Connect testen können. Und das alles, ohne die Authentifizierungs- und Autorisierungskonfigurationen in dotnet 6 zu ändern oder nachzubilden.

Wenn wir neue Anwendungen erstellen oder bestehende Anwendungen aktualisieren, müssen wir die Sicherheit berücksichtigen. Das Protokoll, das wir in diesem Artikel verwenden werden, ist OpenID Connect flow.

Der OpenID Connect Flow ist ein Protokoll, das für die Authentifizierung und Autorisierung zwischen verschiedenen Parteien verwendet wird, z.B. einer Client-Anwendung und einem Server, der Identitätsdienste anbietet.

Eine realistische Testumgebung zu einem frühen Zeitpunkt ermöglicht ein genaueres Testen des Verhaltens der Anwendung. Auf diese Weise lassen sich Probleme im Zusammenhang mit dem Authentifizierungs- und Autorisierungsprozess erkennen, die in einer Testumgebung mit anderen Einstellungen möglicherweise nicht sichtbar sind.

Es sorgt dafür, dass die Anwendung sicher ist und den Industriestandards in einer CI-Pipeline entspricht.

Eine Geschichte, wie sie Ihnen helfen kann...

Ein Kunde benötigte die Unterstützung von zwei Identitätsanbietern für den Zugriff auf unseren Ressourcenserver, und unser Entwickler implementierte die entsprechenden Authentifizierungsschemata. Später änderte sich die Anforderung dahingehend, dass nur ein Identitätsanbieter unterstützt werden sollte, und der entsprechende Code wurde entfernt. Alles schien gut zu funktionieren, bis ein anderes Team versuchte, auf den Ressourcenserver zuzugreifen, ohne zu wissen, dass der zweite Identitätsanbieter nicht mehr unterstützt wurde. In dieser Situation wäre ein HTTP-Statuscode 401 (Nicht autorisiert) zu erwarten gewesen. Aufgrund der unvollständigen Entfernung des Codes wurde jedoch stattdessen ein HTTP 500 (Internal Server Error) angezeigt. Durch die Anwendung der in diesem Artikel beschriebenen Testmethoden konnten wir herausfinden, was passiert war, und das Problem beheben.

Isolierung testen

Es gibt verschiedene Kategorien von Tests, die jeweils unterschiedlichen Zwecken dienen: Unit-Tests, Integrationstests, Systemtests, Akzeptanztests, Leistungstests, Lasttests, Stresstests, Sicherheitstests, Benutzerfreundlichkeitstests, Regressionstests, Smoke-Tests und Kompatibilitätstests.

Die Einbindung von Integrationstests mit simulierten externen Abhängigkeiten in eine Continuous Integration (CI) Pipeline ist unerlässlich. Diese Tests konzentrieren sich auch auf die Interaktionen zwischen Ihrer Anwendung und den externen Systemen.

Es wird eine stabile und vorhersehbare Umgebung auf Ihrem eigenen Rechner und in Ihrer eigenen Pipeline geschaffen, da das Risiko von Ausfällen externer Dienste oder Änderungen, die sich auf die Testergebnisse auswirken, entfällt. Sie können Probleme leichter debuggen und neu erstellen.

Das Hauptziel des Testens besteht darin, die Qualität in allen Bereichen zu gewährleisten. Durch den Einsatz von Test-Driven Development (TDD) und die Verwendung repräsentativer, nicht-trivialer Daten erhalten die Entwickler jedoch ein besseres Verständnis für das Geschäft und die Funktionalität des Codes.

WireMock oder nicht WireMock, das ist hier die Frage

Um einen OpenID Connect Provider nachzustellen, müssen wir das Verhalten der beteiligten Endpunkte des Providers simulieren: JSON Web Key Set (JWKS) Endpunkt /jwks und der OpenID Connect discovery Endpunkt /.well-known/openid-configuration. Die Beschreibung dieser Endpunkte finden Sie in einem anderen Absatz weiter unten.

Für die Validierung des /weatherforecast Endpunkts sollte es einen Algorithmus geben, der gültige und ungültige JWTs erzeugt. Diese generierten Token werden in der Anfrage verwendet. Schließlich validiert der Ressourcenserver die Anfrage.

Es gibt mehrere Strategien, um Endpunkte vorzutäuschen:

  • Mit einer Bibliothek wie WireMock.NET können wir ganz einfach Stubs für HTTP-Anfragen und Antworten erstellen. Durch die Erstellung von JSON-Dokumenten, die die Antworten eines echten OpenID Connect Anbieters imitieren, können wir das erwartete Verhalten der Mock-Endpunkte definieren. In Ausgabe 13 des XPRT Magazins wurde WireMock.NET und seine Einrichtung besprochen. WireMock.Net hilft jedoch nicht dabei, gültige und ungültige JWTs bereitzustellen; es ist auf das Mocking von HTTP-Abhängigkeiten spezialisiert.
  • Eine andere Lösung besteht darin, den ConfigurationManager der Einstellungen von OpenID Connect zu manipulieren und einen eigenen HttpClientHandler zu verwenden.

In beiden Fällen fangen diese Lösungen die HTTP-Anfragen an diese Endpunkte ab und geben vordefinierte JSON-Dokumente als Antwort zurück.

In diesem Artikel wird der ConfigurationManager durch die Bereitstellung einer prägnanten, unkomplizierten Lösung manipuliert. Damit wir uns auf die Erstellung von Tests, die Einrichtung der Webanwendung mit Richtlinien und die Vorbereitung der notwendigen Boilerplate ohne Bibliotheken von Drittanbietern konzentrieren können, vermeiden wir es, Komplexität und zusätzliche Dimensionen hinzuzufügen.

Quellcode

Dieser Artikel enthält Codeschnipsel und Diagramme, um dem Leser einen klaren Überblick zu geben. Ich möchte Sie ermutigen, mit dem Code zu experimentieren. Sie finden den Quellcode an folgender Stelle: MockOpenIdConnectIdpAspnetcore.

JWT-Tokens

Die OpenID Connect Protokolle stützen sich auf das OAuth2-Protokoll. Der Authentifizierungs- und Autorisierungsmechanismus, den sie anbieten, verwendet JWT. Was ist also ein JWT (JSON Web Token)?

Ein JWT-Token ist eine kompakte und in sich geschlossene Methode, um Informationen zwischen zwei Parteien auf sichere Weise zu übermitteln.

Ein JWT-Token besteht aus drei Teilen, die durch Punkte getrennt sind: einem Header, einer Nutzlast und einer Signatur. Der Header enthält Informationen über die Art des Tokens und den zum Signieren verwendeten Algorithmus. Die Nutzdaten enthalten die Informationen, die übertragen werden müssen, wie z.B. die Benutzer-ID oder die Berechtigungen. Die Signatur wird verwendet, um zu überprüfen, dass der Token während der Übertragung nicht manipuliert wurde.

Hier ist ein Beispiel, dekodiert durch JSON Web Tokens

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Kopfzeile:

{
"alg": "HS256",
"typ": "JWT"
}

Körper:

{
"sub": "1234567890",
"Name": "John Doe",
"iat": 1516239022
}

Der Algorithmus, der zum Signieren des JWT-Tokens verwendet wird, ist in der Kopfzeile angegeben. Es gibt mehrere Algorithmen, die verwendet werden können, z. B. HS256, RS256 und andere... RS256 steht für RSA-SHA256, ein asymmetrischer Verschlüsselungsalgorithmus, der einen öffentlichen Schlüssel zur Verschlüsselung und einen privaten Schlüssel zur Entschlüsselung verwendet.

Die öffentlichen Schlüssel zur Validierung von JWTs werden vom OpenID Connect Provider bereitgestellt. Der Ressourcenserver sollte in der Lage sein, auf die öffentlichen Schlüssel zuzugreifen, damit die JWTs validiert werden können.

Warum wird ein privater Schlüssel eines Zertifikats als privat bezeichnet?

In diesem Artikel werden wir ein selbstsigniertes Zertifikat generieren, das uns hilft, gültige und ungültige JWTs zu erzeugen. Denken Sie daran, dass die Zertifikate von OpenID Connect IdP-Providern in Produktions- und Testumgebungen vertraulich behandelt und nicht offengelegt werden sollten. Außerdem sollte es einen Mechanismus zur regelmäßigen Aktualisierung der Zertifikate geben.

Angreifer können sich als IdP (Identity Provider) ausgeben, indem sie das durchgesickerte Zertifikat verwenden, um Token zu signieren oder sichere Verbindungen herzustellen. Dies kann zur Manipulation der Kommunikation zwischen den beteiligten Parteien und zum unbefugten Zugriff auf sensible Ressourcen oder Benutzerdaten führen: Benutzeranmeldeinformationen, persönliche Informationen.

Mit Zugriff auf den privaten Schlüssel können Angreifer gefälschte Token erstellen, die für die Client-Anwendungen und Ressourcenserver gültig erscheinen. Dies kann ihnen unbefugten Zugriff auf geschützte Ressourcen gewähren oder sie in die Lage versetzen, Aktionen im Namen von legitimen Benutzern durchzuführen.

Über die OpenID Connect Abläufe

Lassen Sie uns zwei Standardabläufe besprechen: den Autorisierungscode-Ablauf und den Client Credential-Ablauf.

Der Authorization Code Flow erfordert eine Benutzereingabe, um das Zugriffstoken zu erhalten. Um ein Zugriffstoken zwischen zwei Diensten ohne Benutzereingabe zu erhalten, können Sie den Credential Flow verwenden. Diese Schritte betreffen nicht den von Ihnen erstellten Ressourcenserver. Der Ressourcenserver kann OpenID Connect um zusätzliche Informationen zur Validierung des Autorisierungs-Headers bitten. Der OpenID Connect Idp hat das JWT mit seinem privaten Schlüssel erstellt. Um dieses JWT zu validieren, gewährt der Idp Zugriff auf den entsprechenden öffentlichen Schlüssel.

Erkennung der OpenID-Konfiguration

OpenID Connect unterstützt die Ermittlung der benötigten Endpunkte, die für alle erforderlichen Schritte verwendet werden. Wir sind an zwei Endpunkten interessiert. Einer ist für die Auflistung aller Endpunktkonfigurationen und einer für die Validierung der Signatur des Zugriffstokens:

  • Der Endpunkt /.well-known/openid-configuration, der ein JSON-Dokument zurückgibt, das Metadaten über die Konfiguration des Anbieters enthält, z.B. die Autorisierungs- und Token-Endpunkte, unterstützte Grant-Typen und öffentliche Schlüssel zur Validierung von Token. Einer der in dieser Konfiguration aufgeführten Endpunkte ist der Standort des Endpunkts /jwks.
  • Der Endpunkt /jwks, der ein JSON-Dokument mit den öffentlichen Schlüsseln des Anbieters im JSON Web Key (JWK)-Format zurückgibt, das zur Validierung der Signatur der Zugriffstoken verwendet werden kann.

Indem wir diese Endpunkte nachbilden, können wir das Verhalten eines echten OpenID Connect-Anbieters simulieren, ohne einen solchen einrichten und konfigurieren zu müssen.

Authentifizierung und Autorisierung

Wenn eine HTTP-Anfrage mit einem Autorisierungs-Header eintrifft, prüft der Authentifizierungsprozess die Gültigkeit des Headers, in der Regel durch Verifizierung eines JSON Web Token (JWT), falls vorhanden. Sobald die Authentifizierung erfolgreich war, beginnt der Autorisierungsprozess zu prüfen, ob die Anfrage auf einen bestimmten Endpunkt zugreifen darf. Wenn die Authentifizierungsprüfung fehlschlägt, erhält die Antwort den HTTP-Statuscode 401 (nicht autorisiert). Wenn die Autorisierungsprüfung fehlschlägt, antwortet der Server mit einem HTTP-Statuscode 403 (Verboten).

Wenn eine HTTP-Anfrage mit einem Autorisierungs-Header eintrifft, prüft der Authentifizierungsprozess die Gültigkeit des Headers, in der Regel durch Verifizierung eines JSON Web Token (JWT), falls vorhanden. Sobald die Authentifizierung erfolgreich war, beginnt der Autorisierungsprozess zu prüfen, ob die Anfrage auf einen bestimmten Endpunkt zugreifen darf. Wenn die Authentifizierungsprüfung fehlschlägt, erhält die Antwort den HTTP-Statuscode 401 (nicht autorisiert). Wenn die Autorisierungsprüfung fehlschlägt, antwortet der Server mit einem HTTP-Statuscode 403 (Verboten).

Wie sind die Tests aufgebaut?

Bevor der Code gezeigt wird, wollen wir die Klassen, die Verantwortlichkeiten dieser Klassen und die Interaktion zwischen ihnen vorstellen. Dies sollte Ihnen helfen, den Code zu verstehen, der im folgenden Abschnitt Boilerplate gezeigt wird.

Zunächst definieren wir eine Reihe von Tests, die uns dabei helfen, sicherzustellen, dass die OpenID Connect-Konfiguration und die Middleware zusammen funktionieren. Sie müssen einfach sein, die Absicht zeigen und eindeutig sein. Alle diese Tests folgen der Arrange-Act-Assert (AAA)-Syntax. Um sicherzustellen, dass die Tests Zugriff auf ein anpassbares JWT haben, wird ein selbstsigniertes Zertifikat erstellt. Dieses Zertifikat gibt den Tests die Möglichkeit,:

  • signieren Sie das JWT mit dem privaten Schlüssel des Zertifikats.
  • dem Ressourcenserver über den Endpunkt /jwkset Zugriff auf den öffentlichen Schlüssel des Zertifikats gewähren.

Die Tests-Klasse implementiert IClassfixture<WeatherForecastServerSetupFixture>. Die classfixture repräsentiert den Testserver, der von XUnit erstellt wird. Er hostet das, was in der WeatherApp-Anwendung definiert ist, d.h. das, was in Program.cs definiert ist

sequenceDiagram Teilnehmer Tests Teilnehmer Zertifikat Teilnehmer WeatherForecastServerSetupFixture autonumber Hinweis über Tests, WeatherForecastServerSetupFixture: XUnit: TestServer erstellen (ClassFixture) Hinweis zu WeatherForecastServerSetupFixture: Hosting WeatherApp WeatherForecastServerSetupFixture->>WeatherForecastServerSetupFixture:PostConfigure JwtBearerOptions.ConfigurationManager WeatherForecastServerSetupFixture->>Certificate: Selbstsigniertes Zertifikat erstellen WeatherForecastServerSetupFixture->>Tests:Fixture für Testlauf injizierenBei der zu testenden Anwendung handelt es sich um eine Beispielanwendung namens WeatherApp. Diese Anwendung hat einen definierten Endpunkt namens /weatherforecast. Die Authentifizierungs-Middleware überprüft, ob ein JWT vorhanden ist. Das JWT sollte ein gültiges Publikum, einen Aussteller und eine Signatur enthalten. Nachdem das Token validiert wurde, schaltet sich die Autorisierungs-Middleware ein. Das JWT wird daraufhin überprüft, ob es Daten enthält, die die Anfrage zum Zugriff auf den Endpunkt autorisieren. Der Endpunkt ist durch zwei Richtlinien geschützt. Für eine erfolgreiche HTTP-GET-Operation muss es einen Claim mit dem Namen country und dem Wert Belgium sowie einen Scope geben, der "WeatherForecast:Get" enthalten sollte.
sequenceDiagram participant EndPoint participant AuthenticationMiddleware participant AuthorizationMiddleware autonumber EndPoint->>AuthenticationMiddleware: Ist JWT gültig? Hinweis über AuthenticationMiddleware: Gültige Signatur ? Hinweis über AuthenticationMiddleware: Gültige Audience, Issuer... ? Hinweis zu AuthorizationMiddleware: Endpunkt durch Richtlinien und Geltungsbereich geschützt AuthenticationMiddleware->>AuthorizationMiddleware: Geltungsbereich und Land gültig?In der Klasse fixture werden wir den JwtBearerOptions.ConfigurationManager einstellen. Das geschieht mit der Methode ServiceCollection.PostConfigure<JwtBearerOptions>. Durch das Nachkonfigurieren der Optionen wird die Instanziierung des ConfigurationManagers beeinflusst. Ein HttpClient, der sich auf eine benutzerdefinierte Klasse MockingOpenIdProviderMessageHandler stützt, wird injiziert.
sequenceDiagram Teilnehmer Tests Teilnehmer Zertifikat Teilnehmer WeatherForecastServerSetupFixture Teilnehmer MockingOpenIdProviderMessageHandler autonumber Hinweis über Tests, MockingOpenIdProviderMessageHandler: XUnit: TestServer erstellen (ClassFixture) Hinweis zu WeatherForecastServerSetupFixture: Hosting WeatherApp WeatherForecastServerSetupFixture->>MockingOpenIdProviderMessageHandler:Create zur Verwendung in ConfigurationManager WeatherForecastServerSetupFixture->>WeatherForecastServerSetupFixture:PostConfigure JwtBearerOptions.ConfigurationManager WeatherForecastServerSetupFixture->>Zertifikat: Selbstsigniertes Zertifikat für /jwks und JWT-Signierung erstellen WeatherForecastServerSetupFixture->>Tests:Fixture für Testlauf injizieren

Es liegt in der Verantwortung der Klasse MockingOpenIdProviderMessageHandler, den Abruf der OpenID Connect-Konfiguration abzufangen. Durch die Verwendung eines benutzerdefinierten OpenID Connect-Konfigurationsobjekts wird der Ort des /jwks-Endpunkts manipuliert. Der Aufruf des /jwks Endpunkts wird abgefangen. Wenn der Endpunkt für die öffentlichen Schlüssel aufgerufen wird, antwortet der Messagehandler mit unserem eigenen JwkSet, das auf unserem selbst signierten Zertifikat basiert. Dieses Zertifikat wird auch für die Signierung des JWT verwendet. Da es den privaten Schlüssel des Zertifikats verwendet, das das JWT signiert hat, wird der öffentliche Schlüssel des Zertifikats eine gültige Signatur erzeugen.

sequenceDiagram Teilnehmer EndPoint Teilnehmer Zertifikat Teilnehmer MockingOpenIdProviderMessageHandler Teilnehmer AuthenticationMiddleware Teilnehmer AuthorizationMiddleware autonumber EndPoint->>AuthenticationMiddleware: Ist JWT gültig? Hinweis über AuthenticationMiddleware: Gültige Signatur ? AuthenticationMiddleware->>MockingOpenIdProviderMessageHandler: /.well-known/openidconfiguration MockingOpenIdProviderMessageHandler->>AuthenticationMiddleware: openidconfiguration (json ) location /jwkset AuthenticationMiddleware->>MockingOpenIdProviderMessageHandler: /jwkset (öffentliche Schlüssel für die Signatur) MockingOpenIdProviderMessageHandler->>Zertifikat: Konvertiert Zertifikat in Element in JwkSet Zertifikat->>MockingOpenIdProviderMessageHandler: Konvertiert JwkSet in Json MockingOpenIdProviderMessageHandler ->>AuthenticationMiddleware: JwkSet in Form von Json Hinweis über AuthenticationMiddleware: Gültige Audience, Issuer... ? Hinweis über AuthorizationMiddleware: Endpunkt durch Richtlinien und Geltungsbereich geschützt AuthenticationMiddleware->>AuthorizationMiddleware: Machen Sie die Daten im JWT für den Endpunkt autorisiert? AuthorizationMiddleware->>EndPoint: JWT ist autorisiert. Sie können fortfahren.
Das folgende Bild ist das vollständige Sequenzdiagramm des obigen Textes.
sequenceDiagram Teilnehmer Tests Teilnehmer Zertifikat Teilnehmer WeatherForecastServerSetupFixture Teilnehmer EndPoint Teilnehmer MockingOpenIdProviderMessageHandler Teilnehmer AuthenticationMiddleware Teilnehmer AuthorizationMiddleware autonumber Hinweis über Tests, WeatherForecastServerSetupFixture: XUnit: TestServer erstellen (ClassFixture) Hinweis zu WeatherForecastServerSetupFixture: Hosting WeatherApp WeatherForecastServerSetupFixture->>WeatherForecastServerSetupFixture:PostConfigure JwtBearerOptions.ConfigurationManager WeatherForecastServerSetupFixture->>Certificate: Selbstsigniertes Zertifikat erstellen WeatherForecastServerSetupFixture->>Tests:Fixture für Testlauf injizieren Hinweis über Tests: Tests->>Tests anordnen: Gültige/ungültige AccessTokenParameter anpassen Tests->>Zertifikat: JWT mit privatem Schlüssel signieren Hinweis über Tests: Handeln Testss->>EndPoint: HttpRequest mit JWT EndPoint->>AuthenticationMiddleware: Ist JWT gültig? Hinweis über AuthenticationMiddleware: Gültige Signatur ? Hinweis über AuthenticationMiddleware: Gültige Audience, Issuer... ? Hinweis zu AuthorizationMiddleware: Endpunkt durch Richtlinien und Geltungsbereich geschützt AuthenticationMiddleware->>AuthorizationMiddleware: Geltungsbereich und Land gültig?

Tests einrichten

Als Nächstes beschreiben wir eine Reihe von Tests, mit denen das Verhalten des /WeatherForecast-Endpunkts einer Anwendung bei der Verarbeitung verschiedener Authentifizierungsszenarien überprüft wird. Die Tests decken Fälle ab, in denen kein Autorisierungs-Header, ein gültiges JSON Web Token (JWT), ein ungültiger Aussteller und ein ungültiger Anspruch für das Land vorliegt. Jeder Test verfügt über ein Diagramm, das den Fluss der Interaktionen zwischen den am jeweiligen Test beteiligten Komponenten veranschaulicht und ein klares Verständnis der erwarteten Ergebnisse vermittelt.

Ein Test ohne Autorisierungs-Header sollte mit einem http-Statuscode Unauthorized zurückkehren

Der erste Test ist der einfachste. Es wird ein Standard-HTTP-Client erstellt. Es wird eine Anfrage an den Endpunkt /WeatherForecast gestellt. Der Endpunkt sollte mit unautorisiert antworten.

var httpClient = _fixture.CreateDefaultClient();

var response = await httpClient.GetAsync("WeatherForecast");

response.StatusCode.ShouldBe(HttpStatusCode.Unauthorized);
Ein Bild mit Diagramm Beschreibung automatisch generiert

Ein Test mit einem gültigen JWT in der Autorisierungskopfzeile sollte mit einem http-Statuscode OK zurückkehren

Der folgende Code hat die gleiche Struktur wie der vorherige Test, allerdings mit einem Unterschied: eine Standardinstanz von AccessTokenParameters. Die Standardinstanz von AccessTokenParameters enthält alle gültigen Informationen, die zur Erzeugung eines gültigen JWT erforderlich sind. Diese Instanz wird an den JwtBearerCustomAccessTokenHandler übergeben. JwtBearerCustomAccessTokenHandler generiert das Zugriffstoken und fügt den Authorization-Header mit dem Zugriffstoken zur Anfrage hinzu.

var accessTokenParameters = new AccessTokenParameters();

var httpClient = _fixture.CreateDefaultClient(new JwtBearerCustomAccessTokenHandler(accessTokenParameters));

var response = await httpClient.GetAsync($"/WeatherForecast/");

response.StatusCode.ShouldBe(HttpStatusCode.OK);

In dem Diagramm unten sehen Sie den Begriff HttpClientMessageHandler. Dies ist ein verallgemeinerter Name. Dies ist die Klasse JwtBearerCustomAccessTokenHandler. Ich habe diesen Begriff verwendet, um zu betonen, dass es sich um den HttpClient handelt, der einen Delegaten verwendet, der die HttpRequest manipuliert, bevor sie gesendet wird.

Zeitleiste Beschreibung automatisch generiert

Ein Test mit einem ungültigen Aussteller sollte mit einem http-Statuscode Unauthorized zurückkehren

Der folgende Code hat die gleiche Struktur wie der gültige Test. Eine Standardinstanz der AccessTokenParameters mit einem ungültigen Aussteller wird an den JwtBearerCustomAccessTokenHandler übergeben.

var accessTokenParameters = new AccessTokenParameters()

{ Issuer = "InvalidIssuer" };

var httpClient = _fixture.CreateDefaultClient(new JwtBearerCustomAccessTokenHandler(accessTokenParameters));

var response = await httpClient.GetAsync($"/WeatherForecast/");

response.StatusCode.ShouldBe(HttpStatusCode.Unauthorized);
Zeitleiste Beschreibung automatisch generiert

Ein Test mit einem ungültigen Anspruchsland sollte mit einem http-Statuscode Forbidden zurückkehren

Die gleiche Struktur gilt auch für diesen Test. Der Unterschied besteht nun darin, dass eine Methode verwendet wird, um den gültigen Wert der Angabe "Land" durch einen ungültigen Wert zu ersetzen.

var accessTokenParameters = new AccessTokenParameters();

accessTokenParameters.AddOrReplaceClaim("country", "invalidCountry");

var httpClient = _fixture.CreateDefaultClient(new JwtBearerCustomAccessTokenHandler(accessTokenParameters, _testOutputHelper));

var response = await httpClient.GetAsync($"/WeatherForecast/");

response.StatusCode.ShouldBe(HttpStatusCode.Forbidden);
Diagramm, Zeitleiste Beschreibung automatisch generiert

Einrichten der Webanwendung

Um WeatherForecastController zu schützen, wird das Attribut authorize auf Klassenebene hinzugefügt. Dadurch wird die Authentifizierungs-Middleware angewiesen, alle Endpunkte innerhalb dieses Controllers zu schützen. Außerdem muss die Authentifizierungs-Middleware den JWT-Träger validieren, wenn sie einen Authorization-Header gemäß den angegebenen Einstellungen erkennt. Im Attribut authorize ist eine Richtlinie definiert. Die angegebenen Richtlinien müssen hinzugefügt werden, wenn Sie die Autorisierungs-Middleware konfigurieren.

WeatherForecastController

Um die Endpunkte des WeatherForecastControllers zu schützen, definieren Sie zwei Richtlinien:

  • alle Endpunkte des Controllers können nur von Belgien aus erreicht werden.
  • das Zugriffstoken muss den spezifischen Zugriff auf die Operation GET /weatherforecast definieren.
[Authorize(Policy = "OnlyBelgiumPolicy")]

public class WeatherForecastController : ControllerBase {

[HttpGet()]

[Authorize(Policy = "WeatherForecast:Get")]

public WeatherForecast Get() {

Programm

Dieser Abschnitt behandelt die Erstellung der Webanwendung, die Konfiguration der Authentifizierungs- und Autorisierungs-Middleware und die Reihenfolge ihrer Ausführung.

  1. Erstellen Sie den Builder für die WebApplikation
var builder = WebApplication.CreateBuilder(args);
  1. Fügen Sie die Authentifizierungs-Middleware hinzu und konfigurieren Sie sie mit Hilfe der Klasse JwtBearerDefaults. Konfigurieren Sie die Authentifizierungs-Middleware so, dass sie die Validierung von JWT-Träger-Tokens unterstützt. Im Abschnitt ".AddJwtBearer" gibt es eine Vielzahl von Optionen. Für die Zwecke dieses Artikels beschränken wir die Optionen darauf, was die Middleware mit den Ansprüchen tun soll und welche Teile des JWTs validiert werden sollen.
builder.Services.AddAuthentication(options =>

{

    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;

    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;

    options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;

}).AddJwtBearer(o =>

{

    o.MapInboundClaims = false;

    o.TokenValidationParameters = new TokenValidationParameters

    {

        ValidIssuer = builder.Configuration["Jwt:Issuer"],

        ValidAudience = builder.Configuration["Jwt:Audience"],

        ValidateIssuer = true,

        ValidateAudience = true,

        ValidateLifetime = true,

        ValidateIssuerSigningKey = true,

        NameClaimType = "sub",

    };

});
  1. Die autorisierten Attribute in der Controller-Klasse beziehen sich auf bestimmte Richtlinien. Diese Richtlinien müssen in der Autorisierungs-Middleware konfiguriert werden. Die Richtlinien OnlyBelgiumPolicy und WeatherForecast:Get werden durch die Suche im JWT nach den jeweiligen Ansprüchen country und scope definiert. Sie sollten den Wert Belgien und weatherforecast:read haben.
builder.Services.AddAuthorization(authorizationOptions => {

    authorizationOptions.AddPolicy("OnlyBelgiumPolicy", policy => policy.RequireClaim("country", "Belgium"));

    authorizationOptions.AddPolicy("WeatherForecast:Get", policy => policy.RequireClaim("scope", "weatherforecast:read"));

});
  1. Nachdem die Middleware definiert ist, ist es an der Zeit, die Webanwendung zu erstellen. Von dort aus teilen Sie der Webanwendung mit, dass sie die konfigurierte Middleware verwenden soll. Die Reihenfolge ist wichtig: Die Reihenfolge der Use***-Methoden ist die Reihenfolge, in der die Middleware ausgelöst wird. Es macht keinen Sinn, die Autorisierungs-Middleware vor der Authentifizierungs-Middleware einzusetzen.
var app = builder.Build();

…

app.UseRouting()

   .UseAuthentication()

   .UseAuthorization()

   .UseEndpoints(endpoints => { endpoints.MapControllers();});

Kesselstein

Nachfolgend finden Sie eine Zusammenfassung des benötigten Boilerplate-Codes.

  • Erstellen eines selbstsignierten Zertifikats
    • Die Umwandlung des öffentlichen Schlüssels in ein JWKSet
  • Einrichten des Klassenfixtures
    • Konfigurieren Sie den ConfigurationManager
    • Spotten Sie Endpunkte, indem Sie einen gespiegelten HttpMessageHandler in einem vorkonfigurierten HttpClient verwenden.

Selbstsigniertes Zertifikat

Der Identitätsanbieter wurde so konfiguriert, dass er das JWT mit RS256 signiert. Nachfolgend finden Sie eine Methode, mit der Sie dies nachahmen können.

Die Klasse SelfSignedAccessTokenPemCertificateFactory bietet die Funktionalität, ein Objekt vom Typ PemCertificate zu erstellen. Die Instanz enthält das Zertifikat, den öffentlichen Schlüssel und den privaten Schlüssel.

Das Zertifikat hat die folgenden Eigenschaften:

  • eine Schlüsselgröße von 2048 Bit,
  • gültig für 10 Jahre,
  • Stellen Sie sicher, dass das Zertifikat für Code Signing verwendet werden kann (OID 1.3.6.1.5.5.7.3.3)
  • ein Startdatum, definiert als der Tag vor heute
  • gebunden an die Domäne i.do.not.exist.
using (RSA rsa = RSA.Create()){

    rsa.KeySize = 2048;

    var request = new CertificateRequest("cn=i.do.not.exist", rsa, HashAlgorithmName.SHA256,RSASignaturePadding.Pkcs1);

    request.CertificateExtensions.Add(

        new X509BasicConstraintsExtension(true, false, 0, true));

    request.CertificateExtensions.Add(

        new X509EnhancedKeyUsageExtension(new OidCollection

        {

            new Oid("1.3.6.1.5.5.7.3.1")

        }, false));

    var yesterday = new DateTimeOffset(DateTime.UtcNow.AddDays(-1));

    var tenyearslater = new DateTimeOffset (DateTime.UtcNow.AddDays(3650));

    X509Certificate2 cert = request.CreateSelfSigned(yesterday,tenyearslater));

    var certificatePem = PemEncoding.Write("CERTIFICATE", cert.RawData);

    AsymmetricAlgorithm? key = cert.GetRSAPrivateKey();

    byte[] pubKeyBytes = key.ExportSubjectPublicKeyInfo();

    byte[] privKeyBytes = key.ExportPkcs8PrivateKey();

    char[] pubKeyPem = PemEncoding.Write("PUBLIC KEY", pubKeyBytes);

    char[] privKeyPem = PemEncoding.Write("PRIVATE KEY", privKeyBytes);

    var pemCertificate = new PemCertificate(

        Certificate: new string(certificatePem),

        PublicKey: new string(pubKeyPem),

        PrivateKey: new string(privKeyPem)

    );

    return pemCertificate;

}

Es gibt viele Möglichkeiten, mit den Einstellungen des selbstsignierten Zertifikats herumzuspielen und so Ihre Sicherheitseinstellungen zu validieren.

Erstellen Sie ein Objekt vom Typ JsonWebKeySet

Liegt ein Zertifikat im PEM-Format vor, kann daraus ein JWKSet erstellt werden. Der Endpunkt /jwks erwartet es in diesem Format. Es gibt eine Methode in der Klasse PemCertificate namens ToJwksCertificate. Die Eigenschaft PublicKey des Zertifikats bietet die Möglichkeit, die für die Erstellung eines JsonWebKey erforderlichen Parameter zu exportieren. Diese Instanz wird der Eigenschaft Keys der Klasse JsonWebKeySet hinzugefügt.

var certificate = X509Certificate2.CreateFromPem(CertInPEMString);

var keyParameters = certificate.PublicKey.GetRSAPublicKey()?.ExportParameters(false);

var e = Base64UrlEncoder.Encode(keyParameters.Value.Exponent);

var n = Base64UrlEncoder.Encode(keyParameters.Value.Modulus);

var dict = new Dictionary<string, string>()

{

    { "e", e },

    { "kty", "RSA" },

    { "n", n }

};

var hash = SHA256.Create();

var asciiBytes = ASCII.GetBytes(JsonConvert.SerializeObject(dict))

var hashBytes = hash.ComputeHash(asciiBytes);

JsonWebKey jsonWebKey = new JsonWebKey()

{

    Kid = Base64UrlEncoder.Encode(hashBytes),

    Kty = "RSA",

    E = e,

    N = n

};

JsonWebKeySet jsonWebKeySet = new JsonWebKeySet();

jsonWebKeySet.Keys.Add(jsonWebKey);

return jsonWebKeySet;

Einstellen der Classfixture

Um Systemtests zu erstellen, müssen wir den Server einrichten, der uns unseren Endpunkt /WeatherForecast liefert. Dies geschieht in einer Klassenfixture namens WeatherForecastServerSetupFixture.

public class WeatherForecastServerSetupFixture: WebApplicationFactory<Program>

Die WebApplicationFactory verwendet die Klasse Programm, um den TestServer zu erstellen und seinen Start zu verzögern, allerdings erst, nachdem sie Einstellungen und Dienste, die im Programm festgelegt wurden, hinzugefügt, angewendet und/oder überschrieben hat. Der TestServer wird gestartet, wenn er benötigt wird, in diesem Fall, wenn der HttpClient eine Nachricht sendet.

Die Klasse WebApplicationFactory bietet mehrere Methoden, die Sie außer Kraft setzen können. Überschreiben Sie die Methode ConfigureWebHost in der Klasse fixture WeatherForecastServerSetupFixture. Der ConfigurationManager ist eine Eigenschaft der OpenID Connect-Einstellungen.

In der Methode ConfigureWebHost konfigurieren wir JwtBearerOptions mit einem vordefinierten ConfigurationManager aus ConfigForMockedOpenIdConnectServer nach.

builder.ConfigureTestServices(services =>  {

services.PostConfigure<JwtBearerOptions>(

JwtBearerDefaults.AuthenticationScheme,

options => {

options.ConfigurationManager = ConfigForMockedOpenIdConnectServer.Create();

Der ConfigurationManager wird mit einem vorkonfigurierten HttpClient in der Klasse ConfigForMockedOpenIdConnectServer erstellt. Das Abfangen der Anfrage erfolgt in der Klasse MockingOpenIdProviderMessageHandler.

Grafische Benutzeroberfläche, Tabelle Beschreibung automatisch generiert mit mittlerem Vertrauen

MockingOpenIdProviderMessageHandler

Der Konstruktor der Klasse MockingOpenIdProviderMessageHandler hat zwei Parameter. Es werden zwei Konstanten verwendet:

  • Consts.ValidSigningCertificate: enthält das Zertifikat zur Erzeugung des öffentlichen Schlüssels, wenn eine Anfrage an den Endpunkt /jwks gesendet wird.
  • Consts.ValidOpenIdConnectDiscoveryDocumentConfiguration: enthält OpenID Connect Einstellungen mit vordefinierten Werten.

Die Anfragen, die die Authentifizierungs-Middleware stellt, werden von MockingOpenIdProviderMessageHandler bearbeitet. Wenn Sie die SendAsync-Methode überschreiben, werden die OpenID Connect-Einstellungen und die öffentlichen Schlüssel auf Anfrage zurückgegeben.

if (request.RequestUri.AbsoluteUri.Contains(Consts.WellKnownOpenIdConfiguration))return await GetOpenIdConfigurationHttpResponseMessage();

if (request.RequestUri.AbsoluteUri.Equals(_openIdConnectDiscoveryDocumentConfiguration.JwksUri))return await GetJwksHttpResonseMessage();

Die Antwort auf die OpenID Connect Discovery-Anfrage enthält Einstellungen, die von dem von Ihnen verwendeten realen Idp-Anbieter kopiert wurden, mit einigen geringfügigen Änderungen, z.B. dem Ort des /jwks-Endpunkts. Der Endpunkt /jwks enthält generierte öffentliche Schlüssel aus unserem selbstsignierten Zertifikat. Später wird dasselbe selbstsignierte Zertifikat verwendet, um Signaturen des JWT zu erzeugen.

IConfigurationManager<OpenIdConnectConfiguration>

Alle Bausteine sind nun vorhanden, um den ConfigurationManager<OpenIdConnectConfiguration> zu erstellen.

Die folgenden Parameter sind erforderlich, um die Instanz zu erstellen:

  • Consts.WellKnownOpenIdConfiguration : eine gültige URL des gefälschten Idp-Providers
  • OpenIdConnectConfigurationRetriever: ruft die OpenID Connect-Konfiguration ab
  • HttpDocumentRetriever: die Instanz, die der OpenIdConnectConfigurationRetriever zum Abrufen der Konfiguration verwendet. Er verwendet eine Instanz eines HttpClient, der mit dem MockingOpenIdProviderMessageHandler konfiguriert ist.

Die obigen Angaben werden in den folgenden Code übersetzt.

var handler = new MockingOpenIdProviderMessageHandler(Consts.ValidOpenIdConnectDiscoveryDocumentConfiguration, Consts.ValidSigningCertificate);

var openIdHttpClient = new HttpClient(handler);

var httpDocumentRetriever = new HttpDocumentRetriever(openIdHttpClient);

var openIdConnectConfigRetriever = new OpenIdConnectConfigurationRetriever();

return new ConfigurationManager<OpenIdConnectConfiguration>(

Consts.WellKnownOpenIdConfiguration,

openIdConnectConfigRetriever,

httpDocumentRetriever);

Erzeugen eines Zugriffstokens

Der folgende Code zeigt, wie Sie ein JWT mit einem X509Certificate2 erzeugen.

In diesem Codeschnipsel zeigen wir Ihnen, wie Sie mit einem gültigen Signierzertifikat ein verschlüsseltes Zugriffstoken erstellen. Zunächst konvertieren wir das Zertifikat in ein X509Certificate2-Objekt. Anschließend erstellen wir unter Verwendung des Zertifikats und des RSA-SHA256-Algorithmus Signierberechtigungen. Anschließend definieren wir ein ClaimsIdentity-Objekt und richten einen SecurityTokenDescriptor mit den erforderlichen Informationen wie Zielgruppe, Aussteller, Ablaufzeit und Signierdaten ein. Schließlich verwenden wir den JwtSecurityTokenHandler, um das Token zu erstellen und zu schreiben, was zu einem verschlüsselten Zugriffstoken führt.

var cert = Consts.ValidSigningCertificate.ToX509Certificate2()

var signingCredentials = new SigningCredentials(new X509SecurityKey(cert), SecurityAlgorithms.RsaSha256);

var identity = new ClaimsIdentity(Consts.Claims);

var securityTokenDescriptor = new SecurityTokenDescriptor {

Audience = Consts.Audience,

Issuer = Consts.Issuer,

NotBefore = DateTime.UtcNow,

Expires = DateTime.UtcNow.AddHours(1),

SigningCredentials = signingCredentials,

Subject = identity };

var handler = new JwtSecurityTokenHandler();

var securityToken = handler.CreateToken(securityTokenDescriptor);

var encodedAccessToken = handler.WriteToken(securityToken);

Bei der Generierung des /jwks-Endpunkts wurde das Consts.ValidSigningCertificate verwendet. Die Authentifizierungs-Middleware benötigt eine gültige Signatur. Das Objekt PemCertificate, das die Eigenschaften Certificate, PrivateKey und PublicKey enthält, wird in ein X509Certificate2 umgewandelt.

X509Certificate2.CreateFromPem(Certificate, PrivateKey);

Die Einstellung eines ungültigen Publikums, Ausstellers und Betreffs oder eines falschen Zertifikats sollte in Tests verwendet werden, um sicherzustellen, dass ein nicht authentifizierter oder nicht autorisierter Fehler das erwartete Ergebnis ist.

Hinzufügen des Zugriffstokens zur Anfrage

Es gibt mehrere Möglichkeiten, ein Zugriffstoken zu einer Anfrage hinzuzufügen. Die Klasse WebApplicationFactory bietet eine Methode CreateDefaultClient, die einen HTTP-Client für Sie erstellt. Mit dieser Methode wird ein HTTP-Client erstellt. Sie legt die URL des Test-Servers fest. Wenn Sie diese Methode verwenden, gibt es einen optionalen Parameter, mit dem Sie einen DelegatingHandler übergeben können. JwtBearerCustomAccessTokenHandler ist eine benutzerdefinierte Klasse, die sich von diesem DelagatingHandler ableitet und die von ihm angebotene Send-Methode außer Kraft setzt.

var encodedAccessToken = JwtBearerAccessTokenFactory.Create(_accessTokenParameters);

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

return base.Send(request, cancellationToken);

Wenn der HttpClient die Anfrage sendet, führt JwtBearerCustomAccessTokenHandler die Send-Methode aus und fügt somit das Zugriffstoken zum AuthorizationHeader hinzu.

Die JwtBearerAccessTokenFactory.Create erstellt das Zugriffs-Token

Zusammenfassung

Dieser Artikel befasst sich mit verschiedenen Konzepten und Techniken, wie z.B. der Erstellung von selbstsignierten Zertifikaten, der fliegenden Generierung von JWK-Sets und der Verwaltung von Zugriffstokens. Ich möchte Sie ermutigen, diese Schnipsel auszuprobieren oder GitHub für die gesamte Codebasis zu besuchen. Dies kann ein Schritt sein, um ein tieferes Verständnis von OpenID Connect und seiner Integration in Anwendungen zu erlangen. Das allein wird dazu beitragen, die Sicherheit und Leistung von Anwendungen zu verbessern.

Wenn Sie ein weiteres Tool in Ihrem Werkzeugkasten haben, können Sie eine robuste Testumgebung schaffen. Damit können Sie verschiedene Szenarien auf automatisierte Weise testen. Zu diesen Tests gehört auch das Testen von gültigen und ungültigen Zugriffstokens. So wird sichergestellt, dass Anwendungen die Authentifizierung und Autorisierung korrekt handhaben. Und das alles mit einer realistischen Konfiguration und ohne nachgebildete Klassen.

Da Pakete eine große Blackbox sind, ist es nicht immer einfach zu verstehen, was hinter den Kulissen passiert.

Sie sollten nun ein besseres Verständnis für die Einrichtung eines Test-Servers haben, was Ihnen bei anderen Projekten oder Testszenarien nützlich sein kann.

Quellen

Verfasst von

Kristof Riebbels

Kristof is a dynamic Software Developer, Coach, and DevOps enthusiast who loves travelling, coding, and sci-fi. He's passionate about staying physically active and thrives on positive team dynamics, where collaboration results in "1+1=3." His key talent lies in effectively transmitting knowledge to ensure it resonates with his audience. Kristoff finds joy in every "aha" moment and starts his days with epic modern orchestral music, while dance and trance music keep him focused during work.

Contact

Let’s discuss how we can support your journey.