JWT認証によるアクセストークンの取得(主に署名生成)がうまくいかないのでご教示いただきたいです。

解決


USM  2024-05-15 01:46:50  No: 151477  IP: 192.*.*.*

DelphiXEの環境でJWT認証のアクセストークン取得のためのコードを作っていましたが、

//トークン取得ボタンイベント
procedure TForm4.Button4Click(Sender: TObject);
var
  Settings: TSettings;
  JWTToken: string;
begin
  Settings := LoadSettingsFromFile(ExtractFilePath(ParamStr(0)) + AFileName); // settings.jsonはあなたのJSONファイルの名前に置き換えてください
  JWTToken := GetJWTToken(Settings);
  Memo1.lines.Add(JWTToken);
end;
//JSONファイルから必要なパラメータを抽出
function TForm4.LoadSettingsFromFile(const AFileName: string): TSettings;
var
  JSON: TJSONObject;
  BoxAppSettings: TJSONObject;
  AppAuth: TJSONObject;
  Pair: TJSONPair;
  JSONStringList: TStringList;
  Value: string;
begin
    try
    JSON := TJSONObject.ParseJSONValue(TFile.ReadAllText(AFileName)) as TJSONObject;
    if JSON = nil then
      raise Exception.Create('Failed to parse JSON file');

//    ShowMessage(JSON.ToString);

    // Create a TStringList and add the JSON string to it
    JSONStringList := TStringList.Create;
    try
      JSONStringList.Text := JSON.ToString;
      // Save the JSON content to a file for inspection
      JSONStringList.SaveToFile('JSONContent.txt');
    finally
      JSONStringList.Free;
    end;

    Pair := JSON.Get('boxAppSettings');
    if Pair = nil then
      raise Exception.Create('boxAppSettings not found in JSON file');
    if not (Pair.JsonValue is TJSONObject) then
      raise Exception.Create('boxAppSettings is not a JSON object');
    BoxAppSettings := Pair.JsonValue as TJSONObject;

    Pair := BoxAppSettings.Get('clientID');
    if Pair = nil then
      raise Exception.Create('clientID not found in boxAppSettings');
    // Now you can use Pair.JsonValue.Value

    Pair := BoxAppSettings.Get('clientSecret');
    if Pair = nil then
      raise Exception.Create('clientSecret not found in boxAppSettings');
    // Now you can use Pair.JsonValue.Value

    AppAuth := BoxAppSettings.Get('appAuth').JsonValue as TJSONObject;

    Pair := AppAuth.Get('publicKeyID');
    if Pair = nil then
      raise Exception.Create('publicKeyID not found in appAuth');
    Result.boxAppSettings.appAuth.publicKeyID := Pair.JsonValue.Value;

    Pair := AppAuth.Get('privateKey');
    if Pair = nil then
      raise Exception.Create('privateKey not found in appAuth');
    Result.boxAppSettings.appAuth.privateKey := Pair.JsonValue.Value;

    Pair := AppAuth.Get('passphrase');
    if Pair = nil then
      raise Exception.Create('passphrase not found in appAuth');
    Result.boxAppSettings.appAuth.passphrase := Pair.JsonValue.Value;

    Result.boxAppSettings.clientID := BoxAppSettings.Get('clientID').JsonValue.Value;
    Result.boxAppSettings.clientSecret := BoxAppSettings.Get('clientSecret').JsonValue.Value;
    Result.boxAppSettings.appAuth.publicKeyID := AppAuth.Get('publicKeyID').JsonValue.Value;
    Result.boxAppSettings.appAuth.privateKey := AppAuth.Get('privateKey').JsonValue.Value;
    Result.boxAppSettings.appAuth.passphrase := AppAuth.Get('passphrase').JsonValue.Value;
    Result.enterpriseID := JSON.Get('enterpriseID').JsonValue.Value;
  finally
    JSON.Free;
  end;
end;

//トークンの取得
function TForm4.GetJWTToken(const ASettings: TSettings): string;
var
  IdHTTP: TIdHTTP;
  SSLHandler: TIdSSLIOHandlerSocketOpenSSL;
  Params: TStringList;
  Header, Payload, Signature: string;
begin
  // OpenSSL の初期化
//  if not InitializeOpenSSL then
//    raise Exception.Create('Failed to initialize OpenSSL');

  IdHTTP := TIdHTTP.Create(nil);
  SSLHandler := TIdSSLIOHandlerSocketOpenSSL.Create(IdHTTP);
  Params := TStringList.Create;
  try
    SetHttpSSL(IdHTTP);
    IdHTTP.IOHandler := SSLHandler;
    SSLHandler.SSLOptions.Method := sslvSSLv23;
    IdHTTP.IOHandler := SSLHandler;
    IdHTTP.Request.ContentType := 'application/x-www-form-urlencoded';
    Params.Values['client_id'] := ASettings.boxAppSettings.clientID;
    Params.Values['client_secret'] := ASettings.boxAppSettings.clientSecret;
    Params.Values['grant_type'] := 'client_credentials';
    // JWTトークンの生成
    Header := Base64Encode('{"alg":"SHA256","typ":"JWT"}');
    Payload := Base64Encode('{"iss":"' + ASettings.boxAppSettings.clientID + '","sub":"' + ASettings.boxAppSettings.appAuth.publicKeyID + '"}');
    Signature := GenerateSignature(Header + '.' + Payload, ASettings.boxAppSettings.appAuth.privateKey, ASettings.boxAppSettings.appAuth.passphrase);
 // ここで秘密鍵を使用して署名を生成します

    // ヘッダ、ペイロード、署名の値を表示
    Memo1.Lines.Add('Header: ' + Header);
    Memo1.Lines.Add('Payload: ' + Payload);
    Memo1.Lines.Add('Signature: ' + Signature);

    Params.Values['assertion'] := Header + '.' + Payload + '.' + Signature;
//    Memo1.Lines.Add(Header + '.' + Payload + '.' + Signature);
    Result := IdHTTP.Post('https://api.box.com/oauth2/token', Params);
  finally
    Params.Free;
    SSLHandler.Free;
    IdHTTP.Free;
  end;
end;

//Base64エンコード
function TForm4.Base64Encode(const AValue: string): string;
var
  Encoder: TIdEncoderMIME;
begin
  Encoder := TIdEncoderMIME.Create(nil);
  try
    Result := Encoder.Encode(AValue);
    // Remove padding
//    Result := StringReplace(Result, '=', '', [rfReplaceAll]);
  finally
    Encoder.Free;
  end;
end;

function ExtractPrivateKey(const APEM: AnsiString; const APassword: AnsiString): TBytes;
var
  Bio: PBIO;
  PKey: PEVP_PKEY;
  Len: Integer;
  PEMAsASCII: AnsiString;
begin
  // PEM形式の秘密鍵をASCIIに変換
  PEMAsASCII := AnsiString(APEM);
  // PEMAsASCII の終端に null 文字を追加する
  PEMAsASCII := PEMAsASCII + AnsiChar(#0);

  // 正しい長さを指定して BIO を作成する
  Bio := BIO_new_mem_buf(PAnsiChar(PEMAsASCII), Length(PEMAsASCII) * SizeOf(AnsiChar));

  if Bio = nil then
    raise Exception.Create('Failed to create BIO');

  try
    // 秘密鍵をPEMからEVP_PKEYにデコード
    PKey := PEM_read_bio_PrivateKey(Bio, nil, nil, PAnsiChar(APassword));
    if PKey = nil then
      raise Exception.Create('Failed to read private key');

    try
      // EVP_PKEYからバイト配列に変換
      Len := i2d_PrivateKey(PKey, nil);
      SetLength(Result, Len);

      i2d_PrivateKey(PKey, @Result[0]);
    finally
      EVP_PKEY_free(PKey);
    end;
  finally
    BIO_free(Bio);
  end;
end;


function TForm4.GenerateSignature(const AData: string; AKey: AnsiString; APassword: AnsiString): string;
var
  HMAC: TIdHMAC;
  Signature: TIdBytes;
  Key: TBytes;
begin
  HMAC := TIdHMACSHA256.Create;

  try
    Key := ExtractPrivateKey(AKey, APassword);
    HMAC.Key := Key;
    Signature := HMAC.HashValue(TIdTextEncoding.UTF8.GetBytes(AData));

    Result := TIdEncoderMIME.EncodeBytes(Signature);
  finally
    HMAC.Free;
  end;
end;

というコードを作ったところ、

Bio := BIO_new_mem_buf(PAnsiChar(PEMAsASCII), Length(PEMAsASCII) * SizeOf(AnsiChar));

をステップ実行した時点で一度に多数の例外が発生してしまいました。
PEMも'-----BEGIN ENCRYPTED PRIVATE KEY-----‘と’-----END ENCRYPTED PRIVATE KEY-----'で囲まれた適切になものになっているように見えるので、なにが原因なのか自分ではわかりません。
uses節でIdHMACSHA256を宣言できなかったため、OpenSSLの関数からIdHMACSHA256の署名を作りたいのですが、良い修正方法はありませんか。
ご教示いただけると嬉しいです。よろしくお願いいたします。

編集 削除
vram  2024-05-15 02:21:16  No: 151478  IP: 192.*.*.*

長々と書かれていますがエラーが出る所

Bio := BIO_new_mem_buf(PAnsiChar(PEMAsASCII), Length(PEMAsASCII) * SizeOf(AnsiChar))

BIO_new_mem_bufのサンプルっぽいの見ると

var
  Buffer: TBytes
begin
  Result := BIO_new_mem_buf(Buffer, Length(Buffer));
end;

ってなってるけど AnsiCharaで渡せるかな?

BIO_new_mem_bufのサンプルっぽいWebサイト
https://delphi.developpez.com/tutoriels/rsa/


あとJWTライブラリあるみたいだけど使えない?
https://github.com/paolo-rossi/delphi-jose-jwt

編集 削除
UTM  2024-05-16 06:59:40  No: 151479  IP: 192.*.*.*

OpenSSLの初期化を行ってから
IdSSLOpenSSL.LoadOpenSSLLibrary;

  Bio := BIO_new(BIO_s_mem()); 
  if Bio = nil then
    raise Exception.Create('Failed to create BIO');

  try
    
    if BIO_write(Bio, PAnsiChar(APEM), Length(APEM)) <= 0 then
      raise Exception.Create('Failed to write data to BIO');
...
としたところ変換がうまく処理されました。

JWTのライブラリは残念ながら使用できないようでしたので今回はこの処理を使うことにします。
ありがとうございました。

編集 削除