summaryrefslogtreecommitdiff
path: root/src/api/auth/mod.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/api/auth/mod.rs')
-rw-r--r--src/api/auth/mod.rs238
1 files changed, 238 insertions, 0 deletions
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<sqlx::Error> for Error {
+ fn from(e: sqlx::Error) -> Self {
+ Error::Other(anyhow!(e))
+ }
+}
+
+impl From<anyhow::Error> for Error {
+ fn from(e: anyhow::Error) -> Self {
+ Error::Other(e)
+ }
+}
+
+pub(crate) type Result<T> = std::result::Result<Json<T>, 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<T> 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");
+ },
+ }
+ }
+}