IG 2023.9

JWT validation

The following examples show how to use the JwtValidationFilter to validate signed and encrypted JWT.

The JwtValidationFilter can access JWTs in the request, provided in a header, query parameter, form parameter, cookie, or other way. If an upstream filter makes the JWT available in the request’s attributes context, the JwtValidationFilter can access the JWT through the context, for example, at ${attributes.jwtToValidate}.

For convenience, the JWT in this example is provided by the JwtBuilderFilter, and passed to the JwtValidationFilter in a cookie.

The following figure shows the flow of information in the example:

jwtvalidation
  1. Create a signed then encrypted JWT as described in Pass runtime data in a JWT signed with PEM then encrypted with a symmetric key.

  2. Add the following route to IG, replacing value of the property secretsDir with the directory for the PEM files:

    • Linux

    • Windows

    $HOME/.openig/config/routes/jwt-validate.json
    appdata\OpenIG\config\routes\jwt-validate.json
    {
      "name": "jwt-validate",
      "condition": "${find(request.uri.path, '^/jwt-validate')}",
      "properties": {
        "secretsDir": "path/to/secrets"
      },
      "capture": "all",
      "heap": [
        {
          "name": "SystemAndEnvSecretStore",
          "type": "SystemAndEnvSecretStore",
          "config": {
            "mappings": [{
              "secretId": "id.decrypted.key.for.signing.jwt",
              "format": "BASE64"
            }]
          }
        },
        {
          "name": "pemPropertyFormat",
          "type": "PemPropertyFormat",
          "config": {
            "decryptionSecretId": "id.decrypted.key.for.signing.jwt",
            "secretsProvider": "SystemAndEnvSecretStore"
          }
        },
        {
          "name": "FileSystemSecretStore-1",
          "type": "FileSystemSecretStore",
          "config": {
            "format": "PLAIN",
            "directory": "&{secretsDir}",
            "mappings": [{
              "secretId": "id.encrypted.key.for.signing.jwt.pem",
              "format": "pemPropertyFormat"
            }, {
              "secretId": "symmetric.key.for.encrypting.jwt",
              "format": {
                "type": "SecretKeyPropertyFormat",
                "config": {
                  "format": "BASE64",
                  "algorithm": "AES"
                }
              }
            }]
          }
        }
      ],
      "handler": {
        "type": "Chain",
        "config": {
          "filters": [{
            "type": "JwtValidationFilter",
            "config": {
              "jwt": "${request.cookies['my-jwt'][0].value}",
              "secretsProvider": "FileSystemSecretStore-1",
              "decryptionSecretId": "symmetric.key.for.encrypting.jwt",
              "customizer": {
                "type": "ScriptableJwtValidatorCustomizer",
                "config": {
                  "type": "application/x-groovy",
                  "source": [
                    "builder.claim('name', JsonValue::asString, isEqualTo('demo'))",
                    "builder.claim('email', JsonValue::asString, isEqualTo('demo@example.com'));"
                  ]
                }
              },
              "failureHandler": {
                "type": "ScriptableHandler",
                "config": {
                  "type": "application/x-groovy",
                  "source": [
                    "def response = new Response(Status.FORBIDDEN)",
                    "response.headers['Content-Type'] = 'text/html; charset=utf-8'",
                    "def errors = contexts.jwtValidationError.violations.collect{it.description}",
                    "def display = \"<html>Can't validate JWT:<br> ${contexts.jwtValidationError.jwt} \"",
                    "display <<=\"<br><br>For the following errors:<br> ${errors.join(\"<br>\")}</html>\"",
                    "response.entity=display as String",
                    "return response"
                  ]
                }
              }
            }
          }],
          "handler": {
            "type": "StaticResponseHandler",
            "config": {
              "status": 200,
              "headers": {
                "Content-Type": [ "text/html; charset=UTF-8" ]
              },
              "entity": [
                "<html>",
                "  <h2>Validated JWT:</h2>",
                "    <p>${contexts.jwtValidation.value}</p>",
                "  <h2>JWT payload:</h2>",
                "    <p>${contexts.jwtValidation.info}</p>",
                "</html>"
              ]
            }
          }
        }
      }
    }

    Notice the following features of the route:

    • The route matches requests to /jwt-validate.

    • The JwtValidationFilter takes the value of the JWT from my-jwt.

    • The SystemAndEnvSecretStore, PemPropertyFormat, and FileSystemSecretStore objects in the heap are the same as those in the route to create the JWT. The JwtValidationFilter uses the same objects to validate the JWT.

    • The JwtBuilderFilter customizer requires that the JWT claims match name:demo and email:demo@example.com.

    • If the JWT is validated, the StaticResponseHandler displays the validated value. Otherwise, the FailureHandler displays the reason for the failed validation.

  3. Test the setup:

    1. If you are logged in to AM, log out and clear any cookies.

    2. Go to http://ig.example.com:8080/jwtbuilder-sign-then-encrypt to build a JWT, and log in to AM as user demo, password Ch4ng31t. The sample app displays the signed JWT along with its header and payload.

    3. Go to http://ig.example.com:8080/jwt-validate to validate the JWT. The validated JWT and its payload are displayed.

    4. Test the setup again, but log in to AM as a different user, or change the email address of the demo user in AM. The JWT is not validated, and an error is displayed.

Copyright © 2010-2023 ForgeRock, all rights reserved.