Authentifie un utilisateur dans une application Delphi avec OAuth2 et PKCE

Un composant, un type d'autorisation, un passage par le navigateur. Cette page t'emmène d'une fiche vide à un utilisateur connecté avec un jeton d'accès actif, en utilisant le flux Authorization Code avec PKCE (RFC 7636), celui que tous les fournisseurs attendent désormais d'une application de bureau native.

TsgcHTTP_OAuth2_Client
Code verifier et challenge générés pour toi
Delphi 7 à 13, C++ Builder, VCL et FireMonkey

Ce qu'il te faut pour connecter un utilisateur

Un seul composant non visuel dialogue avec le fournisseur. Tu n'as besoin ni d'un serveur web, ni d'un navigateur embarqué, ni d'un framework REST.

Composant

TsgcHTTP_OAuth2_Client, déclaré dans l'unité sgcHTTP et créé par code, comme le fait chaque démo.

Type d'autorisation

OAuth2Options.GrantType := auth2CodePKCE. Cette simple affectation active PKCE.

Édition

Standard, Professional, Enterprise et All-Access. Le client n'est pas une fonctionnalité Enterprise, le serveur l'est.

Plateformes

Windows, macOS, Linux, iOS et Android. Le composant ouvre le navigateur que la plateforme fournit.

Ce que fait réellement le flux PKCE

PKCE existe parce qu'une application de bureau ne peut pas garder un secret. Il remplace le secret par une valeur dont le client prouve qu'il la connaissait avant le début du flux.

1. Générer un code verifier

Une chaîne aléatoire à forte entropie. sgcWebSockets demande 32 octets au CSPRNG de la plateforme et les encode en Base64URL, ce qui produit le verifier de 43 caractères exigé par la RFC 7636.

2. Dériver le code challenge

Le SHA-256 du verifier, encodé en Base64URL. C'est le challenge qui voyage dans la requête d'autorisation, si bien qu'une oreille indiscrète sur la redirection ne voit jamais le verifier.

3. Ouvrir le navigateur

Le composant construit l'URL d'autorisation avec client_id, redirect_uri, scope, state, code_challenge et code_challenge_method=S256, puis lance le navigateur du système.

4. L'utilisateur se connecte

Le consentement se passe dans le navigateur, sur le domaine du fournisseur, avec la session existante de l'utilisateur, son gestionnaire de mots de passe et son appareil de second facteur. Ton application ne voit jamais le mot de passe.

5. La redirection revient

Le fournisseur redirige vers ton redirect_uri en transportant code et state. Sur le poste de travail cette URI est une adresse loopback, et le composant est déjà à l'écoute dessus.

6. Échanger le code

Le composant envoie en POST le code accompagné du code_verifier d'origine vers l'endpoint de jeton. Le fournisseur recalcule le SHA-256 et compare. S'il correspond, tu obtiens un jeton d'accès.

Pourquoi le verifier compte

Un code d'autorisation est une valeur au porteur pendant les quelques secondes où il vit. Tout ce qui peut observer la redirection, une application malveillante enregistrée sur le même schéma d'URI personnalisé, un proxy, un journal partagé, peut le voler. Sans PKCE, ce code volé suffit à fabriquer un jeton.

Avec PKCE, l'endpoint de jeton refuse le code si l'appelant ne présente pas aussi le verifier dont le hash SHA-256 correspond au challenge envoyé au départ. L'attaquant n'a vu que le hash, le code volé ne vaut donc rien.

Rien de tout cela n'est à ta charge. Mets GrantType à auth2CodePKCE et le composant exécute les étapes 1, 2, 3, 5 et 6 pour toi. Ce qui suit est le code qui le pilote, et les deux décisions qui te reviennent vraiment : l'URI de redirection et l'endroit où vit le refresh token.

sur le fil
# 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

Connecter un utilisateur en une vingtaine de lignes

Crée le composant, choisis auth2CodePKCE, pointe-le vers les deux endpoints du fournisseur, relie OnAfterAccessToken, appelle Start. Le navigateur s'ouvre, l'utilisateur consent, l'événement se déclenche avec le jeton.

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

Où va la redirection dans une application de bureau

C'est la partie qui n'a pas de réponse évidente quand on vient d'OAuth2 côté web, et celle que la plupart des premières tentatives ratent.

Loopback, pas une URL publique

Une application de bureau n'a aucun domaine vers lequel rediriger. La réponse admise, et celle que ce composant implémente, est une redirection loopback : l'application démarre un minuscule listener HTTP sur 127.0.0.1, enregistre cette adresse comme URI de redirection, et arrête le listener dès que le code arrive.

LocalServerOptions.IP vaut par défaut 127.0.0.1 et LocalServerOptions.Port vaut par défaut 8080. Pour une application de bureau distribuée, mets plutôt Port := 0  : le système d'exploitation attribue un port éphémère libre, le composant place ce port dans l'URI de redirection qu'il envoie, et deux copies de ton application sur la même machine n'entrent jamais en collision.

Si le fournisseur exige un chemin enregistré exact plutôt qu'un simple hôte et port, mets LocalServerOptions.RedirectURL à la valeur que tu as enregistrée. Cette chaîne remplace alors celle qui est calculée. Un chemin fixe implique un port fixe, enregistre donc aussi le port et abandonne l'astuce Port := 0.

Le listener n'est actif que pendant l'attente du flux. Il n'est jamais démarré pour auth2ClientCredentials, auth2ResourceOwnerPassword ou auth2DeviceCode, qui n'ont besoin d'aucune redirection.

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;

Lis le jeton, puis mets-le au travail

Après le déclenchement de OnAfterAccessToken, les mêmes valeurs restent disponibles sous forme de propriétés en lecture seule, et le composant peut les transmettre à tes clients HTTP et WebSocket sans que tu touches à un seul en-tête.

Propriétés, et en-têtes Bearer automatiques

Les paramètres de l'événement sont pratiques, mais ils ne sont pas la seule copie. AccessToken, TokenType, CurrentExpiresIn et CurrentRefreshToken conservent les mêmes valeurs tant que le composant existe, si bien qu'un gestionnaire situé ailleurs dans ton code peut les lire sans avoir à les faire circuler.

RawParams est le corps JSON brut renvoyé par l'endpoint de jeton. Quand un fournisseur renvoie quelque chose en dehors du jeu standard, un id_token pour OpenID Connect par exemple, extrais-le de là. Le composant ne décode pas un ID token pour toi.

Pour que chaque requête transporte le jeton automatiquement, affecte le composant OAuth2 à Authentication.Token.OAuth sur TsgcHTTP1Client, TsgcHTTP2Client ou TsgcWebSocketClient. Le client envoie Authorization: Bearer <token> pour toi, en utilisant le token_type que le fournisseur a renvoyé.

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;

Le renouvellement, pour que le navigateur ne s'ouvre jamais deux fois

Un jeton d'accès vit quelques minutes. Un refresh token vit des semaines ou des mois. Conserver le second, c'est ce qui transforme une connexion en session.

Deux problèmes distincts

À l'intérieur d'une même exécution de l'application il n'y a rien à faire. Quand l'endpoint de jeton renvoie à la fois un refresh token et un expires_in, le composant arme un minuteur interne à environ la moitié de cette durée de vie et envoie grant_type=refresh_token quand il se déclenche, bien avant la mort du jeton d'accès. OnAfterRefreshToken se déclenche avec la nouvelle paire, et OnErrorRefreshToken se déclenche si le fournisseur la rejette. Laisse le paramètre Handled de OnAfterAccessToken tranquille : le mettre à True indique au composant que tu prends la main, et il cesse alors aussi bien de stocker le refresh token que d'armer ce minuteur.

D'une exécution à l'autre est ton problème, parce que toi seul sais où un secret peut être écrit sur les machines de tes utilisateurs. Conserve le refresh token, puis au lancement suivant saute complètement Start et appelle Refresh avec la valeur stockée. Aucun navigateur ne s'ouvre, et l'utilisateur est connecté avant que ta fiche principale ne soit dessinée.

Les fournisseurs qui font tourner les refresh tokens t'en donnent un nouveau à chaque renouvellement, écrase donc ce que tu as stocké à chaque OnAfterRefreshToken. Quand le jeton stocké finit par être rejeté, replie-toi sur Start et laisse l'utilisateur se reconnecter.

Utilise Revoke pour déconnecter proprement l'utilisateur, et Introspect pour demander au fournisseur si un jeton est encore actif. Les deux ont besoin de l'endpoint correspondant défini dans 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');

Stocker les jetons sans les laisser traîner

sgcWebSockets ne livre délibérément aucun coffre à jetons. L'endroit où un identifiant sensible peut être écrit est une décision qui dépend de tes utilisateurs et de ton déploiement, la bibliothèque te remet donc le jeton et s'arrête là.

Garde le jeton d'accès en mémoire uniquement

Il expire en quelques minutes et le refresh token peut toujours en fabriquer un autre. Il n'y a aucune raison de l'écrire sur le disque, et toutes les raisons de ne pas le faire.

Chiffre le refresh token par utilisateur

Sur Windows, DPAPI (CryptProtectData) lie le chiffré au compte Windows, si bien qu'un fichier copié est inutilisable sur une autre machine. macOS dispose du Keychain, et les bureaux Linux modernes du Secret Service.

Ne distribue jamais un client secret dont tu dépendrais

Tout ce qui se trouve dans un exécutable distribué est public. C'est toute la prémisse de PKCE. Si ton fournisseur émet un secret pour un client de bureau, traite-le comme un identifiant, pas comme une protection.

Traite le fichier comme un identifiant sensible

Dans les données applicatives propres à l'utilisateur, pas dans Program Files, pas à côté de l'exécutable, pas sur un chemin réseau partagé, et pas dans un simple INI commité dans le gestionnaire de sources.

Supprime à la déconnexion

Appelle Revoke pour que le fournisseur invalide le jeton, puis retire la copie stockée. Un jeton révoqué laissé sur le disque reste un constat d'audit.

Garde les secrets hors des journaux

HTTPClientOptions.LogOptions écrit le trafic vers le serveur d'autorisation. C'est précieux pendant que tu fais fonctionner le flux, et c'est un fichier plein de jetons. Désactive-le avant de distribuer.

Ce dont chaque fournisseur a besoin

Tous les fournisseurs OAuth 2.0 demandent la même poignée de réglages : deux endpoints, un identifiant client, les scopes et une redirection enregistrée. Google et Microsoft ont en plus des composants prêts à l'emploi qui remplissent les endpoints et te renvoient un profil utilisateur.

Google et Microsoft, en un seul appel

TsgcHTTP_OAuth2_Client_Google et TsgcHTTP_OAuth2_Client_Microsoft descendent du même composant de base et pré-remplissent les endpoints. Leur méthode Authenticate est bloquante : elle déroule tout le flux, attend l'aller-retour par le navigateur, et renvoie un objet de données avec Authenticated et un UserProfile.

C'est le chemin le plus court possible pour répondre à « qui est cet utilisateur ». TsgcOAuth2_Google_Data.UserProfile transporte _Name, Given_Name, Family_Name, Id, Locale et Picture. TsgcOAuth2_Microsoft_Data.UserProfile transporte DisplayName, GivenName, Surname, Mail, JobTitle, OfficeLocation et bien d'autres. Le Authenticate de Microsoft prend l'identifiant de tenant en premier.

Pour tous les autres fournisseurs, utilise le TsgcHTTP_OAuth2_Client de base et copie les deux URL depuis leur documentation. Il ne reste rien de spécifique au fournisseur après cela.

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;
Fournisseur Composant Type d'autorisation Redirection Client secret
Google TsgcHTTP_OAuth2_Client_Google ou le client de base auth2CodePKCE Loopback, Port := 0 Émis pour les clients de bureau, renseigne-le si tu en as un
Microsoft Entra ID TsgcHTTP_OAuth2_Client_Microsoft ou le client de base auth2CodePKCE Loopback, enregistrée comme plateforme mobile / bureau Inutilisé par un client public, laisse-le vide
Auth0, Okta, Keycloak, AWS Cognito TsgcHTTP_OAuth2_Client auth2CodePKCE Loopback, enregistrée sur l'application Dépend du caractère public ou confidentiel de l'application
Traitements de fond et services TsgcHTTP_OAuth2_Client auth2ClientCredentials Aucune, aucun navigateur n'intervient Obligatoire, et sans risque, puisque rien n'est distribué
Bornes, téléviseurs, machines sans écran TsgcHTTP_OAuth2_Client auth2DeviceCode (RFC 8628) Aucune, l'utilisateur termine sur un téléphone Généralement pas nécessaire

Se connecter pour envoyer du courrier : OAuth 2.0 et XOAUTH2

Gmail et Microsoft 365 n'acceptent plus les mots de passe sur SMTP, IMAP et POP. Le remplaçant est le même jeton d'accès que tu viens d'obtenir, présenté par le mécanisme SASL XOAUTH2.

Le jeton vient d'ici, l'étape SASL vient de sgcIndy

Obtenir le jeton, c'est exactement le flux ci-dessus : auth2CodePKCE, une redirection loopback, et un scope de messagerie tel que https://mail.google.com/ dans AuthorizationServerOptions.Scope. Rien dans le cas de la messagerie ne change le côté OAuth2.

Le présenter est l'autre moitié. sgcIndy livre TIdSASLXOAUTH2 dans l'unité IdSASLXOAUTH2. Ajoute-le à TIdSMTP.SASLMechanisms, mets AuthType := satSASL, et fournis le nom d'utilisateur et le jeton d'accès depuis son événement OnAuthenticate. Le même mécanisme fonctionne pour TIdIMAP4 et TIdPOP3.

Garde les deux composants distincts dans ton esprit : le client OAuth2 sait obtenir et renouveler un jeton, le mécanisme SASL sait en présenter un. Aucun des deux n'a besoin de connaître l'autre.

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;

Te faut-il un client, ou aussi un serveur ?

Tout ce qui précède se passe côté client. Tu n'as besoin de la seconde moitié que si c'est toi qui émets les jetons.

Client seul

Si tu connectes des utilisateurs au fournisseur d'identité de quelqu'un d'autre, Google, Microsoft, Auth0, Okta, Keycloak, AWS Cognito, le SSO de ton entreprise, tu n'as besoin de rien d'autre que de TsgcHTTP_OAuth2_Client. Ce composant est compilé dans l'édition Standard et dans toutes les éditions au-dessus. Il est aussi disponible seul dans le package autonome sgcAuth, livré avec le runtime dont il a besoin.

C'est le cas courant, et c'est tout le contenu de cette page jusqu'ici.

Quand il te faut aussi un serveur

Tu n'as besoin de la moitié serveur que lorsque ta propre application est le serveur d'autorisation : c'est toi qui émets les identifiants clients, qui héberges la page de connexion, qui fabriques et révoques les jetons d'accès auxquels ton API fait ensuite confiance. C'est TsgcHTTP_OAuth2_Server, rattaché à un TsgcWebSocketHTTPServer, et c'est un composant Enterprise.

Il vérifie PKCE par défaut. OAuth2Options.PKCE vaut True dès le départ, si bien qu'un client qui envoie un challenge doit produire un verifier correspondant, et celui qui n'en produit pas est refusé. Enregistre les applications clientes avec Apps.AddApp, authentifie les utilisateurs dans OnOAuth2Authentication, et restaure les jetons après un redémarrage avec AddToken.

Le même niveau Enterprise apporte TsgcHTTP_JWT_Server pour valider les jetons bearer JWT sur tes endpoints et TsgcWSAPIServer_WebAuthn pour les passkeys. Les clients, TsgcHTTP_OAuth2_Client et TsgcHTTP_JWT_Clientcorrespondants sont disponibles à partir de Standard. Client et serveur se situent dans des niveaux d'édition différents, ce qui mérite une vérification avant de bâtir un plan autour de l'un ou de l'autre.

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;

Ce qui coince habituellement la première fois

Presque toutes les premières tentatives ratées d'OAuth2 sur le poste de travail se ramènent à l'une de ces cinq causes.

redirect_uri_mismatch

L'URI que le composant envoie doit correspondre à ce que tu as enregistré, caractère par caractère, barre oblique finale et port compris. Si tu as enregistré une URI fixe, mets LocalServerOptions.RedirectURL exactement à cette chaîne plutôt que de te fier à celle qui est calculée. Si le fournisseur accepte n'importe quel port loopback, utilise Port := 0 et n'enregistre que l'hôte.

Le navigateur s'ouvre et rien ne revient

Quelque chose occupe le port, ou une règle de firewall bloque le listener loopback. Mets Port := 0, et vérifie qu'une exécution précédente du flux s'est bien terminée par Stop plutôt que d'être restée à l'écoute.

invalid_grant à l'échange du jeton

Les codes d'autorisation sont à usage unique et de courte durée. Déboguer avec un point d'arrêt entre la redirection et l'échange fera expirer le code. Lis l'échec depuis OnErrorAccessToken, qui te donne les error et error_descriptiondu fournisseur lui-même, plutôt que de deviner.

Aucun refresh token n'a été renvoyé

Les fournisseurs n'en émettent un que si tu le demandes. Google veut access_type=offline, Microsoft veut le scope offline_access. Ajoute le scope à AuthorizationServerOptions.Scope, ou ajoute le paramètre de requête en modifiant le paramètre URL dans OnBeforeAuthorizeCode.

TLS échoue sur Linux ou sur mobile

L'échange du jeton est un POST HTTPS, il lui faut donc un back end TLS fonctionnel. HTTPClientOptions.TLSOptions.IOHandler le sélectionne : iohOpenSSL, iohSChannel sur Windows sans aucune DLL à déployer, ou les gestionnaires natifs iohAndroidTLS et iohAppleTLS de l'édition Enterprise.

Tu voulais la page de connexion dans l'application

Gère OnBeforeAuthorizeCode, mets Handled := True et fais naviguer ton propre TsgcWebView2 ou TWebBrowser vers l' URL qu'on t'a donnée. Le listener loopback attrape toujours la redirection. Note que plusieurs fournisseurs refusent désormais d'afficher leur écran de consentement dans un navigateur embarqué.

Questions sur Delphi, OAuth2 et PKCE

Les questions que les développeurs cherchent vraiment avant de commencer.

Dépose un TsgcHTTP_OAuth2_Client, mets OAuth2Options.GrantType := auth2CodePKCE, renseigne OAuth2Options.ClientId, AuthorizationServerOptions.AuthURL, AuthorizationServerOptions.TokenURL et AuthorizationServerOptions.Scope, mets LocalServerOptions.IP à 127.0.0.1 et LocalServerOptions.Port à 0, puis appelle Start. Le composant génère les valeurs PKCE, ouvre le navigateur, attrape la redirection sur un listener loopback, échange le code et lève OnAfterAccessToken avec le jeton.
Tu n'as pas à le faire. Quand GrantType vaut auth2CodePKCE, TsgcHTTP_OAuth2_Client tire 32 octets de la source aléatoire cryptographique de la plateforme, les encode en Base64URL pour former le code verifier de 43 caractères, définit le code challenge comme l'encodage Base64URL du hash SHA-256 de ce verifier, et fixe code_challenge_method à S256. Le verifier reste privé à l'intérieur du composant et il est rejoué lors de l'échange du jeton, il n'apparaît donc jamais dans la redirection. Si tu veux construire la paire à la main pour un autre usage, les mêmes primitives sont publiques : sgcRandomBytes dans l'unité sgcCrypto_Random, plus GetHashSHA256 et EncodeBase64URL dans l'unité sgcBase_Helpers.
Une adresse loopback. TsgcHTTP_OAuth2_Client démarre un petit listener HTTP sur LocalServerOptions.IP et LocalServerOptions.Port uniquement pendant que le flux est en cours, et l'URI de redirection qu'il envoie est construite à partir de ces valeurs. Les valeurs par défaut sont 127.0.0.1 et le port 8080. Pour une application distribuée, mets Port à 0 pour que le système d'exploitation choisisse un port éphémère libre et que deux instances ne se disputent jamais le même. Si le fournisseur exige une URI enregistrée exacte, place cette chaîne dans LocalServerOptions.RedirectURL et elle remplacera la valeur calculée.
Cela dépend du fournisseur. PKCE existe précisément parce qu'une application de bureau distribuée ne peut pas garder un secret, un client public n'envoie donc normalement aucun secret et laisse OAuth2Options.ClientSecret vide. Certains fournisseurs en émettent quand même un pour les clients de bureau et l'attendent sur la requête de jeton. Renseigne-le quand c'est le cas, mais traite-le comme un identifiant plutôt que comme une protection, car tout ce qui se trouve dans un exécutable distribué peut être extrait.
Conserve le refresh token, puis appelle Refresh avec lui au lancement suivant au lieu de Start. Lis-le depuis le paramètre Refresh_Token de OnAfterAccessToken, ou plus tard depuis la propriété CurrentRefreshToken. Écrase la copie stockée à chaque OnAfterRefreshToken, car les fournisseurs qui font tourner les refresh tokens invalident l'ancien. À l'intérieur d'une même exécution, aucun travail n'est nécessaire : le composant arme un minuteur à partir de la valeur expires_in et renouvelle le jeton d'accès tout seul.
Garde le jeton d'accès en mémoire uniquement, il expire en quelques minutes et peut toujours être refabriqué. Conserve le refresh token chiffré et limité à l'utilisateur courant, par exemple avec DPAPI sur Windows, le Keychain sur macOS ou le Secret Service sur Linux, dans les données applicatives propres à l'utilisateur plutôt qu'à côté de l'exécutable. sgcWebSockets ne livre volontairement aucun coffre à jetons : il te remet le jeton et te laisse la décision du stockage. Pense à désactiver HTTPClientOptions.LogOptions avant de distribuer, car ce journal contient les jetons.
Obtiens un jeton d'accès avec le flux de cette page, en demandant le scope de messagerie du fournisseur tel que https://mail.google.com/, puis présente-le par SASL XOAUTH2. sgcIndy livre TIdSASLXOAUTH2 dans l'unité IdSASLXOAUTH2. Ajoute-le à TIdSMTP.SASLMechanisms, mets AuthType := satSASL, et renvoie le nom d'utilisateur et le jeton d'accès depuis son événement OnAuthenticate. Le même mécanisme authentifie TIdIMAP4 et TIdPOP3.
Le client OAuth2 et le client JWT sont compilés dans l'édition Standard et dans toutes les éditions au-dessus, donc Standard, Professional, Enterprise et All-Access les incluent tous. Le serveur OAuth2, le serveur JWT et le serveur WebAuthn sont des composants Enterprise et ne sont pas présents dans les builds Standard ou Professional. Les deux composants clients sont aussi vendus séparément dans le package autonome sgcAuth, livré avec le runtime dont ils ont besoin.
Uniquement si c'est toi qui émets les jetons. Connecter des utilisateurs à Google, Microsoft, Auth0, Okta, Keycloak, AWS Cognito ou à un fournisseur d'identité d'entreprise ne demande que le composant client, et rien d'autre. Tu as besoin de TsgcHTTP_OAuth2_Server quand ta propre application enregistre des identifiants clients, héberge la page de connexion et fabrique les jetons auxquels ton API fait confiance. Il valide PKCE par défaut via OAuth2Options.PKCE, enregistre les applications avec Apps.AddApp, et se rattache à un TsgcWebSocketHTTPServer via Authentication.OAuth.OAuth2.
Oui. Gère OnBeforeAuthorizeCode, qui reçoit l'URL d'autorisation entièrement construite dans un paramètre var, mets Handled := True pour que le composant ne lance pas le navigateur du système, et fais naviguer un contrôle embarqué tel que TsgcWebView2 vers cette URL. Le listener loopback reçoit toujours la redirection et le flux se termine normalement. Sache que plusieurs fournisseurs bloquent désormais leur écran de consentement dans les navigateurs embarqués, ce qui explique que le navigateur du système soit la valeur par défaut.
Oui. TsgcHTTP_OAuth2_Client compile pour Windows, macOS, Linux, iOS et Android, en VCL, FireMonkey et Lazarus / FPC, de Delphi 7 à Delphi 13 et pour les versions correspondantes de C++ Builder. L'ouverture du navigateur utilise ce que la plateforme fournit. Le seul choix spécifique à la plateforme est le back end TLS pour l'échange du jeton, sélectionné par HTTPClientOptions.TLSOptions.IOHandler.

Référence, démo et documentation

La référence du composant, le projet de démonstration prêt à l'emploi, et les documents techniques qui vont plus loin que cette page.

Aide en ligne, TsgcHTTP_OAuth2_Client Toutes les propriétés, méthodes et événements du composant client, avec la rubrique Authorization Code + PKCE.
Aide en ligne, Authorization Code avec PKCE La rubrique du type d'autorisation : ce que fait PKCE, le tableau de configuration et la recommandation de port aléatoire.
Projet de démonstration, Demos\20.HTTP_Protocol\02.OAuth2_Authentication Des projets client et serveur avec des configurations fonctionnelles pour Gmail, Google Pub/Sub, Azure AD, AWS Cognito, Dropbox et Auth0, plus une variante à navigateur embarqué.
Document technique, OAuth2 Client (PDF) Fonctionnalités, démarrage rapide, tous les types d'autorisation et des exemples de code pour Delphi, C++ Builder et .NET.
Document technique, OAuth2 Server (PDF) Le composant serveur d'autorisation Enterprise : endpoints, enregistrement d'applications, validation PKCE et cycle de vie des jetons.
Manuel utilisateur (PDF) Manuel complet couvrant tous les composants de la bibliothèque.

Les spécifications que ce flux implémente

Les sources primaires, quand il faut trancher une discussion avec le support d'un fournisseur.

Les composants et les articles derrière cette page

Les pages de composants portent la liste complète des fonctionnalités, les articles couvrent les cas que cette page ne fait qu'effleurer.

Composant OAuth2 Client

Toute la surface de propriétés, méthodes et événements de TsgcHTTP_OAuth2_Client, y compris Device Code et DPoP.

En savoir plus →

Composant OAuth2 Server

Le serveur d'autorisation Enterprise : tes propres endpoints authorize, token, revoke et introspect.

En savoir plus →

sgcAuth

Les composants clients OAuth2 et JWT sous forme de package autonome, avec le runtime dont ils ont besoin intégré.

En savoir plus →

Composant JWT Client

Signe et attache des JSON Web Tokens, seuls ou comme source Bearer pour tes clients HTTP et WebSocket.

En savoir plus →

Delphi PKCE OAuth2

L'article de sortie d'origine qui présente la prise en charge de PKCE sur les composants client et serveur.

Lire l'article →

sgcIndy XOAUTH2

Envoyer du courrier avec un jeton d'accès OAuth 2.0 en SMTP, IMAP et POP par le mécanisme SASL XOAUTH2.

Lire l'article →

OAuth2 Client Credentials

La variante sans utilisateur, pour les services de fond et l'accès API de machine à machine.

Lire l'article →

OAuth2 DPoP en Delphi

Lier un jeton d'accès à une paire de clés, pour les fournisseurs qui exigent une preuve de possession selon la RFC 9449.

Lire l'article →

AWS Cognito et OAuth2

Une configuration complète face à un vrai fournisseur d'identité, endpoint par endpoint.

Lire l'article →

OAuth2 Server : enregistrer des applications

Enregistrer les applications clientes, les URI de redirection et les types d'autorisation autorisés sur ton propre serveur d'autorisation.

Lire l'article →

Autorisation avec des fournisseurs externes

Laisser ton propre serveur déléguer la connexion à Google, Microsoft ou tout autre fournisseur d'identité externe.

Lire l'article →

WebAuthn et passkeys

L'alternative sans mot de passe, quand tu préfères n'avoir aucun passage de jeton du tout.

En savoir plus →

Cette page fait partie des cas d'usage Delphi, qui prennent chacun une seule tâche de bout en bout. Les autres, pour l'instant, sont appeler un LLM depuis Delphi et connecter deux applications en pair à pair avec WebRTC.

Connecte ton premier utilisateur dès aujourd'hui

Télécharge l'essai gratuit, ouvre la démo OAuth2, pointe-la vers ton fournisseur et regarde l'aller-retour par le navigateur se terminer.