firebase-token 0.5.0

Auth with firebase
Documentation
use std::time::Duration;

use reqwest::header::HeaderMap;
use serde::Deserialize;
use thiserror::Error;

const JWK_URL: &str =
    "https://www.googleapis.com/service_accounts/v1/jwk/securetoken@system.gserviceaccount.com";

#[derive(Debug, Deserialize)]
pub(crate) struct JwkKeys {
    pub(crate) keys: Vec<JwkKey>,
    pub(crate) max_age: Option<Duration>,
}

#[derive(Debug, Deserialize)]
pub(crate) struct JwkKey {
    pub(crate) e: String,
    pub(crate) alg: String,
    pub(crate) kid: String,
    pub(crate) n: String,
}

#[derive(Debug, Deserialize)]
struct KeyResponse {
    keys: Vec<JwkKey>,
}

#[derive(Error, Debug)]
pub enum JwkKeysError {
    #[error("Fetch `{0}`")]
    Fetch(reqwest::Error),
    #[error("NoCacheControlKey")]
    NoCacheControlKey,
    #[error("InvalidCacheControlValue `{0}`")]
    InvalidCacheControlValue(reqwest::header::ToStrError),
    #[error("InvalidMaxAge `{0}`")]
    InvalidMaxAge(String),
    #[error("Parse `{0}`")]
    Parse(reqwest::Error),
}

impl JwkKeys {
    pub(crate) async fn fetch_keys() -> Result<Self, JwkKeysError> {
        let response = reqwest::get(JWK_URL).await.map_err(JwkKeysError::Fetch)?;

        let max_age = match parse_max_age(response.headers()) {
            Ok(v) => Some(v),
            Err(e) => {
                tracing::error!("could not parse max_age: {:?}", e);
                None
            }
        };

        let public_keys = response
            .json::<KeyResponse>()
            .await
            .map_err(JwkKeysError::Parse)?;

        Ok(JwkKeys {
            keys: public_keys.keys,
            max_age,
        })
    }
}

fn parse_max_age(headers: &HeaderMap) -> Result<Duration, JwkKeysError> {
    let cache_control = match headers.get("Cache-Control") {
        Some(header_value) => header_value.to_str(),
        None => return Err(JwkKeysError::NoCacheControlKey),
    };

    let cache_control_value = match cache_control {
        Ok(v) => v,
        Err(err) => return Err(JwkKeysError::InvalidCacheControlValue(err)),
    };

    for v in cache_control_value.split(',') {
        let mut pair = v.split('=').map(|s| s.trim());
        let key = match pair.next() {
            Some(v) => v,
            None => continue,
        };

        if key.to_lowercase() != "max-age" {
            continue;
        }

        let value = match pair.next().and_then(|v| v.parse().ok()) {
            Some(v) => v,
            None => continue,
        };
        return Ok(Duration::from_secs(value));
    }

    Err(JwkKeysError::InvalidMaxAge(cache_control_value.to_owned()))
}