Add API method for revoking access token

This commit is contained in:
silverpill 2022-11-15 15:45:30 +00:00
parent b3d03507ad
commit 445177d9a7
4 changed files with 118 additions and 5 deletions

View file

@ -59,6 +59,25 @@ paths:
example: 1639747526 example: 1639747526
400: 400:
description: Invalid token request 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: /api/v1/accounts:
post: post:
summary: Creates a user and profile records. summary: Creates a user and profile records.

View file

@ -31,3 +31,8 @@ impl TokenResponse {
} }
} }
} }
#[derive(Deserialize)]
pub struct RevocationRequest {
pub token: String,
}

View file

@ -1,18 +1,23 @@
use actix_web::{post, web, HttpResponse, Scope as ActixScope}; use actix_web::{post, web, HttpResponse, Scope as ActixScope};
use actix_web_httpauth::extractors::bearer::BearerAuth;
use chrono::{Duration, Utc}; use chrono::{Duration, Utc};
use crate::config::Config; use crate::config::Config;
use crate::database::{Pool, get_database_client}; 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::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::{ use crate::models::users::queries::{
get_user_by_name, get_user_by_name,
get_user_by_login_address, get_user_by_login_address,
}; };
use crate::utils::currencies::{validate_wallet_address, Currency}; use crate::utils::currencies::{validate_wallet_address, Currency};
use crate::utils::passwords::verify_password; 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; use super::utils::generate_access_token;
const ACCESS_TOKEN_EXPIRES_IN: i64 = 86400 * 7; const ACCESS_TOKEN_EXPIRES_IN: i64 = 86400 * 7;
@ -87,7 +92,28 @@ async fn token_view(
Ok(HttpResponse::Ok().json(token_response)) Ok(HttpResponse::Ok().json(token_response))
} }
#[post("/revoke")]
async fn revoke_token_view(
auth: BearerAuth,
db_pool: web::Data<Pool>,
request_data: web::Json<RevocationRequest>,
) -> Result<HttpResponse, HttpError> {
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,
&current_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 { pub fn oauth_api_scope() -> ActixScope {
web::scope("/oauth") web::scope("/oauth")
.service(token_view) .service(token_view)
.service(revoke_token_view)
} }

View file

@ -9,7 +9,7 @@ use crate::models::users::types::{DbUser, User};
pub async fn save_oauth_token( pub async fn save_oauth_token(
db_client: &impl GenericClient, db_client: &impl GenericClient,
owner_id: &Uuid, owner_id: &Uuid,
access_token: &str, token: &str,
created_at: &DateTime<Utc>, created_at: &DateTime<Utc>,
expires_at: &DateTime<Utc>, expires_at: &DateTime<Utc>,
) -> Result<(), DatabaseError> { ) -> Result<(), DatabaseError> {
@ -18,11 +18,41 @@ pub async fn save_oauth_token(
INSERT INTO oauth_token (owner_id, token, created_at, expires_at) INSERT INTO oauth_token (owner_id, token, created_at, expires_at)
VALUES ($1, $2, $3, $4) VALUES ($1, $2, $3, $4)
", ",
&[&owner_id, &access_token, &created_at, &expires_at], &[&owner_id, &token, &created_at, &expires_at],
).await?; ).await?;
Ok(()) 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( pub async fn get_user_by_oauth_token(
db_client: &impl GenericClient, db_client: &impl GenericClient,
access_token: &str, access_token: &str,
@ -45,3 +75,36 @@ pub async fn get_user_by_oauth_token(
let user = User::new(db_user, db_profile); let user = User::new(db_user, db_profile);
Ok(user) 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();
}
}