use serde::{Deserialize, Serialize};
use thiserror::Error;
#[non_exhaustive]
#[derive(Debug, Error, PartialEq, Eq)]
pub enum TokenParseError {
#[error("token must begin with {expected:?}")]
WrongPrefix {
expected: &'static str,
},
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct OpaqueRefreshToken(String);
impl OpaqueRefreshToken {
#[must_use = "parsed refresh token must be either hashed for storage or returned to client"]
pub fn parse(s: &str) -> Result<Self, TokenParseError> {
if !s.starts_with("upr_") {
return Err(TokenParseError::WrongPrefix { expected: "upr_" });
}
Ok(Self(s.to_string()))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AuthorizationCode(String);
impl AuthorizationCode {
#[must_use = "parsed authorization code must be hashed for storage or redeemed at the token endpoint"]
pub fn parse(s: &str) -> Result<Self, TokenParseError> {
if !s.starts_with("upc_") {
return Err(TokenParseError::WrongPrefix { expected: "upc_" });
}
Ok(Self(s.to_string()))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct McpAccessTokenClaims {
pub iss: String,
pub sub: String,
pub aud: String,
pub client_id: String,
pub scope: String,
pub jti: String,
pub iat: i64,
pub nbf: i64,
pub exp: i64,
pub tenant_id: String,
}
impl McpAccessTokenClaims {
#[must_use]
#[expect(
clippy::too_many_arguments,
reason = "all JWT spec claims are required"
)]
pub fn new(
iss: String,
sub: String,
aud: String,
client_id: String,
scope: String,
jti: String,
iat: i64,
nbf: i64,
exp: i64,
tenant_id: String,
) -> Self {
Self {
iss,
sub,
aud,
client_id,
scope,
jti,
iat,
nbf,
exp,
tenant_id,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn opaque_refresh_token_must_use_upr_prefix() {
OpaqueRefreshToken::parse("upr_abc").unwrap();
assert!(matches!(
OpaqueRefreshToken::parse("upk_abc"),
Err(TokenParseError::WrongPrefix { .. })
));
}
#[test]
fn authorization_code_must_use_upc_prefix() {
AuthorizationCode::parse("upc_abc").unwrap();
assert!(matches!(
AuthorizationCode::parse("upr_abc"),
Err(TokenParseError::WrongPrefix { .. })
));
}
#[test]
fn access_claims_round_trip_json() {
let claims = McpAccessTokenClaims {
iss: "https://example.com".into(),
sub: "00000000-0000-0000-0000-000000000001".into(),
aud: "https://example.com/mcp".into(),
client_id: "abc".into(),
scope: "mcp:read mcp:write".into(),
jti: "00000000-0000-0000-0000-000000000002".into(),
iat: 1_715_520_000,
nbf: 1_715_520_000,
exp: 1_715_520_900,
tenant_id: "00000000-0000-0000-0000-000000000003".into(),
};
let json = serde_json::to_string(&claims).unwrap();
let back: McpAccessTokenClaims = serde_json::from_str(&json).unwrap();
assert_eq!(claims, back);
}
}