e870254
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<i64>,
pub scopes: Option<Vec<String>>,
}
#[tracing::instrument(skip_all, fields(request_id = %request_id.as_str()))]
pub async fn issue(
svc: web::Data<AuthSvc>,
web_cfg: web::Data<WebConfig>,
session: crate::session::Session,
req: HttpRequest,
request_id: crate::session::RequestId,
body: web::Json<TicketRequest>,
) -> 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::protocol::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<HttpResponse, AuthError> {
if !svc.auth().oauth.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::<String>("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<SocketTicket> {
if ticket.len() != 43
|| !self
.auth()
.oauth
.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)
}
}