APIの認証処理について¶
XG APIを利用する際には、Authorization: Bearer ヘッダーに認証トークンを指定してリクエストを行います。
この認証トークンは、JWT (JSON Web Token)形式で提供され、情報の完全性と送信元を確認できるように設計されています。
JWTは、JSONデータ構造をコンパクトでURLセーフな方法で表現するための規格で、JWS (JSON Web Signature) を使用して
その情報をデジタル署名またはMAC(Message Authentication Code)により保護します。
これにより、データが改ざんされることなく安全に送受信され、かつ、送信元が正当なものであることを保証できます。
JWTの作成手順¶
JWTは次の手順で作成します。
BASE64(UTF8(ヘッダー)) + '.' + BASE64(UTF8(ペイロード)) + '.' + BASE64(署名)
ヘッダー
{ "alg": "EdDSA", "typ": "JWT", "kid": "事前に登録した公開鍵の識別ID" }
ペイロード
{ "xgpi": "XG ProjectID", "xgai": "XG AppID", "xg_hash": "URLとRequestBodyを元に作成したハッシュ値", "iat": "トークンの発行日時", "exp": "トークンの有効期限" }
xg_hash は URLとRequestBodyを元に作成します。
ハッシュ値を作成する場合、RequestBodyの末尾の改行は除外して作成してください。
以下は Go のサンプルコードです¶
baseString := fmt.Sprintf("%s\n\n%s\n", apiPath, strings.TrimRight(string(requestBody), "\r\n"))
hasher := sha256.New()
hasher.Write([]byte(baseString))
xgHash := hex.EncodeToString(hasher.Sum(nil))
注意
iatは現在時刻のUNIXタイムスタンプを指定してください。
- expの値は iatの値から60秒以内 に設定してください。(XG側でセキュリティとしてトークンの有効期限を60秒にしています)
JWT生成とXG API通信のイメージ
サンプルコード¶
以下は、PHP言語を用いて認証トークンを作成し、リクエストするためのサンプルコードです。
実行には、 openssl 、 sodium が必要です。
php -m | grep -E '(openssl|sodium)'
openssl
sodium
このコードを実行する前に、以下のコマンドでライブラリをインストールしてください。
composer install
composer.json¶
{
"autoload": {
"psr-4": {
"Sample\\": "sample/"
}
},
"require": {
"guzzlehttp/guzzle": "^7.8",
"ramsey/uuid": "^4.7",
"firebase/php-jwt": "^6.11"
}
}
sample/Xgapi/XGAPI.php¶
<?php
namespace Sample\Xgapi;
use GuzzleHttp\Client;
use Firebase\JWT\JWT;
class XGAPI
{
private $host;
private $xgProjectId;
private $xgApplicationId;
private $kid;
private $privateKey;
/**
* コンストラクタ
* @param string $host APIのホスト
* @param string $projectId XG ProjectID
* @param string $applictionId XG AppID
* @param string $kid 秘密鍵と公開鍵のペアのID
* @param string $keyPath 秘密鍵のディレクトリ
* @return void
*/
public function __construct(string $host, string $projectId, string $applictionId, string $kid, string $keyPath)
{
// $hostの末尾の/があれば削除する
if (substr($host, -1) == '/') {
$host = substr($host, 0, -1);
}
$this->host = $host;
$this->xgProjectId = $projectId;
$this->xgApplicationId = $applictionId;
$this->kid = $kid;
$this->privateKey = $this->_loadPrivateKey($keyPath . '/private.pem');
}
/**
* 秘密鍵を読み込む
* @param string $filename
* @return string sodium形式の秘密鍵
*/
private function _loadPrivateKey(string $filename): string
{
// PEM(PKCS#8)秘密鍵ファイルの読み込み
$privateKey = file_get_contents($filename);
// 秘密鍵を利用して公開鍵を取得する
$publicKey = openssl_pkey_get_details(openssl_get_privatekey($privateKey))['key'];
// sodium形式に変換
$pattern = '/-----BEGIN (?:PRIVATE|PUBLIC) KEY-----(.*)-----END (?:PRIVATE|PUBLIC) KEY-----/s';
array_map(function(&$key) use($pattern) {
if (preg_match($pattern, $key, $matches)) {
$key = substr(base64_decode($matches[1]), -32);
}
}, [&$privateKey, &$publicKey]);
$sodiumKey = base64_encode(sodium_crypto_box_keypair_from_secretkey_and_publickey($privateKey, $publicKey));
return $sodiumKey;
}
/**
* リクエスト送信
* @param string $method (GET, POST, PUT, DELETE) リクエストメソッド
* @param string $apiPath (ex. /user/v1/users) APIのパス
* @param string $requestBody (ex. {"test":123}) リクエストボディーのJSON文字列
* @return \Psr\Http\Message\ResponseInterface
*/
public function request(string $method, string $apiPath, string $requestBody): \Psr\Http\Message\ResponseInterface
{
$baseString = sprintf("%s%s\n\n%s\n", $this->host, $apiPath, rtrim($requestBody, "\r\n"));
$xgHash = hash('sha256', $baseString);
$now = time();
$payload = [
'xgpi' => $this->xgProjectId,
'xgai' => $this->xgApplicationId,
'xg_hash' => $xgHash,
'iat' => $now, //現在の日時
'exp' => $now + 30 //トークンの有効期限(セキュリティの観点で60秒以上はエラーになります)
];
$jwt = JWT::encode($payload, $this->privateKey, 'EdDSA', $this->kid);
$client = new Client();
$response = $client->request($method, $this->host . $apiPath, [
'headers' => [
'Authorization' => 'Bearer ' . $jwt,
'Content-Type' => 'application/json',
],
'body' => $requestBody,
]);
return $response;
}
}
jwt_sample.php¶
<?php
/**
* XG APIのサンプルコード(JWT認証)
*
* PHP8 で動作確認しています
*
* このコードを実行する前に、以下のコマンドでライブラリをインストールしてください
* composer install
*/
require_once 'vendor/autoload.php';
use Sample\Xgapi\XGAPI;
use GuzzleHttp\Exception\BadResponseException;
use Ramsey\Uuid\Uuid;
function errorResponse($response) {
$j = json_decode($response);
if (is_null($j)) {
// JSON解析エラーの処理
throw new \Exception(sprintf("JSON Decode error: %s", $response));
}
if (isset($j->errors) && count($j->errors) > 0) {
throw new \Exception(sprintf("エラーが発生しました: %s", $j->errors[0]->reason));
}
throw new \Exception("不明なエラーが発生しました");
}
function main() {
// 初期設定
$xgProjectId = "xg_sample"; // XG ProjectID
$xgAppId = "dev"; // XG AppID
$kid = "sample_kid"; // XGに登録した公開鍵の識別ID
$host = "http://localhost"; // APIのホスト
$keyPath = "./path/to/"; // 秘密鍵(private.pem)のディレクトリ
// APIのインスタンスを生成
$keyPath = realpath($keyPath);
$xgApi = new XGAPI($host, $xgProjectId, $xgAppId, $kid, $keyPath);
//リクエスト
$method = 'POST';
$path = '/user/v1/users';
$body = "{}";
try {
$response = $xgApi->request($method, $path, $body);
} catch (\Throwable $e) {
error_log($e->getMessage());
}
//レスポンス結果
echo "HttpStatus=" . $response->getStatusCode() . "\n";
echo "ResponseBody=" . $response->getBody() . "\n";
}
main();
以下は、Go言語を用いて認証トークンを作成し、リクエストするためのサンプルコードです。
このコードを実行する前に、以下のコマンドでライブラリをインストールしてください。
go mod download
go.mod¶
module sample
go 1.21
require (
github.com/gofrs/uuid v4.4.0+incompatible
github.com/golang-jwt/jwt/v5 v5.2.0
)
xgapi/xgapi.go¶
package xgapi
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"fmt"
"log"
"net/http"
"os"
"strings"
"time"
"github.com/golang-jwt/jwt/v5"
)
// APIエンドポイントへのリクエスト用の設定情報を持つ構造体
type XGAPI struct {
signingMethod jwt.SigningMethod
privateKey []byte
projectId string
appID string
kid string
host string
}
// XGAPIと通信処理をするためのインスタンスを作成
func NewXGAPI(host string, projectId string, appId string, kid string, keyPath string) *XGAPI {
privateKeyBytes, err := os.ReadFile(keyPath + "/private.pem")
if err != nil {
log.Fatalf("Failed to read private key: %v", err)
}
// host末尾の/ を削除
host = strings.TrimSuffix(host, "/")
return &XGAPI{
signingMethod: jwt.SigningMethodEdDSA, // JWTの署名方式
privateKey: privateKeyBytes, // 秘密鍵
projectId: projectId, // XG ProjectID
appId: appId, // XG AppID
kid: kid, // 事前に登録した公開鍵の識別ID
host: host, // ベースとなるAPIのURL
}
}
// JWTトークンの生成
func (api *XGAPI) createJWTToken(apiPath, body string) (string, error) {
// ハッシュの生成 ( bodyの末尾の改行を削除する必要があります)
baseString := fmt.Sprintf("%s%s\n\n%s\n", api.host, apiPath, strings.TrimRight(string(body), "\r\n"))
hasher := sha256.New()
hasher.Write([]byte(baseString))
xgHash := hex.EncodeToString(hasher.Sum(nil))
// トークンの生成
now := time.Now()
token := jwt.NewWithClaims(api.signingMethod, jwt.MapClaims{
"xgpi": api.projectID,
"xgai": api.appID,
"xg_hash": xgHash,
"iat": now.Unix(), // トークンの発行時刻(現在時刻)
"exp": now.Add(time.Second * 30).Unix(), // トークンの有効期限(セキュリティの観点で60秒以上はエラーになります)
})
token.Header["kid"] = api.kid
// トークンの署名
parsedPrivateKey, err := jwt.ParseEdPrivateKeyFromPEM(api.privateKey)
if err != nil {
return "", err
}
return token.SignedString(parsedPrivateKey)
}
// リクエストの送信
func (api *XGAPI) SendRequest(method string, path string, body string) (*http.Response, error) {
// トークンの生成
jwtToken, err := api.createJWTToken(path, body)
if err != nil {
return nil, err
}
// リクエストの作成
url := api.host + path
req, err := http.NewRequest(method, url, bytes.NewReader([]byte(body)))
if err != nil {
return nil, err
}
// トークンのセット
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", jwtToken))
req.Header.Set("Content-Type", "application/json")
// リクエストの送信
return http.DefaultClient.Do(req)
}
jwt_sample.go¶
package main
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"sample/xgapi"
)
type XgErrors struct {
Reason string `json:"reason"`
}
type ErrorResponse struct {
Type string `json:"type"`
Status int `json:"status"` // 追加: ステータスコードもキャプチャする。
Errors []XgErrors `json:"errors"` // Typo を修正: "erorrs" -> "errors"
Title string `json:"title"`
}
func errorResponse(bodyBytes []byte) error {
errRes := ErrorResponse{}
if err := json.Unmarshal(bodyBytes, &errRes); err != nil {
// JSON解析エラーの処理
log.Printf("JSON Unmarshal error: %v", err)
return err
}
if len(errRes.Errors) > 0 {
return fmt.Errorf("エラーが発生しました: %v", errRes.Errors[0].Reason)
}
return fmt.Errorf("不明なエラーが発生しました")
}
func main() {
// 初期設定
projectId := "xg_sample" // XG ProjectID
appId := "dev" // XG AppID
kid := "sample_kid" // XGに登録した公開鍵の識別ID
host := "http://localhost" // APIのホスト
keyPath := "./path/to/" // 秘密鍵(private.pem)のディレクトリ
// APIのインスタンスを作成
xgApi := xgapi.NewXGAPI(host, projectId, appId, kid, keyPath)
// リクエストの送信
method := http.MethodPost // HTTPメソッドの種類
path := "/user/v1/users" // APIのパス
body := "{}" // リクエストボディ
response, err := xgApi.SendRequest(method, path, body)
if err != nil {
panic(err)
}
defer response.Body.Close()
// responseを確認
fmt.Printf("HTTP status: %v\n", response.StatusCode)
bodyBytes, err := io.ReadAll(response.Body)
if err != nil {
panic(err)
}
fmt.Printf("HTTP body: %s\n", string(bodyBytes))
}