use chrono::Utc; use revolt_config::config; use revolt_database::{ util::{ oauth2, reference::Reference, }, Database, }; use revolt_models::v0; use revolt_result::{create_error, Result}; use rocket::{form::Form, serde::json::Json, State}; use redis_kiss::AsyncCommands; /// # OAuth Token Exchange /// /// #[openapi(tag = "OAuth2")] #[post("/token", data = "")] pub async fn token( db: &State, info: Form, ) -> Result> { let bot = Reference::from_unchecked(info.client_id.clone()) .as_bot(db) .await?; let config = config().await; let claims = oauth2::decode_token(&config.api.security.token_secret, &info.code) .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 { return Err(create_error!(NotAuthenticated)) } if claims.client_id != info.client_id || claims.exp < Utc::now().timestamp() { return Err(create_error!(NotAuthenticated)); }; match info.grant_type { v0::OAuth2GrantType::AuthorizationCode => { if claims.r#type != oauth2::TokenType::Auth { return Err(create_error!(InvalidOperation)) } let token = oauth2::encode_token( &config.api.security.token_secret, oauth2::TokenType::Access, claims.sub, claims.client_id, claims.redirect_uri, claims.scope.clone(), None, ) .map_err(|_| create_error!(InternalError))?; Ok(Json(v0::OAuth2TokenExchangeResponse { access_token: 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)) } } }