Handling Internal, External, and Hybrid API Access the Right Way
In real-world ASP.NET Core applications, not all APIs are meant for the same type of consumers.
Some APIs are:
Used only by internal applications (UI, mobile apps, admin portals)
Used only by external systems (third-party apps, cron jobs, integrations)
Used by both internal and external consumers
Using a single authentication mechanism for all APIs often leads to:
Security loopholes
Broken authentication
Hard-to-maintain code
This article demonstrates the correct and scalable approach to handle:
JWT-only access (Internal applications)
API Key–only access (External applications)
JWT OR API Key access (Hybrid APIs)
All implemented using ASP.NET Core Authentication Handlers and Policies — the recommended enterprise pattern.
Why NOT Use a Single Custom Authentication Attribute?
A common mistake is:
Creating one custom filter
Checking JWT and API key manually
Setting it as the default scheme
This approach breaks because:
JWT middleware is bypassed
[Authorize] stops working correctly
Debugging becomes painful
Authentication Types – When to Use What
| Authentication | Used By | Best For |
|---|
| JWT (Bearer Token) | Internal apps | UI, Mobile, Admin portals |
| API Key (Header-based) | External apps | Integrations, services |
| JWT OR API Key | Both | Shared APIs |
Authentication Schemes
public static class AuthSchemes
{
public const string Jwt = JwtBearerDefaults.AuthenticationScheme;
public const string ApiKey = "ApiKey";
}
API Key Authentication Handler (External Systems)
public class ApiKeyAuthHandler
: AuthenticationHandler<AuthenticationSchemeOptions>
{
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (!Request.Headers.TryGetValue("X-Api-Key", out var apiKey))
return Task.FromResult(AuthenticateResult.NoResult());
if (apiKey != "MY_STATIC_API_KEY_123")
return Task.FromResult(AuthenticateResult.Fail("Invalid API Key"));
var claims = new[]
{
new Claim(ClaimTypes.Name, "ExternalSystem"),
new Claim("AccessedBy", "ApiKey")
};
var identity = new ClaimsIdentity(claims, AuthSchemes.ApiKey);
var principal = new ClaimsPrincipal(identity);
return Task.FromResult(
AuthenticateResult.Success(
new AuthenticationTicket(principal, AuthSchemes.ApiKey)));
}
}
JWT Authentication (Internal Applications)
JWT is used for logged-in users.
Token generated after login
Sent in Authorization: Bearer <token>
Validated by ASP.NET Core middleware
builder.Services.AddAuthentication()
.AddJwtBearer(AuthSchemes.Jwt, options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey =
new SymmetricSecurityKey(
Encoding.UTF8.GetBytes("SUPER_SECRET_KEY"))
};
})
.AddScheme<AuthenticationSchemeOptions, ApiKeyAuthHandler>(
AuthSchemes.ApiKey, null);
Authorization Policy (JWT OR API Key)
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("JwtOrApiKey", policy =>
{
policy.AddAuthenticationSchemes(
AuthSchemes.Jwt,
AuthSchemes.ApiKey);
policy.RequireAuthenticatedUser();
});
});
Login API – JWT Token Generation (Static User)
[HttpPost("login")]
public IActionResult Login(LoginRequest request)
{
if (request.Username != "admin" || request.Password != "password")
return Unauthorized();
var claims = new[]
{
new Claim(ClaimTypes.Name, request.Username),
new Claim("AccessedBy", "Jwt")
};
var key = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes("SUPER_SECRET_KEY"));
var token = new JwtSecurityToken(
claims: claims,
expires: DateTime.UtcNow.AddMinutes(30),
signingCredentials:
new SigningCredentials(key, SecurityAlgorithms.HmacSha256)
);
return Ok(new
{
access_token = new JwtSecurityTokenHandler().WriteToken(token)
});
}
CheckApiController – All 3 Scenarios
[ApiController]
[Route("api/check")]
public class CheckApiController : ControllerBase
{
private readonly ILogger<CheckApiController> _logger;
public CheckApiController(ILogger<CheckApiController> logger)
{
_logger = logger;
}
// CASE 1: Internal apps only (JWT)
[HttpGet("internal")]
[Authorize(AuthenticationSchemes = AuthSchemes.Jwt)]
public IActionResult InternalOnly()
{
LogAccess();
return Ok("Accessible only with JWT");
}
// CASE 2: External apps only (API Key)
[HttpGet("external")]
[Authorize(AuthenticationSchemes = AuthSchemes.ApiKey)]
public IActionResult ExternalOnly()
{
LogAccess();
return Ok("Accessible only with API Key");
}
// CASE 3: Internal OR External
[HttpGet("hybrid")]
[Authorize(Policy = "JwtOrApiKey")]
public IActionResult Hybrid()
{
LogAccess();
return Ok("Accessible with JWT or API Key");
}
private void LogAccess()
{
var accessType = User.Claims
.FirstOrDefault(c => c.Type == "AccessedBy")?.Value;
_logger.LogInformation(
"API accessed using {AccessType}", accessType);
}
}
How to Call APIs
JWT (Internal)
Authorization: Bearer <JWT_TOKEN>
API Key (External)
X-Api-Key: MY_STATIC_API_KEY_123
Postman Request JWT (Internal)

Postman Request API Key (External)

Postman Request Hybrid By JWT (Internal)

Postman Request Hybrid By API Key (External)

When to Use Which Authentication?
Use JWT When:
Use API Key When:
Use Both When:
Conclusion
Supporting JWT and API Key authentication together is a very common enterprise requirement.
The correct ASP.NET Core approach is:
Use separate authentication schemes
Control access via policies
Avoid custom authorization filters
Keep controllers clean with [Authorize]
Source code is attached for your reference.
Join the conversation! Your thoughts help the community grow.