Delphi - Komponente - TNetHTTPClient und Sonderzeichen

Beim Zugriff auf die Keycloak-API zum Abruf eines Tokens kommt die Fehlermeldung 401 Not Authorized wenn das Passwort ein Sonderzeichen enthält.

In einer Python-Testumgebung kann der Token ohne Probleme abgerufen werden.

Mit Wireshark kann man sich die erzeugten Abfragen anzeigen (damit man in Wireshark die unverschlüsselten Daten angezeigt bekommt muss man zum Debuggen die URL von https:// auf http:// umstellen):

Python erzeugt:

POST /keycloak/rest/v1/token/generate HTTP/1.1
Host: demo.wlsoft.de
User-Agent: python-requests/2.22.0
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Authorization: Basic xxxx:yyyyyyyyyyyyyyyy
Content-Length: 70

grant_type=password&username=xxxxx@xxxxxx.xx&password=123456789%C3%840

Delphi erzeugt

POST /keycloak/rest/v1/token/generate HTTP/1.1
Host: demo.wlsoft.de
User-Agent: Embarcadero URI Client/1.0
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded; charset=utf-8
Authorization: Basic xxxx:yyyyyyyyyyyyyyyy
Content-Length: 70

grant_type=password&username=xxxxx@xxxxxx.xx&password=123456789%C3%840

Der Unterschied steckt im Content-Type

Python: Content-Type: application/x-www-form-urlencoded;
Delphi: Content-Type: application/x-www-form-urlencoded; charset=utf-8

Der falsche Eintrag in Delphi wird durch die Funktion THTTPClient.Cre­ateFormFromStrin­gs() erzeugt:

Deshalb darf bei Abruf eines Token nur eine Post-Anweisung aufgerufen werden, die CreateFormFrom­Strings() nicht aufruft.

Das wäre zum Beispiel ein solcher Aufruf:

function TNetHTTPClient.Post(
  const AURL: string;
  const ASource, AResponseContent: TStream;
  const AHeaders: TNetHeaders): IHTTPResponse;
type
  THCloudZugangsdaten = record
    User: string;
    Pass: string;
    Token:string;
    TokenResponseCode: integer;
    TokenResponseText: string;
  end;

function getToken(var aZugangsdaten: TZugangsdaten): boolean;
var
  s, aURL: string;
  j: TJSONObject;
  aSource: TStringStream;
  m: TMemoryStream;
  sl: TStringlist;
  Response: IHTTPResponse;
  HTTP: TNetHTTPClient;
  aHeaders: TNetHeaders;
begin
  Result := False;
  sl := TStringlist.Create;
  aSource := TStringStream.Create(
    'grant_type=password' +
    '&username=' + EncodeURIComponent(aZugangsdaten.User) +
    '&password=' + EncodeURIComponent(aZugangsdaten.Pass));
  m := TMemoryStream.Create;
  HTTP := TNetHTTPClient.Create(nil);
  try
    try
      HTTP.ContentType := cContentType_x_www_form_urlencoded;
      HTTP.AcceptEncoding := 'gzip, deflate';
      HTTP.CustomHeaders['Accept'] := '*/*';
      HTTP.CustomHeaders['Authorization'] := 'Basic xxxx:yyyyyyyyyyyyyyyy';
      aURL := 'https://demo.wlsoft.de/keycloak/rest/v1/token/generate';
      Response := HTTP.Post(aURL, aSource, m, aHeaders);
      if Response.StatusCode = 200 then begin
        m.Position := 0;
        sl.LoadFromStream(m);
        s := sl.Text;
        j := TJSONObject.ParseJSONValue(s) as TJSONObject;
        try
          if Assigned(j) then begin
            if j.TryGetValue('access_token', aZugangsdaten.Token) then begin
            end;
          end;
        finally
          FreeAndNil(j);
        end;
      end;
      aZugangsdaten.TokenResponseCode := Response.StatusCode;
      aZugangsdaten.TokenResponseText := Response.StatusText;
      Result := not aZugangsdaten.Token.IsEmpty;
    except
      on E: Exception do begin
        aZugangsdaten.TokenResponseCode := Response.StatusCode;
        aZugangsdaten.TokenResponseText := E.Message;
        // kein raise;
      end;
    end;
  finally
    m.Free;
    aSource.Free;
    FreeAndNil(sl);
    FreeNetHttp(HTTP);
  end;
end;