Description
Description
We need to secure our API endpoints that perform data mutations (POST
, PUT
, DELETE
) so that only authorized machine-to-machine (M2M) clients — such as CLIs, daemons, or backend services — can access them.
To achieve this, we will adopt JWT Bearer authentication using the OAuth 2.0 Client Credentials Flow. This allows external systems to authenticate with a client_id
and client_secret
, receive a signed JWT, and use it to authorize calls to protected API routes.
This pattern is standard in public APIs (e.g., FedEx, UPS, Stripe) for securing non-user access.
sequenceDiagram
participant Client as Client (Machine-to-Machine app)
participant Server as Server (ASP.NET Core Web API)
Note over Client,Server: Step 1 - Obtain Access Token
Client->>Server: POST /auth/token (client_id, client_secret)
Server-->>Client: 200 (OK) { access_token, expires_in, token_type }
Note over Client,Server: Step 2 - Use Token to Access Protected Resources
Client->>Server: POST /{resource}/{id} (Authorization: Bearer {access_token})
Server-->>Client: 201 (Created)
Client->>Server: PUT /{resource}/{id} (Authorization: Bearer {access_token})
Server-->>Client: 204 (No Content)
Client->>Server: DELETE /{resource}/{id} (Authorization: Bearer {access_token})
Server-->>Client: 204 (No Content)
Proposed Solution
-
Expose a Token Endpoint
Create aPOST /auth/token
endpoint that acceptsclient_id
andclient_secret
via form data. -
Client Authentication
Credentials will be validated against values defined inappsettings.json
(or environment variables in production).
If valid, the server will issue a JWT signed usingHS256
. -
JWT Token Configuration
Tokens will include standard claims and be signed using a 256-bit key stored in configuration.
For production, this key must be injected securely via environment variables. -
Protect API Routes
Apply[Authorize]
toPOST
,PUT
, andDELETE
endpoints.
The app will use ASP.NET Core’sJwtBearer
authentication middleware to validate incoming tokens.
Suggested Approach
1. Store Client Credentials in appsettings.json
"Jwt": {
"Key": "1LnBfWcu7gTDmqT41QCW4ANu1xsHMcseingKWruVveM=",
"Issuer": "dotnet-samples-aspnetcore-webapi-issuer",
"Audience": "dotnet-samples-aspnetcore-webapi-audience",
"ExpiresInSeconds": "3600"
},
"ClientCredentials": {
"ClientId": "foobarbaz",
"ClientSecret": "#!_7h3qu1ck8r0wnf0xjump50v3r7h3l42yd09=@42"
}
2. Create a TokenRequestModel
and TokenResponseModel
public class TokenRequestModel
{
public string GrantType { get; set; } = default!;
public string ClientId { get; set; } = default!;
public string ClientSecret { get; set; } = default!;
}
using System.Text.Json.Serialization;
public class TokenResponseModel
{
[JsonPropertyName("access_token")]
public string AccessToken { get; set; } = null!;
[JsonPropertyName("token_type")]
public string TokenType { get; set; } = "Bearer";
[JsonPropertyName("expires_in")]
public int ExpiresIn { get; set; }
}
3. Create ITokenService
interface
public interface ITokenService
{
(string AccessToken, int ExpiresIn) GenerateToken(string clientId);
}
4. Implement ITokenService
public class TokenService : ITokenService
{
private readonly IConfiguration _config;
public TokenService(IConfiguration config)
{
_config = config;
}
public (string AccessToken, int ExpiresIn) GenerateToken(string clientId)
{
var expiresIn = int.Parse(_config["Jwt:ExpiresInSeconds"] ?? "3600");
var expires = DateTime.UtcNow.AddSeconds(expiresIn);
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Sub, clientId),
new Claim("client_id", clientId),
new Claim("scope", "api:write")
};
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"]!));
var signingCredentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: _config["Jwt:Issuer"],
audience: _config["Jwt:Audience"],
claims: claims,
expires: expires,
signingCredentials: signingCredentials
);
var accessToken = new JwtSecurityTokenHandler().WriteToken(token);
return (accessToken, expiresIn);
}
}
5. Register TokenService
in Program.cs
builder.Services.AddScoped<ITokenService, TokenService>();
6. Create AuthController
with /auth/token
endpoint
[HttpPost("token")]
public IActionResult Token([FromBody] TokenRequestModel request)
{
if (request.GrantType != "client_credentials")
{
return BadRequest(new { error = "unsupported_grant_type" });
}
var clientId = _config["ClientCredentials:ClientId"];
var clientSecret = _config["ClientCredentials:ClientSecret"];
if (request.ClientId != clientId || request.ClientSecret != clientSecret)
{
return Unauthorized(new { error = "invalid_client" });
}
var (accessToken, expiresIn) = _tokenService.GenerateToken(request.ClientId);
var response = new TokenResponseModel
{
AccessToken = accessToken,
ExpiresIn = expiresIn
// TokenType defaults to "Bearer"
};
return Ok(response);
}
7.\ Configure Authentication in Program.cs
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!))
};
});
builder.Services.AddAuthorization();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
8. Protect Sensitive API Routes
[ApiController]
[Route("api/data")]
public class DataController : ControllerBase
{
[HttpGet]
public IActionResult Get() => Ok("Public data.");
[Authorize]
[HttpPost]
public IActionResult Post() => Ok("Created.");
[Authorize]
[HttpPut("{id}")]
public IActionResult Put(int id) => Ok("Updated.");
[Authorize]
[HttpDelete("{id}")]
public IActionResult Delete(int id) => Ok("Deleted.");
}
9. Requesting a Token
curl -X POST http://localhost:9000/auth/token \
-H "Content-Type: application/json" \
-d '{
"grant_type": "client_credentials",
"client_id": "foobarbaz",
"client_secret": "#!_7h3qu1ck8r0wnf0xjump50v3r7h3l42yd09_@42"
}'
Acceptance Criteria
/auth/token
endpoint issues a JWT for valid clients- Credentials are stored in
appsettings.json
and read securely viaIConfiguration
- Token contains correct claims (
sub
,client_id
,scope
) - JWTs are short-lived (default: 1 hour)
- Only authenticated requests can
POST
,PUT
, orDELETE
GET
remains open to anonymous access- Unauthorized clients receive appropriate
401
responses - Documentation updated to include:
- How to obtain a token
- How to call protected routes with Bearer auth