3
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?

Cloudflare Workers(Edge) × Rust(WASM) でJWT検証だけを行うSmart Auth Gateway

3
Last updated at Posted at 2026-01-05

はじめに(なぜこれを作ったか)

こんにちは。
WASM(WebAssembly) が気になったので、勉強を兼ねて小さなサンプルを作ってみました。

このサンプルでやりたかったことは、とてもシンプルです。

明らかに不正なリクエストは、
バックエンドに届く前に止めたい

という考え方を、できるだけ小さな構成で形にすることです。

従来のAPI構成の問題点

多くのWeb APIでは、次のような流れが一般的です。

  1. クライアントからリクエストが送られる
  2. バックエンドのAPIサーバがリクエストを受け取る
  3. バックエンドで認証や検証を行う
  4. 不正であればエラーを返す

この構成自体は間違っていません。

ただしこの方法では、不正なリクエストであっても、毎回バックエンドまで到達してしまうという問題があります。
リクエストが増えれば増えるほど、バックエンド側の負荷も増えていきます。

発想を変える:通す前に落とす

そこで発想を少し変えてみます。

そもそも通してはいけないリクエストは、
バックエンドに届く前に止められないか?

この考え方を実現するために使うのが、EdgeWASM です。

Edge とは何か

ここで言う Edge とは、

クライアントからのリクエストを、
バックエンドよりも手前で受け取る場所

のことです。

Edge では次のような処理を行います。

  • HTTPリクエストを受け取る
  • Authorization ヘッダなどを読み取る
  • 処理を続けるか、ここで止めるかを判断する
  • 必要であれば、その場でレスポンスを返す

今回のサンプルでは、Cloudflare Workers 本番環境そのものではなく
ローカル環境で動作する「Edge的な実行環境」を使っています。

Edge という言葉に馴染みがなくても、

「バックエンドの一歩手前で処理する場所」

と考えてもらえれば問題ありません。

なお、Edge という実行モデルについては、以下の記事がとても分かりやすいです。
https://zenn.dev/moutend/articles/97c98a277f4bae

WASM とは?

一方で WASM(WebAssembly) は、まったく別の役割を持ちます。

WASM は、

  • HTTPを知らない
  • リクエストという概念を持たない
  • ヘッダの存在も知らない

という特徴があります。

このサンプルで WASM が行うことは、たった一つです。

渡された文字列が、
正しい JWT かどうかを判定する

JWT の検証ロジックだけを WASM に閉じ込め、
それ以外のことは一切させません。


なぜ役割を分けるのか

このサンプルでは、役割を次のように分けています。

Edge の責務

  • HTTPリクエストの受付
  • ヘッダの取得
  • リクエストを通すか止めるかの判断

WASM の責務

  • JWTが正しいかどうかの判定ロジックのみ

バックエンドの責務

  • すでに検証を通過したリクエストだけを処理

このように分離することで、

  • HTTP処理と認証ロジックが混ざらない
  • 認証ロジックを他の環境でも再利用しやすい
  • 不正なリクエストでバックエンドを無駄に呼ばなくて済む

といったメリットがあります。

このサンプルの位置づけ

Edge や WASM をまだ触ったことがない人でも(自分自身も含めて)

  • なぜこの構成にするのか
  • どこで何をやっているのか

がイメージできることを目指し、できるだけ小さく、シンプルな構成にしています。

「Edge と WASM を使うと、こういう分離ができるのか」という
入り口として読んでもらえたら嬉しいです。


全体構成

Client → Edge(Cloudflare Workers)→(必要なら)Origin

役割分担

Edge(TypeScript): HTTP受付 / Header抽出 / 制御
WASM(Rust): JWT検証ロジックのみ


API仕様(最小)

Request

GET /protected
Authorization: Bearer

Response

OK: 200 OK
NG: 401 Unauthorized


重要:責務分離(設計の芯)

❌ WASMにやらせないこと

  • HTTPヘッダ取得
  • Cookie処理
  • Authorization: Bearer ... のパース
  • リクエスト拒否判断(ステータス / レスポンス生成)

✅ WASMにやらせること

  • JWT文字列 → 正しいか?
  • HS256署名検証
  • exp / aud / iss 検証
  • 結果を 数値(i32)で返す

リポジトリ構成(最小)

edge-wasm-smart-auth/
├─ worker/
│  ├─ worker.ts
│  ├─ auth.wasm
│  ├─ wasm.d.ts
│  └─ wrangler.toml
├─ wasm/
│  ├─ Cargo.toml
│  └─ src/lib.rs
└─ README.md

WASM(Rust):JWT検証「だけ」を実装する

HS256前提で jsonwebtoken を使って検証する。
返り値は Ok / Expired / Invalid に拡張している。

lib.rs

use std::{mem, slice, str};

use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
use serde::Deserialize;

// 最小構成: HS256 共有鍵(本番は Edge から渡す/Secrets 管理を推奨)
static SECRET: &[u8] = b"super-secret-key";
static AUDIENCE: &str = "my-audience";
static ISSUER: &str = "my-issuer";

#[derive(Debug, Deserialize)]
struct Claims {
    sub: String,
    exp: usize,
    aud: String,
    iss: String,
}

// Edge 側が token bytes を置くための領域を確保する。
// - WASMはHTTPを知らない(bytesの受け渡しのみ)
// - Edgeは検証ロジックを知らない(validate_tokenの結果のみ)
#[no_mangle]
pub extern "C" fn alloc(len: usize) -> *mut u8 {
    let mut buf = Vec::<u8>::with_capacity(len);
    let ptr = buf.as_mut_ptr();
    // Rust側で解放されないようにリークさせ、deallocで回収する
    mem::forget(buf);
    ptr
}

// alloc で確保した領域を解放する。
#[no_mangle]
pub extern "C" fn dealloc(ptr: *mut u8, len: usize) {
    unsafe {
        // capacity=len で確保した前提(Edgeが同じlenで呼ぶ)
        drop(Vec::<u8>::from_raw_parts(ptr, 0, len));
    }
}

#[repr(i32)]
pub enum AuthResult {
    Ok = 1,
    Expired = -1,
    Invalid = 0,
}

// JWT を検証する(純粋ロジックのみ)
// 戻り値:
//  1  = OK
//  0  = Invalid
// -1  = Expired
#[no_mangle]
pub extern "C" fn validate_token(ptr: *const u8, len: usize) -> i32 {
    let bytes = unsafe { slice::from_raw_parts(ptr, len) };

    let token = match str::from_utf8(bytes) {
        Ok(v) => v,
        Err(_) => return AuthResult::Invalid as i32,
    };

    let mut validation = Validation::new(Algorithm::HS256);
    validation.set_audience(&[AUDIENCE]);
    validation.set_issuer(&[ISSUER]);

    let result = decode::<Claims>(
        token,
        &DecodingKey::from_secret(SECRET),
        &validation,
    );

    match result {
        Ok(_) => AuthResult::Ok as i32,
        Err(err)
            if matches!(
                err.kind(),
                jsonwebtoken::errors::ErrorKind::ExpiredSignature
            ) =>
        {
            AuthResult::Expired as i32
        }
        Err(_) => AuthResult::Invalid as i32,
    }
}

Edge(Cloudflare Workers)

Edgeは検証の中身を知らない。
知っているのは関数契約(alloc / validate_token / dealloc)のみ。

result に応じてレスポンスを制御する。

switch (result) {
  case 1:
    return 200 OK
  case -1:
    return 401 Token Expired
  default:
    return 401 Unauthorized
}

ビルド(WASM)

rustup target add wasm32-unknown-unknown
cd wasm
cargo build --release --target wasm32-unknown-unknown

起動(Worker)

cd worker
wrangler dev

動作確認

curl -i -H "Authorization: Bearer <JWT>" \
http://127.0.0.1:8787/protected

JWT前提:

aud = my-audience
iss = my-issuer
secret = super-secret-key


実装のポイント

  • WASMはHTTPを知らない(bytes → i32)
  • WASMは状態を持たない(DB / セッションなし)
  • Edgeが制御を握る(許可 / 拒否 / Refresh導線)
  • WASMは再利用できる(他Gatewayでも使い回せる)

まとめ

  • Edge × WASM の本質は高速化ではない。
  • 責務分離・安全性・差し替え可能性を最小構成で実現できる点に価値がある。

サンプルコードです。
ぜひ実際に動かしてみてください。

3
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
3
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?