DEV Community

Dor Danai
Dor Danai

Posted on

Building an OAuth2 Protected API with C#, IdentityServer, and ASP.NET Core

Intro

This demo demonstrates protecting an ASP.NET Core API with OAuth2 and JWT tokens. We use Duende IdentityServer as the authorization server. The API validates tokens and allows only authenticated requests.

Why this matters: OAuth2 is the standard for securing APIs. Understanding token-based authentication helps you build production-ready applications. This setup shows the complete flow from token request to protected endpoint access.

Project / Setup Overview

The solution contains two projects:

  1. IdentityServer – Issues OAuth2 tokens
  2. ApiDemo – Protected API that validates JWT tokens

Folder Structure:

CSharp-API-OAuth2-Demo/
├── IdentityServer/
│   ├── Config.cs          # Client, scope, and resource definitions
│   ├── Users.cs           # Test user credentials
│   ├── Program.cs         # IdentityServer setup
│   └── wwwroot/
│       └── test-token.html # Token request form
└── ApiDemo/
    ├── Controllers/
    │   └── UsersController.cs  # Protected endpoint
    ├── Configuration/
    │   └── IdentityServerOptions.cs  # Strongly-typed config
    └── Program.cs         # API with JWT validation
Enter fullscreen mode Exit fullscreen mode

Endpoints:

  • POST /connect/token (IdentityServer) – Request access token
  • GET /Users (ApiDemo) – Protected endpoint requiring a valid token

Visual Studio solution showing IdentityServer (authorization server) and ApiDemo (protected API) projects side by side

How It Works

IdentityServer Configuration

IdentityServer defines clients, scopes, and resources. Clients represent applications that request tokens. Scopes define what the token allows. Resources are the APIs being protected.

public static IEnumerable<Client> Clients =>
    new List<Client>
    {
        new Client
        {
            ClientId = "demo-client",
            AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
            ClientSecrets = { new Secret("demo-secret".Sha256()) },
            AllowedScopes = { "api.read" }
        }
    };
Enter fullscreen mode Exit fullscreen mode

Note: Resource Owner Password grant is simple for demos. It allows direct username/password exchange for tokens. Production systems should use Authorization Code with PKCE for better security.

The API resource defines the audience that tokens are issued for:

new ApiResource("api-demo", "API Demo")
{
    Scopes = { "api.read" }
}
Enter fullscreen mode Exit fullscreen mode

API Token Validation

The API validates incoming JWT tokens using the JwtBearer authentication scheme. It checks the token signature against IdentityServer's public key and validates the audience to ensure the token was issued for this API.

builder.Services.AddAuthentication("Bearer")
    .AddJwtBearer("Bearer", options =>
    {
        options.Authority = authority; // IdentityServer URL
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateAudience = true,
            ValidAudience = "api-demo"
        };
    });
Enter fullscreen mode Exit fullscreen mode

Configuration validation: The Authority URL comes from appsettings.json. Validation at startup ensures required settings are present:

builder.Services.AddOptions<IdentityServerOptions>()
    .Bind(builder.Configuration.GetSection("IdentityServer"))
    .ValidateDataAnnotations()
    .Validate(options => !string.IsNullOrWhiteSpace(options.Authority), 
        "IdentityServer:Authority configuration value is required.")
    .ValidateOnStart();
Enter fullscreen mode Exit fullscreen mode

Protected Endpoint

Controllers use the [Authorize] attribute to require authentication:

[HttpGet]
[Authorize]
public IActionResult Get() => Ok(new List<object> 
{ 
    new { Name = "Demo User", Role = "Admin" } 
});
Enter fullscreen mode Exit fullscreen mode
  • Without a valid token: 401 Unauthorized
  • With a valid token: returns user data

Token request form showing username, password, client credentials, and scope fields

Demo / Usage

Step 1: Start Both Projects

Run IdentityServer on https://localhost:5001 and ApiDemo on https://localhost:5002. Both projects enforce HTTPS redirection.

Step 2: Request a Token

Navigate to https://localhost:5001/test-token.html. The form is pre-filled with demo credentials:

  • Username: demo
  • Password: password
  • Client ID: demo-client
  • Client Secret: demo-secret
  • Scope: api.read

Click Get Token. The response contains an access_token field. Copy this token.

Token response showing JSON with  raw `access_token` endraw  field

Step 3: Call the Protected API

Use the token in the Authorization header. Format: Bearer <your-token>

Using Swagger:

  1. Open https://localhost:5002/swagger
  2. Click Authorize
  3. Enter Bearer <your-token>
  4. Click Authorize
  5. Call the GET /Users endpoint

Using curl:

curl -H "Authorization: Bearer <your-token>" \
     https://localhost:5002/Users
Enter fullscreen mode Exit fullscreen mode

Expected response:

[
  {
    "name": "Demo User",
    "role": "Admin"
  }
]
Enter fullscreen mode Exit fullscreen mode

Without token:

curl https://localhost:5002/Users
# Returns 401 Unauthorized
Enter fullscreen mode Exit fullscreen mode

Swagger UI showing authorized request with successful response

Error handling: The HTML form shows clear notifications for errors, including network failures and invalid credentials. Clipboard copy operations also handle browser restrictions.

Next Steps / Extensions

  1. Replace Resource Owner Password grant – Use Authorization Code with PKCE for web apps, or Client Credentials for service-to-service.
  2. Store users in a database – Replace test users with ASP.NET Core Identity.
  3. Store clients dynamically – Use IClientStore for database-driven client management.
  4. Add refresh tokens – Implement token refresh flow for longer sessions.
  5. Add role-based authorization – Enforce roles using claims: [Authorize(Roles = "Admin")].
  6. Environment-specific configuration – Use appsettings.Production.json and secure secrets in Azure Key Vault or similar.
  7. Add CORS – Configure for APIs used by web apps from different origins.

Learning resources:

TL;DR

IdentityServer issues JWT tokens. The API validates them to protect endpoints using [Authorize]. Configuration validation ensures required settings are present. Extend this with database-backed users, different grant types, and production-ready security practices.


GitHub Repository: [https://github.com/karnafun/identityserver-oauth2-demo]

Looking for help building secure APIs? Hire me

Backend engineer & API integrator. Building secure, scalable APIs with .NET.

Top comments (0)