Skip to main content

alibi_plugins/oauth_provider/
revocation.rs

1//! Token retirement and guarded refresh-token family invalidation.
2use super::OAuthProviderPlugin;
3use alibi_core::endpoint::{EndpointResponse, ServerEndpoint, is_endpoint_api_error};
4use alibi_core::session::SessionRequest;
5use alibi_core::store::OAuthTokenKind;
6use alibi_core::utils::json::JsValue;
7use alibi_core::{AuthContext, AuthResult, AuthSchema};
8use serde_json::{Value, json};
9impl OAuthProviderPlugin {
10    /// Revoke an issued token using its client's registered authentication method.
11    pub fn oauth2_revoke_endpoint(
12        input: &impl serde::Serialize,
13    ) -> AuthResult<ServerEndpoint<Value>> {
14        ServerEndpoint::new("oauth-provider", "oauth2Revoke").with_body(input)
15    }
16    pub(super) async fn revoke<S: AuthSchema>(
17        &self,
18        input: Option<&JsValue>,
19        req: &impl SessionRequest,
20        ctx: &AuthContext<S>,
21    ) -> AuthResult<EndpointResponse> {
22        let body = input
23            .map(JsValue::to_json_value)
24            .transpose()?
25            .unwrap_or_else(|| json!({}));
26        for (field, required) in [
27            ("client_id", false),
28            ("client_secret", false),
29            ("client_assertion", false),
30            ("client_assertion_type", false),
31            ("token", true),
32            ("token_type_hint", false),
33        ] {
34            if let Some(value) = body.get(field) {
35                if !value.is_string() {
36                    return error(
37                        400,
38                        "invalid_request",
39                        &format!(
40                            "{field} {}",
41                            if value.is_array() {
42                                "must not appear more than once"
43                            } else {
44                                "must be a string"
45                            }
46                        ),
47                    );
48                }
49            } else if required {
50                return error(400, "invalid_request", &format!("{field} is required"));
51            }
52        }
53        let credentials = match super::credentials::extract(&body, req, ctx, self, "revoke").await?
54        {
55            Ok(credentials) => credentials,
56            Err(response) => return Ok(response),
57        };
58        if credentials.client_id.is_empty() {
59            return error(401, "invalid_client", "missing required credentials");
60        }
61        let raw = body
62            .get("token")
63            .and_then(Value::as_str)
64            .unwrap_or_default();
65        let token = raw
66            .strip_prefix("Bearer ")
67            .or_else(|| raw.strip_prefix("DPoP "))
68            .unwrap_or(raw);
69        if token.is_empty() {
70            return error(
71                400,
72                "invalid_request",
73                "missing a required token for introspection",
74            );
75        }
76        if let Err(response) = credentials.authenticate(ctx, self).await? {
77            return Ok(response);
78        }
79        let hint = body
80            .get("token_type_hint")
81            .and_then(Value::as_str)
82            .unwrap_or_default();
83        let kinds: &[OAuthTokenKind] = match hint {
84            "access_token" => &[OAuthTokenKind::Access],
85            "refresh_token" => &[OAuthTokenKind::Refresh],
86            _ => &[OAuthTokenKind::Access, OAuthTokenKind::Refresh],
87        };
88        for kind in kinds {
89            let Some(prefix_token) = self.decode_token_prefix(token, *kind) else {
90                if matches!(kind, OAuthTokenKind::Refresh) && hint == "refresh_token" {
91                    return error(400, "invalid_token", "refresh token not found");
92                }
93                continue;
94            };
95            let decoded = if matches!(kind, OAuthTokenKind::Refresh) {
96                match self.decode_refresh_token(token).await {
97                    Ok(Some(decoded)) => Some(decoded),
98                    Ok(None) => continue,
99                    Err(error) if is_endpoint_api_error(&error) => {
100                        if hint != "refresh_token" {
101                            continue;
102                        }
103                        return Ok(EndpointResponse::error(error));
104                    }
105                    Err(_) => {
106                        return Ok(EndpointResponse::error(alibi_core::AuthError::Api {
107                            status: 500,
108                            code: None,
109                            message: String::new(),
110                        })
111                        .without_error_body()
112                        .with_header("content-type", "application/json"));
113                    }
114                }
115            } else {
116                None
117            };
118            let token = decoded
119                .as_ref()
120                .map_or(prefix_token, |decoded| decoded.token.as_str());
121            let hash = self
122                .hash_token(
123                    token,
124                    if matches!(kind, OAuthTokenKind::Refresh) {
125                        "refresh_token"
126                    } else {
127                        "access_token"
128                    },
129                )
130                .await?;
131            let Some(row) = ctx.database.get_oauth_token(*kind, &hash).await? else {
132                continue;
133            };
134            if matches!(kind, OAuthTokenKind::Refresh)
135                && row.metadata.get("revoked").is_some_and(|v| !v.is_null())
136            {
137                ctx.database
138                    .delete_oauth_refresh_family(
139                        &credentials.client_id,
140                        row.user_id.as_deref().unwrap_or("undefined"),
141                    )
142                    .await?;
143                return error(400, "invalid_request", "refresh token revoked");
144            }
145            if row.client_id != credentials.client_id {
146                return EndpointResponse::json(&Value::Null);
147            }
148            if matches!(kind, OAuthTokenKind::Access) {
149                ctx.database.delete_oauth_token(*kind, &hash).await?;
150            } else {
151                let now = chrono::DateTime::from_timestamp(chrono::Utc::now().timestamp(), 0)
152                    .ok_or_else(|| alibi_core::AuthError::internal("Invalid revocation time"))?;
153                if !ctx.database.revoke_oauth_refresh_token(&hash, now).await? {
154                    ctx.database
155                        .delete_oauth_refresh_family(
156                            &credentials.client_id,
157                            row.user_id.as_deref().unwrap_or("undefined"),
158                        )
159                        .await?;
160                    return error(400, "invalid_request", "refresh token revoked");
161                }
162                ctx.database
163                    .delete_oauth_access_tokens_for_refresh(&row.id)
164                    .await?;
165            }
166            return Ok(EndpointResponse::empty().with_header("Content-Type", "application/json"));
167        }
168        error(
169            400,
170            "invalid_request",
171            match hint {
172                "access_token" => "Invalid access token",
173                _ => "token not found",
174            },
175        )
176    }
177}
178fn error(status: u16, code: &str, description: &str) -> AuthResult<EndpointResponse> {
179    super::clients::status_protocol_error(status, code, description)
180}