feat: initial work on refresh tokens
This commit is contained in:
@@ -5,8 +5,7 @@ use revolt_database::{
|
||||
};
|
||||
use revolt_models::v0;
|
||||
use revolt_result::{create_error, Result};
|
||||
use rocket::{form::Form, serde::json::Json, State};
|
||||
use redis_kiss::AsyncCommands;
|
||||
use rocket::{serde::json::Json, State};
|
||||
|
||||
/// # OAuth2 Authorization Auth
|
||||
///
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use revolt_database::{util::reference::Reference, Database, User};
|
||||
use revolt_models::v0;
|
||||
use revolt_result::{create_error, Result};
|
||||
use rocket::{form::Form, serde::json::Json, State};
|
||||
use rocket::{serde::json::Json, State};
|
||||
|
||||
/// # Authorize OAuth Information
|
||||
///
|
||||
|
||||
@@ -3,18 +3,25 @@ use rocket::{serde::json::Json, State};
|
||||
use revolt_database::{Database, User};
|
||||
use revolt_result::Result;
|
||||
|
||||
|
||||
#[openapi(tag = "OAuth2")]
|
||||
#[get("/authorized_bots")]
|
||||
pub async fn authorized_bots(
|
||||
db: &State<Database>,
|
||||
user: User
|
||||
) -> Result<Json<Vec<v0::AuthorizedBot>>> {
|
||||
) -> Result<Json<Vec<v0::AuthorizedBotsResponse>>> {
|
||||
let authorized_bots = db.fetch_users_authorized_bots(&user.id).await?;
|
||||
|
||||
Ok(Json(authorized_bots
|
||||
.into_iter()
|
||||
.map(|bot| bot.into())
|
||||
.collect()
|
||||
))
|
||||
let mut response = Vec::new();
|
||||
|
||||
for authorized_bot in authorized_bots {
|
||||
let bot = db.fetch_bot(&authorized_bot.id.bot).await?;
|
||||
let bot_user = db.fetch_user(&authorized_bot.id.bot).await?;
|
||||
|
||||
response.push(v0::AuthorizedBotsResponse {
|
||||
authorized_bot: authorized_bot.into(),
|
||||
public_bot: bot.into_public_bot(bot_user)
|
||||
});
|
||||
};
|
||||
|
||||
Ok(Json(response))
|
||||
}
|
||||
@@ -8,9 +8,8 @@ use revolt_database::{
|
||||
}, AuthorizedBot, AuthorizedBotId, Database
|
||||
};
|
||||
use revolt_models::v0;
|
||||
use revolt_result::{create_error, Result};
|
||||
use revolt_result::{create_error, ErrorType, Result};
|
||||
use rocket::{form::Form, serde::json::Json, State};
|
||||
use redis_kiss::AsyncCommands;
|
||||
|
||||
/// # OAuth Token Exchange
|
||||
///
|
||||
@@ -27,64 +26,136 @@ pub async fn token(
|
||||
|
||||
let config = config().await;
|
||||
|
||||
let claims = oauth2::decode_token(&config.api.security.token_secret, &info.code)
|
||||
let (token, token_type) = match (&info.code, &info.refresh_token) {
|
||||
(Some(code), None) => {
|
||||
(code, oauth2::TokenType::Auth)
|
||||
},
|
||||
(None, Some(refresh_token)) => {
|
||||
(refresh_token, oauth2::TokenType::Refresh)
|
||||
},
|
||||
(Some(_), Some(_)) | (None, None) => {
|
||||
return Err(create_error!(InvalidOperation))
|
||||
}
|
||||
};
|
||||
|
||||
let claims = oauth2::decode_token(&config.api.security.token_secret, token)
|
||||
.map_err(|_| create_error!(NotAuthenticated))?;
|
||||
|
||||
if let Some((client_secret, bot_secret)) = info.client_secret.as_ref().zip(bot.oauth2.as_ref().and_then(|oauth2| oauth2.secret.as_ref())) {
|
||||
if client_secret != bot_secret {
|
||||
return Err(create_error!(NotAuthenticated))
|
||||
}
|
||||
} else if let Some((code_verifier, method)) = info.code_verifier.as_ref().zip(claims.code_challange_method) {
|
||||
let Some(server_code_challenge) = oauth2::get_code_challange(&info.code).await? else {
|
||||
return Err(create_error!(NotAuthenticated))
|
||||
};
|
||||
|
||||
let is_valid = match method {
|
||||
v0::OAuth2CodeChallangeMethod::Plain => &server_code_challenge == code_verifier,
|
||||
v0::OAuth2CodeChallangeMethod::S256 => pkce::code_challenge(code_verifier.as_bytes()) == server_code_challenge,
|
||||
};
|
||||
|
||||
if !is_valid {
|
||||
return Err(create_error!(NotAuthenticated))
|
||||
}
|
||||
} else {
|
||||
if claims.r#type != token_type || claims.client_id != info.client_id || claims.exp < Utc::now().timestamp() {
|
||||
return Err(create_error!(NotAuthenticated))
|
||||
}
|
||||
};
|
||||
|
||||
if claims.client_id != info.client_id || claims.exp < Utc::now().timestamp() {
|
||||
let authorized_bot = db.fetch_authorized_bot(&AuthorizedBotId { user: claims.sub.clone(), bot: claims.client_id.clone() }).await?;
|
||||
|
||||
if authorized_bot.deauthorized_at.is_some() {
|
||||
return Err(create_error!(NotAuthenticated));
|
||||
};
|
||||
|
||||
// TODO: track used tokens and dont allow them to be used twice
|
||||
// - refresh token
|
||||
// - auth tokens (only last 5 mins but still should be considered)
|
||||
|
||||
match info.grant_type {
|
||||
v0::OAuth2GrantType::AuthorizationCode => {
|
||||
if claims.r#type != oauth2::TokenType::Auth {
|
||||
return Err(create_error!(InvalidOperation))
|
||||
}
|
||||
|
||||
if let Some((client_secret, bot_secret)) = info.client_secret.as_ref().zip(bot.oauth2.as_ref().and_then(|oauth2| oauth2.secret.as_ref())) {
|
||||
if client_secret != bot_secret {
|
||||
return Err(create_error!(NotAuthenticated))
|
||||
}
|
||||
} else if let Some((code_verifier, method)) = info.code_verifier.as_ref().zip(claims.code_challange_method) {
|
||||
let Some(server_code_challenge) = oauth2::get_code_challange(token).await? else {
|
||||
return Err(create_error!(NotAuthenticated))
|
||||
};
|
||||
|
||||
let is_valid = match method {
|
||||
v0::OAuth2CodeChallangeMethod::Plain => &server_code_challenge == code_verifier,
|
||||
v0::OAuth2CodeChallangeMethod::S256 => pkce::code_challenge(code_verifier.as_bytes()) == server_code_challenge,
|
||||
};
|
||||
|
||||
if !is_valid {
|
||||
return Err(create_error!(NotAuthenticated))
|
||||
}
|
||||
} else {
|
||||
return Err(create_error!(NotAuthenticated))
|
||||
}
|
||||
|
||||
let token = oauth2::encode_token(
|
||||
&config.api.security.token_secret,
|
||||
oauth2::TokenType::Access,
|
||||
claims.sub.clone(),
|
||||
claims.client_id.clone(),
|
||||
claims.redirect_uri,
|
||||
claims.redirect_uri.clone(),
|
||||
claims.scope.clone(),
|
||||
None,
|
||||
)
|
||||
.map_err(|_| create_error!(InternalError))?;
|
||||
|
||||
db.insert_authorized_bot(&AuthorizedBot {
|
||||
id: AuthorizedBotId { bot: claims.client_id.clone(), user: claims.sub.clone() },
|
||||
created_at: Timestamp::now_utc(),
|
||||
deauthorized_at: None,
|
||||
scope: claims.scope.clone()
|
||||
}).await?;
|
||||
let refresh_token = oauth2::encode_token(
|
||||
&config.api.security.token_secret,
|
||||
oauth2::TokenType::Refresh,
|
||||
claims.sub.clone(),
|
||||
claims.client_id.clone(),
|
||||
claims.redirect_uri.clone(),
|
||||
claims.scope.clone(),
|
||||
None,
|
||||
)
|
||||
.map_err(|_| create_error!(InternalError))?;
|
||||
|
||||
let authorized_bot_id = AuthorizedBotId { bot: claims.client_id.clone(), user: claims.sub.clone() };
|
||||
|
||||
if db.fetch_authorized_bot(&authorized_bot_id).await.is_err_and(|err| err.error_type == ErrorType::NotFound) {
|
||||
db.insert_authorized_bot(&AuthorizedBot {
|
||||
id: authorized_bot_id,
|
||||
created_at: Timestamp::now_utc(),
|
||||
deauthorized_at: None,
|
||||
scope: claims.scope.clone()
|
||||
}).await?;
|
||||
}
|
||||
|
||||
Ok(Json(v0::OAuth2TokenExchangeResponse {
|
||||
access_token: token,
|
||||
refresh_token: Some(refresh_token),
|
||||
token_type: "OAuth2".to_string(),
|
||||
scope: claims.scope,
|
||||
}))
|
||||
},
|
||||
v0::OAuth2GrantType::RefreshToken => {
|
||||
if claims.r#type != oauth2::TokenType::Refresh {
|
||||
return Err(create_error!(InvalidOperation))
|
||||
};
|
||||
|
||||
let token = oauth2::encode_token(
|
||||
&config.api.security.token_secret,
|
||||
oauth2::TokenType::Access,
|
||||
claims.sub.clone(),
|
||||
claims.client_id.clone(),
|
||||
claims.redirect_uri.clone(),
|
||||
claims.scope.clone(),
|
||||
None,
|
||||
)
|
||||
.map_err(|_| create_error!(InternalError))?;
|
||||
|
||||
let refresh_token = oauth2::encode_token(
|
||||
&config.api.security.token_secret,
|
||||
oauth2::TokenType::Refresh,
|
||||
claims.sub.clone(),
|
||||
claims.client_id.clone(),
|
||||
claims.redirect_uri.clone(),
|
||||
claims.scope.clone(),
|
||||
None,
|
||||
)
|
||||
.map_err(|_| create_error!(InternalError))?;
|
||||
|
||||
Ok(Json(v0::OAuth2TokenExchangeResponse {
|
||||
access_token: token,
|
||||
refresh_token: Some(refresh_token),
|
||||
token_type: "OAuth2".to_string(),
|
||||
scope: claims.scope,
|
||||
}))
|
||||
}
|
||||
v0::OAuth2GrantType::Implicit => {
|
||||
// token is already an access token so this endpoint does not need to be called - in theory this should be unreachable
|
||||
Err(create_error!(InvalidOperation))
|
||||
|
||||
@@ -12,7 +12,6 @@ mod fetch_user;
|
||||
mod fetch_user_flags;
|
||||
mod find_mutual;
|
||||
mod get_default_avatar;
|
||||
mod oauth2_connections;
|
||||
mod open_dm;
|
||||
mod remove_friend;
|
||||
mod send_friend_request;
|
||||
|
||||
Reference in New Issue
Block a user