# Advanced security considerations

The base implementation of OIDC should be secure enough in most cases. This page mentions a few measures that can be taken to increase security, if desired.

# PKCE: Proof Key for Code Exchange

Note

We recommend that you always use PKCE, no matter what use case or setup you have. It's a very low-effort implementation detail that adds a lot of extra security.

Most OIDC libraries support PKCE, and there are also libraries and code snippets that implement PKCE. See oauth.net: PKCE (opens new window) for further details.

# Encrypted responses from Signicat

As per OIDC specification, Signicat can send encrypted responses. This means we will always encrypt the ID tokens and UserInfo responses for a given client. Of course responses from Signicat are fully encrypted on the transport layer with HTTPS, but adding message-level encryption can be used as an additional security layer. This section explains how to set up and process encrypted responses.

Important

For FTN you are required to receive encrypted responses. In fact, FTN will fail if you do not set this up correctly and you will not be able to obtain any FTN authentication results.

# Prerequisites

Typically you won't need to create your own keys, as you can generate key pairs in the Signicat dashboard. If you do want to fully manage your own keys, you will need to first generate one and then import it and store it in our system so that we can use it.

If you wish to create your own key pair, these are the 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 (typically .json).

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

# Required configuration

  1. Log into the Dashboard (opens new window).
  2. Click Authentication and then OIDC clients. Click on the client you wish to use. If you haven't configured a client, see Set up an OIDC client.
  3. In the menu for the client, click Advanced > Public keys and then Add public key.
  4. Pick a Name, Not valid before and Not valid after for your key.
  5. Select Encryption as the Usage.
  6. Now click Create.
  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 now have an encryption key which we can use to encrypt the responses we send to you, and that you should be able to decrypt.

Finally, you will need to configure the encryption on your client:

  1. Go back into the Dashboard (opens new window) and find your client again.
  2. In the menu for the client, click Advanced > Security.
  3. Under User Info Response Type select SignedAndEncrypted. (If you prefer you can also select Encrypted, which is out of scope for this guide.)
  4. Enable Encrypt ID Tokens.

You are now ready to move on to the implementation.

# Implementation

You should send your requests as normal OIDC requests. However, the differences will be in the responses from Signicat: The ID token and the response from UserInfo is a nested JWT which is encrypted and signed.

  1. Decrypt it with your private key from the key pair given to Signicat.
  2. Deserialise the resulting signed JWT and verify it exactly the same as for a normal token.

# Example code

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.

from jwcrypto import jwk, jwe

# ATTENTION: Insert your nested JWT (that you got from Signicat OIDC server) here!!!
jwe_serialized = "eyJh...QifQ.Qq5TQR...xN-PQ.ojSGehTV7xw_xE078sFnTg.in8a...SITqA.SyVNrXe0C3UkKBQ_AVyQiw"

# 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 jwe_private_key should be stored securely on your premise. 
jwe_private_key = {
    "alg": "RSA-OAEP",
    "d": "MODJkQVnKx-txB11QSkz2roacDL9z-BqcDq-dYM2OHrI-x4iNpdBWtoS2Hi4OfH6ETmzMnFNVY3oIYoaMTuuWKc0r_QeIIqbkt8MozALusETiB2VtiAiiZU6G2DyAOFaWaoiIlRYO5BciDCM6z4ytfnDFYM7-K-YSA3_V7jDSGjRroGC112lVs9BzA3qjCP8jnb9VvNKUhdcFqsqGU8eRw8fZp73uHyEm9tVHimSPjyLda3XlcxPSEkhqlP4aaleWhkUQJM3bc2YDnpWBjlNuIHsacEA28xrwOqHSqK-b8klvxk-e_w-C-SdlcYhgB5DeSB0bEuCpmV9GKwOp0KhuQ",
    "dp": "hkPXP00qlIrioShp3FiXp0_OahJXe4cVNAXLJYgKRsqv72I7AXZltAOXNRanpQDQutDADFsMTx1XxoEQm_GUvTUe-dS-PkBGKJMLjp28zs9G0L4yXUXenKu_vs-3wD4_06WtT_kOyJD48LaQno6SYdUm4JBvS5A1ve2NYI7xDEk",
    "dq": "WGmNHsCzREbHgwQCStW9munP16NJzdeXJ0ys7QbLwpOxMEP1BkSznoRw4_0IkSxTtyvWPG9Eo-bPCKfkqILUowG7tniIerf0F6LkCjssPk5wNxEK_Ktt-59_3Wrvs9Iik4tYF0bEqlXyidFq7ayBoRdiPZ0-ELjU-hEqY0GR0Ok",
    "e": "AQAB",
    "kid": "mykey-123",
    "kty": "RSA",
    "n": "zOeJ1RJOgP6NlgwEcqa-BtHUC5nNNa2UbsQQsNDQa6KioVcNfiz5WWmQF3FAR7HTAxQveXfgT7PqOmPgiDVHTLcdYlOjcOAESCTFYELsuh58xnA99agM9vPuLVo2x-hwjE_b-1dC3Ph2_gpXwqS6JDjKa5VF-5zMLbVLQJ96yhKGqzQ7TrfXrEcgfzXMipTbQTxpDAFnTyYYQ7lOVp_ms0Kbz4fpRbyGUtzYsgkYBCNwfWgtfXZW7ahMeb84ukbH2nXXQKQsxuSYU-gbK44yVvcvltqg5wMjG39Xo-BUGlJGQzEN0-6QEIqGrMXgTx5B147IV22vT7demmNx2_RXDQ",
    "p": "-3lVxpmyecuu7-tceX7eoVjHmUlbHzUkhehkV_axQx5FtX5NRWdzqg8jhmPBBRpMtvG7g55ZQihVTi3sphfP0k1czEPsgJt8diZ4KIMDgimcksyJ8yPP0dZuvdnBVFVdlJDWfimmLxmFwlmw-p3gCAPePbqRjD1vo25Bwi5Dujk",
    "q": "0JeicVX3YgVhUXuY_f3BX-VDKR0LH_SLaIaEimuvpleo5AM4El_An8SR-_Z1GXZuzsOvff7d_E79NjwsdhtKKxQiW93awsFD9Fd5fqNUOpB0ikAf-gNz6MCujt9nhS_jvbMsNI6gCRexZ4gD-RB9dj_qXsWWJUJQpcu7QMmeE3U",
    "qi": "-MNE31_wnD2aRLiZz0piLD6lHJwSYpGFinBKWp5VPuLdeXAruSdKW82oRTapA-y6s87fB85jcjvW3dAYjzFFsFNpjDzrpCwctIsQVzZt_vVu5Mdon365tFHWyH2xAcTmVxdJGDunR6BKMmnciCdXTgZP8wSAEWrjmVop_ontfM8",
    "use": "enc"
}
# Loading JWK key
jwk_jwe_private_key = jwk.JWK()
jwk_jwe_private_key.import_key(**jwe_private_key)

jwetoken = jwe.JWE()
jwetoken.deserialize(jwe_serialized)
jwetoken.decrypt(jwk_jwe_private_key)
jws_serialized = jwetoken.payload
print("\nSIGNED JWT (just like a normal ID token):\n")
print(jwetoken.payload)

# Encryption of the request object

As per OIDC specification you can optionally use the request parameter to enable signed and optionally encrypted requests. This usually isn't required, but can be used as an additional security measure. It can also be used to comply with any possible Full Message-Level Encryption (MLE) requirements. A common use case is to mask any personal information contained in the request, and also to send requests that cannot be tampered with by any third parties.

# Additional hardening of request object mechanism

There are two things you can do to additionally harden the security:

# Requires Request Object

  1. Log in to the Dashboard (opens new window).

  2. Click Authentication and then OIDC clients. Click on the client you wish to use. If you haven't configured a client, see Set up an OIDC client.

  3. In the menu for the client, click Advanced > Security and tick Requires Request Object.

This will reject any request that does not use a request object for this client. Effectively, it forces all requests on this client to be at least signed.

# Unique/One-Time-Use Request Object

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

If you do not wish to have unique request objects enforced, you 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.

# Type of request objects

The Signicat OIDC solution will only accept two types of request objects:

  1. Signed JWT: A valid JWS token.
  2. Nested JWT: A token which is first signed (JWS) and then encrypted afterwards (JWE). This guide will focus on nested JWTs.

# Prerequisites

Typically you won't need to create your own keys, as you can generate key pairs in the Signicat dashboard. If you do want to fully manage your own keys, you will need to first generate one and then import it and store it in our system so that we can use it.

If you wish to create your own key pair, these are the 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 (typically .json).

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

# Required configuration

  1. Log into the Dashboard (opens new window).
  2. Click Authentication and then OIDC clients. Click on the client you wish to use. If you haven't configured a client, see Set up an OIDC client.
  3. In the menu for the client, click Advanced > Public keys and then Add public key.
  4. Pick a Name, Not valid before and Not valid after for your key.
  5. Select Signing as the Usage.
  6. Now click Create.
  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 (per 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,
}

As seen above, the JWT requires the following additional claims to be valid:

Claim Explanation
iss Issuer - needs to be the same as your client_id.
aud Audience - needs to be equal to your 'Issuer URL'. (Check your client on the Dashboard (opens new window) to find it.)
iat Issued At - A UNIX timestamp (in seconds) for the time at which you created the token.
exp Expire - A UNIX timestamp (in seconds) for the time at which your token will no longer be valid. We recommend now + 10 minutes.
  1. Create a signed JWT using a payload similar to the above. Serialise this as a compact format JWT, which will essentially be 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 (e.g. /auth/open/connect/authorize?client_id={CLIENT_ID}&request={REQUEST_OBJECT_JWT}).

# Example code

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 on how to set this up, including code examples, see Code examples > Signed and encrypted tokens (Advanced).

Last updated: 10/10/2023 10:56 UTC