Stack Exchange
Google get access-token for service account using JWT and schannel
I need to get google OA2.0 access token for service account using JWT and schannel (not openssl - it is working, but I have to use schannel).
Using win10,
Delphi Alexandria 11.3 Patch 1, Indy 10...
Libraries for SChannel: https://github.com/tothpaul/Delphi/tree/master/Indy.SChannel
My code(not working):
function TForm1.IdGetAccessTokenSChannel(GServiceAccount, GKey, GScope: string;
  var GAccessToken: string; var GAccessTokenExpiry: TDateTime;
  var ErrMsg: string): boolean;
var
  LToken: TJWT;
  LKey: TJWK;
  LJWS: TJWS;
  LDT: TDateTime;
  LCompactedToken: TJOSEBytes;
  IdHTTP: TIdHTTP;
  IdSSLIOHandlerSocketSChannel: TIdSSLIOHandlerSocketSChannel;
  InSS, OutSS: TStringStream;
  Response: string;
  LJSONResponse: TJSONObject;
  Expiry: integer;
  JSONstr: string;
begin
 
  Result := false;
  GAccessToken := '';
  GAccessTokenExpiry := 0.0;
  ErrMsg := '';
 
  LToken := TJWT.Create;
  try
    LToken.Claims.SetClaimOfType('iss', GServiceAccount);
    LToken.Claims.SetClaimOfType('scope', GScope);
    LToken.Claims.Audience := 'https://oauth2.googleapis.com/token';
    LToken.Claims.IssuedAt := Now;
    LDT := Now;
    LDT := IncMinute(LDT, 30);
    LToken.Claims.Expiration := LDT;
    LKey := TJWK.Create(GKey);
    try
      LJWS := TJWS.Create(LToken);
      try
        LJWS.Sign(LKey, TJOSEAlgorithmId.RS256);
        LCompactedToken := LJWS.CompactToken;
      finally
        LJWS.Free;
      end;
    finally
      LKey.Free;
    end;
  finally
    LToken.Free;
  end;
 
  IdHTTP := TIdHTTP.Create(nil);
  try
    // Set up SSL
    IdSSLIOHandlerSocketSChannel := TIdSSLIOHandlerSocketSChannel.Create(IdHTTP);
    IdHTTP.IOHandler := IdSSLIOHandlerSocketSChannel;
 
    InSS := TStringStream.Create(
      'grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion='+
      LCompactedToken.AsString// , TEncoding.UTF8
    );
    OutSS := TStringStream.Create;
 
    try
      IdHTTP.Request.ContentType := 'application/x-www-form-urlencoded';
      try
        InSS.Position := 0;
        IdHTTP.Post(
          'https://oauth2.googleapis.com/token',
          //'https://www.googleapis.com/oauth2/v4/token',
          InSS, OutSS
        );
        Response := OutSS.DataString;
        JSONstr := Response;
        LJSONResponse := TJSONObject.ParseJSONValue(JSONstr) as TJSONObject;
        if LJSONResponse <> nil then
        begin
          try
            try
              if (LJSONResponse.Values['access_token'].Value <> '') and
                 (LJSONResponse.Values['expires_in'].Value <> '') then
              begin
                if TryStrToInt(LJSONResponse.Values['expires_in'].Value, Expiry) then
                begin
                  if Expiry > 1 then
                  begin
                    GAccessToken := LJSONResponse.Values['access_token'].Value;
                    GAccessTokenExpiry := IncSecond(Now, Expiry);
                    Result := true;
                  end;
                end;
              end;
            except
              on E: Exception do
                ErrMsg := 'JSON read error: '+E.Message;
            end;
          finally
            LJSONResponse.Free;
          end;
        end
        else
          ErrMsg := 'returned invalid JSON';
      except
        on E: Exception do
          ErrMsg := 'POST call to googleapis error: '+E.Message;
      end;
    finally
      InSS.Free;
      OutSS.Free;
    end;
  finally
    IdHTTP.Free;
  end;
end;
 

I receive only this error: Read timed out...
But when I exchange this code:
    IdSSLIOHandlerSocketSChannel := TIdSSLIOHandlerSocketSChannel.Create(IdHTTP);
    IdHTTP.IOHandler := IdSSLIOHandlerSocketSChannel;  


with this (openSSL):
    IdSSLIOHandlerSocketOpenSSL := TIdSSLIOHandlerSocketOpenSSL.Create(IdHTTP);
    with IdSSLIOHandlerSocketOpenSSL.SSLOptions do
    begin
      Method := sslvTLSv1_2;
      Mode := sslmClient;
      SSLVersions := [sslvTLSv1_2];
    end;
    IdHTTP.IOHandler := IdSSLIOHandlerSocketOpenSSL;

everything starts to work.
Any other google API calls are working with SChannel, only getting access token using JWT no.
I tried also another URLs: https://www.googleapis.com/oauth2/v2(v3,v4)/token

with no success

Any help - explanation is welcome.
Thank you!
Edit 6.8.2023 - Solved (partially):
Author of Indy.Schannel wrote me:
Hi,
I've found the bug, in fact the response is read, but my component returns an invalide information to Indy which throws an exception.
you can fix the problem by changing line 1075 of Execute.SChannel, Info.Readable = 0 is acceptable, so change ">" to ">=" and voilà !
function SSLPending(SSL: THandle; AMSec: Integer): Boolean;
var
  Info: PSSLInfo absolute SSL;
begin
  Result := (Info <> nil) and (Info.Readable(AMSec) >= 0);
end;

It's happens on this server because it doesn't return a Content-Length nor use a chunked reply...so Indy has to read until the connexion is closed (Readable = 0)
Regards
Paul
So thanks Paul!!!
Unfortunately, it turned out that TJWS.Sign from delphi-jose-jwt also uses the openSSL library (RS256 required by Google) to sign with the TJOSEAlgorithmId.RS256 algorithm, so I've made some progress, but I still don't have a solution... Perhaps using BCrypt? Well, I'll keep looking.
If anyone knew?
