In this guide, we'll explore how to implement secure authentication and authorization in an ASP.NET Core application using ASP.NET Core Identity with Neon Postgres as the database backend. We'll cover user management, role-based authorization, and JWT token generation for secure API access.
Prerequisites
Before we begin, ensure you have:
- .NET 8.0 or later installed
- A Neon account
- Basic familiarity with ASP.NET Core and Entity Framework Core
Project Setup
First, create a new ASP.NET Core Web API project with authentication:
With the project created, install the necessary packages:
The above packages provide support for ASP.NET Identity, JWT authentication, and PostgreSQL database integration.
Configuring the Neon Database
Head over to your Neon Dashboard and create a new project.
Once done, grab your database connection string and add it to your appsettings.json
:
The appsettings.json
file is great for local development, but you should use environment variables or a secure vault for production.
While editing appsettings.json
, add JWT configuration as well right below the connection string:
The Key
is a secret key used to sign and verify JWT tokens, while Issuer
and Audience
are used to validate the token's origin and intended recipient.
Configuring ASP.NET Identity with Neon
In order to store additional information about users, we need to create a custom user class that extends the default Identity user provided by ASP.NET Core Identity.
This will allow us to add new properties, like FirstName
and LastName
, that are not included in the default IdentityUser
class.
Custom User Model
Let's create a new file named ApplicationUser.cs
inside the Models
folder with the following content:
In the code above:
-
By inheriting from the
IdentityUser
class,ApplicationUser
gets all the built-in properties likeUserName
,Email
,PasswordHash
, and more. This means we don't have to rewrite any of the existing authentication logic. -
We added three new fields:
FirstName
andLastName
allow us to store the user's personal details. AndCreatedAt
captures the date and time when the user was created, which can be helpful for tracking new sign-ups.
Why extend the IdentityUser
? Well, the default user model is quite limited, and many real-world applications need to store more information than just usernames and emails. By creating a custom ApplicationUser
, you can add more fields as you need to fit your application's requirements.
Database Context Configuration
A database context is a class that represents a session with the database, allowing us to query and save data. In our case, we're setting up a context specifically for handling ASP.NET Identity and our custom ApplicationUser
model.
Create the database context in Data/ApplicationDbContext.cs
, inheriting from IdentityDbContext<ApplicationUser>
:
In this example, we create ApplicationDbContext
by inheriting from IdentityDbContext<ApplicationUser>
. This base class, IdentityDbContext
, already includes all the necessary tables for ASP.NET Identity, such as tables for users, roles, and user claims. By specifying ApplicationUser
as the type, we're telling ASP.NET Identity to use our custom user model.
The OnModelCreating
method provides us with a chance to further configure the database schema. Here, we customize the names of the tables used by Identity to be more straightforward:
- Users Table: We rename the default user table to simply
Users
for clarity. - Roles Table: Similarly, we rename the default roles table to
Roles
.
These configurations allow us to have simpler, more intuitive table names in the database, while still retaining all the built-in functionality of ASP.NET Identity.
This is not a requirement, but it can be helpful for keeping your database schema organized and easy to understand.
Registering Services
Now that we have our ApplicationDbContext
and user model set up, it's time to configure our application's services in Program.cs
to enable Identity and authentication.
Open Program.cs
and update it as follows:
We are doing quite a few things here:
- First, we configure our database context using the connection string we defined earlier in
appsettings.json
. This connects our application to the Neon Postgres database, allowing it to store and retrieve user data. - Next, we set up ASP.NET Identity to manage user accounts which includes:
- We enforce strong passwords.
- We configure lockout settings to protect against brute force attacks.
- We require users to have unique emails.
- After setting up Identity, we configure the JWT authentication. This is where the
JWT
token configuration from theappsettings.json
file comes into play as well. This allows our API to issue tokens to authenticated users, which can then be used to access secured endpoints. - Additionally, we define an authorization policy called
RequireAdminRole
, which restricts certain actions to users with the "Admin" role. - To make sure our application has the necessary roles, we include a piece of code that runs on startup to create roles like "Admin" and "User" if they don't already exist. This is done using a scoped service to access the
RoleManager
. - Finally, we map our controllers to handle HTTP requests and add the necessary middleware for authentication and authorization.
Implementing Authentication Controllers
Now that we have configured our database and Identity services, let's create a controller to manage user registration and login. This controller will handle the core authentication flow for our application.
You can create this in a new file, Controllers/AuthController.cs
with the following content:
To begin, we create a new AuthController
class that will handle user authentication. This includes two primary actions: user registration and user login. By using this controller, users will be able to create accounts and log in to receive a JWT, which they can use to access protected endpoints.
Here's how it works:
- The
AuthController
usesUserManager
for user operations,SignInManager
for handling sign-ins, andIConfiguration
for accessing JWT settings. - The
Register
method creates a new user with the provided details. Once created, the user is assigned the "User" role. If registration succeeds, anOk
response is returned; otherwise, aBadRequest
with errors is sent. - The
Login
method verifies if the email exists and checks the password. On success, a JWT token is generated and returned for authenticated access. - The
GenerateJwtToken
method creates a token with the user's ID and email as claims. It signs the token using the secret key fromappsettings.json
and sets it to expire in 3 hours.
Implementing Role-Based Authorization
To manage user roles effectively, we'll create a helper class that checks if the necessary roles are set up in your system. This is useful when you want to predefine certain roles like "Admin" or "User" and make them available as soon as the application starts instead of manually creating them.
You can create this in a new file, Helpers/RoleHelper.cs
as follows:
In the RoleHelper
class, we define a method called EnsureRolesCreated
which:
- Accepts a
RoleManager
instance to interact with the roles in the database. - Defines an array of roles we want to set up ("Admin", "User", and "Manager").
- For each role, it checks if the role already exists using
RoleExistsAsync()
. If the role doesn't exist, it creates the role withCreateAsync()
.
This way, you only need to call this method once during application startup to ensure all required roles are available for assignment.
Authorization Policies
In this section, we're adding authorization to protect certain API endpoints, so that only authenticated users or users with specific roles can access them.
Create a protected endpoint that requires authentication, and an admin-only endpoint. You can create this in a new file, Controllers/SecureController.cs
:
With the above, we created a new controller called SecureController
with two endpoints:
-
General Protected Endpoint:
- The
/api/secure
route is protected with[Authorize]
, allowing access only to authenticated users with a valid JWT token. - If access is granted, it returns a confirmation message.
- The
-
Admin-Only Endpoint:
- The
/api/secure/admin
route is restricted to users with the "Admin" role using[Authorize(Policy = "RequireAdminRole")]
. - Only "Admin" users can access this. Others will receive a
403 Forbidden
response.
- The
Using the same approach, you can create additional policies for different roles or permissions. This allows you to control access to your API endpoints based on user roles.
Database Migrations
To set up your database schema, we need to run migrations. Migrations help keep your database in sync with your data models, allowing you to make changes to your schema without losing data.
Run the following commands to create the database and apply migrations:
- Create the initial migration:
- Apply the migration to the database:
If you were to make changes to your data models in the future, you would create a new migration and apply it using the same commands. Via the Neon console, you will now see the tables created by ASP.NET Identity.
Testing Authentication
You can test your authentication endpoints using Postman or curl
to actually verify that everything is working correctly. Let's quickly do that using curl
.
1. Register a New User
To create a new account, send a POST
request to the /api/auth/register
endpoint with the user details:
This should return a response confirming that the user was successfully registered. Make sure to use a strong password and valid email format as our password policy requires it.
2. Log In and Get a JWT Token
Once registered, log in using the credentials you just created. Send a POST
request to the /api/auth/login
endpoint:
If the login is successful, you'll receive a JSON response containing a JWT access token. Save this token securely, as it will be used to access protected routes.
3. Access a Protected Endpoint
With the JWT token from the previous step, you can now access secured endpoints. Send a GET
request to /api/secure
and include the token in the Authorization
header:
If the token is valid, you'll receive a response from the protected resource. If not, you'll get an authentication error, indicating the token has expired or is invalid.
User Session Management with Refresh Tokens
As an optional step, to improve user session management, we'll implement a two-token authentication system using both access tokens and refresh tokens:
- An access token which is a short-lived JWT used to authenticate API requests
- A longer-lived tokens used to obtain new access tokens without requiring re-login
Setting Up the Token System
First, create a model for refresh tokens in Models/RefreshToken.cs
with an ID, token, and expiry date:
Next, update the ApplicationUser
model to store refresh tokens:
Implementing Token Management
Modify the login endpoint to return both the access and the refresh tokens:
Add the refresh token endpoint to obtain new tokens:
Here we've added a new endpoint /api/auth/refresh-token
that accepts a refresh token and returns new access and refresh tokens. The refresh token is stored in the user's RefreshTokens
list and used to generate new tokens when needed. The refresh token is valid for 7 days, after which the user will need to log in again.
Using the Token System
Unlike the standard login flow, the new flow involves three steps:
-
User logs in with credentials:
Response includes both tokens:
-
Use the access token for API requests:
-
When the access token expires, use the refresh token to get new tokens:
This returns new access and refresh tokens:
As a security measure, store refresh tokens securely in your Neon database while also making sure that clients use secure methods like HTTP-only cookies. Also, keep access tokens short-lived, rotate refresh tokens on refresh, and implement token expiration and revocation to enhance security.
Integrating Auth0 for Authentication and Authorization (Optional)
If you're looking to add an extra layer of security and use external identity providers, integrating your ASP.NET Core application with Auth0 is a good option. This allows your users to authenticate using social accounts (like Google, GitHub, etc.) or enterprise identity providers.
Auth0 offers a flexible platform for managing user authentication, with built-in JWT token support that integrates seamlessly with your existing ASP.NET Core application.
Let's quickly walk through setting up Auth0 with ASP.NET Core for secure authentication and authorization.
Setting Up Auth0 with ASP.NET Core
To get started, follow these high-level steps:
-
Start by creating an Auth0 API:
- Log in to your Auth0 Dashboard.
- Navigate to the "APIs" section and click Create API.
- Provide a name and a unique identifier for your API (e.g.,
https://your-app.com/api
). Keep the default signing algorithm asRS256
.
-
In the API settings, you can define permissions (scopes) to control access to your API endpoints. For example, you can create a
read:messages
permission to restrict access to certain routes. -
Open your
appsettings.json
and add the following configuration: -
Make sure you have the required packages installed if you haven't already as in the previous steps:
-
Update the
Program.cs
file to add the authentication middleware:
With all that in place, you can secure your API endpoints using Auth0, use the [Authorize]
attribute:
For a more information on integrating Auth0 with ASP.NET Core, refer to the Auth0 Documentation. The documentation covers everything from setting up your Auth0 tenant to configuring scopes and securing your APIs.
Conclusion
In this guide, we implemented a secure authentication and authorization system in an ASP.NET Core application using ASP.NET Identity with Neon Postgres as the backend. We walked through setting up user registration and login endpoints, securing API routes with JWT tokens, and implementing role-based authorization.
For more information, check out:
Need help?
Join our Discord Server to ask questions or see what others are doing with Neon. Users on paid plans can open a support ticket from the console. For more details, see Getting Support.