このサイトは、只今WEB業界で活躍中のデザイナー、プログラマーの方々の情報を集めたweb統合情報サイトです。

Archives Details

Rust – Actix Web JWT 認証認可 APIの作成

Rust

2024.02.25

この記事は最終更新日から1年以上が経過しています。

どもです。

いやぁ、もう2月も終わるということで、本当に時間の流れを早く感じております。

業務ではRustでソフトウェアを作成しておりますが、趣味ではRustでサーバー開発も行っていまして、今回も前回に引き続きRustのサーバーフレームワークである、Actix Webについてツラツラと書いて行こうかと思います。

今回は、Actix Webを用いた簡易なJWT認証認可のAPIを作成していこうと思います。

JWTに関して

まず、JWTに関して軽くご説明をと。

JWT(JSON Web Token)は、Webアプリケーションやサービス間での安全な情報の伝達を目的とした、コンパクトで自己完結型のトークンフォーマットです。主に認証や情報交換に使用されます。JWTは、JSON形式で構造化されたデータを含んでおり、以下の三部分から構成されています。

  1. ヘッダー(Header):
    • トークンのタイプ(通常はJWT)と、使用されている署名アルゴリズム(例えば、HMAC SHA256やRSA)が含まれます。
    • JSON形式で記述され、Base64Url方式でエンコードされます。
  2. ペイロード(Payload):
    • トークンに含める実際のデータが含まれます。
    • 通常、ユーザー識別情報や有効期限などのクレーム(主張)が含まれます。
    • クレームは三種類に分けられます:登録済みクレーム(予約された名前のセット)、公開クレーム(共有情報用)、プライベートクレーム(送信者と受信者の間で合意した情報)。
    • これもJSON形式で記述され、Base64Url方式でエンコードされます。
  3. 署名(Signature):
    • JWTの整合性と検証可能性を確保するための署名が含まれます。
    • ヘッダーのエンコードされた値、ペイロードのエンコードされた値、秘密鍵(または公開鍵/秘密鍵ペア)を使用して生成されます。
    • 署名アルゴリズムはヘッダーで指定されたものを使用します。

JWTは、そのコンパクトな形式と自己完結型の特性により、URL、POSTパラメータ、HTTPヘッダーなどを介して簡単に送信することができます。また、署名されているため、トークンの内容が途中で改ざんされることはなく、送信者がトークンの内容を保証します。ただし、ペイロードはエンコードされているだけで暗号化されているわけではないため、機密性が必要な情報は含めるべきではありません。

JWTは、特にシングルサインオン(SSO)やOAuthなどの認証フレームワークで広く使われています。という訳で、昨今では様々なシーンで幅広く用いられるJWTを用いてAPIを作成していきましょう。

クレートなど準備

それでは、早速作成していきましょう。

今回の作成したソースはこちらとなります。

ソースだけ確認したい方はこちらをご参照ください。

https://github.com/rust-game-samples/actix_web_sample/tree/main/jwt_auth

今回使用するcrate

Cargo.toml

[package]
name = "jwt_auth"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
serde = { version = "1.0", features = ["derive"] }
actix-web = "4"
actix-web-httpauth = "*"
jwt-simple = "0.10"
rust-crypto = "0.2"
derive_more = "0.99.17"
chrono = "0.4.31"

[dependencies.rusqlite]
version = "*"
features = ["bundled"]

[dependencies.uuid]
version = "1.2.2"
features = [
    "v4",
    "fast-rng",
    "macro-diagnostics",
]

 

API

今回のAPIはシンプルに3つ。

  • JWT アクセストークンを生成するAPI
  • JWT リフレッシュを生成するAPI
  • JWT認可のみアクセス可のAPI
エンドポイント

POST /token/

POST /token/refresh

GET /hello

ディレクトリ構成は以下の通り。

jwt_auth/src/
├── api
│   ├── mod.rs
│   └── token.rs
├── constants.rs
├── error.rs
├── main.rs
├── model
│   ├── mod.rs
│   └── token.rs
└── utils
    ├── mod.rs
    └── token.rs

main.rsには、APIの処理を行う「create_token」「refresh_token」「hello」メソッドを用意。

src/main.rs

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .service(
                web::scope("/token")
                    .service(create_token)
                    .service(refresh_token),
            )
            .service(hello)
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

create_token

まずは、「create_token」メソッドから。

通常は、usernameとpasswordをリクエストで受け、処理を行うのですが、今回は簡易な認証ということで、usernameとpasswordは固定で取り敢えず、リクエストが来るとすべて通過するようにしておきます。

src/api/token.rs

#[post("/")]
async fn create_token(data: web::Json<CreateTokenRequest>) -> Result<HttpResponse, ServiceError> {
    if !(data.username == "daisuke" && data.password == "1234") {

usernameとpasswordの認証が通過できると、アクセストークンとリフレッシュトークンを発行します。create_access_tokenメソッドと、create_refresh_tokenメソッドはsrc/utilsに定義

let token = create_access_token()?;
let refresh = create_refresh_token()?;

Ok(HttpResponse::Ok().json(CreateTokenResponse {
    token,
    refresh_token: refresh,
}))

create_custom_claimsメソッドでclaimを作成します。

今回は、有効期限をアクセストークンは15分間、リフレッシュトークンには24時間に設定しておきます。アクセストークンの有効期限が切れるとリフレッシュトークンを用いてアクセストークンを更新するフローとなります。

utils/token.rs

pub fn create_access_token() -> Result<String, ServiceError> {
    let claims = create_custom_claims(false, Duration::from_mins(15));
    claims_authenticate(claims)
}

pub fn create_refresh_token() -> Result<String, ServiceError> {
    let claims = create_custom_claims(true, Duration::from_hours(24));
    claims_authenticate(claims)
}

 

create_custom_claims、claims_authenticate

jwt-simpleクレートを用いて独自のClaimsを作成し、JWTを作成していきます。

アクセストークンのis_refreshフラグはfalse、リフレッシュトークンのis_refreshフラグはtrueを受取、それぞれの有効期限を受取り、is_refreshフラグを含めたカスタムなclaimsを作成します。

create_custom_claimsメソッド

fn create_custom_claims(is_refresh: bool, duration: Duration) -> JWTClaims<TokenClaims> {
    Claims::with_custom_claims(
        TokenClaims {
            refresh: is_refresh,
        },
        duration,
    )
    .with_subject(1)
    .with_jwt_id(Uuid::new_v4().to_string())
}

カスタムclaimsを含めたpayloadを元にJWTの生成を行っていきます。

create_token_key()で、 JWTのSignatureを生成し、claims_authenticateメソッドにて、claimsを引数で渡しJWTを作成していきます。

claims_authenticateメソッド

fn claims_authenticate(claims: JWTClaims<TokenClaims>) -> Result<String, ServiceError> {
    if let Ok(token) = create_token_key().authenticate(claims) {
        Ok(token)
    } else {
        Err(ServiceError::InternalServerError {
            error_message: MESSAGE_PROCESS_TOKEN_ERROR.to_string(),
        })
    }
}

create_token_keyメソッドで、秘密鍵を元に jwt-simpleクレートのHS256アルゴリズムを用いて、Signatureを生成します**。**「”your_secret_key”」が独自で設定する秘密鍵となり、こちらが漏れてしまったりすると、トークンの不正生成などができてしまうので、外部に漏れないように注意が必要となります。

今回は簡易なので「”your_secret_key”」の固定文字列で生成。

fn create_token_key() -> HS256Key {
    HS256Key::from_bytes(b"your_secret_key")
}

といった流れで、ヘッダー(Header)、ペイロード(Payload)、署名(Signature)、を含んだJWT(JSON Web Token)の生成が行えました。

生成したアクセストークン(token)とリフレッシュトークン(refresh_token)を、クライアントのレスポンスにJSONで返却します。クライアント側はこちらを用いて認可APIにアクセスします。

{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3MDY4NDEwODUsImV4cCI6MTcwNjg0MTE0NSwibmJmIjoxNzA2ODQxMDg1LCJzdWIiOiIxIiwianRpIjoiOGNhZDk2MmItYWVhYy00MmMzLWFjNTgtOGYwNTdkODg0YmQzIiwicmVmcmVzaCI6ZmFsc2V9.mm1hUxevMWoWaNhSCfzKEmry6117Fc355AMxnSZ6E6A",
  "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3MDY4NDEwODUsImV4cCI6MTcwNjkyNzQ4NSwibmJmIjoxNzA2ODQxMDg1LCJzdWIiOiIxIiwianRpIjoiZWMxMmRhZTItZWE1Ni00NGZjLWI3ZmQtNTk4NDkyMjc3YmExIiwicmVmcmVzaCI6dHJ1ZX0.OLyYaST_mkIMbZYUU6-QCfT6dYT3URmoUERGQJ5Kwl4"
}

hello

helloのAPIは、JWT認可されたAPIとなるので、HttpRequestのheaderセクションに生成されたJWTを付与してリクエストする形となります。

先程生成されたアクセストークンを用いて、headerセクションのAuthorizationに付与しリクエストを行います。

Authorization [JWT token]

「 [JWT token]」は、Bearerの文字列、半角スペースの後に生成されたアクセストークンををふよします。

Bearer eyJhbGciOiJIUzI1NiIs...

helloメソッドはアクセストークンを検証し、問題なければ「”Authorized Access Success! Hello World!”)」の文字列を返却する流れとなります。

#[get("/hello")]
async fn hello(req: HttpRequest) -> Result<HttpResponse, ServiceError> {
    let token = get_token(req)?;
    let claims = claims_verify_token(&token)?;
    if claims.custom.refresh {
        return Err(ServiceError::BadRequest {
            error_message: MESSAGE_REFRESH_TOKEN_ERROR.to_string(),
        });
    }
    Ok(HttpResponse::Ok().body("Authorized Access Success! Hello World!"))
}

まずは、get_tokenでHttpRequestのheaderセクションのAuthorizationに付与されたJWTを取り出します。Authorization のValueより、「Bearer」または「bearer」を判定、そちらの文字列 をtrimし、トークンを取り出します。

utils/token.rs

pub fn get_token(req: HttpRequest) -> Result<String, ServiceError> {
    if let Some(auth_header) = req.headers().get(AUTHORIZATION) {
        if let Ok(auth_str) = auth_header.to_str() {
            if is_auth_header_valid(auth_header) {
                let token = auth_str[6..auth_str.len()].trim();
                return Ok(token.to_string());
            }
        }
    }
    Err(ServiceError::InternalServerError {
        error_message: MESSAGE_PROCESS_TOKEN_ERROR.to_string(),
    })
}

is_auth_header_validメソッド

pub fn is_auth_header_valid(auth_header: &HeaderValue) -> bool {
    if let Ok(auth_str) = auth_header.to_str() {
        return auth_str.starts_with("bearer") || auth_str.starts_with("Bearer");
    }
    false
}

claims_verify_tokenメソッドでリクエストされたトークンを検証します。

create_token_keyメソッドは先程のトークン生成でも用いたメソッドとなりますので、ここでSignatここが異なると認可エラーとなる形になります。

claims_verify_token

pub fn claims_verify_token(token: &str) -> Result<JWTClaims<TokenClaims>, ServiceError> {
    if let Ok(claims) = create_token_key().verify_token::<TokenClaims>(token, None) {
        Ok(claims)
    } else {
        Err(ServiceError::Unauthorized {
            error_message: MESSAGE_TOKEN_MISSING.to_string(),
        })
    }
}

refresh

最後は、リフレッシュトークンを生成する流れとなります。アクセストークンは15分の有効期限しかないため、有効期限が切れた場合、リフレッシュトークンを用いて新たにアクセストークンを生成します。

先程のhelloAPIとほぼ同じですが、異なる点はpayloadのclaimsに設定した「refresh」がTrueかどうかになります。

#[post("/refresh")]
async fn refresh_token(req: HttpRequest) -> Result<HttpResponse, ServiceError> {
    let request_token = get_token(req)?;
    let claims = claims_verify_token(&request_token)?;
    if !claims.custom.refresh {
        return Err(ServiceError::BadRequest {
            error_message: MESSAGE_REFRESH_TOKEN_ERROR.to_string(),
        });
    }
    let token = create_access_token()?;

    Ok(HttpResponse::Ok().json(CreateRefreshTokenResponse { token }))
}

refreshトークンの場合は、refreshがtrue。

 

refreshトークンのpayload

{
  "iat": 1706841085,
  "exp": 1706841145,
  "nbf": 1706841085,
  "sub": "1",
  "jti": "8cad962b-aeac-42c3-ac58-8f057d884bd3",
  "refresh": true
}

refreshトークンではない場合は、エラーを返却。

if !claims.custom.refresh {

refreshトークンを検証し、問題なければ、最初にも実行したcreate_access_tokenで再びアクセストークンを生成します。

pub fn create_access_token() -> Result<String, ServiceError> {
    let claims = create_custom_claims(false, Duration::from_mins(15));
    claims_authenticate(claims)
}

成功するとアクセストークンが返却されますので、クライアント側で更新処理を行えば再び認可リクエストが行う形となります。

といった形でざっくり説明で申し訳ありませんでしたが、簡易のJWT認証認可のAPIが作成できました。

次回は、MongoDBを用いて登録されたユーザーでの認証認可API作成を行って行きたいと思います。

ではではぁ。

 

Comment

Related Article

Rustでつくる ふっかつのじゅもん

2025.04.29

Tauri(Rust) × AI で作る GitGUIクライアントアプリ その5

2025.04.14

Tauri(Rust) × AI で作る GitGUIクライアントアプリ その4

2025.04.10

Tauri(Rust) × AI で作る GitGUIクライアントアプリ その3

2025.04.08

Tauri(Rust) × AI で作る GitGUIクライアントアプリ その2

2025.04.07

Tauri(Rust) × AI で作る GitGUIクライアントアプリ その1

2025.04.05

keyring-rsで、Macのキーチェーンに登録する。

2025.04.04

RustとWebAssemblyによるゲーム開発 Webpack5対応

2025.03.27

Rustで創る MOS 6502 CPU その3 (Cursorと共に)

2025.02.24

Rust-SDL2 examplesをすべて試す

2024.09.01

CATEGORY LIST

LATEST NEWS

Rustでつくる ふっかつのじゅもん

Rust

2025.04.29

Tauri(Rust) × AI で作る GitGUIクライアントアプリ その5

Rust

2025.04.14

う、動くぞ! Mac mini Apple M4 Pro で PS3ソフトを遊ぶ。RPCS3 Mac版を起動

Game

2025.04.12

Tauri(Rust) × AI で作る GitGUIクライアントアプリ その4

Rust

2025.04.10

Tauri(Rust) × AI で作る GitGUIクライアントアプリ その3

Rust

2025.04.08

Tauri(Rust) × AI で作る GitGUIクライアントアプリ その2

Rust

2025.04.07

時代を先取りし過ぎた ニューラルネットワークが導入されたゲーム『がんばれ森川君2号』を令和に嗜んでみる。

Game

2025.04.06

Tauri(Rust) × AI で作る GitGUIクライアントアプリ その1

Rust

2025.04.05

keyring-rsで、Macのキーチェーンに登録する。

Rust

2025.04.04

RustとWebAssemblyによるゲーム開発 Webpack5対応

Rust

2025.03.27

Rustで創る MOS 6502 CPU その3 (Cursorと共に)

Rust

2025.02.24

あけましておめでとうございます(24日経過)

イベント

2025.01.24

RANKING

Follow

SPONSOR

現在、掲載募集中です。



Links

About Us

WEBデザイナーの、WEBデザイナーによる、WEBデザイナーの為のサイト。「みんなで書こう!」と仲間を募ってみたが、結局書くのは自分だけとなってしまいました。日々のメモを綴っていきます。

Entry Profile

Graphical FrontEnd Engineer
- Daisuke Takayama

MAD CITY 北九州市で生まれ育つ。20代はバンド活動に明け暮れ、ふと「webデザイナーになりたい。」と思い、デジタルハリウッド福岡校入学。卒業後、数々の賞を受賞、web業界をざわつかせる。
現在、主に、ゲーム制作中心に港区六本木界隈で活動中。

FOLLOW US