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/oauth.rs | 163 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 src/api/oauth.rs (limited to 'src/api/oauth.rs') diff --git a/src/api/oauth.rs b/src/api/oauth.rs new file mode 100644 index 0000000..0519125 --- /dev/null +++ b/src/api/oauth.rs @@ -0,0 +1,163 @@ +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::>(), + })) +} -- cgit v1.2.3