Inicia la sesión de un usuario en una aplicación Delphi con OAuth2 y PKCE

Un componente, un tipo de concesión, un traspaso al navegador. Esta página te lleva de un formulario vacío a un usuario con la sesión iniciada y un access token vivo, usando el flujo Authorization Code con PKCE (RFC 7636), el que hoy espera cualquier proveedor de una aplicación de escritorio nativa.

TsgcHTTP_OAuth2_Client
Code verifier y challenge generados por ti
Delphi 7 a 13, C++ Builder, VCL y FireMonkey

Qué necesitas para iniciar la sesión de un usuario

Un único componente no visual habla con el proveedor. No necesitas un servidor web, ni un navegador embebido, ni un framework REST.

Componente

TsgcHTTP_OAuth2_Client, declarado en la unidad sgcHTTP y creado por código, como lo hace cada demo.

Tipo de concesión

OAuth2Options.GrantType := auth2CodePKCE. Esa única asignación activa PKCE.

Edición

Standard, Professional, Enterprise y All-Access. El cliente no es una característica Enterprise, el servidor sí lo es.

Plataformas

Windows, macOS, Linux, iOS y Android. El componente abre el navegador que ofrezca la plataforma.

Qué hace en realidad el flujo PKCE

PKCE existe porque una aplicación de escritorio no puede guardar un secreto. Sustituye el secreto por un valor que el cliente demuestra que conocía antes de empezar el flujo.

1. Genera un code verifier

Una cadena aleatoria de alta entropía. sgcWebSockets pide 32 bytes al CSPRNG de la plataforma y los codifica en Base64URL, lo que produce el verifier de 43 caracteres que pide RFC 7636.

2. Deriva el code challenge

SHA-256 del verifier, codificado en Base64URL. El challenge es lo que viaja en la petición de autorización, así que quien escuche la redirección nunca ve el verifier.

3. Abre el navegador

El componente construye la URL de autorización con client_id, redirect_uri, scope, state, code_challenge y code_challenge_method=S256, y después lanza el navegador del sistema.

4. El usuario inicia sesión

El consentimiento ocurre en el navegador, en el dominio del propio proveedor, con la sesión que la persona ya tiene, su gestor de contraseñas y su dispositivo de doble factor. Tu aplicación nunca ve la contraseña.

5. Vuelve la redirección

El proveedor redirige a tu redirect_uri llevando code y state. En escritorio ese URI es una dirección de loopback, y el componente ya está escuchando en ella.

6. Intercambia el código

El componente hace un POST del código junto al code_verifier original al endpoint de token. El proveedor recalcula SHA-256 y compara. Si coincide, obtienes un access token.

Por qué importa el verifier

Un código de autorización es un valor al portador durante los pocos segundos que vive. Cualquier cosa capaz de observar la redirección, una aplicación maliciosa registrada en el mismo esquema de URI personalizado, un proxy, un log compartido, puede robarlo. Sin PKCE, ese código robado basta para acuñar un token.

Con PKCE, el endpoint de token rechaza el código salvo que quien llama presente además el verifier cuyo hash SHA-256 coincide con el challenge enviado al principio. El atacante solo vio el hash, así que el código robado no sirve de nada.

Aquí no hay nada que tengas que escribir tú. Asigna GrantType a auth2CodePKCE y el componente ejecuta por ti los pasos 1, 2, 3, 5 y 6. Lo que sigue es el código que lo pone en marcha, y las dos decisiones que sí te tocan: el URI de redirección y dónde vive el refresh token.

en el cable
# 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

Inicia la sesión de un usuario, en unas veinte líneas

Crea el componente, elige auth2CodePKCE, apúntalo a los dos endpoints del proveedor, engancha OnAfterAccessToken y llama a Start. Se abre el navegador, la persona da su consentimiento, el evento se dispara con el 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");
}

Adónde va la redirección en una aplicación de escritorio

Esta es la parte que no tiene respuesta obvia cuando vienes de OAuth2 en web, y la parte que la mayoría de los primeros intentos hace mal.

Loopback, no una URL pública

Una aplicación de escritorio no tiene dominio al que redirigir. La respuesta aceptada, y la que implementa este componente, es una redirección por loopback: la aplicación arranca un pequeño listener HTTP en 127.0.0.1, registra esa dirección como URI de redirección y apaga el listener en cuanto llega el código.

LocalServerOptions.IP vale por defecto 127.0.0.1 y LocalServerOptions.Port vale por defecto 8080. Para una aplicación de escritorio que vas a distribuir, asigna Port := 0 en su lugar: el sistema operativo entrega un puerto efímero libre, el componente pone ese puerto en el URI de redirección que envía, y dos copias de tu aplicación en la misma máquina nunca chocan.

Si el proveedor insiste en una ruta registrada exacta y no en un host y un puerto a secas, asigna LocalServerOptions.RedirectURL al valor que registraste. Esa cadena sustituye entonces a la calculada. Una ruta fija implica un puerto fijo, así que registra también el puerto y olvídate del truco de Port := 0.

El listener solo está levantado mientras el flujo espera. Nunca se arranca para auth2ClientCredentials, auth2ResourceOwnerPassword o auth2DeviceCode, que no necesitan ninguna redirección.

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;

Lee el token, y luego ponlo a trabajar

Después de que se dispare OnAfterAccessToken, los mismos valores siguen disponibles como propiedades de solo lectura, y el componente puede pasárselos a tus clientes HTTP y WebSocket sin que toques ninguna cabecera.

Propiedades, y cabeceras Bearer automáticas

Los parámetros del evento son cómodos, pero no son la única copia. AccessToken, TokenType, CurrentExpiresIn y CurrentRefreshToken guardan los mismos valores mientras viva el componente, así que un manejador de otra parte de tu código puede leerlos sin que se los pases de mano en mano.

RawParams es el cuerpo JSON intacto que devolvió el endpoint de token. Cuando un proveedor devuelve algo fuera del conjunto estándar, un id_token para OpenID Connect por ejemplo, sácalo de ahí. El componente no decodifica un ID token por ti.

Para que cada petición lleve el token automáticamente, asigna el componente OAuth2 a Authentication.Token.OAuth en TsgcHTTP1Client, TsgcHTTP2Client o TsgcWebSocketClient. El cliente envía Authorization: Bearer <token> en tu nombre, usando el token_type que haya devuelto el proveedor.

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;

Refrescar, para que el navegador no se abra dos veces

Un access token vive minutos. Un refresh token vive semanas o meses. Conservar el segundo es lo que convierte un inicio de sesión en una sesión.

Dos problemas distintos

Dentro de una misma ejecución de la aplicación no hay nada que hacer. Cuando el endpoint de token devuelve tanto un refresh token como un expires_in, el componente arma un temporizador interno a aproximadamente la mitad de esa vida útil y hace un POST de grant_type=refresh_token cuando salta, bastante antes de que el access token muera. OnAfterRefreshToken se dispara con el nuevo par, y OnErrorRefreshToken se dispara si el proveedor lo rechaza. Deja en paz el parámetro Handled de OnAfterAccessToken: ponerlo a True le dice al componente que tomas tú el control, y entonces ni guarda el refresh token ni arma ese temporizador.

Entre reinicios es cosa tuya, porque solo tú sabes dónde se puede escribir un secreto en las máquinas de tus usuarios. Persiste el refresh token y, en el siguiente arranque, sáltate Start por completo y llama a Refresh con el valor guardado. No se abre ningún navegador, y la sesión está iniciada antes de que se pinte tu formulario principal.

Los proveedores que rotan los refresh tokens te entregan uno nuevo con cada renovación, así que sobrescribe lo que guardaste en cada OnAfterRefreshToken. Cuando el token guardado acabe siendo rechazado, recurre a Start y deja que la persona vuelva a iniciar sesión.

Usa Revoke para cerrar la sesión como es debido, y Introspect para preguntar al proveedor si un token sigue vivo. Los dos necesitan que el endpoint correspondiente esté configurado en 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');

Guardar tokens sin dejarlos tirados por ahí

sgcWebSockets no incluye a propósito ninguna caja fuerte de tokens. Dónde se puede escribir una credencial es una decisión sobre tus usuarios y tu despliegue, así que la biblioteca te entrega el token y se detiene ahí.

Mantén el access token solo en memoria

Caduca en minutos y el refresh token siempre puede acuñar otro. No hay ninguna razón para escribirlo en disco, y todas las razones para no hacerlo.

Cifra el refresh token por usuario

En Windows, DPAPI (CryptProtectData) ata el texto cifrado a la cuenta de Windows, así que un archivo copiado no sirve de nada en otra máquina. macOS tiene Keychain, y los escritorios Linux modernos tienen Secret Service.

No distribuyas nunca un client secret del que dependas

Cualquier cosa dentro de un ejecutable distribuido es pública. Esa es la premisa entera de PKCE. Si tu proveedor emite un secreto para un cliente de escritorio, trátalo como un identificador, no como una protección.

Trata el archivo como una credencial

Datos de aplicación por usuario, no Program Files, no al lado del ejecutable, no una ruta de red compartida, y no un INI en claro subido al control de versiones.

Bórralo al cerrar la sesión

Llama a Revoke para que el proveedor invalide el token, y luego elimina la copia guardada. Un token revocado que sigue en disco es igualmente un hallazgo de auditoría.

Mantén los secretos fuera del log

HTTPClientOptions.LogOptions escribe el tráfico con el servidor de autorización. Es valiosísimo mientras estás haciendo funcionar el flujo, y es un archivo lleno de tokens. Apágalo antes de publicar.

Qué necesita cada proveedor

Todo proveedor de OAuth 2.0 pide el mismo puñado de ajustes: dos endpoints, un client id, los scopes y una redirección registrada. Google y Microsoft tienen además componentes ya preparados que rellenan los endpoints y devuelven un perfil de usuario.

Google y Microsoft, en una sola llamada

TsgcHTTP_OAuth2_Client_Google y TsgcHTTP_OAuth2_Client_Microsoft descienden del mismo componente base y rellenan los endpoints de antemano. Su método Authenticate es bloqueante: ejecuta el flujo entero, espera al viaje de ida y vuelta por el navegador, y devuelve un objeto de datos con Authenticated y un UserProfile.

Ese es el camino más corto posible a "quién es esta persona". TsgcOAuth2_Google_Data.UserProfile lleva _Name, Given_Name, Family_Name, Id, Locale y Picture. TsgcOAuth2_Microsoft_Data.UserProfile lleva DisplayName, GivenName, Surname, Mail, JobTitle, OfficeLocation y más. El Authenticate de Microsoft toma primero el id de tenant.

Para cualquier otro proveedor, usa el TsgcHTTP_OAuth2_Client base y copia las dos URL de su documentación. Después de eso no queda nada específico del proveedor.

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;
Proveedor Componente Tipo de concesión Redirección Client secret
Google TsgcHTTP_OAuth2_Client_Google o el cliente base auth2CodePKCE Loopback, Port := 0 Se emite para clientes de escritorio, ponlo si tienes uno
Microsoft Entra ID TsgcHTTP_OAuth2_Client_Microsoft o el cliente base auth2CodePKCE Loopback, registrado como plataforma móvil / de escritorio No lo usa un cliente público, déjalo vacío
Auth0, Okta, Keycloak, AWS Cognito TsgcHTTP_OAuth2_Client auth2CodePKCE Loopback, registrado en la aplicación Depende de si la app es pública o confidencial
Trabajos en segundo plano y servicios TsgcHTTP_OAuth2_Client auth2ClientCredentials Ninguna, no interviene ningún navegador Obligatorio, y seguro, porque no se distribuye nada
Quioscos, televisores, equipos sin pantalla TsgcHTTP_OAuth2_Client auth2DeviceCode (RFC 8628) Ninguna, la persona termina en un teléfono Normalmente no hace falta

Iniciar sesión para enviar correo: OAuth 2.0 y XOAUTH2

Gmail y Microsoft 365 dejaron de aceptar contraseñas sobre SMTP, IMAP y POP. El sustituto es el mismo access token que acabas de obtener, presentado a través del mecanismo SASL XOAUTH2.

El token viene de aquí, el paso SASL viene de sgcIndy

Obtener el token es exactamente el flujo de arriba: auth2CodePKCE, una redirección por loopback, y un scope de correo como https://mail.google.com/ en AuthorizationServerOptions.Scope. Nada del caso del correo cambia el lado de OAuth2.

Presentarlo es la otra mitad. sgcIndy incluye TIdSASLXOAUTH2 en la unidad IdSASLXOAUTH2. Añádelo a TIdSMTP.SASLMechanisms, asigna AuthType := satSASL, y proporciona el nombre de usuario y el access token desde su evento OnAuthenticate. El mismo mecanismo funciona para TIdIMAP4 y TIdPOP3.

Mantén los dos componentes separados en tu cabeza: el cliente OAuth2 sabe cómo obtener y renovar un token, el mecanismo SASL sabe cómo presentarlo. Ninguno necesita saber del otro.

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;

¿Necesitas un cliente, o también un servidor?

Todo lo de arriba es del lado cliente. Solo necesitas la segunda mitad si eres tú quien emite los tokens.

Solo cliente

Si estás iniciando la sesión de tus usuarios en el proveedor de identidad de otro, Google, Microsoft, Auth0, Okta, Keycloak, AWS Cognito, el SSO corporativo propio, no necesitas nada más que TsgcHTTP_OAuth2_Client. Ese componente se compila en la edición Standard y en todas las ediciones por encima de ella. También está disponible por su cuenta en el paquete independiente sgcAuth, con el runtime que necesita ya incluido.

Este es el caso habitual, y es todo lo que cuenta esta página hasta aquí.

Cuándo necesitas además un servidor

Necesitas la mitad de servidor solo cuando tu propia aplicación es el servidor de autorización: tú emites los client ids, tú alojas la página de inicio de sesión, tú acuñas y revocas los access tokens en los que después confía tu API. Eso es TsgcHTTP_OAuth2_Server, enganchado a un TsgcWebSocketHTTPServer, y es un componente Enterprise.

Verifica PKCE por defecto. OAuth2Options.PKCE vale True de fábrica, así que un cliente que envía un challenge tiene que producir un verifier que coincida, y el que no lo hace es rechazado. Registra aplicaciones cliente con Apps.AddApp, autentica usuarios en OnOAuth2Authentication, y restaura tokens entre reinicios con AddToken.

El mismo nivel Enterprise incluye TsgcHTTP_JWT_Server para validar tokens JWT de portador en tus endpoints y TsgcWSAPIServer_WebAuthn para passkeys. Los clientes, TsgcHTTP_OAuth2_Client y TsgcHTTP_JWT_Client, correspondientes son de Standard en adelante. Cliente y servidor están en niveles de edición distintos, algo que conviene comprobar antes de planificar con cualquiera de los dos.

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;

Qué suele salir mal la primera vez

Casi todo primer intento fallido de OAuth2 en escritorio es uno de estos seis.

redirect_uri_mismatch

El URI que envía el componente tiene que coincidir con el que registraste, carácter por carácter, incluidas la barra final y el puerto. Si registraste un URI fijo, asigna LocalServerOptions.RedirectURL exactamente a esa cadena en lugar de confiar en la calculada. Si el proveedor permite cualquier puerto de loopback, usa Port := 0 y registra solo el host.

El navegador se abre y no vuelve nada

Algo está ocupando el puerto, o una regla de firewall está bloqueando el listener de loopback. Asigna Port := 0, y comprueba que una ejecución anterior del flujo terminó con Stop en lugar de quedarse escuchando.

invalid_grant en el intercambio de código

Los códigos de autorización son de un solo uso y de vida corta. Depurar con un breakpoint entre la redirección y el intercambio hará que el código caduque. Lee el fallo de OnErrorAccessToken, que te da el error y la error_descriptiondel propio proveedor, en lugar de adivinar.

No se devolvió ningún refresh token

Los proveedores solo emiten uno cuando lo pides. Google quiere access_type=offline, Microsoft quiere el scope offline_access. Añade el scope a AuthorizationServerOptions.Scope, o añade el parámetro de consulta editando el parámetro URL en OnBeforeAuthorizeCode.

TLS falla en Linux o en móvil

El intercambio de código es un POST HTTPS, así que necesita un back end TLS que funcione. HTTPClientOptions.TLSOptions.IOHandler lo selecciona: iohOpenSSL, iohSChannel en Windows, sin DLL que desplegar, o los manejadores nativos iohAndroidTLS y iohAppleTLS de la edición Enterprise.

Querías la página de inicio de sesión dentro de la app

Maneja OnBeforeAuthorizeCode, asigna Handled := True y navega tu propio TsgcWebView2 o TWebBrowser a la URL que te dieron. El listener de loopback sigue capturando la redirección. Ten en cuenta que varios proveedores ya se niegan a mostrar su pantalla de consentimiento dentro de un navegador embebido.

Preguntas sobre OAuth2 y PKCE en Delphi

Las preguntas que la gente que desarrolla busca de verdad antes de empezar.

Suelta un TsgcHTTP_OAuth2_Client, asigna OAuth2Options.GrantType := auth2CodePKCE, rellena OAuth2Options.ClientId, AuthorizationServerOptions.AuthURL, AuthorizationServerOptions.TokenURL y AuthorizationServerOptions.Scope, pon LocalServerOptions.IP a 127.0.0.1 y LocalServerOptions.Port a 0, y luego llama a Start. El componente genera los valores de PKCE, abre el navegador, captura la redirección en un listener de loopback, intercambia el código y dispara OnAfterAccessToken con el token.
No hace falta que lo hagas. Cuando GrantType vale auth2CodePKCE, TsgcHTTP_OAuth2_Client toma 32 bytes de la fuente criptográfica aleatoria de la plataforma, los codifica en Base64URL para formar el code verifier de 43 caracteres, pone el code challenge a la codificación Base64URL del hash SHA-256 de ese verifier, y fija code_challenge_method a S256. El verifier se guarda en privado dentro del componente y se reenvía en el intercambio de código, así que nunca aparece en la redirección. Si quieres construir el par a mano para otro propósito, las mismas primitivas son públicas: sgcRandomBytes en la unidad sgcCrypto_Random, además de GetHashSHA256 y EncodeBase64URL en la unidad sgcBase_Helpers.
Una dirección de loopback. TsgcHTTP_OAuth2_Client arranca un pequeño listener HTTP en LocalServerOptions.IP y LocalServerOptions.Port solo mientras el flujo está en marcha, y el URI de redirección que envía se construye a partir de esos valores. Los valores por defecto son 127.0.0.1 y el puerto 8080. Para una aplicación que vas a distribuir, asigna Port a 0 para que el sistema operativo elija un puerto efímero libre y dos instancias nunca se peleen por uno. Si el proveedor exige un URI registrado exacto, pon esa cadena en LocalServerOptions.RedirectURL y sustituirá al valor calculado.
Depende del proveedor. PKCE existe justamente porque una aplicación de escritorio distribuida no puede guardar un secreto, así que un cliente público normalmente no envía ningún secreto y deja OAuth2Options.ClientSecret vacío. Algunos proveedores siguen emitiendo uno para clientes de escritorio y lo esperan en la petición de token. Ponlo cuando lo hagan, pero trátalo como un identificador y no como una protección, porque cualquier cosa dentro de un ejecutable distribuido se puede extraer.
Persiste el refresh token, y luego llama a Refresh con él en el siguiente arranque en lugar de a Start. Léelo del parámetro Refresh_Token de OnAfterAccessToken, o después de la propiedad CurrentRefreshToken. Sobrescribe la copia guardada en cada OnAfterRefreshToken, porque los proveedores que rotan los refresh tokens invalidan el anterior. Dentro de una misma ejecución no hace falta nada: el componente arma un temporizador a partir del valor expires_in y renueva el access token por su cuenta.
Mantén el access token solo en memoria, caduca en minutos y siempre se puede acuñar otro. Persiste el refresh token cifrado y limitado al usuario actual, por ejemplo con DPAPI en Windows, Keychain en macOS o Secret Service en Linux, en los datos de aplicación por usuario y no al lado del ejecutable. sgcWebSockets no incluye a propósito ninguna caja fuerte de tokens: te entrega el token y te deja a ti la decisión de almacenamiento. Acuérdate de apagar HTTPClientOptions.LogOptions antes de publicar, porque ese log contiene los tokens.
Obtén un access token con el flujo de esta página, pidiendo el scope de correo del proveedor, como https://mail.google.com/, y luego preséntalo mediante SASL XOAUTH2. sgcIndy incluye TIdSASLXOAUTH2 en la unidad IdSASLXOAUTH2. Añádelo a TIdSMTP.SASLMechanisms, asigna AuthType := satSASL, y devuelve el nombre de usuario y el access token desde su evento OnAuthenticate. El mismo mecanismo autentica TIdIMAP4 y TIdPOP3.
El cliente OAuth2 y el cliente JWT se compilan en la edición Standard y en todas las ediciones por encima, así que Standard, Professional, Enterprise y All-Access los incluyen. El servidor OAuth2, el servidor JWT y el servidor WebAuthn son componentes Enterprise y no están presentes en las builds Standard ni Professional. Los dos componentes cliente también se venden por su cuenta como el paquete independiente sgcAuth, con el runtime que necesitan ya incluido.
Solo si eres tú quien emite los tokens. Iniciar la sesión de tus usuarios en Google, Microsoft, Auth0, Okta, Keycloak, AWS Cognito o un proveedor de identidad corporativo necesita el componente cliente y nada más. Necesitas TsgcHTTP_OAuth2_Server cuando es tu propia aplicación la que registra client ids, aloja la página de inicio de sesión y acuña los tokens en los que confía tu API. Valida PKCE por defecto mediante OAuth2Options.PKCE, registra aplicaciones con Apps.AddApp, y se engancha a un TsgcWebSocketHTTPServer mediante Authentication.OAuth.OAuth2.
Sí. Maneja OnBeforeAuthorizeCode, que recibe la URL de autorización ya construida en un parámetro var, asigna Handled := True para que el componente no lance el navegador del sistema, y navega un control embebido como TsgcWebView2 a esa URL. El listener de loopback sigue recibiendo la redirección y el flujo termina con normalidad. Ten en cuenta que varios proveedores ya bloquean su pantalla de consentimiento en navegadores embebidos, que es la razón de que el navegador del sistema sea la opción por defecto.
Sí. TsgcHTTP_OAuth2_Client compila para Windows, macOS, Linux, iOS y Android, en VCL, FireMonkey y Lazarus / FPC, desde Delphi 7 hasta Delphi 13 y las versiones de C++ Builder correspondientes. Abrir el navegador usa lo que ofrezca la plataforma. La única decisión específica de plataforma es el back end TLS para el intercambio de código, que se selecciona mediante HTTPClientOptions.TLSOptions.IOHandler.

Referencia, demo y documentación

La referencia del componente, el proyecto de demo listo para ejecutar y los documentos técnicos que van más a fondo que esta página.

Ayuda en línea, TsgcHTTP_OAuth2_Client Todas las propiedades, métodos y eventos del componente cliente, con el tema de Authorization Code + PKCE.
Ayuda en línea, Authorization Code con PKCE El tema del tipo de concesión: qué hace PKCE, la tabla de configuración y la recomendación de puerto aleatorio.
Proyecto de demo, Demos\20.HTTP_Protocol\02.OAuth2_Authentication Proyectos de cliente y de servidor con presets que funcionan para Gmail, Google Pub/Sub, Azure AD, AWS Cognito, Dropbox y Auth0, además de una variante con navegador embebido.
Documento técnico, OAuth2 Client (PDF) Características, inicio rápido, todos los tipos de concesión y ejemplos de código para Delphi, C++ Builder y .NET.
Documento técnico, OAuth2 Server (PDF) El componente Enterprise de servidor de autorización: endpoints, registro de aplicaciones, validación de PKCE y ciclo de vida del token.
Manual de usuario (PDF) Manual completo que cubre todos los componentes de la biblioteca.

Especificaciones que implementa este flujo

Fuentes primarias, para cuando necesitas zanjar una discusión con el soporte de un proveedor.

Componentes y artículos detrás de esta página

Las páginas de componente llevan la lista completa de características, los artículos cubren los casos que esta página solo roza.

Componente OAuth2 Client

Toda la superficie de propiedades, métodos y eventos de TsgcHTTP_OAuth2_Client, incluidos Device Code y DPoP.

Leer más →

Componente OAuth2 Server

El servidor de autorización Enterprise: tus propios endpoints de authorize, token, revoke e introspect.

Leer más →

sgcAuth

Los componentes cliente de OAuth2 y JWT como paquete independiente, con el runtime que necesitan ya incluido.

Leer más →

Componente JWT Client

Firma y adjunta JSON Web Tokens, por su cuenta o como fuente del Bearer para tus clientes HTTP y WebSocket.

Leer más →

PKCE con OAuth2 en Delphi

El artículo original de lanzamiento que presentó el soporte de PKCE en los componentes de cliente y de servidor.

Leer el artículo →

XOAuth2 en sgcIndy

Enviar correo con un access token de OAuth 2.0 sobre SMTP, IMAP y POP a través del mecanismo SASL XOAUTH2.

Leer el artículo →

OAuth2 Client Credentials

La variante sin usuario, para servicios en segundo plano y acceso a APIs de máquina a máquina.

Leer el artículo →

DPoP con OAuth2 en Delphi

Atar un access token a un par de claves, para proveedores que exigen prueba de posesión bajo RFC 9449.

Leer el artículo →

AWS Cognito y OAuth2

Una configuración trabajada contra un proveedor de identidad real, endpoint a endpoint.

Leer el artículo →

Servidor OAuth2: registrar aplicaciones

Registrar aplicaciones cliente, URI de redirección y tipos de concesión permitidos en tu propio servidor de autorización.

Leer el artículo →

Autorización con proveedores externos

Dejar que tu propio servidor delegue el inicio de sesión en Google, Microsoft o cualquier otro proveedor de identidad externo.

Leer el artículo →

WebAuthn y passkeys

La alternativa sin contraseña, para cuando prefieres que no haya ningún traspaso de token.

Leer más →

Esta página es uno de los casos de uso de Delphi, cada uno de los cuales lleva un único trabajo de principio a fin. Los otros que hay hasta ahora son llamar a un LLM desde Delphi y conectar dos aplicaciones entre pares con WebRTC.

Inicia hoy la sesión de tu primer usuario

Descarga la prueba gratuita, abre la demo de OAuth2, apúntala a tu proveedor y mira cómo se completa el viaje por el navegador.