Sign a User In to a Delphi Application with OAuth2 and PKCE

One component, one grant type, one browser hand-off. This page takes you from an empty form to a signed-in user with a live access token, using the Authorization Code flow with PKCE (RFC 7636), the flow every provider now expects from a native desktop application.

TsgcHTTP_OAuth2_Client
Code verifier and challenge generated for you
Delphi 7 to 13, C++ Builder, VCL and FireMonkey

What you need to sign a user in

A single non-visual component talks to the provider. You do not need a web server, an embedded browser, or a REST framework.

Component

TsgcHTTP_OAuth2_Client, declared in unit sgcHTTP and created in code, the way every demo does it.

Grant type

OAuth2Options.GrantType := auth2CodePKCE. That single assignment turns PKCE on.

Edition

Standard, Professional, Enterprise and All-Access. The client is not an Enterprise feature, the server is.

Platforms

Windows, macOS, Linux, iOS and Android. The component opens whichever browser the platform provides.

What the PKCE flow actually does

PKCE exists because a desktop application cannot keep a secret. It replaces the secret with a value the client proves it knew before the flow started.

1. Generate a code verifier

A high-entropy random string. sgcWebSockets asks the platform CSPRNG for 32 bytes and Base64URL-encodes them, which produces the 43-character verifier RFC 7636 asks for.

2. Derive the code challenge

SHA-256 of the verifier, Base64URL-encoded. The challenge is what travels in the authorization request, so an eavesdropper on the redirect never sees the verifier.

3. Open the browser

The component builds the authorization URL with client_id, redirect_uri, scope, state, code_challenge and code_challenge_method=S256, then launches the system browser.

4. The user signs in

Consent happens in the browser, on the provider's own domain, with the user's existing session, password manager and two-factor device. Your application never sees the password.

5. The redirect comes back

The provider redirects to your redirect_uri carrying code and state. On the desktop that URI is a loopback address, and the component is already listening on it.

6. Exchange the code

The component POSTs the code plus the original code_verifier to the token endpoint. The provider recomputes SHA-256 and compares. Match, and you get an access token.

Why the verifier matters

An authorization code is a bearer value for the few seconds it lives. Anything that can observe the redirect, a malicious application registered on the same custom URI scheme, a proxy, a shared log, can steal it. Without PKCE that stolen code is enough to mint a token.

With PKCE the token endpoint refuses the code unless the caller also presents the verifier whose SHA-256 hash matches the challenge sent at the start. The attacker saw only the hash, so the stolen code is worthless.

Nothing here is yours to write. Set GrantType to auth2CodePKCE and the component performs steps 1, 2, 3, 5 and 6 for you. What follows is the code that runs it, and the two decisions you do have to make: the redirect URI and where the refresh token lives.

on the wire
# 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

Sign a user in, in about twenty lines

Create the component, choose auth2CodePKCE, point it at the provider's two endpoints, bind OnAfterAccessToken, call Start. The browser opens, the user consents, the event fires with the 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");
}

Where the redirect goes in a desktop application

This is the part that has no obvious answer when you come from web OAuth2, and the part that most first attempts get wrong.

Loopback, not a public URL

A desktop application has no domain to redirect to. The accepted answer, and the one this component implements, is a loopback redirect: the application starts a tiny HTTP listener on 127.0.0.1, registers that address as the redirect URI, and shuts the listener down as soon as the code arrives.

LocalServerOptions.IP defaults to 127.0.0.1 and LocalServerOptions.Port defaults to 8080. For a shipped desktop application set Port := 0 instead: the operating system hands out a free ephemeral port, the component puts that port in the redirect URI it sends, and two copies of your application on the same machine never collide.

If the provider insists on an exact registered path rather than a bare host and port, set LocalServerOptions.RedirectURL to the value you registered. That string then overrides the computed one. A fixed path implies a fixed port, so register the port too and drop the Port := 0 trick.

The listener is only up while the flow is waiting. It is never started for auth2ClientCredentials, auth2ResourceOwnerPassword or auth2DeviceCode, which need no redirect at all.

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;

Read the token, then put it to work

After OnAfterAccessToken fires, the same values stay available as read-only properties, and the component can feed them to your HTTP and WebSocket clients without you touching a header.

Properties, and automatic Bearer headers

The event parameters are convenient, but they are not the only copy. AccessToken, TokenType, CurrentExpiresIn and CurrentRefreshToken hold the same values for as long as the component lives, so a handler somewhere else in your code can read them without threading them through.

RawParams is the untouched JSON body from the token endpoint. When a provider returns something outside the standard set, an id_token for OpenID Connect for example, parse it out of there. The component does not decode an ID token for you.

To have every request carry the token automatically, assign the OAuth2 component to Authentication.Token.OAuth on TsgcHTTP1Client, TsgcHTTP2Client or TsgcWebSocketClient. The client sends Authorization: Bearer <token> on your behalf, using whatever token_type the provider returned.

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;

Refreshing, so the browser never opens twice

An access token lives for minutes. A refresh token lives for weeks or months. Keeping the second one is what turns a sign-in into a session.

Two different problems

Inside one run of the application there is nothing to do. When the token endpoint returns both a refresh token and an expires_in, the component arms an internal timer at roughly half that lifetime and posts grant_type=refresh_token when it fires, well before the access token dies. OnAfterRefreshToken fires with the new pair, and OnErrorRefreshToken fires if the provider rejects it. Leave the Handled parameter of OnAfterAccessToken alone: setting it to True tells the component you are taking over, and it then neither stores the refresh token nor arms that timer.

Across restarts is your problem, because only you know where a secret may be written on your users' machines. Persist the refresh token, then at next launch skip Start entirely and call Refresh with the stored value. No browser opens, and the user is signed in before your main form is painted.

Providers that rotate refresh tokens hand you a new one with every renewal, so overwrite what you stored on every OnAfterRefreshToken. When the stored token is finally rejected, fall back to Start and let the user sign in again.

Use Revoke to sign the user out properly, and Introspect to ask the provider whether a token is still live. Both need the matching endpoint set in AuthorizationServerOptions.

refresh.pas
procedure TForm1.FormCreate(Sender: TObject);
var
  vStored: string;
begin
  ConfigureOAuth2; // same settings as the QuickStart
  OAuth2.OnAfterRefreshToken := OnAfterRefreshToken;
  OAuth2.OnErrorRefreshToken := OnErrorRefreshToken;

  vStored := LoadRefreshToken;
  if vStored <> '' then
    OAuth2.Refresh(vStored)  // silent, no browser
  else
    OAuth2.Start;            // first run, ask the user
end;

procedure TForm1.OnAfterRefreshToken(Sender: TObject; const Access_Token,
  Token_Type, Expires_In, Refresh_Token, Scope, RawParams: String;
  var Handled: Boolean);
begin
  // providers that rotate hand back a new refresh token
  if Refresh_Token <> '' then
    SaveRefreshToken(Refresh_Token);
end;

procedure TForm1.OnErrorRefreshToken(Sender: TObject; const Error,
  Error_Description, Error_URI, RawParams: String);
begin
  ClearStoredRefreshToken;
  OAuth2.Start; // the stored token is dead, prompt again
end;

// Signing out
OAuth2.AuthorizationServerOptions.RevocationURL :=
  'https://provider.com/oauth2/revoke';
OAuth2.Revoke(OAuth2.CurrentRefreshToken, 'refresh_token');

Storing tokens without leaving them lying around

sgcWebSockets deliberately ships no token vault. Where a credential may be written is a decision about your users and your deployment, so the library hands you the token and stops.

Keep the access token in memory only

It expires in minutes and the refresh token can always mint another. There is no reason to write it to disk, and every reason not to.

Encrypt the refresh token per user

On Windows, DPAPI (CryptProtectData) ties the ciphertext to the Windows account, so a copied file is useless on another machine. macOS has Keychain, and modern Linux desktops have Secret Service.

Never ship a client secret you rely on

Anything inside a distributed executable is public. That is the whole premise of PKCE. If your provider issues a secret for a desktop client, treat it as an identifier, not as protection.

Scope the file like a credential

Per-user application data, not Program Files, not next to the executable, not a shared network path, and not a plain INI committed to source control.

Delete on sign-out

Call Revoke so the provider invalidates the token, then remove the stored copy. A revoked token left on disk is still an audit finding.

Keep secrets out of the log

HTTPClientOptions.LogOptions writes the traffic to the authorization server. It is invaluable while you are getting the flow to work, and it is a file full of tokens. Turn it off before you ship.

Which provider needs what

Every OAuth 2.0 provider asks for the same handful of settings: two endpoints, a client id, the scopes and a registered redirect. Google and Microsoft additionally have ready-made components that fill the endpoints in and hand back a user profile.

Google and Microsoft, in one call

TsgcHTTP_OAuth2_Client_Google and TsgcHTTP_OAuth2_Client_Microsoft descend from the same base component and pre-fill the endpoints. Their Authenticate method is blocking: it runs the whole flow, waits for the browser round trip, and returns a data object with Authenticated and a populated UserProfile.

That is the shortest possible path to "who is this user". TsgcOAuth2_Google_Data.UserProfile carries _Name, Given_Name, Family_Name, Id, Locale and Picture. TsgcOAuth2_Microsoft_Data.UserProfile carries DisplayName, GivenName, Surname, Mail, JobTitle, OfficeLocation and more. Microsoft's Authenticate takes the tenant id first.

For every other provider, use the base TsgcHTTP_OAuth2_Client and copy the two URLs out of their documentation. There is nothing provider-specific left after that.

social-signin.pas
uses
  sgcHTTP, sgcHTTP_OAuth2_Client_Google;

var
  vClient: TsgcHTTP_OAuth2_Client_Google;
  vData: TsgcOAuth2_Google_Data;
begin
  vClient := TsgcHTTP_OAuth2_Client_Google.Create(nil);
  try
    vData := vClient.Authenticate('client-id', 'client-secret');
    if vData.Authenticated then
    begin
      ShowMessage(vData.UserProfile._Name);
      ShowMessage(vData.AccessToken);
    end;
  finally
    vClient.Free;
  end;
end;
Provider Component Grant type Redirect Client secret
Google TsgcHTTP_OAuth2_Client_Google or the base client auth2CodePKCE Loopback, Port := 0 Issued for desktop clients, set it if you have one
Microsoft Entra ID TsgcHTTP_OAuth2_Client_Microsoft or the base client auth2CodePKCE Loopback, registered as a mobile / desktop platform Not used by a public client, leave it empty
Auth0, Okta, Keycloak, AWS Cognito TsgcHTTP_OAuth2_Client auth2CodePKCE Loopback, registered on the application Depends on whether the app is public or confidential
Background jobs and services TsgcHTTP_OAuth2_Client auth2ClientCredentials None, no browser is involved Required, and safe, because nothing is distributed
Kiosks, TVs, headless boxes TsgcHTTP_OAuth2_Client auth2DeviceCode (RFC 8628) None, the user finishes on a phone Usually not required

Signing in to send email: OAuth 2.0 and XOAUTH2

Gmail and Microsoft 365 stopped accepting passwords over SMTP, IMAP and POP. The replacement is the same access token you just obtained, presented through the SASL XOAUTH2 mechanism.

The token comes from here, the SASL step comes from sgcIndy

Getting the token is exactly the flow above: auth2CodePKCE, a loopback redirect, and a mail scope such as https://mail.google.com/ in AuthorizationServerOptions.Scope. Nothing about the mail case changes the OAuth2 side.

Presenting it is the other half. sgcIndy ships TIdSASLXOAUTH2 in unit IdSASLXOAUTH2. Add it to TIdSMTP.SASLMechanisms, set AuthType := satSASL, and supply the user name and the access token from its OnAuthenticate event. The same mechanism works for TIdIMAP4 and TIdPOP3.

Keep the two components apart in your mind: the OAuth2 client knows how to obtain and renew a token, the SASL mechanism knows how to present one. Neither needs to know about the other.

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;

Do you need a client, or a server as well?

Everything above is client side. You only need the second half if you are the one issuing the tokens.

Client only

If you are signing users in to somebody else's identity provider, Google, Microsoft, Auth0, Okta, Keycloak, AWS Cognito, your own corporate SSO, you need nothing but TsgcHTTP_OAuth2_Client. That component is compiled into the Standard edition and every edition above it. It is also available on its own in the standalone sgcAuth package, bundled with the runtime it needs.

This is the common case, and it is the whole of this page up to here.

When you also need a server

You need the server half only when your own application is the authorization server: you issue the client ids, you host the sign-in page, you mint and revoke the access tokens that your API then trusts. That is TsgcHTTP_OAuth2_Server, attached to a TsgcWebSocketHTTPServer, and it is an Enterprise component.

It verifies PKCE by default. OAuth2Options.PKCE is True out of the box, so a client that sends a challenge must produce a matching verifier, and one that does not is refused. Register client applications with Apps.AddApp, authenticate users in OnOAuth2Authentication, and restore tokens across a restart with AddToken.

The same Enterprise tier carries TsgcHTTP_JWT_Server for validating JWT bearer tokens on your endpoints and TsgcWSAPIServer_WebAuthn for passkeys. The matching clients, TsgcHTTP_OAuth2_Client and TsgcHTTP_JWT_Client, are Standard and up. Client and server sit in different edition tiers, which is worth checking before you plan around either.

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;

What usually goes wrong the first time

Almost every failed first attempt at OAuth2 on the desktop is one of these six.

redirect_uri_mismatch

The URI the component sends must match what you registered, character for character, including the trailing slash and the port. If you registered a fixed URI, set LocalServerOptions.RedirectURL to exactly that string instead of relying on the computed one. If the provider allows any loopback port, use Port := 0 and register just the host.

The browser opens and nothing comes back

Something is holding the port, or a firewall rule is blocking the loopback listener. Set Port := 0, and check that a previous run of the flow was ended with Stop rather than left listening.

invalid_grant on the token exchange

Authorization codes are single use and short lived. Debugging with a breakpoint between the redirect and the exchange will expire the code. Read the failure from OnErrorAccessToken, which gives you the provider's own error and error_description, rather than guessing.

No refresh token was returned

Providers only issue one when you ask. Google wants access_type=offline, Microsoft wants the offline_access scope. Add the scope to AuthorizationServerOptions.Scope, or append the query parameter by editing the URL parameter in OnBeforeAuthorizeCode.

TLS fails on Linux or mobile

The token exchange is an HTTPS POST, so it needs a working TLS back end. HTTPClientOptions.TLSOptions.IOHandler selects it: iohOpenSSL, iohSChannel on Windows with no DLLs to deploy, or the native iohAndroidTLS and iohAppleTLS handlers in the Enterprise edition.

You wanted the sign-in page inside the app

Handle OnBeforeAuthorizeCode, set Handled := True and navigate your own TsgcWebView2 or TWebBrowser to the URL you were given. The loopback listener still catches the redirect. Note that several providers now refuse to render their consent screen inside an embedded browser.

Delphi OAuth2 and PKCE questions

The questions developers actually search for before they start.

Drop a TsgcHTTP_OAuth2_Client, set OAuth2Options.GrantType := auth2CodePKCE, fill in OAuth2Options.ClientId, AuthorizationServerOptions.AuthURL, AuthorizationServerOptions.TokenURL and AuthorizationServerOptions.Scope, set LocalServerOptions.IP to 127.0.0.1 and LocalServerOptions.Port to 0, then call Start. The component generates the PKCE values, opens the browser, catches the redirect on a loopback listener, exchanges the code and raises OnAfterAccessToken with the token.
You do not have to. When GrantType is auth2CodePKCE, TsgcHTTP_OAuth2_Client draws 32 bytes from the platform cryptographic random source, Base64URL-encodes them into the 43-character code verifier, sets the code challenge to the Base64URL encoding of the SHA-256 hash of that verifier, and fixes code_challenge_method to S256. The verifier is kept privately inside the component and replayed on the token exchange, so it never appears in the redirect. If you want to build the pair by hand for another purpose, the same primitives are public: sgcRandomBytes in unit sgcCrypto_Random, plus GetHashSHA256 and EncodeBase64URL in unit sgcBase_Helpers.
A loopback address. TsgcHTTP_OAuth2_Client starts a small HTTP listener on LocalServerOptions.IP and LocalServerOptions.Port only while the flow is running, and the redirect URI it sends is built from those values. The defaults are 127.0.0.1 and port 8080. For a shipped application set Port to 0 so the operating system picks a free ephemeral port and two instances never fight over one. If the provider requires an exact registered URI, put that string in LocalServerOptions.RedirectURL and it overrides the computed value.
It depends on the provider. PKCE exists precisely because a shipped desktop application cannot keep a secret, so a public client normally sends no secret at all and leaves OAuth2Options.ClientSecret empty. Some providers still issue one for desktop clients and expect it on the token request. Set it when they do, but treat it as an identifier rather than as a protection, because anything inside a distributed executable can be extracted.
Persist the refresh token, then call Refresh with it at the next launch instead of Start. Read it from the Refresh_Token parameter of OnAfterAccessToken, or later from the CurrentRefreshToken property. Overwrite the stored copy on every OnAfterRefreshToken, because providers that rotate refresh tokens invalidate the old one. Within a single run no work is needed at all: the component arms a timer from the expires_in value and renews the access token on its own.
Keep the access token in memory only, it expires in minutes and can always be minted again. Persist the refresh token encrypted and scoped to the current user, for example with DPAPI on Windows, Keychain on macOS or Secret Service on Linux, in per-user application data rather than beside the executable. sgcWebSockets ships no token vault of its own on purpose: it hands you the token and leaves the storage decision to you. Remember to turn HTTPClientOptions.LogOptions off before shipping, because that log contains the tokens.
Obtain an access token with the flow on this page, requesting the provider's mail scope such as https://mail.google.com/, then present it through SASL XOAUTH2. sgcIndy ships TIdSASLXOAUTH2 in unit IdSASLXOAUTH2. Add it to TIdSMTP.SASLMechanisms, set AuthType := satSASL, and return the user name and the access token from its OnAuthenticate event. The same mechanism authenticates TIdIMAP4 and TIdPOP3.
The OAuth2 client and the JWT client are compiled into the Standard edition and every edition above it, so Standard, Professional, Enterprise and All-Access all include them. The OAuth2 server, the JWT server and the WebAuthn server are Enterprise components and are not present in Standard or Professional builds. The two client components are also sold on their own as the standalone sgcAuth package, bundled with the runtime they need.
Only if you are the one issuing tokens. Signing users in to Google, Microsoft, Auth0, Okta, Keycloak, AWS Cognito or a corporate identity provider needs the client component and nothing else. You need TsgcHTTP_OAuth2_Server when your own application registers client ids, hosts the sign-in page and mints the tokens your API trusts. It validates PKCE by default through OAuth2Options.PKCE, registers applications with Apps.AddApp, and attaches to a TsgcWebSocketHTTPServer through Authentication.OAuth.OAuth2.
Yes. Handle OnBeforeAuthorizeCode, which receives the fully built authorization URL as a var parameter, set Handled := True so the component does not launch the system browser, and navigate an embedded control such as TsgcWebView2 to that URL. The loopback listener still receives the redirect and the flow finishes normally. Be aware that several providers now block their consent screen in embedded browsers, which is why the system browser is the default.
Yes. TsgcHTTP_OAuth2_Client compiles for Windows, macOS, Linux, iOS and Android, in VCL, FireMonkey and Lazarus / FPC, from Delphi 7 to Delphi 13 and the matching C++ Builder versions. Opening the browser uses whatever the platform provides. The one platform-specific choice is the TLS back end for the token exchange, selected through HTTPClientOptions.TLSOptions.IOHandler.

Reference, demo and documentation

The component reference, the ready-to-run demo project, and the technical documents that go deeper than this page.

Online Help, TsgcHTTP_OAuth2_Client Every property, method and event of the client component, with the Authorization Code + PKCE topic.
Online Help, Authorization Code with PKCE The grant-type topic: what PKCE does, the configuration table and the random-port recommendation.
Demo Project, Demos\20.HTTP_Protocol\02.OAuth2_Authentication Client and server projects with working presets for Gmail, Google Pub/Sub, Azure AD, AWS Cognito, Dropbox and Auth0, plus an embedded-browser variant.
Technical Document, OAuth2 Client (PDF) Features, quick start, every grant type and code samples for Delphi, C++ Builder and .NET.
Technical Document, OAuth2 Server (PDF) The Enterprise authorization-server component: endpoints, app registration, PKCE validation and token lifecycle.
User Manual (PDF) Comprehensive manual covering every component in the library.

Specifications this flow implements

Primary sources, when you need to settle an argument with a provider's support desk.

Components and articles behind this page

The component pages carry the full feature list, the articles cover the cases this page only touches.

OAuth2 Client component

The full property, method and event surface of TsgcHTTP_OAuth2_Client, including Device Code and DPoP.

Read more →

OAuth2 Server component

The Enterprise authorization server: your own authorize, token, revoke and introspect endpoints.

Read more →

sgcAuth

The OAuth2 and JWT client components as a standalone package, with the runtime they need bundled in.

Read more →

JWT Client component

Sign and attach JSON Web Tokens, on their own or as the Bearer source for your HTTP and WebSocket clients.

Read more →

Delphi PKCE OAuth2

The original release article introducing PKCE support on both the client and the server components.

Read post →

sgcIndy XOAuth2

Sending mail with an OAuth 2.0 access token over SMTP, IMAP and POP through the SASL XOAUTH2 mechanism.

Read post →

OAuth2 Client Credentials

The no-user variant, for background services and machine-to-machine API access.

Read post →

OAuth2 DPoP in Delphi

Binding an access token to a key pair, for providers that require proof of possession under RFC 9449.

Read post →

AWS Cognito and OAuth2

A worked configuration against a real identity provider, endpoint by endpoint.

Read post →

OAuth2 Server: register apps

Registering client applications, redirect URIs and allowed grant types on your own authorization server.

Read post →

Authorization with external providers

Letting your own server delegate sign-in to Google, Microsoft or any other external identity provider.

Read post →

WebAuthn and passkeys

The passwordless alternative, when you would rather have no token hand-off at all.

Read more →

This page is one of the Delphi use cases, each of which takes a single job end to end. The others so far are calling an LLM from Delphi and connecting two applications peer to peer with WebRTC.

Best value: All-AccessEvery eSeGeCe product, Premium Support included, from €1,059/year.
See All-Access pricing

Sign your first user in today

Download the free trial, open the OAuth2 demo, point it at your provider and watch the browser round trip complete.