#![cfg_attr(not(feature = "axum08"), allow(dead_code))]
use std::borrow::ToOwned;
use std::string::String;
use jiff::Timestamp;
use super::{Auth, AuthBuilder, Error, Result};
use crate::google::{Callback, Flow, GoogleConfig, Provider, Url};
use crate::is_same_origin_path;
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(all(feature = "google", feature = "test-util")))
)]
pub fn google_endpoints(mut self, endpoints: crate::google::Endpoints) -> Self {
self.google_endpoints = endpoints;
self
}
}
#[derive(Debug)]
pub(crate) struct GoogleReturn {
pub(crate) next: Option<String>,
pub(crate) result: Result<(User, Session), GoogleFailure>,
}
#[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, next: Option<&str>) -> Result<Url> {
let provider = self.google_provider()?;
let next = next.filter(|next| is_same_origin_path(next)).map(location);
Ok(provider.start(Flow::SignIn { next }))
}
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, session: &Session) -> 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,
impersonator: session.impersonator,
}))
}
pub(crate) async fn google_callback(&self, callback: &Callback) -> GoogleReturn {
let Some(provider) = self.inner.google.as_ref() else {
tracing::warn!("Google sign-in: a callback arrived, but Google is not configured");
return GoogleReturn {
next: None,
result: Err(GoogleFailure::AuthFailed),
};
};
let flow = callback
.state
.as_deref()
.and_then(|state| provider.take(state));
let next = match &flow {
Some(Flow::SignIn { next }) => next.clone(),
_ => None,
};
let result = self.complete_google(provider, callback, flow).await;
GoogleReturn { next, result }
}
async fn complete_google(
&self,
provider: &Provider,
callback: &Callback,
flow: Option<Flow>,
) -> 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(_)) = (&callback.code, &callback.state) else {
tracing::info!("Google sign-in: the callback has no code or state");
return Err(GoogleFailure::AuthFailed);
};
let Some(flow) = flow 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, impersonator } => match self
.switch_to_google(*user, *impersonator, 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",
}
}
fn location(next: &str) -> String {
const HEX: &[u8; 16] = b"0123456789ABCDEF";
let mut out = String::with_capacity(next.len());
for &b in next.as_bytes() {
if b.is_ascii_graphic()
&& !matches!(b, b'"' | b'<' | b'>' | b'`' | b'{' | b'|' | b'}' | b'^')
{
out.push(char::from(b));
} else {
out.push('%');
out.push(char::from(HEX[usize::from(b >> 4)]));
out.push(char::from(HEX[usize::from(b & 0xf)]));
}
}
out
}