Anyone work with the IRS A2A?

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:

  • JWT requires Base64Url encoding (not the standard base64) - you need to remove “=” chars and replace “+” with “-” and “/” with “_”.
  • in the request body URL User JWT goes first, and Client JWT is the second:
    ...&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