use rocket::{ http::Status, response::{self, Responder}, serde::json::Json, Request, Response, }; use serde::{Deserialize, Serialize}; use serde_json::json; use crate::{ api::Empty, types::{OauthToken, UserID}, }; use crate::{db::DbConn, types::oauth::Scope}; use super::EMPTY; // 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 { InvalidParameter, Unauthorized, PayloadTooLarge, 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::InvalidParameter => (Status::BadRequest, 109, "invalid request parameter"), Error::Unauthorized => (Status::Forbidden, 111, "unauthorized"), Error::PayloadTooLarge => (Status::PayloadTooLarge, 999, "payload too large"), 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>; #[catch(default)] pub(crate) fn catch_all(status: Status, _r: &Request<'_>) -> Error { match status.code { 401 => Error::Unauthorized, // these three are caused by Json errors 400 => Error::InvalidParameter, 413 => Error::PayloadTooLarge, 422 => Error::InvalidParameter, // generic unauthorized instead of 404 for eg wrong method or nonexistant endpoints 404 => Error::Unauthorized, _ => { error!("caught unexpected error {status}"); Error::UnexpectedStatus(status) }, } } fn map_error(e: sqlx::Error) -> Error { match &e { sqlx::Error::RowNotFound => Error::InvalidParameter, _ => Error::Other(anyhow!(e)), } } // MISSING GET /v1/authorization // MISSING POST /v1/authorization // MISSING POST /v1/authorized-clients // MISSING POST /v1/authorized-clients/destroy // MISSING GET /v1/client/:id // MISSING POST /v1/introspect // MISSING GET /v1/jwks // MISSING POST /v1/key-data // MISSING POST /v1/token // MISSING POST /v1/verify #[derive(Debug, Deserialize)] #[serde(deny_unknown_fields)] pub(crate) struct DestroyReq { access_token: Option, refresh_token: Option, // NOTE this field does not exist in the spec, but fenix sends it token: Option, // MISSING client_id // MISSING client_secret // MISSING refresh_token_id } #[post("/destroy", data = "")] pub(crate) async fn destroy( db: &DbConn, req: Json, ) -> std::result::Result, Error> { // MISSING spec says basic auth is allowed, but nothing seems to use it if let Some(t) = req.0.access_token { db.delete_oauth_token(&t.hash()).await?; } if let Some(t) = req.0.refresh_token { db.delete_oauth_token(&t.hash()).await?; } if let Some(t) = req.0.token { db.delete_oauth_token(&t.hash()).await?; } Ok(EMPTY) } #[get("/jwks")] pub(crate) async fn jwks() -> Json { // HACK we need to return *something* for /jwks, otherwise PyFxA fails. // since syncstorage-rs uses PyFxA to check oauth tokens this is bad. EMPTY } #[derive(Debug, Deserialize)] #[serde(deny_unknown_fields)] pub(crate) struct VerifyReq { token: OauthToken, } #[derive(Debug, Serialize)] pub(crate) struct VerifyResp { user: UserID, client_id: String, scope: Vec>, // MISSING generation // MISSING profile_changed_at } #[post("/verify", data = "")] pub(crate) async fn verify(db: &DbConn, req: Json) -> Result { let token = db.get_access_token(&req.token.hash()).await.map_err(map_error)?; Ok(Json(VerifyResp { user: token.user_id, client_id: token.client_id, scope: token.scope.split().map(|s| s.into_owned()).collect::>(), })) }