use super::resolve_audience;
use crate::token_verifier::{
GenericOauthTokenVerifier, TokenVerifierOptions, VerificationStrategies,
};
use async_trait::async_trait;
use bytes::Bytes;
use http::{header::CONTENT_TYPE, StatusCode};
use http_body_util::{BodyExt, Full};
use rust_mcp_sdk::{
auth::{
create_discovery_endpoints, Audience, AuthInfo, AuthMetadataBuilder, AuthProvider,
AuthenticationError, AuthorizationServerMetadata, OauthEndpoint,
OauthProtectedResourceMetadata, OauthTokenVerifier,
},
error::McpSdkError,
mcp_http::{
middleware::CorsMiddleware, GenericBody, GenericBodyExt, McpAppState, McpHttpError,
McpHttpResult, Middleware,
},
mcp_server::join_url,
};
use std::{collections::HashMap, sync::Arc, vec};
static SCOPES_SUPPORTED: &[&str] = &["email", "offline_access", "openid", "profile"];
pub struct WorkOSAuthOptions<'a> {
pub authkit_domain: String,
pub mcp_server_url: String,
pub required_scopes: Option<Vec<&'a str>>,
pub token_verifier: Option<Box<dyn OauthTokenVerifier>>,
pub resource_name: Option<String>,
pub resource_documentation: Option<String>,
pub validate_audience: Option<Audience>,
pub disable_audience_validation: bool,
}
pub struct WorkOsAuthProvider {
auth_server_meta: AuthorizationServerMetadata,
protected_resource_meta: OauthProtectedResourceMetadata,
endpoint_map: HashMap<String, OauthEndpoint>,
protected_resource_metadata_url: String,
token_verifier: Box<dyn OauthTokenVerifier>,
}
impl WorkOsAuthProvider {
pub fn new(mut options: WorkOSAuthOptions) -> Result<Self, McpSdkError> {
let (endpoint_map, protected_resource_metadata_url) =
create_discovery_endpoints(&options.mcp_server_url)?;
let required_scopes = options.required_scopes.take();
let scopes_supported = required_scopes.clone().unwrap_or(SCOPES_SUPPORTED.to_vec());
let mut builder = AuthMetadataBuilder::new(&options.mcp_server_url)
.issuer(&options.authkit_domain)
.authorization_servers(vec![&options.authkit_domain])
.authorization_endpoint("/oauth2/authorize")
.introspection_endpoint("/oauth2/introspection")
.registration_endpoint("/oauth2/register")
.token_endpoint("/oauth2/token")
.jwks_uri("/oauth2/jwks")
.scopes_supported(scopes_supported);
if let Some(scopes) = required_scopes {
builder = builder.reqquired_scopes(scopes)
}
if let Some(resource_name) = options.resource_name.as_ref() {
builder = builder.resource_name(resource_name)
}
if let Some(resource_documentation) = options.resource_documentation.as_ref() {
builder = builder.service_documentation(resource_documentation)
}
let (auth_server_meta, protected_resource_meta) = builder.build()?;
let Some(jwks_uri) = auth_server_meta.jwks_uri.as_ref().map(|s| s.to_string()) else {
return Err(McpSdkError::Internal {
description: "jwks_uri is not defined!".to_string(),
});
};
let userinfo_uri = join_url(&auth_server_meta.issuer, "oauth2/userinfo")
.map_err(|err| McpSdkError::Internal {
description: format!("invalid userinfo url :{err}"),
})?
.to_string();
let validate_audience = resolve_audience(
options.disable_audience_validation,
options.validate_audience.take(),
&options.mcp_server_url,
);
let token_verifier: Box<dyn OauthTokenVerifier> = match options.token_verifier {
Some(verifier) => verifier,
None => Box::new(GenericOauthTokenVerifier::new(TokenVerifierOptions {
strategies: vec![
VerificationStrategies::JWKs { jwks_uri },
VerificationStrategies::UserInfo { userinfo_uri },
],
validate_audience,
validate_issuer: Some(options.authkit_domain.clone()),
cache_capacity: None,
})?),
};
Ok(Self {
endpoint_map,
protected_resource_metadata_url,
token_verifier,
auth_server_meta,
protected_resource_meta,
})
}
fn handle_authorization_server_metadata(
response_str: String,
) -> McpHttpResult<http::Response<GenericBody>> {
let body = Full::new(Bytes::from(response_str))
.map_err(|err| McpHttpError::HttpError(err.to_string()))
.boxed();
http::Response::builder()
.status(StatusCode::OK)
.header(CONTENT_TYPE, "application/json")
.body(body)
.map_err(|err| McpHttpError::HttpError(err.to_string()))
}
fn handle_protected_resource_metadata(
response_str: String,
) -> McpHttpResult<http::Response<GenericBody>> {
use http_body_util::BodyExt;
let body = Full::new(Bytes::from(response_str))
.map_err(|err| McpHttpError::HttpError(err.to_string()))
.boxed();
http::Response::builder()
.status(StatusCode::OK)
.header(CONTENT_TYPE, "application/json")
.body(body)
.map_err(|err| McpHttpError::HttpError(err.to_string()))
}
}
#[async_trait]
impl AuthProvider for WorkOsAuthProvider {
fn auth_endpoints(&self) -> Option<&HashMap<String, OauthEndpoint>> {
Some(&self.endpoint_map)
}
async fn handle_request(
&self,
request: http::Request<&str>,
state: Arc<McpAppState>,
) -> Result<http::Response<GenericBody>, McpHttpError> {
let Some(endpoint) = self.endpoint_type(&request) else {
return http::Response::builder()
.status(StatusCode::NOT_FOUND)
.body(GenericBody::empty())
.map_err(|err| McpHttpError::HttpError(err.to_string()));
};
if let Some(response) = self.validate_allowed_methods(endpoint, request.method()) {
return Ok(response);
}
match endpoint {
OauthEndpoint::AuthorizationServerMetadata => {
let json_payload = serde_json::to_string(&self.auth_server_meta)
.map_err(|err| McpHttpError::HttpError(err.to_string()))?;
let cors = &CorsMiddleware::default();
cors.handle(
request,
state,
Box::new(move |_req, _state| {
Box::pin(
async move { Self::handle_authorization_server_metadata(json_payload) },
)
}),
)
.await
}
OauthEndpoint::ProtectedResourceMetadata => {
let json_payload = serde_json::to_string(&self.protected_resource_meta)
.map_err(|err| McpHttpError::HttpError(err.to_string()))?;
let cors = &CorsMiddleware::default();
cors.handle(
request,
state,
Box::new(move |_req, _state| {
Box::pin(
async move { Self::handle_protected_resource_metadata(json_payload) },
)
}),
)
.await
}
_ => Ok(GenericBody::create_404_response()),
}
}
async fn verify_token(&self, access_token: String) -> Result<AuthInfo, AuthenticationError> {
self.token_verifier.verify_token(access_token).await
}
fn protected_resource_metadata_url(&self) -> Option<&str> {
Some(self.protected_resource_metadata_url.as_str())
}
}