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:
- Authorization Code Flow (with PKCE)
- Web applications
- Mobile applications
-
Secure client-side authentication
-
Client Credentials Flow
- Service-to-service authentication
- Machine-to-machine communication
-
API authentication
-
Implicit Flow (deprecated, use Authorization Code with PKCE)
- 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:
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¶
-
Always Validate Tokens
-
Use HTTPS for Token Transmission
-
Implement Token Expiration
-
Use PKCE for OAuth2
-
Store Secrets Securely
-
Log Authentication Failures
Don'ts¶
-
Don't Skip Token Validation
-
Don't Use Long-Lived Tokens
-
Don't Store Secrets in Code
-
Don't Use HTTP for Token Transmission
-
Don't Expose Token Details in Errors
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
Related Documentation¶
- Authorization: Policy-based authorization
- Security: Overall security architecture
- Secrets Management: Secure secret storage
- CORS: Cross-origin resource sharing
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.