🐀

GoでLINE Messaging APIのチャネルアクセストークンv2.1を取得する

2022/09/28に公開

やりたいこと

Goを使って、LINE Messaging API[1]を叩くのが目標です。
LINE Messaging APIを使うには、チャネルアクセストークンが必要になるのですが、この取得方法には大きく分けて以下の3つあります[2]

  1. 任意の有効期間を指定できるチャネルアクセストークン(チャネルアクセストークンv2.1)
  2. 短期のチャネルアクセストークン
  3. 長期のチャネルアクセストークン

この中で1番の方法が推奨されているので、今回は、この方法でチャネルトークンをGo言語で取得します。
基本的には公式ドキュメントを参考に実装していけば良いのですが、いくつか躓いたポイントがあるので、その解決策を重点的に書いていきたいと思います。

躓いたポイント

  1. JWT(JSON Web Token)のヘッダーにkidプロパティと、その値を追加する方法
  2. JWTのペイロードに必要なプロパティを追加する方法

この2点を踏まえながら、以下でアクセストークンv2.1の発行方法を書きます。

アクセストークンv2.1発行方法

必要なパッケージ

チャネルアクセストークンv2.1を発行する[3]に記載されているGoのパッケージgithub.com/lestrrat-go/jwx/v2を使います。
以下のコマンドでパッケージを取得してください。

go get github.com/lestrrat-go/jwx/v2

アサーション署名キーの取得

JWTを作成するためには、アサーション署名キーが必要になります。
チャネルアクセストークンv2.1を発行する[3:1]には、取得方法についてGoのライブラリで生成する、Pythonのライブラリで生成する、ブラウザで生成するの3つが紹介されていますが、ブラウザで生成するが最も簡単なので、これでアサーション署名キーを生成します。

生成されるアサーション署名キーの例は、ブラウザで生成するに掲載されているので、参考にしてください。

公開鍵を登録し、kidを取得する。

前の節で取得した、アサーション署名キーのうち、公開鍵をLINE Developerコンソールに登録することでkidを取得できます。
手順については公開鍵を登録し、kidを取得するを参照してください。

Goを使ってJWTを生成する

では、Goを使ってJWTを生成します。
LINEのドキュメント[3:2]には、Node.jsのライブラリとPythonのライブラリを使う方法しか書いていません。
ここでは、jwt.ioに公開されているライブラリの一覧から、LINEのドキュメント[3:3]でも紹介されていたlestrrat-go/jwxを使います。

今回作成し、動作確認したコードは以下になります。
private.keyに先の節「アサーション署名キーの取得」で取得した秘密鍵をコピペして、更にkidを追加して保存します。
その上で、以下のコードを実行してください。

main.go
package main

import (
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"net/http"
	"net/url"
	"os"
	"strings"
	"time"

	"github.com/joho/godotenv"
	"github.com/lestrrat-go/jwx/v2/jwa"
	"github.com/lestrrat-go/jwx/v2/jwk"
	"github.com/lestrrat-go/jwx/v2/jwt"
)

type Foo struct {
	Token string `json:"access_token"`
	Type  string `json:"token_type"`
	Exp   int64  `json:"expires_in"`
	Id    string `json:"key_id"`
}

func main() {
	// 環境変数ファイルの読み込み
	godotenv.Load(".env")

	// 秘密鍵のファイルを開く
	f, err := os.Open("private.key")
	if err != nil {
		log.Fatal(err)
	}
	defer f.Close()

	// ファイルから秘密鍵の読み込み
	b, err := ioutil.ReadAll(f)
	privkey, err := jwk.ParseKey(b)
	if err != nil {
		fmt.Printf("failed to parse JWK: %s\n", err)
		return
	}

	{
		// audプロパティに追加するために、aud変数を作成
		var aud []string
		aud = append(aud, "https://api.line.me/") // audプロパティの値を追加

		// JWTを構成する
		tok, err := jwt.NewBuilder().
			Subject(os.Getenv("CHID")).                   // subプロパティ、チャネルIDを入れる
			Issuer(os.Getenv("CHID")).                    // issプロパティ、チャネルIDを入れる
			Audience(aud).                                // audプロパティ、先程作った値audを入れる
			Expiration(time.Now().Add(30 * time.Minute)). // expプロパティ、JWTの有効期間、最大30分を入れる
			Build()
		if err != nil {
			fmt.Printf("failed to build token: %s\n", err)
			return
		}

		// token_expプロパティはメソッドが用意されてないので、.Setで追加。
		tok.Set("token_exp", 60*60*24*30) // token_expプロパティ、チャネルアクセストークンの有効期間を指定

		// JWTを発行する
		signed, err := jwt.Sign(tok, jwt.WithKey(jwa.RS256, privkey)) // signedにJWTがエンコードされ代入される
		if err != nil {
			fmt.Printf("failed to sign token: %s\n", err)
			return
		}

		fmt.Println("🏷 JWT")
		fmt.Println(string(signed)) // JWTの確認

		// チャネルアクセストークンv2.1を発行するリクエストの作成
		// 参考)https://developers.line.biz/ja/reference/messaging-api/#issue-channel-access-token-v2-1
		form := url.Values{}
		form.Set("grant_type", "client_credentials")
		form.Add("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer")
		form.Add("client_assertion", string(signed))

		body := strings.NewReader(form.Encode()) // リクエストのbodyを作成

		// リクエストの作成
		req, err := http.NewRequest(http.MethodPost, "https://api.line.me/oauth2/v2.1/token", body)
		if err != nil {
			log.Fatal(err)
		}
		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

		// 作成したリクエストの送信
		client := &http.Client{}
		res, err := client.Do(req)
		if err != nil {
			log.Fatal(err)
		}
		defer res.Body.Close()

		// レスポンスの解析
		var r io.Reader = res.Body

		var foo Foo
		err = json.NewDecoder(r).Decode(&foo)
		if err != nil {
			log.Fatal(err)
		}

		bytes, err := json.Marshal(foo)

		fmt.Println("🎁チャネルアクセストークンを含むペイロード")
		fmt.Println(string(bytes))

		fmt.Println("🔑チャネルアクセストークン")
		fmt.Println(foo.Token)
	}
}

ここで、躓いたポイントについて解説します。

1. JWT(JSON Web Token)のヘッダーにkidプロパティと、その値を追加する方法

これは、Goのコード上で追加しようと思ったのですが、うまくいきません。
色々と、ドキュメントを読んでいると、秘密鍵にkidというプロパティがあると、自動的に追加されそうだということがわかりました。
そのため、先の節で取得したkidを、秘密鍵に追加します。例えばこんな感じです。

private.json
{
  "alg": "RS256",
  "d": "GaDzOmc4......",
  "dp": "WAByrYmh......",
  "dq": "WLwjYun0......",
  "e": "AQ......",
  "ext": true,
  "key_ops": [
    "sign"
  ],
  "kid": "ここにkidで取得した値を入れるよ!",
  "kty": "RSA",
  "n": "vsbOUoFA......",
  "p": "5QJitCu9......",
  "q": "1ULfGui5......",
  "qi": "2cK4apee......"
}

この秘密鍵ファイルを用意した上で、上で紹介したコードを実行して得たJWTを、以下のサイトのEncodedに入れてもらうと、Headerにkidが追加されているのが確認できると思います。

https://jwt.io/

2. JWTのペイロードに必要なプロパティを追加する方法

JWTのペイロードに必要なプロパティを追加する方法は、既にコードで示しましたが、jwt.NewBuilder()のメソッドチェーンを使うのが簡単です。もしも、用意されていないプロパティであれば、JWTに.Set()を使って追加できます。
例えばこんな感じです。

// JWTを構成する
		tok, err := jwt.NewBuilder().
			Subject(os.Getenv("CHID")).                   // subプロパティ、チャネルIDを入れる
			Issuer(os.Getenv("CHID")).                    // issプロパティ、チャネルIDを入れる
			Audience(aud).                                // audプロパティ、先程作った値audを入れる
			Expiration(time.Now().Add(30 * time.Minute)). // expプロパティ、JWTの有効期間、最大30分を入れる
			Build()
		if err != nil {
			fmt.Printf("failed to build token: %s\n", err)
			return
		}

		// token_expプロパティはメソッドが用意されてないので、.Setで追加。
		tok.Set("token_exp", 60*60*24*30) // token_expプロパティ、チャネルアク

実行結果

実行結果はこんな感じです。
result

まとめと感想

APIを使うとき、認証と認可のところが一番苦労します。
続きも頑張ってやっていきたいです。
もしも、「ここまずいよ!」とか「分からない!」という所があれば、コメントでご指摘いただけると幸いです。

参考

コードを作る上で参考にしたサイトを紹介します。

https://pkg.go.dev/github.com/lestrrat-go/jwx/v2#section-readme

https://qiita.com/konchanxxx/items/dce130f79c49e04e9931

https://leben.mobi/go/time/go-programming/#i-4

https://qiita.com/po3rin/items/740445d21487dfcb5d9f

https://qiita.com/yyoshiki41/items/fc878494e19b9de93d56

脚注
  1. LINE Messaging API / LINE https://developers.line.biz/ja/docs/messaging-api/ (2022-09-28閲覧) ↩︎

  2. チャネルアクセストークン / LINE https://developers.line.biz/ja/docs/messaging-api/channel-access-tokens/ (2022-09-28閲覧) ↩︎

  3. チャネルアクセストークンv2.1を発行する / LINE https://developers.line.biz/ja/docs/messaging-api/generate-json-web-token/ (2022-09-28閲覧) ↩︎ ↩︎ ↩︎ ↩︎

Discussion