PingGateway 2024.11

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

Before you start, set up and test the example in Pass runtime data in a JWT signed with PEM then encrypted with a symmetric key.

  1. Add a second route to PingGateway, 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.

  2. Test the setup:

    1. In your browser’s privacy or incognito mode, go to https://ig.example.com:8443/jwtbuilder-sign-then-encrypt to build a JWT.

    2. Log in to AM as user demo, password Ch4ng31t. The sample application displays the signed JWT.

    3. Go to https://ig.example.com:8443/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 isn’t validated, and an error is displayed.