Autentique um usuário em uma aplicação Delphi com OAuth2 e PKCE

Um componente, um grant type, uma passagem pelo navegador. Esta página leva você de um formulário vazio até um usuário autenticado com um access token válido, usando o fluxo Authorization Code com PKCE (RFC 7636), o fluxo que todo provedor hoje espera de uma aplicação desktop nativa.

TsgcHTTP_OAuth2_Client
Code verifier e challenge gerados para você
Do Delphi 7 ao 13, C++ Builder, VCL e FireMonkey

O que você precisa para autenticar um usuário

Um único componente não visual conversa com o provedor. Você não precisa de um servidor web, de um navegador embutido nem de um framework REST.

Componente

TsgcHTTP_OAuth2_Client, declarado na unit sgcHTTP e criado em código, do jeito que todas as demonstrações fazem.

Grant type

OAuth2Options.GrantType := auth2CodePKCE. Essa única atribuição liga o PKCE.

Edição

Standard, Professional, Enterprise e All-Access. O cliente não é um recurso Enterprise, o servidor é.

Plataformas

Windows, macOS, Linux, iOS e Android. O componente abre o navegador que a plataforma oferecer.

O que o fluxo PKCE realmente faz

O PKCE existe porque uma aplicação desktop não consegue guardar um segredo. Ele troca o segredo por um valor que o cliente prova ter conhecido antes de o fluxo começar.

1. Gerar um code verifier

Uma string aleatória de alta entropia. O sgcWebSockets pede 32 bytes ao CSPRNG da plataforma e os codifica em Base64URL, o que produz o verifier de 43 caracteres que a RFC 7636 exige.

2. Derivar o code challenge

SHA-256 do verifier, codificado em Base64URL. É o challenge que viaja na requisição de autorização, então quem estiver espionando o redirecionamento nunca vê o verifier.

3. Abrir o navegador

O componente monta a URL de autorização com client_id, redirect_uri, scope, state, code_challenge e code_challenge_method=S256, e então abre o navegador do sistema.

4. O usuário faz login

O consentimento acontece no navegador, no domínio do próprio provedor, com a sessão que o usuário já tem, o gerenciador de senhas e o dispositivo de segundo fator dele. A sua aplicação nunca vê a senha.

5. O redirecionamento volta

O provedor redireciona para o seu redirect_uri levando code e state. No desktop essa URI é um endereço de loopback, e o componente já está escutando nele.

6. Trocar o code

O componente faz um POST do code junto com o code_verifier original para o endpoint de token. O provedor recalcula o SHA-256 e compara. Se bater, você recebe um access token.

Por que o verifier importa

Um authorization code é um valor ao portador durante os poucos segundos em que vive. Qualquer coisa capaz de observar o redirecionamento, uma aplicação maliciosa registrada no mesmo esquema de URI personalizado, um proxy, um log compartilhado, consegue roubá-lo. Sem PKCE, esse code roubado basta para emitir um token.

Com PKCE, o endpoint de token recusa o code a menos que quem chama também apresente o verifier cujo hash SHA-256 corresponde ao challenge enviado no início. O atacante viu apenas o hash, então o code roubado não vale nada.

Nada disso cabe a você escrever. Defina GrantType como auth2CodePKCE e o componente executa os passos 1, 2, 3, 5 e 6 por você. O que vem a seguir é o código que roda tudo isso, e as duas decisões que realmente são suas: a URI de redirecionamento e onde o refresh token fica guardado.

na rede
# 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

Autentique um usuário em cerca de vinte linhas

Crie o componente, escolha auth2CodePKCE, aponte-o para os dois endpoints do provedor, ligue OnAfterAccessToken e chame Start. O navegador abre, o usuário consente, o evento dispara com o 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");
}

Para onde vai o redirecionamento em uma aplicação desktop

Esta é a parte que não tem resposta óbvia para quem vem do OAuth2 na web, e a parte que a maioria das primeiras tentativas erra.

Loopback, não uma URL pública

Uma aplicação desktop não tem domínio para o qual redirecionar. A resposta consagrada, e a que este componente implementa, é o redirecionamento por loopback: a aplicação sobe um pequeno listener HTTP em 127.0.0.1, registra esse endereço como URI de redirecionamento e derruba o listener assim que o code chega.

LocalServerOptions.IP tem como padrão 127.0.0.1 e LocalServerOptions.Port tem como padrão 8080. Para uma aplicação desktop distribuída, use Port := 0: o sistema operacional entrega uma porta efêmera livre, o componente coloca essa porta na URI de redirecionamento que envia, e duas cópias da sua aplicação na mesma máquina nunca colidem.

Se o provedor exigir um caminho registrado exato em vez de apenas host e porta, defina LocalServerOptions.RedirectURL com o valor que você registrou. Essa string então sobrepõe a que seria calculada. Um caminho fixo implica uma porta fixa, então registre a porta também e abandone o truque do Port := 0.

O listener só fica de pé enquanto o fluxo está esperando. Ele nunca é iniciado para auth2ClientCredentials, auth2ResourceOwnerPassword ou auth2DeviceCode, que não precisam de redirecionamento nenhum.

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;

Leia o token e coloque-o para trabalhar

Depois que OnAfterAccessToken dispara, os mesmos valores continuam disponíveis como propriedades somente leitura, e o componente pode entregá-los aos seus clientes HTTP e WebSocket sem que você toque em um cabeçalho.

Propriedades, e cabeçalhos Bearer automáticos

Os parâmetros do evento são convenientes, mas não são a única cópia. AccessToken, TokenType, CurrentExpiresIn e CurrentRefreshToken guardam os mesmos valores enquanto o componente existir, então um manipulador em outro ponto do seu código consegue lê-los sem que você precise passá-los adiante.

RawParams é o corpo JSON intocado vindo do endpoint de token. Quando um provedor devolve algo fora do conjunto padrão, um id_token para OpenID Connect, por exemplo, extraia dali. O componente não decodifica um ID token para você.

Para que toda requisição carregue o token automaticamente, atribua o componente OAuth2 a Authentication.Token.OAuth em TsgcHTTP1Client, TsgcHTTP2Client ou TsgcWebSocketClient. O cliente envia Authorization: Bearer <token> em seu nome, usando o token_type que o provedor tiver devolvido.

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;

Renovação, para que o navegador nunca abra duas vezes

Um access token vive por minutos. Um refresh token vive por semanas ou meses. Guardar o segundo é o que transforma um login em uma sessão.

Dois problemas diferentes

Dentro de uma execução da aplicação não há nada a fazer. Quando o endpoint de token devolve um refresh token e um expires_in, o componente arma um timer interno em cerca de metade desse tempo de vida e envia grant_type=refresh_token quando ele dispara, bem antes de o access token morrer. OnAfterRefreshToken dispara com o novo par, e OnErrorRefreshToken dispara se o provedor recusar. Não mexa no parâmetro Handled de OnAfterAccessToken: defini-lo como True avisa ao componente que você está assumindo o controle, e aí ele nem guarda o refresh token nem arma esse timer.

Entre reinicializações o problema é seu, porque só você sabe onde um segredo pode ser gravado nas máquinas dos seus usuários. Persista o refresh token e, no próximo início, pule o Start por completo e chame Refresh com o valor guardado. Nenhum navegador abre, e o usuário já está autenticado antes de o seu formulário principal ser desenhado.

Provedores que rotacionam refresh tokens entregam um novo a cada renovação, então sobrescreva o que você guardou a cada OnAfterRefreshToken. Quando o token guardado finalmente for recusado, volte para o Start e deixe o usuário entrar de novo.

Use Revoke para encerrar a sessão do usuário como se deve, e Introspect para perguntar ao provedor se um token ainda está válido. Os dois precisam do endpoint correspondente definido em 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 sem deixá-los à mostra

O sgcWebSockets deliberadamente não inclui um cofre de tokens. Onde uma credencial pode ser gravada é uma decisão sobre os seus usuários e a sua implantação, então a biblioteca entrega o token a você e para por aí.

Mantenha o access token só na memória

Ele expira em minutos e o refresh token sempre consegue emitir outro. Não há razão para gravá-lo em disco, e há todas as razões para não gravar.

Criptografe o refresh token por usuário

No Windows, a DPAPI (CryptProtectData) amarra o texto cifrado à conta do Windows, então um arquivo copiado não serve para nada em outra máquina. O macOS tem o Keychain, e os desktops Linux modernos têm o Secret Service.

Nunca distribua um client secret do qual você dependa

Tudo que está dentro de um executável distribuído é público. Essa é justamente a premissa do PKCE. Se o seu provedor emite um segredo para um cliente desktop, trate-o como um identificador, não como proteção.

Trate o arquivo como uma credencial

Dados de aplicação por usuário, não Program Files, não ao lado do executável, não um caminho de rede compartilhado e não um INI em texto puro versionado no controle de código.

Apague ao sair

Chame Revoke para que o provedor invalide o token e depois remova a cópia guardada. Um token revogado esquecido em disco continua sendo um apontamento de auditoria.

Mantenha segredos fora do log

HTTPClientOptions.LogOptions grava o tráfego com o servidor de autorização. É valiosíssimo enquanto você faz o fluxo funcionar, e é um arquivo cheio de tokens. Desligue-o antes de publicar.

Do que cada provedor precisa

Todo provedor OAuth 2.0 pede o mesmo punhado de configurações: dois endpoints, um client id, os scopes e um redirecionamento registrado. Google e Microsoft ainda têm componentes prontos que preenchem os endpoints e devolvem um perfil de usuário.

Google e Microsoft, em uma chamada

TsgcHTTP_OAuth2_Client_Google e TsgcHTTP_OAuth2_Client_Microsoft descendem do mesmo componente base e já preenchem os endpoints. O método Authenticate deles é bloqueante: roda o fluxo inteiro, espera a ida e volta pelo navegador e devolve um objeto de dados com Authenticated e um UserProfile preenchido.

Esse é o caminho mais curto possível para "quem é este usuário". TsgcOAuth2_Google_Data.UserProfile traz _Name, Given_Name, Family_Name, Id, Locale e Picture. TsgcOAuth2_Microsoft_Data.UserProfile traz DisplayName, GivenName, Surname, Mail, JobTitle, OfficeLocation e outros. O Authenticate da Microsoft recebe o tenant id primeiro.

Para todos os outros provedores, use o TsgcHTTP_OAuth2_Client base e copie as duas URLs da documentação deles. Depois disso não sobra nada específico de provedor.

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;
Provedor Componente Grant type Redirecionamento Client secret
Google TsgcHTTP_OAuth2_Client_Google ou o cliente base auth2CodePKCE Loopback, Port := 0 Emitido para clientes desktop, defina se você tiver um
Microsoft Entra ID TsgcHTTP_OAuth2_Client_Microsoft ou o cliente base auth2CodePKCE Loopback, registrado como plataforma mobile / desktop Não usado por um cliente público, deixe vazio
Auth0, Okta, Keycloak, AWS Cognito TsgcHTTP_OAuth2_Client auth2CodePKCE Loopback, registrado na aplicação Depende de o aplicativo ser público ou confidencial
Tarefas em segundo plano e serviços TsgcHTTP_OAuth2_Client auth2ClientCredentials Nenhum, nenhum navegador entra na história Obrigatório, e seguro, porque nada é distribuído
Quiosques, TVs, máquinas sem tela TsgcHTTP_OAuth2_Client auth2DeviceCode (RFC 8628) Nenhum, o usuário conclui pelo celular Normalmente não é necessário

Autenticar para enviar e-mail: OAuth 2.0 e XOAUTH2

Gmail e Microsoft 365 pararam de aceitar senhas por SMTP, IMAP e POP. A substituição é o mesmo access token que você acabou de obter, apresentado pelo mecanismo SASL XOAUTH2.

O token vem daqui, a etapa SASL vem do sgcIndy

Obter o token é exatamente o fluxo acima: auth2CodePKCE, um redirecionamento por loopback e um scope de correio como https://mail.google.com/ em AuthorizationServerOptions.Scope. Nada no caso do e-mail muda o lado OAuth2.

Apresentá-lo é a outra metade. O sgcIndy traz TIdSASLXOAUTH2 na unit IdSASLXOAUTH2. Adicione-o a TIdSMTP.SASLMechanisms, defina AuthType := satSASL e forneça o nome de usuário e o access token pelo evento OnAuthenticate dele. O mesmo mecanismo funciona para TIdIMAP4 e TIdPOP3.

Mantenha os dois componentes separados na sua cabeça: o cliente OAuth2 sabe obter e renovar um token, o mecanismo SASL sabe apresentar um. Nenhum dos dois precisa saber do outro.

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;

Você precisa de um cliente, ou também de um servidor?

Tudo acima é do lado cliente. Você só precisa da segunda metade se for você quem emite os tokens.

Somente cliente

Se você está autenticando usuários no provedor de identidade de outra pessoa, Google, Microsoft, Auth0, Okta, Keycloak, AWS Cognito, o SSO corporativo da sua empresa, você não precisa de nada além do TsgcHTTP_OAuth2_Client. Esse componente é compilado na edição Standard e em todas as edições acima dela. Ele também está disponível sozinho no pacote independente sgcAuth, com o runtime de que precisa incluído.

Esse é o caso comum, e é tudo o que esta página tratou até aqui.

Quando você também precisa de um servidor

Você precisa da metade do servidor só quando a sua própria aplicação é o servidor de autorização: é você quem emite os client ids, quem hospeda a página de login, quem cria e revoga os access tokens em que a sua API confia. Isso é o TsgcHTTP_OAuth2_Server, ligado a um TsgcWebSocketHTTPServer, e ele é um componente Enterprise.

Ele verifica PKCE por padrão. OAuth2Options.PKCE já vem como True, então um cliente que envia um challenge precisa apresentar um verifier correspondente, e o que não apresenta é recusado. Registre as aplicações cliente com Apps.AddApp, autentique usuários em OnOAuth2Authentication e restaure tokens após um reinício com AddToken.

A mesma faixa Enterprise traz TsgcHTTP_JWT_Server para validar bearer tokens JWT nos seus endpoints e TsgcWSAPIServer_WebAuthn para passkeys. Os clientes correspondentes, TsgcHTTP_OAuth2_Client e TsgcHTTP_JWT_Client, são Standard para cima. Cliente e servidor ficam em faixas de edição diferentes, o que vale conferir antes de planejar em cima de qualquer um dos dois.

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;

O que costuma dar errado na primeira vez

Quase toda primeira tentativa fracassada de OAuth2 no desktop é um destes seis casos.

redirect_uri_mismatch

A URI que o componente envia precisa bater com o que você registrou, caractere por caractere, incluindo a barra final e a porta. Se você registrou uma URI fixa, defina LocalServerOptions.RedirectURL exatamente com essa string em vez de confiar na que seria calculada. Se o provedor aceitar qualquer porta de loopback, use Port := 0 e registre apenas o host.

O navegador abre e nada volta

Alguma coisa está segurando a porta, ou uma regra de firewall está bloqueando o listener de loopback. Defina Port := 0 e verifique se uma execução anterior do fluxo foi encerrada com Stop em vez de ter ficado escutando.

invalid_grant na troca do token

Authorization codes são de uso único e vida curta. Depurar com um breakpoint entre o redirecionamento e a troca faz o code expirar. Leia a falha em OnErrorAccessToken, que entrega o error e o error_description do próprio provedor, em vez de ficar adivinhando.

Nenhum refresh token foi devolvido

Os provedores só emitem um quando você pede. O Google quer access_type=offline, a Microsoft quer o scope offline_access. Adicione o scope a AuthorizationServerOptions.Scope, ou acrescente o parâmetro de consulta editando o parâmetro URL em OnBeforeAuthorizeCode.

O TLS falha no Linux ou no mobile

A troca do token é um POST HTTPS, então ela precisa de um back end TLS funcionando. HTTPClientOptions.TLSOptions.IOHandler escolhe qual: iohOpenSSL, iohSChannel no Windows sem DLLs para distribuir, ou os handlers nativos iohAndroidTLS e iohAppleTLS na edição Enterprise.

Você queria a página de login dentro do aplicativo

Trate OnBeforeAuthorizeCode, defina Handled := True e navegue o seu próprio TsgcWebView2 ou TWebBrowser até a URL recebida. O listener de loopback continua capturando o redirecionamento. Repare que vários provedores hoje se recusam a exibir a tela de consentimento dentro de um navegador embutido.

Perguntas sobre OAuth2 e PKCE no Delphi

As perguntas que os desenvolvedores realmente pesquisam antes de começar.

Coloque um TsgcHTTP_OAuth2_Client, defina OAuth2Options.GrantType := auth2CodePKCE, preencha OAuth2Options.ClientId, AuthorizationServerOptions.AuthURL, AuthorizationServerOptions.TokenURL e AuthorizationServerOptions.Scope, defina LocalServerOptions.IP como 127.0.0.1 e LocalServerOptions.Port como 0, e então chame Start. O componente gera os valores de PKCE, abre o navegador, captura o redirecionamento em um listener de loopback, troca o code e dispara OnAfterAccessToken com o token.
Você não precisa. Quando GrantType é auth2CodePKCE, o TsgcHTTP_OAuth2_Client tira 32 bytes da fonte criptográfica de aleatoriedade da plataforma, codifica-os em Base64URL formando o code verifier de 43 caracteres, define o code challenge como a codificação Base64URL do hash SHA-256 desse verifier e fixa code_challenge_method em S256. O verifier fica guardado de forma privada dentro do componente e é reapresentado na troca do token, então ele nunca aparece no redirecionamento. Se você quiser montar o par à mão para outra finalidade, as mesmas primitivas são públicas: sgcRandomBytes na unit sgcCrypto_Random, mais GetHashSHA256 e EncodeBase64URL na unit sgcBase_Helpers.
Um endereço de loopback. O TsgcHTTP_OAuth2_Client sobe um pequeno listener HTTP em LocalServerOptions.IP e LocalServerOptions.Port apenas enquanto o fluxo está rodando, e a URI de redirecionamento que ele envia é montada a partir desses valores. Os padrões são 127.0.0.1 e a porta 8080. Para uma aplicação distribuída, defina Port como 0 para que o sistema operacional escolha uma porta efêmera livre e duas instâncias nunca briguem por ela. Se o provedor exigir uma URI registrada exata, coloque essa string em LocalServerOptions.RedirectURL e ela sobrepõe o valor calculado.
Depende do provedor. O PKCE existe justamente porque uma aplicação desktop distribuída não consegue guardar um segredo, então um cliente público normalmente não envia segredo nenhum e deixa OAuth2Options.ClientSecret vazio. Alguns provedores ainda emitem um para clientes desktop e o esperam na requisição de token. Defina-o quando for o caso, mas trate-o como um identificador e não como proteção, porque tudo que está dentro de um executável distribuído pode ser extraído.
Persista o refresh token e, no início seguinte, chame Refresh com ele em vez de Start. Leia-o no parâmetro Refresh_Token de OnAfterAccessToken, ou mais tarde na propriedade CurrentRefreshToken. Sobrescreva a cópia guardada a cada OnAfterRefreshToken, porque provedores que rotacionam refresh tokens invalidam o antigo. Dentro de uma única execução não é preciso fazer nada: o componente arma um timer a partir do valor de expires_in e renova o access token sozinho.
Mantenha o access token apenas na memória, ele expira em minutos e sempre pode ser emitido de novo. Persista o refresh token criptografado e restrito ao usuário atual, por exemplo com DPAPI no Windows, Keychain no macOS ou Secret Service no Linux, em dados de aplicação por usuário e não ao lado do executável. O sgcWebSockets de propósito não traz um cofre de tokens próprio: ele entrega o token a você e deixa a decisão de armazenamento por sua conta. Lembre-se de desligar HTTPClientOptions.LogOptions antes de publicar, porque esse log contém os tokens.
Obtenha um access token com o fluxo desta página, pedindo o scope de correio do provedor, como https://mail.google.com/, e depois apresente-o pelo SASL XOAUTH2. O sgcIndy traz TIdSASLXOAUTH2 na unit IdSASLXOAUTH2. Adicione-o a TIdSMTP.SASLMechanisms, defina AuthType := satSASL e devolva o nome de usuário e o access token pelo evento OnAuthenticate dele. O mesmo mecanismo autentica TIdIMAP4 e TIdPOP3.
O cliente OAuth2 e o cliente JWT são compilados na edição Standard e em todas as edições acima dela, então Standard, Professional, Enterprise e All-Access incluem os dois. O servidor OAuth2, o servidor JWT e o servidor WebAuthn são componentes Enterprise e não estão presentes nas builds Standard ou Professional. Os dois componentes cliente também são vendidos à parte, como o pacote independente sgcAuth, com o runtime de que precisam incluído.
Só se for você quem emite os tokens. Autenticar usuários no Google, na Microsoft, no Auth0, no Okta, no Keycloak, no AWS Cognito ou em um provedor de identidade corporativo precisa apenas do componente cliente. Você precisa do TsgcHTTP_OAuth2_Server quando a sua própria aplicação registra client ids, hospeda a página de login e cria os tokens em que a sua API confia. Ele valida PKCE por padrão através de OAuth2Options.PKCE, registra aplicações com Apps.AddApp e se liga a um TsgcWebSocketHTTPServer através de Authentication.OAuth.OAuth2.
Sim. Trate OnBeforeAuthorizeCode, que recebe a URL de autorização já montada como parâmetro var, defina Handled := True para que o componente não abra o navegador do sistema e navegue um controle embutido, como TsgcWebView2, até essa URL. O listener de loopback continua recebendo o redirecionamento e o fluxo termina normalmente. Fique atento: vários provedores hoje bloqueiam a tela de consentimento em navegadores embutidos, e é por isso que o navegador do sistema é o padrão.
Sim. O TsgcHTTP_OAuth2_Client compila para Windows, macOS, Linux, iOS e Android, em VCL, FireMonkey e Lazarus / FPC, do Delphi 7 ao Delphi 13 e nas versões correspondentes do C++ Builder. A abertura do navegador usa o que a plataforma oferecer. A única escolha específica de plataforma é o back end TLS para a troca do token, selecionado através de HTTPClientOptions.TLSOptions.IOHandler.

Referência, demonstração e documentação

A referência do componente, o projeto de demonstração pronto para rodar e os documentos técnicos que vão além desta página.

Ajuda online, TsgcHTTP_OAuth2_Client Todas as propriedades, métodos e eventos do componente cliente, com o tópico Authorization Code + PKCE.
Ajuda online, Authorization Code com PKCE O tópico do grant type: o que o PKCE faz, a tabela de configuração e a recomendação de porta aleatória.
Projeto de demonstração, Demos\20.HTTP_Protocol\02.OAuth2_Authentication Projetos de cliente e de servidor com presets prontos para Gmail, Google Pub/Sub, Azure AD, AWS Cognito, Dropbox e Auth0, além de uma variante com navegador embutido.
Documento técnico, OAuth2 Client (PDF) Recursos, início rápido, todos os grant types e exemplos de código para Delphi, C++ Builder e .NET.
Documento técnico, OAuth2 Server (PDF) O componente Enterprise de servidor de autorização: endpoints, registro de aplicações, validação de PKCE e ciclo de vida do token.
Manual do usuário (PDF) Manual completo cobrindo todos os componentes da biblioteca.

Especificações que este fluxo implementa

Fontes primárias, para quando você precisar encerrar uma discussão com o suporte de um provedor.

Componentes e artigos por trás desta página

As páginas dos componentes trazem a lista completa de recursos, os artigos cobrem os casos que esta página apenas toca de leve.

Componente OAuth2 Client

Toda a superfície de propriedades, métodos e eventos do TsgcHTTP_OAuth2_Client, incluindo Device Code e DPoP.

Saiba mais →

Componente OAuth2 Server

O servidor de autorização Enterprise: os seus próprios endpoints de authorize, token, revoke e introspect.

Saiba mais →

sgcAuth

Os componentes cliente de OAuth2 e JWT como pacote independente, com o runtime de que precisam incluído.

Saiba mais →

Componente JWT Client

Assine e anexe JSON Web Tokens, sozinhos ou como origem do Bearer para os seus clientes HTTP e WebSocket.

Saiba mais →

OAuth2 com PKCE no Delphi

O artigo de lançamento original apresentando o suporte a PKCE nos componentes de cliente e de servidor.

Ler o artigo →

sgcIndy XOAuth2

Envio de correio com um access token OAuth 2.0 por SMTP, IMAP e POP através do mecanismo SASL XOAUTH2.

Ler o artigo →

OAuth2 Client Credentials

A variante sem usuário, para serviços em segundo plano e acesso de API entre máquinas.

Ler o artigo →

OAuth2 DPoP no Delphi

Vincular um access token a um par de chaves, para provedores que exigem prova de posse conforme a RFC 9449.

Ler o artigo →

AWS Cognito e OAuth2

Uma configuração trabalhada contra um provedor de identidade real, endpoint por endpoint.

Ler o artigo →

OAuth2 Server: registrar aplicações

Registro de aplicações cliente, URIs de redirecionamento e grant types permitidos no seu próprio servidor de autorização.

Ler o artigo →

Autorização com provedores externos

Deixar o seu próprio servidor delegar o login ao Google, à Microsoft ou a qualquer outro provedor de identidade externo.

Ler o artigo →

WebAuthn e passkeys

A alternativa sem senha, para quando você prefere não ter passagem de token nenhuma.

Saiba mais →

Esta página faz parte dos casos de uso Delphi, cada um deles levando um único trabalho do início ao fim. Os outros até agora são chamar um LLM a partir do Delphi e conectar duas aplicações peer to peer com WebRTC.

Autentique o seu primeiro usuário hoje

Baixe a versão de avaliação gratuita, abra a demonstração de OAuth2, aponte-a para o seu provedor e veja a ida e volta pelo navegador se completar.