Implementing a JWT Authorization (AuthZ) Service for Your Application¶
This article provides an in-depth overview of creating a JWT-based Authorization (AuthZ) service. It includes architectural design, recommended best practices, an illustrative sequence diagram, and implementation strategies using C# and .NET. We will also cover how to minimize token sizes and integrate with external identity providers like Azure Entra ID or Auth0. The entire solution will be hosted on Azure and leverage Azure services, with user profile and permission mappings stored in a SQL Server database.
Overview¶
Modern applications rely heavily on robust authentication and authorization frameworks. Authentication confirms who the user is, while authorization determines what they can do. In this architecture:
-
Identity Provider (IdP): Handles user authentication and issues an ID token (e.g., Azure Entra ID or Auth0).
-
AuthZ Service: Validates the incoming ID token from the IdP, fetches user-specific profiles, roles, and permissions from a SQL Server database, and issues a JWT access token. This token is then used by clients to access protected APIs.
-
Protected APIs: Validate the JWT and apply fine-grained authorization logic.
Key Components and Technologies¶
-
C#/.NET 8: For building the AuthZ service endpoints.
-
Azure Entra ID or Auth0: External IdP for initial user authentication.
-
Azure App Service: For hosting the AuthZ service.
-
Azure SQL Database: For storing user profiles, roles, permissions, and token data.
-
Azure Key Vault: For securely storing and managing signing keys and database credentials.
-
Azure Application Insights: For monitoring and logging.
-
Azure Redis Cache (optional): For caching roles/permissions to reduce database load.
Token Types and Their Purpose¶
Identity Token¶
-
Purpose: Confirms the identity of the user authenticated by the IdP.
-
Lifespan: Short-lived (e.g., 5-15 minutes).
-
Generated By: The external IdP (e.g., Azure Entra ID or Auth0).
-
Attributes:
sub(user ID),email,name,iss(issuer),aud(intended audience), and additional claims as needed.
Example Payload¶
{
"iss": "https://idp.example.com/",
"sub": "12345",
"aud": "client-id",
"email": "kevin.osullivan@example.com",
"name": "Kevin O'Sullivan",
"exp": 1700000000,
"iat": 1699999000
}
Sequence Diagram¶
::: mermaid sequenceDiagram
participant User
participant IdP as Identity Provider
participant AuthZ as Authorization Service
User->>IdP: Authenticate (e.g., Username/Password)
IdP→>User: Returns ID Token
User->>AuthZ: Send ID Token
AuthZ→>User: Acknowledges Valid Token
:::
Access Token¶
-
Purpose: Grants access to protected APIs for a specific profile.
-
Lifespan: Medium-lived (e.g., 1 hour).
-
Generated By: The Authorization Service.
-
Attributes:
sub(user ID),profileId,roleId,scopes,iss(issuer),aud(audiences such as GRID, IOT, etc.), andexp(expiration time).
Example Payload¶
{
"iss": "https://authz.example.com/",
"sub": "12345",
"aud": ["GRID", "IOT"],
"profileId": "profile-001",
"roleId": "Admin",
"scopes": ["region:emea", "Municipality:00012134"],
"exp": 1700003600,
"iat": 1699999000
}
Sequence Diagram¶
:::mermaid
sequenceDiagram
participant User
participant AuthZ as Authorization Service
participant API
User->>AuthZ: Request Access Token with Profile ID and Valid ID Token
AuthZ->>AuthZ: Validate ID Token and Fetch User Details
AuthZ→>User: Returns Access Token
User->>API: Access Protected Resource with Access Token
API->>AuthZ: Validate Access Token
API→>User: Returns Resource Data
:::
Renewal Token¶
-
Purpose: Used to obtain new access tokens without requiring re-authentication.
-
Lifespan: Long-lived (e.g., 7-30 days).
-
Generated By: The Authorization Service.
-
Attributes:
sub(user ID),iss(issuer),created_at, andexpires_at.
Example Payload¶
{
"iss": "https://authz.example.com/",
"sub": "12345",
"created_at": "2025-01-01T12:00:00Z",
"expires_at": "2025-02-01T12:00:00Z"
}
Sequence Diagram¶
:::mermaid
sequenceDiagram
participant User
participant AuthZ as Authorization Service
User->>AuthZ: Request New Access Token with Valid Refresh Token
AuthZ->>AuthZ: Validate Refresh Token
AuthZ→>User: Returns New Access Token and Refresh Token
:::
Database Schema for Token Management¶
To maintain token details, the following database tables are required:
Tokens Table¶
| Column | Data Type | Nullable | Description |
|---|---|---|---|
| Id | INT (IDENTITY) | NO | Primary key, auto-incremented identity column. |
| UserId | UNIQUEIDENTIFIER or INT | NO | Foreign key referencing the Users table. Represents the token owner. |
| ProfileId | NVARCHAR(50) | NO | Profile associated with the token. |
| Token | NVARCHAR(512) | NO | The actual token (e.g., refresh token). Consider hashing this value for security. |
| TokenType | NVARCHAR(50) | NO | Indicates the type of token (e.g., "refresh"). |
| CreatedAt | DATETIMEOFFSET(7) | NO | The UTC date/time the token was created. |
| ExpiresAt | DATETIMEOFFSET(7) | NO | The UTC expiration date/time of the token. |
| RevokedAt | DATETIMEOFFSET(7) | YES | The UTC date/time the token was revoked. NULL if token is still valid. |
| IsRevoked | BIT | NO | Flag indicating if the token is revoked (1 = revoked, 0 = active). |
| ClientId | NVARCHAR(100) | YES | Identifies which client or application the token was issued to. |
| CreatedByIp | NVARCHAR(45) | YES | The IP address from which the token was originally issued. |
| RevokedByIp | NVARCHAR(45) | YES | The IP address from which the token was revoked. NULL if not revoked. |
Example Tokens Table Creation Script¶
CREATE TABLE Tokens (
Id INT IDENTITY(1,1) PRIMARY KEY,
UserId UNIQUEIDENTIFIER NOT NULL,
ProfileId NVARCHAR(50) NOT NULL,
Token NVARCHAR(512) NOT NULL,
TokenType NVARCHAR(50) NOT NULL,
CreatedAt DATETIMEOFFSET(7) NOT NULL,
ExpiresAt DATETIMEOFFSET(7) NOT NULL,
RevokedAt DATETIMEOFFSET(7) NULL,
IsRevoked BIT NOT NULL DEFAULT 0,
ClientId NVARCHAR(100) NULL,
CreatedByIp NVARCHAR(45) NULL,
RevokedByIp NVARCHAR(45) NULL
);
CREATE UNIQUE INDEX IX_Tokens_Token ON Tokens(Token);
CREATE INDEX IX_Tokens_UserId ON Tokens(UserId);
Clients Table¶
| Column | Data Type | Nullable | Description |
|---|---|---|---|
| ClientId | NVARCHAR(100) | NO | Unique identifier for the client application. |
| ClientSecret | NVARCHAR(512) | NO | Hashed secret for authenticating the client. |
| Name | NVARCHAR(255) | NO | A friendly name for the client application. |
| Description | NVARCHAR(MAX) | YES | Optional description of the client application. |
| CreatedAt | DATETIMEOFFSET(7) | NO | The date and time when the client was registered. |
| IsActive | BIT | NO | Indicates whether the client is active (1 = active, 0 = inactive). |
CREATE TABLE Clients (
ClientId NVARCHAR(100) PRIMARY KEY,
ClientSecret NVARCHAR(512) NOT NULL,
Name NVARCHAR(255) NOT NULL,
Description NVARCHAR(MAX) NULL,
CreatedAt DATETIMEOFFSET(7) NOT NULL DEFAULT SYSDATETIMEOFFSET(),
IsActive BIT NOT NULL DEFAULT 1
);
Example Clients Table Creation Script¶
Implementation Details¶
Example TokenRequest objest¶
public class TokenRequest
{
[Required]
[JsonPropertyName("grant_type")]
public string GrantType { get; set; }
[JsonPropertyName("id_token")]
public string IdToken { get; set; }
[JsonPropertyName("refresh_token")]
public string RefreshToken { get; set; }
[Required]
[JsonPropertyName("profileId")]
public string ProfileId { get; set; }
[Required]
[JsonPropertyName("client_id")]
public string ClientId { get; set; }
[Required]
[JsonPropertyName("client_secret")]
public string ClientSecret { get; set; }
}
Explanation of Properties:¶
GrantType: Specifies the type of request, such asurn:eportal:authz:exchangefor exchanging an ID token orrefresh_tokenfor obtaining a new access token using a refresh token.IdToken: The ID token received from the IdP. This is optional for refresh token requests.RefreshToken: The refresh token issued earlier by the authorization service. This is required for refresh token grant type.ProfileId: Specifies the user profile for which the token is requested.ClientId: Identifies the client application making the request.ClientSecret: A secret key that authenticates the client application. This class uses attributes from theSystem.ComponentModel.DataAnnotationsnamespace (e.g.,[Required]) to enforce validation rules and theSystem.Text.Json.Serializationnamespace (e.g.,[JsonPropertyName]) to map JSON properties to C# properties. Would you like this example added directly to your document?
/token Endpoint Implementation with Database Updates¶
[HttpPost("token")]
public async Task<IActionResult> Token([FromBody] TokenRequest request)
{
// Validate client credentials
if (!await ValidateClientCredentials(request.ClientId, request.ClientSecret))
{
return Unauthorized(new { error = "Invalid client credentials" });
}
if (request.GrantType == "urn:eportal:authz:exchange")
{
var userInfo = await _tokenService.ValidateIdTokenAsync(request.IdToken);
if (userInfo == null)
return Unauthorized();
var userProfiles = await _db.UserProfiles
.Where(up => up.UserId == userInfo.UserId && up.ProfileId == request.ProfileId)
.Include(up => up.Role)
.ToListAsync();
if (!userProfiles.Any())
return BadRequest("Invalid profile");
var jwt = _tokenService.CreateAccessToken(userInfo.UserId, request.ProfileId, userProfiles.First().Role.RoleId, new[] { "GRID", "IOT" });
var refreshToken = _tokenService.CreateRenewalToken(userInfo.UserId);
await _db.Tokens.AddAsync(new TokenEntity
{
UserId = userInfo.UserId,
ProfileId = request.ProfileId,
Token = refreshToken,
TokenType = "refresh",
CreatedAt = DateTimeOffset.UtcNow,
ExpiresAt = DateTimeOffset.UtcNow.AddDays(30),
IsRevoked = false
});
await _db.SaveChangesAsync();
return Ok(new
{
access_token = jwt,
refresh_token = refreshToken,
token_type = "Bearer",
expires_in = 3600
});
}
else if (request.GrantType == "refresh_token")
{
var validRefresh = await _db.Tokens
.FirstOrDefaultAsync(rt => rt.Token == request.RefreshToken && rt.ExpiresAt > DateTimeOffset.UtcNow && rt.IsRevoked == false);
if (validRefresh == null)
return Unauthorized();
var newAccessToken = _tokenService.CreateAccessToken(validRefresh.UserId, validRefresh.ProfileId, "RoleId", new[] { "GRID", "IOT" });
var newRefreshToken = _tokenService.CreateRenewalToken(validRefresh.UserId);
validRefresh.Token = newRefreshToken;
validRefresh.CreatedAt = DateTimeOffset.UtcNow;
validRefresh.ExpiresAt = DateTimeOffset.UtcNow.AddDays(30);
validRefresh.IsRevoked = false;
await _db.SaveChangesAsync();
return Ok(new
{
access_token = newAccessToken,
refresh_token = newRefreshToken,
token_type = "Bearer",
expires_in = 3600
});
}
return BadRequest("Unsupported grant_type");
}
Client Credentials Validation¶
private async Task<bool> ValidateClientCredentials(string clientId, string clientSecret)
{
var client = await _db.Clients.FirstOrDefaultAsync(c => c.ClientId == clientId && c.IsActive);
if (client == null)
{
return false; // Client does not exist or is inactive.
}
// Validate the provided secret against the stored hashed secret.
return BCrypt.Net.BCrypt.Verify(clientSecret, client.ClientSecret);
}
Summary¶
**Clients**Table: Manages application credentials (ClientId,ClientSecret).- Validation: Token service validates
ClientIdandClientSecretbefore issuing tokens. - Best Practices:
- Hash
ClientSecretsecurely (e.g., using bcrypt). - Rotate secrets periodically.
- Restrict client access using
IsActiveflag.
This structure and process improve the security and manageability of your token service. Let me know if further adjustments are needed!
/revoke Endpoint Implementation with Database Updates¶
[HttpPost("revoke")]
public async Task<IActionResult> Revoke([FromBody] RevokeRequest request)
{
var tokenEntity = await _db.Tokens
.FirstOrDefaultAsync(t => t.Token == request.Token && t.IsRevoked == false && t.ExpiresAt > DateTimeOffset.UtcNow);
if (tokenEntity == null)
return NotFound(new { status = "token_not_found_or_already_revoked" });
tokenEntity.IsRevoked = true;
tokenEntity.RevokedAt = DateTimeOffset.UtcNow;
tokenEntity.RevokedByIp = HttpContext.Connection.RemoteIpAddress?.ToString();
await _db.SaveChangesAsync();
return Ok(new { status = "success" });
}
Profile-Specific Tokens¶
A token will be issued for a single profile. Users must specify the desired profile when requesting an access token. To switch profiles, they need to explicitly request a new token for the new profile.
Revocation Strategy¶
Revocation of tokens will be a stretch goal. To mitigate risks, we will keep token lifetimes short:
-
Identity Tokens: 15 minutes
-
Access Tokens: 1 hour
-
Renewal Tokens: 30 days
/validate-token Endpoint Details¶
Purpose¶
The /validate-token endpoint allows services to validate the authenticity and status of a token without implementing their own token validation logic.
Request¶
Endpoint: POST /validate-token
Request Body:
Response¶
- Success:
{
"is_valid": true,
"claims": {
"sub": "12345",
"aud": ["GRID", "IOT"],
"profileId": "profile-001",
"roleId": "Admin",
"scopes": ["region:emea", "Municipality:00012134"],
"exp": 1700003600,
"iat": 1699999000
}
}
- Failure:
Implementation¶
[HttpPost("validate-token")]
public async Task<IActionResult> ValidateToken([FromBody] TokenValidationRequest request)
{
try
{
var handler = new JwtSecurityTokenHandler();
// Validate the token
var validationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = "https://authz.example.com/",
ValidateAudience = true,
ValidAudiences = new[] { "GRID", "IOT" },
ValidateLifetime = true,
IssuerSigningKey = _signingKey
};
var principal = handler.ValidateToken(request.Token, validationParameters, out var validatedToken);
// Extract claims if needed
var claims = principal.Claims.ToDictionary(c => c.Type, c => c.Value);
return Ok(new
{
is_valid = true,
claims
});
}
catch (Exception ex)
{
// Log the error (if needed)
return BadRequest(new
{
is_valid = false,
error = ex.Message
});
}
}
Sequence Diagram¶
:::mermaid
sequenceDiagram
participant Service as Client Service
participant AuthZ as Authorization Service
Service->>AuthZ: POST /validate-token with Token
AuthZ->>AuthZ: Validate Token (issuer, audience, lifetime, signature)
alt Valid Token
AuthZ→>Service: Response with claims and validation success
else Invalid Token
AuthZ→>Service: Response with error and validation failure
end
:::
Benefits¶
-
Centralized Validation: Ensures all services rely on a consistent and secure validation process.
-
Dynamic Revocation Check: Can incorporate additional checks, such as verifying the token against a database for revocation.
-
Simplified API Logic: APIs no longer need to implement token validation logic independently.
Hosting on Azure¶
- Deploy the .NET