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クレット
-
actix-web-httpauth:HTTP authentication schemes for actix-web framework.
- リクエストヘッダのAuthorization情報の解析
- 便利なミドルウエアを提供
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を利用できて良かった