2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Rust】LINEチャネルアクセストークンの取得とメッセージ送信

Posted at

概要

LINE Messaging APIを使用するにはチャネルアクセストークン(チャネルシークレットとは別物)が必要になる

チャネルアクセストークンには4種類あるみたいだが、今回はステートレスチャネルアクセストークンの取得と使用を行う。ステートレスチャネルアクセストークンの特徴と利点は以下が考えられる

  • トークンの取り消しはできないが、有効期限が15分と短いのでトークンの漏洩のリスクを抑えられる
  • 発行数が無制限のため、トークンの使いまわしなどを考えずに都度発行できる
    • APIの発行頻度がそんなにないケースでは特に有用と思われる

その他詳細は以下を参照

事前準備

LINEビジネスアカウントの作成とLINE Messaging APIの有効化は済ませているものとする

クイックスタートのアサーション署名キーのキーペアを生成で説明されている手順を以下で説明する

アサーション署名キーのキーペアを生成する

  • uvをインストール
  • ターミナルでuvx --with jwcrypto python3を実行してipythonを起動。以下をペーストして実行する
from jwcrypto import jwk
import json
key = jwk.JWK.generate(kty='RSA', alg='RS256', use='sig', size=2048)

private_key = key.export_private()
public_key = key.export_public()

print("=== private key ===\n"+json.dumps(json.loads(private_key),indent=2))
print("=== public key ===\n"+json.dumps(json.loads(public_key),indent=2))
  • 秘密鍵(=== private key ===以下の出力)の内容をコピーして任意のファイルに保存する
    • 後でRustで参照する

Line Developersコンソールで公開鍵を登録

  • 公開鍵(=== public key ===以下の出力)の内容をコピペする
  • アサーション署名キーに紐づくID (kid)は、後でRustで参照する

コード

ほぼ同じコードをGitHubにも上げている

以下の順序で処理する

  • JWTの生成
  • (JWTを使って)ステートレスチャネルアクセストークンを取得
  • メッセージの送信

JWTの生成やメッセージの送信に必要なものはすべて環境変数から取得している

main.rs
use chrono::{Duration, Utc};
use jsonwebkey::JsonWebKey;
use jsonwebtoken::{Algorithm, EncodingKey, Header};
use line_bot_messaging_api::LineClient;
use line_bot_messaging_api::message::{LineMessageText, LineMessagesBuilder};
use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::env;
use std::fs::File;
use std::io::prelude::*;

#[derive(Debug, Serialize, Deserialize, Clone)]
struct Claims {
    iss: String,
    sub: String,
    aud: String,
    exp: usize,
    // 有効期限は15分で固定のため、ペイロード(Claims)にtoken_expは不要
}

impl Claims {
    pub fn new(line_channel_id: &str) -> Self {
        // expは30分以内に失効するように指定する
        let exp = Utc::now() + Duration::minutes(1);
        Self {
            iss: line_channel_id.to_string(),
            sub: line_channel_id.to_string(),
            exp: exp.timestamp() as usize,
            aud: "https://api.line.me/".to_string(),
        }
    }
}

#[tokio::main]
async fn main() {
    // Get environment variables
    let line_private_jwk_path = env::var("LINE_PRIVATE_JWK_PATH")
        .expect("environment variable LINE_PRIVATE_JWK_PATH is not set");
    let line_kid = env::var("LINE_KID").expect("environment variable LINE_KID is not set");
    let line_channel_id =
        env::var("LINE_CHANNEL_ID").expect("environment variable LINE_CHANNEL_ID is not set");
    let line_user_id =
        env::var("LINE_USER_ID").expect("environment variable LINE_USER_ID is not set");

    // === JWTの生成 ===
    // Read private JWK
    let mut f = File::open(&line_private_jwk_path)
        .expect(format!("file not found: {}", line_private_jwk_path).as_str());
    let mut contents = String::new();
    f.read_to_string(&mut contents)
        .expect("failed to read file");
    let jwk: JsonWebKey = contents.parse().expect("failed to parse JWK");
    let key = EncodingKey::from_rsa_pem(jwk.key.to_pem().as_bytes()).expect("failed to create key");

    // Create JWT header
    let header = Header {
        alg: Algorithm::RS256,
        kid: Some(line_kid.to_string()),
        // typはデフォルトで"JWT"のため未指定
        ..Default::default()
    };

    // Create JWT payload (claims)
    let claims = Claims::new(&line_channel_id);

    // Generate JWT
    let token = jsonwebtoken::encode(&header, &claims, &key).expect("failed to encode JWT");

    // === ステートレスチャネルアクセストークンを取得 ===
    // Get line channel access token
    let access_token_client = Client::new();
    let response = access_token_client
        .post("https://api.line.me/oauth2/v3/token")
        .header("Content-Type", "application/x-www-form-urlencoded")
        .query(&[
            ("grant_type", "client_credentials"),
            (
                "client_assertion_type",
                "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
            ),
            ("client_assertion", &token),
        ])
        .send()
        .await
        .expect("failed to request for line channel access token");
    let line_channel_access_token = response
        .json::<serde_json::Value>()
        .await
        .expect("failed to parse line channel access token to json");
    // 成功すれば以下のようなレスポンスが返ってくる
    // {
    //  "token_type": "Bearer",
    //  "access_token": "ey....",
    //  "expires_in": 900
    // }
    let line_channel_access_token = line_channel_access_token["access_token"]
        .as_str()
        .expect("failed to get line channel access token from json");

    // === メッセージの送信 ===
    // Send message request
    let message_client = LineClient::new(&line_channel_access_token);
    let message = LineMessageText::new("Hello, world!");
    let mut builder = LineMessagesBuilder::new();
    builder.append(message);

    message_client
        .message_send_push(&builder.to_push_request(&line_user_id))
        .await
        .expect("failed to send message");

    println!("message sent");
}

ヘッダー(Header)やペイロード(Claims)は、それぞれ以下を参照している

  • ヘッダー

  • ペイロード

また、POSTリクエストで指定しているヘッダーとクエリ文字列は以下を参照している

依存しているクレートは以下の通り

Cargo.toml
[package]
name = "line_stateless_channel_access_token_and_messaging_api"
version = "0.1.0"
edition = "2024"

[dependencies]
chrono = "0.4.41"
jsonwebkey = { version = "0.3.5", features = ["jwt-convert"] }
jsonwebtoken = "9.3.1"
line-bot-messaging-api = "0.1.12"
reqwest = "0.12.23"
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.142"
tokio = { version = "1.47.1", features = ["rt-multi-thread", "macros"] }

主要なクレートの用途は以下の様になる

クレート 用途
jsonwebkey JWTの署名に使用する秘密鍵の読み込み
jsonwebtoken JWTの生成
serde jsonwebtokenでの処理に必要
reqwest チャネルアクセストークンの取得
serde_json チャネルアクセストークンの取得
line-bot-messaging-api チャネルアクセストークンを使用したLINE Messaging APIの使用

実行

実行に際し以下が必要になるため調べておく。いずれもLine Developersコンソールから取得可能(チャネルのチャネル基本設定から確認できる)

  • アサーション署名キーに紐づくID (kid)
  • チャネルID
  • ユーザーID

その上で必要な環境変数を指定する

export LINE_USER_ID='ユーザーID'
export LINE_KID='kid'
export LINE_CHANNEL_ID='チャネルID'
export LINE_PRIVATE_JWK_PATH='保存した秘密鍵への絶対パス'

以下コマンドでメッセージを送信する

cargo run

成功すれば、標準出力で以下が出力されると同時に、LINEにHello, world!とメッセージが飛んでくる

message sent

LINEのスクショの例

markup_1000001045.png

注意

  • jsonwebkeyクレートのexampleではto_encoding_key()が使われている。これを使えば、jsonwebkey→jsonwebtokenのオブジェクト変換が容易にできると思われるが、対応しているjsonwebtokenのバージョンが古いので使えない (おそらく互換性がない)
    • jsonwebtoken::encode()の部分で以下のようなエラーがでる
       error[E0308]: mismatched types
       ...
       note: two different versions of crate `jsonwebtoken` are being used; two types coming from two different versions of the same crate are different types even if they look the same
      
  • 今回はステートレスチャネルアクセストークンを発行するので、JWTのペイロード(Claims)内にtoken_expは不要となる

参考

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?