Einen Nutzer in einer Delphi-Anwendung mit OAuth2 und PKCE anmelden

Eine Komponente, ein Grant Type, eine Übergabe an den Browser. Diese Seite bringt dich vom leeren Formular zu einem angemeldeten Nutzer mit einem gültigen Access Token, über den Authorization-Code-Flow mit PKCE (RFC 7636), den Flow, den heute jeder Anbieter von einer nativen Desktop-Anwendung erwartet.

TsgcHTTP_OAuth2_Client
Code Verifier und Challenge werden für dich erzeugt
Delphi 7 bis 13, C++ Builder, VCL und FireMonkey

Was du brauchst, um einen Nutzer anzumelden

Eine einzige nicht visuelle Komponente spricht mit dem Anbieter. Du brauchst keinen Webserver, keinen eingebetteten Browser und kein REST-Framework.

Komponente

TsgcHTTP_OAuth2_Client, deklariert in der Unit sgcHTTP und im Code erzeugt, so wie es jede Demo macht.

Grant Type

OAuth2Options.GrantType := auth2CodePKCE. Diese eine Zuweisung schaltet PKCE ein.

Edition

Standard, Professional, Enterprise und All-Access. Der Client ist keine Enterprise-Funktion, der Server schon.

Plattformen

Windows, macOS, Linux, iOS und Android. Die Komponente öffnet den Browser, den die jeweilige Plattform bereitstellt.

Was der PKCE-Flow tatsächlich tut

PKCE gibt es, weil eine Desktop-Anwendung kein Geheimnis hüten kann. Es ersetzt das Geheimnis durch einen Wert, von dem der Client belegt, dass er ihn schon vor dem Start des Flows kannte.

1. Einen Code Verifier erzeugen

Eine Zufallszeichenfolge mit hoher Entropie. sgcWebSockets holt 32 Bytes vom CSPRNG der Plattform und kodiert sie Base64URL, was den 43 Zeichen langen Verifier ergibt, den RFC 7636 verlangt.

2. Die Code Challenge ableiten

SHA-256 des Verifiers, Base64URL-kodiert. In der Autorisierungsanfrage reist die Challenge, wer den Redirect mitliest, sieht den Verifier also nie.

3. Den Browser öffnen

Die Komponente baut die Autorisierungs-URL mit client_id, redirect_uri, scope, state, code_challenge und code_challenge_method=S256 und startet dann den Systembrowser.

4. Der Nutzer meldet sich an

Die Zustimmung passiert im Browser, auf der Domain des Anbieters, mit der bestehenden Sitzung, dem Passwortmanager und dem Zwei-Faktor-Gerät des Nutzers. Deine Anwendung sieht das Passwort nie.

5. Der Redirect kommt zurück

Der Anbieter leitet auf deine redirect_uri weiter, die code und state trägt. Auf dem Desktop ist diese URI eine Loopback-Adresse, und die Komponente lauscht dort bereits.

6. Den Code eintauschen

Die Komponente schickt den Code zusammen mit dem ursprünglichen code_verifier per POST an den Token-Endpunkt. Der Anbieter berechnet SHA-256 erneut und vergleicht. Passt es, bekommst du ein Access Token.

Warum der Verifier zählt

Ein Authorization Code ist für die wenigen Sekunden seines Lebens ein Bearer-Wert. Alles, was den Redirect beobachten kann, eine bösartige Anwendung auf demselben eigenen URI-Schema, ein Proxy, ein gemeinsames Log, kann ihn stehlen. Ohne PKCE genügt dieser gestohlene Code, um ein Token zu prägen.

Mit PKCE weist der Token-Endpunkt den Code ab, solange der Aufrufer nicht auch den Verifier vorlegt, dessen SHA-256-Hash zur eingangs gesendeten Challenge passt. Der Angreifer hat nur den Hash gesehen, der gestohlene Code ist also wertlos.

Nichts davon musst du selbst schreiben. Setze GrantType auf auth2CodePKCE, und die Komponente erledigt die Schritte 1, 2, 3, 5 und 6 für dich. Es folgt der Code, der das ausführt, und die beiden Entscheidungen, die du treffen musst: die Redirect-URI und der Ort, an dem das Refresh Token lebt.

auf der Leitung
# 1. Browser is sent here (query wrapped for reading)
GET https://provider.com/oauth2/authorize
    ?response_type=code
    &client_id=your-client-id
    &redirect_uri=http://127.0.0.1:52413/
    &scope=openid%20profile
    &state=8F3B1C2A-...-9D4E
    &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
    &code_challenge_method=S256

# 2. Provider redirects back to the loopback listener
GET http://127.0.0.1:52413/?code=4/0Ab_5q...&state=8F3B1C2A-...-9D4E

# 3. Component exchanges the code, adding the verifier
POST https://provider.com/oauth2/token
grant_type=authorization_code
&code=4/0Ab_5q...
&redirect_uri=http://127.0.0.1:52413/
&client_id=your-client-id
&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk

Einen Nutzer anmelden, in etwa zwanzig Zeilen

Komponente erzeugen, auth2CodePKCE wählen, auf die beiden Endpunkte des Anbieters zeigen, OnAfterAccessToken binden, Start aufrufen. Der Browser öffnet sich, der Nutzer stimmt zu, das Ereignis wird mit dem Token ausgelöst.

uses
  Classes, SysUtils,
  // sgc
  sgcHTTP, sgcHTTP_OAuth_Types;

// OAuth2 is a form field: OAuth2: TsgcHTTP_OAuth2_Client;
procedure TForm1.SignIn;
begin
  OAuth2 := TsgcHTTP_OAuth2_Client.Create(nil);
  OAuth2.OnAfterAccessToken := OnAfterAccessToken;
  OAuth2.OnErrorAccessToken := OnErrorAccessToken;

  // PKCE. The verifier and the S256 challenge are generated internally.
  OAuth2.OAuth2Options.GrantType := auth2CodePKCE;
  OAuth2.OAuth2Options.ClientId := 'your-client-id';

  // The two endpoints from the provider's documentation.
  OAuth2.AuthorizationServerOptions.AuthURL :=
    'https://provider.com/oauth2/authorize';
  OAuth2.AuthorizationServerOptions.TokenURL :=
    'https://provider.com/oauth2/token';
  OAuth2.AuthorizationServerOptions.Scope.Clear;
  OAuth2.AuthorizationServerOptions.Scope.Add('openid');
  OAuth2.AuthorizationServerOptions.Scope.Add('profile');

  // Loopback redirect. Port 0 asks the OS for a free port.
  OAuth2.LocalServerOptions.IP := '127.0.0.1';
  OAuth2.LocalServerOptions.Port := 0;

  OAuth2.Start; // opens the browser and returns immediately
end;

procedure TForm1.OnAfterAccessToken(Sender: TObject; const Access_Token,
  Token_Type, Expires_In, Refresh_Token, Scope, RawParams: String;
  var Handled: Boolean);
begin
  Memo1.Lines.Add('Signed in. Token expires in ' + Expires_In + ' s');
  SaveRefreshToken(Refresh_Token); // your own storage, see below
end;

procedure TForm1.OnErrorAccessToken(Sender: TObject; const Error,
  Error_Description, Error_URI, RawParams: String);
begin
  Memo1.Lines.Add('Sign-in failed: ' + Error + ' / ' + Error_Description);
end;
// include: sgcHTTP.hpp, sgcHTTP_OAuth_Types.hpp
TsgcHTTP_OAuth2_Client *OAuth2 = new TsgcHTTP_OAuth2_Client(this);
OAuth2->OnAfterAccessToken = OnAfterAccessToken;
OAuth2->OnErrorAccessToken = OnErrorAccessToken;

OAuth2->OAuth2Options->GrantType = auth2CodePKCE;
OAuth2->OAuth2Options->ClientId = "your-client-id";

OAuth2->AuthorizationServerOptions->AuthURL =
  "https://provider.com/oauth2/authorize";
OAuth2->AuthorizationServerOptions->TokenURL =
  "https://provider.com/oauth2/token";
OAuth2->AuthorizationServerOptions->Scope->Clear();
OAuth2->AuthorizationServerOptions->Scope->Add("openid");
OAuth2->AuthorizationServerOptions->Scope->Add("profile");

OAuth2->LocalServerOptions->IP = "127.0.0.1";
OAuth2->LocalServerOptions->Port = 0;

OAuth2->Start();

void __fastcall TForm1::OnAfterAccessToken(TObject *Sender,
  const UnicodeString Access_Token, const UnicodeString Token_Type,
  const UnicodeString Expires_In, const UnicodeString Refresh_Token,
  const UnicodeString Scope, const UnicodeString RawParams, bool &Handled)
{
  Memo1->Lines->Add("Signed in. Token expires in " + Expires_In + " s");
}

Wohin der Redirect in einer Desktop-Anwendung geht

Das ist der Teil, für den es keine offensichtliche Antwort gibt, wenn du von Web-OAuth2 kommst, und der Teil, den die meisten ersten Versuche falsch machen.

Loopback, keine öffentliche URL

Eine Desktop-Anwendung hat keine Domain, auf die umgeleitet werden könnte. Die anerkannte Antwort, und die, die diese Komponente umsetzt, ist ein Loopback-Redirect: Die Anwendung startet einen winzigen HTTP-Listener auf 127.0.0.1, registriert diese Adresse als Redirect-URI und fährt den Listener herunter, sobald der Code ankommt.

LocalServerOptions.IP steht standardmäßig auf 127.0.0.1 und LocalServerOptions.Port auf 8080. Für eine ausgelieferte Desktop-Anwendung setze stattdessen Port := 0: Das Betriebssystem vergibt einen freien ephemeren Port, die Komponente trägt diesen Port in die gesendete Redirect-URI ein, und zwei Kopien deiner Anwendung auf demselben Rechner kollidieren nie.

Besteht der Anbieter auf einem exakt registrierten Pfad statt auf reinem Host und Port, setze LocalServerOptions.RedirectURL auf den registrierten Wert. Diese Zeichenfolge überschreibt dann die berechnete. Ein fester Pfad bedeutet einen festen Port, registriere also auch den Port und lass den Trick mit Port := 0 weg.

Der Listener läuft nur, solange der Flow wartet. Für auth2ClientCredentials, auth2ResourceOwnerPassword oder auth2DeviceCode wird er nie gestartet, die brauchen überhaupt keinen Redirect.

redirect.pas
// Recommended for a shipped desktop app:
// random free port, no collisions, no registration of a port
OAuth2.LocalServerOptions.IP := '127.0.0.1';
OAuth2.LocalServerOptions.Port := 0;

// When the provider requires an exact registered redirect URI:
OAuth2.LocalServerOptions.IP := '127.0.0.1';
OAuth2.LocalServerOptions.Port := 8080;
OAuth2.LocalServerOptions.RedirectURL := 'http://localhost:8080/oauth/';

// Replace the browser page the user is left looking at
OAuth2.OnHTTPResponse := OnHTTPResponse;

procedure TForm1.OnHTTPResponse(Sender: TObject; var Code: Integer;
  var Text: String);
begin
  Code := 200;
  Text := '<html><body>You are signed in. ' +
          'Close this tab and return to the app.</body></html>';
end;

Das Token lesen und einsetzen

Nachdem OnAfterAccessToken ausgelöst wurde, bleiben dieselben Werte als schreibgeschützte Eigenschaften verfügbar, und die Komponente kann sie deinen HTTP- und WebSocket-Clients zuführen, ohne dass du einen Header anfasst.

Eigenschaften und automatische Bearer-Header

Die Ereignisparameter sind bequem, aber nicht die einzige Kopie. AccessToken, TokenType, CurrentExpiresIn und CurrentRefreshToken halten dieselben Werte, solange die Komponente lebt, ein Handler an anderer Stelle in deinem Code kann sie also lesen, ohne dass du sie durchreichst.

RawParams ist der unveränderte JSON-Körper vom Token-Endpunkt. Wenn ein Anbieter etwas außerhalb des Standardsatzes zurückgibt, etwa ein id_token für OpenID Connect, parse es dort heraus. Die Komponente dekodiert kein ID Token für dich.

Damit jede Anfrage das Token automatisch trägt, weise die OAuth2-Komponente Authentication.Token.OAuth an TsgcHTTP1Client, TsgcHTTP2Client oder TsgcWebSocketClient zu. Der Client sendet Authorization: Bearer <token> für dich, mit dem token_type, den der Anbieter zurückgegeben hat.

use-token.pas
var
  vHTTP: TsgcHTTP1Client;
begin
  // Read the tokens at any time after the flow completed
  Memo1.Lines.Add(OAuth2.AccessToken);
  Memo1.Lines.Add(OAuth2.TokenType);           // normally 'Bearer'
  Memo1.Lines.Add(IntToStr(OAuth2.CurrentExpiresIn));
  Memo1.Lines.Add(OAuth2.CurrentRefreshToken);

  // Let the HTTP client attach the Authorization header itself
  vHTTP := TsgcHTTP1Client.Create(nil);
  vHTTP.Authentication.Token.OAuth := OAuth2;
  Memo1.Lines.Add(vHTTP.Get('https://api.provider.com/v1/me'));
end;

Erneuern, damit der Browser nie zweimal aufgeht

Ein Access Token lebt Minuten. Ein Refresh Token lebt Wochen oder Monate. Das zweite aufzubewahren macht aus einer Anmeldung eine Sitzung.

Zwei verschiedene Probleme

Innerhalb eines Laufs der Anwendung ist nichts zu tun. Wenn der Token-Endpunkt sowohl ein Refresh Token als auch ein expires_in zurückgibt, stellt die Komponente einen internen Timer auf ungefähr die Hälfte dieser Lebensdauer und schickt grant_type=refresh_token, wenn er auslöst, lange bevor das Access Token stirbt. OnAfterRefreshToken wird mit dem neuen Paar ausgelöst, und OnErrorRefreshToken wird ausgelöst, wenn der Anbieter es ablehnt. Lass den Parameter Handled von OnAfterAccessToken in Ruhe: Setzt du ihn auf True, sagst du der Komponente, dass du übernimmst, und sie speichert dann weder das Refresh Token noch stellt sie diesen Timer.

Über Neustarts hinweg ist dein Problem, weil nur du weißt, wo auf den Rechnern deiner Nutzer ein Geheimnis geschrieben werden darf. Speichere das Refresh Token dauerhaft, überspringe beim nächsten Start Start vollständig und rufe Refresh mit dem gespeicherten Wert auf. Es öffnet sich kein Browser, und der Nutzer ist angemeldet, bevor dein Hauptformular gezeichnet ist.

Anbieter, die Refresh Tokens rotieren, geben dir bei jeder Erneuerung ein neues, überschreibe also bei jedem OnAfterRefreshToken das Gespeicherte. Wird das gespeicherte Token schließlich abgelehnt, fall auf Start zurück und lass den Nutzer sich erneut anmelden.

Nutze Revoke, um den Nutzer sauber abzumelden, und Introspect, um den Anbieter zu fragen, ob ein Token noch lebt. Beide brauchen den passenden Endpunkt in AuthorizationServerOptions.

refresh.pas
procedure TForm1.FormCreate(Sender: TObject);
var
  vStored: string;
begin
  ConfigureOAuth2; // same settings as the QuickStart
  OAuth2.OnAfterRefreshToken := OnAfterRefreshToken;
  OAuth2.OnErrorRefreshToken := OnErrorRefreshToken;

  vStored := LoadRefreshToken;
  if vStored <> '' then
    OAuth2.Refresh(vStored)  // silent, no browser
  else
    OAuth2.Start;            // first run, ask the user
end;

procedure TForm1.OnAfterRefreshToken(Sender: TObject; const Access_Token,
  Token_Type, Expires_In, Refresh_Token, Scope, RawParams: String;
  var Handled: Boolean);
begin
  // providers that rotate hand back a new refresh token
  if Refresh_Token <> '' then
    SaveRefreshToken(Refresh_Token);
end;

procedure TForm1.OnErrorRefreshToken(Sender: TObject; const Error,
  Error_Description, Error_URI, RawParams: String);
begin
  ClearStoredRefreshToken;
  OAuth2.Start; // the stored token is dead, prompt again
end;

// Signing out
OAuth2.AuthorizationServerOptions.RevocationURL :=
  'https://provider.com/oauth2/revoke';
OAuth2.Revoke(OAuth2.CurrentRefreshToken, 'refresh_token');

Tokens speichern, ohne sie herumliegen zu lassen

sgcWebSockets liefert bewusst keinen Token-Tresor mit. Wo eine Anmeldeinformation geschrieben werden darf, ist eine Entscheidung über deine Nutzer und deine Auslieferung, die Bibliothek gibt dir das Token und hört dort auf.

Das Access Token nur im Speicher halten

Es läuft in Minuten ab, und das Refresh Token kann jederzeit ein neues prägen. Es gibt keinen Grund, es auf die Platte zu schreiben, und jeden Grund, es nicht zu tun.

Das Refresh Token pro Nutzer verschlüsseln

Unter Windows bindet DPAPI (CryptProtectData) den Geheimtext an das Windows-Konto, eine kopierte Datei ist auf einem anderen Rechner also nutzlos. macOS hat den Schlüsselbund, und moderne Linux-Desktops haben Secret Service.

Liefere nie ein Client Secret aus, auf das du dich verlässt

Alles in einer verteilten ausführbaren Datei ist öffentlich. Das ist die ganze Prämisse von PKCE. Wenn dein Anbieter für einen Desktop-Client ein Secret ausgibt, behandle es als Kennung, nicht als Schutz.

Die Datei wie eine Anmeldeinformation eingrenzen

Anwendungsdaten pro Nutzer, nicht Program Files, nicht neben der ausführbaren Datei, kein gemeinsamer Netzwerkpfad und keine schlichte INI in der Versionsverwaltung.

Beim Abmelden löschen

Rufe Revoke auf, damit der Anbieter das Token ungültig macht, und entferne dann die gespeicherte Kopie. Ein widerrufenes Token, das auf der Platte bleibt, ist immer noch ein Auditbefund.

Geheimnisse aus dem Log heraushalten

HTTPClientOptions.LogOptions schreibt den Verkehr zum Autorisierungsserver mit. Beim Zum-Laufen-Bringen des Flows ist das unbezahlbar, und es ist eine Datei voller Tokens. Schalte es aus, bevor du ausrollst.

Welcher Anbieter was braucht

Jeder OAuth-2.0-Anbieter will dieselbe Handvoll Einstellungen: zwei Endpunkte, eine Client-ID, die Scopes und einen registrierten Redirect. Für Google und Microsoft gibt es zusätzlich fertige Komponenten, die die Endpunkte ausfüllen und ein Nutzerprofil zurückgeben.

Google und Microsoft, in einem Aufruf

TsgcHTTP_OAuth2_Client_Google und TsgcHTTP_OAuth2_Client_Microsoft stammen von derselben Basiskomponente ab und füllen die Endpunkte vor. Die Methode Authenticate der beiden blockiert: Der Aufruf führt den ganzen Flow aus, wartet auf den Browser-Roundtrip und gibt ein Datenobjekt mit Authenticated und einem gefüllten UserProfile.

Das ist der kürzestmögliche Weg zu "wer ist dieser Nutzer". TsgcOAuth2_Google_Data.UserProfile trägt _Name, Given_Name, Family_Name, Id, Locale und Picture. TsgcOAuth2_Microsoft_Data.UserProfile trägt DisplayName, GivenName, Surname, Mail, JobTitle, OfficeLocation und mehr. Microsofts Authenticate nimmt zuerst die Tenant-ID.

Für jeden anderen Anbieter nimm den Basis-TsgcHTTP_OAuth2_Client und kopiere die beiden URLs aus dessen Dokumentation. Danach bleibt nichts Anbieterspezifisches mehr.

social-signin.pas
uses
  sgcHTTP, sgcHTTP_OAuth2_Client_Google;

var
  vClient: TsgcHTTP_OAuth2_Client_Google;
  vData: TsgcOAuth2_Google_Data;
begin
  vClient := TsgcHTTP_OAuth2_Client_Google.Create(nil);
  try
    vData := vClient.Authenticate('client-id', 'client-secret');
    if vData.Authenticated then
    begin
      ShowMessage(vData.UserProfile._Name);
      ShowMessage(vData.AccessToken);
    end;
  finally
    vClient.Free;
  end;
end;
Anbieter Komponente Grant Type Redirect Client Secret
Google TsgcHTTP_OAuth2_Client_Google oder der Basis-Client auth2CodePKCE Loopback, Port := 0 Wird für Desktop-Clients ausgegeben, setze es, wenn du eines hast
Microsoft Entra ID TsgcHTTP_OAuth2_Client_Microsoft oder der Basis-Client auth2CodePKCE Loopback, registriert als Mobile-/Desktop-Plattform Von einem öffentlichen Client nicht genutzt, lass es leer
Auth0, Okta, Keycloak, AWS Cognito TsgcHTTP_OAuth2_Client auth2CodePKCE Loopback, an der Anwendung registriert Hängt davon ab, ob die App öffentlich oder vertraulich ist
Hintergrundjobs und Dienste TsgcHTTP_OAuth2_Client auth2ClientCredentials Keiner, es ist kein Browser beteiligt Erforderlich und sicher, weil nichts verteilt wird
Kioske, TVs, Headless-Geräte TsgcHTTP_OAuth2_Client auth2DeviceCode (RFC 8628) Keiner, der Nutzer schließt am Telefon ab Meist nicht erforderlich

Anmelden, um E-Mail zu senden: OAuth 2.0 und XOAUTH2

Gmail und Microsoft 365 nehmen über SMTP, IMAP und POP keine Passwörter mehr an. Der Ersatz ist dasselbe Access Token, das du gerade bekommen hast, vorgelegt über den SASL-Mechanismus XOAUTH2.

Das Token kommt von hier, der SASL-Schritt kommt von sgcIndy

Das Token zu bekommen ist genau der Flow oben: auth2CodePKCE, ein Loopback-Redirect und ein Mail-Scope wie https://mail.google.com/ in AuthorizationServerOptions.Scope. Am OAuth2-Teil ändert der Mail-Fall nichts.

Es vorzulegen ist die andere Hälfte. sgcIndy liefert TIdSASLXOAUTH2 in der Unit IdSASLXOAUTH2. Füge es TIdSMTP.SASLMechanisms hinzu, setze AuthType := satSASL und liefere Benutzernamen und Access Token aus seinem Ereignis OnAuthenticate. Derselbe Mechanismus funktioniert für TIdIMAP4 und TIdPOP3.

Halte die beiden Komponenten gedanklich auseinander: Der OAuth2-Client weiß, wie man ein Token beschafft und erneuert, der SASL-Mechanismus weiß, wie man eines vorlegt. Keiner muss vom anderen wissen.

smtp-xoauth2.pas
uses
  IdSMTP, IdSASLXOAUTH2;

var
  vSASL: TIdSASLXOAUTH2;
  vSMTP: TIdSMTP;
begin
  vSASL := TIdSASLXOAUTH2.Create(nil);
  vSASL.OnAuthenticate := OnXOAuth2Authenticate;

  vSMTP := TIdSMTP.Create(nil);
  vSMTP.AuthType := satSASL;
  vSMTP.SASLMechanisms.Clear;
  vSMTP.SASLMechanisms.Add.SASL := vSASL;
end;

procedure TForm1.OnXOAuth2Authenticate(Sender: TObject;
  var Username: string; var Token: string);
begin
  Username := 'user@example.com';
  Token := OAuth2.AccessToken; // from the PKCE flow above
end;

Brauchst du einen Client oder auch einen Server?

Alles oben ist Client-Seite. Die zweite Hälfte brauchst du nur, wenn du derjenige bist, der die Tokens ausgibt.

Nur Client

Wenn du Nutzer bei einem fremden Identitätsanbieter anmeldest, bei Google, Microsoft, Auth0, Okta, Keycloak, AWS Cognito oder deinem eigenen Unternehmens-SSO, brauchst du nichts außer TsgcHTTP_OAuth2_Client. Diese Komponente ist in der Edition Standard und jeder Edition darüber einkompiliert. Es gibt sie auch einzeln im eigenständigen Paket sgcAuth, gebündelt mit der Runtime, die es braucht.

Das ist der übliche Fall, und es ist alles, worum es auf dieser Seite bis hierher ging.

Wann du auch einen Server brauchst

Die Server-Hälfte brauchst du nur, wenn deine eigene Anwendung der Autorisierungsserver ist: Du gibst die Client-IDs aus, du hostest die Anmeldeseite, du prägst und widerrufst die Access Tokens, denen deine API dann vertraut. Das ist TsgcHTTP_OAuth2_Server, angehängt an einen TsgcWebSocketHTTPServer, und es ist eine Enterprise-Komponente.

Es prüft PKCE standardmäßig. OAuth2Options.PKCE ist True ab Werk, ein Client, der eine Challenge schickt, muss also einen passenden Verifier liefern, und einer, der das nicht tut, wird abgewiesen. Registriere Client-Anwendungen mit Apps.AddApp, authentifiziere Nutzer in OnOAuth2Authentication und stelle Tokens über einen Neustart hinweg mit AddToken wieder her.

Dieselbe Enterprise-Stufe bringt TsgcHTTP_JWT_Server für die Prüfung von JWT-Bearer-Tokens an deinen Endpunkten und TsgcWSAPIServer_WebAuthn für Passkeys. Die passenden Clients, TsgcHTTP_OAuth2_Client und TsgcHTTP_JWT_Client sind ab Standard dabei. Client und Server liegen in verschiedenen Editionsstufen, das lohnt eine Prüfung, bevor du um eines von beiden herum planst.

own-server.pas
uses
  sgcWebSocket, sgcWebSocket_Classes, sgcHTTP,
  sgcHTTP_OAuth_Types, sgcHTTP_OAuth2_Server;

var
  vOAuth2: TsgcHTTP_OAuth2_Server;
  vServer: TsgcWebSocketHTTPServer;
begin
  vOAuth2 := TsgcHTTP_OAuth2_Server.Create(nil);
  vOAuth2.OAuth2Options.PKCE := True; // default
  vOAuth2.OnOAuth2Authentication := OnOAuth2Authentication;
  vOAuth2.Apps.AddApp('MyDesktopApp', 'http://127.0.0.1:8080',
    'my-client-id', 'my-client-secret', 3600, True,
    [auth2Code, auth2CodePKCE]);

  vServer := TsgcWebSocketHTTPServer.Create(nil);
  vServer.Authentication.Enabled := True;
  vServer.Authentication.OAuth.OAuth2 := vOAuth2;
  vServer.Port := 8080;
  vServer.Active := True;
end;

procedure TForm1.OnOAuth2Authentication(Connection: TsgcWSConnection;
  OAuth2: TsgcHTTPOAuth2Request; aUser, aPassword: String;
  var Authenticated: Boolean);
begin
  Authenticated := CheckUserInYourDatabase(aUser, aPassword);
end;

Was beim ersten Mal meist schiefgeht

Fast jeder gescheiterte erste Versuch mit OAuth2 auf dem Desktop ist einer dieser sechs.

redirect_uri_mismatch

Die URI, die die Komponente sendet, muss Zeichen für Zeichen zu dem passen, was du registriert hast, einschließlich abschließendem Schrägstrich und Port. Hast du eine feste URI registriert, setze LocalServerOptions.RedirectURL auf genau diese Zeichenfolge, statt dich auf die berechnete zu verlassen. Erlaubt der Anbieter jeden Loopback-Port, nimm Port := 0 und registriere nur den Host.

Der Browser öffnet sich und es kommt nichts zurück

Irgendetwas belegt den Port, oder eine Firewallregel blockiert den Loopback-Listener. Setze Port := 0 und prüfe, ob ein früherer Durchlauf des Flows mit Stop beendet und nicht lauschend gelassen wurde.

invalid_grant beim Token-Tausch

Authorization Codes sind einmalig nutzbar und kurzlebig. Debuggen mit einem Haltepunkt zwischen Redirect und Tausch lässt den Code ablaufen. Lies den Fehler aus OnErrorAccessToken, das dir error und error_description des Anbieters selbst gibt, statt zu raten.

Es kam kein Refresh Token zurück

Anbieter geben nur eines aus, wenn du danach fragst. Google will access_type=offline, Microsoft will den Scope offline_access. Füge den Scope AuthorizationServerOptions.Scope hinzu oder häng den Query-Parameter an, indem du den Parameter URL in OnBeforeAuthorizeCode bearbeitest.

TLS scheitert unter Linux oder mobil

Der Token-Tausch ist ein HTTPS-POST, er braucht also ein funktionierendes TLS-Backend. HTTPClientOptions.TLSOptions.IOHandler wählt es aus: iohOpenSSL, iohSChannel unter Windows ohne auszuliefernde DLLs, oder die nativen Handler iohAndroidTLS und iohAppleTLS in der Edition Enterprise.

Du wolltest die Anmeldeseite in der App

Behandle OnBeforeAuthorizeCode, setze Handled := True und navigiere dein eigenes TsgcWebView2 oder TWebBrowser zur URL, die du bekommen hast. Der Loopback-Listener fängt den Redirect weiterhin ab. Beachte, dass mehrere Anbieter ihren Zustimmungsbildschirm inzwischen nicht mehr in einem eingebetteten Browser anzeigen.

Fragen zu Delphi, OAuth2 und PKCE

Die Fragen, nach denen Entwickler tatsächlich suchen, bevor sie anfangen.

Setze ein TsgcHTTP_OAuth2_Client ab, setze OAuth2Options.GrantType := auth2CodePKCE, fülle OAuth2Options.ClientId, AuthorizationServerOptions.AuthURL, AuthorizationServerOptions.TokenURL und AuthorizationServerOptions.Scope aus, setze LocalServerOptions.IP auf 127.0.0.1 und LocalServerOptions.Port auf 0 und rufe dann Start auf. Die Komponente erzeugt die PKCE-Werte, öffnet den Browser, fängt den Redirect an einem Loopback-Listener ab, tauscht den Code ein und löst OnAfterAccessToken mit dem Token aus.
Musst du nicht. Wenn GrantType gleich auth2CodePKCE ist, TsgcHTTP_OAuth2_Client zieht 32 Bytes aus der kryptografischen Zufallsquelle der Plattform, kodiert sie Base64URL zum 43 Zeichen langen Code Verifier, setzt die Code Challenge auf die Base64URL-Kodierung des SHA-256-Hashes dieses Verifiers und legt code_challenge_method auf S256 fest. Der Verifier bleibt privat in der Komponente und wird beim Token-Tausch erneut gesendet, er taucht also nie im Redirect auf. Willst du das Paar für einen anderen Zweck von Hand bauen, sind dieselben Primitive öffentlich: sgcRandomBytes in der Unit sgcCrypto_Random, dazu GetHashSHA256 und EncodeBase64URL in der Unit sgcBase_Helpers.
Eine Loopback-Adresse. TsgcHTTP_OAuth2_Client startet einen kleinen HTTP-Listener auf LocalServerOptions.IP und LocalServerOptions.Port, nur solange der Flow läuft, und die gesendete Redirect-URI wird aus diesen Werten gebaut. Die Standardwerte sind 127.0.0.1 und Port 8080. Für eine ausgelieferte Anwendung setze Port auf 0, damit das Betriebssystem einen freien ephemeren Port wählt und zwei Instanzen sich nie um einen streiten. Verlangt der Anbieter eine exakt registrierte URI, trag diese Zeichenfolge in LocalServerOptions.RedirectURL ein, und sie überschreibt den berechneten Wert.
Das hängt vom Anbieter ab. PKCE gibt es genau deshalb, weil eine ausgelieferte Desktop-Anwendung kein Geheimnis hüten kann, ein öffentlicher Client sendet also normalerweise gar kein Secret und lässt OAuth2Options.ClientSecret leer. Manche Anbieter geben für Desktop-Clients trotzdem eines aus und erwarten es bei der Token-Anfrage. Setze es dann, behandle es aber als Kennung und nicht als Schutz, denn alles in einer verteilten ausführbaren Datei lässt sich herausziehen.
Speichere das Refresh Token dauerhaft und rufe beim nächsten Start Refresh damit auf, statt Start. Lies es aus dem Parameter Refresh_Token von OnAfterAccessToken oder später aus der Eigenschaft CurrentRefreshToken. Überschreibe die gespeicherte Kopie bei jedem OnAfterRefreshToken, weil Anbieter, die Refresh Tokens rotieren, das alte ungültig machen. Innerhalb eines einzelnen Laufs ist überhaupt nichts zu tun: Die Komponente stellt einen Timer anhand des Werts expires_in und erneuert das Access Token von selbst.
Halte das Access Token nur im Speicher, es läuft in Minuten ab und kann jederzeit neu geprägt werden. Speichere das Refresh Token verschlüsselt und auf den aktuellen Nutzer begrenzt, zum Beispiel mit DPAPI unter Windows, dem Schlüsselbund unter macOS oder Secret Service unter Linux, in den Anwendungsdaten pro Nutzer statt neben der ausführbaren Datei. sgcWebSockets liefert absichtlich keinen eigenen Token-Tresor: Es gibt dir das Token und überlässt dir die Entscheidung über die Speicherung. Denk daran, HTTPClientOptions.LogOptions vor dem Ausliefern auszuschalten, weil dieses Log die Tokens enthält.
Hol dir mit dem Flow auf dieser Seite ein Access Token und fordere dabei den Mail-Scope des Anbieters an, etwa https://mail.google.com/, und leg es dann über SASL XOAUTH2 vor. sgcIndy liefert TIdSASLXOAUTH2 in der Unit IdSASLXOAUTH2. Füge es TIdSMTP.SASLMechanisms hinzu, setze AuthType := satSASL und gib Benutzernamen und Access Token aus seinem Ereignis OnAuthenticate zurück. Derselbe Mechanismus authentifiziert TIdIMAP4 und TIdPOP3.
Der OAuth2-Client und der JWT-Client sind in der Edition Standard und jeder Edition darüber einkompiliert, Standard, Professional, Enterprise und All-Access enthalten sie also alle. Der OAuth2-Server, der JWT-Server und der WebAuthn-Server sind Enterprise-Komponenten und in Standard- oder Professional-Builds nicht enthalten. Die beiden Client-Komponenten werden außerdem einzeln als eigenständiges Paket sgcAuth verkauft, gebündelt mit der Runtime, die sie brauchen.
Nur wenn du derjenige bist, der Tokens ausgibt. Nutzer bei Google, Microsoft, Auth0, Okta, Keycloak, AWS Cognito oder einem Unternehmens-Identitätsanbieter anzumelden braucht die Client-Komponente und sonst nichts. Du brauchst TsgcHTTP_OAuth2_Server, wenn deine eigene Anwendung Client-IDs registriert, die Anmeldeseite hostet und die Tokens prägt, denen deine API vertraut. Es prüft PKCE standardmäßig über OAuth2Options.PKCE, registriert Anwendungen mit Apps.AddApp und hängt sich an einen TsgcWebSocketHTTPServer über Authentication.OAuth.OAuth2 an.
Ja. Behandle OnBeforeAuthorizeCode, das die fertig gebaute Autorisierungs-URL als Parameter var bekommt, setze Handled := True, damit die Komponente nicht den Systembrowser startet, und navigiere ein eingebettetes Steuerelement wie TsgcWebView2 zu dieser URL. Der Loopback-Listener empfängt den Redirect weiterhin, und der Flow läuft normal zu Ende. Beachte, dass mehrere Anbieter ihren Zustimmungsbildschirm inzwischen in eingebetteten Browsern blockieren, deshalb ist der Systembrowser die Voreinstellung.
Ja. TsgcHTTP_OAuth2_Client kompiliert für Windows, macOS, Linux, iOS und Android, in VCL, FireMonkey und Lazarus / FPC, von Delphi 7 bis Delphi 13 und den passenden C++-Builder-Versionen. Zum Öffnen des Browsers wird genutzt, was die Plattform bereitstellt. Die einzige plattformspezifische Wahl ist das TLS-Backend für den Token-Tausch, ausgewählt über HTTPClientOptions.TLSOptions.IOHandler.

Referenz, Demo und Dokumentation

Die Komponentenreferenz, das lauffähige Demoprojekt und die technischen Dokumente, die tiefer gehen als diese Seite.

Online-Hilfe, TsgcHTTP_OAuth2_Client Jede Eigenschaft, Methode und jedes Ereignis der Client-Komponente, mit dem Thema Authorization Code + PKCE.
Online-Hilfe, Authorization Code mit PKCE Das Thema zum Grant Type: was PKCE tut, die Konfigurationstabelle und die Empfehlung zum zufälligen Port.
Demoprojekt, Demos\20.HTTP_Protocol\02.OAuth2_Authentication Client- und Server-Projekte mit funktionierenden Voreinstellungen für Gmail, Google Pub/Sub, Azure AD, AWS Cognito, Dropbox und Auth0, dazu eine Variante mit eingebettetem Browser.
Technisches Dokument, OAuth2 Client (PDF) Funktionen, Schnellstart, jeder Grant Type und Codebeispiele für Delphi, C++ Builder und .NET.
Technisches Dokument, OAuth2 Server (PDF) Die Enterprise-Komponente für den Autorisierungsserver: Endpunkte, App-Registrierung, PKCE-Prüfung und Token-Lebenszyklus.
Benutzerhandbuch (PDF) Umfassendes Handbuch zu jeder Komponente der Bibliothek.

Spezifikationen, die dieser Flow umsetzt

Primärquellen, wenn du eine Diskussion mit dem Support eines Anbieters klären musst.

Komponenten und Artikel hinter dieser Seite

Die Komponentenseiten führen die vollständige Funktionsliste, die Artikel decken die Fälle ab, die diese Seite nur streift.

Komponente OAuth2 Client

Die vollständige Oberfläche aus Eigenschaften, Methoden und Ereignissen von TsgcHTTP_OAuth2_Client, einschließlich Device Code und DPoP.

Mehr erfahren →

Komponente OAuth2 Server

Der Enterprise-Autorisierungsserver: deine eigenen Endpunkte authorize, token, revoke und introspect.

Mehr erfahren →

sgcAuth

Die OAuth2- und JWT-Client-Komponenten als eigenständiges Paket, mit der benötigten Runtime gebündelt.

Mehr erfahren →

Komponente JWT Client

JSON Web Tokens signieren und anhängen, für sich allein oder als Bearer-Quelle für deine HTTP- und WebSocket-Clients.

Mehr erfahren →

Delphi PKCE OAuth2

Der ursprüngliche Release-Artikel, der die PKCE-Unterstützung in Client- und Server-Komponenten vorstellt.

Beitrag lesen →

sgcIndy XOAuth2

Mail mit einem OAuth-2.0-Access-Token über SMTP, IMAP und POP versenden, per SASL-Mechanismus XOAUTH2.

Beitrag lesen →

OAuth2 Client Credentials

Die Variante ohne Nutzer, für Hintergrunddienste und Maschine-zu-Maschine-API-Zugriff.

Beitrag lesen →

OAuth2 DPoP in Delphi

Ein Access Token an ein Schlüsselpaar binden, für Anbieter, die nach RFC 9449 einen Besitznachweis verlangen.

Beitrag lesen →

AWS Cognito und OAuth2

Eine durchgearbeitete Konfiguration gegen einen echten Identitätsanbieter, Endpunkt für Endpunkt.

Beitrag lesen →

OAuth2 Server: Apps registrieren

Client-Anwendungen, Redirect-URIs und erlaubte Grant Types auf deinem eigenen Autorisierungsserver registrieren.

Beitrag lesen →

Autorisierung mit externen Anbietern

Den eigenen Server die Anmeldung an Google, Microsoft oder einen anderen externen Identitätsanbieter delegieren lassen.

Beitrag lesen →

WebAuthn und Passkeys

Die passwortlose Alternative, wenn du lieber gar keine Token-Übergabe hättest.

Mehr erfahren →

Diese Seite ist einer der Delphi-Anwendungsfälle, die jeweils eine einzelne Aufgabe von Anfang bis Ende durchgehen. Die anderen bisher sind ein LLM aus Delphi aufrufen und zwei Anwendungen per WebRTC Peer-to-Peer verbinden.

Melde heute deinen ersten Nutzer an

Lade die kostenlose Testversion herunter, öffne die OAuth2-Demo, richte sie auf deinen Anbieter und sieh dem Browser-Roundtrip beim Abschließen zu.