Skip to main content
SEMastery
DevOpsintermediate

Implementing API Gateway Authentication With YARP in .NET

Learn to build a secure API gateway in .NET using YARP. Add authentication, per-route authorization policies, and pass user identity to backend services.

11 min readUpdated January 24, 2026

One guard at the front gate

Think about a big housing society in your city. There are ten towers inside. Now imagine every single tower had its own guard who checked your ID card, wrote your name in a register, and asked where you were going. That is a lot of guards, and each one might check in a slightly different way. Some might forget to check at all.

A smarter society puts one main gate at the front. A single guard there checks your ID once. If you are allowed in, the guard waves you through and even tells the tower "this is Ravi from flat 304, he is fine." Now the towers can relax a little. The hard check already happened at the front gate.

An API gateway is exactly that front gate for your software. Instead of every backend service checking the caller again and again, you put one gateway in front. It checks who you are (authentication) and what you are allowed to do (authorization) in one place. In the .NET world, a wonderful tool for building this gateway is YARP.

YARP stands for Yet Another Reverse Proxy. It is a free, open-source library from Microsoft, built right on top of ASP.NET Core. Because it is just an ASP.NET Core app underneath, you can use the same authentication and authorization tools you already know. In this guide we will build a secure gateway step by step, in simple words.

What is a reverse proxy?

A proxy stands in the middle of a conversation. A reverse proxy sits in front of your servers and speaks on their behalf. The client talks to the proxy. The proxy decides which backend should answer, forwards the request, and sends the reply back.

Figure 1: Without a gateway, every service guards itself. With YARP, one gateway checks the caller first.

The client never talks to the Orders, Users, or Payments services directly. It only knows the gateway. This gives you one tidy place to add security, logging, rate limiting, and routing rules.

How a request flows through the gateway

Client
Authenticate
Authorize
Forward
Backend

Steps

1

Client

Sends request with a token

2

Authenticate

Gateway reads the token

3

Authorize

Checks the route policy

4

Forward

YARP proxies to the service

5

Backend

Service does the work

The gateway checks identity, then forwards.

Setting up YARP

First, create a normal ASP.NET Core empty web app. Then add the YARP package. It is free and open source under the MIT license, so you can ship it to production with no fees. This is different from some other popular .NET libraries such as MediatR and MassTransit, which have moved to commercial licensing. YARP has not.

dotnet new web -n ApiGateway
cd ApiGateway
dotnet add package Yarp.ReverseProxy

Now wire it up in Program.cs. The two key lines are AddReverseProxy() to register the services and MapReverseProxy() to handle the requests.

var builder = WebApplication.CreateBuilder(args);
 
// Read the proxy routes and clusters from configuration.
builder.Services
    .AddReverseProxy()
    .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
 
var app = builder.Build();
 
// This single line turns the app into a working reverse proxy.
app.MapReverseProxy();
 
app.Run();

That is a complete, working reverse proxy already. It has no security yet, but it can forward requests. The routing rules live in configuration, which we look at next.

Routes and clusters in plain words

YARP uses two simple ideas:

  • A route describes which incoming requests to match, for example "any path starting with /orders."
  • A cluster describes where to send them, for example "the Orders service running at https://localhost:7001."

A route points at a cluster. Here is a basic appsettings.json with two routes.

{
  "ReverseProxy": {
    "Routes": {
      "orders-route": {
        "ClusterId": "orders-cluster",
        "Match": { "Path": "/orders/{**catch-all}" }
      },
      "users-route": {
        "ClusterId": "users-cluster",
        "Match": { "Path": "/users/{**catch-all}" }
      }
    },
    "Clusters": {
      "orders-cluster": {
        "Destinations": {
          "d1": { "Address": "https://localhost:7001/" }
        }
      },
      "users-cluster": {
        "Destinations": {
          "d1": { "Address": "https://localhost:7002/" }
        }
      }
    }
  }
}

The pattern /orders/{**catch-all} means "match /orders and everything under it." So a call to GET /orders/42 would be matched by orders-route and forwarded to the Orders cluster. Note how we wrap route patterns like /orders/{**catch-all} in backticks when we mention them in normal text, because the curly braces would otherwise confuse the page.

Figure 2: A route matches the path, then sends traffic to its cluster, which holds one or more destinations.

A cluster can have more than one destination. When it does, YARP load-balances across them. That is a bonus you get for free, but our focus today is security.

Adding authentication

Authentication answers the question "who is this caller?" In a token-based world, the client sends a JWT bearer token in the Authorization header. The gateway reads that token, checks the signature, and builds a user identity from it.

Because YARP runs on ASP.NET Core, you add authentication the exact same way you would in any API. Register the JWT bearer handler, then turn on the middleware.

var builder = WebApplication.CreateBuilder(args);
 
builder.Services
    .AddReverseProxy()
    .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
 
// Tell the gateway how to read and trust JWT tokens.
builder.Services
    .AddAuthentication("Bearer")
    .AddJwtBearer("Bearer", options =>
    {
        options.Authority = "https://your-identity-server";
        options.Audience = "api-gateway";
    });
 
// Register named authorization policies the routes can use.
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("admin-only", policy =>
        policy.RequireRole("Admin"));
});
 
var app = builder.Build();
 
// Order matters: authenticate, then authorize, then proxy.
app.UseAuthentication();
app.UseAuthorization();
 
app.MapReverseProxy();
 
app.Run();

Two details matter a lot here:

  1. Order of middleware. UseAuthentication must come before UseAuthorization, and both must come before MapReverseProxy. The gateway needs to know the user before it decides whether to forward the request.
  2. The Authority and Audience. These tell the gateway which identity provider issued the token and who the token is meant for. If the token does not match, it is rejected.

Per-route authorization policies

Now the fun part. Authentication tells you who the caller is. Authorization decides whether that caller may use a given route. With YARP you attach an authorization policy to each route, right in the configuration. No code change needed to switch a route's rules.

You add an AuthorizationPolicy value to the route. It can be one of three kinds of value:

ValueMeaning
A policy nameUse a policy you registered with AddAuthorization, like admin-only.
"default"Require an authenticated user using the default policy.
"anonymous"Skip authorization checks. Good for public routes.

Here is the same config from before, now with policies added. The login route is public, the orders route needs a signed-in user, and the admin route needs the admin-only policy.

{
  "ReverseProxy": {
    "Routes": {
      "login-route": {
        "ClusterId": "auth-cluster",
        "AuthorizationPolicy": "anonymous",
        "Match": { "Path": "/auth/{**catch-all}" }
      },
      "orders-route": {
        "ClusterId": "orders-cluster",
        "AuthorizationPolicy": "default",
        "Match": { "Path": "/orders/{**catch-all}" }
      },
      "admin-route": {
        "ClusterId": "admin-cluster",
        "AuthorizationPolicy": "admin-only",
        "Match": { "Path": "/admin/{**catch-all}" }
      }
    },
    "Clusters": {
      "auth-cluster": {
        "Destinations": { "d1": { "Address": "https://localhost:7000/" } }
      },
      "orders-cluster": {
        "Destinations": { "d1": { "Address": "https://localhost:7001/" } }
      },
      "admin-cluster": {
        "Destinations": { "d1": { "Address": "https://localhost:7003/" } }
      }
    }
  }
}

A big strength here: you can change these policy names and reload them without restarting the proxy. YARP watches the config and applies updates live. That makes it easy to lock down a route quickly if something goes wrong.

Figure 3: The gateway decides per route. Public routes pass freely. Protected routes need a valid user, and admin routes need the right role.

Notice the two different rejections. A missing or invalid token returns 401 Unauthorized ("I do not know who you are"). A valid user without the right role returns 403 Forbidden ("I know who you are, but you cannot do this").

Passing the user identity to the backend

Here is a question students often ask. The gateway checked the user. Does the backend service have to check again? And how does the backend even know who the user is?

The good news: YARP forwards the original credentials by default. Cookies, bearer tokens, and API keys flow through to the destination service inside the normal request headers. So the Orders service receives the same Authorization: Bearer ... header, reads the same token, and learns the same user identity.

Identity flows to the backend

Client
Gateway
Backend

Steps

1

Client

Sends Bearer token

2

Gateway

Verifies, then forwards token

3

Backend

Reads same token, verifies again

The token is forwarded so the backend can verify it too.

Should the backend trust the gateway blindly and skip its own check? No. A safer rule is defense in depth: the gateway rejects obvious bad traffic early, and each service still verifies the caller itself. That way, if a service is ever reachable by another path, it is still protected.

Sometimes you want the gateway to swap the public token for an internal one before forwarding. For example, the outside world uses one token, but your internal network uses a different header. You can do that with a request transform.

builder.Services
    .AddReverseProxy()
    .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"))
    .AddTransforms(context =>
    {
        // Add an internal header before the request leaves the gateway.
        context.AddRequestTransform(transformContext =>
        {
            var user = transformContext.HttpContext.User;
            var name = user.Identity?.Name ?? "anonymous";
            transformContext.ProxyRequest.Headers.Add("X-Forwarded-User", name);
            return ValueTask.CompletedTask;
        });
    });

This adds an X-Forwarded-User header carrying the signed-in user's name. The backend can read it for logging or lightweight checks. Just remember: a forwarded header is only as trustworthy as the network. Keep your gateway-to-service traffic private, and still verify real tokens for anything important.

Gateway versus per-service checks

It helps to compare the two places you could enforce security. Both have a role.

WhereWhat it does wellWatch out for
At the gatewayOne place for rules, blocks bad traffic early, less load on servicesDo not treat it as the only wall
In each serviceDefense in depth, protects services reachable by other pathsEasy to forget or do inconsistently

The best setups use both. The gateway is your front gate that turns away the obvious troublemakers. Each service is the locked door behind it. Together they give you a calm, layered defense.

A quick mental model of the whole flow

Let us walk one real request end to end. A client wants to read order 42, so it calls GET /orders/42 with a bearer token.

  1. The request hits the YARP gateway.
  2. UseAuthentication reads the token and builds the user.
  3. The route orders-route has policy default, so UseAuthorization checks that the user is signed in.
  4. If the user is valid, YARP forwards the request to the Orders cluster, token included.
  5. The Orders service verifies the token again and returns the order.
  6. YARP sends the response back to the client.

If the token was missing, step 3 would stop everything with a 401, and the Orders service would never be bothered. That early rejection is the whole point of the gateway.

Common mistakes to avoid

  • Wrong middleware order. If MapReverseProxy runs before UseAuthorization, your policies are skipped. Authenticate and authorize first.
  • Forgetting public routes. A health check or login endpoint behind a default policy will reject everyone, including your monitoring tools. Mark those routes anonymous.
  • Trusting forwarded headers as proof of identity. Headers like X-Forwarded-User are convenient but can be faked if your network is open. Verify real tokens for sensitive actions.
  • Skipping service-side checks. The gateway is a helper, not your only guard. Keep verification in the services too.

Quick recap

  • An API gateway is one front gate for many services. It checks the caller once, so each service does not have to repeat the hard work.
  • YARP is Microsoft's free, open-source reverse proxy for .NET. It is built on ASP.NET Core, so you reuse the authentication and authorization you already know.
  • Add the Yarp.ReverseProxy package, register it with AddReverseProxy(), and serve it with MapReverseProxy().
  • Routes match incoming paths; clusters point at backend destinations.
  • Turn on UseAuthentication and UseAuthorization before MapReverseProxy. Order matters.
  • Set a route's AuthorizationPolicy to a policy name, "default" (require a signed-in user), or "anonymous" (public). You can change these and reload without restarting.
  • YARP forwards the original token to the backend by default, so services can verify the user too. Use defense in depth: check at the gateway and in each service.

References and further reading

Related Posts