1. Basic Concepts
Modern web applications rely on identity systems to verify users and control access to resources. Before discussing authentication flows and implementation details, it is helpful to understand several key concepts.
Identity Provider (IdP): A service responsible for managing digital identities and authenticating users or applications. It issues identity-related tokens that other systems can trust.
Authentication: The process of verifying that a user, service, or device is who it claims to be.
Authorization: The process of determining what an authenticated identity is allowed to access or perform.
Auditing and Monitoring: Recording and analyzing authentication events and access activities (such as sign-in time, accessed resources, and authentication methods) to support security monitoring and compliance.
2. Tokens in Modern Authentication
Modern authentication systems commonly rely on tokens to represent identity and access permissions. Instead of storing login sessions on the server, many applications issue tokens that the client can include in subsequent requests.
The most common tokens used in OAuth and OpenID Connect are:
Access Token
A token used to access protected APIs. It represents the permissions granted to the client and is typically included in requests using theAuthorization: Bearerheader.Refresh Token
A long-lived token used to obtain a new access token when the current one expires. This allows users to remain logged in without repeatedly authenticating.ID Token
A token that contains information about the authenticated user. It is defined by OpenID Connect (OIDC) and typically includes claims such as the user identifier, issuer, and expiration time.
In practice, access tokens are usually short-lived for security reasons, while refresh tokens allow applications to maintain user sessions without requiring frequent logins.
3.Authentication and Authorization Flows
Modern identity systems typically implement authentication and authorization using standardized OAuth2 flows. This section briefly introduces three commonly used flows. More detailed diagrams can be found in the Auth0 documentation: https://auth0.com/docs/get-started/authentication-and-authorization-flow
3.1 Authorization Code Flow
This flow involves four participants:
- User
- Web Application
- Identity Provider (Authorization Server)
- API (Resource Server)
A key characteristic of this flow is that tokens are not directly returned to the browser. Instead, the backend exchanges the authorization code for tokens and may maintain a server-side session. Although some architectures allow the frontend to store tokens directly, storing them on the server is generally considered more secure.
Steps
- The user clicks Login in the application.
- The application redirects the browser to the Identity Provider’s /authorize endpoint.
- The Identity Provider displays the login page.
- After successful authentication, the Identity Provider redirects the browser back to the application with a one-time authorization code.
- The application’s backend receives the code and sends a request to - the Identity Provider’s /oauth/token endpoint.
- The Identity Provider verifies: the authorization code, the client identity, the redirect URI
- If the verification succeeds, the Identity Provider returns: access token, ID token (if using OpenID Connect), refresh token (optional)
- The backend stores the login state.
- Subsequent requests from the frontend are authenticated using a session, rather than directly exposing tokens to the browser. ### 3.2Authorization Code Flow + PKCE Participants
- User
- SPA / Mobile App / Native App
- Identity Provider
-
API
Why PKCE is needed
Public clients such as single-page applications or mobile apps cannot securely store a client_secret. PKCE (Proof Key for Code Exchange) protects the authorization code exchange using a temporary cryptographic proof.
Steps
The client generates a random string called code_verifier.
The client computes a hashed value called code_challenge.
The client redirects the browser to /authorize with: code_challenge
The user completes authentication at the Identity Provider.
The Identity Provider redirects the browser back with an authorization code.
The client sends a request to /oauth/token including: authorization_code and code_verifier
The Identity Provider computes: SHA256(code_verifier) and compares it with the previously received code_challenge.
If the verification succeeds, the Identity Provider returns: access token, ID token, refresh token (optional)
-
The client uses the access token to call protected APIs.
3.3 Client Credentials Flow
Participants
Client Service (calling service)
Authorization Server (Identity Service)
-
Resource Server (API)
This flow is used for service-to-service authentication, where no user is involved.Steps
A service needs to call another service’s API.
The calling service requests a token from the Authorization Server.
The Authorization Server verifies the client identity (client_id and client_secret).
If verification succeeds, the Authorization Server returns an access token.
The calling service sends the API request with: Authorization: Bearer access_token
The target API validates the token.
If valid, the API processes the request and returns the result.
Summary of the Three Flows
- Authorization Code Flow is designed for user authentication in traditional web applications.
- Authorization Code + PKCE protects the same flow for public clients that cannot store secrets.
- Client Credentials Flow enables secure communication between backend services.
4. Choosing an Authentication Architecture
In practice, the choice between a pure JWT approach and a session-based architecture depends mainly on the security requirements of the system.
For simpler applications, it is common to return the JWT directly to the frontend. The client stores the token and includes it in API requests using the Authorization: Bearer header. This approach keeps the system stateless and easy to scale, making it suitable for public APIs, mobile backends, and lower-risk systems.
For systems with higher security requirements, a session-based architecture is often preferred. In this model, the frontend only receives a session cookie, while the gateway or backend stores and manages tokens on the server side. This allows better control over sessions, such as immediate logout, device management, and token revocation.
In many modern SaaS systems, a hybrid model is used: user sessions are managed by the gateway, while internal microservices continue to authenticate using JWT.
5. Implementing Authentication in ASP.NET Core
ASP.NET Core provides built-in support for authentication and authorization through middleware and Identity services.
A typical implementation includes configuring Identity for user management and JWT for API authentication.
Identity Setup
First, define a user entity and role entity:
public class MyUser : IdentityUser<long>
{
public string? WechatAccout { get; set; }
}
Add a class named MyRole(any name you want) class that inherits from IdentityRole, T is type of Primary key.
public class MyRole : IdentityRole<long>
{
}
Then create a DbContext for Identity:
public class ApplicationDbContext : IdentityDbContext<MyUser, MyRole, long>
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
{
}
}
Register Identity and the database in the dependency injection container:
builder.Services.AddDbContext<ApplicationDbContext>(options =>
{
var connectionString = builder.Configuration.GetValue<string>("MySQLConnectionString");
options.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString));
});
builder.Services.AddIdentityCore<MyUser>(options =>
{
options.Password.RequireDigit = false;
options.Password.RequiredLength = 6;
});
var identityBuilder = new IdentityBuilder(typeof(MyUser), typeof(MyRole), builder.Services);
identityBuilder
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders()
.AddRoleManager<RoleManager<MyRole>>()
.AddUserManager<UserManager<MyUser>>();
JWT Authentication
Install required packages:
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
dotnet add package System.IdentityModel.Tokens.Jwt
Configurate JWT
"JwtTokenOption": {
"Issuer": "Alex",
"Audience": "EveryOne",
"IssuerSigningKey": "Alex$%%^&%*!&^@*GUH976&^^&T*u34",
"AccessTokenExpiresMinutes": "30"
}
Register JWT authentication:
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
var jwtTokenOption = builder.Configuration
.GetSection("JwtTokenOption")
.Get<JwtTokenOption>();
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = jwtTokenOption.Issuer,
ValidateAudience = true,
ValidAudience = jwtTokenOption.Audience,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(jwtTokenOption.IssuerSigningKey)),
ValidateLifetime = true
};
});
builder.Services.AddAuthorization();
Enable authentication middleware:
app.UseAuthentication();
app.UseAuthorization();
Protected endpoints can then require authentication using the [Authorize] attribute:
[HttpPost]
[Authorize]
public async Task<IActionResult> SendRestPasswordToken(string username)
{
}
Clients must include the JWT in the request header:
Authorization: Bearer <access_token>
6. Practical Implementation in My Project
In my current microservices project (FinTrack-Microservices), authentication is implemented using JWT with a multi-token design.
Token Types
The system issues several token types for different purposes:
- AccountAccessToken Identifies the user account and is mainly used to fetch user profile information and tenant memberships.
- TenantAccessToken Represents access to a specific tenant. All tenant-scoped APIs require this token.
- RefreshToken Used to obtain a new access token when the current access token expires.
- InviteToken Used in tenant invitation flows when a user accepts an organization invitation.
Token Revocation Strategy
To maintain a stateless architecture while still supporting token invalidation, the system uses a token version + Redis strategy. Each user record stores a tokenVersion. When a token is issued, the version is embedded into the JWT claims. During request validation, the service compares: token.tokenVersion == currentVersion. If the version changes (for example after logout or password reset), previously issued tokens become invalid. Redis is used to cache this version for fast validation across services.
Why Not Use Session Yet
The current architecture intentionally avoids server-side session management.
Returning JWT tokens directly to the client keeps the system fully stateless, simplifies scaling, and avoids maintaining session state in the gateway or backend.
For the current stage of the project, this approach keeps the architecture simpler.
Future Evolution
If stronger session control becomes necessary, the system can evolve to a Gateway + Session architecture:
Client → Gateway (Session)
Gateway → Internal Services (JWT)
In that model:
- The client only holds a session cookie.
- The gateway manages tokens and refresh logic.
- Internal microservices continue using access tokens for service-to-service authentication. Because the internal services already rely on JWT, migrating to this architecture would require only gateway-level changes.
Top comments (0)