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