Authorization Server Example
Implement production-ready role-based access control using Lifecycle Hooks.
Overview
The Authorization server demonstrates:
- ✅ Role-based access control - Admin, Manager, User roles
- ✅ Custom attributes -
[RequireRole]and[AllowAnonymous] - ✅ Lifecycle hooks - Enforce authorization before tool execution
- ✅ JWT authentication - Industry-standard token validation
- ✅ Audit logging - Track authorization decisions
Complete Code
Program.cs
using Mcp.Gateway.Tools;
using AuthorizationMcpServer.Authorization;
var builder = WebApplication.CreateBuilder(args);
// Register services
builder.Services.AddHttpContextAccessor();
// Register MCP Gateway
builder.AddToolsService();
// Add authorization lifecycle hook
builder.AddToolLifecycleHook<AuthorizationHook>();
var app = builder.Build();
// stdio mode
if (args.Contains("--stdio"))
{
await ToolInvoker.RunStdioModeAsync(app.Services);
return;
}
// Authentication middleware
app.Use(async (context, next) =>
{
// Skip auth for health check
if (context.Request.Path == "/health")
{
await next();
return;
}
// Check Authorization header
if (!context.Request.Headers.TryGetValue("Authorization", out var authHeader))
{
context.Response.StatusCode = 401;
context.Response.ContentType = "application/json";
await context.Response.WriteAsJsonAsync(new
{
jsonrpc = "2.0",
error = new
{
code = -32000,
message = "Unauthorized",
data = new { detail = "Missing Authorization header" }
},
id = (object?)null
});
return;
}
// Extract and validate token
var token = authHeader.ToString().Replace("Bearer ", "");
var validation = ValidateToken(token);
if (!validation.IsValid)
{
context.Response.StatusCode = 403;
context.Response.ContentType = "application/json";
await context.Response.WriteAsJsonAsync(new
{
jsonrpc = "2.0",
error = new
{
code = -32002,
message = "Forbidden",
data = new { detail = validation.ErrorMessage }
},
id = (object?)null
});
return;
}
// Store user info for AuthorizationHook
context.Items["UserId"] = validation.UserId;
context.Items["UserRoles"] = validation.Roles;
await next();
});
app.UseWebSockets();
app.UseProtocolVersionValidation();
app.MapStreamableHttpEndpoint("/mcp");
app.Run();
// Simple token validation (use JWT in production!)
TokenValidation ValidateToken(string token)
{
// Demo tokens (use proper JWT validation in production)
return token switch
{
"admin-token" => new(true, "admin-user", new[] { "Admin" }),
"manager-token" => new(true, "manager-user", new[] { "Manager" }),
"user-token" => new(true, "regular-user", new[] { "User" }),
_ => new(false, null, null, "Invalid token")
};
}
record TokenValidation(
bool IsValid,
string? UserId,
string[]? Roles,
string? ErrorMessage = null);
AuthorizationHook.cs
using Mcp.Gateway.Tools;
using Mcp.Gateway.Tools.Lifecycle;
using System.Reflection;
namespace AuthorizationMcpServer.Authorization;
public class AuthorizationHook : IToolLifecycleHook
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly ILogger<AuthorizationHook> _logger;
private readonly Dictionary<string, MethodInfo> _toolMethods = new();
private bool _methodsCached = false;
public AuthorizationHook(
IHttpContextAccessor httpContextAccessor,
ILogger<AuthorizationHook> logger)
{
_httpContextAccessor = httpContextAccessor;
_logger = logger;
}
public Task OnToolInvokingAsync(string toolName, JsonRpcMessage request)
{
var httpContext = _httpContextAccessor.HttpContext;
if (httpContext == null) return Task.CompletedTask;
var method = GetToolMethod(toolName);
if (method == null) return Task.CompletedTask;
// Check [AllowAnonymous]
if (method.GetCustomAttribute<AllowAnonymousAttribute>() != null)
{
_logger.LogDebug("Tool '{ToolName}' allows anonymous access", toolName);
return Task.CompletedTask;
}
// Get required roles
var requiredRoles = method.GetCustomAttributes<RequireRoleAttribute>()
.Select(attr => attr.Role)
.ToList();
if (!requiredRoles.Any()) return Task.CompletedTask;
// Get user roles
var userId = httpContext.Items["UserId"] as string ?? "anonymous";
var userRoles = httpContext.Items["UserRoles"] as string[]
?? Array.Empty<string>();
// Check authorization (OR logic - any role matches)
var hasRequiredRole = requiredRoles.Any(role =>
userRoles.Contains(role, StringComparer.OrdinalIgnoreCase));
if (!hasRequiredRole)
{
_logger.LogWarning(
"User '{UserId}' denied access to '{ToolName}'. " +
"Required: [{Required}], Has: [{UserRoles}]",
userId, toolName,
string.Join(", ", requiredRoles),
string.Join(", ", userRoles));
throw new ToolInvalidParamsException(
$"Insufficient permissions. Required: {string.Join(" or ", requiredRoles)}",
toolName);
}
_logger.LogInformation(
"User '{UserId}' authorized to invoke '{ToolName}'",
userId, toolName);
return Task.CompletedTask;
}
public Task OnToolCompletedAsync(
string toolName,
JsonRpcMessage response,
TimeSpan duration)
{
return Task.CompletedTask;
}
public Task OnToolFailedAsync(
string toolName,
Exception error,
TimeSpan duration)
{
return Task.CompletedTask;
}
private MethodInfo? GetToolMethod(string toolName)
{
if (_methodsCached && _toolMethods.TryGetValue(toolName, out var cached))
return cached;
if (!_methodsCached)
{
ScanToolMethods();
_methodsCached = true;
}
return _toolMethods.TryGetValue(toolName, out var method) ? method : null;
}
private void ScanToolMethods()
{
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
foreach (var assembly in assemblies)
{
try
{
var types = assembly.GetTypes();
foreach (var type in types)
{
var methods = type.GetMethods(
BindingFlags.Public | BindingFlags.Instance);
foreach (var method in methods)
{
var toolAttr = method.GetCustomAttribute<McpToolAttribute>();
if (toolAttr != null)
{
_toolMethods[toolAttr.Name] = method;
}
}
}
}
catch (Exception ex)
{
_logger.LogWarning(ex,
"Failed to scan assembly {Assembly}", assembly.FullName);
}
}
}
}
Authorization Attributes
namespace AuthorizationMcpServer.Authorization;
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public class RequireRoleAttribute : Attribute
{
public string Role { get; }
public RequireRoleAttribute(string role)
{
Role = role;
}
}
[AttributeUsage(AttributeTargets.Method)]
public class AllowAnonymousAttribute : Attribute
{
}
AdminTools.cs
using Mcp.Gateway.Tools;
using AuthorizationMcpServer.Authorization;
namespace AuthorizationMcpServer.Tools;
public class AdminTools
{
[McpTool("delete_user", Description = "Delete a user (Admin only)")]
[RequireRole("Admin")]
public JsonRpcMessage DeleteUser(TypedJsonRpc<DeleteUserArgs> request)
{
var args = request.GetParams()!;
// Delete user logic...
return ToolResponse.Success(request.Id,
new { deleted = true, userId = args.UserId });
}
[McpTool("create_user", Description = "Create user (Admin or Manager)")]
[RequireRole("Admin")]
[RequireRole("Manager")] // Admin OR Manager
public JsonRpcMessage CreateUser(TypedJsonRpc<CreateUserArgs> request)
{
var args = request.GetParams()!;
// Create user logic...
return ToolResponse.Success(request.Id,
new { created = true, username = args.Username });
}
[McpTool("get_public_info", Description = "Get public information")]
[AllowAnonymous]
public JsonRpcMessage GetPublicInfo(JsonRpcMessage request)
{
return ToolResponse.Success(request.Id,
new { info = "Public data available to all" });
}
}
public record DeleteUserArgs(string UserId);
public record CreateUserArgs(string Username, string Email);
Testing
Test with curl
Admin-only tool (success):
curl -X POST http://localhost:5000/mcp \
-H "Content-Type: application/json" \
-H "MCP-Protocol-Version: 2025-11-25" \
-H "Authorization: Bearer admin-token" \
-d '{
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "delete_user",
"arguments": { "UserId": "user-123" }
},
"id": 1
}'
Admin-only tool (denied):
curl -X POST http://localhost:5000/mcp \
-H "Content-Type: application/json" \
-H "MCP-Protocol-Version: 2025-11-25" \
-H "Authorization: Bearer user-token" \
-d '{
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "delete_user",
"arguments": { "UserId": "user-123" }
},
"id": 2
}'
Response (denied):
{
"jsonrpc": "2.0",
"error": {
"code": -32603,
"message": "Internal error",
"data": {
"detail": "Insufficient permissions. Required: Admin"
}
},
"id": 2
}
Production Patterns
JWT Authentication
Replace simple token validation with proper JWT:
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
builder.Services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
options.Authority = "https://your-identity-server.com";
options.Audience = "mcp-gateway-api";
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
};
});
app.UseAuthentication();
app.UseAuthorization();
// Extract claims
app.Use(async (context, next) =>
{
if (context.User.Identity?.IsAuthenticated == true)
{
context.Items["UserId"] = context.User.FindFirst("sub")?.Value;
context.Items["UserRoles"] = context.User.FindAll(ClaimTypes.Role)
.Select(c => c.Value)
.ToArray();
}
await next();
});
Audit Logging
Track all authorization decisions:
public class AuditLoggingHook : IToolLifecycleHook
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IAuditLogger _auditLogger;
public async Task OnToolInvokingAsync(string toolName, JsonRpcMessage request)
{
var httpContext = _httpContextAccessor.HttpContext;
var userId = httpContext?.Items["UserId"] as string ?? "anonymous";
await _auditLogger.LogAsync(new AuditLog
{
Timestamp = DateTime.UtcNow,
UserId = userId,
ToolName = toolName,
Action = "Invoking",
IpAddress = httpContext?.Connection.RemoteIpAddress?.ToString()
});
}
// ... other methods
}
// Register
builder.AddToolLifecycleHook<AuthorizationHook>();
builder.AddToolLifecycleHook<AuditLoggingHook>();
Integration Tests
[Fact]
public async Task AdminTool_WithAdminToken_Succeeds()
{
// Arrange
using var server = new McpGatewayFixture();
var client = server.CreateClient();
var request = new HttpRequestMessage(HttpMethod.Post, "/mcp")
{
Content = JsonContent.Create(new
{
jsonrpc = "2.0",
method = "tools/call",
@params = new
{
name = "delete_user",
arguments = new { UserId = "user-123" }
},
id = 1
})
};
request.Headers.Authorization =
new AuthenticationHeaderValue("Bearer", "admin-token");
// Act
var response = await client.SendAsync(request);
// Assert
response.EnsureSuccessStatusCode();
}
[Fact]
public async Task AdminTool_WithUserToken_ReturnsError()
{
// Arrange
using var server = new McpGatewayFixture();
var client = server.CreateClient();
var request = new HttpRequestMessage(HttpMethod.Post, "/mcp")
{
Content = JsonContent.Create(new
{
jsonrpc = "2.0",
method = "tools/call",
@params = new
{
name = "delete_user",
arguments = new { UserId = "user-123" }
},
id = 1
})
};
request.Headers.Authorization =
new AuthenticationHeaderValue("Bearer", "user-token");
// Act
var response = await client.SendAsync(request);
var content = await response.Content.ReadAsStringAsync();
// Assert
Assert.Contains("Insufficient permissions", content);
}
Source Code
Full source code available at:
See Also
- Authorization Guide - Complete authorization patterns
- Lifecycle Hooks - Hook infrastructure
- Lifecycle Hooks API - Complete API docs