Autentica un utente in un'applicazione Delphi con OAuth2 e PKCE

Un componente, un grant type, un passaggio al browser. Questa pagina ti porta da un form vuoto a un utente autenticato con un access token attivo, usando il flusso Authorization Code con PKCE (RFC 7636), il flusso che oggi ogni provider si aspetta da un'applicazione desktop nativa.

TsgcHTTP_OAuth2_Client
Code verifier e code challenge generati per te
Da Delphi 7 a 13, C++ Builder, VCL e FireMonkey

Cosa ti serve per autenticare un utente

Un unico componente non visuale dialoga con il provider. Non ti servono un web server, un browser incorporato o un framework REST.

Componente

TsgcHTTP_OAuth2_Client, dichiarato nella unit sgcHTTP e creato da codice, come fa ogni demo.

Grant type

OAuth2Options.GrantType := auth2CodePKCE. Questa singola assegnazione attiva PKCE.

Edizione

Standard, Professional, Enterprise e All-Access. Il client non è una funzionalità Enterprise, il server sì.

Piattaforme

Windows, macOS, Linux, iOS e Android. Il componente apre il browser che la piattaforma mette a disposizione.

Cosa fa davvero il flusso PKCE

PKCE esiste perché un'applicazione desktop non può custodire un segreto. Sostituisce il segreto con un valore che il client dimostra di conoscere già prima dell'avvio del flusso.

1. Genera un code verifier

Una stringa casuale ad alta entropia. sgcWebSockets chiede 32 byte al CSPRNG della piattaforma e li codifica in Base64URL, ottenendo il verifier di 43 caratteri richiesto da RFC 7636.

2. Deriva il code challenge

Lo SHA-256 del verifier, codificato in Base64URL. È il challenge a viaggiare nella richiesta di autorizzazione, quindi chi intercetta il redirect non vede mai il verifier.

3. Apri il browser

Il componente costruisce l'URL di autorizzazione con client_id, redirect_uri, scope, state, code_challenge e code_challenge_method=S256, poi avvia il browser di sistema.

4. L'utente accede

Il consenso avviene nel browser, sul dominio del provider stesso, con la sessione già attiva dell'utente, il suo gestore di password e il dispositivo per il secondo fattore. La tua applicazione non vede mai la password.

5. Il redirect torna indietro

Il provider reindirizza al tuo redirect_uri portando code e state. Su desktop quell'URI è un indirizzo di loopback, e il componente è già in ascolto su di esso.

6. Scambia il code

Il componente invia in POST il code insieme al code_verifier originale all'endpoint del token. Il provider ricalcola lo SHA-256 e confronta. Se coincidono, ottieni un access token.

Perché il verifier è importante

Un authorization code è un valore al portatore per i pochi secondi in cui vive. Chiunque possa osservare il redirect, un'applicazione malevola registrata sullo stesso schema URI personalizzato, un proxy, un log condiviso, può rubarlo. Senza PKCE quel code rubato basta a generare un token.

Con PKCE l'endpoint del token rifiuta il code se il chiamante non presenta anche il verifier il cui hash SHA-256 corrisponde al challenge inviato all'inizio. L'attaccante ha visto solo l'hash, quindi il code rubato non vale nulla.

Non devi scrivere nulla di tutto questo. Imposta GrantType su auth2CodePKCE e il componente esegue per te i passi 1, 2, 3, 5 e 6. Quello che segue è il codice che lo mette in moto, e le due decisioni che invece spettano a te: il redirect URI e dove risiede il refresh token.

sulla rete
# 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

Autentica un utente in una ventina di righe

Crea il componente, scegli auth2CodePKCE, indicagli i due endpoint del provider, aggancia OnAfterAccessToken, chiama Start. Il browser si apre, l'utente concede il consenso, l'evento scatta con il 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");
}

Dove va il redirect in un'applicazione desktop

È la parte che non ha una risposta ovvia se arrivi dall'OAuth2 per il web, ed è la parte che quasi tutti sbagliano al primo tentativo.

Loopback, non un URL pubblico

Un'applicazione desktop non ha un dominio verso cui reindirizzare. La risposta accettata, e quella implementata da questo componente, è il redirect di loopback: l'applicazione avvia un piccolo listener HTTP su 127.0.0.1, registra quell'indirizzo come redirect URI e chiude il listener non appena arriva il code.

LocalServerOptions.IP vale 127.0.0.1 per impostazione predefinita e LocalServerOptions.Port vale 8080. In un'applicazione desktop distribuita imposta invece Port := 0: il sistema operativo assegna una porta effimera libera, il componente inserisce quella porta nel redirect URI che invia, e due copie della tua applicazione sulla stessa macchina non entrano mai in conflitto.

Se il provider pretende un percorso registrato esatto invece del solo host con la porta, imposta LocalServerOptions.RedirectURL sul valore che hai registrato. Quella stringa ha allora la precedenza su quella calcolata. Un percorso fisso implica una porta fissa, quindi registra anche la porta e rinuncia al trucco di Port := 0.

Il listener resta attivo solo mentre il flusso è in attesa. Non viene mai avviato per auth2ClientCredentials, auth2ResourceOwnerPassword o auth2DeviceCode, che non hanno bisogno di alcun 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;

Leggi il token, poi mettilo al lavoro

Dopo che OnAfterAccessToken è scattato, gli stessi valori restano disponibili come proprietà di sola lettura, e il componente può passarli ai tuoi client HTTP e WebSocket senza che tu tocchi un header.

Proprietà e header Bearer automatici

I parametri dell'evento sono comodi, ma non sono l'unica copia. AccessToken, TokenType, CurrentExpiresIn e CurrentRefreshToken conservano gli stessi valori per tutta la vita del componente, così un gestore situato altrove nel tuo codice può leggerli senza doverli passare di funzione in funzione.

RawParams è il corpo JSON intatto restituito dall'endpoint del token. Quando un provider restituisce qualcosa fuori dall'insieme standard, per esempio un id_token per OpenID Connect, estrailo da lì. Il componente non decodifica un ID token al posto tuo.

Per far sì che ogni richiesta porti il token automaticamente, assegna il componente OAuth2 a Authentication.Token.OAuth su TsgcHTTP1Client, TsgcHTTP2Client o TsgcWebSocketClient. Il client invia Authorization: Bearer <token> per conto tuo, usando il token_type restituito dal provider.

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;

Il refresh, così il browser non si apre due volte

Un access token vive pochi minuti. Un refresh token vive settimane o mesi. Conservare il secondo è ciò che trasforma un accesso in una sessione.

Due problemi diversi

All'interno di una singola esecuzione dell'applicazione non c'è nulla da fare. Quando l'endpoint del token restituisce sia un refresh token sia un expires_in, il componente arma un timer interno a circa metà di quella durata e invia grant_type=refresh_token quando scatta, molto prima che l'access token scada. OnAfterRefreshToken scatta con la nuova coppia, e OnErrorRefreshToken scatta se il provider la rifiuta. Lascia stare il parametro Handled di OnAfterAccessToken: impostarlo a True dice al componente che te ne occupi tu, e a quel punto non memorizza il refresh token né arma quel timer.

Tra un riavvio e l'altro il problema è tuo, perché solo tu sai dove è lecito scrivere un segreto sulle macchine dei tuoi utenti. Salva il refresh token, poi al lancio successivo salta del tutto Start e chiama Refresh con il valore memorizzato. Non si apre nessun browser, e l'utente è già autenticato prima che il form principale venga disegnato.

I provider che ruotano i refresh token te ne consegnano uno nuovo a ogni rinnovo, quindi sovrascrivi quello che hai memorizzato a ogni OnAfterRefreshToken. Quando il token memorizzato viene infine rifiutato, ripiega su Start e fai accedere di nuovo l'utente.

Usa Revoke per disconnettere l'utente come si deve, e Introspect per chiedere al provider se un token è ancora valido. Entrambi richiedono il relativo endpoint impostato 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');

Conservare i token senza lasciarli in giro

sgcWebSockets non include deliberatamente alcun archivio di token. Dove sia lecito scrivere una credenziale è una decisione che riguarda i tuoi utenti e il tuo deployment, quindi la libreria ti consegna il token e si ferma lì.

Tieni l'access token solo in memoria

Scade in pochi minuti e il refresh token può sempre generarne un altro. Non c'è motivo di scriverlo su disco, e ci sono tutti i motivi per non farlo.

Cifra il refresh token per singolo utente

Su Windows, DPAPI (CryptProtectData) lega il testo cifrato all'account Windows, quindi un file copiato è inutile su un'altra macchina. macOS ha Keychain, e i desktop Linux moderni hanno Secret Service.

Non distribuire mai un client secret su cui fai affidamento

Tutto ciò che sta dentro un eseguibile distribuito è pubblico. È l'intera premessa di PKCE. Se il tuo provider emette un secret per un client desktop, trattalo come un identificatore, non come una protezione.

Colloca il file come faresti con una credenziale

Nei dati applicativi del singolo utente, non in Program Files, non accanto all'eseguibile, non su un percorso di rete condiviso, e non in un INI in chiaro versionato nel controllo del codice sorgente.

Cancellalo alla disconnessione

Chiama Revoke perché il provider invalidi il token, poi rimuovi la copia memorizzata. Un token revocato lasciato su disco resta comunque un rilievo di audit.

Tieni i segreti fuori dal log

HTTPClientOptions.LogOptions registra il traffico verso l'authorization server. È preziosissimo mentre stai facendo funzionare il flusso, ed è un file pieno di token. Disattivalo prima di distribuire.

Di cosa ha bisogno ciascun provider

Ogni provider OAuth 2.0 chiede lo stesso ristretto insieme di impostazioni: due endpoint, un client id, gli scope e un redirect registrato. Google e Microsoft hanno in più componenti già pronti che compilano gli endpoint e restituiscono un profilo utente.

Google e Microsoft, in una sola chiamata

TsgcHTTP_OAuth2_Client_Google e TsgcHTTP_OAuth2_Client_Microsoft derivano dallo stesso componente base e precompilano gli endpoint. Il loro metodo Authenticate è bloccante: esegue l'intero flusso, attende il giro completo nel browser e restituisce un oggetto dati con Authenticated e uno UserProfile popolato.

È il percorso più breve possibile per sapere "chi è questo utente". TsgcOAuth2_Google_Data.UserProfile porta _Name, Given_Name, Family_Name, Id, Locale e Picture. TsgcOAuth2_Microsoft_Data.UserProfile porta DisplayName, GivenName, Surname, Mail, JobTitle, OfficeLocation e altro ancora. L'Authenticate di Microsoft vuole per primo il tenant id.

Per ogni altro provider, usa il TsgcHTTP_OAuth2_Client base e copia i due URL dalla sua documentazione. Dopo di che non resta nulla di specifico del provider.

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 Componente Grant type Redirect Client secret
Google TsgcHTTP_OAuth2_Client_Google o il client base auth2CodePKCE Loopback, Port := 0 Emesso per i client desktop, impostalo se ne hai uno
Microsoft Entra ID TsgcHTTP_OAuth2_Client_Microsoft o il client base auth2CodePKCE Loopback, registrato come piattaforma mobile / desktop Non usato da un client pubblico, lascialo vuoto
Auth0, Okta, Keycloak, AWS Cognito TsgcHTTP_OAuth2_Client auth2CodePKCE Loopback, registrato sull'applicazione Dipende dal fatto che l'app sia pubblica o confidenziale
Job in background e servizi TsgcHTTP_OAuth2_Client auth2ClientCredentials Nessuno, non è coinvolto alcun browser Obbligatorio, e sicuro, perché non viene distribuito nulla
Chioschi, TV, dispositivi headless TsgcHTTP_OAuth2_Client auth2DeviceCode (RFC 8628) Nessuno, l'utente completa da telefono Di solito non richiesto

Autenticarsi per inviare e-mail: OAuth 2.0 e XOAUTH2

Gmail e Microsoft 365 hanno smesso di accettare le password su SMTP, IMAP e POP. Il sostituto è lo stesso access token che hai appena ottenuto, presentato attraverso il meccanismo SASL XOAUTH2.

Il token arriva da qui, il passo SASL arriva da sgcIndy

Ottenere il token è esattamente il flusso descritto sopra: auth2CodePKCE, un redirect di loopback e uno scope di posta come https://mail.google.com/ in AuthorizationServerOptions.Scope. Nel caso della posta nulla cambia sul lato OAuth2.

Presentarlo è l'altra metà. sgcIndy include TIdSASLXOAUTH2 nella unit IdSASLXOAUTH2. Aggiungilo a TIdSMTP.SASLMechanisms, imposta AuthType := satSASL e fornisci il nome utente e l'access token dal suo evento OnAuthenticate. Lo stesso meccanismo funziona per TIdIMAP4 e TIdPOP3.

Tieni i due componenti distinti nella tua testa: il client OAuth2 sa come ottenere e rinnovare un token, il meccanismo SASL sa come presentarlo. Nessuno dei due ha bisogno di conoscere l'altro.

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;

Ti serve un client, o anche un server?

Tutto quello che precede riguarda il lato client. Ti serve la seconda metà solo se sei tu a emettere i token.

Solo client

Se stai autenticando utenti sull'identity provider di qualcun altro, Google, Microsoft, Auth0, Okta, Keycloak, AWS Cognito, l'SSO della tua azienda, non ti serve altro che TsgcHTTP_OAuth2_Client. Quel componente è compilato nell'edizione Standard e in tutte le edizioni superiori. È disponibile anche da solo nel pacchetto autonomo sgcAuth, con il runtime di cui ha bisogno già incluso.

È il caso più comune, ed è tutto ciò di cui parla questa pagina fino a qui.

Quando ti serve anche un server

Ti serve la metà server solo quando è la tua applicazione a fare da authorization server: emetti tu i client id, ospiti tu la pagina di accesso, generi e revochi tu gli access token di cui la tua API si fida. È TsgcHTTP_OAuth2_Server, collegato a un TsgcWebSocketHTTPServer, ed è un componente Enterprise.

Verifica PKCE per impostazione predefinita. OAuth2Options.PKCE è True fin da subito, quindi un client che invia un challenge deve produrre un verifier corrispondente, e chi non lo fa viene rifiutato. Registra le applicazioni client con Apps.AddApp, autentica gli utenti in OnOAuth2Authentication e ripristina i token dopo un riavvio con AddToken.

Lo stesso livello Enterprise porta TsgcHTTP_JWT_Server per validare i bearer token JWT sui tuoi endpoint e TsgcWSAPIServer_WebAuthn per le passkey. I client corrispondenti, TsgcHTTP_OAuth2_Client e TsgcHTTP_JWT_Client, sono disponibili da Standard in su. Client e server stanno in livelli di edizione diversi, e vale la pena verificarlo prima di impostare un piano su uno dei due.

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;

Cosa va storto di solito la prima volta

Quasi ogni primo tentativo fallito di OAuth2 su desktop rientra in uno di questi sei casi.

redirect_uri_mismatch

L'URI che il componente invia deve coincidere con quello che hai registrato, carattere per carattere, barra finale e porta comprese. Se hai registrato un URI fisso, imposta LocalServerOptions.RedirectURL esattamente su quella stringa invece di affidarti a quella calcolata. Se il provider ammette qualsiasi porta di loopback, usa Port := 0 e registra solo l'host.

Il browser si apre e non torna nulla

Qualcosa sta occupando la porta, oppure una regola del firewall sta bloccando il listener di loopback. Imposta Port := 0 e verifica che un'esecuzione precedente del flusso sia stata chiusa con Stop invece di essere lasciata in ascolto.

invalid_grant nello scambio del token

Gli authorization code sono monouso e di vita breve. Fare debug con un breakpoint tra il redirect e lo scambio fa scadere il code. Leggi il fallimento da OnErrorAccessToken, che ti fornisce error ed error_description del provider stesso, invece di tirare a indovinare.

Non è stato restituito alcun refresh token

I provider ne emettono uno solo se lo chiedi. Google vuole access_type=offline, Microsoft vuole lo scope offline_access. Aggiungi lo scope a AuthorizationServerOptions.Scope, oppure accoda il parametro di query modificando il parametro URL in OnBeforeAuthorizeCode.

TLS non funziona su Linux o su mobile

Lo scambio del token è una POST HTTPS, quindi richiede un back end TLS funzionante. HTTPClientOptions.TLSOptions.IOHandler lo seleziona: iohOpenSSL, iohSChannel su Windows senza DLL da distribuire, oppure i gestori nativi iohAndroidTLS e iohAppleTLS dell'edizione Enterprise.

Volevi la pagina di accesso dentro l'app

Gestisci OnBeforeAuthorizeCode, imposta Handled := True e porta il tuo TsgcWebView2 o TWebBrowser all'URL che ti viene passato. Il listener di loopback continua a intercettare il redirect. Tieni presente che diversi provider oggi rifiutano di mostrare la loro schermata di consenso dentro un browser incorporato.

Domande su OAuth2 e PKCE in Delphi

Le domande che gli sviluppatori cercano davvero prima di iniziare.

Inserisci un TsgcHTTP_OAuth2_Client, imposta OAuth2Options.GrantType := auth2CodePKCE, compila OAuth2Options.ClientId, AuthorizationServerOptions.AuthURL, AuthorizationServerOptions.TokenURL e AuthorizationServerOptions.Scope, imposta LocalServerOptions.IP su 127.0.0.1 e LocalServerOptions.Port su 0, poi chiama Start. Il componente genera i valori PKCE, apre il browser, intercetta il redirect su un listener di loopback, scambia il code e solleva OnAfterAccessToken con il token.
Non serve. Quando GrantType è auth2CodePKCE, TsgcHTTP_OAuth2_Client preleva 32 byte dalla sorgente casuale crittografica della piattaforma, li codifica in Base64URL nel code verifier di 43 caratteri, imposta il code challenge sulla codifica Base64URL dell'hash SHA-256 di quel verifier e fissa code_challenge_method a S256. Il verifier resta privato dentro il componente e viene riproposto allo scambio del token, quindi non compare mai nel redirect. Se vuoi costruire la coppia a mano per un altro scopo, le stesse primitive sono pubbliche: sgcRandomBytes nella unit sgcCrypto_Random, più GetHashSHA256 ed EncodeBase64URL nella unit sgcBase_Helpers.
Un indirizzo di loopback. TsgcHTTP_OAuth2_Client avvia un piccolo listener HTTP su LocalServerOptions.IP e LocalServerOptions.Port solo mentre il flusso è in corso, e il redirect URI che invia è costruito da quei valori. I valori predefiniti sono 127.0.0.1 e la porta 8080. In un'applicazione distribuita imposta Port a 0 così il sistema operativo sceglie una porta effimera libera e due istanze non se la contendono mai. Se il provider richiede un URI registrato esatto, metti quella stringa in LocalServerOptions.RedirectURL e avrà la precedenza sul valore calcolato.
Dipende dal provider. PKCE esiste proprio perché un'applicazione desktop distribuita non può custodire un segreto, quindi un client pubblico di norma non invia alcun secret e lascia vuoto OAuth2Options.ClientSecret. Alcuni provider ne emettono comunque uno per i client desktop e se lo aspettano nella richiesta del token. Impostalo quando è così, ma trattalo come un identificatore più che come una protezione, perché tutto ciò che sta dentro un eseguibile distribuito può essere estratto.
Salva il refresh token, poi al lancio successivo chiama Refresh con quel valore invece di Start. Leggilo dal parametro Refresh_Token di OnAfterAccessToken, oppure più tardi dalla proprietà CurrentRefreshToken. Sovrascrivi la copia memorizzata a ogni OnAfterRefreshToken, perché i provider che ruotano i refresh token invalidano quello vecchio. All'interno di una singola esecuzione non serve fare nulla: il componente arma un timer a partire dal valore expires_in e rinnova l'access token da solo.
Tieni l'access token solo in memoria, scade in pochi minuti e può sempre essere rigenerato. Salva il refresh token cifrato e legato all'utente corrente, per esempio con DPAPI su Windows, Keychain su macOS o Secret Service su Linux, nei dati applicativi del singolo utente e non accanto all'eseguibile. sgcWebSockets non include di proposito un proprio archivio di token: ti consegna il token e lascia a te la decisione su dove conservarlo. Ricorda di disattivare HTTPClientOptions.LogOptions prima di distribuire, perché quel log contiene i token.
Ottieni un access token con il flusso descritto in questa pagina, chiedendo lo scope di posta del provider come https://mail.google.com/, poi presentalo tramite SASL XOAUTH2. sgcIndy include TIdSASLXOAUTH2 nella unit IdSASLXOAUTH2. Aggiungilo a TIdSMTP.SASLMechanisms, imposta AuthType := satSASL e restituisci il nome utente e l'access token dal suo evento OnAuthenticate. Lo stesso meccanismo autentica TIdIMAP4 e TIdPOP3.
Il client OAuth2 e il client JWT sono compilati nell'edizione Standard e in tutte le edizioni superiori, quindi Standard, Professional, Enterprise e All-Access li includono tutte. Il server OAuth2, il server JWT e il server WebAuthn sono componenti Enterprise e non sono presenti nelle build Standard o Professional. I due componenti client sono venduti anche da soli come pacchetto autonomo sgcAuth, con il runtime di cui hanno bisogno già incluso.
Solo se sei tu a emettere i token. Autenticare gli utenti su Google, Microsoft, Auth0, Okta, Keycloak, AWS Cognito o un identity provider aziendale richiede il componente client e nient'altro. Ti serve TsgcHTTP_OAuth2_Server quando è la tua applicazione a registrare i client id, a ospitare la pagina di accesso e a generare i token di cui la tua API si fida. Valida PKCE per impostazione predefinita tramite OAuth2Options.PKCE, registra le applicazioni con Apps.AddApp e si collega a un TsgcWebSocketHTTPServer tramite Authentication.OAuth.OAuth2.
Sì. Gestisci OnBeforeAuthorizeCode, che riceve l'URL di autorizzazione già costruito come parametro var, imposta Handled := True così il componente non avvia il browser di sistema, e porta un controllo incorporato come TsgcWebView2 a quell'URL. Il listener di loopback riceve comunque il redirect e il flusso si conclude normalmente. Tieni presente che diversi provider oggi bloccano la loro schermata di consenso nei browser incorporati, ed è per questo che il browser di sistema è l'impostazione predefinita.
Sì. TsgcHTTP_OAuth2_Client compila per Windows, macOS, Linux, iOS e Android, in VCL, FireMonkey e Lazarus / FPC, da Delphi 7 a Delphi 13 e nelle versioni corrispondenti di C++ Builder. L'apertura del browser usa quello che la piattaforma mette a disposizione. L'unica scelta specifica per piattaforma è il back end TLS per lo scambio del token, selezionato tramite HTTPClientOptions.TLSOptions.IOHandler.

Riferimento, demo e documentazione

Il riferimento del componente, il progetto demo pronto all'uso e i documenti tecnici che vanno più a fondo di questa pagina.

Guida online, TsgcHTTP_OAuth2_Client Ogni proprietà, metodo ed evento del componente client, con l'argomento Authorization Code + PKCE.
Guida online, Authorization Code con PKCE L'argomento sul grant type: cosa fa PKCE, la tabella di configurazione e la raccomandazione sulla porta casuale.
Progetto demo, Demos\20.HTTP_Protocol\02.OAuth2_Authentication Progetti client e server con preset funzionanti per Gmail, Google Pub/Sub, Azure AD, AWS Cognito, Dropbox e Auth0, più una variante con browser incorporato.
Documento tecnico, OAuth2 Client (PDF) Funzionalità, guida rapida, ogni grant type ed esempi di codice per Delphi, C++ Builder e .NET.
Documento tecnico, OAuth2 Server (PDF) Il componente authorization server dell'edizione Enterprise: endpoint, registrazione delle app, validazione PKCE e ciclo di vita dei token.
Manuale utente (PDF) Manuale completo che copre ogni componente della libreria.

Specifiche implementate da questo flusso

Fonti primarie, quando devi chiudere una discussione con l'assistenza di un provider.

Componenti e articoli dietro questa pagina

Le pagine dei componenti riportano l'elenco completo delle funzionalità, gli articoli coprono i casi che questa pagina si limita a sfiorare.

Componente OAuth2 Client

L'intera superficie di proprietà, metodi ed eventi di TsgcHTTP_OAuth2_Client, Device Code e DPoP compresi.

Scopri di più →

Componente OAuth2 Server

L'authorization server dell'edizione Enterprise: i tuoi endpoint authorize, token, revoke e introspect.

Scopri di più →

sgcAuth

I componenti client OAuth2 e JWT come pacchetto autonomo, con il runtime di cui hanno bisogno già incluso.

Scopri di più →

Componente JWT Client

Firma e allega i JSON Web Token, da soli o come sorgente Bearer per i tuoi client HTTP e WebSocket.

Scopri di più →

Delphi PKCE OAuth2

L'articolo di rilascio originale che introduce il supporto PKCE sia sul componente client sia su quello server.

Leggi il post →

sgcIndy XOAuth2

Inviare posta con un access token OAuth 2.0 su SMTP, IMAP e POP attraverso il meccanismo SASL XOAUTH2.

Leggi il post →

OAuth2 Client Credentials

La variante senza utente, per servizi in background e accesso alle API da macchina a macchina.

Leggi il post →

OAuth2 DPoP in Delphi

Legare un access token a una coppia di chiavi, per i provider che richiedono la prova di possesso secondo RFC 9449.

Leggi il post →

AWS Cognito e OAuth2

Una configurazione completa su un identity provider reale, endpoint per endpoint.

Leggi il post →

OAuth2 Server: registrare le app

Registrare applicazioni client, redirect URI e grant type consentiti sul tuo authorization server.

Leggi il post →

Autorizzazione con provider esterni

Lasciare che il tuo server deleghi l'accesso a Google, Microsoft o a qualsiasi altro identity provider esterno.

Leggi il post →

WebAuthn e passkey

L'alternativa senza password, quando preferisci non avere alcun passaggio di token.

Scopri di più →

Questa pagina è uno dei casi d'uso Delphi, ognuno dei quali affronta un singolo compito dall'inizio alla fine. Gli altri finora sono chiamare un LLM da Delphi e collegare due applicazioni peer to peer con WebRTC.

Autentica oggi il tuo primo utente

Scarica la versione di prova gratuita, apri la demo OAuth2, puntala al tuo provider e osserva il giro completo nel browser andare a buon fine.