Asp.Net Core Security OAuth ( Open Authorization ) Created: 24 Jan 2026 Updated: 24 Jan 2026

Implementing OAuth 2.0 Client Credentials Flow with Scope-Based Authorization in ASP.NET Core

Introduction

In modern distributed systems, securing machine-to-machine (M2M) communication is crucial. OAuth 2.0's Client Credentials grant type is the industry-standard protocol for authenticating services without user interaction. This article demonstrates how to implement OAuth 2.0 Client Credentials flow with scope-based authorization in ASP.NET Core 10, providing fine-grained access control for API resources.

What You'll Learn

  1. Implementing OAuth 2.0 Client Credentials flow
  2. Generating and validating JWT tokens with symmetric keys
  3. Implementing scope-based authorization
  4. Two different authorization approaches (Custom Handler vs. Policy-Based)
  5. Best practices for production-ready security

Prerequisites

  1. .NET 10 SDK
  2. Understanding of JWT tokens
  3. Basic knowledge of OAuth 2.0
  4. Familiarity with ASP.NET Core

Understanding OAuth 2.0 Client Credentials Flow

The Client Credentials flow is used when applications request access tokens to access their own resources, not on behalf of a user.

Flow Diagram

┌──────────┐ ┌────────────────────┐
│ Client │ │ Authorization │
│ App 1 │ │ Server │
└──────────┘ └────────────────────┘
│ │
│ 1. POST /oauth/token │
│ (client_id, client_secret) │
│ ───────────────────────────────────────────> │
│ │
│ │ 2. Validate
│ │ Credentials
│ │
│ 3. JWT Token (scope: products.read) │
│ <─────────────────────────────────────────── │
│ │
│ │
│ ┌──────────────────────┴─────┐
│ │ Resource Server │
│ │ (Protected APIs) │
│ └────────────────────────────┘
│ │
│ 4. GET /api/products │
│ Authorization: Bearer {token} │
│ ───────────────────────────────────────────> │
│ │
│ │ 5. Validate Token
│ │ Check Scope
│ │
│ 6. 200 OK (Products List) │
│ <─────────────────────────────────────────── │
│ │

Key Concepts

  1. Client Credentials: Application identifies itself using client_id and client_secret
  2. Scopes: Define what resources the client can access
  3. JWT Token: Contains client identity and authorized scopes
  4. Scope-Based Authorization: Resources validate required scopes before granting access

Project Architecture

Project Structure

SecurityApp.API/
├── Authorization/
│ └── ScopeAuthorizationHandler.cs # Custom authorization handler (example)
├── Models/
│ ├── OAuthClient.cs # Client configuration with scopes
│ ├── TokenRequest.cs # OAuth token request DTO
│ └── TokenResponse.cs # OAuth token response DTO
├── Options/
│ ├── JwtOptions.cs # JWT configuration
│ └── OAuthOptions.cs # OAuth clients configuration
├── Services/
│ └── TokenService.cs # Token generation & validation
├── Program.cs # Application startup & endpoints
├── appsettings.json # Configuration
└── SecurityApp.API.http # HTTP test file

Technology Stack

  1. Framework: ASP.NET Core 10 (Minimal APIs)
  2. Authentication: JWT Bearer Authentication
  3. Authorization: Policy-Based Authorization
  4. Token Generation: System.IdentityModel.Tokens.Jwt
  5. Cryptography: HMAC-SHA256 (Symmetric Key)

Implementation Guide

Step 1: Create Models

OAuthClient.cs

This model represents a registered OAuth client with its credentials and authorized scopes.

namespace SecurityApp.API.Models;

public class OAuthClient
{
public string ClientId { get; set; } = string.Empty;
public string ClientSecret { get; set; } = string.Empty;
public List<string> Scopes { get; set; } = [];
}

TokenRequest.cs

OAuth 2.0 token request following RFC 6749 standard (using snake_case for JSON properties).

using System.Text.Json.Serialization;

namespace SecurityApp.API.Models;

public class TokenRequest
{
[JsonPropertyName("grant_type")]
public string GrantType { get; set; } = string.Empty;
[JsonPropertyName("client_id")]
public string ClientId { get; set; } = string.Empty;
[JsonPropertyName("client_secret")]
public string ClientSecret { get; set; } = string.Empty;
}

Note: JsonPropertyName attributes ensure compatibility with OAuth 2.0 RFC 6749 naming conventions.

TokenResponse.cs

Standard OAuth 2.0 token response.

namespace SecurityApp.API.Models;

public class TokenResponse
{
public string AccessToken { get; set; } = string.Empty;
public string TokenType { get; set; } = "Bearer";
public int ExpiresIn { get; set; }
}

Step 2: Configuration Models

JwtOptions.cs

JWT configuration using the Options Pattern.

namespace SecurityApp.API.Options;

public class JwtOptions
{
public string SecretKey { get; set; } = string.Empty;
public string Issuer { get; set; } = string.Empty;
public string Audience { get; set; } = string.Empty;
public int ExpirationMinutes { get; set; }
}

OAuthOptions.cs

OAuth clients configuration.

using SecurityApp.API.Models;

namespace SecurityApp.API.Options;

public class OAuthOptions
{
public List<OAuthClient> Clients { get; set; } = [];
}

Step 3: Application Configuration

appsettings.json

{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"Jwt": {
"SecretKey": "MyVerySecureSymmetricKey12345678901234567890",
"Issuer": "http://localhost:5292",
"Audience": "http://localhost:5292",
"ExpirationMinutes": 60
},
"OAuth": {
"Clients": [
{
"ClientId": "client_app_1",
"ClientSecret": "secret_12345",
"Scopes": [ "products.read" ]
},
{
"ClientId": "client_app_2",
"ClientSecret": "secret_67890",
"Scopes": [ "orders.read" ]
}
]
}
}

Scope Assignments:

  1. client_app_1: Can only access /api/products (scope: products.read)
  2. client_app_2: Can only access /api/orders (scope: orders.read)

Step 4: Token Service Implementation

TokenService.cs

Handles client validation and JWT token generation with scopes.

using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using SecurityApp.API.Models;
using SecurityApp.API.Options;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;

namespace SecurityApp.API.Services;

public interface ITokenService
{
TokenResponse? GenerateToken(string clientId);
bool ValidateClient(string clientId, string clientSecret);
}

public class TokenService : ITokenService
{
private readonly JwtOptions _jwtOptions;
private readonly OAuthOptions _oauthOptions;

public TokenService(IOptions<JwtOptions> jwtOptions, IOptions<OAuthOptions> oauthOptions)
{
_jwtOptions = jwtOptions.Value;
_oauthOptions = oauthOptions.Value;
}

public bool ValidateClient(string clientId, string clientSecret)
{
var client = _oauthOptions.Clients
.FirstOrDefault(c => c.ClientId == clientId && c.ClientSecret == clientSecret);
return client is not null;
}

public TokenResponse? GenerateToken(string clientId)
{
// Find client configuration
var client = _oauthOptions.Clients.FirstOrDefault(c => c.ClientId == clientId);
if (client is null)
{
return null;
}

// Create signing credentials with symmetric key
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtOptions.SecretKey));
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);

// Build claims list
var claims = new List<Claim>
{
new Claim(JwtRegisteredClaimNames.Sub, clientId),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim("client_id", clientId)
};

// Add scope claims for authorization
foreach (var scope in client.Scopes)
{
claims.Add(new Claim("scope", scope));
}

// Create JWT token
var token = new JwtSecurityToken(
issuer: _jwtOptions.Issuer,
audience: _jwtOptions.Audience,
claims: claims,
expires: DateTime.UtcNow.AddMinutes(_jwtOptions.ExpirationMinutes),
signingCredentials: credentials
);

var tokenHandler = new JwtSecurityTokenHandler();
var accessToken = tokenHandler.WriteToken(token);

return new TokenResponse
{
AccessToken = accessToken,
TokenType = "Bearer",
ExpiresIn = _jwtOptions.ExpirationMinutes * 60
};
}
}

Key Points:

  1. Client Validation: Verifies client credentials before token generation
  2. Scope Claims: Each client's scopes are added as claims to the JWT
  3. Symmetric Signature: Uses HMAC-SHA256 for token signing
  4. Standard Claims: Includes sub (subject), jti (JWT ID), and custom client_id

Step 5: Application Startup Configuration

Program.cs (Part 1: Service Registration)

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.IdentityModel.Tokens;
using SecurityApp.API.Models;
using SecurityApp.API.Options;
using SecurityApp.API.Services;
using System.Text;

var builder = WebApplication.CreateBuilder(args);

// Register OpenAPI for API documentation
builder.Services.AddOpenApi();

// Configure options
builder.Services.Configure<JwtOptions>(builder.Configuration.GetSection("Jwt"));
builder.Services.Configure<OAuthOptions>(builder.Configuration.GetSection("OAuth"));

// Register token service
builder.Services.AddScoped<ITokenService, TokenService>();

// Get JWT options for authentication configuration
var jwtOptions = builder.Configuration.GetSection("Jwt").Get<JwtOptions>();
ArgumentNullException.ThrowIfNull(jwtOptions);

// Configure JWT Bearer Authentication
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = jwtOptions.Issuer,
ValidAudience = jwtOptions.Audience,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtOptions.SecretKey)),
ClockSkew = TimeSpan.Zero // Remove default 5-minute tolerance
};

// JWT Bearer Events for debugging and monitoring
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<Program>>();
logger.LogInformation("JWT Token Received");
return Task.CompletedTask;
},

OnTokenValidated = context =>
{
var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<Program>>();
logger.LogInformation("JWT Token Validated Successfully");
if (context.Principal?.Identity is not null)
{
foreach (var claim in context.Principal.Claims)
{
logger.LogInformation("Claim: {Type} = {Value}", claim.Type, claim.Value);
}
}
return Task.CompletedTask;
},

OnAuthenticationFailed = context =>
{
var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<Program>>();
logger.LogError("JWT Authentication Failed: {Message}", context.Exception.Message);
return Task.CompletedTask;
},

OnChallenge = context =>
{
var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<Program>>();
logger.LogWarning("JWT Challenge: {Error}", context.AuthenticateFailure?.Message ?? "No token");
return Task.CompletedTask;
}
};
});

// Configure Authorization Policies
builder.Services.AddAuthorization(options =>
{
// Approach 2: Simple policy-based authorization using RequireClaim
options.AddPolicy("ProductsRead", policy =>
policy.RequireClaim("scope", "products.read"));

options.AddPolicy("OrdersRead", policy =>
policy.RequireClaim("scope", "orders.read"));
});

var app = builder.Build();

// Configure middleware pipeline
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}

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

Program.cs (Part 2: Endpoint Definitions)

// OAuth 2.0 Token Endpoint
app.MapPost("/oauth/token", (TokenRequest request, ITokenService tokenService) =>
{
// Validate grant type
if (request.GrantType != "client_credentials")
{
return Results.BadRequest(new
{
error = "unsupported_grant_type",
error_description = "Only client_credentials grant type is supported"
});
}

// Validate required parameters
if (string.IsNullOrWhiteSpace(request.ClientId) || string.IsNullOrWhiteSpace(request.ClientSecret))
{
return Results.BadRequest(new
{
error = "invalid_request",
error_description = "ClientId and ClientSecret are required"
});
}

// Validate client credentials
if (!tokenService.ValidateClient(request.ClientId, request.ClientSecret))
{
return Results.Unauthorized();
}

// Generate and return token
var token = tokenService.GenerateToken(request.ClientId);
return Results.Ok(token);
})
.WithName("GetToken")
.WithOpenApi();

// Protected Resource: Products (requires products.read scope)
app.MapGet("/api/products", [Authorize(Policy = "ProductsRead")] () =>
{
var products = new[]
{
new { Id = 1, Name = "Laptop", Price = 1200.00 },
new { Id = 2, Name = "Mouse", Price = 25.50 },
new { Id = 3, Name = "Keyboard", Price = 75.00 }
};

return Results.Ok(products);
})
.WithName("GetProducts")
.WithOpenApi();

// Protected Resource: Orders (requires orders.read scope)
app.MapGet("/api/orders", [Authorize(Policy = "OrdersRead")] (HttpContext context) =>
{
var clientId = context.User.FindFirst("client_id")?.Value;

var orders = new[]
{
new { Id = 1, ClientId = clientId, Product = "Laptop", Quantity = 2, Total = 2400.00 },
new { Id = 2, ClientId = clientId, Product = "Mouse", Quantity = 5, Total = 127.50 }
};

return Results.Ok(new { ClientId = clientId, Orders = orders });
})
.WithName("GetOrders")
.WithOpenApi();

// Diagnostic Endpoint: Token Inspection (for debugging)
app.MapGet("/api/diagnostics/token", (HttpContext context) =>
{
var authHeader = context.Request.Headers.Authorization.ToString();
if (string.IsNullOrEmpty(authHeader))
{
return Results.Ok(new { Message = "No Authorization header found" });
}

var parts = authHeader.Split(' ');
if (parts.Length != 2 || parts[0] != "Bearer")
{
return Results.Ok(new { Message = "Invalid Authorization header format" });
}

try
{
var handler = new System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler();
var jwtToken = handler.ReadJwtToken(parts[1]);
return Results.Ok(new
{
Message = "Token parsed successfully",
Issuer = jwtToken.Issuer,
Audiences = jwtToken.Audiences,
ValidFrom = jwtToken.ValidFrom,
ValidTo = jwtToken.ValidTo,
IsExpired = jwtToken.ValidTo < DateTime.UtcNow,
Claims = jwtToken.Claims.Select(c => new { c.Type, c.Value })
});
}
catch (Exception ex)
{
return Results.Ok(new { Message = "Failed to parse token", Error = ex.Message });
}
})
.WithName("DiagnoseToken")
.WithOpenApi();

app.Run();

Authorization Approaches

This project demonstrates two different approaches for implementing scope-based authorization. Both are included in the codebase as examples.

Approach 1: Custom Authorization Handler (Example Only)

This approach uses a custom AuthorizationHandler for more control and flexibility.

ScopeAuthorizationHandler.cs

using Microsoft.AspNetCore.Authorization;

namespace SecurityApp.API.Authorization;

// Custom Requirement
public class ScopeRequirement : IAuthorizationRequirement
{
public string Scope { get; }

public ScopeRequirement(string scope)
{
Scope = scope;
}
}

// Custom Handler
public class ScopeAuthorizationHandler : AuthorizationHandler<ScopeRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
ScopeRequirement requirement)
{
// Extract scope claims from the token
var scopeClaims = context.User.FindAll("scope").Select(c => c.Value);

// Check if required scope exists
if (scopeClaims.Contains(requirement.Scope))
{
context.Succeed(requirement);
}

return Task.CompletedTask;
}
}

Registration (Alternative - Commented Out)

// In Program.cs
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("ProductsRead", policy =>
policy.Requirements.Add(new ScopeRequirement("products.read")));

options.AddPolicy("OrdersRead", policy =>
policy.Requirements.Add(new ScopeRequirement("orders.read")));
});

builder.Services.AddSingleton<IAuthorizationHandler, ScopeAuthorizationHandler>();

Advantages:

  1. ✅ Full control over authorization logic
  2. ✅ Can inject dependencies (database, external services)
  3. ✅ Suitable for complex authorization rules
  4. ✅ Easy to unit test
  5. ✅ Custom logging and error handling

Use Cases:

  1. Complex business rules
  2. Database-driven permissions
  3. Multi-condition authorization
  4. Audit logging requirements

Approach 2: Policy-Based with RequireClaim (Currently Used)

This approach uses ASP.NET Core's built-in RequireClaim method.

builder.Services.AddAuthorization(options =>
{
options.AddPolicy("ProductsRead", policy =>
policy.RequireClaim("scope", "products.read"));

options.AddPolicy("OrdersRead", policy =>
policy.RequireClaim("scope", "orders.read"));
});

Advantages:

  1. ✅ Simpler and more readable
  2. ✅ Built-in ASP.NET Core feature
  3. ✅ Less code to maintain
  4. ✅ Better performance (no custom handler overhead)
  5. ✅ Perfect for simple claim validation

Use Cases:

  1. Simple scope/claim validation
  2. JWT claim-based authorization
  3. Quick implementation
  4. When custom logic is not required

Comparison Table

FeatureCustom HandlerRequireClaim
Code ComplexityHighLow
FlexibilityVery HighMedium
PerformanceGoodExcellent
TestabilityEasyMedium
DI Support✅ Yes❌ No
Custom Logic✅ Yes❌ No
Learning CurveHighLow
Best ForComplex rulesSimple claims

Project Decision

Currently Using: Approach 2 (RequireClaim)

Why?

  1. This project only validates JWT scope claims
  2. No additional business logic required
  3. Simpler and more maintainable
  4. Better performance

When to use Approach 1? Switch to custom handler if you need:

  1. Database permission checks
  2. Role + scope combinations
  3. Custom audit logging
  4. Complex authorization rules

Testing the Application

Authorization Matrix

ClientScope/api/products/api/orders
client_app_1products.read✅ 200 OK❌ 403 Forbidden
client_app_2orders.read❌ 403 Forbidden✅ 200 OK
No Token-❌ 401 Unauthorized❌ 401 Unauthorized
Invalid Token-❌ 401 Unauthorized❌ 401 Unauthorized

Test Scenarios Using .http File

SecurityApp.API.http

@SecurityApp.API_HostAddress = http://localhost:5292
@token_client1 =
@token_client2 =

### Scenario 1: Get Token for Client 1 (products.read scope)
POST {{SecurityApp.API_HostAddress}}/oauth/token
Content-Type: application/json

{
"grant_type": "client_credentials",
"client_id": "client_app_1",
"client_secret": "secret_12345"
}

###

### Scenario 2: Get Token for Client 2 (orders.read scope)
POST {{SecurityApp.API_HostAddress}}/oauth/token
Content-Type: application/json

{
"grant_type": "client_credentials",
"client_id": "client_app_2",
"client_secret": "secret_67890"
}

###

### ✅ SUCCESS: Client 1 can access Products
GET {{SecurityApp.API_HostAddress}}/api/products
Authorization: Bearer {{token_client1}}

###

### ❌ FORBIDDEN: Client 1 CANNOT access Orders
GET {{SecurityApp.API_HostAddress}}/api/orders
Authorization: Bearer {{token_client1}}

###

### ❌ FORBIDDEN: Client 2 CANNOT access Products
GET {{SecurityApp.API_HostAddress}}/api/products
Authorization: Bearer {{token_client2}}

###

### ✅ SUCCESS: Client 2 can access Orders
GET {{SecurityApp.API_HostAddress}}/api/orders
Authorization: Bearer {{token_client2}}

###

### Diagnostic: Inspect Token
GET {{SecurityApp.API_HostAddress}}/api/diagnostics/token
Authorization: Bearer {{token_client1}}

###

Expected JWT Token Structure

Client 1 Token (products.read)

{
"header": {
"alg": "HS256",
"typ": "JWT"
},
"payload": {
"sub": "client_app_1",
"jti": "unique-jwt-id",
"client_id": "client_app_1",
"scope": "products.read",
"iss": "http://localhost:5292",
"aud": "http://localhost:5292",
"exp": 1234567890
},
"signature": "HMAC-SHA256-signature"
}

Client 2 Token (orders.read)

{
"header": {
"alg": "HS256",
"typ": "JWT"
},
"payload": {
"sub": "client_app_2",
"jti": "unique-jwt-id",
"client_id": "client_app_2",
"scope": "orders.read",
"iss": "http://localhost:5292",
"aud": "http://localhost:5292",
"exp": 1234567890
},
"signature": "HMAC-SHA256-signature"
}

Testing with PowerShell

# Get token for Client 1
$body1 = @{
grant_type = "client_credentials"
client_id = "client_app_1"
client_secret = "secret_12345"
} | ConvertTo-Json

$response1 = Invoke-RestMethod -Uri "http://localhost:5292/oauth/token" `
-Method Post `
-ContentType "application/json" `
-Body $body1

$token1 = $response1.accessToken
Write-Host "Client 1 Token: $token1" -ForegroundColor Green

# Test Products endpoint (should succeed)
$headers1 = @{ Authorization = "Bearer $token1" }
try {
$products = Invoke-RestMethod -Uri "http://localhost:5292/api/products" `
-Method Get -Headers $headers1
Write-Host "✅ Products access successful!" -ForegroundColor Green
$products | ConvertTo-Json
} catch {
Write-Host "❌ Access denied" -ForegroundColor Red
}

# Test Orders endpoint (should fail with 403)
try {
$orders = Invoke-RestMethod -Uri "http://localhost:5292/api/orders" `
-Method Get -Headers $headers1
} catch {
Write-Host "❌ Orders access denied (Expected - 403 Forbidden)" -ForegroundColor Yellow
}

Testing with curl

# Get token for Client 1
curl -X POST http://localhost:5292/oauth/token \
-H "Content-Type: application/json" \
-d '{
"grant_type": "client_credentials",
"client_id": "client_app_1",
"client_secret": "secret_12345"
}'

# Response
{
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"tokenType": "Bearer",
"expiresIn": 3600
}

# Access Products (Success)
curl -X GET http://localhost:5292/api/products \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

# Response: 200 OK
[
{ "id": 1, "name": "Laptop", "price": 1200.00 },
{ "id": 2, "name": "Mouse", "price": 25.50 },
{ "id": 3, "name": "Keyboard", "price": 75.00 }
]

# Access Orders (Forbidden)
curl -X GET http://localhost:5292/api/orders \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

# Response: 403 Forbidden

Security Considerations

Production Best Practices

1. Secret Management

Never do this in production:

{
"Jwt": {
"SecretKey": "MyVerySecureSymmetricKey12345678901234567890"
}
}

Use secret management services:

// Azure Key Vault
builder.Configuration.AddAzureKeyVault(
new Uri($"https://{keyVaultName}.vault.azure.net/"),
new DefaultAzureCredential());

// AWS Secrets Manager
builder.Configuration.AddSecretsManager();

// Environment Variables
var secretKey = Environment.GetEnvironmentVariable("JWT_SECRET_KEY");

2. HTTPS Enforcement

// Force HTTPS in production
if (!app.Environment.IsDevelopment())
{
app.UseHttpsRedirection();
app.UseHsts();
}

3. Token Expiration

{
"Jwt": {
"ExpirationMinutes": 60 // Adjust based on security requirements
}
}

Recommendations:

  1. Short-lived tokens: 15-60 minutes
  2. Long-lived tokens: Implement refresh tokens
  3. Critical operations: Require re-authentication

4. Scope Granularity

Good Practice:

{
"Scopes": [
"products.read",
"products.write",
"products.delete",
"orders.read",
"orders.write"
]
}

Poor Practice:

{
"Scopes": [ "admin", "all" ] // Too broad
}

5. Rate Limiting

builder.Services.AddRateLimiter(options =>
{
options.AddFixedWindowLimiter("token", options =>
{
options.Window = TimeSpan.FromMinutes(1);
options.PermitLimit = 10;
});
});

app.MapPost("/oauth/token", ...)
.RequireRateLimiting("token");

6. Logging and Monitoring

options.Events = new JwtBearerEvents
{
OnAuthenticationFailed = context =>
{
var logger = context.HttpContext.RequestServices
.GetRequiredService<ILogger<Program>>();
// Log security events
logger.LogWarning("Authentication failed: {Error} from IP: {IP}",
context.Exception.Message,
context.Request.HttpContext.Connection.RemoteIpAddress);
// Track failed attempts
// Implement IP blocking if necessary
return Task.CompletedTask;
}
};

7. Token Validation Parameters

options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true, // Verify token issuer
ValidateAudience = true, // Verify intended audience
ValidateLifetime = true, // Check expiration
ValidateIssuerSigningKey = true, // Verify signature
ClockSkew = TimeSpan.Zero, // Remove default 5-min tolerance
RequireExpirationTime = true, // Must have exp claim
RequireSignedTokens = true // Must be signed
};

8. Client Secret Storage

Best Practices:

  1. Hash client secrets (like passwords)
  2. Use strong secrets (minimum 32 characters)
  3. Rotate secrets regularly
  4. Store in secure vaults (Azure Key Vault, AWS Secrets Manager)
// Example: Hash client secrets
public bool ValidateClient(string clientId, string clientSecret)
{
var client = _oauthOptions.Clients.FirstOrDefault(c => c.ClientId == clientId);
if (client is null) return false;
// Use proper password hashing (e.g., BCrypt, Argon2)
return BCrypt.Net.BCrypt.Verify(clientSecret, client.ClientSecretHash);
}

Advanced Scenarios

Multiple Scopes (OR Logic)

// Client can have products.read OR products.write
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("ProductsAccess", policy =>
policy.RequireAssertion(context =>
context.User.HasClaim("scope", "products.read") ||
context.User.HasClaim("scope", "products.write")));
});

app.MapGet("/api/products", [Authorize(Policy = "ProductsAccess")] () => { ... });

Multiple Scopes (AND Logic)

// Client must have BOTH scopes
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("AdminProducts", policy =>
{
policy.RequireClaim("scope", "products.read");
policy.RequireClaim("scope", "admin");
});
});

Database-Driven Permissions

public class DatabasePermissionHandler : AuthorizationHandler<PermissionRequirement>
{
private readonly IPermissionService _permissionService;

public DatabasePermissionHandler(IPermissionService permissionService)
{
_permissionService = permissionService;
}

protected override async Task HandleRequirementAsync(
AuthorizationHandlerContext context,
PermissionRequirement requirement)
{
var clientId = context.User.FindFirst("client_id")?.Value;
var hasPermission = await _permissionService
.CheckPermissionAsync(clientId, requirement.Permission);

if (hasPermission)
{
context.Succeed(requirement);
}
}
}

Troubleshooting

Common Issues

1. 401 Unauthorized

Possible Causes:

  1. Token missing or malformed
  2. Token expired
  3. Invalid signature
  4. Issuer/Audience mismatch

Solution:

// Enable detailed logging
"Logging": {
"LogLevel": {
"Microsoft.AspNetCore.Authentication": "Debug"
}
}

2. 403 Forbidden

Cause: Token is valid but lacks required scope.

Verify:

GET /api/diagnostics/token
Authorization: Bearer {token}

Check if the scope claim matches the policy requirement.

3. Token Not Validated

Check:

  1. SecretKey matches between token generation and validation
  2. Issuer and Audience values are correct
  3. ClockSkew settings

Performance Optimization

1. Token Caching

// Cache token validation results
builder.Services.AddMemoryCache();
builder.Services.AddDistributedMemoryCache();

options.SaveToken = true; // Cache token in AuthenticationProperties

2. Use Singleton for Handler

// Handlers are stateless - use singleton
builder.Services.AddSingleton<IAuthorizationHandler, ScopeAuthorizationHandler>();

3. Async Where Possible

public async Task<TokenResponse?> GenerateTokenAsync(string clientId)
{
// If you need database/external service calls
var client = await _clientRepository.GetByIdAsync(clientId);
// ...
}
Share this lesson: