Hi Edward,
The code is available in the thread above.
And it’s possible that your JWT is correct and yet you are getting the error.
Check the consent part.
Bikash Shah
Hi Edward,
The code is available in the thread above.
And it’s possible that your JWT is correct and yet you are getting the error.
Check the consent part.
Bikash Shah
I could make working prototype for getting access and refresh tokens for IRS IRIS A2A.
The code is in C# and partially generated by MS Copilot, just be aware of that.
Pay attention:
...&assertion=<UserJWT>&...&client_assertion=<Client JWT>In the end there is also a function for generating JSON Web Key which you upload to IRS website when you request for Client ID.
using Newtonsoft.Json;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Net.Http.Headers;
using Azure.Identity;
using Azure.Security.KeyVault.Keys.Cryptography;
using Azure.Security.KeyVault.Keys;
internal class Program
{
private static void Main(string[] args)
{
string accessToken, refreshToken;
int accessTokenExpiresIn;
string userID = "<your user ID>";
string clientID = "<your client ID>";
string keyID = "<your key ID>";
RequestTokens(keyID, userID, clientID, out accessToken, out refreshToken, out accessTokenExpiresIn);
}
public static void RequestTokens(string keyID, string userID, string clientID, out string accessToken, out string refreshToken, out int accessTokenExpiresIn)
{
string grantType = "urn:ietf:params:oauth:grant-type:jwt-bearer";
string clientAssertionType = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
string authUrl = "https://api.www4.irs.gov/auth/oauth/v2/token"; // Replace with your actual auth URL
string issuer = clientID;
string audience = authUrl;
string subject;
subject = userID;
string userJwt = CreateJWT(keyID, issuer, subject, audience);
subject = clientID;
string clientJwt = CreateJWT(keyID, issuer, subject, audience);
// Prepare the request body
string requestBody = string.Format(
"grant_type={0}&assertion={1}&client_assertion_type={2}&client_assertion={3}",
grantType,
userJwt,
clientAssertionType,
clientJwt);
SendPOSTHttpRequestUrlEncoded(requestBody, authUrl, out HttpResponseMessage httpResponseMessage);
// Parse the response
string responseText = httpResponseMessage.Content.ReadAsStringAsync().Result;
if (!GetTokensFromResponse(
responseText,
out accessToken,
out refreshToken,
out accessTokenExpiresIn))
{
Console.WriteLine($"Failed to parse response: {responseText}");
Console.WriteLine($"Status code: {httpResponseMessage.StatusCode}");
}
else
{
Console.WriteLine($"Access token: {accessToken}");
Console.WriteLine($"Refresh token: {refreshToken}");
Console.WriteLine($"Expires in: {accessTokenExpiresIn} seconds");
}
}
private static string CreateJWT(string keyID, string issuer, string subject, string audience)
{
// Create JWT header
string JWTSignAlgorithm = "RS256"; // replace with your actual signing algorithm if required
string base64Header = CreateJWTHeader(keyID, JWTSignAlgorithm);
// Prepare the JWT payload
DateTime issuedAtTime = DateTime.UtcNow;
// Token valid for 15 minutes
DateTime expirationTime = issuedAtTime.AddMinutes(15);
// Create unique JWT ID
string jwtID = $"{Guid.NewGuid()} {DateTime.UtcNow.ToString("o")}"; // make your own unique ID if required
// Create the JWT payload
string base64Payload = CreateJWTPayload(issuer, subject, audience, issuedAtTime, expirationTime, jwtID);
// Sign the JWT
string signature = CreateSignature(base64Header, base64Payload);
// Combine all parts into complete JWT
return $"{base64Header}.{base64Payload}.{signature}";
}
private static string CreateJWTHeader(string keyID, string signAlgorithm)
{
var jwtHeader = new Dictionary<string, object>
{
{ "kid", keyID },
{ "alg", signAlgorithm }
};
string jwtHeaderText = JsonConvert.SerializeObject(jwtHeader);
return Base64UrlEncode(Encoding.UTF8.GetBytes(jwtHeaderText));
}
private static string CreateJWTPayload(string issuer, string subject, string audience, DateTime issuedAtTime, DateTime expirationTime, string jwtID)
{
var jwtPayload = new Dictionary<string, object>
{
{ "iss", issuer },
{ "sub", subject },
{ "aud", audience },
{ "iat", GetEpochTime(issuedAtTime) },
{ "exp", GetEpochTime(expirationTime) },
{ "jti", jwtID }
};
string jwtPayloadText = JsonConvert.SerializeObject(jwtPayload);
return Base64UrlEncode(Encoding.UTF8.GetBytes(jwtPayloadText));
}
private static string CreateSignature(string base64Header, string base64Payload)
{
// Create the string to sign (header.payload)
string stringToSign = $"{base64Header}.{base64Payload}";
// Get certificate
string pfxFilePath = "<YourPathToPfxFile>.pfx"; // the certificate that you used to create JSON Web Key which you uploaded to IRS website when you requested the client ID
// Load the PFX file and extract the private key
var certificate = new X509Certificate2(pfxFilePath, "", X509KeyStorageFlags.Exportable);
using (RSA? rsa = certificate.GetRSAPrivateKey())
{
if (rsa == null)
throw new InvalidOperationException("Private key not found in the PFX file.");
// Convert the string to sign to bytes
byte[] dataToSign = Encoding.UTF8.GetBytes(stringToSign);
// Sign the data with SHA-256
byte[] signatureBytes = rsa.SignData(dataToSign, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
// Convert the signature to Base64
return Base64UrlEncode(signatureBytes);
}
}
private static bool SendPOSTHttpRequestUrlEncoded(string requestBody, string requestUri, out HttpResponseMessage httpResponseMessage)
{
var httpClient = new HttpClient();
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, requestUri);
var httpContent = new StringContent(requestBody);
// Set the content type header for URL-encoded form data
httpContent.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded");
// Set the content on the request message
httpRequestMessage.Content = httpContent;
try
{
// Send the request
httpResponseMessage = httpClient.SendAsync(httpRequestMessage).Result;
return true;
}
catch (Exception)
{
httpResponseMessage = null;
return false;
}
}
private static bool GetTokensFromResponse(string responseJson, out string accessToken, out string refreshToken, out int accessTokenExpiresIn)
{
accessToken = string.Empty;
refreshToken = string.Empty;
accessTokenExpiresIn = 0;
try
{
// Parse the JSON using Newtonsoft.Json's JObject
var jsonObj = Newtonsoft.Json.Linq.JObject.Parse(responseJson);
// Extract the tokens and expiration time
if (jsonObj.TryGetValue("access_token", out var accessTokenValue))
accessToken = accessTokenValue.ToString();
if (jsonObj.TryGetValue("refresh_token", out var refreshTokenValue))
refreshToken = refreshTokenValue.ToString();
if (jsonObj.TryGetValue("expires_in", out var expiresInValue) &&
int.TryParse(expiresInValue.ToString(), out int expiresIn))
accessTokenExpiresIn = expiresIn;
// Return true if all values were successfully extracted
return !string.IsNullOrEmpty(accessToken) &&
!string.IsNullOrEmpty(refreshToken) &&
accessTokenExpiresIn > 0;
}
catch (Exception ex)
{
Console.WriteLine($"Error parsing token response: {ex.Message}");
return false;
}
}
private static long GetEpochTime(DateTime dateTime)
{
DateTime epochStart = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
// Ensure the input datetime is in UTC
if (dateTime.Kind != DateTimeKind.Utc)
{
dateTime = dateTime.ToUniversalTime();
}
// Calculate total seconds between the provided datetime and epoch start
TimeSpan timeSpan = dateTime - epochStart;
// Return total seconds as a long value (Unix timestamp)
return (long)timeSpan.TotalSeconds;
}
private static string Base64UrlEncode(byte[] input)
{
var output = Convert.ToBase64String(input);
output = output.Split('=')[0]; // Remove any trailing '='
output = output.Replace('+', '-'); // Replace '+' with '-'
output = output.Replace('/', '_'); // Replace '/' with '_'
return output;
}
/// <summary>
/// Creates a JSON Web Key (JWK) from an X.509 certificate.
/// </summary>
private static void CreateJWK()
{
string certFilePath = "<PathToYourCerFile>.cer"; // the certificate that you received from one of CAs authorized by IRS
var certificate = new X509Certificate2(certFilePath);
RSA? rsa = certificate.GetRSAPublicKey();
if (rsa == null)
throw new InvalidOperationException("RSA public key not found in the certificate.");
var rsaParameters = rsa.ExportParameters(false);
if (rsaParameters.Modulus == null || rsaParameters.Exponent == null)
throw new InvalidOperationException("RSA parameters are not valid.");
var jwk = new
{
kty = "RSA",
kid = DateTime.Today.ToString("yyyy-MM-dd"), // make your own Key ID if required
use = "sig",
n = Convert.ToBase64String(rsaParameters.Modulus),
e = Convert.ToBase64String(rsaParameters.Exponent),
x5c = new[] { Convert.ToBase64String(certificate.RawData) },
x5t = Convert.ToBase64String(certificate.GetCertHash())
};
var jwkSet = new { keys = new[] { jwk } };
File.WriteAllBytes("<PathToJSONFile>.json", Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(jwkSet, Formatting.Indented)));
}
}
Alex - I have a working code but still getting 401 unauthorised. There seems to be some issue with the IRIS setup probably consent. The same code works for another client id.
Thanks
Bikash Shah