summaryrefslogtreecommitdiff
path: root/src/api/oauth.rs
diff options
context:
space:
mode:
authorpennae <github@quasiparticle.net>2022-07-13 10:33:30 +0200
committerpennae <github@quasiparticle.net>2022-07-13 13:27:12 +0200
commit2f8dce44d3f2be74b5c6ec0a2e7f4ceced715328 (patch)
treecaff55807c5fc773a36aa773cfde9cd6ebbbb6c8 /src/api/oauth.rs
downloadminor-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.rs163
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<_>>(),
+ }))
+}