Cis2 - Exchanging code with id_token and refresh token


I am trying to complete flow for Cis2 authentication. I have NHS Care Identity button, when user clicks on it, I redirects them to integration environment’s authorisation URL. I use one of the authentication methods and I am redirected to my redirect URL with code and scope query strings.

I am now sending the code that I received to integration token endpoint so that I can receive Id_token and refresh tokens. I am using private jwt.

I am doing following steps:

  1. Generating Jwt token using my private key file to receive client assertion. I am passing my app_key as iss and sub, as shown in my code below.

  2. Sending the assertion that I received from step 1 along with access code, client_id, redirect_uri etc. Note in this step I am sending client_id as 00000.apps.supplier. Replacing 00000 with my own number.

I receive following error: “{"error_description":"JWT is not valid","error":"invalid_client"}”.

My controller (UserRestrictedAuthController), it uses UserRestrictedjwtHandler to create jwt token which is used for client assertion.

public class UserRestrictedAuthController : ControllerBase
private UserRestrictedJwtHandler userRestrictedJwtHandler;
string audience = “”;
string clientId = “Mynumber.apps.supplier”;
string apiKey = “myApiKey”;

string privateKeyFile = @"C:\Users\path\Resources\test-13.pem";
string kid = "test-13";

public async Task Get(string accessCode)
    if (!System.IO.File.Exists(privateKeyFile))
        throw new FileNotFoundException($"The private key file was not found: {privateKeyFile}");

    userRestrictedJwtHandler = new UserRestrictedJwtHandler(privateKeyFile, audience, apiKey, kid);
    var clientAssertion = userRestrictedJwtHandler.GenerateJwt();

    using (var client = new HttpClient())
        client.BaseAddress = new Uri("");

        var requestData = new FormUrlEncodedContent(new[]
            new KeyValuePair<string, string>("grant_type", "authorization_code"),
            new KeyValuePair<string, string>("code", accessCode),
            new KeyValuePair<string, string>("redirect_uri", "https://localhost:44358/authorise.aspx"),
            new KeyValuePair<string, string>("client_id", clientId),
            new KeyValuePair<string, string>("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"),
            new KeyValuePair<string, string>("client_assertion", clientAssertion)

        HttpResponseMessage response = await client.PostAsync("/openam/oauth2/realms/root/realms/NHSIdentity/realms/Healthcare/access_token", requestData);

        if (response.IsSuccessStatusCode)
            string responseData = await response.Content.ReadAsStringAsync();
            Console.WriteLine("Response Data: " + responseData);
            string errorResponse = await response.Content.ReadAsStringAsync();
            Console.WriteLine("Error: " + response.StatusCode + ", Response: " + errorResponse);


Jwt handler code is given below.

public class UserRestrictedJwtHandler
private readonly SigningCredentials _signingCredentials;
private readonly string _audience;
private readonly string _clientId;
private readonly string _kid;

public UserRestrictedJwtHandler(string certPath, string audience, string clientId, string kid, bool isPem = true)
    _audience = audience;
    _clientId = clientId;
    _kid = kid;
    _signingCredentials = LoadCertificateFromPem(certPath, kid);

private SigningCredentials LoadCertificateFromPem(string pemPath, string kid)
    var privateKey = File.ReadAllText(pemPath);
    privateKey = privateKey.Replace("-----BEGIN PRIVATE KEY-----", "")
                           .Replace("-----END PRIVATE KEY-----", "")
                           .Replace("-----BEGIN RSA PRIVATE KEY-----", "")
                           .Replace("-----END RSA PRIVATE KEY-----", "")

    var keyBytes = Convert.FromBase64String(privateKey);
    var rsa = RSA.Create();
    rsa.ImportRSAPrivateKey(keyBytes, out _);

    var rsaSecurityKey = new RsaSecurityKey(rsa)
        KeyId = kid

    return new SigningCredentials(rsaSecurityKey, SecurityAlgorithms.RsaSha512)
        CryptoProviderFactory = new CryptoProviderFactory { CacheSignatureProviders = false }

public string GenerateJwt()
    var now = DateTime.UtcNow;

    var claims = new List<Claim>
    new Claim(JwtRegisteredClaimNames.Sub, _clientId),
    new Claim(JwtRegisteredClaimNames.Iss, _clientId),
    new Claim(JwtRegisteredClaimNames.Aud, _audience),
    new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
    new Claim(JwtRegisteredClaimNames.Exp, new DateTimeOffset(now.AddMinutes(5)).ToUnixTimeSeconds().ToString()),
    new Claim(JwtRegisteredClaimNames.Iat, new DateTimeOffset(now).ToUnixTimeSeconds().ToString()),
    new Claim(JwtRegisteredClaimNames.Nbf, new DateTimeOffset(now).ToUnixTimeSeconds().ToString())

    var token = new JwtSecurityToken(

    var tokenHandler = new JwtSecurityTokenHandler();
    var jwt = tokenHandler.WriteToken(token);

    // Debug: Print the JWT to verify its structure
    Console.WriteLine("Generated JWT: " + jwt);

    return jwt;


My approach

do all the POC work using postman or some rest client

utilise Online JWT tool and generate the jwt accordingly

use to generate the timestamp (5 mins in future)

retrieve code, and then use for the access token request

only when I have this part working will i write the c# code

ps: in your code, I couldnt see where you are setting the KID, or the algorithm?

1 Like