Carpe Diem

備忘録

JWTを使ってGoogleAPIのアクセストークン取得する

概要

GoogleAPIを使う際、多くの場合は「ユーザごとに認証させてアクセストークンを発行し、リクエストに利用する」という流れですが、APIによってはわざわざユーザ個別にアクセストークンを発行させる必要がないケースもあります。
そんなケースでは「Service Accounts」という方式を使い、サービス側でアクセストークンを発行してAPIを利用します。以下の様な流れです。

f:id:quoll00:20150604215717p:plain

今回は例としてGoogleDriveにアクセスしてみます。

手順

  1. Developer Consoleでアプリ作成し、「Service Accounts」を選択して、秘密鍵を取得
  2. 秘密鍵で署名したJWTを作る
  3. Googleトークンエンドポイントを叩いてアクセストークンを取得
  4. 取得したアクセストークンでAPIを叩く

環境

  • Node.js v0.12.0

Developer Consoleでアプリを作る

プロジェクトの作成

にアクセスします。 f:id:quoll00:20150604213913p:plain

利用するAPIをONに

検索して

f:id:quoll00:20150604214008p:plain

有効にするボタンを押します。

f:id:quoll00:20150604214044p:plain

認証情報で新しいクライアントIDを作成

左メニューの認証情報を選択し、クライアントIDを作成 f:id:quoll00:20150604214148p:plain

サービスアカウントを選択 f:id:quoll00:20150604214226p:plain

勝手に公開鍵、秘密鍵のペアが作成され、秘密鍵がダウンロードされます。 f:id:quoll00:20150604214308p:plain

クライアントIDの作成が完了すると情報が表示されるようになります。 f:id:quoll00:20150604214554p:plain

秘密鍵からJWTを作成する

JWTフォーマット

JWTは以下のフォーマットです。

{base64エンコードしたheader}.{base64エンコードしたclaims}.{署名}

ヘッダ

パラメータ 説明
alg 署名アルゴリズムRSA-SHA256を使用
typ タイプ。JWTに設定

実際には以下の形です。

{
  "alg": "RS256",
  "typ": "JWT"
}

Base64エンコードしたらこうなります。

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9

クレーム

パラメータ 説明
iss Developer Consoleで作ったクライアントIDの部分に書いてあるメールアドレス。
xxxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxx@developer.gserviceaccount.comの形
scope 利用するAPIスコープ
aud クライアント識別子。アクセストークンを発行する場合は
https://www.googleapis.com/oauth2/v3/token
exp トークンの有効期限。最大でiatから1時間分まで
iat トークンの発行日

実際には以下の形です。

{
  "iss": "xxxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxx@developer.gserviceaccount.com",
  "scope": "https://www.googleapis.com/auth/drive",
  "aud": "https://www.googleapis.com/oauth2/v3/token",
  "exp": 1433469193,
  "iat": 1433465593
}

Base64エンコードしたらこうなります。

eyJpc3MiOiI2jMzNDUzNDI1ODQtNmgyaW0xamIwOHJsbWVodTFuOGtxNm43MWpvYmlwNGVAZGV2ZWxvcGVyLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJzY29wZSI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL2F1dGgvZHJpdmUiLCJhdWQiOiJodHRwczovL3d3dy5nb29nbGVhcGlzLmNvbS9vYXV0aDIvdjMvdG9rZW4iLCJleHAiOjE0MzM0MjgyMjcsImlhdCI6MTQzMzQyNDYyN30=

署名

以下の文字列を、秘密鍵を使ってRSA-SHA256形式で署名します。

{base64エンコードしたheader}.{base64エンコードしたclaims}

Developer Console からダウンロードしたJSONprivate_keyにある秘密鍵を使用します。

生成されたJWT

実際の値は以下のようになります。

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiI2jMzNDUzNDI1ODQtNmgyaW0xamIwOHJsbWVodTFuOGtxNm43MWpvYmlwNGVAZGV2ZWxvcGVyLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJzY29wZSI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL2F1dGgvZHJpdmUiLCJhdWQiOiJodHRwczovL3d3dy5nb29nbGVhcGlzLmNvbS9vYXV0aDIvdjMvdG9rZW4iLCJleHAiOjE0MzM0MjgyMjcsImlhdCI6MTQzMzQyNDYyN30=.kRH1WwWBg1pwx5lKrLjFzD444jxasDZGlfd5xGK9BX1GmLWQTavRr15jZu0FcGNUYPyZ1u+wsSRYOTk91ePIEu8hKb6HzD1nz6QrxxM48xJtDGFtwxZC57ZsqiR+qLKt79PdndrcCFeItHub3L7v/7QfbeaR7T4CRMvyyLfiQs=

JWTをリクエストにつけてアクセストークンを取得する

アクセストークンを取得するのに必要なパラメータは以下です。

パラメータ 説明
assertion JWT
grant_type urn:ietf:params:oauth:grant-type:jwt-bearerURL-encoded必須

URL-encoded必須なので、Content-Type: application/x-www-form-urlencodedのヘッダを付けてください。

以下のBodyをhttps://www.googleapis.com/oauth2/v3/tokenに対してPOSTメソッドでリクエストを送ります。

{
  "assertion": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiI2jMzNDUzNDI1ODQtNmgyaW0xamIwOHJsbWVodTFuOGtxNm43MWpvYmlwNGVAZGV2ZWxvcGVyLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJzY29wZSI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL2F1dGgvZHJpdmUiLCJhdWQiOiJodHRwczovL3d3dy5nb29nbGVhcGlzLmNvbS9vYXV0aDIvdjMvdG9rZW4iLCJleHAiOjE0MzM0MjgyMjcsImlhdCI6MTQzMzQyNDYyN30=.kRH1WwWBg1pwx5lKrLjFzD444jxasDZGlfd5xGK9BX1GmLWQTavRr15jZu0FcGNUYPyZ1u+wsSRYOTk91ePIEu8hKb6HzD1nz6QrxxM48xJtDGFtwxZC57ZsqiR+qLKt79PdndrcCFeItHub3L7v/7QfbeaR7T4CRMvyyLfiQs=",
  "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer"
}

プログラム作成&実行

あらかじめ必要なライブラリをインストールしておきます。

$ npm install superagent
$ npm install crypto

今までの説明を反映したプログラムは以下の通りです。

var request = require('superagent');
var crypto = require('crypto');
var fs = require('fs');
var privateKey = require('./key.json'); // private key generated on Developer Console.

var driveScope = 'https://www.googleapis.com/auth/drive';
var tokenEndpoint = 'https://www.googleapis.com/oauth2/v3/token';
var grantType = 'urn:ietf:params:oauth:grant-type:jwt-bearer';

var now = Math.floor( new Date().getTime() / 1000 );

var header = {
    alg: 'RS256',
    typ: 'JWT'
};

var claims = {
    iss: 'xxxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxx@developer.gserviceaccount.com',
    scope: driveScope,
    aud: tokenEndpoint,
    exp: now + 3600, // maximum expiry date is 1 hour after the issued time.
    iat: now
};

var body = {
    assertion: createJWT(header, claims),
    grant_type: grantType
}

// send request to get access token
request.post(tokenEndpoint)
    .set('Content-Type', 'application/x-www-form-urlencoded') // you must set this header.
    .send(body)
    .end(function(err, res){
        console.log('result: ', res.body);
    });


function createJWT(header, claims) {
    var encodedHeader = new Buffer(JSON.stringify(header)).toString('base64');
    var encodedClaims = new Buffer(JSON.stringify(claims)).toString('base64');
    var sign = crypto.createSign('RSA-SHA256');
    var data = new Buffer(encodedHeader + '.' + encodedClaims);

    sign.update(data);
    signature = sign.sign(new Buffer(privateKey.private_key), 'base64');

    // JWT format is '{base64 encoded header}.{base64 encoded claims}.{signature}'
    return [encodedHeader, encodedClaims, signature].join('.');
}

成功すると以下のようになります。

{
  "access_token": "ya29.iAGB3kySHSQDUEo4NPMwWEgZ8kjOMj1F0T5uibEA_9TR2p1cd8LFjM2FLhyw3Re5RE4j-42BPvn9g",
  "token_type": "Bearer",
  "expires_in": 3600
}

失敗すると以下のようになります。

{
  "error": "invalid_grant",
  "error_description": "Bad Request"
}

APIを叩く

ファイルの一覧表示するAPIを叩いてみます。

$ curl -H "Authorization: OAuth ya29.iAGB3kySHSQDUEo4NPMwWEgZ8kjOMj1F0T5uibEA_9TR2p1cd8LFjM2FLhyw3Re5RE4j-42BPvn9g" \
https://www.googleapis.com/drive/v2/files

アクセストークンが有効期限内であれば一覧を表示してくれます。

ソース