rust言語によるapiサーバ側でfirebase認証情報(JWT)を検証

firebaseクラウドサービスでログイン認証が成功した後、フロントからサーバAPIを呼び出すときに、それを有効なユーザであるかをチェックしないできない。本文はrust言語で対応する作業内容をメモする。

# 1. 基本知識
#### 1-1. firebase認証とは

googleのクラウドサービスで、ログイン認証はそれに任せてほぼゼロ開発でユーザ認証機能が完成できる。それと似てるもう１つ有名なサービスはAuth0。

#### 1-2. JWT情報と

Json Web Tokenの略語である。

JWT ではいかなる情報も暗号化されず、そのまま (base64 エンコードされた状態で) 保持される。サーバーしか持っていない秘密鍵で作られる署名やメッセージ認証コードを横に置くことで、情報の有効性をチェックするという仕組みである。

# 2. rustでfirebase認証情報(JWT)の検証

#### 2-1. フロント側

firebase認証のIDトークンを取得してhttpリクエストヘッダAuthorizationに載せる。

```js
const { getIdToken } = useAuth();
...
// 通信処理
const idToken = await getIdToken(); // idTokenはJWT情報
  console.log('idToken:', idToken);
  axios.get('/api/hoge', {
    headers: {
      Authorization: `Bearer ${idToken}`,
    },
  }).then((response) => {
    console.log(response);
});
```

通信ヘッダはこんな感じ。

```
Authorization: Bearer <token>
```

#### 2-2. APIサーバ側

##### 2-2-1. 処理内容
- リクエストヘッダのAuthorization情報を取得
- JWKSを取得（ネット経由）
- バリデーション（署名情報、JWT中身情報など）

##### 2-2-2. 利用するRustクレット
- [actix-web-httpauth:HTTP authentication schemes for actix-web framework.](https://github.com/actix/actix-web-httpauth)
  - リクエストヘッダのAuthorization情報の解析
  - 便利なミドルウエアを提供

##### 2-2-3. 検証用の実装(actix-webベース)

- 参考した実装：[actix-diesel-auth](https://github.com/idatte2018/actix-diesel-auth)

- main.rsでHttpAuthentication::bearerミドルウエアを生成してappのwrapメソッドで登録する

```rust
use actix_web::{dev::ServiceRequest, web, App, Error, HttpServer};
use mypkg::routes;

use actix_web_httpauth::extractors::bearer::{BearerAuth, Config};
use actix_web_httpauth::extractors::AuthenticationError;
use actix_web_httpauth::middleware::HttpAuthentication;

mod auth;
mod errors;

async fn validator(req: ServiceRequest, credentials: BearerAuth) -> Result<ServiceRequest, Error> {
    let config = req.app_data::<Config>().cloned().unwrap_or_default();

    println!("req.app_data::<Config>():{:?}", req.app_data::<Config>());
    println!("credentials.token():{}", credentials.token());

    match auth::validate_token(credentials.token()).await {
        Ok(res) => {
            if res {
                Ok(req)
            } else {
                Err(AuthenticationError::from(config).into())
            }
        }
        Err(_) => Err(AuthenticationError::from(config).into()),
    }
}

#[actix_rt::main]
async fn main() -> std::io::Result<()> {
    dotenv::dotenv().ok();
    std::env::set_var("RUST_LOG", "actix_web=debug");

    HttpServer::new(|| {
        let auth = HttpAuthentication::bearer(validator);
        App::new()
            .service(web::scope("/api").wrap(auth).configure(routes::api_routes)) // 認証要のスコープに限定
            .configure(routes::health_routes) // 認証不要
    })
    .bind("localhost:8080")?
    .run()
    .await
}

```

- auth.rsでfirebaseのjws取得やバリデーションを行なう

```rust
use crate::errors::ServiceError;
use alcoholic_jwt::{token_kid, validate, Validation, JWKS};
use serde::{Deserialize, Serialize};
use std::error::Error;

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

pub async fn validate_token(token: &str) -> Result<bool, ServiceError> {
    let jwk_url = std::env::var("JWK_URL").expect("JWK_URL must be set");
    let jwk_issuer = std::env::var("JWK_ISSUER").expect("JWK_ISSUER must be set");
    println!("jwk_url:{}", jwk_url);
    println!("jwk_issuer:[{}]", jwk_issuer);

    let jwks = fetch_jwks(jwk_url.as_str())
        .await
        .expect("failed to fetch jwks");

    // issとsub（user_id）をチェック
    // 必須ではなく、カスタマイズできる
    let validations = vec![Validation::Issuer(jwk_issuer), Validation::SubjectPresent];

    let kid = match token_kid(token) {
        Ok(res) => res.expect("failed to decode kid"),
        Err(_) => return Err(ServiceError::JWKSFetchError),
    };

    println!("kid:{:?}", kid);

    let jwk = jwks.find(&kid).expect("Specified key not found in set");

    println!("jwk:{:?}", jwk);

    let res = validate(token, jwk, validations);
    println!("res.is_ok():{}", res.is_ok());
    Ok(res.is_ok())
}

async fn fetch_jwks(uri: &str) -> Result<JWKS, Box<dyn Error>> {
    let res = reqwest::get(uri).await?;
    let val = res.json::<JWKS>().await?;
    Ok(val)
}
```

- .envでJWKのURLを定義

```rust
# JWT設定
JWK_URL=https://www.googleapis.com/service_accounts/v1/jwk/securetoken@system.gserviceaccount.com
// JWTと同じ内容を設定（一致するかをチェックのため）
JWK_ISSUER=https://securetoken.google.com/hogehoge-prod
```

- errors.rs

```rust
use actix_web::{error::ResponseError, HttpResponse};
use derive_more::Display;

#[derive(Debug, Display)]
pub enum ServiceError {
    #[display(fmt = "Internal Server Error")]
    InternalServerError,

    #[display(fmt = "BadRequest: {}", _0)]
    BadRequest(String),

    #[display(fmt = "JWKSFetchError")]
    JWKSFetchError,
}

// impl ResponseError trait allows to convert our errors into http responses with appropriate data
impl ResponseError for ServiceError {
    fn error_response(&self) -> HttpResponse {
        match self {
            ServiceError::InternalServerError => {
                HttpResponse::InternalServerError().json("Internal Server Error, Please try later")
            }
            ServiceError::BadRequest(ref message) => HttpResponse::BadRequest().json(message),
            ServiceError::JWKSFetchError => {
                HttpResponse::InternalServerError().json("Could not fetch JWKS")
            }
        }
    }
}
```

- lib.rs

```rust
#[macro_use]
extern crate diesel;
extern crate dotenv;

pub mod controllers;
pub mod errors;
pub mod routes;

```

- routes.rs

```rust
use crate::controllers::health;
use crate::controllers::hoge;
use actix_web::web;

pub fn api_routes(cfg: &mut web::ServiceConfig) {
    cfg.route("/hoge", web::get().to(hoge::test));
}

pub fn health_routes(cfg: &mut web::ServiceConfig) {
    cfg.route("/health", web::get().to(health::check));
}
```

- controllers.rs

```rust
pub mod health;
pub mod hoge;
```

- controllers/hoge.rs

```rust
use crate::errors::ServiceError;
use actix_web::HttpResponse;

pub async fn test() -> Result<HttpResponse, ServiceError> {
    Ok(HttpResponse::Created().body("test"))
}

- controllers/health.rs

```rust
use crate::errors::ServiceError;
use actix_web::HttpResponse;

pub async fn check() -> Result<HttpResponse, ServiceError> {
    Ok(HttpResponse::Created().body("OK"))
}

```

- Cargo.toml

```toml
[package]
name = "mypkg"
version = "0.1.0"
edition = "2021"

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

[dependencies]
actix-web = "4.0.1"
actix-rt = "2.7.0"
serde = { version = "1.0.136", features = ["derive"] }
serde_json = "1.0.79"
dotenv = "0.15.0"
chrono = { version = "0.4.19", features = ["serde"] }
time = { version = "0.3.9" }
reqwest = { version = "0.11.10", features = ["json"] }
actix-web-httpauth = "0.6.0"
alcoholic_jwt = "1.0.0"
derive_more = "0.99.2"
url = "2.2.2"
http = "0.2"
```

# 3.感想
- ３年前の英語記事はまだ独自実装だったが、actix-web-httpauthを利用できて良かった

# 参考記事
- [Build an API in Rust with JWT Authentication](https://auth0.com/blog/build-an-api-in-rust-with-jwt-authentication-using-actix-web/)
- [Rust jwtの発行と検証](https://www.letitride.jp/entry/2020/12/14/122527)
- [Rustのactix-webでJWTを試してみた](https://steavevaivai.hatenablog.com/entry/2021/09/20/200245)
- [JWTの仕組み](https://www.wakuwakubank.com/posts/523-it-jwt/)