summaryrefslogtreecommitdiff
path: root/src/api/oauth.rs
blob: 05191254505a8bf6664c6eb838dee39d3e388877 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
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<_>>(),
    }))
}