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)
![Internal_Call_SP]()
Postman Request API Key (External)
![External_Call_SP]()
Postman Request Hybrid By JWT (Internal)
![Hybrid_Call_SP_JWT]()
Postman Request Hybrid By API Key (External)
![Hybrid_Call_SP_API_Key]()
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.