use crate::auth::custom_token::CustomTokenSigner;
use crate::auth::error::AuthError;
use crate::auth::id_token::{IdTokenClaims, IdTokenVerifier, JwksCache};
use crate::auth::identity_toolkit::IdentityToolkitEndpoints;
use crate::auth::mode::ClientMode;
use crate::auth::session_cookie::{SessionCookieCertCache, SessionCookieVerifier};
use crate::auth::users::{
CreateUserRequest, UpdateUserRequest, UserOperations, UserPage, UserRecord,
};
use crate::core::{Credentials, HttpClient, ProjectId, ServiceAccountKey};
use std::time::Duration;
pub struct AuthClient {
http: HttpClient,
project_id: ProjectId,
mode: ClientMode,
credentials: Credentials,
id_token_verifier: IdTokenVerifier,
session_cookie_verifier: SessionCookieVerifier,
endpoints: IdentityToolkitEndpoints,
#[cfg(feature = "live-user-management")]
token_provider: tokio::sync::OnceCell<crate::auth::token_provider::TokenProvider>,
}
impl AuthClient {
pub fn builder(project_id: impl Into<String>) -> AuthClientBuilder {
AuthClientBuilder::new(project_id)
}
pub fn project_id(&self) -> &ProjectId {
&self.project_id
}
pub async fn verify_id_token(
&self,
token: &str,
) -> Result<crate::auth::id_token::IdTokenClaims, AuthError> {
Ok(self.id_token_verifier.verify(token).await?)
}
pub fn create_custom_token(
&self,
uid: &str,
claims: Option<serde_json::Map<String, serde_json::Value>>,
) -> Result<String, AuthError> {
let Credentials::ServiceAccount(key) = &self.credentials else {
return Err(AuthError::Core(crate::core::CoreError::Credentials(
"create_custom_token requires an explicit service account key".to_string(),
)));
};
let signer = CustomTokenSigner::new((**key).clone());
Ok(signer.create_custom_token(uid, claims)?)
}
pub async fn create_session_cookie(
&self,
id_token: &str,
valid_duration: Duration,
) -> Result<String, AuthError> {
let token = self.bearer_token().await?;
crate::auth::session_cookie::create_session_cookie(
&self.http,
&self.endpoints.create_session_cookie(),
id_token,
valid_duration,
token.as_deref(),
self.mode.emulator_api_key(),
)
.await
}
pub async fn verify_session_cookie(&self, cookie: &str) -> Result<IdTokenClaims, AuthError> {
Ok(self.session_cookie_verifier.verify(cookie).await?)
}
async fn bearer_token(&self) -> Result<Option<String>, AuthError> {
if !self.mode.requires_bearer_token() {
return Ok(self.mode.emulator_bearer_token().map(str::to_string));
}
#[cfg(feature = "live-user-management")]
{
let provider = self
.token_provider
.get_or_try_init(|| async {
match &self.credentials {
Credentials::ServiceAccount(key) => {
crate::auth::token_provider::TokenProvider::from_service_account(key)
}
Credentials::ApplicationDefault => {
crate::auth::token_provider::TokenProvider::from_application_default()
.await
}
Credentials::Emulator => unreachable!(
"requires_bearer_token() is false in emulator mode; \
bearer_token() returns before reaching this branch"
),
}
})
.await?;
return provider.access_token().await.map(Some);
}
#[cfg(not(feature = "live-user-management"))]
{
Err(AuthError::Core(crate::core::CoreError::Credentials(
"user management against production Firebase requires the \
`live-user-management` feature"
.to_string(),
)))
}
}
fn user_operations<'a>(&'a self, bearer_token: Option<&'a str>) -> UserOperations<'a> {
UserOperations::new(
&self.http,
&self.endpoints,
bearer_token,
self.mode.emulator_api_key(),
)
}
pub async fn get_user(&self, uid: &str) -> Result<UserRecord, AuthError> {
let token = self.bearer_token().await?;
self.user_operations(token.as_deref()).get_user(uid).await
}
pub async fn get_user_by_email(&self, email: &str) -> Result<UserRecord, AuthError> {
let token = self.bearer_token().await?;
self.user_operations(token.as_deref())
.get_user_by_email(email)
.await
}
pub async fn create_user(&self, request: CreateUserRequest) -> Result<UserRecord, AuthError> {
let token = self.bearer_token().await?;
self.user_operations(token.as_deref())
.create_user(request)
.await
}
pub async fn update_user(
&self,
uid: &str,
request: UpdateUserRequest,
) -> Result<UserRecord, AuthError> {
let token = self.bearer_token().await?;
self.user_operations(token.as_deref())
.update_user(uid, request)
.await
}
pub async fn set_custom_user_claims(
&self,
uid: &str,
claims: serde_json::Map<String, serde_json::Value>,
) -> Result<(), AuthError> {
let token = self.bearer_token().await?;
self.user_operations(token.as_deref())
.set_custom_user_claims(uid, claims)
.await
}
pub async fn delete_user(&self, uid: &str) -> Result<(), AuthError> {
let token = self.bearer_token().await?;
self.user_operations(token.as_deref())
.delete_user(uid)
.await
}
pub async fn list_users(
&self,
max_results: u32,
page_token: Option<&str>,
) -> Result<UserPage, AuthError> {
let token = self.bearer_token().await?;
self.user_operations(token.as_deref())
.list_users(max_results, page_token)
.await
}
}
pub struct AuthClientBuilder {
project_id: String,
service_account: Option<ServiceAccountKey>,
#[cfg(feature = "live-user-management")]
use_application_default_credentials: bool,
emulator_host: Option<String>,
http_client: Option<reqwest::Client>,
}
impl AuthClientBuilder {
pub fn new(project_id: impl Into<String>) -> Self {
Self {
project_id: project_id.into(),
service_account: None,
#[cfg(feature = "live-user-management")]
use_application_default_credentials: false,
emulator_host: None,
http_client: None,
}
}
pub fn service_account_key(mut self, key: ServiceAccountKey) -> Self {
self.service_account = Some(key);
self
}
#[cfg(feature = "live-user-management")]
pub fn application_default_credentials(mut self) -> Self {
self.use_application_default_credentials = true;
self
}
#[cfg(feature = "emulator")]
pub fn use_emulator(mut self, host: impl Into<String>) -> Self {
self.emulator_host = Some(host.into());
self
}
pub fn http_client(mut self, client: reqwest::Client) -> Self {
self.http_client = Some(client);
self
}
pub fn build(self) -> Result<AuthClient, AuthError> {
let project_id = ProjectId::new(self.project_id)?;
let mode = ClientMode::resolve(self.emulator_host);
let credentials = if let Some(key) = self.service_account {
Credentials::ServiceAccount(Box::new(key))
} else {
#[cfg(feature = "live-user-management")]
if self.use_application_default_credentials {
Credentials::ApplicationDefault
} else if matches!(mode, ClientMode::Emulator { .. }) {
Credentials::Emulator
} else {
return Err(AuthError::Core(crate::core::CoreError::Credentials(
"no credentials configured: call service_account_key(...) or \
application_default_credentials()"
.to_string(),
)));
}
#[cfg(not(feature = "live-user-management"))]
if matches!(mode, ClientMode::Emulator { .. }) {
Credentials::Emulator
} else {
return Err(AuthError::Core(crate::core::CoreError::Credentials(
"no credentials configured: call service_account_key(...)".to_string(),
)));
}
};
let http = HttpClient::new(self.http_client.unwrap_or_default());
let endpoints = mode.endpoints();
let jwks = JwksCache::new(http.clone());
let id_token_verifier = IdTokenVerifier::new(project_id.clone(), jwks);
let session_cookie_certs = SessionCookieCertCache::new(http.clone());
let session_cookie_verifier =
SessionCookieVerifier::new(project_id.clone(), session_cookie_certs);
Ok(AuthClient {
http,
project_id,
mode,
credentials,
id_token_verifier,
session_cookie_verifier,
endpoints,
#[cfg(feature = "live-user-management")]
token_provider: tokio::sync::OnceCell::new(),
})
}
}