diff options
author | pennae <github@quasiparticle.net> | 2022-07-13 10:33:30 +0200 |
---|---|---|
committer | pennae <github@quasiparticle.net> | 2022-07-13 13:27:12 +0200 |
commit | 2f8dce44d3f2be74b5c6ec0a2e7f4ceced715328 (patch) | |
tree | caff55807c5fc773a36aa773cfde9cd6ebbbb6c8 /src/api/oauth.rs | |
download | minor-skulk-2f8dce44d3f2be74b5c6ec0a2e7f4ceced715328.tar.gz minor-skulk-2f8dce44d3f2be74b5c6ec0a2e7f4ceced715328.tar.xz minor-skulk-2f8dce44d3f2be74b5c6ec0a2e7f4ceced715328.zip |
initial import
Diffstat (limited to 'src/api/oauth.rs')
-rw-r--r-- | src/api/oauth.rs | 163 |
1 files changed, 163 insertions, 0 deletions
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<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>; + +#[catch(default)] +pub(crate) fn catch_all(status: Status, _r: &Request<'_>) -> Error { + match status.code { + 401 => Error::Unauthorized, + // these three are caused by Json<T> 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<OauthToken>, + refresh_token: Option<OauthToken>, + // NOTE this field does not exist in the spec, but fenix sends it + token: Option<OauthToken>, + // MISSING client_id + // MISSING client_secret + // MISSING refresh_token_id +} + +#[post("/destroy", data = "<req>")] +pub(crate) async fn destroy( + db: &DbConn, + req: Json<DestroyReq>, +) -> std::result::Result<Json<Empty>, 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<Empty> { + // 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<Scope<'static>>, + // MISSING generation + // MISSING profile_changed_at +} + +#[post("/verify", data = "<req>")] +pub(crate) async fn verify(db: &DbConn, req: Json<VerifyReq>) -> Result<VerifyResp> { + 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::<Vec<_>>(), + })) +} |