LoginSignup
11
4

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-05-14

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

1. 基本知識

1-1. firebase認証とは

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

1-2. JWT情報と

Json Web Tokenの略語である。

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

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

2-1. フロント側

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

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クレット
2-2-3. 検証用の実装(actix-webベース)
  • 参考した実装:actix-diesel-auth

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

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取得やバリデーションを行なう
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を定義
# 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
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
#[macro_use]
extern crate diesel;
extern crate dotenv;

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

  • routes.rs
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
pub mod health;
pub mod hoge;
  • controllers/hoge.rs
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
[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.感想

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

参考記事

11
4
1

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
11
4