alibi_plugins/oauth_provider/
revocation.rs1use 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 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}