Een gebruiker inloggen in een Delphi-toepassing met OAuth2 en PKCE

Eén component, één grant type, één overdracht naar de browser. Deze pagina brengt je van een leeg formulier naar een ingelogde gebruiker met een geldig access token, via de Authorization Code-flow met PKCE (RFC 7636), de flow die elke provider tegenwoordig verwacht van een native desktoptoepassing.

TsgcHTTP_OAuth2_Client
Code verifier en code challenge worden voor je gegenereerd
Delphi 7 tot 13, C++ Builder, VCL en FireMonkey

Wat je nodig hebt om een gebruiker in te loggen

Eén niet-visuele component praat met de provider. Je hebt geen webserver, ingebouwde browser of REST-framework nodig.

Component

TsgcHTTP_OAuth2_Client, gedeclareerd in unit sgcHTTP en in code aangemaakt, zoals elke demo het doet.

Grant type

OAuth2Options.GrantType := auth2CodePKCE. Die ene toewijzing zet PKCE aan.

Editie

Standard, Professional, Enterprise en All-Access. De client is geen Enterprise-functie, de server wel.

Platforms

Windows, macOS, Linux, iOS en Android. De component opent de browser die het platform aanbiedt.

Wat de PKCE-flow werkelijk doet

PKCE bestaat omdat een desktoptoepassing geen geheim kan bewaren. Het vervangt het geheim door een waarde waarvan de client bewijst dat hij die al kende voordat de flow begon.

1. Genereer een code verifier

Een willekeurige string met hoge entropie. sgcWebSockets vraagt de CSPRNG van het platform om 32 bytes en codeert die als Base64URL, wat de verifier van 43 tekens oplevert die RFC 7636 verlangt.

2. Leid de code challenge af

SHA-256 van de verifier, gecodeerd als Base64URL. De challenge is wat meereist in het autorisatieverzoek, dus wie de redirect afluistert ziet de verifier nooit.

3. Open de browser

De component bouwt de autorisatie-URL op met client_id, redirect_uri, scope, state, code_challenge en code_challenge_method=S256, en start daarna de systeembrowser.

4. De gebruiker logt in

De toestemming vindt plaats in de browser, op het eigen domein van de provider, met de bestaande sessie, wachtwoordmanager en tweefactorapparaat van de gebruiker. Jouw toepassing ziet het wachtwoord nooit.

5. De redirect komt terug

De provider stuurt je door naar je redirect_uri met code en state erin. Op de desktop is die URI een loopback-adres, en de component luistert er al.

6. Wissel de code in

De component POST de code plus de oorspronkelijke code_verifier naar het token-endpoint. De provider berekent SHA-256 opnieuw en vergelijkt. Komt het overeen, dan krijg je een access token.

Waarom de verifier ertoe doet

Een authorization code is de paar seconden dat hij leeft een bearer-waarde. Alles wat de redirect kan waarnemen, een kwaadaardige toepassing die op hetzelfde custom URI-schema geregistreerd staat, een proxy, een gedeeld logbestand, kan hem stelen. Zonder PKCE is die gestolen code genoeg om een token aan te maken.

Met PKCE weigert het token-endpoint de code, tenzij de aanroeper ook de verifier meestuurt waarvan de SHA-256-hash overeenkomt met de challenge die aan het begin is verzonden. De aanvaller zag alleen de hash, dus de gestolen code is waardeloos.

Niets hiervan hoef je zelf te schrijven. Zet GrantType op auth2CodePKCE en de component voert stap 1, 2, 3, 5 en 6 voor je uit. Hierna volgt de code die het aanstuurt, en de twee beslissingen die je wel zelf moet nemen: de redirect-URI en waar het refresh token komt te staan.

over de lijn
# 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

Log een gebruiker in, in een stuk of twintig regels

Maak de component aan, kies auth2CodePKCE, wijs hem naar de twee endpoints van de provider, koppel OnAfterAccessToken en roep Start aan. De browser gaat open, de gebruiker geeft toestemming, en de gebeurtenis vuurt af met het token.

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");
}

Waar de redirect naartoe gaat in een desktoptoepassing

Dit is het onderdeel waarop geen voor de hand liggend antwoord bestaat als je van OAuth2 op het web komt, en het onderdeel dat de meeste eerste pogingen fout doen.

Loopback, geen publieke URL

Een desktoptoepassing heeft geen domein om naartoe te sturen. Het geaccepteerde antwoord, en het antwoord dat deze component implementeert, is een loopback-redirect: de toepassing start een kleine HTTP-listener op 127.0.0.1, registreert dat adres als de redirect-URI, en sluit de listener zodra de code binnenkomt.

LocalServerOptions.IP staat standaard op 127.0.0.1 en LocalServerOptions.Port standaard op 8080. Zet voor een uitgeleverde desktoptoepassing liever Port := 0: het besturingssysteem deelt een vrije ephemeral port uit, de component zet die poort in de redirect-URI die hij verstuurt, en twee kopieën van je toepassing op dezelfde machine botsen nooit.

Staat de provider erop dat je een exact geregistreerd pad gebruikt in plaats van alleen een host en een poort, zet LocalServerOptions.RedirectURL dan op de waarde die je hebt geregistreerd. Die string overschrijft vervolgens de berekende waarde. Een vast pad betekent een vaste poort, dus registreer de poort ook en laat de truc met Port := 0 vallen.

De listener draait alleen zolang de flow staat te wachten. Hij wordt nooit gestart voor auth2ClientCredentials, auth2ResourceOwnerPassword of auth2DeviceCode, die helemaal geen redirect nodig hebben.

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;

Lees het token uit en zet het aan het werk

Nadat OnAfterAccessToken is afgevuurd, blijven dezelfde waarden beschikbaar als alleen-lezen properties, en de component kan ze doorgeven aan je HTTP- en WebSocket-clients zonder dat jij een header hoeft aan te raken.

Properties, en automatische Bearer-headers

De parameters van de gebeurtenis zijn handig, maar ze zijn niet de enige kopie. AccessToken, TokenType, CurrentExpiresIn en CurrentRefreshToken houden dezelfde waarden vast zolang de component bestaat, dus een handler elders in je code kan ze uitlezen zonder dat je ze hoeft door te geven.

RawParams is de onbewerkte JSON-body van het token-endpoint. Geeft een provider iets terug dat buiten de standaardset valt, bijvoorbeeld een id_token voor OpenID Connect, dan haal je dat daaruit. De component decodeert een ID-token niet voor je.

Wil je dat elk verzoek het token automatisch meestuurt, wijs de OAuth2-component dan toe aan Authentication.Token.OAuth op TsgcHTTP1Client, TsgcHTTP2Client of TsgcWebSocketClient. De client stuurt namens jou Authorization: Bearer <token> mee, met het token_type dat de provider heeft teruggegeven.

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;

Vernieuwen, zodat de browser nooit twee keer opengaat

Een access token leeft minuten. Een refresh token leeft weken of maanden. Het bewaren van dat tweede token maakt van één keer inloggen een sessie.

Twee verschillende problemen

Binnen één run van de toepassing hoef je niets te doen. Geeft het token-endpoint zowel een refresh token als een expires_in terug, dan zet de component een interne timer klaar op ongeveer de helft van die levensduur en verstuurt grant_type=refresh_token zodra die afgaat, ruim voordat het access token verloopt. OnAfterRefreshToken vuurt af met het nieuwe paar, en OnErrorRefreshToken vuurt af als de provider het weigert. Laat de parameter Handled van OnAfterAccessToken met rust: door hem op True te zetten vertel je de component dat jij het overneemt, en dan slaat hij het refresh token niet op en zet hij die timer ook niet klaar.

Over herstarts heen is jouw probleem, want alleen jij weet waar op de machines van je gebruikers een geheim mag worden weggeschreven. Bewaar het refresh token, sla bij de volgende start Start helemaal over en roep Refresh aan met de opgeslagen waarde. Er gaat geen browser open, en de gebruiker is ingelogd voordat je hoofdformulier getekend is.

Providers die refresh tokens roteren geven je bij elke vernieuwing een nieuwe, dus overschrijf wat je hebt opgeslagen bij elke OnAfterRefreshToken. Wordt het opgeslagen token uiteindelijk geweigerd, val dan terug op Start en laat de gebruiker opnieuw inloggen.

Gebruik Revoke om de gebruiker netjes uit te loggen, en Introspect om de provider te vragen of een token nog geldig is. Voor beide moet het bijbehorende endpoint in AuthorizationServerOptions zijn ingesteld.

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 opslaan zonder ze te laten rondslingeren

sgcWebSockets levert bewust geen tokenkluis mee. Waar een credential mag worden weggeschreven is een beslissing over jouw gebruikers en jouw uitrol, dus de bibliotheek geeft je het token en houdt daar op.

Houd het access token alleen in het geheugen

Het verloopt binnen enkele minuten en het refresh token kan er altijd een nieuwe aanmaken. Er is geen enkele reden om het naar schijf te schrijven, en elke reden om dat niet te doen.

Versleutel het refresh token per gebruiker

Op Windows koppelt DPAPI (CryptProtectData) de versleutelde tekst aan het Windows-account, zodat een gekopieerd bestand op een andere machine nutteloos is. macOS heeft Keychain, en moderne Linux-desktops hebben Secret Service.

Lever nooit een client secret uit waar je op vertrouwt

Alles in een uitgeleverde executable is openbaar. Dat is precies het uitgangspunt van PKCE. Geeft je provider een secret uit voor een desktopclient, behandel die dan als een identifier en niet als bescherming.

Behandel het bestand als een credential

Toepassingsgegevens per gebruiker, niet Program Files, niet naast de executable, geen gedeeld netwerkpad, en geen kaal INI-bestand in versiebeheer.

Verwijderen bij uitloggen

Roep Revoke aan zodat de provider het token ongeldig maakt, en verwijder daarna de opgeslagen kopie. Een ingetrokken token dat op schijf blijft staan is nog steeds een auditbevinding.

Houd geheimen uit het logbestand

HTTPClientOptions.LogOptions schrijft het verkeer naar de autorisatieserver weg. Het is onmisbaar terwijl je de flow aan de praat krijgt, en het is een bestand vol tokens. Zet het uit voordat je uitlevert.

Welke provider wat nodig heeft

Elke OAuth 2.0-provider vraagt om dezelfde handvol instellingen: twee endpoints, een client id, de scopes en een geregistreerde redirect. Voor Google en Microsoft zijn er bovendien kant-en-klare componenten die de endpoints invullen en een gebruikersprofiel teruggeven.

Google en Microsoft, in één aanroep

TsgcHTTP_OAuth2_Client_Google en TsgcHTTP_OAuth2_Client_Microsoft stammen af van dezelfde basiscomponent en vullen de endpoints alvast in. Hun methode Authenticate is blokkerend: hij doorloopt de hele flow, wacht op de rondgang door de browser, en geeft een dataobject terug met Authenticated en een gevulde UserProfile.

Dat is de kortst mogelijke weg naar "wie is deze gebruiker". TsgcOAuth2_Google_Data.UserProfile bevat _Name, Given_Name, Family_Name, Id, Locale en Picture. TsgcOAuth2_Microsoft_Data.UserProfile bevat DisplayName, GivenName, Surname, Mail, JobTitle, OfficeLocation en meer. Bij Microsoft neemt Authenticate als eerste het tenant id.

Gebruik voor elke andere provider de basiscomponent TsgcHTTP_OAuth2_Client en kopieer de twee URL's uit hun documentatie. Daarna blijft er niets providerspecifieks meer over.

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;
Provider Component Grant type Redirect Client secret
Google TsgcHTTP_OAuth2_Client_Google of de basisclient auth2CodePKCE Loopback, Port := 0 Wordt uitgegeven voor desktopclients, stel hem in als je er een hebt
Microsoft Entra ID TsgcHTTP_OAuth2_Client_Microsoft of de basisclient auth2CodePKCE Loopback, geregistreerd als mobiel / desktopplatform Wordt niet gebruikt door een public client, laat hem leeg
Auth0, Okta, Keycloak, AWS Cognito TsgcHTTP_OAuth2_Client auth2CodePKCE Loopback, geregistreerd op de toepassing Hangt ervan af of de app public of confidential is
Achtergrondtaken en services TsgcHTTP_OAuth2_Client auth2ClientCredentials Geen, er komt geen browser aan te pas Vereist, en veilig, omdat er niets wordt uitgeleverd
Kiosken, tv's, headless machines TsgcHTTP_OAuth2_Client auth2DeviceCode (RFC 8628) Geen, de gebruiker rondt het af op een telefoon Meestal niet vereist

Inloggen om e-mail te versturen: OAuth 2.0 en XOAUTH2

Gmail en Microsoft 365 accepteren geen wachtwoorden meer over SMTP, IMAP en POP. De vervanging is hetzelfde access token dat je zojuist hebt opgehaald, aangeboden via het SASL XOAUTH2-mechanisme.

Het token komt hiervandaan, de SASL-stap komt van sgcIndy

Het token ophalen is precies de flow hierboven: auth2CodePKCE, een loopback-redirect, en een mailscope zoals https://mail.google.com/ in AuthorizationServerOptions.Scope. Aan de OAuth2-kant verandert er niets voor het mailgeval.

Het aanbieden is de andere helft. sgcIndy levert TIdSASLXOAUTH2 in unit IdSASLXOAUTH2. Voeg hem toe aan TIdSMTP.SASLMechanisms, zet AuthType := satSASL, en lever de gebruikersnaam en het access token aan vanuit de gebeurtenis OnAuthenticate. Hetzelfde mechanisme werkt voor TIdIMAP4 en TIdPOP3.

Houd de twee componenten in gedachten uit elkaar: de OAuth2-client weet hoe hij een token ophaalt en vernieuwt, het SASL-mechanisme weet hoe je er een aanbiedt. Geen van beide hoeft van de ander te weten.

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;

Heb je een client nodig, of ook een server?

Alles hierboven speelt zich af aan de clientkant. Je hebt de tweede helft alleen nodig als jij degene bent die de tokens uitgeeft.

Alleen een client

Log je gebruikers in bij de identityprovider van iemand anders, Google, Microsoft, Auth0, Okta, Keycloak, AWS Cognito, je eigen bedrijfs-SSO, dan heb je niets anders nodig dan TsgcHTTP_OAuth2_Client. Die component zit gecompileerd in de Standard-editie en in elke editie daarboven. Hij is ook los verkrijgbaar in het zelfstandige package sgcAuth, met de runtime die hij nodig heeft erbij.

Dit is het gebruikelijke geval, en het is alles wat deze pagina tot hier behandelt.

Wanneer je ook een server nodig hebt

Je hebt de serverhelft alleen nodig als je eigen toepassing de autorisatieserver is: jij geeft de client ids uit, jij host de inlogpagina, jij maakt de access tokens aan die je API vervolgens vertrouwt en trekt ze weer in. Dat is TsgcHTTP_OAuth2_Server, gekoppeld aan een TsgcWebSocketHTTPServer, en dat is een Enterprise-component.

Hij controleert PKCE standaard. OAuth2Options.PKCE staat direct op True, dus een client die een challenge stuurt moet een bijpassende verifier leveren, en een client die dat niet doet wordt geweigerd. Registreer clienttoepassingen met Apps.AddApp, authenticeer gebruikers in OnOAuth2Authentication, en herstel tokens na een herstart met AddToken.

Dezelfde Enterprise-laag bevat TsgcHTTP_JWT_Server om JWT bearer tokens op je endpoints te valideren en TsgcWSAPIServer_WebAuthn voor passkeys. De bijbehorende clients, TsgcHTTP_OAuth2_Client en TsgcHTTP_JWT_Client, zitten in Standard en hoger. Client en server zitten in verschillende editielagen, en dat is het waard om te controleren voordat je je planning op een van beide baseert.

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;

Wat er de eerste keer meestal misgaat

Bijna elke mislukte eerste poging met OAuth2 op de desktop is er een van deze zes.

redirect_uri_mismatch

De URI die de component verstuurt moet teken voor teken overeenkomen met wat je hebt geregistreerd, inclusief de afsluitende slash en de poort. Heb je een vaste URI geregistreerd, zet LocalServerOptions.RedirectURL dan op precies die string in plaats van te vertrouwen op de berekende waarde. Staat de provider elke loopback-poort toe, gebruik dan Port := 0 en registreer alleen de host.

De browser gaat open en er komt niets terug

Iets houdt de poort bezet, of een firewallregel blokkeert de loopback-listener. Zet Port := 0, en controleer of een eerdere run van de flow met Stop is beëindigd en niet is blijven luisteren.

invalid_grant bij de tokenuitwisseling

Authorization codes zijn eenmalig en kortlevend. Debuggen met een breakpoint tussen de redirect en de uitwisseling laat de code verlopen. Lees de fout uit OnErrorAccessToken, dat je de eigen error en error_description van de provider geeft, in plaats van te gokken.

Er is geen refresh token teruggegeven

Providers geven er alleen een uit als je erom vraagt. Google wil access_type=offline, Microsoft wil de scope offline_access. Voeg de scope toe aan AuthorizationServerOptions.Scope, of plak de queryparameter erachter door de parameter URL in OnBeforeAuthorizeCode aan te passen.

TLS mislukt op Linux of mobiel

De tokenuitwisseling is een HTTPS POST, dus er is een werkende TLS-backend nodig. HTTPClientOptions.TLSOptions.IOHandler kiest die: iohOpenSSL, iohSChannel op Windows zonder DLL's om uit te rollen, of de native handlers iohAndroidTLS en iohAppleTLS in de Enterprise-editie.

Je wilde de inlogpagina in de app zelf

Handel OnBeforeAuthorizeCode af, zet Handled := True en navigeer je eigen TsgcWebView2 of TWebBrowser naar de URL die je hebt gekregen. De loopback-listener vangt de redirect nog steeds op. Houd er rekening mee dat meerdere providers hun toestemmingsscherm inmiddels weigeren te tonen in een ingebouwde browser.

Vragen over OAuth2 en PKCE in Delphi

De vragen waar ontwikkelaars echt op zoeken voordat ze beginnen.

Zet een TsgcHTTP_OAuth2_Client neer, stel OAuth2Options.GrantType := auth2CodePKCE in, vul OAuth2Options.ClientId, AuthorizationServerOptions.AuthURL, AuthorizationServerOptions.TokenURL en AuthorizationServerOptions.Scope in, zet LocalServerOptions.IP op 127.0.0.1 en LocalServerOptions.Port op 0, en roep dan Start aan. De component genereert de PKCE-waarden, opent de browser, vangt de redirect op met een loopback-listener, wisselt de code in en vuurt OnAfterAccessToken af met het token.
Dat hoeft niet. Als GrantType op auth2CodePKCE staat, haalt TsgcHTTP_OAuth2_Client 32 bytes uit de cryptografische willekeurbron van het platform, codeert die als Base64URL tot de code verifier van 43 tekens, zet de code challenge op de Base64URL-codering van de SHA-256-hash van die verifier, en legt code_challenge_method vast op S256. De verifier blijft privé binnen de component en wordt bij de tokenuitwisseling opnieuw meegestuurd, dus hij komt nooit in de redirect terecht. Wil je het paar met de hand opbouwen voor een ander doel, dan zijn dezelfde primitieven publiek: sgcRandomBytes in unit sgcCrypto_Random, plus GetHashSHA256 en EncodeBase64URL in unit sgcBase_Helpers.
Een loopback-adres. TsgcHTTP_OAuth2_Client start alleen tijdens de flow een kleine HTTP-listener op LocalServerOptions.IP en LocalServerOptions.Port, en de redirect-URI die hij verstuurt wordt uit die waarden opgebouwd. De standaardwaarden zijn 127.0.0.1 en poort 8080. Zet voor een uitgeleverde toepassing Port op 0, zodat het besturingssysteem een vrije ephemeral port kiest en twee instanties nooit om dezelfde poort vechten. Vereist de provider een exact geregistreerde URI, zet die string dan in LocalServerOptions.RedirectURL en hij overschrijft de berekende waarde.
Dat hangt van de provider af. PKCE bestaat juist omdat een uitgeleverde desktoptoepassing geen geheim kan bewaren, dus een public client stuurt normaal gesproken helemaal geen secret mee en laat OAuth2Options.ClientSecret leeg. Sommige providers geven er toch een uit voor desktopclients en verwachten hem bij het tokenverzoek. Stel hem in als dat zo is, maar behandel hem als een identifier en niet als bescherming, want alles in een uitgeleverde executable kan eruit worden gehaald.
Bewaar het refresh token en roep bij de volgende start Refresh ermee aan in plaats van Start. Lees het uit de parameter Refresh_Token van OnAfterAccessToken, of later uit de property CurrentRefreshToken. Overschrijf de opgeslagen kopie bij elke OnAfterRefreshToken, want providers die refresh tokens roteren maken de oude ongeldig. Binnen één run hoef je helemaal niets te doen: de component zet een timer klaar op basis van de waarde expires_in en vernieuwt het access token vanzelf.
Houd het access token alleen in het geheugen, het verloopt binnen enkele minuten en kan altijd opnieuw worden aangemaakt. Bewaar het refresh token versleuteld en gekoppeld aan de huidige gebruiker, bijvoorbeeld met DPAPI op Windows, Keychain op macOS of Secret Service op Linux, in toepassingsgegevens per gebruiker en niet naast de executable. sgcWebSockets levert bewust geen eigen tokenkluis mee: het geeft je het token en laat de opslagbeslissing aan jou. Vergeet niet HTTPClientOptions.LogOptions uit te zetten voordat je uitlevert, want dat logbestand bevat de tokens.
Haal een access token op met de flow op deze pagina en vraag daarbij de mailscope van de provider aan, zoals https://mail.google.com/, en bied het daarna aan via SASL XOAUTH2. sgcIndy levert TIdSASLXOAUTH2 in unit IdSASLXOAUTH2. Voeg hem toe aan TIdSMTP.SASLMechanisms, zet AuthType := satSASL, en geef de gebruikersnaam en het access token terug vanuit de gebeurtenis OnAuthenticate. Hetzelfde mechanisme authenticeert TIdIMAP4 en TIdPOP3.
De OAuth2-client en de JWT-client zitten gecompileerd in de Standard-editie en in elke editie daarboven, dus Standard, Professional, Enterprise en All-Access bevatten ze allemaal. De OAuth2-server, de JWT-server en de WebAuthn-server zijn Enterprise-componenten en zitten niet in Standard- of Professional-builds. De twee clientcomponenten worden ook los verkocht als het zelfstandige package sgcAuth, met de runtime die ze nodig hebben erbij.
Alleen als jij degene bent die tokens uitgeeft. Gebruikers laten inloggen bij Google, Microsoft, Auth0, Okta, Keycloak, AWS Cognito of een bedrijfsidentityprovider vraagt om de clientcomponent en niets anders. Je hebt TsgcHTTP_OAuth2_Server nodig als je eigen toepassing client ids registreert, de inlogpagina host en de tokens uitgeeft die je API vertrouwt. Hij valideert PKCE standaard via OAuth2Options.PKCE, registreert toepassingen met Apps.AddApp, en koppelt aan een TsgcWebSocketHTTPServer via Authentication.OAuth.OAuth2.
Ja. Handel OnBeforeAuthorizeCode af, dat de volledig opgebouwde autorisatie-URL als var-parameter meekrijgt, zet Handled := True zodat de component de systeembrowser niet start, en navigeer een ingebouwd besturingselement zoals TsgcWebView2 naar die URL. De loopback-listener ontvangt de redirect nog steeds en de flow wordt normaal afgerond. Houd er rekening mee dat meerdere providers hun toestemmingsscherm inmiddels blokkeren in ingebouwde browsers, en dat is precies waarom de systeembrowser de standaard is.
Ja. TsgcHTTP_OAuth2_Client compileert voor Windows, macOS, Linux, iOS en Android, in VCL, FireMonkey en Lazarus / FPC, van Delphi 7 tot en met Delphi 13 en de bijbehorende versies van C++ Builder. Voor het openen van de browser wordt gebruikt wat het platform aanbiedt. De enige platformspecifieke keuze is de TLS-backend voor de tokenuitwisseling, die je kiest via HTTPClientOptions.TLSOptions.IOHandler.

Referentie, demo en documentatie

De componentreferentie, het kant-en-klare demoproject, en de technische documenten die dieper gaan dan deze pagina.

Online help, TsgcHTTP_OAuth2_Client Elke property, methode en gebeurtenis van de clientcomponent, met het onderwerp Authorization Code + PKCE.
Online help, Authorization Code met PKCE Het onderwerp over het grant type: wat PKCE doet, de configuratietabel en de aanbeveling voor een willekeurige poort.
Demoproject, Demos\20.HTTP_Protocol\02.OAuth2_Authentication Client- en serverprojecten met werkende presets voor Gmail, Google Pub/Sub, Azure AD, AWS Cognito, Dropbox en Auth0, plus een variant met een ingebouwde browser.
Technisch document, OAuth2 Client (PDF) Functies, snelstart, elk grant type en codevoorbeelden voor Delphi, C++ Builder en .NET.
Technisch document, OAuth2 Server (PDF) De autorisatieservercomponent uit Enterprise: endpoints, appregistratie, PKCE-validatie en de levenscyclus van tokens.
Gebruikershandleiding (PDF) Uitgebreide handleiding over elk component in de bibliotheek.

Specificaties die deze flow implementeert

Primaire bronnen, voor als je een discussie met de supportafdeling van een provider moet beslechten.

Componenten en artikelen achter deze pagina

De componentpagina's bevatten de volledige functielijst, de artikelen behandelen de gevallen die deze pagina alleen aanstipt.

OAuth2 Client-component

Het volledige aanbod aan properties, methoden en gebeurtenissen van TsgcHTTP_OAuth2_Client, inclusief Device Code en DPoP.

Lees meer →

OAuth2 Server-component

De autorisatieserver uit Enterprise: je eigen authorize-, token-, revoke- en introspect-endpoints.

Lees meer →

sgcAuth

De OAuth2- en JWT-clientcomponenten als zelfstandig package, met de runtime die ze nodig hebben erbij.

Lees meer →

JWT Client-component

Onderteken en koppel JSON Web Tokens, op zichzelf of als Bearer-bron voor je HTTP- en WebSocket-clients.

Lees meer →

Delphi PKCE OAuth2

Het oorspronkelijke release-artikel dat PKCE-ondersteuning introduceerde op zowel de client- als de servercomponent.

Lees het artikel →

sgcIndy XOAuth2

Mail versturen met een OAuth 2.0 access token over SMTP, IMAP en POP via het SASL XOAUTH2-mechanisme.

Lees het artikel →

OAuth2 Client Credentials

De variant zonder gebruiker, voor achtergrondservices en machine-to-machine API-toegang.

Lees het artikel →

OAuth2 DPoP in Delphi

Een access token binden aan een sleutelpaar, voor providers die proof of possession vereisen onder RFC 9449.

Lees het artikel →

AWS Cognito en OAuth2

Een uitgewerkte configuratie tegen een echte identityprovider, endpoint voor endpoint.

Lees het artikel →

OAuth2 Server: apps registreren

Clienttoepassingen, redirect-URI's en toegestane grant types registreren op je eigen autorisatieserver.

Lees het artikel →

Autorisatie met externe providers

Je eigen server het inloggen laten delegeren aan Google, Microsoft of een andere externe identityprovider.

Lees het artikel →

WebAuthn en passkeys

Het wachtwoordloze alternatief, voor als je liever helemaal geen tokenoverdracht hebt.

Lees meer →

Deze pagina is een van de Delphi use cases, die elk één taak van begin tot eind doorlopen. De andere tot nu toe zijn een LLM aanroepen vanuit Delphi en twee toepassingen peer-to-peer verbinden met WebRTC.

Log vandaag nog je eerste gebruiker in

Download de gratis proefversie, open de OAuth2-demo, wijs hem naar je provider en zie de rondgang door de browser voltooid worden.