Skip to content

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.), and exp (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, and expires_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 as urn:eportal:authz:exchange for exchanging an ID token or refresh_token for 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 the System.ComponentModel.DataAnnotations namespace (e.g., [Required]) to enforce validation rules and the System.Text.Json.Serialization namespace (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 ClientId and ClientSecret before issuing tokens.
  • Best Practices:
  • Hash ClientSecret securely (e.g., using bcrypt).
  • Rotate secrets periodically.
  • Restrict client access using IsActive flag.

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:

{
  "token": "<JWT_TOKEN>"
}

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:
{
  "is_valid": false,

  "error": "Token is invalid or expired"
}

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