Skip to main content

Client authentication with Private Key JWT

When requesting tokens you can use a signed assertion as authentication instead of the basic authentication with a client ID and client secret pair. In Private Key JWT your application creates and signs a JWT using a private key.

Private Key JWT is a client authentication method that is more secure than the default “Client Secret” method. Note that Private Key JWT involves additional steps on your end.

Prerequisites

Before configuring an application that authenticates using Private Key JWT, you must generate an RSA key pair.

You can choose between:

  • Import the public part of an existing key pair into the Signicat Dashboard, or
  • Generate an RSA key pair directly in the Signicat Dashboard.

Import the public key

If you wish to generate your own key pair, make sure you meet these technical requirements:

  1. Generate a JWK RSA key pair (2048-bit) suitable for encryption. We also support 4096-bit if required. It is important that you create a JWK or an X509 certificate. We recommend using the JWK format.
  2. Store the public part of your key pair in a text file (or .json).

If you chose to create your own key pair, then in the next section click Import public key instead of Add public key in step 2.

Generate the key pair

To generate a key pair in the Signicat Dashboard:

  1. In the Signicat Dashboard, navigate to Products > eID Hub > OIDC clients.
  2. Select the client you wish to use. If you haven't created a client yet, see Set up an OIDC client.
  3. In the client menu, navigate to Advanced > Public keys and then Add public key.
  4. Fill in the Name, Not valid before and Not valid after fields for your key.
  5. Select Signing as the Usage.
  6. Now press Create. If you want to upload your public key, either paste your key content or upload a file with the key.
  7. A key pair is generated for you. This contains a public key and a private key. Take a copy of the private key and store it somewhere safe.

You are now ready to move on to the implementation.

Implementation

Before you continue

Private Key JWT modifies how your application sends the token endpoint request, therefore this section assumes that you first start an OIDC authentication request as for any client authentication method.

Before you continue, your application should have already completed these steps:

  • Start an OIDC authentication request.
  • Authenticate the end-user.
  • Receive an authorization code on your chosen redirect URI.

After these steps, the Private Key JWT implementation below applies.

Authenticating using Private Key JWT consists of two steps:

  1. Building the assertion. This is a JWT signed by the private key that you generated.
  2. Exchanging the assertion for an access token to authenticate with Signicat.

Step 1. Building the assertion

Begin by crafting an assertion in the form of a JSON Web Token (JWT).

What is a JWT?

A JSON Web Token (JWT) consists of three parts separated by dots (.), which correspond to:

  • Header
  • Payload
  • Signature

A JWT typically looks like:

xxxxx.yyyyy.zzzzz

The payload of the JWT should contain the following valid claims:

An example of JWT payload would look like:

{
"iss": "<OIDC_CLIENT_ID>",
"sub": "<OIDC_CLIENT_ID>",
"aud": "https://<YOUR_SIGNICAT_DOMAIN>/auth/open/connect/token",
"jti": <unique string, for instance a GUID>,
"iat": 1673955575,
"exp": 1673961575,
}

Now, create a signed JWT using a payload similar to the one above. Serialise this as a compact format JWT. The serialised JWT is a long string that looks like: eyJhbGciOiJSUzI...AiOiJKV1QifQ.eyJpc3Mi...J1ZX0.nmupzTs...H9whojA

Code example

In the example below, the Python script shows how to generate the assertion:


import json
from time import time
from uuid import uuid4
from jwcrypto import jwk, jws

# For the sake of simplicity, the JWKs has been manually loaded into a dictionary.
# In a real life scenario you should do this in a secure manner: The jws_private_key should be stored securely on your premise.
jws_private_key = {
"kty": "RSA",
"kid": "my-kid-123",
"use": "sig",
"alg": "RS256",
"e": "AQAB",
"n": "2sZ0sfbK7bZWOZgAYGWvEkL-9HWt-UadCKRyZHgIRNxvS1KdNZE4chcXF7sXfuAim6Ec_WbcEqGTOzOmQF-VejluB6jO-banEKl3pTGxRj2xj5tBFbtpWyj_AENHxpzbb7EPR_ahIPyvQeCU3VFOEw8juVd9X3gn_iJdmxbN7yIWzhuMRimAleT04AR8SP-g9NUauaEZSrIwny_JbeZ_Fb1m2XfPTB8KbbnYPeyQTQRy_2H5vMLRYGhOFyF9DS_6TguJlSc2VdPLsfW_uhbS4X1fd8-RVbF8UJ6lSanOhmIw1Choh_ncy90-tmmsk6T2QPKBwY0UqgU6bAv5iD2pdw",
"d": "eRHSipn0-1Aor3661p3vIMAKr-Zf_M9jH-FBnPAAQ3tp69kwPvC6uAinMu7Ktd_7xvyGOoWtzHG2NNEEdCNxaU5W4c49nFvEYKgoGjdBz4lctghJIGmyiExLsi2JjxRHK6xktIJ78PFlW6OZPlE8T7fVIUCVlTu9hhomiyk3ldnVjrwJ5ESzaJs_zh6dqNcbfP-8APbG74ACejG3ojp9DWadOwA6kDtKIGdBAHZxObDJtG8qKQDcqvrxBUDXkifl-7t3b97HhxMRyR-d6wvrmz5mkYEzNZJt6LYkCyeGkE-IoQmPM0qYiWI7F3H4E1YjCoGlueoAQCoGOkFCMLYMQQ",
"p": "8_fKHLlwG_t57jsYG97ggGiYnFV9P6FMU3xe6dptMMrqhuPl_XbMic8CA-vOVP_nTgxDfaEnv_wlGcYgVLwdlIICjvoLB1Fma4SguAVCloIZbYsiXBwpfXiTWn-RgSe1HtTbfzpmBpt4RJiHEPoWx34ZUDZhcSpREuO2sIG_U18",
"q": "5ZCYsdA2yJmGJ_quksnNVIa8cby_a_SfFKg_5-CYSKyoZFaZttaNL064f0DCCO4tEp31W37_SankFUkdMDN8ccyW6xwnOj3PJRTX2J79sXdKaJDGleEXCKzuF7Rbi0nouP-1YFxjRmRh5WjBRDvbyCc-m_1ofxbwRskw3CPiOOk",
"dp": "GU9YoXg_gDero6Jv0txhcBDp3DYmQ0apk3OwqRQnBcvXXt0fzBbaC2X1cJCzHDBcP8WX7t2cMReoha7_RasqanC-cTTRlhXEyVy-C7lH-jNPDgVEMEgfqcurhdT8NGj5KlSs3NsjIIZaiMtGH-XCHTogyCiMHWBlfs8u8crUHYM",
"dq": "WiqSHv0mF2JdlCRdHyCOOo31REMbeH6LYSS4fQ31Ik5WkZqGI49fwt4Lj0fTLojGQVKzhS17feZxxH6ELWN7lIMEH_Jd4f1W-DyYjufbwzGUkz-SEFppnqm1lq_raOkttEQTbHa9M2_IF8AucOuF5rarW7-LpKdQ1qy9OSoK98k",
"qi": "rR6CplTi8roFIPwTes4puAABvN-agWh6jRxVVll1SfjqBFoTQvBHp8bUhJO7zmAEhOh-oDQX5Prn24CDKu_kr7emD-WeF9qaGJG1Gltrnh_-nj2XWpfMFmeJWOcflAWJPeltRn3f9PUrnujMDHNDg4EsD8f-ckzPOldqTH_zF_Y"
}

# Loading key
rsa_jws_private_key = jwk.JWK()
rsa_jws_private_key.import_key(**jws_private_key)

# Token payload (JSON)
payload = {
"iss": "<OIDC_CLIENT_ID>",
"sub": "<OIDC_CLIENT_ID>",
"aud": "https://<YOUR_SIGNICAT_DOMAIN>/auth/open/connect/token",
"jti": str(uuid4()),
"iat": int(time()),
"exp": int(time()+6000),
}
payload_bytes = json.dumps(payload).encode('utf-8')

# Token headers
jws_header = {
"alg": jws_private_key["alg"],
"kid": jws_private_key["kid"],
}

# Creating JWS token
jwstoken = jws.JWS(payload_bytes)
jwstoken.add_signature(rsa_jws_private_key, protected=jws_header)
jws_serialized = jwstoken.serialize(compact=True)

print(f"JWT: '{jws_serialized}'")
Important

This code example is for illustrative purposes only. It is not intended for production usage. Signicat takes no responsibility if this code example is used inappropriately.

Step 2. Exchanging the assertion

After you build and sign the JWT with the required claims, you are ready to authenticate your application to receive an access token.

Use the signed JWT as your assertion towards Signicat token endpoint. For example,

client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer
client_assertion={JWT}

The HTTP request would be:

curl --request POST https://api.signicat.com/auth/open/connect/token \ 
--header 'Content-Type: application/x-www-form-urlencoded' \
--data 'grant_type=authorization_code' \
--data 'scope=openid+profile' \
--data 'code={CODE}' \
--data 'redirect_uri=https%3A%2F%2Fyourdomain.com%2Fredirect' \
--data 'client_id={OIDC_CLIENT_ID}' \
--data 'client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer'
--data 'client_assertion={JWT}'

Note that the response you receive back is the same as from any other token endpoint request.