Skip to main content

Overview

RealtimeChat uses ASP.NET Core Identity with Entity Framework Core for user management and authentication. This page covers the Identity configuration, database schema, and user model details.
ASP.NET Identity provides a complete membership system with built-in support for password hashing, user lockout, two-factor authentication, and more.

ApplicationUser Model

The ApplicationUser class extends Identity’s IdentityUser to add custom properties specific to RealtimeChat.

Source Code

Infrastructure/RealtimeChat.Infrastructure.DB/Entities/ApplicationUser.cs
using Microsoft.AspNetCore.Identity;

namespace RealtimeChat.Persistence.DB.Entities;

public class ApplicationUser: IdentityUser
{
    public string? FirstName { get; set; }
    public string? LastName { get; set; }
    
    public ICollection<MessageEntity> Messages { get; set; } = null!;
    public ICollection<ChatRoomParticipantEntity> ChannelParticipants { get; set; } = null!;
}

Properties Reference

From IdentityUser (Base Class)

Id
string
Primary key - Unique identifier (GUID as string)
UserName
string
Username - Typically set to the user’s email address
NormalizedUserName
string
Uppercase username for case-insensitive lookups
Email
string
User’s email address
NormalizedEmail
string
Uppercase email for case-insensitive lookups
EmailConfirmed
bool
Indicates if the email address has been verified
PasswordHash
string
Hashed password using PBKDF2 algorithm (never plain text)
SecurityStamp
string
Random value that changes when credentials change (used to invalidate cookies)
ConcurrencyStamp
string
Token for optimistic concurrency control
PhoneNumber
string
Optional phone number
PhoneNumberConfirmed
bool
Indicates if phone number has been verified
TwoFactorEnabled
bool
Whether two-factor authentication is enabled
LockoutEnd
DateTimeOffset?
UTC datetime when lockout ends (null if not locked out)
LockoutEnabled
bool
Whether user can be locked out
AccessFailedCount
int
Number of failed login attempts (for lockout)

Custom Properties

FirstName
string?
User’s first name (nullable) - Populated from OAuth or registration
LastName
string?
User’s last name (nullable) - Populated from OAuth or registration
Messages
ICollection<MessageEntity>
Navigation property for all messages sent by the user
ChannelParticipants
ICollection<ChatRoomParticipantEntity>
Navigation property for all chat rooms the user participates in

Identity Configuration

Database Configuration

The IdentityConfiguration class configures the Entity Framework Core table names for Identity entities:
Infrastructure/RealtimeChat.Infrastructure.DB/Configurations/IdentityConfiguration.cs
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using RealtimeChat.Persistence.DB.Entities;

namespace RealtimeChat.Infrastructure.DB.Configurations;

public class IdentityConfiguration: ITypeConfiguration
{
    public void Configure(ModelBuilder builder)
    {
        builder.Entity<ApplicationUser>().ToTable("users");
        builder.Entity<IdentityRole>().ToTable("roles");
        builder.Entity<IdentityUserToken<string>>().ToTable("user_tokens");
        builder.Entity<IdentityUserRole<string>>().ToTable("user_roles");
        builder.Entity<IdentityRoleClaim<string>>().ToTable("role_claims");
        builder.Entity<IdentityUserClaim<string>>().ToTable("user_claims");
        builder.Entity<IdentityUserLogin<string>>().ToTable("user_logins");
    }
}
By default, Identity uses table names like “AspNetUsers”. This configuration customizes them to follow snake_case naming conventions.

Service Registration

Identity is registered in the dependency injection container via AuthExtensions:
Applications/RealtimeChat.API/Extensions/AuthExtensions.cs
builder.Services
    .AddIdentityApiEndpoints<ApplicationUser>()
    .AddEntityFrameworkStores<RealtimeChatDbContext>()
    .AddApiEndpoints();
What this does:
  • AddIdentityApiEndpoints<ApplicationUser>() - Registers Identity services with API endpoint support
  • AddEntityFrameworkStores<RealtimeChatDbContext>() - Configures EF Core as the storage provider
  • AddApiEndpoints() - Maps RESTful authentication endpoints

Database Schema

Identity Tables

ASP.NET Identity creates the following tables in your PostgreSQL database:

users (ApplicationUser)

ColumnTypeDescription
idstring (PK)Unique user identifier (GUID)
user_namestringUsername (typically email)
normalized_user_namestringUppercase username for lookups
emailstringEmail address
normalized_emailstringUppercase email for lookups
email_confirmedboolEmail verification status
password_hashstringHashed password (PBKDF2)
security_stampstringCredential change token
concurrency_stampstringConcurrency token
phone_numberstringOptional phone number
phone_number_confirmedboolPhone verification status
two_factor_enabledbool2FA enabled status
lockout_enddatetimeoffsetLockout expiration
lockout_enabledboolCan user be locked out
access_failed_countintFailed login attempts
first_namestringCustom: User’s first name
last_namestringCustom: User’s last name

user_logins (External OAuth Logins)

ColumnTypeDescription
login_providerstring (PK)OAuth provider (e.g., “Google”)
provider_keystring (PK)Provider’s user identifier
provider_display_namestringDisplay name for provider
user_idstring (FK)Foreign key to users table
This table links external OAuth providers to user accounts, allowing users to sign in with Google.

roles

ColumnTypeDescription
idstring (PK)Unique role identifier
namestringRole name (e.g., “Admin”)
normalized_namestringUppercase name for lookups
concurrency_stampstringConcurrency token

user_roles (Many-to-Many)

ColumnTypeDescription
user_idstring (PK, FK)Foreign key to users
role_idstring (PK, FK)Foreign key to roles

user_claims

ColumnTypeDescription
idint (PK)Unique claim identifier
user_idstring (FK)Foreign key to users
claim_typestringClaim type (e.g., “email”)
claim_valuestringClaim value

role_claims

ColumnTypeDescription
idint (PK)Unique claim identifier
role_idstring (FK)Foreign key to roles
claim_typestringClaim type
claim_valuestringClaim value

user_tokens

ColumnTypeDescription
user_idstring (PK, FK)Foreign key to users
login_providerstring (PK)Token provider
namestring (PK)Token name
valuestringToken value

ER Diagram

┌──────────────────┐
│      users       │
├──────────────────┤
│ id (PK)          │
│ user_name        │
│ email            │
│ password_hash    │
│ first_name       │
│ last_name        │
│ ...              │
└────────┬─────────┘

         │ 1

         │ *
    ┌────┴─────────────┐
    │                  │
┌───▼──────────┐  ┌───▼──────────┐
│ user_logins  │  │ user_claims  │
├──────────────┤  ├──────────────┤
│ user_id (FK) │  │ id (PK)      │
│ provider (PK)│  │ user_id (FK) │
│ provider_key │  │ claim_type   │
└──────────────┘  │ claim_value  │
                  └──────────────┘

User Claims

Claims are key-value pairs that represent user attributes. They are used for authorization and can be accessed in your application.

Standard Claims

When a user logs in, the following claims are automatically added:
ClaimTypes.NameIdentifier    // User ID
ClaimTypes.Name              // Username
ClaimTypes.Email             // Email address

Custom Claims from OAuth

When authenticating via Google OAuth, additional claims are extracted:
Applications/RealtimeChat.API/Services/ExternalAuthService.cs
var email = principal.FindFirst(ClaimTypes.Email)?.Value;
var providerKey = principal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var fullName = principal.FindFirst(ClaimTypes.Name)?.Value;

Accessing Claims in Code

In your API endpoints, access user claims via HttpContext.User:
app.MapGet("/api/profile", (ClaimsPrincipal user) =>
{
    var userId = user.FindFirst(ClaimTypes.NameIdentifier)?.Value;
    var email = user.FindFirst(ClaimTypes.Email)?.Value;
    
    return new { UserId = userId, Email = email };
}).RequireAuthorization();

UserManager and SignInManager

ASP.NET Identity provides two key services for user management:

UserManager<ApplicationUser>

Manages user operations:
// Create user
await userManager.CreateAsync(user);
await userManager.CreateAsync(user, password);

// Find user
var user = await userManager.FindByEmailAsync(email);
var user = await userManager.FindByIdAsync(userId);
var user = await userManager.FindByLoginAsync(provider, providerKey);

// Update user
await userManager.UpdateAsync(user);

// Password operations
await userManager.AddPasswordAsync(user, password);
await userManager.ChangePasswordAsync(user, oldPassword, newPassword);
await userManager.CheckPasswordAsync(user, password);

// External logins
await userManager.AddLoginAsync(user, loginInfo);
await userManager.GetLoginsAsync(user);
await userManager.RemoveLoginAsync(user, provider, providerKey);

SignInManager<ApplicationUser>

Manages sign-in operations:
// Sign in
await signInManager.SignInAsync(user, isPersistent: false);
await signInManager.PasswordSignInAsync(username, password, isPersistent, lockoutOnFailure);

// Sign out
await signInManager.SignOutAsync();

// Check authentication
var result = await context.AuthenticateAsync(IdentityConstants.ApplicationScheme);

Usage in ExternalAuthService

See how these services are used together:
Applications/RealtimeChat.API/Services/ExternalAuthService.cs
public class ExternalAuthService(
    UserManager<ApplicationUser> userManager,
    SignInManager<ApplicationUser> signInManager)
{
    public async Task<ApplicationUser?> HandleExternalLoginAsync(ClaimsPrincipal principal, string provider)
    {
        var email = principal.FindFirst(ClaimTypes.Email)?.Value;
        var providerKey = principal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
        var fullName = principal.FindFirst(ClaimTypes.Name)?.Value;

        if (string.IsNullOrEmpty(email) || string.IsNullOrEmpty(providerKey))
        {
            return null;
        }

        var (firstName, lastName) = SplitName(fullName);
        var loginInfo = new UserLoginInfo(provider, providerKey, provider);
        
        // Try to find existing user
        var user = await userManager.FindByLoginAsync(provider, providerKey);
        user ??= await userManager.FindByEmailAsync(email);
        
        // Create new user if doesn't exist
        if (user == null)
        {
            user = new ApplicationUser
            {
                UserName = email,
                Email = email,
                FirstName = firstName,
                LastName = lastName
            };
            
            var createResult = await userManager.CreateAsync(user);
            if (!createResult.Succeeded)
            {
                return null;
            }
        }
        
        // Link external login to user
        var existingLogins = await userManager.GetLoginsAsync(user);
        var alreadyLinked = existingLogins
            .Any(l => l.LoginProvider == provider && l.ProviderKey == providerKey);

        if (!alreadyLinked)
        {
            var addLoginResult = await userManager.AddLoginAsync(user, loginInfo);
            if (!addLoginResult.Succeeded)
            {
                return null;
            }
        }

        // Sign in the user
        await signInManager.SignInAsync(user, isPersistent: false);
        return user;
    }
}

Database Migrations

To create or update the Identity database schema:

Create Migration

cd Infrastructure/RealtimeChat.Infrastructure.DB
dotnet ef migrations add InitialIdentity --startup-project ../../Applications/RealtimeChat.API

Apply Migration

dotnet ef database update --startup-project ../../Applications/RealtimeChat.API

Connection String

Configure your PostgreSQL connection string in appsettings.json:
Applications/RealtimeChat.API/appsettings.json
{
  "ConnectionStrings": {
    "DefaultConnectionString": "Server=localhost;Port=5432;Username=postgres;Password=postgres;Database=realtime_chat_db;Include Error Detail=true;MaxPoolSize=10;MinPoolSize=2;"
  }
}
Never commit database credentials to source control! Use environment variables or Azure Key Vault in production.

Password Hashing

ASP.NET Identity uses PBKDF2 (Password-Based Key Derivation Function 2) for password hashing:
  • Algorithm: PBKDF2 with HMAC-SHA256
  • Iterations: 10,000+ (configurable)
  • Salt: Unique random salt per password
  • Output: Base64-encoded hash including algorithm metadata

Example Password Hash

AQAAAAIAAYagAAAAEJ5... (Base64 encoded)
Breakdown:
  • AQ - Version 1 of the hash format
  • AAAAIA - PBKDF2-HMAC-SHA256
  • AYag - Iteration count
  • Remaining bytes - Salt + hash
You never need to implement password hashing manually. ASP.NET Identity handles this automatically when you call userManager.CreateAsync(user, password).

Security Considerations

Database Security

  • Use parameterized queries - EF Core does this automatically
  • Encrypt connection strings - Use Azure Key Vault or environment variables
  • Restrict database access - Use least-privilege database users
  • Enable SSL/TLS - For database connections in production

Password Policy

Configure password requirements in AddIdentity:
builder.Services.AddIdentity<ApplicationUser, IdentityRole>(options =>
{
    options.Password.RequireDigit = true;
    options.Password.RequireLowercase = true;
    options.Password.RequireUppercase = true;
    options.Password.RequireNonAlphanumeric = true;
    options.Password.RequiredLength = 8;
    options.Password.RequiredUniqueChars = 4;
})

User Lockout

Protect against brute force attacks:
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(15);
options.Lockout.MaxFailedAccessAttempts = 5;
options.Lockout.AllowedForNewUsers = true;

Security Stamp

The SecurityStamp invalidates all existing authentication cookies when:
  • Password is changed
  • External login is added/removed
  • Two-factor authentication is enabled/disabled
This ensures compromised sessions are invalidated immediately.

Next Steps

Authentication Overview

Learn about the overall authentication system

OAuth Setup

Configure Google OAuth authentication

Build docs developers (and LLMs) love