Skip to content

Instantly share code, notes, and snippets.

@rbrayb
Created April 5, 2025 23:43
Show Gist options
  • Save rbrayb/b7fabb3d79eb8e5cdd8421ace70846b3 to your computer and use it in GitHub Desktop.
Save rbrayb/b7fabb3d79eb8e5cdd8421ace70846b3 to your computer and use it in GitHub Desktop.
Validating the ID and Access JWT signature in Entra External ID
using Microsoft.IdentityModel.Logging;
using Microsoft.IdentityModel.Tokens;
using Newtonsoft.Json.Linq;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Cryptography;
using System.Text;
class Program
{
// https://xsreality.medium.com/making-azure-ad-oidc-compliant-5734b70c43ff
private static readonly HttpClient client = new HttpClient();
static string? idToken = null;
static string? accessToken = null;
static async Task Main(string[] args)
{
IdentityModelEventSource.ShowPII = true; // Enable PII logging
IdentityModelEventSource.LogCompleteSecurityArtifact = true;
string clientId = "22fc...6342";
string username = "[email protected]";
string password = "Th...90";
string idTokenAud = "22fc...6342";
string idTokenIss = "https://263f...2b2a.ciamlogin.com/263f...2b2a/v2.0";
//string accessTokenAud = "00000003-0000-0000-c000-000000000000";
//string accessTokenIss = "https://sts.windows.net/263f...2b2a/";
string accessTokenAud = "22fc...6342";
string accessTokenIss = "https://263f...2b2a.ciamlogin.com/263f...2b2a/v2.0";
string continuationToken = await StartSignUp(clientId, username);
continuationToken = await Challenge(clientId, continuationToken);
await GetToken(clientId, password, continuationToken);
Console.WriteLine("\nID Token");
await ValidateToken(idToken, idTokenAud, idTokenIss);
Console.WriteLine("\nAccess token");
await ValidateToken(accessToken, accessTokenAud, accessTokenIss);
CreatePEM();
}
static async Task<string> StartSignUp(string clientId, string username)
{
var values = new Dictionary<string, string>
{
{ "client_id", clientId },
{ "challenge_type", "password redirect" },
{ "username", username },
{ "redirect_uri", "urn:ietf:wg:oauth:2.0:oob" } // Add redirect_uri parameter
};
var content = new FormUrlEncodedContent(values);
client.DefaultRequestHeaders.Host = "tenant.ciamlogin.com";
var response = await client.PostAsync("https://tenant.ciamlogin.com/tenant.onmicrosoft.com/oauth2/v2.0/initiate", content);
var responseString = await response.Content.ReadAsStringAsync();
var jsonResponse = Newtonsoft.Json.Linq.JObject.Parse(responseString);
return jsonResponse["continuation_token"].ToString();
}
static async Task<string> Challenge(string clientId, string continuationToken)
{
var values = new Dictionary<string, string>
{
{ "client_id", clientId },
{ "challenge_type", "password redirect" },
{ "continuation_token", continuationToken }
};
var content = new FormUrlEncodedContent(values);
var response = await client.PostAsync("https://tenant.ciamlogin.com/tenant.onmicrosoft.com/oauth2/v2.0/challenge", content);
var responseString = await response.Content.ReadAsStringAsync();
var jsonResponse = Newtonsoft.Json.Linq.JObject.Parse(responseString);
return jsonResponse["continuation_token"].ToString();
}
static async Task GetToken(string clientId, string password, string continuationToken)
{
var values = new Dictionary<string, string>
{
{ "client_id", clientId },
{ "password", password },
{ "continuation_token", continuationToken },
{ "grant_type", "password" },
//{ "scope", "openid offline_access" }
{ "scope", "openid offline_access api://validate/ValidateJWT"}
};
var content = new FormUrlEncodedContent(values);
var response = await client.PostAsync("https://tenant.ciamlogin.com/tenant.onmicrosoft.com/oauth2/v2.0/token", content);
var responseString = await response.Content.ReadAsStringAsync();
//Console.WriteLine(responseString);
var jsonResponse = Newtonsoft.Json.Linq.JObject.Parse(responseString);
idToken = jsonResponse["id_token"]?.ToString();
if (idToken != null)
{
Console.WriteLine("\nID Token: " + idToken);
}
else
{
Console.WriteLine("ID Token not found in the response.");
}
accessToken = jsonResponse["access_token"]?.ToString();
if (accessToken != null)
{
Console.WriteLine("\nAccess Token: " + accessToken);
}
else
{
Console.WriteLine("Access Token not found in the response.");
}
}
static async Task<bool> ValidateToken(string token, string aud, string iss)
{
var wellKnownEndpoint = "https://tenant.ciamlogin.com/tenant.onmicrosoft.com/.well-known/openid-configuration";
var response = await client.GetAsync(wellKnownEndpoint);
var responseString = await response.Content.ReadAsStringAsync();
var jsonResponse = JObject.Parse(responseString);
var jwksUri = jsonResponse["jwks_uri"].ToString();
var keysResponse = await client.GetAsync(jwksUri);
var keysResponseString = await keysResponse.Content.ReadAsStringAsync();
var keys = JObject.Parse(keysResponseString)["keys"];
var signingKeys = new List<SecurityKey>();
foreach (var key in keys)
{
var kty = key["kty"].ToString();
if (kty == "RSA")
{
var rsa = new RsaSecurityKey(new RSAParameters
{
Modulus = Base64UrlEncoder.DecodeBytes(key["n"].ToString()),
Exponent = Base64UrlEncoder.DecodeBytes(key["e"].ToString())
})
{
KeyId = key["kid"].ToString()
};
signingKeys.Add(rsa);
}
}
var tokenHandler = new JwtSecurityTokenHandler();
var validationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = iss,
ValidateAudience = true,
ValidAudience = aud,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKeys = signingKeys,
RequireExpirationTime = true,
RequireSignedTokens = true,
RequireAudience = true,
};
try
{
tokenHandler.ValidateToken(token, validationParameters, out SecurityToken validatedToken);
Console.WriteLine("\n" + "Token is valid.");
return true;
}
catch (Exception ex)
{
Console.WriteLine("\n" + "Token validation failed: " + ex.Message);
//Console.WriteLine("\n" + "Token: " + token);
Console.WriteLine("\n" + "Keys: " + string.Join(", ", signingKeys.Select(k => k.KeyId)));
return false;
}
}
static void CreatePEM()
{
// n
string modulus = "hz6...7xw";
// e
string exponent = "AQAB";
RSAParameters rsaParameters = new RSAParameters
{
Modulus = Base64UrlEncoder.DecodeBytes(modulus),
Exponent = Base64UrlEncoder.DecodeBytes(exponent)
};
using (var rsa = RSA.Create())
{
rsa.ImportParameters(rsaParameters);
string publicKeyPem = ExportPublicKeyToPem(rsa);
Console.WriteLine(publicKeyPem);
}
}
public static string ExportPublicKeyToPem(RSA rsa)
{
var sb = new StringBuilder();
sb.AppendLine("\n");
sb.AppendLine("-----BEGIN PUBLIC KEY-----");
sb.AppendLine(Convert.ToBase64String(rsa.ExportSubjectPublicKeyInfo(), Base64FormattingOptions.InsertLineBreaks));
sb.AppendLine("-----END PUBLIC KEY-----");
return sb.ToString();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment