.NET
JWT Authentication: Complete Implementation Guide
Implementing secure authentication is a critical challenge for modern web APIs and applications. JSON Web Tokens (JWTs) have emerged as the industry-standard solution for stateless authentication, allowing servers to verify user identity without maintaining server-side session state . By the end of this guide, you'll understand the complete architecture of JWT-based authentication, be able to implement secure token issuance and validation in your own applications, and have a production-ready strategy for handling token lifecycle management. You'll walk away with the practical knowledge to implement JWT authentication securely in any tech stack.
What You'll Learn
You'll understand how JWTs work architecturally, why proper implementation matters for security, and gain hands-on knowledge for configuring authentication middleware, issuing tokens, and protecting API endpoints. By the end, you'll be equipped to implement JWT authentication in your own projects with confidence in the security decisions you're making.
Understanding the JWT Authentication Architecture
Before diving into code, it's essential to grasp how JWT authentication works. A JSON Web Token is a compact, URL-safe means of representing claims between two parties . The token consists of three Base64URL-encoded parts separated by dots: header, payload, and signature (header.payload.signature) .
The JWT authentication flow follows a clear pattern: a client authenticates (typically via a login endpoint), receives a signed token, and then includes that token in the Authorization header of subsequent requests. The server validates the token's signature and claims on each request, making the system stateless — no server-side session storage is required .
Key Components of a JWT
| Component | Purpose | Example |
|---|---|---|
| Header | Specifies signing algorithm and token type | {"alg":"RS256","typ":"JWT"} |
| Payload | Contains claims (user data, expiration, issuer) | {"sub":"user123","exp":1700000000} |
| Signature | Ensures token integrity and authenticity | Cryptographic hash of header+payload with secret |
This design makes JWTs ideal for distributed systems and microservices where maintaining shared session state would be impractical .
How to Implement JWT Authentication: Step-by-Step Guide
The following steps outline a production-ready approach to implementing JWT authentication. While examples use .NET and Node.js, the principles apply across all programming languages.
Step 1: Install the Required Authentication Packages
Modern frameworks provide official JWT Bearer authentication handlers. For .NET, install the Microsoft.AspNetCore.Authentication.JwtBearer package . For Node.js, use jsonwebtoken .
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
# Node.js
npm install jsonwebtoken bcryptjs dotenv
Relying on official, battle-tested libraries is essential — never implement your own cryptographic signing logic .
Step 2: Configure Authentication Services
The authentication configuration defines how your API validates incoming tokens. Critical validation parameters include :
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options => {
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true, // Verify token came from known authority
ValidateAudience = true, // Verify token is intended for this API
ValidateLifetime = true, // Reject expired tokens automatically
ValidateIssuerSigningKey = true, // Verify cryptographic signature
ValidIssuer = "https://your-identity-provider.com",
ValidAudience = "https://your-api.com",
IssuerSigningKey = publicKey // The key for signature verification
};
});
All four validation flags should be set to true in production. Disabling any of these creates security vulnerabilities . The IssuerSigningKey can be loaded from a PEM file or, in production, automatically retrieved from the identity provider's metadata endpoint .
Step 3: Add Authentication and Authorization Middleware
The order of middleware components is crucial — authentication must run before authorization :
app.UseAuthentication(); // Validates JWT and populates HttpContext.User
app.UseAuthorization(); // Enforces [Authorize] policies on protected endpoints
When a request reaches UseAuthentication, the handler examines the Authorization header, validates the JWT, and creates a ClaimsPrincipal representing the authenticated user. This object is then available throughout the request pipeline for authorization decisions.
Step 4: Protect Endpoints with the [Authorize] Attribute
Securing API endpoints requires the [Authorize] attribute :
[Authorize]
[ApiController]
[Route("api/[controller]")]
public class SecureController : ControllerBase
{
[HttpGet]
public IActionResult GetSecureData()
{
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
return Ok($"Authenticated user: {userId}");
}
}
Requests without a valid token receive a 401 Unauthorized response. When authentication succeeds but lacks required permissions, the API returns 403 Forbidden .
Step 5: Implement Token Issuance (Development Only)
In production, token issuance belongs to a dedicated Identity Provider (IdP) implementing OAuth 2.0 or OpenID Connect . The API's role is limited to validation.
However, for development and testing, you may need a local token endpoint:
// Node.js example with jsonwebtoken
router.post('/login', async (req, res) => {
const { email, password } = req.body;
const user = await User.findOne({ email });
if (!user || !(await bcrypt.compare(password, user.password))) {
return res.status(401).json({ message: 'Invalid credentials' });
}
const token = jwt.sign(
{ id: user._id, email: user.email },
process.env.JWT_SECRET,
{ expiresIn: '15m' } // Short-lived access tokens
);
res.json({ token });
});
Access tokens should have short lifetimes — typically 15 minutes — to limit the damage from token theft .
Production Security Best Practices
Never Store Tokens in LocalStorage
For web applications, store JWTs in HttpOnly, Secure cookies rather than localStorage or sessionStorage . This prevents cross-site scripting (XSS) attacks from accessing tokens. Cookie configuration should include:
HttpOnly: true // Prevents JavaScript access
Secure: true // Only sent over HTTPS
SameSite: Lax or Strict // CSRF protection
Use Refresh Token Rotation
Since access tokens expire quickly, implement a refresh token mechanism to maintain user sessions without requiring re-authentication :
- Refresh token: Long-lived (e.g., 7 days), stored securely on the server
- Access token: Short-lived (e.g., 15 minutes), sent with each request
- Rotation: Each refresh request issues a new access+refresh pair, invalidating the old refresh token
This approach mitigates the impact of stolen tokens and enables clean logout functionality.
Employ Asymmetric Signing (RS256/ES256) for Production
Use asymmetric algorithms (RS256 or ES256) rather than HMAC (HS256) in production :
| Algorithm | Key Type | Use Case |
|---|---|---|
| HS256 | Symmetric (shared secret) | Single-service applications |
| RS256/ES256 | Asymmetric (public/private key) | Multi-service systems, microservices |
Asymmetric signing allows multiple services to verify tokens using a public key while only the issuer holds the private key — following the principle of least privilege.
Validate All Standard Claims
The following claims are required for OAuth 2.0 access tokens and must be validated :
iss(Issuer) — matches expected authorityaud(Audience) — matches your API identifierexp(Expiration) — token not expiredsub(Subject) — user identifieriat(Issued At) — token issued in the pastjti(JWT ID) — unique identifier for token tracking
Stateless Authentication with Redis
While JWTs are stateless by design, implementing token revocation requires a storage layer. Redis provides an ideal solution :
// Store token metadata for revocation
func (r *Redis) SetJTI(ctx context.Context, key, userID string, exp time.Time) error {
return r.Client.Set(ctx, key, userID, time.Until(exp)).Err()
}
// Check if token is revoked
func (r *Redis) GetUserByJTI(ctx context.Context, key string) (string, error) {
return r.Client.Get(ctx, key).Result()
}
Store the token's unique identifier (jti) in Redis with a TTL matching the token's expiration. This enables immediate logout and prevents reuse of compromised tokens .
Common JWT Implementation Mistakes to Avoid
Self-issuing tokens in production: APIs should validate tokens, not issue them. Use a dedicated IdP for issuance .
Weak signing keys: Use cryptographically strong secrets (at least 32 bytes) generated via
openssl rand -base64 32.Disabling claim validation: Never set
ValidateIssuerorValidateAudiencetofalsein production .Long token lifetimes: Access tokens should expire within 15 minutes. Longer lifetimes increase attack surface .
No revocation mechanism: Implement logout and token revocation using Redis or a similar store .
Frequently Asked Questions
What is the difference between JWT authentication and OAuth 2.0? JWT is a token format, while OAuth 2.0 is an authorization framework. OAuth 2.0 defines how to obtain tokens (including JWTs), while JWT defines the structure of the token itself. They are complementary — OAuth 2.0 can use JWTs as access tokens, but you can also use JWTs independently for authentication in smaller systems.
How do I securely store JWTs on the client side? Store JWTs in HttpOnly, Secure cookies rather than localStorage or sessionStorage. HttpOnly cookies prevent JavaScript access, protecting against XSS attacks. For mobile applications, use secure platform-specific storage mechanisms.
Can I revoke a JWT before it expires? Yes, by maintaining a revocation list or store of token identifiers (jti claims). Redis is ideal for this — store the jti with a TTL matching the token's expiration. For each request, check if the jti exists in the revocation store. This enables immediate logout and token invalidation.
Should I use HS256 or RS256 for signing JWTs? Use RS256 (asymmetric) for production multi-service environments where token issuance and validation occur on different systems. Use HS256 only for single-service applications where the same secret can be securely shared. Asymmetric signing allows validation services to use only the public key, following the principle of least privilege.
How do I implement refresh tokens securely?
Issue a long-lived refresh token (7 days) alongside the short-lived access token (15 minutes). Provide a /refresh endpoint that accepts the refresh token, validates it, and returns a new access+refresh pair. Rotate refresh tokens on each use to prevent token replay attacks. Store refresh tokens securely (e.g., in Redis) and invalidate them on logout .
Sources
- Duende Software. "A step by step JWT authentication example." Duende Software. 2025.
- Microsoft Learn. "Configure JWT bearer authentication in ASP.NET Core." Microsoft. 2025.
- Duende Software. "Implementing Token Authentication in Controller-Based ASP.NET Core Web APIs." Duende Software. 2026.
- QuickNode. "How to Implement JSON Web Tokens (JWT) Authorization." QuickNode. 2025.
- freeCodeCamp. "How to Build a Secure Authentication System with JWT and Refresh Tokens." freeCodeCamp. 2025.
- Vonage. "JWT Authentication in Go with Gin." Vonage Developer. 2025.
— Editorial Team
No comments yet.