---
title: "Require OAuth sign-in on a server"
url: https://recursiveai.org/onemodelcontext/cookbook/require-sign-in-on-a-server/
description: "Protect tool calls with OAuth access tokens. This local example uses OneModelContext’s embedded authorization server and a simulated signed-in user."
version: "1.0.1"
---

# Require OAuth sign-in on a server

**OneModelContext** is a free, open-source .NET SDK for the Model Context Protocol® from Recursive AI. MIT or Apache-2.0, no native dependencies.

[NuGet](https://www.nuget.org/packages/RecursiveAI.OneModelContext) · [GitHub](https://github.com/recursive-ai-oss/onemodelcontext-csharp) · [Docs](https://recursiveai.org/onemodelcontext/docs/)

Require OAuth access tokens for Model Context Protocol® (MCP) tool calls.

```shell
dotnet add package RecursiveAI.OneModelContext
```

Requires [.NET 10](https://dotnet.microsoft.com/download/dotnet/10.0) (net10.0); .NET 9 and earlier aren’t supported.

```csharp
using RecursiveAI.OneModelContext;
using RecursiveAI.OneModelContext.Auth;
using RecursiveAI.OneModelContext.Hosting;
using RecursiveAI.OneModelContext.Jose;

var url = new Uri("https://example.com/tools");
var validator = new JwtTokenValidator(
    "https://example.com",
    new HttpJwksProvider(
        new Uri("https://example.com/jwks")));
var protection = new ResourceServer(
    new ResourceServerOptions(url, validator));
using var endpoint = new StreamableHttpEndpoint(
    new ContextServer("protected", "1.0.0"),
    new StreamableHttpOptions
    { ResourceServer = protection });
Console.WriteLine("Protected endpoint configured.");
```

**[Download this example (.zip)](https://recursiveai.org/onemodelcontext/cookbook/require-sign-in-on-a-server/require-sign-in-on-a-server.zip)**: unzip, then `dotnet run`.

The download prints `Anonymous: 401`, then `Authorized: 42`.

The snippet at the top attaches bearer-token validation to an endpoint. It only configures objects: it does not bind a listener or fetch signing keys from the placeholder URLs. The full program below instead runs an embedded local authorization server, refuses an anonymous connection and accepts an authorized tool call.

Running the snippet prints:

```text
Protected endpoint configured.
```

## The full program

The download starts an authorization server and a protected resource endpoint on one local listener. It tries an anonymous connection, then signs in a pre-registered public client with authorization code and PKCE and calls the tool.

```csharp
using System.Net;
using System.Security.Cryptography;
using RecursiveAI.OneModelContext;
using RecursiveAI.OneModelContext.Auth;
using RecursiveAI.OneModelContext.Auth.Server;
using RecursiveAI.OneModelContext.Hosting;
using RecursiveAI.OneModelContext.Http;
using RecursiveAI.OneModelContext.Jose;
using RecursiveAI.OneModelContext.OAuth;

using var key = ECDsa.Create(ECCurve.NamedCurves.nistP256);
AuthorizationServer? authorization = null;
ResourceServer? resource = null;
StreamableHttpEndpoint? endpoint = null;
await using var listener = new HttpServer(async exchange =>
{
    if (authorization!.IsServerPath(exchange.Request.Path))
        await authorization.HandleAsync(exchange);
    else if (exchange.Request.Path == "/tools" || resource!.IsMetadataPath(exchange.Request.Path))
        await endpoint!.HandleAsync(exchange);
    else
        exchange.Response.StatusCode = 404;
});
listener.Start();
var issuer = new Uri(listener.BaseAddress!.GetLeftPart(UriPartial.Authority));
var url = new Uri(issuer, "/tools");
var redirect = new Uri($"http://{IPAddress.Loopback}:9/callback");
using var auth = new AuthorizationServer(new AuthorizationServerOptions
{
    Issuer = issuer,
    Resources = [url.AbsoluteUri],
    SigningKeys = [JsonWebSigningKey.FromECDsa(key, "demo")],
    ScopesSupported = ["calculate"],
    Store = new MemoryAuthorizationStore(),
    // Local demonstration only: stand in for a signed-in user and consent.
    AuthorizeUser = (request, _) => ValueTask.FromResult(
        AuthorizationDecision.Approve("alice", request.RequestedScopes)),
});
authorization = auth;
await auth.Store.SaveClientAsync(new RegisteredClient
{
    ClientId = "demo-app",
    RedirectUris = [redirect.AbsoluteUri],
    Scopes = ["calculate"],
    CreatedAt = DateTimeOffset.UtcNow,
}, CancellationToken.None);
resource = new ResourceServer(new ResourceServerOptions(url, auth.CreateTokenValidator())
{
    AuthorizationServers = [issuer.GetLeftPart(UriPartial.Authority)],
    ScopesSupported = ["calculate"],
    RequiredScopes = ["calculate"],
});
var server = new ContextServer("protected-calculator", "1.0.0");
new Calculator().AddTo(server);
using var handler = new StreamableHttpEndpoint(server,
    new StreamableHttpOptions { ResourceServer = resource });
endpoint = handler;

try
{
    await using var anonymous = await ModelContext.ConnectAsync(ContextTransport.Http(url));
}
catch (AuthorizationException ex)
{
    Console.WriteLine($"Anonymous: {ex.StatusCode}");
}
using var oauth = new OAuthClient(new OAuthClientOptions(url)
{
    UserAgent = new DemoBrowser(redirect),
    PreRegistered = new PreRegisteredClient("demo-app", Issuer: issuer.GetLeftPart(UriPartial.Authority)),
});
using var http = oauth.CreateHttpClient();
await using (var client = await ModelContext.ConnectAsync(ContextTransport.Http(url, http)))
    Console.WriteLine("Authorized: " + (await client.CallToolAsync("add", new { a = 2, b = 40 })).Text);

// Simulates the browser redirect without opening a window or printing tokens.
sealed class DemoBrowser(Uri redirect) : IAuthorizationUserAgent
{
    public Uri RedirectUri => redirect;
    public async ValueTask<Uri> AuthorizeAsync(Uri url, CancellationToken cancellationToken)
    {
        using var browser = new HttpClient(new SocketsHttpHandler { AllowAutoRedirect = false });
        using var response = await browser.GetAsync(url, cancellationToken);
        if (response.StatusCode != HttpStatusCode.Found)
            throw new InvalidOperationException("Authorization did not redirect.");
        return response.Headers.Location!;
    }
}

public partial class Calculator
{
    /// <summary>Add two integers.</summary>
    [Tool]
    public int Add(int a, int b) => a + b;
}
```

`dotnet run` prints:

```text
Anonymous: 401
Authorized: 42
```

The demo approves “alice” automatically; it is not a login system. As the [authorization-server docs](https://recursiveai.org/onemodelcontext/docs/authorization-server/) say, “you keep the users.”

## Variations

Change the arguments. Replace the tool call inside the full program with:

```csharp
Console.WriteLine((await client.CallToolAsync(
    "add", new { a = 1, b = 41 })).Text);
```

```text
42
```

## Related recipes

- [Build a Model Context Protocol® server in C#](https://recursiveai.org/onemodelcontext/cookbook/build-a-server-in-csharp/)
- [Connect to a Model Context Protocol® server in C#](https://recursiveai.org/onemodelcontext/cookbook/connect-to-a-server/)
- [Host a Model Context Protocol® server on ASP.NET Core](https://recursiveai.org/onemodelcontext/cookbook/host-on-aspnet-core/)
- [Serve the Model Context Protocol® over HTTP without ASP.NET](https://recursiveai.org/onemodelcontext/cookbook/serve-over-http-without-aspnet/)
- [Sign in to a protected Model Context Protocol® server in .NET](https://recursiveai.org/onemodelcontext/cookbook/sign-in-to-a-protected-server/)

---

## The short answer

To protect an MCP server with OAuth in C# (Model Context Protocol®), place a `ResourceServer` on the HTTP endpoint. Configure its resource URL, authorization server and token validator. OneModelContext also includes an `AuthorizationServer` when your application should issue its own tokens rather than use an external identity provider.

The downloadable example runs both pieces locally. It refuses an anonymous connection, then accepts a client that completed an authorization-code exchange. It does not ask you to create an account or provide a real signing key: a temporary signing key is generated for the demonstration.

## Can protocol servers use OAuth for authentication and authorization?

Yes. The resource server checks bearer access tokens before tool dispatch and supplies protected-resource metadata and challenges. The authorization server handles the OAuth endpoints. Your application remains responsible for its users, their sign-in and their consent.

Those responsibilities are intentionally separate. A token validator determines whether an access token is acceptable for this resource. An authorization callback determines whether the user should be allowed to grant the requested access. Putting an automatic approval callback into production would remove the user check, even if every cryptographic step still worked.

The sample’s `AuthorizeUser` callback approves “alice” only to keep the exchange local and reproducible. Replace it with the host’s real authenticated user and consent decision. The [authorization-server documentation](https://recursiveai.org/onemodelcontext/docs/authorization-server/) describes `Approve`, `Deny`, `Interact` and `Redirect`, including returning to the authorization request after sign-in.

## How do I add authentication to a server?

Start with the resource URI. Configure it, rather than deriving it from the incoming Host header. The example uses the locally bound endpoint URL. `CreateTokenValidator` connects the embedded authorization server to the resource server’s validation path, and the endpoint receives that resource server through `StreamableHttpOptions`.

Set the scopes your authorization server supports and the scopes the resource requires. Here both sides use `calculate`. The registered public client is also allowed that scope. The [server docs](https://recursiveai.org/onemodelcontext/docs/authorization-server/) warn that without a scope allow-list an interactive request can be granted any well-formed scope string. Scope configuration is therefore part of authorization, not merely documentation for a consent screen.

The listener sends authorization paths to `AuthorizationServer.HandleAsync` and sends the tool path and protected-resource metadata paths to `StreamableHttpEndpoint`. Omitting metadata routing can leave a token-protected tool endpoint that clients cannot discover how to sign in to.

## What happens when a client is not authorized?

The example’s anonymous connection receives 401. The [resource-server guide](https://recursiveai.org/onemodelcontext/docs/resource-server/) distinguishes missing or invalid tokens from insufficient scope: the latter receives 403 with an `insufficient_scope` challenge and the required scope. An unavailable issuer or introspection endpoint can instead produce 503. These are HTTP authorization outcomes, not calculator errors.

After the challenge, the OAuth client discovers metadata and performs authorization code with PKCE S256. The local test browser returns the authorization redirect without opening a window; the library then exchanges the code and sends the access token on the retried request. No token is printed.

For an actual service, persist authorization state with a documented store and keep signing keys stable across restarts. The in-memory store and newly generated key in this download intentionally last only for the process. They demonstrate the protocol, not durable sessions. The [store guide](https://recursiveai.org/onemodelcontext/docs/stores/) describes the available persistence choices and their responsibilities.
