use aion_core::{AssistantSessionId, AssistantSessionState};
use crate::assistant::sessions::{AssistantSessionError, AssistantSessions};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct AssistantSessionCaller {
session_id: AssistantSessionId,
}
impl AssistantSessionCaller {
pub(crate) const fn session_id(&self) -> AssistantSessionId {
self.session_id
}
}
#[derive(Debug, thiserror::Error)]
pub(crate) enum AssistantMcpAuthError {
#[error("the request carries no readable `authorization: Bearer …` header")]
NoBearer,
#[error("the request carries no `{header}` header naming the calling session")]
NoSession {
header: &'static str,
},
#[error("`{presented}` in the session header is not an assistant session id")]
MalformedSession {
presented: String,
},
#[error("assistant session {session_id} has no record on this server")]
UnknownSession {
session_id: AssistantSessionId,
},
#[error("assistant session {session_id} was opened with no MCP server and mints no bearer")]
NoTokenMinted {
session_id: AssistantSessionId,
},
#[error("the presented bearer does not verify against assistant session {session_id}")]
WrongToken {
session_id: AssistantSessionId,
},
#[error(
"assistant session {session_id} has ended, so its bearer no longer authorizes anything"
)]
SessionEnded {
session_id: AssistantSessionId,
},
#[error("the assistant session store could not be read to authorize this call: {0}")]
Unreadable(#[from] AssistantSessionError),
}
impl AssistantMcpAuthError {
pub(crate) const fn client_message() -> &'static str {
"this route accepts only the session-scoped bearer this server minted for a live assistant \
session, presented with the session it belongs to. A human token does not authorize it, \
and a session that has ended no longer does either."
}
}
pub(crate) const AUTHORIZATION_HEADER: &str = "authorization";
pub(crate) use crate::assistant::sessions::token::SESSION_ID_HEADER;
pub(crate) async fn resolve(
sessions: &AssistantSessions,
headers: &[(String, String)],
) -> Result<AssistantSessionCaller, AssistantMcpAuthError> {
let presented = bearer(headers).ok_or(AssistantMcpAuthError::NoBearer)?;
let named = header(headers, SESSION_ID_HEADER).ok_or(AssistantMcpAuthError::NoSession {
header: SESSION_ID_HEADER,
})?;
let session_id = AssistantSessionId::parse(named).map_err(|_error| {
AssistantMcpAuthError::MalformedSession {
presented: named.to_owned(),
}
})?;
let record = sessions
.record(session_id)
.await?
.ok_or(AssistantMcpAuthError::UnknownSession { session_id })?;
let digest = record
.mcp_token_digest
.as_deref()
.ok_or(AssistantMcpAuthError::NoTokenMinted { session_id })?;
if !crate::assistant::sessions::token::matches(presented, digest) {
return Err(AssistantMcpAuthError::WrongToken { session_id });
}
let (state, _reason) = sessions.state_of_session(session_id).await?;
if state == AssistantSessionState::Ended {
return Err(AssistantMcpAuthError::SessionEnded { session_id });
}
Ok(AssistantSessionCaller { session_id })
}
fn bearer(headers: &[(String, String)]) -> Option<&str> {
let value = header(headers, AUTHORIZATION_HEADER)?;
let (scheme, secret) = value.split_once(' ')?;
if !scheme.eq_ignore_ascii_case(crate::assistant::sessions::token::SESSION_TOKEN_SCHEME) {
return None;
}
let secret = secret.trim();
(!secret.is_empty()).then_some(secret)
}
fn header<'headers>(headers: &'headers [(String, String)], name: &str) -> Option<&'headers str> {
headers
.iter()
.find(|(header, _value)| header.eq_ignore_ascii_case(name))
.map(|(_header, value)| value.as_str())
}
#[cfg(test)]
mod tests {
use super::*;
fn headers(pairs: &[(&str, &str)]) -> Vec<(String, String)> {
pairs
.iter()
.map(|(name, value)| ((*name).to_owned(), (*value).to_owned()))
.collect()
}
#[test]
fn a_bearer_is_read_whatever_case_the_scheme_and_header_arrive_in() {
let presented = headers(&[("Authorization", "bearer secret-token")]);
assert_eq!(bearer(&presented), Some("secret-token"));
}
#[test]
fn another_scheme_is_not_a_bearer() {
assert_eq!(bearer(&headers(&[("authorization", "Basic abc")])), None);
assert_eq!(bearer(&headers(&[("authorization", "Bearer")])), None);
assert_eq!(bearer(&headers(&[("authorization", "Bearer ")])), None);
assert_eq!(bearer(&headers(&[])), None);
}
#[test]
fn every_refusal_renders_the_same_message_to_the_caller() {
let message = AssistantMcpAuthError::client_message();
assert!(message.contains("session-scoped bearer"));
assert!(
!message.contains("not found") && !message.contains("ended,"),
"the client-facing message must not distinguish the refusals: {message}"
);
}
}