Skip to main content

Encryption/signing of the request object

As per OIDC specification, you can use the (optional) request parameter to enable signed and, additionally, encrypted requests. Though not required, using the request parameter provides additional security and helps you comply with any Full Message-Level Encryption (MLE) requirements.

Common scenarios for using encrypted/signed requests include masking personal information contained in the request and sending requests that cannot be tampered with by third parties.

Additional hardening of request object mechanism

You can harden the security of your requests in two ways:

  1. Require request object
  2. Unique/One-Time-Use request object

These options are discussed below.

Require request object

You can configure your OIDC clients to require the request object in any future request. To do this:

  1. In the Signicat Dashboard, go to Products > eID Hub > OIDC clients.
  2. Select your OIDC client. To create an OIDC client, see Set up an OIDC client.
  3. In the OIDC client menu, go to Advanced > Security and tick Requires Request Object to enable it.

After you enable the Requires Request Object, all future requests with this OIDC client will require a request object. Requests without a request object will fail. Effectively, this setting forces all requests on the OIDC client to be at least signed.

Unique/One-Time-Use request object

If you submit a request object token that contains the jti claim, we will ensure that it is unique for this client within 24 hours. This will work automatically without additional configuration required.

If you do not wish to have unique request objects enforced, simply omit the jti claim.

This mechanism can effectively prevent “replay attacks” when using request objects, as each request object token can only be used once.

Types of request objects

The Signicat OIDC solution accepts two types of request objects:

  1. Signed JWT: A valid JWS token.
  2. Nested JWT: A token that is first signed (JWS) and then encrypted (JWE).
Encryption keys

To encrypt your nested JWT, you must use one of our public encryption keys, available at: https://api.signicat.com/auth/open/.well-known/openid-configuration/jwks.


You can retrieve encryption keys at any time. Encryption keys have the following properties:

  • Always contain "use": "enc".
  • "kid" (key id) starts with encryption-key. For sandbox accounts, the kid starts with sandbox-encryption-key.
Key rotation

When using our public encryption keys, you must account for key rotation. A key is available for max 39 days, and on occasion even shorter.


To ensure that you always have a valid key, we recommend that you refresh your cache within 7 days.

Prerequisites

Before configuring an application that authenticates using nested JWTs, you must generate an RSA key pair. To do this, you can either:

  • 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

Begin crafting the OIDC authorisation request. The parameters are standard OIDC parameters. The requirements are as follows:

  • The HTTP request can be made using either GET or POST.
  • The payload must be a request object, following the OIDC Core specifications, section 6.1). An example of a typical payload request object looks as follows:
{
"client_id": "dev-annoyed-sloth-492",
"response_type": "code",
"redirect_uri": "https://oauth.tools/callback/code",
"acr_values": "idp:ftn",
"state": "ABCDEF012345",
"scope": "openid profile",
"iss": "dev-annoyed-sloth-492",
"aud": "https://team-connect-demo.test.signicat.dev/auth/open",
"iat": 1673955575,
"exp": 1673961575,
}

Note that the JWT requires the following additional claims to be valid:

Steps:

  1. 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.
  2. Now, create an encrypted JWT using the serialised signed JWT from above as the payload. This should again be serialised as a compact format JWT.
  3. Use this nested JWT as your request object towards Signicat. For example, /auth/open/connect/authorize?client_id={CLIENT_ID}&request={REQUEST_OBJECT_JWT}.

Code example

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.

import json
from time import time
from jwcrypto import jwk, jwe, 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.
# The jwe_public_key should be dynamically fetched from Signicat's JWKS endpoint.
jws_private_key = {
"alg": "RS256",
"d": "IfHsADmK2xsxbie2Q4pX5cOEfWEeD8efIC82_PtK7AT-ZO3vwll9WUQ_VMWXZuUkmDDwuLukp-GCAq871YfsJKTEXYibArYZSB6yPo7FHMxtEtsfOqDGEjCJHd76AE99ZH8qA2H_0j_xGMGbAP44x5DoTAidj1B3fKEF_IQfoHNfvQBwLaE2lyJVxQp_Xzay_VbdeGRlacmzc8ccGWU3lpAYDONMPxh0Z9tx2aexarDOjFbZ0jKN9b9-Hyntnz7iOFdiOWSHo41NOd5l2BG4SARdRPUwoMxcEexxVVPjpr1smNwLHJZdAvOMiHSAKDAwKh4Vgu4p5CfD6O6JM_PoSQ",
"dp": "TSi83Xlf9XP9DMeqCQgMXUcrxt4nSh56C0RnqRPneGShlObkUZmxJoaEh5TL5OJ-9MQKcGCBN0_LOFls1p4NOvdhZxxwHi2hEZisdd40IY37Z-4JN0L5Xs4jqLDrZAP_paEW1SOI33sqhHCeLi034DZw0-4BxKXm5Fhn-uS2WZ8",
"dq": "jSvo3MsPzigQN_20txhQFxqQaw6M7udXgt51QW50tnM896DlpEd0hbcryOncpfc8AVhjyxLnGG2sYQRLswcq838STu4wDEhuPfhgKAak_5cc38N_TSyRJWLfnwF9pTTWPgkk0Io8MwWpsIpf9qIbyBOl0jHnBdfSuuLKoYM6QfU",
"e": "AQAB",
"kid": "abc123321cba",
"kty": "RSA",
"n": "muNJBpq6VtZbGj0kb5YeMe5lE14CZ4OAKnY6Epzholp61rVrrWRlErmyV9C8J4G6jCgDKrGnjQp143gD71ATVLIus__wO1YtRplKCyz2hypvhvId8GwYOLm1k3TCjKeSa6DwKiKZZOUg011NWN9TSSkCdWb-xNgSV5gZesC_JngofTyrAXT92MDIzGoCMpA1D6tDIzadIigHA7_FznpT1eN5cAHZqMeRC8MHRH2_K8erUxx4QFuencrADmaf4vIWjTlmPioxa4XLRrYcXsnKrrKeVp8CXRFqMwb0fAtRmvQu3RHUqZ02dlTER7ocIbrHeYx2_VAxhqFtLhR9J2ih0w",
"p": "zpfR9mwYoPB-r1wTy5owD8RaoufPeVnabefe7IK9nwGUOkgINHDGTRPvXx2f0UuKz9kAVB6ZIsPgwCfhoMrbdQemNLXu5VZ_MRny6Uesk_5Awox5QIBbYvzF6PnMfV-pwLtieNLpiN_sP-pF6jfvJ8cy3XFBUnF5IDy0zeuI4Lc",
"q": "v-3u72-IPZpVuY8hkqi8gPpc5FTSWh-MJ5BvOcifDFjVYEE-KCTL5I2MJ2R9u-1sVceeFwfa-HfgV3ArizO91-pSJSqk-Py3KqHDXXdno30wwwJWR8noSwH8eRwqdQHBQQPa0ClywQJ2bbqiiw30CpjXDHknL5qXQEskpoJq88U",
"qi": "SGo4PyG344Uxddy_g_YGTt5pa_lvq0G42jGWU-fOPQfjSiNWhzuyflys9YoSf_x3kzwf1oJosdUdCpfUXzLX1LHsWrUld-52JFPtuFT3nNVZiFXCnboUHJSIlgaP1GlA1rHhh491gMIPqr4uJY3BSKp7aj2efL61Z-1l-LvwrSw",
"use": "sig"
}
jwe_public_key = {
"kty": "RSA",
"use": "enc",
"kid": "sandbox-encryption-key-0eb954d0e12b824b80a3c5664232320a",
"e": "AQAB",
"n": "trU6figprTeSBBTdW7Lqm6OjgNg4neNSvtEUs__bf5iK59W9oH2oeZLPx7ixmuJ3Cane6WHCR4BumxtnoIixWlNAJgdu_xosI-zO_7fhUVIgS-qH5kY9rj8GUa6GUquAV92_L3nnEWgKJ_220jV8_kdaqab1Pm5QMJ9RSC73BWuvCZ5fWb57okFN5-dTtjZ-WCmgij-9axPKjlM0PTp4c8Lm8KJDO-B-6n8DO9JEfdBpa6ejEIGi3tNJAyveluTzZ5YyF_WFD4rJZ2JIJIuGth7-o1Myq5QhmaXBTrL01aY-bLgiqWrtBNpM2rnU3MMUQn0TOpzKVvRaTl-x5HIpvw",
"alg": "RSA-OAEP"
}
# Loading keys
jwk_jws_private_key = jwk.JWK()
jwk_jws_private_key.import_key(**jws_private_key)
jwk_jwe_public_key = jwk.JWK()
jwk_jwe_public_key.import_key(**jwe_public_key)

# Token payload (JSON)
payload = {
"client_id": "dev-annoyed-sloth-492",
"response_type": "code",
"redirect_uri": "https://oauth.tools/callback/code",
"state": "ABCDEF012345",
"scope": "openid profile",
"iss": "dev-annoyed-sloth-492",
"aud": "https://team-connect-demo.test.signicat.dev/auth/open",
"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"],
}
jwe_header = {
"alg": jwe_public_key["alg"],
"enc": "A128CBC-HS256",
"kid": jwe_public_key["kid"],
}

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

# Encoding JWE token
jwetoken = jwe.JWE(jws_serialized, recipient=jwk_jwe_public_key, protected=jwe_header)
jwe_serialized = jwetoken.serialize(compact=True)

print(f"https://team-connect-demo.test.signicat.dev/auth/open/connect/authorize?client_id={payload['client_id']}&request={jwe_serialized}")

Detailed tutorial and code example

For an even more detailed description of how to set this up, including code examples, see the Signed and encrypted tokens code example.