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:
- Require request object
- 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:
- In the Signicat Dashboard, go to Products > eID Hub > OIDC clients.
- Select your OIDC client. To create an OIDC client, see Set up an OIDC client.
- 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:
- Signed JWT: A valid JWS token.
- Nested JWT: A token that is first signed (JWS) and then encrypted (JWE).
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 withencryption-key. For sandbox accounts, thekidstarts withsandbox-encryption-key.
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:
- 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.
- 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:
- In the Signicat Dashboard, navigate to Products > eID Hub > OIDC clients.
- Select the client you wish to use. If you haven't created a client yet, see Set up an OIDC client.
- In the client menu, navigate to Advanced > Public keys and then Add public key.
- Fill in the Name, Not valid before and Not valid after fields for your key.
- Select Signing as the Usage.
- Now press Create. If you want to upload your public key, either paste your key content or upload a file with the key.
- 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:
- 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.
- 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.
- 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
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.