feat: allow connecting to ws via oauth2
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
use iso8601_timestamp::Timestamp;
|
||||
|
||||
use crate::OAuth2Scope;
|
||||
|
||||
auto_derived! (
|
||||
/// Unique id of the user and bot
|
||||
#[derive(Hash)]
|
||||
@@ -23,6 +25,6 @@ auto_derived! (
|
||||
pub deauthorized_at: Option<Timestamp>,
|
||||
|
||||
/// Scopes the bot has access to
|
||||
pub scope: String
|
||||
pub scope: Vec<OAuth2Scope>,
|
||||
}
|
||||
);
|
||||
@@ -49,9 +49,14 @@ auto_derived_partial!(
|
||||
|
||||
auto_derived!(
|
||||
#[derive(Copy, Hash)]
|
||||
#[serde(rename = "lowercase")]
|
||||
pub enum OAuth2Scope {
|
||||
Identify,
|
||||
#[serde(rename = "read:identify")]
|
||||
ReadIdentify,
|
||||
#[serde(rename = "read:servers")]
|
||||
ReadServers,
|
||||
#[serde(rename = "events")]
|
||||
Events,
|
||||
#[serde(rename = "full")]
|
||||
Full,
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ impl<'r> FromRequest<'r> for User {
|
||||
|
||||
let claims = oauth2::decode_token(&config.api.security.token_secret, &header_oauth_token).ok()?;
|
||||
|
||||
if oauth2::scopes_can_access_route(&claims.scope, request) {
|
||||
if oauth2::scopes_can_access_route(&claims.scopes, request) {
|
||||
if let Ok(user) = db.fetch_user(&claims.sub).await {
|
||||
return Some(user)
|
||||
}
|
||||
|
||||
@@ -92,7 +92,9 @@ impl From<crate::BotOauth2> for BotOauth2 {
|
||||
impl From<crate::OAuth2Scope> for OAuth2Scope {
|
||||
fn from(value: crate::OAuth2Scope) -> Self {
|
||||
match value {
|
||||
crate::OAuth2Scope::Identify => OAuth2Scope::Identify,
|
||||
crate::OAuth2Scope::ReadIdentify => OAuth2Scope::ReadIdentify,
|
||||
crate::OAuth2Scope::ReadServers => OAuth2Scope::ReadServers,
|
||||
crate::OAuth2Scope::Events => OAuth2Scope::Events,
|
||||
crate::OAuth2Scope::Full => OAuth2Scope::Full,
|
||||
}
|
||||
}
|
||||
@@ -101,7 +103,9 @@ impl From<crate::OAuth2Scope> for OAuth2Scope {
|
||||
impl From<OAuth2Scope> for crate::OAuth2Scope {
|
||||
fn from(value: OAuth2Scope) -> Self {
|
||||
match value {
|
||||
OAuth2Scope::Identify => crate::OAuth2Scope::Identify,
|
||||
OAuth2Scope::ReadIdentify => crate::OAuth2Scope::ReadIdentify,
|
||||
OAuth2Scope::ReadServers => crate::OAuth2Scope::ReadServers,
|
||||
OAuth2Scope::Events => crate::OAuth2Scope::Events,
|
||||
OAuth2Scope::Full => crate::OAuth2Scope::Full,
|
||||
}
|
||||
}
|
||||
@@ -1479,7 +1483,7 @@ impl From<AuthorizedBot> for crate::AuthorizedBot {
|
||||
id: value.id.into(),
|
||||
created_at: value.created_at,
|
||||
deauthorized_at: value.deauthorized_at,
|
||||
scope: value.scope
|
||||
scope: value.scope.into_iter().map(|scope| scope.into()).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1490,7 +1494,7 @@ impl From<crate::AuthorizedBot> for AuthorizedBot {
|
||||
id: value.id.into(),
|
||||
created_at: value.created_at,
|
||||
deauthorized_at: value.deauthorized_at,
|
||||
scope: value.scope
|
||||
scope: value.scope.into_iter().map(|scope| scope.into()).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,10 @@ use redis_kiss::AsyncCommands;
|
||||
use revolt_result::Result;
|
||||
|
||||
#[cfg(feature = "rocket")]
|
||||
use rocket::{http::Method, Request};
|
||||
use rocket::Request;
|
||||
|
||||
pub use jsonwebtoken::errors::{Error as JWTError, ErrorKind as JWTErrorKind};
|
||||
use ulid::Ulid;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum TokenType {
|
||||
@@ -27,12 +30,13 @@ impl TokenType {
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Claims {
|
||||
pub jti: String,
|
||||
pub sub: String,
|
||||
pub exp: i64,
|
||||
|
||||
pub r#type: TokenType,
|
||||
pub client_id: String,
|
||||
pub scope: String,
|
||||
pub scopes: Vec<v0::OAuth2Scope>,
|
||||
pub redirect_uri: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub code_challange_method: Option<v0::OAuth2CodeChallangeMethod>,
|
||||
@@ -45,21 +49,25 @@ pub fn encode_token(
|
||||
user_id: String,
|
||||
client_id: String,
|
||||
redirect_uri: String,
|
||||
scope: String,
|
||||
scopes: Vec<v0::OAuth2Scope>,
|
||||
code_challange_method: Option<v0::OAuth2CodeChallangeMethod>,
|
||||
) -> Result<String, Error> {
|
||||
let exp = Utc::now()
|
||||
let now = Utc::now();
|
||||
|
||||
let exp = now
|
||||
.checked_add_signed(token_type.lifetime())
|
||||
.unwrap()
|
||||
.timestamp();
|
||||
.unwrap();
|
||||
|
||||
println!("{now:?} {exp:}");
|
||||
|
||||
let claims = Claims {
|
||||
jti: Ulid::new().to_string(),
|
||||
sub: user_id,
|
||||
exp,
|
||||
exp: exp.timestamp(),
|
||||
|
||||
r#type: token_type,
|
||||
client_id,
|
||||
scope,
|
||||
scopes,
|
||||
redirect_uri,
|
||||
code_challange_method
|
||||
};
|
||||
@@ -78,7 +86,9 @@ pub fn decode_token(token_secret: &str, code: &str) -> Result<Claims, Error> {
|
||||
}
|
||||
|
||||
#[cfg(feature = "rocket")]
|
||||
pub fn scope_can_access_route(scope: &str, request: &Request<'_>) -> bool {
|
||||
pub fn scope_can_access_route(scope: v0::OAuth2Scope, request: &Request<'_>) -> bool {
|
||||
println!("{:?}", request.route());
|
||||
|
||||
// TODO: figure out why request.segments(0..) is skipping the first segment
|
||||
let mut segments = request.uri().path().segments();
|
||||
|
||||
@@ -86,27 +96,30 @@ pub fn scope_can_access_route(scope: &str, request: &Request<'_>) -> bool {
|
||||
segments.next(); // skip first segment
|
||||
};
|
||||
|
||||
// Extract the function name of the route
|
||||
let Some(route_name) = request
|
||||
.route()
|
||||
.and_then(|route| route.name.as_ref())
|
||||
.map(|name| name.as_ref())
|
||||
else {
|
||||
return false
|
||||
};
|
||||
|
||||
match scope {
|
||||
"identify" => {
|
||||
request.method() == Method::Get &&
|
||||
segments.get(0) == Some("users") &&
|
||||
segments.get(1) == Some("@me")
|
||||
},
|
||||
"full" => true,
|
||||
_ => false
|
||||
v0::OAuth2Scope::ReadIdentify => route_name == "fetch_self",
|
||||
v0::OAuth2Scope::ReadServers => route_name == "fetch_self_servers" || route_name == "fetch_server",
|
||||
// This only grants access to the events websocket and not any routes
|
||||
v0::OAuth2Scope::Events => false,
|
||||
// TODO: maybe disallow revoking other sessions
|
||||
v0::OAuth2Scope::Full => true,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "rocket")]
|
||||
pub fn scopes_can_access_route(scopes: &str, request: &Request<'_>) -> bool {
|
||||
println!("{scopes}");
|
||||
for scope in scopes.split(' ') {
|
||||
if scope_can_access_route(scope, request) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
pub fn scopes_can_access_route(scopes: &[v0::OAuth2Scope], request: &Request<'_>) -> bool {
|
||||
scopes
|
||||
.iter()
|
||||
.any(|&scope| scope_can_access_route(scope, request))
|
||||
}
|
||||
|
||||
pub async fn add_code_challange(token: &str, code_challenge: &str) -> Result<()> {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use iso8601_timestamp::Timestamp;
|
||||
|
||||
use crate::v0::PublicBot;
|
||||
use crate::v0::{PublicBot, OAuth2Scope};
|
||||
|
||||
auto_derived!(
|
||||
/// Unique id of the user and bot
|
||||
@@ -24,11 +24,11 @@ auto_derived!(
|
||||
pub deauthorized_at: Option<Timestamp>,
|
||||
|
||||
/// Scopes the bot has access to
|
||||
pub scope: String
|
||||
pub scope: Vec<OAuth2Scope>,
|
||||
}
|
||||
|
||||
pub struct AuthorizedBotsResponse {
|
||||
pub public_bot: PublicBot,
|
||||
pub authorized_bot: AuthorizedBot
|
||||
pub authorized_bot: AuthorizedBot,
|
||||
}
|
||||
);
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::v0::{PublicBot, User};
|
||||
use std::collections::HashMap;
|
||||
use std::{collections::HashMap, fmt::Display};
|
||||
|
||||
auto_derived!(
|
||||
pub struct OAuth2AuthorizeAuthResponse {
|
||||
@@ -85,18 +85,37 @@ auto_derived!(
|
||||
}
|
||||
|
||||
#[derive(Copy, Hash)]
|
||||
#[cfg_attr(feature = "serde", serde(rename = "lowercase"))]
|
||||
#[cfg_attr(feature = "serde", serde(rename = "snake_case"))]
|
||||
pub enum OAuth2Scope {
|
||||
Identify,
|
||||
#[cfg_attr(feature = "serde", serde(rename = "read:identify"))]
|
||||
ReadIdentify,
|
||||
#[cfg_attr(feature = "serde", serde(rename = "read:servers"))]
|
||||
ReadServers,
|
||||
#[cfg_attr(feature = "serde", serde(rename = "events"))]
|
||||
Events,
|
||||
#[cfg_attr(feature = "serde", serde(rename = "full"))]
|
||||
Full,
|
||||
}
|
||||
);
|
||||
|
||||
impl Display for OAuth2Scope {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(match self {
|
||||
OAuth2Scope::ReadIdentify => "read:identify",
|
||||
OAuth2Scope::ReadServers => "read:servers",
|
||||
OAuth2Scope::Events => "events",
|
||||
OAuth2Scope::Full => "full",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl OAuth2Scope {
|
||||
#[allow(clippy::should_implement_trait)]
|
||||
pub fn from_str(str: &str) -> Option<OAuth2Scope> {
|
||||
match str {
|
||||
"identify" => Some(OAuth2Scope::Identify),
|
||||
"read:identify" => Some(OAuth2Scope::ReadIdentify),
|
||||
"read:servers" => Some(OAuth2Scope::ReadServers),
|
||||
"events" => Some(OAuth2Scope::Events),
|
||||
"full" => Some(OAuth2Scope::Full),
|
||||
_ => None,
|
||||
}
|
||||
|
||||
@@ -78,6 +78,9 @@ impl IntoResponse for Error {
|
||||
ErrorType::InvalidFlagValue => StatusCode::BAD_REQUEST,
|
||||
ErrorType::FeatureDisabled { .. } => StatusCode::BAD_REQUEST,
|
||||
|
||||
ErrorType::MissingScope { .. } => StatusCode::UNAUTHORIZED,
|
||||
ErrorType::ExpiredToken => StatusCode::UNAUTHORIZED,
|
||||
|
||||
ErrorType::ProxyError => StatusCode::BAD_REQUEST,
|
||||
ErrorType::FileTooSmall => StatusCode::UNPROCESSABLE_ENTITY,
|
||||
ErrorType::FileTooLarge { .. } => StatusCode::UNPROCESSABLE_ENTITY,
|
||||
|
||||
@@ -168,6 +168,12 @@ pub enum ErrorType {
|
||||
ImageProcessingFailed,
|
||||
NoEmbedData,
|
||||
|
||||
// ? OAuth2 Errors
|
||||
ExpiredToken,
|
||||
MissingScope {
|
||||
scope: String
|
||||
},
|
||||
|
||||
// ? Legacy errors
|
||||
VosoUnavailable,
|
||||
|
||||
|
||||
@@ -84,6 +84,9 @@ impl<'r> Responder<'r, 'static> for Error {
|
||||
ErrorType::FailedValidation { .. } => Status::BadRequest,
|
||||
ErrorType::FeatureDisabled { .. } => Status::BadRequest,
|
||||
|
||||
ErrorType::MissingScope { .. } => Status::Unauthorized,
|
||||
ErrorType::ExpiredToken => Status::Unauthorized,
|
||||
|
||||
ErrorType::ProxyError => Status::BadRequest,
|
||||
ErrorType::FileTooSmall => Status::UnprocessableEntity,
|
||||
ErrorType::FileTooLarge { .. } => Status::UnprocessableEntity,
|
||||
|
||||
Reference in New Issue
Block a user