#![deny(
missing_debug_implementations,
missing_copy_implementations,
trivial_casts,
trivial_numeric_casts,
unsafe_code,
unused_imports,
unstable_features,
unused_import_braces,
unused_qualifications
)]
mod bearer_token;
pub use bearer_token::BearerToken;
pub mod errors;
pub mod jwk;
pub mod plugins;
use crate::errors::{Error, InvalidJwt};
#[cfg(feature = "env")]
pub use dotenvy;
#[cfg(feature = "env")]
pub use serde_json;
#[cfg(feature = "encode")]
use chrono::Utc;
pub use jsonwebtoken::{decode_header, Algorithm, DecodingKey, Validation};
#[cfg(feature = "encode")]
use jsonwebtoken::{EncodingKey, Header};
use serde::{Deserialize, Serialize};
#[cfg(feature = "env")]
use std::{env, fs::read_to_string};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FirebaseToken {
pub aud: String,
pub iss: String,
pub iat: u64,
pub exp: u64,
pub sub: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub azp: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub email: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub email_verified: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub family_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub given_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub at_hash: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub hd: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub locale: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub nonce: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub picture: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub profile: Option<String>,
}
impl FirebaseToken {
pub const ISSUER_IDENTIFIER: &'static str = "https://accounts.google.com";
#[cfg(feature = "encode")]
pub fn new(uid: &str, project_id: &str) -> Self {
let iat = Utc::now().timestamp() as u64;
Self {
aud: project_id.to_string(),
iat,
exp: iat + (60 * 60),
iss: Self::ISSUER_IDENTIFIER.to_string(),
sub: uid.to_string(),
azp: None,
email: None,
email_verified: None,
family_name: None,
given_name: None,
at_hash: None,
hd: None,
locale: None,
name: None,
nonce: None,
picture: None,
profile: None,
}
}
}
#[cfg(feature = "encode")]
#[derive(Debug)]
pub struct EncodedToken(pub String);
#[derive(Debug, Clone, Deserialize, Default)]
pub struct FirebaseAdminCredentials {
pub project_id: String,
pub private_key_id: String,
pub private_key: String,
pub client_email: String,
pub client_id: String,
}
impl FirebaseAdminCredentials {
pub fn new(
project_id: String,
private_key_id: String,
private_key: String,
client_email: String,
client_id: String,
) -> Self {
Self {
project_id,
private_key_id,
private_key,
client_email,
client_id,
}
}
}
#[derive(Debug, Clone)]
pub struct FirebaseAuth {
pub(crate) admin_credentials: FirebaseAdminCredentials,
pub jwks_url: String,
pub(crate) client: reqwest::Client,
}
impl Default for FirebaseAuth {
fn default() -> Self {
let client = reqwest::Client::new();
Self {
admin_credentials: FirebaseAdminCredentials::default(),
jwks_url: Self::JWKS_URL.to_string(),
client,
}
}
}
#[derive(Debug, Clone)]
pub enum EnvSource {
Var,
#[cfg(feature = "env")]
Env {
file_path: String,
variable: String,
},
#[cfg(feature = "env")]
Json(String),
}
#[derive(Debug, Clone)]
pub struct FirebaseAuthBuilder {
admin_credentials: FirebaseAdminCredentials,
jwks_url: String,
env_source: EnvSource,
}
impl Default for FirebaseAuthBuilder {
fn default() -> Self {
Self {
admin_credentials: FirebaseAdminCredentials::default(),
jwks_url: FirebaseAuth::JWKS_URL.to_string(),
#[cfg(feature = "env")]
env_source: EnvSource::Var,
}
}
}
impl FirebaseAuthBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn admin_credentials(mut self, admin_credentials: FirebaseAdminCredentials) -> Self {
self.admin_credentials = admin_credentials;
self
}
pub fn jwks_url(mut self, jwks_url: &str) -> Self {
self.jwks_url = jwks_url.to_string();
self
}
#[cfg(feature = "env")]
pub fn env(mut self, variable_name: &str) -> Self {
self.env_source = EnvSource::Env {
file_path: ".env".to_string(),
variable: variable_name.to_string(),
};
self
}
#[cfg(feature = "env")]
pub fn env_file(mut self, filepath: &str, variable_name: &str) -> Self {
self.env_source = EnvSource::Env {
file_path: filepath.to_string(),
variable: variable_name.to_string(),
};
self
}
#[cfg(feature = "env")]
pub fn json_file(mut self, filepath: &str) -> Self {
self.env_source = EnvSource::Json(filepath.to_string());
self
}
pub fn build(self) -> Result<FirebaseAuth, Error> {
let credentials =
match self.env_source {
#[cfg(feature = "env")]
EnvSource::Env {
file_path,
variable,
} => {
dotenvy::from_filename(file_path)?;
env::var(variable)
.map_err(Error::from)
.and_then(|credentials| {
serde_json::from_str::<FirebaseAdminCredentials>(&credentials)
.map_err(Error::from)
})
}
#[cfg(feature = "env")]
EnvSource::Json(filepath) => read_to_string(filepath)
.map_err(Error::from)
.and_then(|credentials| {
serde_json::from_str::<FirebaseAdminCredentials>(&credentials)
.map_err(Error::from)
}),
_ => Ok(self.admin_credentials),
}?;
Ok(FirebaseAuth {
admin_credentials: credentials,
jwks_url: self.jwks_url,
client: reqwest::Client::new(),
})
}
}
impl FirebaseAuth {
pub const JWKS_URL: &'static str =
"https://www.googleapis.com/service_accounts/v1/jwk/securetoken@system.gserviceaccount.com";
pub fn new(credentials: FirebaseAdminCredentials) -> Self {
Self {
admin_credentials: credentials,
jwks_url: Self::JWKS_URL.to_string(),
client: reqwest::Client::new(),
}
}
pub fn builder() -> FirebaseAuthBuilder {
FirebaseAuthBuilder::new()
}
#[cfg(feature = "encode")]
pub fn encode(&self, uid: &str) -> Result<EncodedToken, Error> {
let header = Header {
kid: Some(self.admin_credentials.private_key_id.clone()),
..Header::new(Algorithm::RS256)
};
EncodingKey::from_rsa_pem(self.admin_credentials.private_key.as_bytes())
.and_then(|key| {
jsonwebtoken::encode(
&header,
&FirebaseToken::new(uid, &self.admin_credentials.project_id),
&key,
)
})
.map(EncodedToken)
.map_err(Error::from)
}
pub async fn verify(&self, token: &str) -> Result<FirebaseToken, Error> {
let mut validation = Validation::new(Algorithm::RS256);
validation.set_issuer(&[format!(
"https://securetoken.google.com/{}",
&self.admin_credentials.project_id
)]);
validation.set_audience(&[&self.admin_credentials.project_id]);
let kid = decode_header(token)
.map_err(Error::from)
.and_then(|header| header.kid.ok_or(Error::InvalidJwt(InvalidJwt::MissingKid)))?;
let jwk = self.jwks().await.and_then(|mut key_map| {
key_map
.remove(&kid)
.ok_or(Error::InvalidJwt(InvalidJwt::MatchingJwkNotFound))
})?;
DecodingKey::from_rsa_components(&jwk.n, &jwk.e)
.and_then(|key| jsonwebtoken::decode::<FirebaseToken>(token, &key, &validation))
.map(|data| data.claims)
.map_err(Error::from)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn should_fail_with_invalid_env_var() {
let firebase_auth = FirebaseAuth::builder()
.env_file(".env", "INVALID_VAR_NAME")
.build();
assert!(firebase_auth.is_err());
}
#[test]
fn should_fail_with_invalid_json_contents() {
let firebase_auth = FirebaseAuth::builder()
.json_file("tests/env_files/firebase-creds.empty.json")
.build();
assert!(firebase_auth.is_err());
}
#[test]
fn should_succeed_with_set_jwks_url() {
let firebase_auth = FirebaseAuth::builder()
.json_file("tests/env_files/firebase-creds.json")
.jwks_url("some_dummy_value")
.build()
.unwrap();
assert_eq!(firebase_auth.jwks_url, "some_dummy_value");
}
}