Skip to content

Authentication in ConnectSoft Microservice Template

Purpose & Overview

Authentication in the ConnectSoft Microservice Template provides a comprehensive framework for verifying the identity of users, services, and systems making requests to the microservice. Authentication is the foundation of security, ensuring that only verified identities can access protected resources and establishing the security context for authorization decisions.

Authentication in the template provides:

  • Multiple Authentication Schemes: Support for JWT, OAuth2/OIDC, API Keys, and Azure Managed Identity
  • Claims-Based Identity: Rich identity information through claims and tokens
  • Token Validation: Automatic validation of JWT tokens with expiration and signature verification
  • Flexible Configuration: Environment-specific authentication configuration
  • Integration with Authorization: Seamless integration with policy-based authorization
  • Service-to-Service Authentication: Support for service principal authentication
  • Azure Integration: Native support for Azure AD and Managed Identity
  • OpenID Connect: Full OIDC discovery and token validation

Authentication Philosophy

Authentication establishes who is making the request. It verifies identity through tokens, credentials, or certificates, creating a security principal that can be used for authorization decisions. The template supports multiple authentication schemes to accommodate different client types (browsers, mobile apps, services) while maintaining security best practices. All authentication is token-based where possible, with proper validation, expiration, and revocation support.

Architecture Overview

Authentication Flow

Client Request
Authentication Middleware
    ├── JWT Bearer Token
    ├── OAuth2/OIDC Token
    ├── API Key
    └── Managed Identity
Token Validation
    ├── Signature Verification
    ├── Expiration Check
    ├── Audience Validation
    └── Issuer Validation
Claims Extraction
    ├── User Identity
    ├── Roles
    ├── Scopes
    └── Custom Claims
Security Principal Creation
    ├── ClaimsPrincipal
    ├── Identity (ClaimsIdentity)
    └── AuthenticationType
Authorization Middleware
    └── Policy Evaluation

Authentication in Clean Architecture

API Layer (REST/gRPC/GraphQL)
    ├── [Authorize] Attributes
    ├── Authentication Middleware
    └── Token Validation
Application Layer (DomainModel)
    ├── Access to ClaimsPrincipal
    ├── User Context
    └── Identity-Based Business Logic
Infrastructure Layer
    ├── Token Validation Services
    ├── Identity Provider Clients
    └── Certificate Management

Key Integration Points

Layer Component Responsibility
ApplicationModel AuthenticationExtensions Authentication scheme configuration
ApplicationModel AuthenticationMiddleware Token validation and principal creation
Options AuthenticationOptions Authentication configuration
ServiceModel Controllers/Endpoints Authorization attributes and policies
Infrastructure Token Validators JWT/OAuth2 token validation

Authentication Schemes

JWT (JSON Web Token) Bearer

Purpose: Stateless, token-based authentication using signed JSON tokens.

Use Cases: - API authentication - Mobile app authentication - Service-to-service communication - Single-page applications (SPAs)

Configuration:

// AuthenticationExtensions.cs
public static IServiceCollection AddMicroserviceAuthentication(
    this IServiceCollection services,
    IConfiguration configuration)
{
    var authOptions = configuration.GetSection("Authentication")
        .Get<AuthenticationOptions>();

    services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddJwtBearer(options =>
        {
            options.Authority = authOptions.Jwt?.Authority;
            options.Audience = authOptions.Jwt?.Audience;
            options.TokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuer = true,
                ValidateAudience = true,
                ValidateLifetime = true,
                ValidateIssuerSigningKey = true,
                ClockSkew = TimeSpan.FromSeconds(30),
                RequireExpirationTime = true,
                RequireSignedTokens = true
            };

            // Events for custom validation
            options.Events = new JwtBearerEvents
            {
                OnTokenValidated = context =>
                {
                    // Custom validation logic
                    return Task.CompletedTask;
                },
                OnAuthenticationFailed = context =>
                {
                    // Custom error handling
                    return Task.CompletedTask;
                }
            };
        });

    return services;
}

appsettings.json:

{
  "Authentication": {
    "Jwt": {
      "Authority": "https://login.microsoftonline.com/{tenant-id}/v2.0",
      "Audience": "api://your-api-id",
      "RequireHttpsMetadata": true,
      "ValidateIssuer": true,
      "ValidateAudience": true,
      "ValidateLifetime": true,
      "ClockSkew": "00:00:30"
    }
  }
}

Token Structure:

{
  "sub": "user-123",
  "name": "John Doe",
  "email": "john.doe@example.com",
  "roles": ["Admin", "User"],
  "scope": "api.read api.write",
  "tenant_id": "tenant-xyz",
  "iat": 1234567890,
  "exp": 1234571490,
  "iss": "https://login.microsoftonline.com/{tenant-id}/v2.0",
  "aud": "api://your-api-id"
}

Usage:

// Controller
[Authorize]
public class OrdersController : ControllerBase
{
    [HttpGet]
    public IActionResult GetOrders()
    {
        var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
        var roles = User.FindAll(ClaimTypes.Role).Select(c => c.Value);

        // Use identity information
        return Ok();
    }
}

OAuth2 / OpenID Connect (OIDC)

Purpose: Industry-standard authentication protocol with identity provider integration.

Use Cases: - Web applications - Enterprise SSO - Third-party integrations - Multi-tenant applications

OAuth2 Flows Supported:

  1. Authorization Code Flow (with PKCE)
  2. Web applications
  3. Mobile applications
  4. Secure client-side authentication

  5. Client Credentials Flow

  6. Service-to-service authentication
  7. Machine-to-machine communication
  8. API authentication

  9. Implicit Flow (deprecated, use Authorization Code with PKCE)

  10. Not recommended for new applications

Configuration:

// AuthenticationExtensions.cs
services.AddAuthentication(options =>
{
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
    options.Authority = authOptions.Oidc?.Authority;
    options.ClientId = authOptions.Oidc?.ClientId;
    options.ClientSecret = authOptions.Oidc?.ClientSecret;
    options.ResponseType = "code";
    options.SaveTokens = true;
    options.GetClaimsFromUserInfoEndpoint = true;
    options.Scope.Add("openid");
    options.Scope.Add("profile");
    options.Scope.Add("email");
    options.Scope.Add("api.read");

    // PKCE for enhanced security
    options.UsePkce = true;

    // Token validation
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuer = true,
        ValidateAudience = true,
        ValidateLifetime = true,
        ClockSkew = TimeSpan.FromSeconds(30)
    };

    // Events
    options.Events = new OpenIdConnectEvents
    {
        OnTokenValidated = context =>
        {
            // Custom claims transformation
            return Task.CompletedTask;
        },
        OnAuthorizationCodeReceived = context =>
        {
            // Handle authorization code
            return Task.CompletedTask;
        }
    };
});

appsettings.json:

{
  "Authentication": {
    "Oidc": {
      "Authority": "https://login.microsoftonline.com/{tenant-id}/v2.0",
      "ClientId": "your-client-id",
      "ClientSecret": "@Microsoft.KeyVault(SecretUri=...)",
      "CallbackPath": "/signin-oidc",
      "SignedOutCallbackPath": "/signout-callback-oidc",
      "ResponseType": "code",
      "SaveTokens": true,
      "GetClaimsFromUserInfoEndpoint": true,
      "Scopes": ["openid", "profile", "email", "api.read", "api.write"]
    }
  }
}

OIDC Discovery:

OIDC supports automatic discovery of configuration:

GET https://login.microsoftonline.com/{tenant-id}/v2.0/.well-known/openid-configuration

Response includes: - authorization_endpoint - token_endpoint - userinfo_endpoint - jwks_uri - issuer - supported_scopes

API Key Authentication

Purpose: Simple authentication using API keys for service-to-service or partner integrations.

Use Cases: - Service-to-service authentication - Partner API access - Internal automation - Limited-scope integrations

Configuration:

// AuthenticationExtensions.cs
services.AddAuthentication(options =>
{
    options.DefaultScheme = ApiKeyAuthenticationDefaults.AuthenticationScheme;
})
.AddScheme<ApiKeyAuthenticationSchemeOptions, ApiKeyAuthenticationHandler>(
    ApiKeyAuthenticationDefaults.AuthenticationScheme,
    options =>
    {
        options.HeaderName = authOptions.ApiKey?.HeaderName ?? "X-API-Key";
        options.QueryParameterName = authOptions.ApiKey?.QueryParameterName;
        options.RequireHttps = true;
    });

// API Key validation service
services.AddSingleton<IApiKeyValidator, ApiKeyValidator>();

API Key Handler:

// ApiKeyAuthenticationHandler.cs
public class ApiKeyAuthenticationHandler : AuthenticationHandler<ApiKeyAuthenticationSchemeOptions>
{
    private readonly IApiKeyValidator apiKeyValidator;

    public ApiKeyAuthenticationHandler(
        IOptionsMonitor<ApiKeyAuthenticationSchemeOptions> options,
        ILoggerFactory logger,
        UrlEncoder encoder,
        ISystemClock clock,
        IApiKeyValidator apiKeyValidator)
        : base(options, logger, encoder, clock)
    {
        this.apiKeyValidator = apiKeyValidator;
    }

    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        string? apiKey = null;

        // Check header
        if (this.Request.Headers.TryGetValue(this.Options.HeaderName, out var headerValue))
        {
            apiKey = headerValue.ToString();
        }
        // Check query parameter (if configured)
        else if (!string.IsNullOrEmpty(this.Options.QueryParameterName) &&
                 this.Request.Query.TryGetValue(this.Options.QueryParameterName, out var queryValue))
        {
            apiKey = queryValue.ToString();
        }

        if (string.IsNullOrEmpty(apiKey))
        {
            return AuthenticateResult.NoResult();
        }

        // Validate API key
        var validationResult = await this.apiKeyValidator.ValidateAsync(apiKey);
        if (!validationResult.IsValid)
        {
            return AuthenticateResult.Fail("Invalid API key");
        }

        // Create claims
        var claims = new List<Claim>
        {
            new Claim(ClaimTypes.NameIdentifier, validationResult.ClientId),
            new Claim("api_key_id", validationResult.ApiKeyId)
        };

        if (validationResult.Scopes != null)
        {
            foreach (var scope in validationResult.Scopes)
            {
                claims.Add(new Claim("scope", scope));
            }
        }

        var identity = new ClaimsIdentity(claims, this.Scheme.Name);
        var principal = new ClaimsPrincipal(identity);
        var ticket = new AuthenticationTicket(principal, this.Scheme.Name);

        return AuthenticateResult.Success(ticket);
    }
}

API Key Validator:

// ApiKeyValidator.cs
public interface IApiKeyValidator
{
    Task<ApiKeyValidationResult> ValidateAsync(string apiKey);
}

public class ApiKeyValidator : IApiKeyValidator
{
    private readonly IApiKeyStore apiKeyStore;
    private readonly ILogger<ApiKeyValidator> logger;

    public async Task<ApiKeyValidationResult> ValidateAsync(string apiKey)
    {
        // Hash API key for lookup
        var hashedKey = HashApiKey(apiKey);

        // Lookup in store
        var apiKeyRecord = await this.apiKeyStore.GetByHashedKeyAsync(hashedKey);
        if (apiKeyRecord == null)
        {
            return ApiKeyValidationResult.Invalid();
        }

        // Check expiration
        if (apiKeyRecord.ExpiresAt.HasValue && apiKeyRecord.ExpiresAt.Value < DateTime.UtcNow)
        {
            return ApiKeyValidationResult.Expired();
        }

        // Check revocation
        if (apiKeyRecord.IsRevoked)
        {
            return ApiKeyValidationResult.Revoked();
        }

        return ApiKeyValidationResult.Valid(
            apiKeyRecord.ClientId,
            apiKeyRecord.ApiKeyId,
            apiKeyRecord.Scopes);
    }

    private static string HashApiKey(string apiKey)
    {
        // Use SHA-256 for key hashing
        using var sha256 = SHA256.Create();
        var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(apiKey));
        return Convert.ToBase64String(hash);
    }
}

appsettings.json:

{
  "Authentication": {
    "ApiKey": {
      "HeaderName": "X-API-Key",
      "QueryParameterName": null,
      "RequireHttps": true,
      "Store": {
        "Type": "Database",
        "ConnectionString": "@Microsoft.KeyVault(SecretUri=...)"
      }
    }
  }
}

Usage:

[Authorize(AuthenticationSchemes = ApiKeyAuthenticationDefaults.AuthenticationScheme)]
public class ApiKeyController : ControllerBase
{
    [HttpGet("data")]
    public IActionResult GetData()
    {
        // API key validated, proceed
        return Ok();
    }
}

Azure Managed Identity

Purpose: Service-to-service authentication using Azure Managed Identity without storing credentials.

Use Cases: - Azure service authentication - Key Vault access - Azure Resource Manager operations - Azure Storage access

Configuration:

// AuthenticationExtensions.cs
services.AddAuthentication()
    .AddAzureManagedIdentity(options =>
    {
        options.ClientId = authOptions.ManagedIdentity?.ClientId; // For user-assigned
        // For system-assigned, don't set ClientId
    });

// Token acquisition
services.AddSingleton<IAzureManagedIdentityTokenProvider, AzureManagedIdentityTokenProvider>();

Managed Identity Token Provider:

// AzureManagedIdentityTokenProvider.cs
public interface IAzureManagedIdentityTokenProvider
{
    Task<string> GetAccessTokenAsync(string resource, string? clientId = null);
}

public class AzureManagedIdentityTokenProvider : IAzureManagedIdentityTokenProvider
{
    private readonly DefaultAzureCredential credential;

    public AzureManagedIdentityTokenProvider(string? clientId = null)
    {
        var options = new DefaultAzureCredentialOptions();
        if (!string.IsNullOrEmpty(clientId))
        {
            options.ManagedIdentityClientId = clientId;
        }
        this.credential = new DefaultAzureCredential(options);
    }

    public async Task<string> GetAccessTokenAsync(string resource, string? clientId = null)
    {
        var tokenRequestContext = new TokenRequestContext(new[] { $"{resource}/.default" });
        var token = await this.credential.GetTokenAsync(tokenRequestContext);
        return token.Token;
    }
}

Usage:

public class KeyVaultService
{
    private readonly IAzureManagedIdentityTokenProvider tokenProvider;

    public async Task<string> GetSecretAsync(string secretName)
    {
        var token = await this.tokenProvider.GetAccessTokenAsync(
            "https://vault.azure.net");

        // Use token to access Key Vault
    }
}

Authentication Options

Options Configuration

// AuthenticationOptions.cs
namespace ConnectSoft.MicroserviceTemplate.Options
{
    using System.ComponentModel.DataAnnotations;

    public sealed class AuthenticationOptions
    {
        public const string SectionName = "Authentication";

        [Required]
        required public string DefaultScheme { get; set; } = "Bearer";

        public JwtAuthenticationOptions? Jwt { get; set; }
        public OidcAuthenticationOptions? Oidc { get; set; }
        public ApiKeyAuthenticationOptions? ApiKey { get; set; }
        public ManagedIdentityOptions? ManagedIdentity { get; set; }
    }

    public sealed class JwtAuthenticationOptions
    {
        [Required]
        required public string Authority { get; set; }

        [Required]
        required public string Audience { get; set; }

        public bool RequireHttpsMetadata { get; set; } = true;
        public bool ValidateIssuer { get; set; } = true;
        public bool ValidateAudience { get; set; } = true;
        public bool ValidateLifetime { get; set; } = true;
        public TimeSpan ClockSkew { get; set; } = TimeSpan.FromSeconds(30);
    }

    public sealed class OidcAuthenticationOptions
    {
        [Required]
        required public string Authority { get; set; }

        [Required]
        required public string ClientId { get; set; }

        public string? ClientSecret { get; set; }
        public string CallbackPath { get; set; } = "/signin-oidc";
        public string SignedOutCallbackPath { get; set; } = "/signout-callback-oidc";
        public string ResponseType { get; set; } = "code";
        public bool SaveTokens { get; set; } = true;
        public bool GetClaimsFromUserInfoEndpoint { get; set; } = true;
        public List<string> Scopes { get; set; } = new();
        public bool UsePkce { get; set; } = true;
    }

    public sealed class ApiKeyAuthenticationOptions
    {
        public string HeaderName { get; set; } = "X-API-Key";
        public string? QueryParameterName { get; set; }
        public bool RequireHttps { get; set; } = true;
        public ApiKeyStoreOptions? Store { get; set; }
    }

    public sealed class ManagedIdentityOptions
    {
        public string? ClientId { get; set; } // For user-assigned identity
    }
}

Middleware Configuration

Program.cs Setup

// Program.cs
var builder = WebApplication.CreateBuilder(args);

// Add authentication services
builder.Services.AddMicroserviceAuthentication(builder.Configuration);

// Add authorization services
builder.Services.AddAuthorization(options =>
{
    options.FallbackPolicy = options.DefaultPolicy;
});

var app = builder.Build();

// Configure middleware pipeline
app.UseRouting();

// Authentication must be after routing
app.UseAuthentication();

// Authorization must be after authentication
app.UseAuthorization();

app.MapControllers();

await app.RunAsync();

Middleware Order

Critical Order: 1. Routing (UseRouting) 2. Authentication (UseAuthentication) 3. Authorization (UseAuthorization) 4. Endpoints (MapControllers, MapGrpcService)

Why This Order: - Routing determines which endpoint handles the request - Authentication establishes identity - Authorization uses identity for policy evaluation - Endpoints execute with authenticated/authorized context

Claims and Identity

Accessing Claims

// In controllers
[Authorize]
public class OrdersController : ControllerBase
{
    [HttpGet]
    public IActionResult GetOrders()
    {
        // Get user ID
        var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;

        // Get email
        var email = User.FindFirst(ClaimTypes.Email)?.Value;

        // Get name
        var name = User.FindFirst(ClaimTypes.Name)?.Value;

        // Get roles
        var roles = User.FindAll(ClaimTypes.Role)
            .Select(c => c.Value)
            .ToList();

        // Get custom claims
        var tenantId = User.FindFirst("tenant_id")?.Value;
        var scope = User.FindFirst("scope")?.Value;

        // Check if user is authenticated
        if (User.Identity?.IsAuthenticated == true)
        {
            // User is authenticated
        }

        return Ok();
    }
}

Claims Transformation

// ClaimsTransformer.cs
public class ClaimsTransformer : IClaimsTransformation
{
    public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
    {
        var identity = principal.Identity as ClaimsIdentity;
        if (identity == null)
        {
            return Task.FromResult(principal);
        }

        // Add custom claims
        if (!identity.HasClaim("custom_claim", "value"))
        {
            identity.AddClaim(new Claim("custom_claim", "value"));
        }

        // Transform claims
        var emailClaim = identity.FindFirst(ClaimTypes.Email);
        if (emailClaim != null)
        {
            var domain = emailClaim.Value.Split('@')[1];
            identity.AddClaim(new Claim("domain", domain));
        }

        return Task.FromResult(principal);
    }
}

// Registration
services.AddScoped<IClaimsTransformation, ClaimsTransformer>();

Token Validation

JWT Token Validation

Automatic Validation: - Signature verification using issuer's public key - Expiration (exp) claim validation - Not-before (nbf) claim validation - Issuer (iss) validation - Audience (aud) validation

Custom Validation:

// JwtBearerEvents
options.Events = new JwtBearerEvents
{
    OnTokenValidated = context =>
    {
        var claims = context.Principal?.Claims;

        // Custom validation logic
        var tenantId = claims?.FirstOrDefault(c => c.Type == "tenant_id")?.Value;
        if (string.IsNullOrEmpty(tenantId))
        {
            context.Fail("Missing tenant_id claim");
            return Task.CompletedTask;
        }

        // Check token revocation (if using token revocation list)
        var tokenId = context.SecurityToken?.Id;
        if (await IsTokenRevokedAsync(tokenId))
        {
            context.Fail("Token has been revoked");
            return Task.CompletedTask;
        }

        return Task.CompletedTask;
    },

    OnAuthenticationFailed = context =>
    {
        this.logger.LogError(
            context.Exception,
            "JWT authentication failed: {Error}",
            context.Exception.Message);

        return Task.CompletedTask;
    },

    OnChallenge = context =>
    {
        // Custom challenge response
        context.HandleResponse();
        context.Response.StatusCode = 401;
        context.Response.WriteAsJsonAsync(new
        {
            error = "Unauthorized",
            message = "Authentication required"
        });

        return Task.CompletedTask;
    }
};

Token Refresh

Refresh Token Flow:

// TokenRefreshService.cs
public interface ITokenRefreshService
{
    Task<TokenRefreshResult> RefreshTokenAsync(string refreshToken);
}

public class TokenRefreshService : ITokenRefreshService
{
    private readonly IHttpClientFactory httpClientFactory;
    private readonly IConfiguration configuration;

    public async Task<TokenRefreshResult> RefreshTokenAsync(string refreshToken)
    {
        var client = this.httpClientFactory.CreateClient();

        var tokenRequest = new
        {
            grant_type = "refresh_token",
            refresh_token = refreshToken,
            client_id = this.configuration["Authentication:Oidc:ClientId"],
            client_secret = this.configuration["Authentication:Oidc:ClientSecret"]
        };

        var response = await client.PostAsJsonAsync(
            this.configuration["Authentication:Oidc:TokenEndpoint"],
            tokenRequest);

        if (!response.IsSuccessStatusCode)
        {
            return TokenRefreshResult.Failed("Token refresh failed");
        }

        var tokenResponse = await response.Content.ReadFromJsonAsync<TokenResponse>();
        return TokenRefreshResult.Success(
            tokenResponse.AccessToken,
            tokenResponse.RefreshToken,
            tokenResponse.ExpiresIn);
    }
}

Integration with Clean Architecture

Accessing Identity in Domain Layer

Through Application Layer:

// Processor.cs
public class CreateOrderProcessor : ICreateOrderProcessor
{
    private readonly IHttpContextAccessor httpContextAccessor;

    public CreateOrderProcessor(IHttpContextAccessor httpContextAccessor)
    {
        this.httpContextAccessor = httpContextAccessor;
    }

    public async Task<CreateOrderOutput> ProcessAsync(CreateOrderInput input)
    {
        // Get current user from HttpContext
        var userId = this.httpContextAccessor.HttpContext?.User
            .FindFirst(ClaimTypes.NameIdentifier)?.Value;

        // Pass to domain logic
        var order = Order.Create(
            input.OrderData,
            userId ?? throw new UnauthorizedException());

        // Domain logic doesn't directly depend on HttpContext
        return new CreateOrderOutput { OrderId = order.Id };
    }
}

User Context Service

Abstraction for Identity:

// IUserContext.cs
public interface IUserContext
{
    string? UserId { get; }
    string? Email { get; }
    string? Name { get; }
    bool IsAuthenticated { get; }
    IEnumerable<string> Roles { get; }
    string? GetClaimValue(string claimType);
}

// UserContext.cs
public class UserContext : IUserContext
{
    private readonly IHttpContextAccessor httpContextAccessor;

    public UserContext(IHttpContextAccessor httpContextAccessor)
    {
        this.httpContextAccessor = httpContextAccessor;
    }

    public string? UserId => this.httpContextAccessor.HttpContext?.User
        .FindFirst(ClaimTypes.NameIdentifier)?.Value;

    public string? Email => this.httpContextAccessor.HttpContext?.User
        .FindFirst(ClaimTypes.Email)?.Value;

    public string? Name => this.httpContextAccessor.HttpContext?.User
        .FindFirst(ClaimTypes.Name)?.Value;

    public bool IsAuthenticated => this.httpContextAccessor.HttpContext?.User
        .Identity?.IsAuthenticated ?? false;

    public IEnumerable<string> Roles => this.httpContextAccessor.HttpContext?.User
        .FindAll(ClaimTypes.Role)
        .Select(c => c.Value) ?? Enumerable.Empty<string>();

    public string? GetClaimValue(string claimType) =>
        this.httpContextAccessor.HttpContext?.User
            .FindFirst(claimType)?.Value;
}

// Registration
services.AddHttpContextAccessor();
services.AddScoped<IUserContext, UserContext>();

// Usage in processors
public class CreateOrderProcessor
{
    private readonly IUserContext userContext;

    public CreateOrderProcessor(IUserContext userContext)
    {
        this.userContext = userContext;
    }

    public async Task<CreateOrderOutput> ProcessAsync(CreateOrderInput input)
    {
        if (!this.userContext.IsAuthenticated)
        {
            throw new UnauthorizedException();
        }

        var order = Order.Create(input.OrderData, this.userContext.UserId!);
        // ...
    }
}

Multi-Tenant Authentication

Tenant Isolation

Tenant-Specific Claims:

// Tenant-aware authentication
options.Events = new JwtBearerEvents
{
    OnTokenValidated = async context =>
    {
        var tenantId = context.Principal?.FindFirst("tenant_id")?.Value;
        if (string.IsNullOrEmpty(tenantId))
        {
            context.Fail("Missing tenant_id claim");
            return;
        }

        // Validate tenant exists and is active
        var tenantValidator = context.HttpContext.RequestServices
            .GetRequiredService<ITenantValidator>();

        if (!await tenantValidator.IsValidAsync(tenantId))
        {
            context.Fail("Invalid or inactive tenant");
            return;
        }

        // Add tenant context to claims
        var identity = context.Principal?.Identity as ClaimsIdentity;
        identity?.AddClaim(new Claim("tenant_validated", "true"));
    }
};

Tenant Context:

// ITenantContext.cs
public interface ITenantContext
{
    string? TenantId { get; }
    bool IsMultiTenant { get; }
}

// TenantContext.cs
public class TenantContext : ITenantContext
{
    private readonly IUserContext userContext;

    public TenantContext(IUserContext userContext)
    {
        this.userContext = userContext;
    }

    public string? TenantId => this.userContext.GetClaimValue("tenant_id");
    public bool IsMultiTenant => !string.IsNullOrEmpty(this.TenantId);
}

Configuration

appsettings.json

{
  "Authentication": {
    "DefaultScheme": "Bearer",
    "Jwt": {
      "Authority": "https://login.microsoftonline.com/{tenant-id}/v2.0",
      "Audience": "api://your-api-id",
      "RequireHttpsMetadata": true,
      "ValidateIssuer": true,
      "ValidateAudience": true,
      "ValidateLifetime": true,
      "ClockSkew": "00:00:30"
    },
    "Oidc": {
      "Authority": "https://login.microsoftonline.com/{tenant-id}/v2.0",
      "ClientId": "your-client-id",
      "ClientSecret": "@Microsoft.KeyVault(SecretUri=...)",
      "CallbackPath": "/signin-oidc",
      "SignedOutCallbackPath": "/signout-callback-oidc",
      "ResponseType": "code",
      "SaveTokens": true,
      "GetClaimsFromUserInfoEndpoint": true,
      "UsePkce": true,
      "Scopes": [
        "openid",
        "profile",
        "email",
        "api.read",
        "api.write"
      ]
    },
    "ApiKey": {
      "HeaderName": "X-API-Key",
      "QueryParameterName": null,
      "RequireHttps": true,
      "Store": {
        "Type": "Database",
        "ConnectionString": "@Microsoft.KeyVault(SecretUri=...)"
      }
    },
    "ManagedIdentity": {
      "ClientId": null
    }
  }
}

Environment Variables

# JWT Configuration
Authentication__Jwt__Authority=https://login.microsoftonline.com/{tenant-id}/v2.0
Authentication__Jwt__Audience=api://your-api-id

# OIDC Configuration
Authentication__Oidc__ClientId=your-client-id
Authentication__Oidc__ClientSecret=@Microsoft.KeyVault(SecretUri=...)

# API Key Configuration
Authentication__ApiKey__HeaderName=X-API-Key

Best Practices

Do's

  1. Always Validate Tokens

    // ✅ GOOD - Validate all token claims
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuer = true,
        ValidateAudience = true,
        ValidateLifetime = true,
        ValidateIssuerSigningKey = true
    };
    

  2. Use HTTPS for Token Transmission

    // ✅ GOOD - Require HTTPS
    options.RequireHttpsMetadata = true;
    

  3. Implement Token Expiration

    // ✅ GOOD - Short-lived tokens
    "AccessTokenLifetime": "00:15:00", // 15 minutes
    "RefreshTokenLifetime": "30.00:00:00" // 30 days
    

  4. Use PKCE for OAuth2

    // ✅ GOOD - PKCE for enhanced security
    options.UsePkce = true;
    

  5. Store Secrets Securely

    // ✅ GOOD - Key Vault reference
    {
      "ClientSecret": "@Microsoft.KeyVault(SecretUri=...)"
    }
    

  6. Log Authentication Failures

    // ✅ GOOD - Log for security monitoring
    options.Events = new JwtBearerEvents
    {
        OnAuthenticationFailed = context =>
        {
            this.logger.LogWarning("Authentication failed: {Error}",
                context.Exception.Message);
            return Task.CompletedTask;
        }
    };
    

Don'ts

  1. Don't Skip Token Validation

    // ❌ BAD - Missing validation
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuer = false, // ❌ Don't skip!
        ValidateAudience = false // ❌ Don't skip!
    };
    

  2. Don't Use Long-Lived Tokens

    // ❌ BAD - Token valid for too long
    {
      "AccessTokenLifetime": "365.00:00:00" // ❌ 1 year is too long!
    }
    

  3. Don't Store Secrets in Code

    // ❌ BAD - Hardcoded secret
    options.ClientSecret = "hardcoded-secret-123";
    
    // ✅ GOOD - Configuration with Key Vault
    options.ClientSecret = configuration["Authentication:Oidc:ClientSecret"];
    

  4. Don't Use HTTP for Token Transmission

    // ❌ BAD - Allows HTTP
    options.RequireHttpsMetadata = false;
    

  5. Don't Expose Token Details in Errors

    // ❌ BAD - Exposes token details
    catch (Exception ex)
    {
        return BadRequest($"Token error: {ex.Message}");
    }
    
    // ✅ GOOD - Generic error message
    catch (Exception ex)
    {
        this.logger.LogError(ex, "Token validation failed");
        return Unauthorized();
    }
    

Troubleshooting

Issue: Token Validation Fails

Symptoms: 401 Unauthorized responses, authentication failures.

Solutions: - Verify token issuer matches Authority configuration - Check token audience matches Audience configuration - Verify token hasn't expired (check exp claim) - Ensure token signature is valid (check issuer's public key) - Verify clock skew settings (system clock differences)

Issue: Claims Not Available

Symptoms: Claims are missing from User.Claims.

Solutions: - Check token contains expected claims - Verify GetClaimsFromUserInfoEndpoint is enabled for OIDC - Use claims transformation to add missing claims - Check token scopes include required claims

Issue: API Key Not Validated

Symptoms: API key requests return unauthorized.

Solutions: - Verify API key header name matches configuration - Check API key exists in store - Verify API key hasn't expired - Check API key isn't revoked - Ensure HTTPS is used if RequireHttps is enabled

Issue: OIDC Redirect Loop

Symptoms: Infinite redirects during authentication.

Solutions: - Verify CallbackPath matches identity provider configuration - Check SignedOutCallbackPath is configured correctly - Ensure cookies are enabled and working - Verify SaveTokens is enabled if needed

Summary

Authentication in the ConnectSoft Microservice Template provides:

  • Multiple Schemes: JWT, OAuth2/OIDC, API Keys, Managed Identity
  • Token Validation: Automatic validation with expiration and signature verification
  • Claims-Based Identity: Rich identity information through claims
  • Flexible Configuration: Environment-specific authentication setup
  • Azure Integration: Native support for Azure AD and Managed Identity
  • Clean Architecture: Framework-agnostic identity abstraction
  • Security Best Practices: HTTPS enforcement, token expiration, PKCE support
  • Multi-Tenant Support: Tenant-aware authentication and validation

By implementing authentication, teams can:

  • Verify Identity: Establish who is making requests
  • Enable Authorization: Provide identity context for authorization decisions
  • Support Multiple Clients: Browser apps, mobile apps, services
  • Integrate with Identity Providers: Azure AD, Auth0, custom providers
  • Secure Service-to-Service Communication: API keys and managed identities
  • Maintain Security: Token validation, expiration, revocation support

Authentication is the foundation of security, establishing identity that enables authorization, audit logging, and secure multi-tenant operations. The template's authentication framework provides a solid base for building secure microservices that can authenticate users and services across diverse client types and identity providers.