use crate::transport::http::core::types::DefaultClaims;
use std::fmt::Debug;
#[cfg(feature = "server-oauth")]
use volga::auth::OAuthConfig as VolgaOAuthConfig;
use volga::auth::{Algorithm, AuthClaims, Authorizer, BearerAuthConfig, DecodingKey, predicate};
impl AuthClaims for DefaultClaims {
#[inline]
fn role(&self) -> Option<&str> {
self.role.as_deref()
}
#[inline]
fn roles(&self) -> Option<&[String]> {
self.roles.as_deref()
}
#[inline]
fn permissions(&self) -> Option<&[String]> {
self.permissions.as_deref()
}
}
pub struct AuthConfig<C: AuthClaims = DefaultClaims> {
inner: BearerAuthConfig,
authorizer: Authorizer<C>,
#[cfg(feature = "server-oauth")]
aud_configured: bool,
#[cfg(feature = "server-oauth")]
iss_configured: bool,
#[cfg(feature = "server-oauth")]
oauth: Option<OAuthConfig>,
}
impl Debug for AuthConfig {
#[inline]
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("AuthConfig { .. }")
}
}
impl Default for AuthConfig {
#[inline]
fn default() -> Self {
Self {
inner: BearerAuthConfig::default(),
authorizer: default_auth_rules(),
#[cfg(feature = "server-oauth")]
aud_configured: false,
#[cfg(feature = "server-oauth")]
iss_configured: false,
#[cfg(feature = "server-oauth")]
oauth: None,
}
}
}
impl From<AuthConfig> for BearerAuthConfig {
#[inline]
fn from(auth: AuthConfig) -> Self {
auth.inner
}
}
impl<C: AuthClaims> AuthConfig<C> {
pub fn set_decoding_key(mut self, secret: &[u8]) -> Self {
self.inner = self
.inner
.set_decoding_key(DecodingKey::from_secret(secret));
self
}
pub fn with_alg(mut self, alg: Algorithm) -> Self {
self.inner = self.inner.with_alg(alg);
self
}
pub fn with_aud<I, T>(mut self, aud: I) -> Self
where
T: ToString,
I: AsRef<[T]>,
{
self.inner = self.inner.with_aud(aud);
#[cfg(feature = "server-oauth")]
{
self.aud_configured = true;
}
self
}
#[cfg(feature = "server-oauth")]
pub fn with_resource(mut self, uri: impl Into<String>) -> Self {
self.inner = self.inner.with_resource(uri);
self.aud_configured = true;
self
}
#[cfg(feature = "server-oauth")]
pub fn with_resources<I, U>(mut self, uris: I) -> Self
where
I: IntoIterator<Item = U>,
U: Into<String>,
{
self.inner = self.inner.with_resources(uris);
self.aud_configured = true;
self
}
pub fn with_iss<I, T>(mut self, iss: I) -> Self
where
T: ToString,
I: AsRef<[T]>,
{
self.inner = self.inner.with_iss(iss);
#[cfg(feature = "server-oauth")]
{
self.iss_configured = true;
}
self
}
pub fn validate_aud(mut self, validate: bool) -> Self {
self.inner = self.inner.validate_aud(validate);
self
}
pub fn validate_exp(mut self, validate: bool) -> Self {
self.inner = self.inner.validate_exp(validate);
self
}
pub fn validate_nbf(mut self, validate: bool) -> Self {
self.inner = self.inner.validate_nbf(validate);
self
}
#[cfg(feature = "server-oauth")]
pub fn with_oauth<F>(mut self, config: F) -> Self
where
F: FnOnce(OAuthConfig) -> OAuthConfig,
{
self.oauth = Some(config(OAuthConfig::default()));
self
}
#[cfg(feature = "server-oauth")]
pub(crate) fn oauth_issuer(&self) -> Option<&str> {
self.oauth.as_ref().and_then(|o| o.issuer.as_deref())
}
#[cfg(feature = "server-oauth")]
pub(crate) fn apply_mcp_defaults(&mut self, resource: Option<&str>) {
let Some(oauth) = &self.oauth else {
return;
};
if !self.iss_configured
&& let Some(issuer) = oauth.issuer.as_deref()
{
let issuer = issuer.to_owned();
self.inner = std::mem::take(&mut self.inner).with_iss([issuer]);
}
if !self.aud_configured
&& let Some(resource) = resource
{
self.inner = std::mem::take(&mut self.inner).with_resource(resource);
}
}
#[cfg(feature = "server-oauth")]
pub(crate) fn take_oauth(&mut self) -> Result<Option<VolgaOAuthConfig>, crate::error::Error> {
match self.oauth.take() {
None => Ok(None),
Some(oauth) if oauth.issuer.is_some() => Ok(Some(oauth.inner)),
Some(_) => Err(crate::error::Error::new(
crate::error::ErrorCode::InternalError,
"OAuth issuer is not configured; call `with_oauth(|oauth| oauth.with_issuer(..))`",
)),
}
}
pub(crate) fn into_parts(self) -> (BearerAuthConfig, Authorizer<C>) {
(self.inner, self.authorizer)
}
}
#[cfg(feature = "server-oauth")]
#[derive(Default)]
pub struct OAuthConfig {
issuer: Option<String>,
inner: VolgaOAuthConfig,
}
#[cfg(feature = "server-oauth")]
impl Debug for OAuthConfig {
#[inline]
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("OAuthConfig")
.field("issuer", &self.issuer)
.finish()
}
}
#[cfg(feature = "server-oauth")]
impl OAuthConfig {
pub fn with_issuer(mut self, issuer: impl Into<String>) -> Self {
let issuer = issuer.into();
self.inner = self.inner.with_issuer(issuer.as_str());
self.issuer = Some(issuer);
self
}
pub fn with_refresh_cooldown(mut self, cooldown: std::time::Duration) -> Self {
self.inner = self.inner.with_refresh_cooldown(cooldown);
self
}
pub fn with_max_key_age(mut self, max_age: std::time::Duration) -> Self {
self.inner = self.inner.with_max_key_age(max_age);
self
}
pub fn with_config<F>(mut self, config: F) -> Self
where
F: FnOnce(VolgaOAuthConfig) -> VolgaOAuthConfig,
{
self.inner = config(self.inner);
self
}
}
#[inline]
pub(super) fn default_auth_rules() -> Authorizer<DefaultClaims> {
predicate(|_| true)
}
#[cfg(all(test, feature = "server-oauth"))]
mod tests {
use super::*;
#[test]
fn it_records_the_issuer() {
let auth = AuthConfig::default().with_oauth(|o| o.with_issuer("https://auth.example.com"));
assert_eq!(auth.oauth_issuer(), Some("https://auth.example.com"));
}
#[test]
fn take_oauth_without_oauth_mode_is_none() {
let mut auth = AuthConfig::default();
assert!(auth.take_oauth().unwrap().is_none());
}
#[test]
fn take_oauth_without_issuer_fails() {
let mut auth = AuthConfig::default().with_oauth(|o| o);
assert!(auth.take_oauth().is_err());
}
#[test]
fn take_oauth_with_issuer_yields_volga_config() {
let mut auth =
AuthConfig::default().with_oauth(|o| o.with_issuer("https://auth.example.com"));
assert!(auth.take_oauth().unwrap().is_some());
}
fn resources_of(auth: &AuthConfig) -> String {
format!("{:?}", auth.inner)
}
#[test]
fn mcp_defaults_bind_audience_to_the_resource() {
let mut auth =
AuthConfig::default().with_oauth(|o| o.with_issuer("https://auth.example.com"));
auth.apply_mcp_defaults(Some("http://127.0.0.1:3000/mcp"));
assert!(resources_of(&auth).contains("http://127.0.0.1:3000/mcp"));
}
#[test]
fn explicit_audience_suppresses_the_default() {
let mut auth = AuthConfig::default()
.with_aud(["my-audience"])
.with_oauth(|o| o.with_issuer("https://auth.example.com"));
auth.apply_mcp_defaults(Some("http://127.0.0.1:3000/mcp"));
assert!(!resources_of(&auth).contains("http://127.0.0.1:3000/mcp"));
}
#[test]
fn mcp_defaults_outside_oauth_mode_are_noop() {
let mut auth = AuthConfig::default();
auth.apply_mcp_defaults(Some("http://127.0.0.1:3000/mcp"));
assert!(!resources_of(&auth).contains("http://127.0.0.1:3000/mcp"));
}
}