use rocket::{ http::Status, response::{self, Responder}, serde::json::Json, Request, Response, }; use serde_json::json; use crate::{ auth::Authenticated, types::{HawkKey, OauthToken, SessionID, UserSession}, }; pub(crate) mod account; pub(crate) mod device; pub(crate) mod email; pub(crate) mod invite; pub(crate) mod oauth; pub(crate) mod password; pub(crate) mod session; // we don't provide any additional fields. some we can't provide anyway (eg // invalid parameter `validation`), others are implied by the request body (eg // account exists `email`), and *our* client doesn't care about them anyway #[derive(Debug)] pub(crate) enum Error { AccountExists, UnknownAccount, IncorrectPassword, UnverifiedAccount, InvalidVerificationCode, InvalidBody, InvalidParameter, MissingParameter, InvalidSignature, InvalidAuthToken, RequestTooLarge, IncorrectEmailCase, UnknownDevice, UnverifiedSession, EmailFailed, NoDeviceCommand, UnknownClientID, ScopesNotAllowed, InviteOnly, InviteNotFound, Other(anyhow::Error), UnexpectedStatus(Status), } #[rustfmt::skip] impl<'r> Responder<'r, 'static> for Error { fn respond_to(self, request: &'r Request<'_>) -> response::Result<'static> { let (code, errno, msg) = match self { Error::AccountExists => (Status::BadRequest, 101, "account already exists"), Error::UnknownAccount => (Status::BadRequest, 102, "unknown account"), Error::IncorrectPassword => (Status::BadRequest, 103, "incorrect password"), Error::UnverifiedAccount => (Status::BadRequest, 104, "unverified account"), Error::InvalidVerificationCode => (Status::BadRequest, 105, "invalid verification code"), Error::InvalidBody => (Status::BadRequest, 106, "invalid json in request body"), Error::InvalidParameter => (Status::BadRequest, 107, "invalid parameter in request body"), Error::MissingParameter => (Status::BadRequest, 108, "missing parameter in request body"), Error::InvalidSignature => (Status::Unauthorized, 109, "invalid request signature"), Error::InvalidAuthToken => (Status::Unauthorized, 110, "invalid authentication token"), Error::RequestTooLarge => (Status::PayloadTooLarge, 113, "request too large"), Error::IncorrectEmailCase => (Status::BadRequest, 120, "incorrect email case"), Error::UnknownDevice => (Status::BadRequest, 123, "unknown device"), Error::UnverifiedSession => (Status::BadRequest, 138, "unverified session"), Error::EmailFailed => (Status::UnprocessableEntity, 151, "failed to send email"), Error::NoDeviceCommand => (Status::BadRequest, 157, "unavailable device command"), Error::UnknownClientID => (Status::BadRequest, 162, "unknown client_id"), Error::ScopesNotAllowed => (Status::BadRequest, 169, "requested scopes not allowed"), Error::InviteOnly => (Status::BadRequest, -1, "invite code required"), Error::InviteNotFound => (Status::BadRequest, -2, "invite code not found"), Error::Other(e) => { error!("non-api error during request: {:#?}", e); (Status::InternalServerError, 999, "internal error") }, Error::UnexpectedStatus(s) => (s, 999, ""), }; let body = json!({ "code": code.code, "errno": errno, "error": code.reason_lossy(), "message": msg }); Response::build_from(Json(body).respond_to(request)?).status(code).ok() } } impl From for Error { fn from(e: sqlx::Error) -> Self { Error::Other(anyhow!(e)) } } impl From for Error { fn from(e: anyhow::Error) -> Self { Error::Other(e) } } pub(crate) type Result = std::result::Result, Error>; // hack marker type to convey that auth failed due to an unverified session. // without this the catcher could convert the Unauthorized error we get from // auth failures into just one thing, even though we have multiple causes. #[derive(Clone, Copy, Debug)] struct UsedUnverifiedSession; #[catch(default)] pub(crate) fn catch_all(status: Status, req: &Request<'_>) -> Error { match req.local_cache(|| None) { Some(UsedUnverifiedSession) => Error::UnverifiedSession, _ => { match status.code { 401 => Error::InvalidSignature, // these three are caused by Json errors 400 => Error::InvalidBody, 413 => Error::RequestTooLarge, 422 => Error::InvalidParameter, // generic unauthorized instead of 404 for eg wrong method or nonexistant endpoints 404 => Error::InvalidSignature, _ => { error!("caught unexpected error {status}"); Error::UnexpectedStatus(status) }, } }, } } #[derive(Debug)] pub(crate) struct WithFxaLogin; #[async_trait] impl crate::auth::AuthSource for WithFxaLogin { type ID = SessionID; type Context = UserSession; async fn hawk(r: &Request<'_>, id: &SessionID) -> anyhow::Result<(HawkKey, Self::Context)> { let db = Authenticated::<(), Self>::get_conn(r).await?; let k = db.use_session(id).await?; Ok((k.req_hmac_key, k)) } async fn bearer_token( _: &Request<'_>, _: &OauthToken, ) -> anyhow::Result<(SessionID, Self::Context)> { bail!("refresh tokens not allowed here"); } } #[derive(Debug)] pub(crate) struct WithVerifiedFxaLogin; #[async_trait] impl crate::auth::AuthSource for WithVerifiedFxaLogin { type ID = SessionID; type Context = UserSession; async fn hawk(r: &Request<'_>, id: &SessionID) -> anyhow::Result<(HawkKey, Self::Context)> { let res = WithFxaLogin::hawk(r, id).await?; match res.1.verified { true => Ok(res), false => { r.local_cache(|| Some(UsedUnverifiedSession)); bail!("session not verified"); }, } } async fn bearer_token( _: &Request<'_>, _: &OauthToken, ) -> anyhow::Result<(SessionID, Self::Context)> { bail!("refresh tokens not allowed here"); } } #[derive(Debug)] pub(crate) struct WithSession; #[rocket::async_trait] impl crate::auth::AuthSource for WithSession { type ID = SessionID; type Context = UserSession; async fn hawk(r: &Request<'_>, id: &SessionID) -> anyhow::Result<(HawkKey, Self::Context)> { WithFxaLogin::hawk(r, id).await } async fn bearer_token( r: &Request<'_>, token: &OauthToken, ) -> anyhow::Result<(SessionID, Self::Context)> { let db = Authenticated::<(), Self>::get_conn(r).await?; Ok(db.use_session_from_refresh(&token.hash()).await?) } } #[derive(Debug)] pub(crate) struct WithVerifiedSession; #[rocket::async_trait] impl crate::auth::AuthSource for WithVerifiedSession { type ID = SessionID; type Context = UserSession; async fn hawk(r: &Request<'_>, id: &SessionID) -> anyhow::Result<(HawkKey, Self::Context)> { WithVerifiedFxaLogin::hawk(r, id).await } async fn bearer_token( r: &Request<'_>, token: &OauthToken, ) -> anyhow::Result<(SessionID, Self::Context)> { let db = Authenticated::<(), Self>::get_conn(r).await?; let res = db.use_session_from_refresh(&token.hash()).await?; match res.1.verified { true => Ok(res), false => { // technically unreachable because generating a refresh token requires a // valid fxa session r.local_cache(|| Some(UsedUnverifiedSession)); bail!("session not verified"); }, } } }