headless_lms_server/controllers/main_frontend/oauth/
revoke.rs1use crate::domain::exercise_services::token::{invalidate_cached_user, invalidate_cached_users};
2use crate::domain::oauth::oauth_validated::OAuthValidated;
3use crate::domain::oauth::revoke_query::RevokeQuery;
4use crate::prelude::*;
5use actix_web::{HttpResponse, web};
6use headless_lms_base::config::ApplicationConfiguration;
7use headless_lms_utils::cache::Cache;
8use models::{
9 error::ModelErrorType, library::oauth::token_digest_sha256,
10 oauth_access_token::OAuthAccessToken, oauth_client::OAuthClient,
11 oauth_refresh_tokens::OAuthRefreshTokens,
12};
13use secrecy::ExposeSecret;
14use sqlx::PgPool;
15use utoipa::OpenApi;
16
17#[derive(OpenApi)]
18#[openapi(paths(revoke))]
19#[allow(dead_code)]
20pub(crate) struct MainFrontendOauthRevokeApiDoc;
21
22#[instrument(skip(pool, form, app_conf, cache))]
51#[utoipa::path(
52 post,
53 path = "/revoke",
54 operation_id = "revokeOauthToken",
55 tag = "oauth",
56 request_body(
57 content = serde_json::Value,
58 content_type = "application/x-www-form-urlencoded"
59 ),
60 responses(
61 (status = 200, description = "OAuth token revocation acknowledged")
62 )
63)]
64pub async fn revoke(
65 pool: web::Data<PgPool>,
66 OAuthValidated(form): OAuthValidated<RevokeQuery>,
67 app_conf: web::Data<ApplicationConfiguration>,
68 cache: web::Data<Cache>,
69) -> ControllerResult<HttpResponse> {
70 let mut conn = pool.acquire().await?;
71 let server_token = skip_authorize();
72
73 let client_result = OAuthClient::find_by_client_id(&mut conn, &form.client_id).await;
80
81 tracing::Span::current().record("client_id", &form.client_id);
83
84 let client = match client_result {
86 Ok(c) => c,
87 Err(err) => {
88 match err.error_type() {
89 ModelErrorType::RecordNotFound | ModelErrorType::NotFound => {
91 return server_token.authorized_ok(HttpResponse::Ok().finish());
92 }
93 _ => {
95 tracing::error!(err = %err, "OAuth revoke: client lookup failed");
96 return Err(ControllerError::new(
97 ControllerErrorType::InternalServerError,
98 "Failed to authenticate client due to storage error".to_string(),
99 Some(err.into()),
100 ));
101 }
102 }
103 }
104 };
105
106 let token_hmac_key = &app_conf.oauth_server_configuration.oauth_token_hmac_key;
108 let client_valid = if client.is_confidential() {
109 match &client.client_secret {
110 Some(secret) => {
111 let provided_secret_digest = token_digest_sha256(
112 form.client_secret
113 .as_ref()
114 .map(|s| s.expose_secret())
115 .unwrap_or_default(),
116 token_hmac_key,
117 );
118 secret.constant_eq(&provided_secret_digest)
119 }
120 None => false,
121 }
122 } else {
123 true };
125
126 if !client_valid {
128 return server_token.authorized_ok(HttpResponse::Ok().finish());
129 }
130
131 let hint = form.token_type_hint.as_deref().and_then(|h| {
137 match h {
138 "access_token" | "refresh_token" => Some(h),
139 _ => None, }
141 });
142 if let Some(h) = hint {
143 tracing::Span::current().record("token_type_hint", h);
144 }
145
146 if hint == Some("refresh_token") {
149 if !revoke_refresh_grant_of_client(&mut conn, &form, &client, token_hmac_key, &cache)
150 .await?
151 {
152 revoke_access_token_of_client(&mut conn, &form, &client, token_hmac_key, &cache)
153 .await?;
154 }
155 } else if !revoke_access_token_of_client(&mut conn, &form, &client, token_hmac_key, &cache)
156 .await?
157 {
158 revoke_refresh_grant_of_client(&mut conn, &form, &client, token_hmac_key, &cache).await?;
159 }
160
161 server_token.authorized_ok(HttpResponse::Ok().finish())
163}
164
165fn not_found_or_storage_error(err: models::ModelError, what: &str) -> Result<(), ControllerError> {
169 match err.error_type() {
170 ModelErrorType::RecordNotFound | ModelErrorType::NotFound => Ok(()),
171 _ => Err(controller_err!(
172 InternalServerError,
173 format!("Failed to look up {what} due to storage error"),
174 err
175 )),
176 }
177}
178
179async fn revoke_access_token_of_client(
184 conn: &mut sqlx::PgConnection,
185 form: &crate::domain::oauth::revoke_query::RevokeParams,
186 client: &OAuthClient,
187 token_hmac_key: &secrecy::SecretString,
188 cache: &Cache,
189) -> Result<bool, ControllerError> {
190 let digest = token_digest_sha256(form.token.expose_secret(), token_hmac_key);
191 match OAuthAccessToken::find_valid(conn, digest).await {
192 Ok(access_token) => {
193 if access_token.client_id == client.id {
194 let digest = token_digest_sha256(form.token.expose_secret(), token_hmac_key);
195 OAuthAccessToken::revoke_by_digest(conn, digest).await?;
196 let digest = token_digest_sha256(form.token.expose_secret(), token_hmac_key);
197 invalidate_cached_user(cache, &digest, token_hmac_key).await;
198 }
199 Ok(true)
200 }
201 Err(err) => not_found_or_storage_error(err, "access token").map(|_| false),
202 }
203}
204
205async fn revoke_refresh_grant_of_client(
215 conn: &mut sqlx::PgConnection,
216 form: &crate::domain::oauth::revoke_query::RevokeParams,
217 client: &OAuthClient,
218 token_hmac_key: &secrecy::SecretString,
219 cache: &Cache,
220) -> Result<bool, ControllerError> {
221 let digest = token_digest_sha256(form.token.expose_secret(), token_hmac_key);
222 match OAuthRefreshTokens::find_valid(conn, digest).await {
223 Ok(refresh_token) => {
224 if refresh_token.client_id == client.id {
225 let revoked_access_digests =
226 OAuthRefreshTokens::revoke_grant(conn, refresh_token.user_id, client.id)
227 .await?;
228 invalidate_cached_users(cache, &revoked_access_digests, token_hmac_key).await;
229 }
230 Ok(true)
231 }
232 Err(err) => not_found_or_storage_error(err, "refresh token").map(|_| false),
233 }
234}
235
236pub fn _add_routes(cfg: &mut web::ServiceConfig) {
237 cfg.route("/revoke", web::post().to(revoke));
238}