diff --git a/docs/openapi.yaml b/docs/openapi.yaml index b6d118c..e61aed0 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -59,6 +59,25 @@ paths: example: 1639747526 400: description: Invalid token request + /oauth/revoke: + post: + summary: Revoke an access token to make it no longer valid for use. + security: + - tokenAuth: [] + requestBody: + content: + application/json: + schema: + type: object + properties: + token: + description: The previously obtained token, to be invalidated. + type: string + responses: + 200: + description: Successful operation + 403: + description: Token doesn't belong to user. /api/v1/accounts: post: summary: Creates a user and profile records. diff --git a/src/mastodon_api/oauth/types.rs b/src/mastodon_api/oauth/types.rs index db86ad7..4f2482b 100644 --- a/src/mastodon_api/oauth/types.rs +++ b/src/mastodon_api/oauth/types.rs @@ -31,3 +31,8 @@ impl TokenResponse { } } } + +#[derive(Deserialize)] +pub struct RevocationRequest { + pub token: String, +} diff --git a/src/mastodon_api/oauth/views.rs b/src/mastodon_api/oauth/views.rs index 75ef7c7..49f5643 100644 --- a/src/mastodon_api/oauth/views.rs +++ b/src/mastodon_api/oauth/views.rs @@ -1,18 +1,23 @@ use actix_web::{post, web, HttpResponse, Scope as ActixScope}; +use actix_web_httpauth::extractors::bearer::BearerAuth; use chrono::{Duration, Utc}; use crate::config::Config; use crate::database::{Pool, get_database_client}; -use crate::errors::{HttpError, ValidationError}; +use crate::errors::{DatabaseError, HttpError, ValidationError}; use crate::ethereum::eip4361::verify_eip4361_signature; -use crate::models::oauth::queries::save_oauth_token; +use crate::models::oauth::queries::{ + delete_oauth_token, + save_oauth_token, +}; use crate::models::users::queries::{ get_user_by_name, get_user_by_login_address, }; use crate::utils::currencies::{validate_wallet_address, Currency}; use crate::utils::passwords::verify_password; -use super::types::{TokenRequest, TokenResponse}; +use super::auth::get_current_user; +use super::types::{RevocationRequest, TokenRequest, TokenResponse}; use super::utils::generate_access_token; const ACCESS_TOKEN_EXPIRES_IN: i64 = 86400 * 7; @@ -87,7 +92,28 @@ async fn token_view( Ok(HttpResponse::Ok().json(token_response)) } +#[post("/revoke")] +async fn revoke_token_view( + auth: BearerAuth, + db_pool: web::Data, + request_data: web::Json, +) -> Result { + let db_client = &mut **get_database_client(&db_pool).await?; + let current_user = get_current_user(db_client, auth.token()).await?; + match delete_oauth_token( + db_client, + ¤t_user.id, + &request_data.token, + ).await { + Ok(_) => (), + Err(DatabaseError::NotFound(_)) => return Err(HttpError::PermissionError), + Err(other_error) => return Err(other_error.into()), + }; + Ok(HttpResponse::Ok().finish()) +} + pub fn oauth_api_scope() -> ActixScope { web::scope("/oauth") .service(token_view) + .service(revoke_token_view) } diff --git a/src/models/oauth/queries.rs b/src/models/oauth/queries.rs index d9eb98f..2d0ff8a 100644 --- a/src/models/oauth/queries.rs +++ b/src/models/oauth/queries.rs @@ -9,7 +9,7 @@ use crate::models::users::types::{DbUser, User}; pub async fn save_oauth_token( db_client: &impl GenericClient, owner_id: &Uuid, - access_token: &str, + token: &str, created_at: &DateTime, expires_at: &DateTime, ) -> Result<(), DatabaseError> { @@ -18,11 +18,41 @@ pub async fn save_oauth_token( INSERT INTO oauth_token (owner_id, token, created_at, expires_at) VALUES ($1, $2, $3, $4) ", - &[&owner_id, &access_token, &created_at, &expires_at], + &[&owner_id, &token, &created_at, &expires_at], ).await?; Ok(()) } +pub async fn delete_oauth_token( + db_client: &mut impl GenericClient, + current_user_id: &Uuid, + token: &str, +) -> Result<(), DatabaseError> { + let transaction = db_client.transaction().await?; + let maybe_row = transaction.query_opt( + " + SELECT owner_id FROM oauth_token + WHERE token = $1 + FOR UPDATE + ", + &[&token], + ).await?; + if let Some(row) = maybe_row { + let owner_id: Uuid = row.try_get("owner_id")?; + if owner_id != *current_user_id { + // Return error if token is owned by a different user + return Err(DatabaseError::NotFound("token")); + } else { + transaction.execute( + "DELETE FROM oauth_token WHERE token = $1", + &[&token], + ).await?; + }; + }; + transaction.commit().await?; + Ok(()) +} + pub async fn get_user_by_oauth_token( db_client: &impl GenericClient, access_token: &str, @@ -45,3 +75,36 @@ pub async fn get_user_by_oauth_token( let user = User::new(db_user, db_profile); Ok(user) } + +#[cfg(test)] +mod tests { + use serial_test::serial; + use crate::database::test_utils::create_test_database; + use crate::models::users::queries::create_user; + use crate::models::users::types::UserCreateData; + use super::*; + + #[tokio::test] + #[serial] + async fn test_delete_oauth_token() { + let db_client = &mut create_test_database().await; + let user_data = UserCreateData { + username: "test".to_string(), + ..Default::default() + }; + let user = create_user(db_client, user_data).await.unwrap(); + let token = "test-token"; + save_oauth_token( + db_client, + &user.id, + token, + &Utc::now(), + &Utc::now(), + ).await.unwrap(); + delete_oauth_token( + db_client, + &user.id, + token, + ).await.unwrap(); + } +}