use crate::auth::error::TokenVerificationError;
use crate::auth::id_token::verifier::verify_with_key;
use crate::auth::id_token::IdTokenClaims;
use crate::auth::session_cookie::certs::SessionCookieCertCache;
use crate::core::ProjectId;
use jsonwebtoken::{decode_header, Algorithm, DecodingKey};
const SESSION_COOKIE_ISSUER_PREFIX: &str = "https://session.firebase.google.com/";
pub struct SessionCookieVerifier {
project_id: ProjectId,
certs: SessionCookieCertCache,
}
impl SessionCookieVerifier {
pub fn new(project_id: ProjectId, certs: SessionCookieCertCache) -> Self {
Self { project_id, certs }
}
pub async fn verify(&self, cookie: &str) -> Result<IdTokenClaims, TokenVerificationError> {
let header = decode_header(cookie)?;
if header.alg != Algorithm::RS256 {
return Err(TokenVerificationError::InvalidSignature);
}
let kid = header.kid.ok_or(TokenVerificationError::InvalidSignature)?;
let (n, e) = self.certs.public_key(&kid).await?;
let decoding_key = DecodingKey::from_rsa_components(&n, &e)
.map_err(|_| TokenVerificationError::InvalidSignature)?;
verify_with_key(
cookie,
&decoding_key,
self.project_id.as_str(),
SESSION_COOKIE_ISSUER_PREFIX,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use jsonwebtoken::{encode, Algorithm, EncodingKey, Header};
use serde_json::json;
const TEST_PRIVATE_KEY_PEM: &str = include_str!("../../../tests/fixtures/test_private_key.pem");
const TEST_PROJECT_ID: &str = "test-project";
fn decoding_key() -> DecodingKey {
DecodingKey::from_rsa_pem(include_bytes!(
"../../../tests/fixtures/test_public_key.pem"
))
.unwrap()
}
fn now() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs() as i64
}
fn sign(claims: &serde_json::Value) -> String {
let mut header = Header::new(Algorithm::RS256);
header.kid = Some("test-key".to_string());
let key = EncodingKey::from_rsa_pem(TEST_PRIVATE_KEY_PEM.as_bytes()).unwrap();
encode(&header, claims, &key).unwrap()
}
fn valid_session_cookie_claims(now: i64) -> serde_json::Value {
json!({
"iss": format!("{SESSION_COOKIE_ISSUER_PREFIX}{TEST_PROJECT_ID}"),
"aud": TEST_PROJECT_ID,
"iat": now - 10,
"exp": now + 3600,
"auth_time": now - 10,
"sub": "test-uid",
})
}
#[test]
fn accepts_a_valid_session_cookie() {
let token = sign(&valid_session_cookie_claims(now()));
let claims = verify_with_key(
&token,
&decoding_key(),
TEST_PROJECT_ID,
SESSION_COOKIE_ISSUER_PREFIX,
)
.unwrap();
assert_eq!(claims.sub, "test-uid");
}
#[test]
fn rejects_an_id_token_issuer_on_a_session_cookie_endpoint() {
let mut claims = valid_session_cookie_claims(now());
claims["iss"] = json!(format!("https://securetoken.google.com/{TEST_PROJECT_ID}"));
let token = sign(&claims);
let err = verify_with_key(
&token,
&decoding_key(),
TEST_PROJECT_ID,
SESSION_COOKIE_ISSUER_PREFIX,
)
.unwrap_err();
assert!(matches!(err, TokenVerificationError::IssuerMismatch));
}
#[test]
fn rejects_an_expired_session_cookie() {
let mut claims = valid_session_cookie_claims(now());
claims["exp"] = json!(now() - 100);
let token = sign(&claims);
let err = verify_with_key(
&token,
&decoding_key(),
TEST_PROJECT_ID,
SESSION_COOKIE_ISSUER_PREFIX,
)
.unwrap_err();
assert!(matches!(err, TokenVerificationError::Expired));
}
}