I am implementing SSO in my application via Microsoft Entra. It has a client application with Cloud APIs.
The Client passes an Entra JWT to the server obtained from the MS Entra libraries. My database has a list of Users with each user under a tenant. Each tenant has only one possible Entra tenant ID, and this list is collected to tell the validator what tenants to accept in the below code (and later used with the email address to validate that the user is allowed to log in from that Entra tenant).
So for the purposes of my app's security model, is there an obvious way to pass a JWT to the below code that impersonates both the issuer and email address and could therefore send a correct tenant ID and email to the authentication code?
using Dapper;
using HealthLibrary;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Protocols;
using Microsoft.IdentityModel.Tokens;
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Data.SqlClient;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Threading;
using System.Web.Configuration;
using System.Threading.Tasks;
using System.Text.RegularExpressions;
//called from the web service with token, IP address of user and terminal name of the user
public async Task<ApiResponse> ValidateJwt(string token, string ip, string terminalName)
{
if (string.IsNullOrEmpty(token))
{
throw new Exception("No token was supplied");
}
// Get list of valid Entra tenant IDs
List<string> validTenantIds = validTenantIDs();
var claimsPrincipal = await ValidateToken(token, validTenantIds);
// Extract Tenant ID
var tenantId = claimsPrincipal.Claims.FirstOrDefault(c => c.Type == "iss")?.Value;
if (string.IsNullOrEmpty(tenantId))
{
throw new Exception("No Entra tenant ID supplied");
}
tenantId = extractGuid(tenantId);
// Extract Email
var email = claimsPrincipal.Claims.FirstOrDefault(c => c.Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn")?.Value;
if (string.IsNullOrEmpty(email))
{
throw new Exception("Email missing");
}
return GenerateAppKey(email, tenantId, ip, terminalName);
}
//Gets valid tenant IDs from the database and links them with the Entra issuer URL
private List<string> validTenantIDs()
{
using (SqlConnection connection = new SqlConnection(ConfigurationManager.ConnectionStrings["ssoConnection"].ConnectionString))
{
var procedure = WebConfigurationManager.AppSettings["DatabaseName"] + ".dbo.SSO_GetValidTenants";
var p = new DynamicParameters();
var results = connection.Query(procedure, p, commandType: CommandType.StoredProcedure, commandTimeout: 120).ToList();
List<string> list = new List<string>();
foreach (var result in results)
{
list.Add("https://sts.windows.net/" + result.MicrosoftEntraTenantID + "/");
}
return list;
}
}
//Extract the GUID from the Entra URL for validation purposes later
private static string extractGuid(string url)
{
//extract the GUID from the Entra URL
var pattern = @"https:\/\/sts\.windows\.net\/([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})\/";
var match = Regex.Match(url, pattern);
if (match.Success && match.Groups.Count > 1)
{
return match.Groups[1].Value;
}
throw new Exception("No GUID could be identified from the Azure URL. Please contact your administrator for assistance");
}
//validates the token and returns a ClaimsPrincipal object
private async Task< ClaimsPrincipal> ValidateToken(string token, List<string> validTenantIds)
{
string invalidTokenMessage = "A sign in was attempted for an invalid Entra tenant."
+ Environment.NewLine
+ "Your site may not be set up for Single Sign On - please contact your Administrator for more details.";
try
{
var tokenHandler = new JwtSecurityTokenHandler();
var configurationManager = new ConfigurationManager<OpenIdConnectConfiguration>(
"https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration",
new OpenIdConnectConfigurationRetriever());
var openIdConfig = configurationManager.GetConfigurationAsync(CancellationToken.None);
var validationParameters = new TokenValidationParameters
{
IssuerSigningKeys = openIdConfig.Result.SigningKeys,
ValidIssuers = validTenantIds,
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidAudience = "api://" + WebConfigurationManager.AppSettings["EntraClientID"]
};
return tokenHandler.ValidateToken(token, validationParameters, out _);
}
catch(SecurityTokenInvalidIssuerException)
{
throw new Exception(invalidTokenMessage);
}
}
usingstatements at least. As a guess,ValidateTokenonly catchesSecurityTokenInvalidIssuerExceptionto rethrow it as genericException, and all other validation exceptions (I assume there's alsoSecurityTokenInvalidAudienceException, for example, unless the library is doing something in a deeply wrong/confusing way) are kept as-is - is that intentional? \$\endgroup\$