Skip to content

Instantly share code, notes, and snippets.

@SteveNew
Last active November 1, 2023 08:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save SteveNew/ebcaf649a283afd65ef896c3fa898f7d to your computer and use it in GitHub Desktop.
Save SteveNew/ebcaf649a283afd65ef896c3fa898f7d to your computer and use it in GitHub Desktop.
Delphi Google IAPClient
// From https://fixedbycode.blogspot.com/2023/02/iap-client-therefore-iam.html
unit FbC.IAPClient;
interface
uses
System.Net.HttpClient,
System.Net.URLClient,
System.Net.HttpClientComponent,
JOSE.Core.JWT,
JOSE.Core.JWS,
JOSE.Core.JWK,
JOSE.Core.JWA,
JOSE.Types.JSON;
type
TIAPClient = class
const
// iam_scope='https://www.googleapis.com/auth/iam';
oauth_token_uri='https://www.googleapis.com/oauth2/v4/token';
type
TServiceAccountKey = class
private
Ftype: string;
Fproject_id: string;
Fprivate_key_id: string;
Fprivate_key: string;
Fclient_email: string;
Fclient_id: string;
Fauth_uri: string;
Ftoken_uri: string;
Fauth_provider_x509_cert_url: string;
Fclient_x509_cert_url: string;
public
property &Type: string read FType write FType;
property project_id: string read Fproject_id write Fproject_id;
property private_key_id: string read Fprivate_key_id write Fprivate_key_id;
property private_key: string read Fprivate_key write Fprivate_key;
property client_email: string read Fclient_email write Fclient_email;
property client_id: string read Fclient_id write Fclient_id;
property auth_uri: string read Fauth_uri write Fauth_uri;
property token_uri: string read Ftoken_uri write Ftoken_uri;
property auth_provider_x509_cert_url: string read Fauth_provider_x509_cert_url write Fauth_provider_x509_cert_url;
property client_x509_cert_url: string read Fclient_x509_cert_url write Fclient_x509_cert_url;
end;
strict private
FLatestToken: TDateTime;
FSecBeforeExpire: Int64;
FOIDCToken: string;
FServiceAccountKey: TServiceAccountKey;
FIAPClientId: string;
FURLDomain: string;
FHTTP: TNetHTTPClient;
FHeaders: TNetHeaders;
function getIdToken: string;
public
constructor Create(const serviceAccountKeyfile, iAPClientId, uRLDomain: string);
destructor Destroy; override;
function Get(const AURLPath: string): IHTTPResponse;
function Put(const AURLPath, ASource: string): IHTTPResponse;
function Patch(const AURLPath, ASource: string): IHTTPResponse;
function Post(const AURLPath, ASource: string): IHTTPResponse;
// public for demo purposes
property IdToken: string read getIdToken;
end;
implementation
uses
System.Classes,
System.SysUtils,
System.IOUtils,
System.DateUtils,
REST.Json,
JOSE.Types.Bytes;
{ TIAPClient }
constructor TIAPClient.Create(const serviceAccountKeyfile, iAPClientId, uRLDomain: string);
var
secKey: string;
begin
inherited Create;
FHTTP := TNetHTTPClient.Create(nil);
SetLength(FHeaders, 1);
FHeaders[0].Name := 'Authorization';
FLatestToken := -1;
FSecBeforeExpire := -1;
FOIDCToken := '';
// Mimic kind of ADC
secKey := GetEnvironmentVariable('GOOGLE_APPLICATION_CREDENTIALS');
if secKey='' then
begin
{$IFDEF MSWINDOWS}
if FileExists('%APPDATA%\gcloud\application_default_credentials.json') then
secKey := '%APPDATA%\gcloud\application_default_credentials.json';
{$ELSE} // Linux and OSX
if FileExists('$HOME/.config/gcloud/application_default_credentials.json') then
secKey := '$HOME/.config/gcloud/application_default_credentials.json';
{$ENDIF}
end;
if secKey='' then
secKey := serviceAccountKeyfile;
if secKey<>'' then
FServiceAccountKey := TJson.JsonToObject<TServiceAccountKey>(TFile.ReadAllText(serviceAccountKeyfile));
Self.FIAPClientId := iAPClientId;
Self.FURLDomain := uRLDomain;
end;
destructor TIAPClient.Destroy;
begin
FreeAndNil(FHTTP);
FServiceAccountKey.Free;
inherited;
end;
function TIAPClient.Get(const AURLPath: string): IHTTPResponse;
begin
FHeaders[0].Value := 'Bearer ' + IdToken;
Result := FHTTP.Get(FURLDomain+AURLPath, nil, FHeaders);
end;
function TIAPClient.Put(const AURLPath, ASource: string): IHTTPResponse;
begin
FHeaders[0].Value := 'Bearer ' + IdToken;
var lRequestBody: TStringStream := TStringStream.Create(ASource, TEncoding.UTF8);
try
Result := FHTTP.Put(FURLDomain+AURLPath, lRequestBody, nil, FHeaders);
finally
FreeAndNil(lRequestBody);
end;
end;
function TIAPClient.Patch(const AURLPath, ASource: string): IHTTPResponse;
begin
FHeaders[0].Value := 'Bearer ' + IdToken;
var lRequestBody: TStringStream := TStringStream.Create(ASource, TEncoding.UTF8);
try
Result := FHTTP.Patch(FURLDomain+AURLPath, lRequestBody, nil, FHeaders);
finally
FreeAndNil(lRequestBody);
end;
end;
function TIAPClient.Post(const AURLPath, ASource: string): IHTTPResponse;
begin
FHeaders[0].Value := 'Bearer ' + IdToken;
var lRequestBody: TStringStream := TStringStream.Create(ASource, TEncoding.UTF8);
try
Result := FHTTP.Post(FURLDomain+AURLPath, lRequestBody, nil, FHeaders);
finally
FreeAndNil(lRequestBody);
end;
end;
function TIAPClient.getIdToken: string;
var
lToken: TJWT;
lSigner: TJWS;
lKey: TJWK;
HTTP: TNetHTTPClient;
lAssertionToken: TJOSEBytes;
lRequestBody: TStringStream;
lResponse: IHTTPResponse;
lJSONResponse: TJSONObject;
begin
if (FLatestToken = -1) or (Now >= IncSecond(FLatestToken, FSecBeforeExpire - 30)) then { 30 seconds respite }
begin
{ new token }
FLatestToken := Now;
FOIDCToken := '';
lToken := TJWT.Create;
try
lToken.Claims.Subject := FServiceAccountKey.client_email;
lToken.Claims.Issuer := FServiceAccountKey.client_email;
lToken.Claims.SetClaimOfType('target_audience', FIAPClientId);
lToken.Claims.IssuedAt := Now;
lToken.Claims.Expiration := Now + OneHour;
lToken.Claims.Audience := oauth_token_uri;
lToken.Header.KeyID := FServiceAccountKey.private_key_id;
lKey := TJWK.Create(FServiceAccountKey.private_key);
try
lSigner := TJWS.Create(LToken);
try
lSigner.SkipKeyValidation := True; // skip keys < algorithm length check
lSigner.Sign(lKey, TJOSEAlgorithmId.RS256);
lAssertionToken := LSigner.CompactToken;
lRequestBody := nil;
lJSONResponse := nil;
HTTP := TNetHTTPClient.Create(nil);
try
// pre-URL encode content
lRequestBody := TStringStream.Create
('grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion='+lAssertionToken.AsString);
HTTP.ContentType := 'application/x-www-form-urlencoded';
lResponse := HTTP.Post(oauth_token_uri, lRequestBody);
if lResponse.StatusCode = 200 then
begin
lJSONResponse := TJSONObject.ParseJSONValue(lResponse.ContentAsString) as TJSONObject;
FOIDCToken := LJSONResponse.Values['id_token'].Value;
// Since we do want only a OIDC ID Token, there is nothing other than the id_token in the response
// having specified a scope in the request would had given us an access_token and an expires_in
FSecBeforeExpire := SecsPerHour;
end;
finally
FreeAndNil(HTTP);
FreeAndNil(lJSONResponse);
FreeAndNil(lRequestBody);
end;
finally
lSigner.Free;
end;
finally
lKey.Free;
end;
finally
lToken.Free;
end;
end;
Result := FOIDCToken;
end;
end.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment