Created
April 5, 2025 23:43
-
-
Save rbrayb/b7fabb3d79eb8e5cdd8421ace70846b3 to your computer and use it in GitHub Desktop.
Validating the ID and Access JWT signature in Entra External ID
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
https://medium.com/the-new-control-plane/validating-the-id-and-access-jwt-signature-in-entra-external-id-d363a0e9dbb2