You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
20 KiB
20 KiB
marp | paginate | math | theme | title |
---|---|---|---|---|
true | true | mathjax | buutti | 7. Authentication and Authorization |
Authentication and Authorization
Contents
- Introduction to Authentication & Authorization
- JWT Tokens
- Setting up Authentication in ASP.NET Core
- User Registration and Login
- Protected Endpoints
- Role-based Authorization
- Testing Authentication
Introduction to Authentication & Authorization
What is authentication?
- Authentication is the process of verifying who a user is
- "Who are you?"
- Confirms the identity of a user trying to access the system
- Common authentication methods:
- Username/email and password
- Social login (Google, Facebook, etc.)
- Multi-factor authentication (MFA)
- Biometric authentication
What is authorization?
- Authorization is the process of determining what a user can access
- "What are you allowed to do?"
- Controls access to resources based on user permissions
- Example: Only admins can delete users
- Different authorization levels:
- Public: Anyone can access
- Authenticated: Only logged-in users can access
- Role-based: Only users with specific roles can access
- Resource-based: Users can only access their own resources
Why do we need authentication & authorization?
- Security: Protect sensitive data and operations
- User experience: Personalized content and features
- Compliance: Meet legal and regulatory requirements
- Audit Trail: Track who did what and when
- Resource management: Control access to limited resources
JWT Tokens
What is JWT?
- JWT (JSON Web Token) is an open standard for a compact, URL-safe means of representing claims between two parties
- Self-contained tokens that include all necessary information
- It is stateless: no need to store session data on the server
- Notes:
- Tokens have a set expiration time, and cannot be invalidated beforehand
- Client must store tokens securely!
JWT structure
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
- JWTs consist of three parts separated by dots
.
- Header contains token type and signing algorithm
- Payload contains claims (user data, permissions, etc.)
- Signature verifies the token hasn't been tampered with
JWT claims
- There are two types of JWT claims, registered (defined in the JWT specification) and custom (which can be either public or private)
Some registered claims (standard)
iss
(issuer): Who issued the tokensub
(subject): Who the token is forexp
(expiration): When the token expiresiat
(issued at): When the token was issued
Custom claim examples
userId
: User's unique identifieremail
: User's email addressrole
: User's role (admin, user, etc.)permissions
: Specific permissions
Setting up Authentication in ASP.NET Core
Required NuGet Packages
- Install the following packages from the NuGet Package manager:
Microsoft.AspNetCore.Authentication.JwtBearer Microsoft.IdentityModel.Tokens System.IdentityModel.Tokens.Jwt
Configuration in appsettings.json
{
"JwtSettings": {
"SecretKey": "your-super-secret-key-with-at-least-32-characters",
"Issuer": "your-app-name",
"Audience": "your-app-users",
"ExpirationInMinutes": 60
},
"ConnectionStrings": {
"DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=AuthDemoDb;Trusted_Connection=true;MultipleActiveResultSets=true"
}
}
- To generate the
SecretKey
, you can use random.org
Program.cs Configuration
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container
builder.Services.AddControllers();
// Configure JWT Authentication
var jwtSettings = builder.Configuration.GetSection("JwtSettings");
var secretKey = Encoding.ASCII.GetBytes(jwtSettings["SecretKey"]);
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = jwtSettings["Issuer"],
ValidAudience = jwtSettings["Audience"],
IssuerSigningKey = new SymmetricSecurityKey(secretKey)
};
});
builder.Services.AddAuthorization();
var app = builder.Build();
// Configure the HTTP request pipeline
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
Exercise 1: Basic JWT Setup
- Create a new ASP.NET Core Web API project
- Install the required NuGet packages
- Configure JWT authentication in
Program.cs
- Add JWT settings to
appsettings.json
- Test that the application starts without errors
User Registration and Login
User Model
public class User
{
public int Id { get; set; }
public string Username { get; set; }
public string Email { get; set; }
public string PasswordHash { get; set; }
public string Role { get; set; } = "User";
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}
DTOs (Data Transfer Objects)
public class RegisterRequest
{
[Required]
[StringLength(50)]
public string Username { get; set; }
[Required]
[EmailAddress]
public string Email { get; set; }
[Required]
[StringLength(100, MinimumLength = 6)]
public string Password { get; set; }
}
public class LoginRequest
{
[Required]
public string Username { get; set; }
[Required]
public string Password { get; set; }
}
public class AuthResponse
{
public string Token { get; set; }
public string Username { get; set; }
public string Role { get; set; }
public DateTime ExpiresAt { get; set; }
}
JWT Service
public interface IJwtService
{
string GenerateToken(User user);
bool ValidateToken(string token);
}
public class JwtService : IJwtService
{
private readonly IConfiguration _configuration;
public JwtService(IConfiguration configuration)
{
_configuration = configuration;
}
public string GenerateToken(User user)
{
var jwtSettings = _configuration.GetSection("JwtSettings");
var secretKey = Encoding.ASCII.GetBytes(jwtSettings["SecretKey"]);
var tokenHandler = new JwtSecurityTokenHandler();
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
new Claim(ClaimTypes.Name, user.Username),
new Claim(ClaimTypes.Email, user.Email),
new Claim(ClaimTypes.Role, user.Role)
}),
Expires = DateTime.UtcNow.AddMinutes(
Convert.ToDouble(jwtSettings["ExpirationInMinutes"])
),
Issuer = jwtSettings["Issuer"],
Audience = jwtSettings["Audience"],
SigningCredentials = new SigningCredentials(
new SymmetricSecurityKey(secretKey),
SecurityAlgorithms.HmacSha256Signature
)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
}
public bool ValidateToken(string token)
{
var jwtSettings = _configuration.GetSection("JwtSettings");
var secretKey = Encoding.ASCII.GetBytes(jwtSettings["SecretKey"]);
var tokenHandler = new JwtSecurityTokenHandler();
try
{
tokenHandler.ValidateToken(token, new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(secretKey),
ValidateIssuer = true,
ValidIssuer = jwtSettings["Issuer"],
ValidateAudience = true,
ValidAudience = jwtSettings["Audience"],
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
}, out SecurityToken validatedToken);
return true;
}
catch
{
return false;
}
}
}
Auth Controller
[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
private readonly IUserService _userService;
private readonly IJwtService _jwtService;
public AuthController(IUserService userService, IJwtService jwtService)
{
_userService = userService;
_jwtService = jwtService;
}
[HttpPost("register")]
public async Task<ActionResult<AuthResponse>> Register(RegisterRequest request)
{
try
{
var user = await _userService.RegisterUser(request);
var token = _jwtService.GenerateToken(user);
return Ok(new AuthResponse
{
Token = token,
Username = user.Username,
Role = user.Role,
ExpiresAt = DateTime.UtcNow.AddMinutes(60)
});
}
catch (Exception ex)
{
return BadRequest(new { message = ex.Message });
}
}
[HttpPost("login")]
public async Task<ActionResult<AuthResponse>> Login(LoginRequest request)
{
try
{
var user = await _userService.ValidateUser(request);
var token = _jwtService.GenerateToken(user);
return Ok(new AuthResponse
{
Token = token,
Username = user.Username,
Role = user.Role,
ExpiresAt = DateTime.UtcNow.AddMinutes(60)
});
}
catch (Exception ex)
{
return Unauthorized(new { message = ex.Message });
}
}
}
Exercise 2: User Authentication Implementation
- Create the
User
model and DTOs - Implement the
JwtService
class - Create the
AuthController
with register and login endpoints - Implement password hashing using
BCrypt.Net.BCrypt
- Test the registration and login endpoints using Postman
Protected Endpoints
Using [Authorize] Attribute
[ApiController]
[Route("api/[controller]")]
public class UserController : ControllerBase
{
[HttpGet("profile")]
[Authorize] // Requires authentication
public ActionResult<UserProfile> GetProfile()
{
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var username = User.FindFirst(ClaimTypes.Name)?.Value;
var email = User.FindFirst(ClaimTypes.Email)?.Value;
return Ok(new UserProfile
{
Id = int.Parse(userId),
Username = username,
Email = email
});
}
[HttpPut("profile")]
[Authorize] // Requires authentication
public ActionResult UpdateProfile(UpdateProfileRequest request)
{
// Update user profile logic
return Ok(new { message = "Profile updated successfully" });
}
}
Accessing User Claims
[HttpGet("me")]
[Authorize]
public ActionResult GetCurrentUser()
{
var claims = new
{
UserId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value,
Username = User.FindFirst(ClaimTypes.Name)?.Value,
Email = User.FindFirst(ClaimTypes.Email)?.Value,
Role = User.FindFirst(ClaimTypes.Role)?.Value,
IsAuthenticated = User.Identity.IsAuthenticated
};
return Ok(claims);
}
Custom Authorization Attributes
public class RequireAdminAttribute : AuthorizeAttribute
{
public RequireAdminAttribute()
{
Roles = "Admin";
}
}
[HttpGet("admin-only")]
[RequireAdmin]
public ActionResult AdminOnly()
{
return Ok(new { message = "This endpoint is only for admins" });
}
Exercise 3: Protected Endpoints
- Create a
UserController
with protected endpoints - Implement an endpoint that returns the current user's profile
- Create a custom authorization attribute for admin users
- Test the endpoints with and without valid JWT tokens
- Verify that unauthorized requests return 401 status codes
Role-based Authorization
Role-based Access Control (RBAC)
- Roles define sets of permissions
- Users are assigned one or more roles
- Endpoints are protected based on required roles
- Common roles:
Admin
,User
,Moderator
,Guest
Using Role-based Authorization
[ApiController]
[Route("api/[controller]")]
public class AdminController : ControllerBase
{
[HttpGet("users")]
[Authorize(Roles = "Admin")]
public ActionResult<List<User>> GetAllUsers()
{
// Only admins can access this endpoint
return Ok(/* user list */);
}
[HttpDelete("users/{id}")]
[Authorize(Roles = "Admin")]
public ActionResult DeleteUser(int id)
{
// Only admins can delete users
return Ok(new { message = "User deleted successfully" });
}
[HttpGet("stats")]
[Authorize(Roles = "Admin,Moderator")]
public ActionResult GetStats()
{
// Both admins and moderators can access this
return Ok(/* statistics */);
}
}
Multiple Roles
[HttpGet("moderate")]
[Authorize(Roles = "Admin,Moderator")]
public ActionResult ModerateContent()
{
// Users with Admin OR Moderator role can access
return Ok(new { message = "Content moderated" });
}
[HttpGet("super-admin")]
[Authorize(Roles = "Admin")]
[Authorize(Roles = "SuperAdmin")]
public ActionResult SuperAdminOnly()
{
// Users must have BOTH Admin AND SuperAdmin roles
return Ok(new { message = "Super admin access granted" });
}
Policy-based Authorization
// In Program.cs
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("MinimumAge", policy =>
policy.RequireAssertion(context =>
{
var ageClaim = context.User.FindFirst("Age");
if (ageClaim != null && int.TryParse(ageClaim.Value, out int age))
{
return age >= 18;
}
return false;
}));
options.AddPolicy("CanEditProfile", policy =>
policy.RequireAssertion(context =>
{
var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var requestedUserId = context.Resource as string;
return userId == requestedUserId || context.User.IsInRole("Admin");
}));
});
// In controller
[HttpPut("profile/{userId}")]
[Authorize(Policy = "CanEditProfile")]
public ActionResult UpdateProfile(string userId, UpdateProfileRequest request)
{
// Users can only edit their own profile, unless they're admin
return Ok(new { message = "Profile updated" });
}
Exercise 4: Role-based Authorization
- Create an
AdminController
with role-protected endpoints - Implement endpoints that require different roles
- Create a policy that allows users to edit only their own profiles
- Test role-based access with different user roles
- Verify that users without required roles get 403 Forbidden responses
Testing Authentication
Testing with Postman
- Register a new user:
POST /api/auth/register Content-Type: application/json { "username": "testuser", "email": "test@example.com", "password": "password123" }
- Login to get JWT token:
POST /api/auth/login Content-Type: application/json { "username": "testuser", "password": "password123" }
- Use JWT token in protected requests:
GET /api/user/profile Authorization: Bearer <your-jwt-token>
JWT Token Structure Analysis
- Decode JWT tokens at jwt.io
- Examine the payload to see user claims
- Verify token expiration and issuer information
Testing Different Scenarios
- Valid token: Should return 200 OK
- Invalid token: Should return 401 Unauthorized
- Expired token: Should return 401 Unauthorized
- Missing token: Should return 401 Unauthorized
- Wrong role: Should return 403 Forbidden
Exercise 5: Testing Authentication
- Create a Postman collection for testing authentication
- Test user registration with valid and invalid data
- Test login with correct and incorrect credentials
- Test protected endpoints with valid JWT tokens
- Test role-based endpoints with different user roles
- Verify error responses for unauthorized access
Security Best Practices
Password Security
- Use strong password requirements
- Hash passwords with bcrypt or similar
- Never store plain-text passwords
- Implement password reset functionality
// Password hashing
public string HashPassword(string password)
{
return BCrypt.Net.BCrypt.HashPassword(password);
}
public bool VerifyPassword(string password, string hash)
{
return BCrypt.Net.BCrypt.Verify(password, hash);
}
JWT Security
- Use strong secret keys (at least 32 characters)
- Set appropriate token expiration times
- Use HTTPS in production
- Implement token refresh mechanism
- Consider token blacklisting for logout
Input Validation
public class RegisterRequest
{
[Required]
[StringLength(50, MinimumLength = 3)]
[RegularExpression(@"^[a-zA-Z0-9_]+$")]
public string Username { get; set; }
[Required]
[EmailAddress]
public string Email { get; set; }
[Required]
[StringLength(100, MinimumLength = 8)]
[RegularExpression(@"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]")]
public string Password { get; set; }
}
Rate Limiting
// In Program.cs
builder.Services.AddRateLimiter(options =>
{
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(context =>
RateLimitPartition.GetFixedWindowLimiter(
partitionKey: context.User.Identity?.Name ?? context.Request.Headers.Host.ToString(),
factory: partition => new FixedWindowRateLimiterOptions
{
AutoReplenishment = true,
PermitLimit = 100,
Window = TimeSpan.FromMinutes(1)
}));
});
// In controller
[EnableRateLimiting("fixed")]
[HttpPost("login")]
public async Task<ActionResult<AuthResponse>> Login(LoginRequest request)
{
// Login logic
}
Advanced authentication
- Refresh tokens: You can have long-lived refresh tokens for automatic re-authentication, and short-lived access tokens for API calls
- Secure token storage and rotation
- OAuth 2.0 Integration
- Social login with Google, Facebook, etc.
- Third-party application authorization
- Multi-factor authentication (MFA)
- SMS/email-based verification, Authenticator app integration, backup codes...
- API Key authentication
- Alternative to JWT for API access
- Key-based authentication for services with rate limiting per API key
Resources
- JWT.io: JWT token decoder and debugger
- ASP.NET Core Security Documentation
- OAuth 2.0 Specification
- OWASP Authentication Cheat Sheet
- C# corner: JWT Token creation tutorial
- ASP.NET Core: Configure JWT bearer authentication
- Remember, if you don't restrict access to your endpoints, something like
this might happen