Implement a secure MCP server using OAuth DPoP and Duende identity provider

This post demonstrates how an ASP.NET Core application can connect to a secure MCP server using OpenID Connect and OAuth. Both applications use Duende IdentityServer as the identity provider. The MCP server requires delegated DPoP access tokens.

Code: https://github.com/damienbod/McpOidcOAuth

Setup

The UI application authenticates with the Duende IdentityServer using OpenID Connect. Upon successful authentication, a DPoP access token is issued and used to access the Model Context Protocol (MCP) server.

DPoP (Demonstration of Proof-of-Possession) enables token binding without requiring mutual TLS (MTLS), enhancing API security by making token theft significantly more difficult. To use the token, the client must possess the private key associated with it.

MCP server using OAuth DPoP

The MCP server requires access tokens issued by the Duende IdentityServer. These JWT tokens are validated using standard mechanisms, along with additional DPoP validation. DPoP validation is implemented via the Duende.AspNetCore.Authentication.JwtBearer NuGet package. All MCP server endpoints require a valid DPoP access token for access. Required packages:

  • ModelContextProtocol.AspNetCore
  • Microsoft.AspNetCore.Authentication.JwtBearer
  • Duende.AspNetCore.Authentication.JwtBearer

var httpMcpServerUrl = builder.Configuration["HttpMcpServerUrl"];
var identityProvider = builder.Configuration["IdentityProvider"];

builder.Services.AddAuthentication("Bearer")
    .AddJwtBearer(options =>
    {
        options.Authority = identityProvider;
        options.Audience = $"{identityProvider}/resources";
        
        options.TokenValidationParameters.ValidateAudience = true;
        options.TokenValidationParameters.ValidateIssuer = true;

        options.MapInboundClaims = false;
        options.TokenValidationParameters.ValidTypes = ["at+jwt"];
    })
    .AddMcp(options =>
    {
        options.ResourceMetadata = new()
        {
            Resource = new Uri($"{httpMcpServerUrl}/mcp"), 
            ResourceName = "MCP demo server",
            AuthorizationServers = [ new Uri(identityProvider!) ], 
            DpopBoundAccessTokensRequired = true,
            ResourceDocumentation = new Uri($"{httpMcpServerUrl}/health"),
            ScopesSupported = ["mcp:tools"], 
        };
    });

// layers DPoP onto the "token" scheme above
builder.Services.ConfigureDPoPTokensForScheme("Bearer", opt =>
{
    opt.ValidationMode = ExpirationValidationMode.IssuedAt; // IssuedAt is the default.
});

builder.Services.AddAuthorization();

builder.Services
    .AddMcpServer()
    .WithHttpTransport()
    .WithPrompts<PromptExamples>()
    .WithResources<DocumentationResource>()
    .WithTools<RandomNumberTools>()
    .WithTools<DateTools>();

builder.Services.AddHttpClient();

// change to scp or scope if not using magic namespaces from MS
// The scope must be validate as we want to force only delegated access tokens
// The scope is requires to only allow access tokens intended for this API
builder.Services.AddAuthorizationBuilder()
  .AddPolicy("mcp_tools", policy =>
        policy.RequireClaim("scope", "mcp:tools"));

// Add services to the container.

Authorization is also implemented to require the “mcp:tools” scope.

app.UseAuthentication();
app.UseAuthorization();

app.MapMcp("/mcp").RequireAuthorization("mcp_tools");

MCP Client

The MCP client is implemented as part of an ASP.NET Core web application. This application requires authenticated identities for both the user and the application itself. Authentication is performed using the OpenID Connect code flow with PKCE, and the application requests a DPoP-bound access token. Duende IdentityServer is used as the identity provider.

  • Duende.AccessTokenManagement.OpenIdConnect
  • Microsoft.AspNetCore.Authentication.OpenIdConnect
  • Microsoft.SemanticKernel
  • ModelContextProtocol
  • ModelContextProtocol.AspNetCore

The AddOpenIdConnect method is used to configure the OpenID Connect client in the ASP.NET Core application. Token access management is handled via the AddOpenIdConnectAccessTokenManagement method, provided by the Duende client NuGet packages. The AddUserAccessTokenHttpClient method registers an HTTP client that automatically includes the DPoP access token and DPoP proof in all outgoing HTTP requests.

        builder.Services.AddAuthentication(options =>
        {
            options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
            options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
        })
       .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
       {
           options.ExpireTimeSpan = TimeSpan.FromHours(8);
           options.SlidingExpiration = false;
           options.Events.OnSigningOut = async e =>
           {
               await e.HttpContext.RevokeRefreshTokenAsync();
           };
       })
       .AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
       {
           options.Authority = "https://localhost:5101";
           options.ClientId = "McpWebClient";
           options.ClientSecret = "ddedF4f289k$3eDa23ed0iTk4Raq&tttk23d08nhzd";
           options.ResponseType = "code";
           options.ResponseMode = "query";
           options.UsePkce = true;

           options.Scope.Clear();
           options.Scope.Add("openid");
           options.Scope.Add("profile");
           options.Scope.Add("mcp:tools");
           options.Scope.Add("offline_access");
           options.GetClaimsFromUserInfoEndpoint = true;
           options.SaveTokens = true;

           options.TokenValidationParameters = new TokenValidationParameters
           {
               NameClaimType = "name",
               RoleClaimType = "role"
           };
       });

        var privatePem = File.ReadAllText(Path.Combine(builder.Environment.ContentRootPath, "ecdsa384-private.pem"));
        var publicPem = File.ReadAllText(Path.Combine(builder.Environment.ContentRootPath, "ecdsa384-public.pem"));
        var ecdsaCertificate = X509Certificate2.CreateFromPem(publicPem, privatePem);
        var ecdsaCertificateKey = new ECDsaSecurityKey(ecdsaCertificate.GetECDsaPrivateKey());

        // add automatic token management
        builder.Services.AddOpenIdConnectAccessTokenManagement(options =>
        {
            var jwk = JsonWebKeyConverter.ConvertFromSecurityKey(ecdsaCertificateKey);
            jwk.Alg = "ES384";
            options.DPoPJsonWebKey = DPoPProofKey.ParseOrDefault(JsonSerializer.Serialize(jwk));
        });

        builder.Services.AddHttpClient();
        builder.Services.AddUserAccessTokenHttpClient("dpop-api-client", configureClient: client =>
        {
            client.BaseAddress = new Uri("https://localhost:5103");
        });

        builder.Services.AddAuthorization(options =>
        {
            options.FallbackPolicy = options.DefaultPolicy;
        });

The CreateMcpTransport method uses the HttpClient which uses the DPoP access token.

 private IClientTransport CreateMcpTransport(IHttpClientFactory clientFactory)
    {
        var httpClient = clientFactory.CreateClient("dpop-api-client");
        var httpMcpServerUrl = _configuration["HttpMcpServerUrl"] ?? throw new ArgumentNullException("Configuration missing for HttpMcpServerUrl");
        return new SseClientTransport(new() { Endpoint = new Uri(httpMcpServerUrl), Name = "Secure Client" }, httpClient);
    }

This is used in the EnsureSetupAsync method which sets up the MCP client.

  public async Task EnsureSetupAsync(IHttpClientFactory clientFactory)
    {
        if (_initialized) return;

        _mcpClient = await McpClientFactory.CreateAsync(CreateMcpTransport(clientFactory), GetMcpOptions());
        await _kernel.ImportMcpClientToolsAsync(_mcpClient);

        _promptingService = new PromptingService(_kernel, autoInvoke: _mode == ApprovalMode.Elicitation);
        _initialized = true;
    }

Duende IdentityServer client configuration

For this to work, Duende IdentityServer requires a Web application configuration and a scope for the API.

public static IEnumerable<ApiScope> ApiScopes =>
[
	new ApiScope("mcp:tools")
];

public static IEnumerable<Client> Clients =>
[
	new Client
	{
		ClientId = "McpWebClient",
		// In a real app, use a key vault
		ClientSecrets = { new Secret("ddedF4f289k$3eDa23ed0iTk4Raq&tttk23d08nhzd".Sha256()) },

		AllowedGrantTypes = GrantTypes.CodeAndClientCredentials,

		RedirectUris = { "https://localhost:5102/signin-oidc" },
		FrontChannelLogoutUri = "https://localhost:5102/signout-oidc",
		PostLogoutRedirectUris = { "https://localhost:5102/signout-callback-oidc" },

		RequireDPoP = true,
		RequirePushedAuthorization = true,

		AllowOfflineAccess = true,
		AllowedScopes = { "openid", "profile", "offline_access", "mcp:tools" }
	}
];

Notes

Using OAuth DPoP access tokens for the MCP server adds an extra layer of security through token binding. In this enterprise setup, no additional security protocols are required to secure the MCP server.
The MCP server functions like any other API, where the client acts on behalf of the authenticated identity, both the application and the user via OpenID Connect authentication using Duende IdentityServer.

Links

https://mcp.azure.com/

https://github.com/microsoft/azure-devops-mcp

https://auth0.com/blog/an-introduction-to-mcp-and-authorization/

https://learning.postman.com/docs/postman-ai-agent-builder/mcp-server-flows/mcp-server-flows/

https://stytch.com/blog/MCP-authentication-and-authorization-guide/

.NET MCP server

https://learn.microsoft.com/en-us/dotnet/ai/quickstarts/build-mcp-server

Standards, draft Standards

OAuth 2.0 Dynamic Client Registration Protocol

OAuth 2.0 Authorization Server Metadata

https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization

https://modelcontextprotocol.io/specification/2025-06-18/basic/security_best_practices

https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1299

https://den.dev/blog/mcp-authorization-resource/

SPIFFE

https://spiffe.io/docs/latest/spiffe-about/overview/

Azure OpenAI

https://learn.microsoft.com/en-us/azure/ai-foundry/

One comment

  1. […] Implement a secure MCP server using OAuth DPoP and Duende identity provider (Damien Bowden) […]

Leave a reply to Dew Drop – November 3, 2025 (#4532) – Morning Dew by Alvin Ashcraft Cancel reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.