From 2f8dce44d3f2be74b5c6ec0a2e7f4ceced715328 Mon Sep 17 00:00:00 2001 From: pennae Date: Wed, 13 Jul 2022 10:33:30 +0200 Subject: initial import --- src/api/auth/mod.rs | 238 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 238 insertions(+) create mode 100644 src/api/auth/mod.rs (limited to 'src/api/auth/mod.rs') diff --git a/src/api/auth/mod.rs b/src/api/auth/mod.rs new file mode 100644 index 0000000..2c6d34d --- /dev/null +++ b/src/api/auth/mod.rs @@ -0,0 +1,238 @@ +use rocket::{ + http::Status, + response::{self, Responder}, + serde::json::Json, + Request, Response, +}; +use serde_json::json; + +use crate::{ + auth::Authenticated, + crypto::SecretBytes, + types::{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<(SecretBytes<32>, Self::Context)> { + let db = Authenticated::<(), Self>::get_conn(r).await?; + let k = db.use_session(id).await?; + Ok((k.req_hmac_key.0.clone(), 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<(SecretBytes<32>, 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<(SecretBytes<32>, 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<(SecretBytes<32>, 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"); + }, + } + } +} -- cgit v1.2.3