use std::time::Duration; use crate::config::web::WebConfig; use actix_web::{HttpRequest, HttpResponse, web}; use serde::{Deserialize, Serialize}; use crate::server::share::crypto::{random_b64url, sha256_hex}; use crate::server::{AuthError, AuthResult, AuthSvc}; #[derive(Deserialize)] pub struct TicketRequest { origin: String, } #[derive(Serialize, Deserialize)] pub struct SocketTicket { pub user_id: i64, pub version: i32, pub session_id: String, pub origin: String, pub expires_at: i64, pub credential_expires_at: Option, pub scopes: Option>, } #[tracing::instrument(skip_all, fields(request_id = %request_id.as_str()))] pub async fn issue( svc: web::Data, web_cfg: web::Data, session: crate::session::Session, req: HttpRequest, request_id: crate::session::RequestId, body: web::Json, ) -> HttpResponse { use actix_web::ResponseError; let response = issue_inner(&svc, session, &req, &body, Some(request_id.as_str())) .await .unwrap_or_else(|error| error.error_response()); crate::server::http::cors::cors(&svc, &web_cfg, &req, response) } #[tracing::instrument(skip_all, fields(request_id = %request_id.unwrap_or_default()))] async fn issue_inner( svc: &AuthSvc, session: crate::session::Session, req: &HttpRequest, body: &TicketRequest, request_id: Option<&str>, ) -> Result { if !svc.auth().socket_origins.contains(&body.origin) { return Err(AuthError::Forbidden); } let origin = req .headers() .get("origin") .and_then(|value| value.to_str().ok()); let authorization = req.headers().get("authorization"); let now = crate::jwt::now_secs(); let (user_id, version, session_id, scopes, expires_at, credential_expires_at) = if let Some(header) = authorization { let (scheme, token) = header .to_str() .ok() .and_then(|value| value.split_once(' ')) .ok_or(AuthError::Unauthenticated)?; if !scheme.eq_ignore_ascii_case("bearer") { return Err(AuthError::Unauthenticated); } let claims = crate::jwt::authority::authenticate(token.trim()) .await .map_err(|_| AuthError::Unauthenticated)?; if claims.scope.as_ref().is_some_and(|scopes| { !scopes .iter() .any(|scope| matches!(scope.as_str(), "event" | "guild")) }) { return Err(AuthError::ScopeInsufficient { required: "event or guild".into(), }); } ( claims.user_id, claims.security_version, claims.session_id.ok_or(AuthError::Unauthenticated)?, claims.scope, claims.expires_at.min(now + 60), Some(claims.expires_at), ) } else { if origin != Some(body.origin.as_str()) { return Err(AuthError::Forbidden); } let user_id = svc.require_session(&session, request_id).await?; ( user_id, session .security_version()? .ok_or(AuthError::Unauthenticated)?, session .get::("auth_session_id")? .ok_or(AuthError::Unauthenticated)?, None, now + 60, None, ) }; if origin.is_some_and(|origin| origin != body.origin) || expires_at <= now { return Err(AuthError::Forbidden); } let ticket = random_b64url(32); let grant = SocketTicket { user_id, version, session_id, scopes, origin: body.origin.clone(), expires_at, credential_expires_at, }; svc.cache() .set_ex( &format!("auth:socket:ticket:{}", sha256_hex(ticket.as_bytes())), serde_json::to_string(&grant)?, Duration::from_secs((expires_at - now) as u64), ) .await?; Ok(HttpResponse::Ok() .insert_header(("cache-control", "no-store")) .insert_header(("pragma", "no-cache")) .json(serde_json::json!({ "ticket": ticket, "expires_in": expires_at - now }))) } impl AuthSvc { #[tracing::instrument(skip_all, fields(request_id = %request_id.unwrap_or_default()))] pub async fn redeem_socket_ticket( &self, ticket: &str, origin: &str, request_id: Option<&str>, ) -> AuthResult { if ticket.len() != 43 || !self .auth() .socket_origins .iter() .any(|allowed| allowed == origin) { return Err(AuthError::Unauthenticated); } let raw: String = self .cache() .get_del(&format!( "auth:socket:ticket:{}", sha256_hex(ticket.as_bytes()) )) .await? .ok_or(AuthError::Unauthenticated)?; let grant: SocketTicket = serde_json::from_str(&raw)?; if grant.origin != origin || grant.expires_at <= crate::jwt::now_secs() { return Err(AuthError::Unauthenticated); } self.check_session(grant.user_id, grant.version, &grant.session_id, request_id) .await .map_err(|_| AuthError::Unauthenticated)?; Ok(grant) } }