Last active
November 1, 2023 08:59
-
-
Save SteveNew/ebcaf649a283afd65ef896c3fa898f7d to your computer and use it in GitHub Desktop.
Delphi Google IAPClient
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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