From cc239a6c5896d0fd88ce1231aed20348b83e9d0b Mon Sep 17 00:00:00 2001 From: borb Date: Sun, 13 Jul 2025 17:32:10 +0300 Subject: [PATCH] add lecture 7 initial version, update readme --- ...thentication-and-authorization-slides.html | 810 ++++++++++++++++++ 7-authentication-and-authorization.md | 757 +++++++++++++++- README.md | 4 +- 3 files changed, 1557 insertions(+), 14 deletions(-) create mode 100644 7-authentication-and-authorization-slides.html diff --git a/7-authentication-and-authorization-slides.html b/7-authentication-and-authorization-slides.html new file mode 100644 index 0000000..96d0aa8 --- /dev/null +++ b/7-authentication-and-authorization-slides.html @@ -0,0 +1,810 @@ +7. Authentication and Authorization
+

Authentication and Authorization

+
+
+

Contents

+ +
+
+

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 token
  • +
  • sub (subject): Who the token is for
  • +
  • exp (expiration): When the token expires
  • +
  • iat (issued at): When the token was issued
  • +
+
+
+

Custom claim examples

+
    +
  • userId: User's unique identifier
  • +
  • email: User's email address
  • +
  • role: 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

+ +
    +
  1. Create a new ASP.NET Core Web API project
  2. +
  3. Install the required NuGet packages
  4. +
  5. Configure JWT authentication in Program.cs
  6. +
  7. Add JWT settings to appsettings.json
  8. +
  9. Test that the application starts without errors
  10. +
+
+
+

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

+ +
    +
  1. Create the User model and DTOs
  2. +
  3. Implement the JwtService class
  4. +
  5. Create the AuthController with register and login endpoints
  6. +
  7. Implement password hashing using BCrypt.Net.BCrypt
  8. +
  9. Test the registration and login endpoints using Postman
  10. +
+
+
+

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

+ +
    +
  1. Create a UserController with protected endpoints
  2. +
  3. Implement an endpoint that returns the current user's profile
  4. +
  5. Create a custom authorization attribute for admin users
  6. +
  7. Test the endpoints with and without valid JWT tokens
  8. +
  9. Verify that unauthorized requests return 401 status codes
  10. +
+
+
+

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

+ +
    +
  1. Create an AdminController with role-protected endpoints
  2. +
  3. Implement endpoints that require different roles
  4. +
  5. Create a policy that allows users to edit only their own profiles
  6. +
  7. Test role-based access with different user roles
  8. +
  9. Verify that users without required roles get 403 Forbidden responses
  10. +
+
+
+

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

+ +
    +
  1. Create a Postman collection for testing authentication
  2. +
  3. Test user registration with valid and invalid data
  4. +
  5. Test login with correct and incorrect credentials
  6. +
  7. Test protected endpoints with valid JWT tokens
  8. +
  9. Test role-based endpoints with different user roles
  10. +
  11. Verify error responses for unauthorized access
  12. +
+
+
+

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

+ +
+
\ No newline at end of file diff --git a/7-authentication-and-authorization.md b/7-authentication-and-authorization.md index 344b81d..1a4cd0c 100644 --- a/7-authentication-and-authorization.md +++ b/7-authentication-and-authorization.md @@ -1,16 +1,749 @@ -# Authentication & Authorization +--- +marp: true +paginate: true +math: mathjax +theme: buutti +title: 7. Authentication and Authorization +--- -__Kato täältä esimerkki__ +# Authentication and Authorization -* Authentication: Verifying that you are indeed you -* Authorization: Determining access rights based on your privileges -* If you don't restrict access to your endpoints, something like [this](https://mastodon.gamedev.place/@badlogic/111246798083590676) [might happen](https://firesky.tv/) !!! -* Tee loput luennot tän esimerkin mukaan: - * [https://www.c-sharpcorner.com/article/jwt-token-creation-authentication-and-authorization-in-asp-net-core-6-0-with-po/](https://www.c-sharpcorner.com/article/jwt-token-creation-authentication-and-authorization-in-asp-net-core-6-0-with-po/) + + -Esimerkkisisältö: Tehtävänanto +## Contents -* Make a simple HTML website using the template from the previous slide, so that it contains paragraphs, headings, images and links. -* Add at least two links; one remote and one local. - * How would you make an image to be a link? -* Later on, we will add more stuff to the site. +- [Introduction to Authentication & Authorization](#introduction-to-authentication--authorization) +- [JWT Tokens](#jwt-tokens) +- [Setting up Authentication in ASP.NET Core](#setting-up-authentication-in-aspnet-core) +- [User Registration and Login](#user-registration-and-login) +- [Protected Endpoints](#protected-endpoints) +- [Role-based Authorization](#role-based-authorization) +- [Testing Authentication](#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](https://auth0.com/docs/secure/tokens/json-web-tokens) (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](https://auth0.com/docs/secure/tokens/json-web-tokens/json-web-token-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 token +* `sub` (subject): Who the token is for +* `exp` (expiration): When the token expires +* `iat` (issued at): When the token was issued + +
+
+ +#### Custom claim examples + +* `userId`: User's unique identifier +* `email`: User's email address +* `role`: 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: + ```bash + Microsoft.AspNetCore.Authentication.JwtBearer + Microsoft.IdentityModel.Tokens + System.IdentityModel.Tokens.Jwt + ``` + +### Configuration in appsettings.json + +```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](https://www.random.org/strings) + +### Program.cs Configuration + +```csharp +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 + + +1) Create a new ASP.NET Core Web API project +2) Install the required NuGet packages +3) Configure JWT authentication in `Program.cs` +4) Add JWT settings to `appsettings.json` +5) Test that the application starts without errors + +## User Registration and Login + +### User Model + +```csharp +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) + +```csharp +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 + +```csharp +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 + +```csharp +[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> 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> 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 + + +1) Create the `User` model and DTOs +2) Implement the `JwtService` class +3) Create the `AuthController` with register and login endpoints +4) Implement password hashing using `BCrypt.Net.BCrypt` +5) Test the registration and login endpoints using Postman + +## Protected Endpoints + +### Using [Authorize] Attribute + +```csharp +[ApiController] +[Route("api/[controller]")] +public class UserController : ControllerBase +{ + [HttpGet("profile")] + [Authorize] // Requires authentication + public ActionResult 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 + +```csharp +[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 + +```csharp +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 + + +1) Create a `UserController` with protected endpoints +2) Implement an endpoint that returns the current user's profile +3) Create a custom authorization attribute for admin users +4) Test the endpoints with and without valid JWT tokens +5) 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 + +```csharp +[ApiController] +[Route("api/[controller]")] +public class AdminController : ControllerBase +{ + [HttpGet("users")] + [Authorize(Roles = "Admin")] + public ActionResult> 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 + +```csharp +[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 + +```csharp +// 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 + + +1) Create an `AdminController` with role-protected endpoints +2) Implement endpoints that require different roles +3) Create a policy that allows users to edit only their own profiles +4) Test role-based access with different user roles +5) 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 + ``` + +
+
+ + + +### JWT Token Structure Analysis + +* Decode JWT tokens at [jwt.io](https://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 + + +1) Create a Postman collection for testing authentication +2) Test user registration with valid and invalid data +3) Test login with correct and incorrect credentials +4) Test protected endpoints with valid JWT tokens +5) Test role-based endpoints with different user roles +6) 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 + +```csharp +// 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 + +```csharp +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 + +```csharp +// In Program.cs +builder.Services.AddRateLimiter(options => +{ + options.GlobalLimiter = PartitionedRateLimiter.Create(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> 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](https://jwt.io): JWT token decoder and debugger +* [ASP.NET Core Security Documentation](https://docs.microsoft.com/en-us/aspnet/core/security/) +* [OAuth 2.0 Specification](https://tools.ietf.org/html/rfc6749) +* [OWASP Authentication Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html) +* [C# corner: JWT Token creation tutorial](https://www.c-sharpcorner.com/article/jwt-token-creation-authentication-and-authorization-in-asp-net-core-6-0-with-po/) +* [ASP.NET Core: Configure JWT bearer authentication](https://learn.microsoft.com/en-us/aspnet/core/security/authentication/configure-jwt-bearer-authentication?view=aspnetcore-9.0) +* Remember, if you don't restrict access to your endpoints, something like +[this](https://mastodon.gamedev.place/@badlogic/) [might happen](https://firesky.tv/) \ No newline at end of file diff --git a/README.md b/README.md index f87d2e3..0101627 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ | 3 | [MVP Pattern and Repositories](3-mvc-pattern-and-repositories.md) | [Download slides](3-mvc-pattern-and-repositories-slides.html?raw=1) | | 4 | [RESTful HTTP Methods](4-restful-http-methods.md) | [Download slides](4-restful-http-methods-slides.html?raw=1) | | 5 | [Model Validation & API Design](5-model-validation-and-designing-apis.md) | [Download slides](5-model-validation-and-designing-apis-slides.html?raw=1) | -| | [SQL Databases](sql-databases.md) | [Download slides](sql-databases-slides.html?raw=1) | +| | [SQL Databases](sql-databases.md) | [Download slides](5-model-validation-and-designing-apis-slides.html?raw=1) | | 6 | [Databases with Entity Framework](6-databases-with-entity-framework.md) | [Download slides](6-databases-with-entity-framework-slides.html?raw=1) | -| 7 | [Authentication & Authorization](7-authentication-and-authorization.md) | [Download slides](7-authentication-and-authorization-slides.html?raw=1) | +| 7 | [Authentication and Authorization](7-authentication-and-authorization.md) | [Download slides](7-authentication-and-authorization-slides.html?raw=1) | | 8 | [Testing](8-testing.md) | [Download slides](8-testing-slides.html?raw=1) |