AuthDemo is a .NET Web API application designed to provide a practical learning demonstration of implementing authorization and authentication mechanisms in a .NET Web API application using JWT. Additionally, it showcases chore management functionalities, serving as an educational example of integrating these features into simulated and some real-world scenarios.
- App Overview
- ASP.NET Authentication and Authorization
- ASP.NET Core Identity
- Getting Started
- API Endpoints
- API Endpoints Details
- Data Objects
- Architecture Overview
- Technical Highlights
- Future Enhancements
- Contributions
- Acknowledgments
- Contact
AuthDemo application provides secure authentication, allowing users to log in safely with assigned roles such as administrators, managers, or employees.
Users can efficiently manage chores, user accounts, and access permissions, while audit tracking ensures transparency across all actions within the system.
Capability | Description |
---|---|
Secure Authentication | Users can securely log in to our application using JWT tokens. Upon successful authentication, a JWT token is issued, allowing users access to protected resources throughout their session. |
Role-Based Authorization | Users can be assigned one of three roles: Administrator, Manager, or Employee, each with different levels of access and permissions within the application. |
Chore Management | All users can view all chores and mark them as finished. Managers and Admins can approve chores, assign others or themselves to specific chores. Additionally, they have the authority to create, edit, and delete chores. |
User Management | Users can view all users or specific user profiles. Admins can create new user accounts and activate or deactivate existing ones. Users can also modify their own email addresses and passwords, as well as those of other users, if granted appropriate permissions. |
Token Management | Users have the ability to invalidate their specific token, all of their tokens, or the tokens of other users if they have administrative privileges. This feature enhances security measures, allowing users to maintain control over access to their accounts. |
Audit tracking | Audit tracking of entities is implemented to track changes made withing AuthDemo application. |
Authentication is confirming a user's identity, a process in which a user provides credentials that are compared to those stored in an operating system, database, app or resource.
Authorization means checking if an authenticated user is allowed to access a resource.
On each request, we get the user from HttpContext as a ClaimsPrincipal.
HttpContext is like the environment in which the app is running, and it contains info about the current request and response.
ClaimsPrincipal is a collection of user's identities (ClaimsIdentity), each identity is containing user's claims.
Identity is like a document that proves who you are, identity is represented by set of information about the user (age, name, role), each of those infos is a Claim.
Sometimes a user can have more identities, for example:
Let's say that you are an employee but also have a gym membership. One identity (employee card) is a set of claims that represent your employee identity which contains claims like: emplyee Id, role, status etc.
Other identity (gym card) represent your gym identity and contains claims like membership Id, membership type etc. In real life scenario, a passport would be an identity that proves your nationality.
In ASP.NET Core, authentication is handled by authentication service (IAuthenticationService) and authentication middleware. Authentication service uses authentication handlers, called "schemes", to perform tasks like user authentication and handling unauthorized access attempts.
Authentication services are registered in Program.cs (builder.Services.AddAuthentication()).
Schemes are registered by calling specific methods like AddJwtBearer or AddCookie after AddAuthentication in Program.cs.
Example that defines using schemes that allows us to use both Cookies and JWT tokens for authentication from official Microsoft documentation):
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme,
options => builder.Configuration.Bind("JwtSettings", options))
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme,
options => builder.Configuration.Bind("CookieSettings", options));
The Authentication middleware is added to Program.cs by calling UseAuthentication.
Calling UseAuthentication registers the middleware that uses the previously registered authentication schemes.
Call UseAuthentication before any middleware that depends on users being authenticated.
It is a setup of how authentication will work in the application. It involves defining mechanisms used to verify a user's identity. Examples:
- Setting up JWT (JSON Web Token) authentication.
- Configuring thg JWT bearer auth scheme
- Specify options like token validation parameters, token expiration, issuer, audience, signing key.
- Setting up Cookie based authentication
- Configuring Cookie name, expiration time, login path, etc.
- Setting up external authentication providers like Oauth 2.0, OIDC (Facebook, Twitter/X, Google, Microsoft or other)
- Configuring connections using industry-defined protocols between our application and other unrelated applications to share user profile information.
Configuration of AuthDemo JWT bearer scheme:
TokenValidationParameters tokenValidationParameters = new TokenValidationParameters()
{
ValidateIssuer = true,
ValidIssuer = configuration.GetSection(nameof(JwtSettings))["Issuer"],
ValidateAudience = true,
ValidAudience = configuration.GetSection(nameof(JwtSettings))["Audience"],
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(secretKey),
ValidateLifetime = true,
RequireExpirationTime = true,
ClockSkew = TimeSpan.Zero
};
services.AddAuthentication(configureOptions =>
{
configureOptions.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
configureOptions.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
configureOptions.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(jwtBearerOptions =>
{
//...
jwtBearerOptions.Events = new JwtBearerEvents
{
OnTokenValidated = context => {
long.TryParse(context.Principal.FindFirstValue(ClaimTypes.NameIdentifier), out long userId);
string? tokenId = context.Principal.FindFirstValue(JwtRegisteredClaimNames.Jti);
// ...
var result = tokenService.IsAccessTokenCached(tokenId, userId).Result;
if (!result)
{
context.Fail("Access token expired. Use the refresh token to obtain a new access token.");
}
return Task.CompletedTask;
}
};
});
Authentication scheme actions:
Action | Description |
---|---|
Authenticate | Responsible for constructing user's identity. Cookie authentication scheme constructs user's identity from cookies. JWT bearer scheme deserializes and validates JWT token to construct user's identity. |
Challenge | Invoked when an unauthenticated user requests resource from enpoint that requires authentication. Cookie auth scheme will redirect the user to login page. JWT bearer scheme will return a 401 result with a www-authenticate: bearer header. |
Forbid | Invoked when user is authenticated but not permitted to access. Cookie auth scheme will redirect the user to a page indicating that access was forbidden. JWT bearer scheme will return 403 result. |
You can use a combination of:
- Policy (a rule or a requirement that has to be satisfied) based authorization.
- Claim (a part of identity) based authorization.
- Role (user's role, like admin) based authorization.
Registering the policy takes place as part of the Authorization service configuration, typically in the Program.cs.
Role-based example from official Microsoft documentation:
builder.Services.AddAuthorization(options => {
options.AddPolicy("RequireAdministratorRole",
policy => policy.RequireRole("Administrator"));
});
//...
app.UseAuthorization();
Policy-based example from official Microsoft documentation:
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("AtLeast21", policy =>
policy.Requirements.Add(new MinimumAgeRequirement(21)));
});
Claim-based authorization in AuthDemo (based on a user's role in Claim that are part of the JWT bearer token):
options.AddPolicy(AuthDemoPolicies.Roles.Admin, policy =>
{
policy.RequireClaim(ClaimTypes.Role, Roles.Administrator.GetValue());
});
options.AddPolicy(AuthDemoPolicies.Roles.Manager, policy =>
{
policy.RequireClaim(ClaimTypes.Role, Roles.Manager.GetValue());
});
options.AddPolicy(AuthDemoPolicies.Roles.AdminOrManager, policy =>
{
policy.RequireClaim(
ClaimTypes.Role,
Roles.Administrator.GetValue(),
Roles.Manager.GetValue());
});
To apply the policy, use the Policy property on the [Authorize] attribute:
[Authorize(Policy = Policies.Roles.AdminOrManager)]
It is a ASP.NET CORE built-in authentication provider.
It is an API that supports user interface login functionality, manages users, user registration, passwords, profile data, roles, claims, tokens, email configuration and more.
You do not need to use it, you can create your custom User and Role classes and use dbContext to do DB operations.
By default, it creates tables in your DB using EntityFramework. Running your first migrations, you can see these tables:
Table |
---|
dbo.AspNetRoleClaims |
dbo.AspNetRoles |
dbo.AspNetUserClaims |
dbo.AspNetUserLogins |
dbo.AspNetUserRoles |
dbo.AspNetUsers |
dbo.AspNetUserTokens |
The most important for AuthDemo are AspNetUsers and AspNetRoles. You can customize those tables and add new fields, for example in AuthDemo, a user can have only one role, but ASP.NET Identity by default defines multiple roles per user.
More info about the tables and fields they contain can be found here.
Functionalities:
- UserManager class in ASP.NET Core Identity interacts with the AspNetUsers table to handle user-related operations like creation, deletion, updating profiles, etc.
- SignInManager class in ASP.NET Core Identity handles sign-in operations, including password checks, cookie management, etc.
- RoleManager class in ASP.NET Core Identity provides the RoleManager class to manage roles. This class is used to create, delete, and update roles and to assign roles to users.
For more info about ASP.NET Identity, check out this link.
You are free to not use ASP.NET Core Identity and use your custom users, roles, etc. I used it because it simplifies the validation of user registration, login, configuring password rules and other security functionality.
ASP.NET Core Identity vs IdentityServer
ASP.NET Core Identity is not related to Duende IdentityServer or Microsoft Entra ID (formerly Azure Active Directory).
These are frameworks that act as an authentication and authorization server and provide centralized authentication and single sign-on (SSO) capabilities across multiple applications and services.
Possible implementation for demonstration of a separate Authentication and Authorization OIDC Server:
- Authentication and Authorization is integrated into AuthDemo as part of Security project/ class library.
We could customize and exclude Security project from AuthDemo and move it to separate Web API application that will act as a centralized auth server for AuthDemo and any other app. AuthDemo would communicate with that IdentityServer to authorize and authenticate user. That new IdentityServer could use ASP.NET Core Identity to manage users, tokens, roles etc.
To get started with the tutorial, follow these steps:
-
Clone the Repository: Clone this repository to your local machine using the following command:
git clone https://github.com/domkris/AuthDemo.git
-
Navigate to the Project Directory: Change your directory to the cloned repository:
-
Install Dependencies: Install the required dependencies using NuGet Package Manager:
dotnet restore
-
Run the Application: Start the AuthDemo.Web API application:
dotnet run
-
Or Run Using Docker Compose: Start the application using Docker Compose:
docker-compose up
-
Explore the Endpoints: Once the application is running, you can explore the available API endpoints. Refer to the API Endpoints section below for a summary list of endpoints and their descriptions.
-
Interact with Endpoints: Use tools like Postman, curl or the built-in Swagger UI to interact with the endpoints. You can send requests to endpoints such as login, logout, create users, manage chores, etc.
-
Follow the Tutorial: For a detailed understanding of how authorization and authentication are implemented within the application, refer to the tutorial provided in the repository. The tutorial offers step-by-step guidance and explanations on key concepts and functionalities.
-
Explore Endpoint Details: Each endpoint has detailed explanations provided in the repository. Refer to the API Endpoints Details to understand the purpose and usage of each endpoint effectively.
By following these steps, you'll be able to navigate through AuthDemo, understand its functionalities, and gain insights into implementing authorization and authentication mechanisms within your own .NET Web API applications.
Endpoint | Description | Method |
---|---|---|
/api/auth/login | Login | POST |
/api/auth/logout | Logout | POST |
/api/auth/logoutAllSessions | Logout from all Sessions | POST |
/api/auth/changePassword | Change User's password | POST |
/api/auth/changeEmail | Change User's email | POST |
/api/auth/toggleUserActivation/{id} | Activate/Deactivate user | POST |
/api/authTokens/refreshTokens | Request a new Access Token | POST |
/api/authTokens/invalidateUserTokens/{id} | Invalidate all User's tokens | POST |
/api/chores | Get all chores | GET |
/api/chores | Create new chore | POST |
/api/chores/{id} | Get specific chore | GET |
/api/chores/{id} | Update specific chore | PUT |
/api/chores/{id} | Delete specific chore | DELETE |
/api/chores/assignUser | Assing User to chore | PUT |
/api/chores/finish/{id} | Finish chore | PUT |
/api/chores/approve/{id} | Approve chore | PUT |
/api/users | Create a user | POST |
/api/users | Get all users | GET |
/api/users/{id} | Get specific user | GET |
Click to expand!
POST |
api/Auth/Login |
EndPoint Authorization: None/Anonymous |
|
|
|||
Request {
"email": "admin@authdemo.com",
"password": "12345678"
} |
Response 200 OK: {
"accessToken": "****",
"refreshToken": "****"
} |
Response 400 Bad Request on Model Validation Response 400 Bad Request: Response 400 Bad Request: |
Click to expand!
POST |
api/Auth/Logout |
EndPoint Authorization: Authorized |
|
|
|||
Request |
Response 200 OK: |
Response 400 Bad Request Response 400 Bad Request Response 401 Unauthorized |
Click to expand!
POST |
api/Auth/LogoutAllSessions |
EndPoint Authorization: Authorized |
|
|
|||
Request |
Response 200 OK: |
Response 400 Bad Request Response 400 Bad Request Response 401 Unauthorized |
Click to expand!
POST |
api/Auth/ChangePassword |
EndPoint Authorization: Authorized |
|
|
|||
Request {
"userId": 0,
"currentPassword": "string",
"newPassword": "stringst",
"confirmNewPassword": "string"
} |
Response 200 OK |
Response 400 Bad Request on Model Validation **Response 400 Bad Request ** Response 404 Not Found: Response 403 Forbidden Response 401 Unauthorized |
Click to expand!
POST |
api/Auth/ChangeEmail |
EndPoint Authorization: Authorized |
|
|
|||
Request {
"userId": 0,
"currentEmail": "user@example.com",
"newEmail": "user@example.com"
} |
Response 200 OK |
Response 400 Bad Request on Model Validation **Response 400 Bad Request ** Response 404 Not Found: Response 400 Bad Request: Response 400 Bad Request: Response 403 Forbidden Response 401 Unauthorized |
Click to expand!
POST |
api/Auth/ToggleUserActivation/{id} |
EndPoint Authorization: Authorized |
|
|
|||
Request |
Response 200 OK |
Response 404 Not Found **Response 400 Bad Request ** Response 401 Unauthorized Response 403 Forbidden |
Click to expand!
POST |
api/AuthTokens/RefreshToken |
EndPoint Authorization: None/Anonymous |
|
|
|||
Request {
"accessToken": "string",
"refreshToken": "string"
} |
Response 200 OK {
"accessToken": "****",
"refreshToken": "****"
} |
**Response 400 Bad Request ** Response 401 Unauthorized: |
Click to expand!
PUT |
api/AuthTokens/InvalidateUserTokens/{id} |
EndPoint Authorization: Authorized |
|
|
|||
Request |
Response 200 OK |
Response 404 Not Found: Response 403 Forbidden Response 401 Unauthorized |
Click to expand!
GET |
api/Chores |
EndPoint Authorization: Authorized |
|
|
|||
Request |
Response 200 OK [
{
"id": 1,
"title": "Chore 1",
"description": "chore chore 1",
"isFinished": false,
"isApproved": false,
"createdBy": {
"id": 3,
"username": "alicemanager",
"email": "alice@authdemo.com"
},
"updatedBy": null,
"userAssignee": null,
"createdAt": "2024-04-05T08:27:38.009824+00:00",
"updatedAt": null
},
{
"id": 2,
"title": "Chore 2",
"description": "chore desc2",
"isFinished": false,
"isApproved": false,
"createdBy": {
"id": 3,
"username": "alicemanager",
"email": "alice@authdemo.com"
},
"updatedBy": null,
"userAssignee": null,
"createdAt": "2024-04-05T08:29:04.774988+00:00",
"updatedAt": null
}
] |
Response 401 Unauthorized |
Click to expand!
POST |
api/Chores |
EndPoint Authorization: Authorized |
|
|
|||
Request {
"title": "Chore 1",
"description": "Chore 1 Desc"
} |
Response 201 Created {
"id": "3"
} |
Response 400 Bad Request on Model Validation Response 401 Unauthorized Response 403 Forbidden |
Click to expand!
GET |
api/Chores/{id} |
EndPoint Authorization: Authorized |
|
|
|||
Request |
Response 200 OK {
"id": 2,
"title": "Chore 2",
"description": "chore desc2",
"isFinished": false,
"isApproved": false,
"createdBy": {
"id": 3,
"username": "alicemanager",
"email": "alice@authdemo.com"
},
"updatedBy": null,
"userAssignee": null,
"createdAt": "2024-04-05T08:29:04.774988+00:00",
"updatedAt": null
} |
Response 401 Unauthorized Response 404 Not Found |
Click to expand!
PUT |
api/Chores/{id} |
EndPoint Authorization: Authorized |
|
|
|||
Request {
"title": "Chore 1A",
"description": "Chore 1A Desc"
} |
Response 200 OK |
Response 400 Bad Request on Model Validation Response 404 Not Found Response 401 Unauthorized Response 403 Forbidden |
Click to expand!
DELETE |
api/Chores/{id} |
EndPoint Authorization: Authorized |
|
|
|||
Request |
Response 200 OK |
Response 404 Not Found Response 401 Unauthorized Response 403 Forbidden |
Click to expand!
PUT |
api/Chores/AssignUser |
EndPoint Authorization: Authorized |
|
|
|||
Request {
"choreId": 0,
"userId": 0
} |
Response 200 OK |
Response 400 Bad Request on Model Validation Response 404 Not Found Response 404 Not Found Response 401 Unauthorized Response 403 Forbidden |
Click to expand!
PUT |
api/Chores/Finish/{id} |
EndPoint Authorization: Authorized |
|
|
|||
Request |
Response 200 OK |
Response 404 Not Found Response 401 Unauthorized Response 403 Forbidden |
Click to expand!
PUT |
api/Chores/Approve/{id} |
EndPoint Authorization: Authorized |
|
|
|||
Request |
Response 200 OK |
Response 404 Not Found Response 401 Unauthorized Response 403 Forbidden |
Click to expand!
GET |
api/Users |
EndPoint Authorization: Authorized |
|
|
|||
Request |
Response 200 OK [
{
"id": 2,
"username": "adminauthdemo",
"email": "admin@authdemo.com",
"createdBy": {
"id": 1,
"username": "system",
"email": null
},
"updatedBy": null,
"createdAt": null,
"updatedAt": null
},
{
"id": 3,
"username": "alicemanager",
"email": "alice@authdemo.com",
"createdBy": {
"id": 2,
"username": "adminauthdemo",
"email": "admin@authdemo.com"
},
"updatedBy": null,
"createdAt": "2024-04-04T19:09:18.463805+00:00",
"updatedAt": null
},
{
"id": 4,
"username": "bobemployee",
"email": "bob@authdemo.com",
"createdBy": {
"id": 2,
"username": "adminauthdemo",
"email": "admin@authdemo.com"
},
"updatedBy": null,
"createdAt": "2024-04-04T19:12:44.189219+00:00",
"updatedAt": null
}
] |
Response 401 Unauthorized |
Click to expand!
POST |
api/Users |
EndPoint Authorization: Authorized |
|
|
|||
Request {
"firstName": "string",
"lastName": "string",
"email": "user@example.com",
"password": "stringst",
"confirmPassword": "string",
"role": 1
} // not a part of request
public enum Role
{
Administrator = 1,
Manager = 2,
Employee = 3
} |
Response 200 OK |
Response 400 Bad Request on Model Validation Response 400 Bad Request Response 401 Unauthorized Response 403 Forbidden |
Click to expand!
GET |
api/Users/{id} |
EndPoint Authorization: Authorized |
|
|
|||
Request |
Response 200 OK {
"id": 4,
"username": "bobemployee",
"email": "bob@authdemo.com",
"createdBy": {
"id": 2,
"username": "adminauthdemo",
"email": "admin@authdemo.com"
},
"updatedBy": null,
"createdAt": "2024-04-04T19:12:44.189219+00:00",
"updatedAt": null
} |
Response 401 Unauthorized Response 404 Not Found |
AuthDemo Access Token is a JWT Token. JWT stands for JSON Web Token, open industry standard RFC 7519 method for representing claims securely between two parties.
It is like a digital passport that helps Server recognize who you are (Authentication) and what you are allowed to do (Authorization).
JWT Token contains Claims which are pieces of information about the user like: user role, user email, user Id, token expiration, etc.
JWT Token has three components: Header, Payload and Signature.
- Header typically consists of two parts: the type of token (JWT) and the signing algorithm being used, such as HMAC SHA256 or RSA.
- Payload: The payload contains the claims. Claims are statements about an entity (typically, the user) and additional data.
- Signature: The signature is created by combining the encoded header, encoded payload, and a secret (or private key) using the specified algorithm. This signature verifies that the sender of the JWT is who it says it is and ensures that the message wasn't changed along the way.
Encoded JWT Token looks like:
{
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiYWRtaW5hdXRoZGVtbyIsImh0dHA6Ly9zY2hlbWFzLnhtbHNvYXAub3JnL3dzLzIwMDUvMDUvaWRlbnRpdHkvY2xhaW1zL2VtYWlsYWRkcmVzcyI6ImFkbWluQGF1dGhkZW1vLmNvbSIsImh0dHA6Ly9zY2hlbWFzLnhtbHNvYXAub3JnL3dzLzIwMDUvMDUvaWRlbnRpdHkvY2xhaW1zL25hbWVpZGVudGlmaWVyIjoiMiIsImh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd3MvMjAwOC8wNi9pZGVudGl0eS9jbGFpbXMvcm9sZSI6IjEiLCJqdGkiOiJmMzk1NzBiNy01NTZjLTQ2MzEtYWU4OS1iZGY5Y2EwMGRjNzQiLCJleHAiOjE3MTE3MTQyMTEsImlzcyI6IkF1dGhEZW1vIiwiYXVkIjoiQXV0aERlbW8ifQ.azNf-2S6s8aNNrorRxaqP5YTn9Kc1mZq03Xh2ITJ6IM"
}
Decoded JWT Token looks like this (visit JWT.io to decode):
{
"header": {
"alg": "HS256",
"typ": "JWT"
},
"payload": {
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name": "adminauthdemo",
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress": "admin@authdemo.com",
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier": "2",
"http://schemas.microsoft.com/ws/2008/06/identity/claims/role": "1",
"jti": "f39570b7-556c-4631-ae89-bdf9ca00dc74",
"exp": 1711714211,
"iss": "AuthDemo",
"aud": "AuthDemo"
},
"signature": "HMACSHA256(base64UrlEncode(header) + \".\" + base64UrlEncode(payload), your-256-bit-secret)"
}
The Encoded/Decoded JWT Token is shared to the User, but on Server side we use Redis to store it as a key, value pair. Redis is an in-memory data structure, and it is not persistent if not configured to save data on a disk. In AuthDemo we do not care if our Redis instance crashes/loses data, it basically means that all Users will have to log in again.
Run this command in your Redis terminal:
redis-cli KEYS '*'
You should get a list of KEYs, Key looks like:
"authdemo:user:2:accesstoken:aa921d69-4f9d-4212-88f1-1bc3b8249c82"
Check this for naming Redis keys How to name Redis Keys.
Value is serialized json that is structured like this:
public sealed class UserAgentInfo
{
public required string BrowserName { get; set; }
public required string Version { get; set; }
public required string Platform { get; set; }
}
public class AccessToken
{
public required string UserId { get; set; }
public required string TokenId { get; set; }
public string? RefreshToken { get; set; }
public required DateTime TokenExpiration { get; set; }
public required TimeSpan TokenDuration { get; set; }
public UserAgentInfo? UserAgentInfo { get; set; }
}
Property UserAgenInfo contains info like browser and OS user is using to log in.
using System;
[ExcludeFromAuditLog]
public class Token : BaseEntity, IAuditableEntity
{
public long? UserId { get; set; }
public User? User { get; set; }
public string JwtAccessTokenId { get; set; } = default!;
public string RefreshToken { get; set; } = default!;
public DateTimeOffset Expires { get; set; }
public DateTimeOffset? Revoked { get; set; }
public string? ReplacedByRefreshToken { get; set; }
public string? ReasonRevoked { get; set; }
public bool IsExpired => DateTimeOffset.UtcNow >= Expires;
public bool IsRevoked => Revoked != null;
public bool IsActive => !IsRevoked && !IsExpired && !string.IsNullOrEmpty(ReplacedByRefreshToken);
public long? CreatedById { get; set; }
public User? CreatedBy { get; set; }
public long? UpdatedById { get; set; }
public User? UpdatedBy { get; set; }
public DateTimeOffset? CreatedAt { get; set; }
public DateTimeOffset? UpdatedAt { get; set; }
}
A Refresh token is long-lived random string token (on the UI side, in DB it is stored as a Token class) used in authentication system to requets a new short-lived access token when they expire.
AuthDemo Refresh Token contains properties that describe the User that RefreshToken is meant for User, Id of Access Token JwtAccessTokenId that was generated with that RefreshToken.
In AuthDemo, for each new Access Token we also create a new Refresh Token, old Refresh Token's property ReplacedByRefreshToken is set to new random string RefreshToken therefore we know that that Refresh Token has been used.
Properties Revoked, ReasonRevoked are used when User's role, email or password has been changed and we want the user to log in again. Property Expires defines a time and date when the Refresh Token will expire.
Refresh Token is not sent to the user as a whole Token class, we only send to the user property RefreshToken which looks like this:
{
"refreshToken": "A84SdiKyDfPgy2zzg1vs9uXueGr101s5uPOEa1DdKvfIsei4LI0L8UBndZE0zL2Hc/jzjbwSiw2mbZHFLMSapQ=="
}
public class User : IdentityUser<long>, IAuditableEntity
{
public string? FirstName { get; set; }
public string? LastName { get; set; }
[Required]
public long RoleId { get; set; }
public Role? Role { get; set; }
public bool IsActive { get; set; }
public long? CreatedById { get; set; }
public User? CreatedBy { get; set; }
public long? UpdatedById { get; set; }
public User? UpdatedBy { get; set; }
public DateTimeOffset? CreatedAt { get; set; }
public DateTimeOffset? UpdatedAt { get; set; }
}
User class represents the user. It inherits IdentityUser from ASP.NET Core Identity.
More info about ASP.NET Identity User can be found here.
public class Role : IdentityRole<long>
{
public virtual ICollection<User>? Users { get; set; }
}
Role class represents the role. It inherits IdentityRole from ASP.NET Core Identity.
More info about ASP.NET Identity Role can be found here.
public class Chore : BaseEntity, IAuditableEntity
{
[MinLength(3)]
public required string Title { get; set; }
public string? Description { get; set; }
public long? UserAssigneeId { get; set; }
public User? UserAssignee { get; set; }
public bool IsFinished { get; set; }
public bool IsApproved { get; set; }
public long? CreatedById { get; set; }
public User? CreatedBy { get; set; }
public long? UpdatedById { get; set; }
public User? UpdatedBy { get; set; }
public DateTimeOffset? CreatedAt { get; set; }
public DateTimeOffset? UpdatedAt { get; set; }
}
Chore class represents the chore.
It contains the fields like: the user assigned to it, isFinished, isApproved statuses, etc.
public abstract class BaseEntity
{
public long Id { get; set; }
}
Base entity is used to define the primary key of all entities in DB.
public interface IAuditableEntity
{
long? CreatedById { get; set; }
long? UpdatedById { get; set; }
DateTimeOffset? CreatedAt { get; set; }
DateTimeOffset? UpdatedAt { get; set; }
}
IAuditableEntity interface is part of the Audit functionality.
On each DELETE, UPDATE or CREATE of an entity in DB, it will update the corresponding properties of a class that implements IAuditableEntity.
Each change of object that implements IAuditableEntity will generate audit Logs in DB.
public class AuditLog : BaseEntity
{
public long? UserId { get; set; }
public User? User { get; set; }
public string? EntityType { get; set; }
public long? EntityId { get; set; }
public string? Action { get; set; }
public required DateTimeOffset CreatedAt { get; set; }
public virtual ICollection<AuditLogDetail> AuditLogDetails { get; set; } = new HashSet<AuditLogDetail>();
}
AuditLog class represents audit logs in DB.
Only classes that implement IAuditableEntity will be audited, if we do not want to audit a class that implements IAuditableEntity then add the attribute ExcludeFromAuditLogAttribute.
public class AuditLogDetail: BaseEntity
{
public required long AuditLogId { get; set; }
public required AuditLog AuditLog { get; set; }
public required string Property { get; set; }
public string? OldValue { get; set; }
public string? NewValue { get; set; }
}
AuditLogDetail class represents the details of each audit log in DB.
[AttributeUsage(AttributeTargets.Class)]
internal class ExcludeFromAuditLogAttribute : Attribute
{
}
This attribute is added to the classes that implement IAuditableEntity, but for which we do not want to generate audit logs.
AuthDemo follows a structured architecture to ensure modularity, scalability, and maintainability. Here's a breakdown of the main components:
Component | Purpose | Functionality |
---|---|---|
AuthDemo.Cache | Handles Redis JWT token caching. | Responsible for caching JWT tokens in Redis for efficient session management and token validation. |
AuthDemo.Contracts | Contains request and response objects used throughout the application. | Defines contracts and data transfer objects (DTOs) for communication between different layers of the application. |
AuthDemo.Domain | Houses all repositories and services responsible for interacting with the database. | Implements business logic and data access operations. |
AuthDemo.Infrastructure | Centralizes entities, audits, entity type configurations, lookup data, and migration files. | Provides infrastructure-related functionalities such as database entity definitions, database migrations, and data seeding. |
AuthDemo.Security | Manages security-related functionalities such as JWT settings, token creation, and authorization policies. | Defines JWT settings, generates JWT tokens, and enforces authorization policies based on user roles and claims. |
AuthDemo.Web | Contains controllers and automapper configurations for handling HTTP requests and responses. | Exposes RESTful API endpoints, maps requests to appropriate actions, and transforms data between DTOs and domain models. |
- JWT Authentication
Learn how to implement JSON Web Token (JWT) authentication in .NET Web API application. - Role-based Authorization
Explore role-based access control (RBAC) to restrict access to specific endpoints based on user roles. - Bearer Token Authorization with Claims
Understand how to use bearer tokens to authenticate and authorize requests and learn how to utilize claims to carry additional information about the user within JWTs. - Policies
Define and enforce custom authorization policies to control access to resources based on various conditions and requirements. - Access and Refresh Tokens
Implement access and refresh token functionality to manage user sessions securely and efficiently. - Redis Integration
Utilize Redis for storing and managing access tokens, improving scalability and session management. - Entity Framework
Utilize Entity Framework for data access and database management in your .NET Web API application. - PostgreSQL Integration
Integrate PostgreSQL as the database backend for your .NET Web API application. - Audit Log
Implement auditing functionality to track and log user actions such as additions, deletions, and modifications, enhancing transparency and accountability. - Docker Integration
Utilize Docker for containerization to ensure consistent deployment across different environments. - Docker Compose
Leverage Docker Compose for orchestrating multi-container Docker applications, simplifying the deployment process. - Demo Application
Get hands-on experience with a fully functional .NET Web API application showcasing these concepts.
Feature | Description |
---|---|
Pagination | Implementation of pagination support in API endpoints for efficient handling of large datasets. |
Global Error Handler | Centralized error handling for improved error logging, standardized error responses, and user feedback. |
Logging | Incorporation of logging mechanisms for monitoring, debugging, and better visibility into application behavior. |
Database Concurrency | Implementation of concurrency control mechanisms to prevent data inconsistency issues during concurrent access. |
Multi-Factor Authentication | Implementation of multifactor authentication for enhanced security by requiring additional verification steps during login. |
Contributions to this repository are welcome!
- Gifs and images created using Figma and Motion UI Figma plugin.
- Guided by Microsoft Security & Identity Documentation.
- Inspiration was drawn from tutorials and resources on authorization and authentication in .NET Web API primarily provided by Barry Dorrans and Mohamad Lawand, as well as various other contributors in the field.
- Special thanks to Sandi Zeher for helping me understand the concept of Refresh Tokens and explaining their practical application in real-life scenarios and projects.
For any inquiries or feedback, please contact me at domagojkk@gmail.com.