Cis2 - Exchanging code with id_token and refresh token

Hi,

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 = “https://am.nhsint.auth-ptl.cis2.spineservices.nhs.uk:443/openam/oauth2/realms/root/realms/NHSIdentity/realms/Healthcare/access_token”;
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("https://am.nhsint.auth-ptl.cis2.spineservices.nhs.uk:443");

        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);
        }
        else
        {
            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-----", "")
                           .Trim();

    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(
        _clientId,
        _audience,
        claims,
        now,
        now.AddMinutes(5),
        _signingCredentials
    );

    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 https://www.unixtimestamp.com/ 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

Have you published your public keys as part of the client configuration and exposed them over the internet - if CIS2 Auth cannot access them, it cannot validate your signature and will fail. Any questions regarding your client config, please reach out to the onboarding teams.