Logowanie użytkownika w aplikacji Delphi za pomocą OAuth2 i PKCE

Jeden komponent, jeden typ grantu, jedno przekazanie sterowania do przeglądarki. Ta strona prowadzi cię od pustego formularza do zalogowanego użytkownika z ważnym tokenem dostępu, używając przepływu Authorization Code z PKCE (RFC 7636), czyli tego, którego każdy dostawca oczekuje dziś od natywnej aplikacji desktopowej.

TsgcHTTP_OAuth2_Client
Code verifier i code challenge generowane za ciebie
Delphi 7 do 13, C++ Builder, VCL i FireMonkey

Czego potrzebujesz, aby zalogować użytkownika

Z dostawcą rozmawia jeden niewizualny komponent. Nie potrzebujesz serwera WWW, osadzonej przeglądarki ani frameworka REST.

Komponent

TsgcHTTP_OAuth2_Client, zadeklarowany w module sgcHTTP i tworzony w kodzie, tak jak robi to każde demo.

Typ grantu

OAuth2Options.GrantType := auth2CodePKCE. To jedno przypisanie włącza PKCE.

Edycja

Standard, Professional, Enterprise i All-Access. Klient nie jest funkcją Enterprise, serwer już tak.

Platformy

Windows, macOS, Linux, iOS i Android. Komponent otwiera tę przeglądarkę, którą udostępnia dana platforma.

Co naprawdę robi przepływ PKCE

PKCE istnieje, ponieważ aplikacja desktopowa nie potrafi utrzymać sekretu. Zastępuje sekret wartością, o której klient dowodzi, że znał ją przed rozpoczęciem przepływu.

1. Wygeneruj code verifier

Losowy ciąg o wysokiej entropii. sgcWebSockets prosi platformowy CSPRNG o 32 bajty i koduje je w Base64URL, co daje 43-znakowy verifier wymagany przez RFC 7636.

2. Wyprowadź code challenge

SHA-256 z verifiera, zakodowane w Base64URL. To challenge wędruje w żądaniu autoryzacji, więc ktoś podsłuchujący przekierowanie nigdy nie zobaczy verifiera.

3. Otwórz przeglądarkę

Komponent buduje URL autoryzacji z client_id, redirect_uri, scope, state, code_challenge i code_challenge_method=S256, a następnie uruchamia systemową przeglądarkę.

4. Użytkownik się loguje

Zgoda jest udzielana w przeglądarce, w domenie samego dostawcy, z istniejącą sesją użytkownika, jego menedżerem haseł i urządzeniem drugiego składnika. Twoja aplikacja nigdy nie widzi hasła.

5. Przekierowanie wraca

Dostawca przekierowuje na twój redirect_uri, niosąc code i state. Na desktopie ten URI jest adresem loopback, a komponent już na nim nasłuchuje.

6. Wymień kod

Komponent wysyła metodą POST kod wraz z pierwotnym code_verifier do endpointu tokenu. Dostawca ponownie wylicza SHA-256 i porównuje. Jeśli się zgadza, dostajesz token dostępu.

Dlaczego verifier ma znaczenie

Kod autoryzacyjny przez te kilka sekund swojego życia jest wartością na okaziciela. Wszystko, co może obserwować przekierowanie, złośliwa aplikacja zarejestrowana na tym samym własnym schemacie URI, proxy, współdzielony log, może go ukraść. Bez PKCE taki skradziony kod wystarczy, aby wygenerować token.

Z PKCE endpoint tokenu odrzuca kod, jeśli wywołujący nie przedstawi także verifiera, którego skrót SHA-256 pasuje do challenge wysłanego na początku. Atakujący widział tylko skrót, więc skradziony kod jest bezwartościowy.

Nic z tego nie musisz pisać sam. Ustaw GrantType na auth2CodePKCE, a komponent wykona za ciebie kroki 1, 2, 3, 5 i 6. Dalej znajdziesz kod, który to uruchamia, oraz dwie decyzje, które musisz podjąć sam: redirect URI i miejsce, w którym mieszka refresh token.

na łączu
# 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

Zaloguj użytkownika w około dwudziestu liniach

Utwórz komponent, wybierz auth2CodePKCE, wskaż mu dwa endpointy dostawcy, podepnij OnAfterAccessToken, wywołaj Start. Otwiera się przeglądarka, użytkownik wyraża zgodę, a zdarzenie zgłasza się z tokenem.

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

Dokąd trafia przekierowanie w aplikacji desktopowej

To ta część, która nie ma oczywistej odpowiedzi, gdy przychodzisz od webowego OAuth2, i ta, którą większość pierwszych prób robi źle.

Loopback, a nie publiczny URL

Aplikacja desktopowa nie ma domeny, na którą można przekierować. Przyjętym rozwiązaniem, i tym, które implementuje ten komponent, jest przekierowanie na loopback: aplikacja uruchamia maleńki nasłuch HTTP na 127.0.0.1, rejestruje ten adres jako redirect URI i wyłącza nasłuch, gdy tylko przyjdzie kod.

LocalServerOptions.IP ma domyślnie wartość 127.0.0.1, a LocalServerOptions.Port domyślnie 8080. W aplikacji desktopowej przeznaczonej do dystrybucji ustaw zamiast tego Port := 0: system operacyjny przydziela wolny port efemeryczny, komponent umieszcza ten port w wysyłanym redirect URI, a dwie kopie twojej aplikacji na tej samej maszynie nigdy się nie zderzą.

Jeśli dostawca wymaga dokładnej zarejestrowanej ścieżki, a nie samego hosta i portu, ustaw LocalServerOptions.RedirectURL na zarejestrowaną wartość. Ten ciąg nadpisze wtedy wartość wyliczoną. Stała ścieżka oznacza stały port, więc zarejestruj także port i zrezygnuj ze sztuczki z Port := 0.

Nasłuch działa tylko wtedy, gdy przepływ czeka. Nigdy nie jest uruchamiany dla auth2ClientCredentials, auth2ResourceOwnerPassword ani auth2DeviceCode, które w ogóle nie potrzebują przekierowania.

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;

Odczytaj token, a potem wykorzystaj go w praktyce

Po zgłoszeniu OnAfterAccessToken te same wartości pozostają dostępne jako właściwości tylko do odczytu, a komponent może przekazać je twoim klientom HTTP i WebSocket bez dotykania przez ciebie jakiegokolwiek nagłówka.

Właściwości i automatyczne nagłówki Bearer

Parametry zdarzenia są wygodne, ale nie są jedyną kopią. AccessToken, TokenType, CurrentExpiresIn i CurrentRefreshToken przechowują te same wartości tak długo, jak żyje komponent, więc procedura obsługi w innym miejscu twojego kodu może je odczytać bez przekazywania ich przez kolejne wywołania.

RawParams to nietknięte ciało JSON z endpointu tokenu. Gdy dostawca zwraca coś spoza standardowego zestawu, na przykład id_token dla OpenID Connect, wyciągnij to właśnie stamtąd. Komponent nie dekoduje za ciebie tokenu ID.

Aby każde żądanie automatycznie niosło token, przypisz komponent OAuth2 do Authentication.Token.OAuth w TsgcHTTP1Client, TsgcHTTP2Client lub TsgcWebSocketClient. Klient wysyła w twoim imieniu Authorization: Bearer <token>, używając tego token_type, który zwrócił dostawca.

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;

Odświeżanie, aby przeglądarka nigdy nie otworzyła się dwa razy

Token dostępu żyje minuty. Refresh token żyje tygodnie lub miesiące. To zachowanie tego drugiego zamienia logowanie w sesję.

Dwa różne problemy

Wewnątrz jednego uruchomienia aplikacji nie ma nic do zrobienia. Gdy endpoint tokenu zwraca zarówno refresh token, jak i expires_in, komponent uzbraja wewnętrzny zegar na mniej więcej połowę tego czasu życia i po jego zadziałaniu wysyła grant_type=refresh_token, na długo przed śmiercią tokenu dostępu. OnAfterRefreshToken zgłasza się z nową parą, a OnErrorRefreshToken zgłasza się, jeśli dostawca ją odrzuci. Zostaw parametr Handled zdarzenia OnAfterAccessToken w spokoju: ustawienie go na True mówi komponentowi, że przejmujesz kontrolę, a wtedy ani nie zapisze on refresh tokena, ani nie uzbroi tego zegara.

Pomiędzy uruchomieniami to już twój problem, bo tylko ty wiesz, gdzie na maszynach twoich użytkowników wolno zapisać sekret. Zachowaj refresh token, a przy następnym starcie całkowicie pomiń Start i wywołaj Refresh z zapisaną wartością. Żadna przeglądarka się nie otwiera, a użytkownik jest zalogowany, zanim twój główny formularz zostanie narysowany.

Dostawcy, którzy rotują refresh tokeny, przekazują ci nowy przy każdym odnowieniu, więc nadpisuj to, co zapisałeś, przy każdym OnAfterRefreshToken. Gdy zapisany token zostanie w końcu odrzucony, wróć do Start i pozwól użytkownikowi zalogować się ponownie.

Użyj Revoke, aby poprawnie wylogować użytkownika, i Introspect, aby zapytać dostawcę, czy token nadal żyje. Oba wymagają ustawienia odpowiedniego endpointu w 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');

Przechowywanie tokenów bez zostawiania ich byle gdzie

sgcWebSockets celowo nie dostarcza sejfu na tokeny. To, gdzie wolno zapisać poświadczenie, jest decyzją dotyczącą twoich użytkowników i twojego wdrożenia, więc biblioteka przekazuje ci token i na tym kończy.

Trzymaj token dostępu wyłącznie w pamięci

Wygasa w ciągu minut, a refresh token zawsze może wygenerować kolejny. Nie ma powodu, aby zapisywać go na dysku, i są wszelkie powody, aby tego nie robić.

Szyfruj refresh token osobno dla każdego użytkownika

W systemie Windows DPAPI (CryptProtectData) wiąże szyfrogram z kontem Windows, więc skopiowany plik jest bezużyteczny na innej maszynie. macOS ma Keychain, a nowoczesne środowiska graficzne Linuksa mają Secret Service.

Nigdy nie dostarczaj client secret, na którym polegasz

Wszystko, co znajduje się w dystrybuowanym pliku wykonywalnym, jest publiczne. Na tym opiera się całe PKCE. Jeśli twój dostawca wydaje sekret dla klienta desktopowego, traktuj go jak identyfikator, a nie jak zabezpieczenie.

Traktuj plik jak poświadczenie

Dane aplikacji przypisane do użytkownika, nie Program Files, nie obok pliku wykonywalnego, nie współdzielona ścieżka sieciowa i nie zwykły plik INI wrzucony do systemu kontroli wersji.

Usuwaj przy wylogowaniu

Wywołaj Revoke, aby dostawca unieważnił token, a potem usuń zapisaną kopię. Unieważniony token pozostawiony na dysku nadal jest uchybieniem w audycie.

Trzymaj sekrety poza logiem

HTTPClientOptions.LogOptions zapisuje ruch do serwera autoryzacji. Jest nieoceniony, gdy dopracowujesz przepływ, i jest plikiem pełnym tokenów. Wyłącz go, zanim wydasz aplikację.

Czego wymaga który dostawca

Każdy dostawca OAuth 2.0 prosi o tę samą garść ustawień: dwa endpointy, client id, listę scope i zarejestrowane przekierowanie. Google i Microsoft mają dodatkowo gotowe komponenty, które wypełniają endpointy i zwracają profil użytkownika.

Google i Microsoft, w jednym wywołaniu

TsgcHTTP_OAuth2_Client_Google i TsgcHTTP_OAuth2_Client_Microsoft dziedziczą po tym samym komponencie bazowym i z góry wypełniają endpointy. Ich metoda Authenticate jest blokująca: przeprowadza cały przepływ, czeka na powrót z przeglądarki i zwraca obiekt danych z Authenticated oraz wypełnionym UserProfile.

To najkrótsza możliwa droga do odpowiedzi na pytanie „kim jest ten użytkownik”. TsgcOAuth2_Google_Data.UserProfile niesie _Name, Given_Name, Family_Name, Id, Locale i Picture. TsgcOAuth2_Microsoft_Data.UserProfile niesie DisplayName, GivenName, Surname, Mail, JobTitle, OfficeLocation i więcej. Metoda Authenticate Microsoftu przyjmuje najpierw identyfikator dzierżawy.

Dla każdego innego dostawcy użyj bazowego TsgcHTTP_OAuth2_Client i skopiuj dwa adresy URL z jego dokumentacji. Poza tym nie zostaje już nic specyficznego dla dostawcy.

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;
Dostawca Komponent Typ grantu Przekierowanie Client secret
Google TsgcHTTP_OAuth2_Client_Google lub klient bazowy auth2CodePKCE Loopback, Port := 0 Wydawany dla klientów desktopowych, ustaw go, jeśli go masz
Microsoft Entra ID TsgcHTTP_OAuth2_Client_Microsoft lub klient bazowy auth2CodePKCE Loopback, zarejestrowany jako platforma mobilna lub desktopowa Nieużywany przez klienta publicznego, zostaw puste
Auth0, Okta, Keycloak, AWS Cognito TsgcHTTP_OAuth2_Client auth2CodePKCE Loopback, zarejestrowany w aplikacji Zależy od tego, czy aplikacja jest publiczna, czy poufna
Zadania w tle i usługi TsgcHTTP_OAuth2_Client auth2ClientCredentials Brak, przeglądarka nie bierze udziału Wymagany i bezpieczny, ponieważ nic nie jest dystrybuowane
Kioski, telewizory, urządzenia bez ekranu TsgcHTTP_OAuth2_Client auth2DeviceCode (RFC 8628) Brak, użytkownik kończy na telefonie Zwykle niewymagany

Logowanie w celu wysyłania poczty: OAuth 2.0 i XOAUTH2

Gmail i Microsoft 365 przestały akceptować hasła przez SMTP, IMAP i POP. Zastępuje je ten sam token dostępu, który właśnie uzyskałeś, przedstawiony przez mechanizm SASL XOAUTH2.

Token pochodzi stąd, krok SASL pochodzi z sgcIndy

Uzyskanie tokenu to dokładnie ten przepływ, który opisano powyżej: auth2CodePKCE, przekierowanie na loopback i scope pocztowy taki jak https://mail.google.com/ w AuthorizationServerOptions.Scope. Przypadek poczty niczego nie zmienia po stronie OAuth2.

Przedstawienie go to druga połowa. sgcIndy dostarcza TIdSASLXOAUTH2 w module IdSASLXOAUTH2. Dodaj go do TIdSMTP.SASLMechanisms, ustaw AuthType := satSASL i podaj nazwę użytkownika oraz token dostępu w jego zdarzeniu OnAuthenticate. Ten sam mechanizm działa dla TIdIMAP4 i TIdPOP3.

Rozdziel te dwa komponenty w myślach: klient OAuth2 wie, jak uzyskać i odnowić token, a mechanizm SASL wie, jak go przedstawić. Żaden z nich nie musi wiedzieć o drugim.

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;

Potrzebujesz klienta, czy także serwera?

Wszystko powyżej dotyczy strony klienta. Druga połowa jest potrzebna tylko wtedy, gdy to ty wydajesz tokeny.

Tylko klient

Jeśli logujesz użytkowników do cudzego dostawcy tożsamości, Google, Microsoft, Auth0, Okta, Keycloak, AWS Cognito, własnego firmowego SSO, nie potrzebujesz nic poza TsgcHTTP_OAuth2_Client. Ten komponent jest wkompilowany w edycję Standard i każdą edycję wyższą. Jest też dostępny osobno w samodzielnym pakiecie sgcAuth, razem z potrzebnym mu środowiskiem uruchomieniowym.

To najczęstszy przypadek i wypełnia on całą tę stronę aż do tego miejsca.

Kiedy potrzebujesz także serwera

Serwerowa połowa jest potrzebna tylko wtedy, gdy to twoja własna aplikacja jest serwerem autoryzacji: to ty wydajesz client id, ty hostujesz stronę logowania, ty tworzysz i unieważniasz tokeny dostępu, którym potem ufa twoje API. To TsgcHTTP_OAuth2_Server, podłączony do TsgcWebSocketHTTPServer, i jest to komponent Enterprise.

Domyślnie weryfikuje PKCE. OAuth2Options.PKCE ma zaraz po instalacji wartość True, więc klient, który wysyła challenge, musi przedstawić pasujący verifier, a ten, który tego nie zrobi, zostaje odrzucony. Rejestruj aplikacje klienckie przez Apps.AddApp, uwierzytelniaj użytkowników w OnOAuth2Authentication i odtwarzaj tokeny po restarcie przez AddToken.

Ten sam poziom Enterprise zawiera TsgcHTTP_JWT_Server do walidacji tokenów bearer JWT na twoich endpointach oraz TsgcWSAPIServer_WebAuthn dla passkeys. Odpowiadające im klienty, TsgcHTTP_OAuth2_Client i TsgcHTTP_JWT_Client, są dostępne od Standard w górę. Klient i serwer leżą na różnych poziomach edycji, co warto sprawdzić, zanim zaplanujesz cokolwiek wokół któregokolwiek z nich.

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;

Co zwykle idzie nie tak za pierwszym razem

Prawie każda nieudana pierwsza próba OAuth2 na desktopie to jeden z tych sześciu przypadków.

redirect_uri_mismatch

URI wysyłany przez komponent musi zgadzać się z tym, co zarejestrowałeś, znak w znak, łącznie z końcowym ukośnikiem i portem. Jeśli zarejestrowałeś stały URI, ustaw LocalServerOptions.RedirectURL dokładnie na ten ciąg, zamiast polegać na wyliczonym. Jeśli dostawca dopuszcza dowolny port loopback, użyj Port := 0 i zarejestruj sam host.

Przeglądarka się otwiera i nic nie wraca

Coś zajmuje port albo reguła zapory blokuje nasłuch na loopbacku. Ustaw Port := 0 i sprawdź, czy poprzednie uruchomienie przepływu zostało zakończone przez Stop, a nie pozostawione w stanie nasłuchu.

invalid_grant przy wymianie tokenu

Kody autoryzacyjne są jednorazowe i krótko żyją. Debugowanie z punktem przerwania między przekierowaniem a wymianą doprowadzi do wygaśnięcia kodu. Zamiast zgadywać, odczytaj przyczynę błędu z OnErrorAccessToken, które przekazuje ci error i error_description od samego dostawcy.

Nie zwrócono refresh tokena

Dostawcy wydają go tylko wtedy, gdy o niego poprosisz. Google chce access_type=offline, Microsoft chce scope offline_access. Dodaj ten scope do AuthorizationServerOptions.Scope albo dopisz parametr zapytania, edytując parametr URL w OnBeforeAuthorizeCode.

TLS zawodzi na Linuksie lub na urządzeniach mobilnych

Wymiana tokenu to żądanie POST przez HTTPS, więc potrzebuje działającego zaplecza TLS. Wybiera je HTTPClientOptions.TLSOptions.IOHandler: iohOpenSSL, iohSChannel na Windows bez żadnych bibliotek DLL do wdrożenia albo natywne handlery iohAndroidTLS i iohAppleTLS w edycji Enterprise.

Chciałeś mieć stronę logowania wewnątrz aplikacji

Obsłuż OnBeforeAuthorizeCode, ustaw Handled := True i przejdź własnym TsgcWebView2 lub TWebBrowser pod przekazany URL. Nasłuch na loopbacku nadal przechwyci przekierowanie. Pamiętaj, że kilku dostawców odmawia teraz wyświetlania swojego ekranu zgody wewnątrz osadzonej przeglądarki.

Pytania o Delphi, OAuth2 i PKCE

Pytania, których deweloperzy naprawdę szukają, zanim zaczną.

Upuść na formularz TsgcHTTP_OAuth2_Client, ustaw OAuth2Options.GrantType := auth2CodePKCE, uzupełnij OAuth2Options.ClientId, AuthorizationServerOptions.AuthURL, AuthorizationServerOptions.TokenURL i AuthorizationServerOptions.Scope, ustaw LocalServerOptions.IP na 127.0.0.1, a LocalServerOptions.Port na 0, a następnie wywołaj Start. Komponent generuje wartości PKCE, otwiera przeglądarkę, przechwytuje przekierowanie na nasłuchu loopback, wymienia kod i zgłasza OnAfterAccessToken z tokenem.
Nie musisz. Gdy GrantType ma wartość auth2CodePKCE, TsgcHTTP_OAuth2_Client pobiera 32 bajty z platformowego kryptograficznego źródła losowości, koduje je w Base64URL do 43-znakowego code verifiera, ustawia code challenge na zakodowany w Base64URL skrót SHA-256 tego verifiera i ustala code_challenge_method na S256. Verifier jest przechowywany prywatnie wewnątrz komponentu i odtwarzany przy wymianie tokenu, więc nigdy nie pojawia się w przekierowaniu. Jeśli chcesz zbudować tę parę ręcznie do innego celu, te same prymitywy są publiczne: sgcRandomBytes w module sgcCrypto_Random oraz GetHashSHA256 i EncodeBase64URL w module sgcBase_Helpers.
Adresu loopback. TsgcHTTP_OAuth2_Client uruchamia mały nasłuch HTTP na LocalServerOptions.IP i LocalServerOptions.Port tylko na czas trwania przepływu, a wysyłany redirect URI jest budowany z tych wartości. Domyślne wartości to 127.0.0.1 i port 8080. W aplikacji przeznaczonej do dystrybucji ustaw Port na 0, aby system operacyjny wybrał wolny port efemeryczny i aby dwie instancje nigdy nie walczyły o ten sam. Jeśli dostawca wymaga dokładnego zarejestrowanego URI, wpisz ten ciąg w LocalServerOptions.RedirectURL, a nadpisze on wartość wyliczoną.
To zależy od dostawcy. PKCE istnieje właśnie dlatego, że dystrybuowana aplikacja desktopowa nie potrafi utrzymać sekretu, więc klient publiczny zwykle nie wysyła żadnego sekretu i zostawia OAuth2Options.ClientSecret puste. Niektórzy dostawcy nadal wydają go dla klientów desktopowych i oczekują go w żądaniu tokenu. Ustaw go wtedy, gdy tak jest, ale traktuj go jak identyfikator, a nie jak zabezpieczenie, ponieważ wszystko, co znajduje się w dystrybuowanym pliku wykonywalnym, da się wydobyć.
Zachowaj refresh token, a przy następnym uruchomieniu wywołaj z nim Refresh zamiast Start. Odczytaj go z parametru Refresh_Token zdarzenia OnAfterAccessToken albo później z właściwości CurrentRefreshToken. Nadpisuj zapisaną kopię przy każdym OnAfterRefreshToken, ponieważ dostawcy rotujący refresh tokeny unieważniają stary. W obrębie jednego uruchomienia nie trzeba robić nic: komponent uzbraja zegar na podstawie wartości expires_in i sam odnawia token dostępu.
Token dostępu trzymaj wyłącznie w pamięci, wygasa w ciągu minut i zawsze można wygenerować kolejny. Refresh token zapisuj zaszyfrowany i przypisany do bieżącego użytkownika, na przykład przez DPAPI na Windows, Keychain na macOS lub Secret Service na Linuksie, w danych aplikacji przypisanych do użytkownika, a nie obok pliku wykonywalnego. sgcWebSockets celowo nie dostarcza własnego sejfu na tokeny: przekazuje ci token i zostawia decyzję o przechowywaniu tobie. Pamiętaj, aby przed wydaniem wyłączyć HTTPClientOptions.LogOptions, bo ten log zawiera tokeny.
Uzyskaj token dostępu przepływem opisanym na tej stronie, prosząc o pocztowy scope dostawcy, taki jak https://mail.google.com/, a następnie przedstaw go przez SASL XOAUTH2. sgcIndy dostarcza TIdSASLXOAUTH2 w module IdSASLXOAUTH2. Dodaj go do TIdSMTP.SASLMechanisms, ustaw AuthType := satSASL i zwróć nazwę użytkownika oraz token dostępu w jego zdarzeniu OnAuthenticate. Ten sam mechanizm uwierzytelnia TIdIMAP4 i TIdPOP3.
Klient OAuth2 i klient JWT są wkompilowane w edycję Standard i każdą edycję wyższą, więc Standard, Professional, Enterprise i All-Access zawierają je wszystkie. Serwer OAuth2, serwer JWT i serwer WebAuthn to komponenty Enterprise i nie ma ich w kompilacjach Standard ani Professional. Te dwa komponenty klienckie są też sprzedawane osobno jako samodzielny pakiet sgcAuth, razem z potrzebnym im środowiskiem uruchomieniowym.
Tylko wtedy, gdy to ty wydajesz tokeny. Logowanie użytkowników do Google, Microsoft, Auth0, Okta, Keycloak, AWS Cognito lub firmowego dostawcy tożsamości wymaga komponentu klienckiego i niczego więcej. TsgcHTTP_OAuth2_Server jest potrzebny wtedy, gdy twoja własna aplikacja rejestruje client id, hostuje stronę logowania i tworzy tokeny, którym ufa twoje API. Domyślnie waliduje PKCE przez OAuth2Options.PKCE, rejestruje aplikacje przez Apps.AddApp i podłącza się do TsgcWebSocketHTTPServer przez Authentication.OAuth.OAuth2.
Tak. Obsłuż OnBeforeAuthorizeCode, które otrzymuje w pełni zbudowany URL autoryzacji jako parametr var, ustaw Handled := True, aby komponent nie uruchamiał systemowej przeglądarki, i przejdź osadzoną kontrolką, taką jak TsgcWebView2, pod ten URL. Nasłuch na loopbacku nadal odbiera przekierowanie, a przepływ kończy się normalnie. Miej na uwadze, że kilku dostawców blokuje teraz swój ekran zgody w osadzonych przeglądarkach, i dlatego domyślna jest przeglądarka systemowa.
Tak. TsgcHTTP_OAuth2_Client kompiluje się dla Windows, macOS, Linux, iOS i Android, w VCL, FireMonkey oraz Lazarus / FPC, od Delphi 7 do Delphi 13 i odpowiadających im wersji C++ Builder. Otwarcie przeglądarki korzysta z tego, co udostępnia platforma. Jedynym wyborem zależnym od platformy jest zaplecze TLS do wymiany tokenu, wybierane przez HTTPClientOptions.TLSOptions.IOHandler.

Dokumentacja referencyjna, demo i dokumentacja

Dokumentacja referencyjna komponentu, gotowy do uruchomienia projekt demo oraz dokumenty techniczne, które sięgają głębiej niż ta strona.

Pomoc online, TsgcHTTP_OAuth2_Client Każda właściwość, metoda i zdarzenie komponentu klienckiego, wraz z tematem Authorization Code + PKCE.
Pomoc online, Authorization Code z PKCE Temat o typie grantu: co robi PKCE, tabela konfiguracji i zalecenie dotyczące losowego portu.
Projekt demo, Demos\20.HTTP_Protocol\02.OAuth2_Authentication Projekty klienta i serwera z działającymi ustawieniami dla Gmaila, Google Pub/Sub, Azure AD, AWS Cognito, Dropboksa i Auth0, plus wariant z osadzoną przeglądarką.
Dokument techniczny, OAuth2 Client (PDF) Funkcje, szybki start, każdy typ grantu oraz przykłady kodu dla Delphi, C++ Builder i .NET.
Dokument techniczny, OAuth2 Server (PDF) Komponent serwera autoryzacji z edycji Enterprise: endpointy, rejestracja aplikacji, walidacja PKCE i cykl życia tokenu.
Podręcznik użytkownika (PDF) Kompletny podręcznik obejmujący każdy komponent w bibliotece.

Specyfikacje, które realizuje ten przepływ

Źródła pierwotne, gdy trzeba rozstrzygnąć spór z działem wsparcia dostawcy.

Komponenty i artykuły stojące za tą stroną

Strony komponentów zawierają pełną listę funkcji, a artykuły omawiają przypadki, których ta strona jedynie dotyka.

Komponent OAuth2 Client

Pełny zestaw właściwości, metod i zdarzeń TsgcHTTP_OAuth2_Client, w tym Device Code i DPoP.

Dowiedz się więcej →

Komponent OAuth2 Server

Serwer autoryzacji z edycji Enterprise: twoje własne endpointy authorize, token, revoke i introspect.

Dowiedz się więcej →

sgcAuth

Komponenty klienckie OAuth2 i JWT jako samodzielny pakiet, z dołączonym potrzebnym im środowiskiem uruchomieniowym.

Dowiedz się więcej →

Komponent JWT Client

Podpisuj i dołączaj tokeny JSON Web Token, samodzielnie lub jako źródło Bearer dla twoich klientów HTTP i WebSocket.

Dowiedz się więcej →

Delphi PKCE OAuth2

Pierwotny artykuł premierowy wprowadzający obsługę PKCE w komponentach klienta i serwera.

Czytaj wpis →

sgcIndy XOAuth2

Wysyłanie poczty z tokenem dostępu OAuth 2.0 przez SMTP, IMAP i POP za pomocą mechanizmu SASL XOAUTH2.

Czytaj wpis →

OAuth2 Client Credentials

Wariant bez użytkownika, dla usług działających w tle i dostępu do API w modelu maszyna do maszyny.

Czytaj wpis →

OAuth2 DPoP w Delphi

Wiązanie tokenu dostępu z parą kluczy, dla dostawców wymagających dowodu posiadania zgodnie z RFC 9449.

Czytaj wpis →

AWS Cognito i OAuth2

Przerobiona krok po kroku konfiguracja z prawdziwym dostawcą tożsamości, endpoint po endpoincie.

Czytaj wpis →

OAuth2 Server: rejestracja aplikacji

Rejestrowanie aplikacji klienckich, redirect URI i dozwolonych typów grantów na twoim własnym serwerze autoryzacji.

Czytaj wpis →

Autoryzacja u zewnętrznych dostawców

Pozwolenie twojemu własnemu serwerowi na delegowanie logowania do Google, Microsoftu lub dowolnego innego zewnętrznego dostawcy tożsamości.

Czytaj wpis →

WebAuthn i passkeys

Alternatywa bez haseł, gdy wolisz w ogóle nie przekazywać żadnego tokenu.

Dowiedz się więcej →

Ta strona to jedna z pozycji w serii przypadki użycia Delphi, gdzie każdy przewodnik prowadzi pojedyncze zadanie od początku do końca. Pozostałe to jak dotąd wywołanie LLM z Delphi i połączenie dwóch aplikacji Delphi przez WebRTC.

Zaloguj swojego pierwszego użytkownika już dziś

Pobierz bezpłatną wersję próbną, otwórz demo OAuth2, wskaż mu swojego dostawcę i zobacz, jak dopełnia się obieg przez przeglądarkę.