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};
4use alibi_core::session::SessionRequest;
5use alibi_core::store::OAuthTokenKind;
6use alibi_core::utils::json::JsValue;
7use alibi_core::{AuthContext, AuthResult, AuthSchema};
8use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
9use serde_json::{Value, json};
10use sha2::{Digest as _, Sha256};
11impl OAuthProviderPlugin {
12    /// Revoke an issued token using its client's registered authentication method.
13    pub fn oauth2_revoke_endpoint(
14        input: &impl serde::Serialize,
15    ) -> AuthResult<ServerEndpoint<Value>> {
16        ServerEndpoint::new("oauth-provider", "oauth2Revoke").with_body(input)
17    }
18    pub(super) async fn revoke<S: AuthSchema>(
19        &self,
20        input: Option<&JsValue>,
21        req: &impl SessionRequest,
22        ctx: &AuthContext<S>,
23    ) -> AuthResult<EndpointResponse> {
24        let body = input
25            .map(JsValue::to_json_value)
26            .transpose()?
27            .unwrap_or_else(|| json!({}));
28        for (field, required) in [
29            ("client_id", false),
30            ("client_secret", false),
31            ("client_assertion", false),
32            ("client_assertion_type", false),
33            ("token", true),
34            ("token_type_hint", false),
35        ] {
36            if let Some(value) = body.get(field) {
37                if !value.is_string() {
38                    return error(
39                        400,
40                        "invalid_request",
41                        &format!(
42                            "{field} {}",
43                            if value.is_array() {
44                                "must not appear more than once"
45                            } else {
46                                "must be a string"
47                            }
48                        ),
49                    );
50                }
51            } else if required {
52                return error(400, "invalid_request", &format!("{field} is required"));
53            }
54        }
55        let credentials = super::credentials::extract(&body, req);
56        if credentials.client_id.is_empty() {
57            return error(401, "invalid_client", "missing required credentials");
58        }
59        let raw = body
60            .get("token")
61            .and_then(Value::as_str)
62            .unwrap_or_default();
63        let token = raw
64            .strip_prefix("Bearer ")
65            .or_else(|| raw.strip_prefix("DPoP "))
66            .unwrap_or(raw);
67        if token.is_empty() {
68            return error(
69                400,
70                "invalid_request",
71                "missing a required token for introspection",
72            );
73        }
74        if let Err(response) = credentials.authenticate(ctx).await? {
75            return Ok(response);
76        }
77        let hint = body
78            .get("token_type_hint")
79            .and_then(Value::as_str)
80            .unwrap_or_default();
81        let kinds: &[OAuthTokenKind] = match hint {
82            "access_token" => &[OAuthTokenKind::Access],
83            "refresh_token" => &[OAuthTokenKind::Refresh],
84            _ => &[OAuthTokenKind::Access, OAuthTokenKind::Refresh],
85        };
86        let hash = URL_SAFE_NO_PAD.encode(Sha256::digest(token));
87        for kind in kinds {
88            let Some(row) = ctx.database.get_oauth_token(*kind, &hash).await? else {
89                continue;
90            };
91            if matches!(kind, OAuthTokenKind::Refresh)
92                && row.metadata.get("revoked").is_some_and(|v| !v.is_null())
93            {
94                ctx.database
95                    .delete_oauth_refresh_family(
96                        &credentials.client_id,
97                        row.user_id.as_deref().unwrap_or("undefined"),
98                    )
99                    .await?;
100                return error(400, "invalid_request", "refresh token revoked");
101            }
102            if row.client_id != credentials.client_id {
103                return EndpointResponse::json(&Value::Null);
104            }
105            if matches!(kind, OAuthTokenKind::Access) {
106                ctx.database.delete_oauth_token(*kind, &hash).await?;
107            } else {
108                let now = chrono::DateTime::from_timestamp(chrono::Utc::now().timestamp(), 0)
109                    .ok_or_else(|| alibi_core::AuthError::internal("Invalid revocation time"))?;
110                if !ctx.database.revoke_oauth_refresh_token(&hash, now).await? {
111                    ctx.database
112                        .delete_oauth_refresh_family(
113                            &credentials.client_id,
114                            row.user_id.as_deref().unwrap_or("undefined"),
115                        )
116                        .await?;
117                    return error(400, "invalid_request", "refresh token revoked");
118                }
119                ctx.database
120                    .delete_oauth_access_tokens_for_refresh(&row.id)
121                    .await?;
122            }
123            return Ok(EndpointResponse::empty().with_header("Content-Type", "application/json"));
124        }
125        error(
126            400,
127            "invalid_request",
128            match hint {
129                "access_token" => "Invalid access token",
130                _ => "token not found",
131            },
132        )
133    }
134}
135fn error(status: u16, code: &str, description: &str) -> AuthResult<EndpointResponse> {
136    super::clients::status_protocol_error(status, code, description)
137}