#![cfg_attr(not(feature = "axum08"), allow(dead_code))]
use jiff::Timestamp;
use super::{Auth, AuthBuilder, Error, Result};
use crate::google::{Callback, Flow, GoogleConfig, Provider, Url};
use crate::store::{Session, User};
use crate::types::AuthError;
impl AuthBuilder {
#[cfg_attr(watchword_docsrs, doc(cfg(feature = "google")))]
pub fn google(mut self, config: Option<GoogleConfig>) -> Self {
self.google = config;
self
}
#[cfg(any(test, feature = "test-util"))]
#[cfg_attr(watchword_docsrs, doc(cfg(feature = "test-util")))]
pub fn google_endpoints(mut self, endpoints: crate::google::Endpoints) -> Self {
self.google_endpoints = endpoints;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum GoogleFailure {
UserNotFound,
AuthFailed,
}
impl GoogleFailure {
pub(crate) fn code(self) -> &'static str {
match self {
GoogleFailure::UserNotFound => "user-not-found",
GoogleFailure::AuthFailed => "auth-failed",
}
}
}
impl Auth {
fn google_provider(&self) -> Result<&Provider> {
Ok(self.inner.google.as_ref().ok_or(AuthError::NotFound)?)
}
pub(crate) fn google_sign_in(&self) -> Result<Url> {
Ok(self.google_provider()?.start(Flow::SignIn))
}
pub(crate) async fn google_register(&self, token: &str) -> Result<Url> {
let provider = self.google_provider()?;
self.usable_token(token, Timestamp::now()).await?;
Ok(provider.start(Flow::Register {
token: token.to_owned(),
}))
}
pub(crate) fn google_link(&self, user: &User) -> Result<Url> {
let provider = self.google_provider()?;
if user.access != crate::types::Access::Active {
return Err(AuthError::Forbidden.into());
}
Ok(provider.start(Flow::Link { user: user.id }))
}
pub(crate) async fn google_callback(
&self,
callback: &Callback,
) -> Result<(User, Session), GoogleFailure> {
if let Some(error) = &callback.error {
tracing::info!(error, "Google sign-in: Google returned an error");
return Err(GoogleFailure::AuthFailed);
}
let (Some(code), Some(state)) = (&callback.code, &callback.state) else {
tracing::info!("Google sign-in: the callback has no code or state");
return Err(GoogleFailure::AuthFailed);
};
let Some(provider) = self.inner.google.as_ref() else {
tracing::warn!("Google sign-in: a callback arrived, but Google is not configured");
return Err(GoogleFailure::AuthFailed);
};
let Some(flow) = provider.take(state) else {
tracing::info!("Google sign-in: unknown, expired or replayed state");
return Err(GoogleFailure::AuthFailed);
};
let account = match provider.verify(code).await {
Ok(account) => account,
Err(error) => {
tracing::warn!(
error = &error as &dyn std::error::Error,
"Google sign-in failed"
);
return Err(GoogleFailure::AuthFailed);
}
};
let (sub, email) = (&account.sub, &account.email);
let result = match &flow {
Flow::SignIn => self.sign_in_google(sub, email).await,
Flow::Register { token } => self.register_google(token, sub, email).await,
Flow::Link { user } => match self.switch_to_google(*user, None, sub, email).await {
Ok(user) => self
.create_session(user.id)
.await
.map(|session| (user, session)),
Err(error) => Err(error),
},
};
result.map_err(|error| {
let failure = match (&flow, &error) {
(Flow::SignIn, Error::Auth(AuthError::Unauthorized)) => GoogleFailure::UserNotFound,
_ => GoogleFailure::AuthFailed,
};
match error.auth_error() {
Some(refused) => tracing::info!(
email,
flow = flow_name(&flow),
error = %refused,
"Google sign-in refused"
),
None => tracing::error!(
email,
flow = flow_name(&flow),
error = &error as &dyn std::error::Error,
"Google sign-in failed"
),
}
failure
})
}
}
fn flow_name(flow: &Flow) -> &'static str {
match flow {
Flow::SignIn => "sign-in",
Flow::Register { .. } => "register",
Flow::Link { .. } => "link",
}
}