How to Use JWT Signer Plugin

Kong has many powerful plugins, among all of them, OIDC is no doubt the most complicated plugin considering it has over 200 parameters. JWT signer plugin might be runner-up as it has over 70 parameters. Both plugins are quite scary at first glance, but they are actually easy to use once you know what to look for.

In today’s post I will explain everything you need to know to use JWT signer plugin. You can find this plugin official documentation here.

Concepts

TL;DR: JWT signer plugin has the ability to authenticate and re-sign JWT token with two IDPs independently on a single plugin instance.

The trickiest part to understand this plugin is that both of its authentication and token re-signing features are optional. Also both features can be done TWICE with two different IDPs on a single plugin instance.

A common request flow would be:

  1. Client send a request with a token to Kong route that has JWT signer plugin enabled.

  2. JWT signer plugin will validate this token either by checking its signature (when passed in token is JWT) or via Introspection (when passed in token is an opaque token)

  3. Once authentication is completed, Kong re-signs a new token with the same info on JWT or from introspection result with Kong’s own private key and then put this new token on the upstream request.

Common Questions

  • Why do you want to authenticate with two IDPs at the same time?

    Imagine when requests go through a restricted network, a channel_token will be attached to the request automatically. Users can only consume the API when the requests have both channel_token and users’ access_token.

    The channel_token helps to confirm a request comes from the restricted network and the access_token helps to identify which user send in the request.

  • What are the benefits of re-signing tokens?

    1. Upstream server only need to trust Kong’s public key instead of keys from different IDPs.

    2. It is easier to rotate key-sets. If you want Kong to generate a new key pair, you just need to call POST kong:8001/jwt-signer/jwks/<JWK_ID>/rotate.

    3. For IDPs that only issues opaque tokens, this feature helps the upstream server to avoid calling introspection endpoint in case they have restricted Internet access. The re-signed JWT tokens contains all information returned from introspection result, the upstream server can just decode and use the info on it.

Features

Let me break down these two features and tell you the minimum parameters you need to use.

  • Parameters for both access_token and channel_token are identical, I will use {token} as a placeholder. For example config.{token}_issuer represents config.access_token_issuer and config.channel_token_issuer.

  • If you only need to validate one token, you can use config.{token}_optional=true to disable the other one. This plugin expects to validate both tokens by default.

Authentication

Signature verification

When the tokens issued by your IdP are in JWT format, you can use your IDP’s public key to validate these tokens. When validation pass, we can trust the information on the JWT token to be valid.

  • config.{token}_jwks_uri : This parameter is used to specify the uri of the IDP’s public key.

  • config.verify_{token}_signature : This is a Boolean value to turn on/off signature verification. It is true by default.

Signature verification is pretty straight forward. User just need to let Kong know where to find the public key.

JWT tokens does not need to be issued by IDP, you can generate your own key pairs. I’ve written a post before about generating key pairs to sign JWT token locally. You can find it here.

Introspection

If the tokens issued by your IdP are opaque tokens, you can validate and get token information from introspection endpoint. Different IDPs have different implementation/requirements for using introspection endpoint. Some might not even provide introspection endpoint when they are issuing opaque tokens (I am talking about you Auth0).

  • config.{token}_introspection_endpoint : This parameter is used to specify the uri of IDP’s introspection endpoint.
  • config.{token}_introspection_authorization : It is most likely you need to use some authorization headers for introspecting tokens. If you use client_credential flow, you need to put the value of Basic base64encode('client_id:client_secret') on this parameter. For example, if your client_id and client_secret are both admin, you need to set this parameter as Basic YWRtaW46YWRtaW4=.

As long as you know what info your IDP introspection endpoint is expecting, you should be able to tweak plugin configs to make the introspection call successful.

For example you can useconfig.{token}_introspection_body_args for passing additional body arguments and config.{token}_introspection_hint to give a hint to the introspection endpoint what token you are passing in.

JWT token re-signing

You can control the re-signing process with below parameters.

  • config.{token}_keyset : If you use a url on this parameter, Kong will expect to load private key in JWK format from that url. If a string is given, Kong will generate a new keyset with the name of the string.

  • config.{token}_signing_algorithm : You can specify what signature algorithm you want to use for signing the new token. Kong use RS256 by default.

  • config.{token}_upstream_leeway : You can add/subtract expiry time of the re-signed token.

As you can see you have the ability to control how Kong signs the token but you can’t manipulate the claims on the new token. (except exp claim)

Kong Enterprise supports add_claims and set_claims from 3.3 version.

This is to keep this plugin simple to avoid adding extra processing time to the request. It is better to request tokens with all the claims you need from IDP in the first place.

Other features

Consumer Mapping

The following parameters let you map a claim from JWT token or introspection result to a consumer.

  • config.{token}_consumer_claim
  • config.{token}_consumer_by
  • config.{token}_introspection_consumer_claim
  • config.{token}_introspection_consumer_by

These parameter names are self-explanatory. You probably don’t need to worry about *_consumer_by parameters and only need to use *_consumer_claim to map your consumer username or custom_id.

Claim value validation

There are two sets of parameters for claim value validations.

config.{token}_introspection_scopes_required and config.{token}_introspection_scopes_claim can be used to validate key:value pair on introspection results.

config.{token}_scopes_claim and config.{token}_scopes_required can be used to validate claims on JWT token.

  • config.{token}_scopes_claim and config.{token}_introspection_scopes_claim
    These two parameter supports nested claims. The array indices traverse the claim array.

  • config.{token}_scopes_required and config.{token}_introspection_scopes_required

    These two parameter checks the related claim values and claim values are case sensitive.

    • When [“scope1 scope2”] are in the same array indices, both scope1 AND scope2 need to be present on access token or introspection results.
    • When [“scope1”, “scope2”] are in different array indices, either scope1 OR scope2 need to be present in access token or introspection results.

Let’s say my JWT token has below in payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
...
"employee": {
"groups": [
"default",
"it"
],
"name": "li",
"favourites": {
"beverage": "coffee"
}
},
...
}

If I only want coffee lover to consume my API, I can set config.{token}_scopes_claim=["employee","favourites","beverage"] and config.{token}_scopes_required=["coffee"]".

Demos

Prerequisite :

Kong Gateway running with an enterprise license.

In my demo I am running Kong Gateway 2.7.1.1

Kong uses default value for plugin parameters when they are not set. These default values are normally set to the best practise to help you using plugins easily. The default values should work for most cases but I strongly advise you to check the official doc to understand what default values are used.

Here are some common use cases:

  • Validate one JWT token and re-sign JWT token with Kong’s private key

    1
    2
    3
    4
    5
    6
    curl --request POST \
    --url https://$KONG_ADMIN_API/plugins \
    --header 'Content-Type: application/x-www-form-urlencoded' \
    --data name=jwt-signer \
    --data config.access_token_jwks_uri=https://<Keycloak>/auth/realms/demo/protocol/openid-connect/certs \
    --data config.channel_token_optional=true
  • Introspect one opaque and re-sign JWT token with Kong’s private key

    1
    2
    3
    4
    5
    6
    7
    curl --request POST \
    --url https://$KONG_ADMIN_API/plugins \
    --header 'Content-Type: application/x-www-form-urlencoded' \
    --data name=jwt-signer \
    --data config.access_token_introspection_endpoint=https://<Curity>/oauth/v2/oauth-introspect \
    --data 'config.access_token_introspection_authorization=Basic base64encode('client_id:client_secret')' \
    --data config.channel_token_optional=true
  • Validate two tokens at the same time

    You can combine settings above to validate two JWT token, introspect two opaque tokens or validate one JWT token and introspect one opaque token.

    By default Kong looks for access_token in Authorization:Bearer or Authorization: Basic header. We need to use config.channel_token_request_header to tell Kong where to look for the channel token.

    If we need channel token to be re-sign and sent to upstream, we need to use config.channel_token_upstream_header to set header to upstream. By default authorization:bearer will be used for sending re-signed access_token to the upstream.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    curl --request POST \
    --url --url https://$KONG_ADMIN_API/plugins \
    --header 'Content-Type: application/x-www-form-urlencoded' \
    --data name=jwt-signer \
    --data config.access_token_introspection_endpoint=https://<Curity>/oauth/v2/oauth-introspect \
    --data 'config.access_token_introspection_authorization=Basic base64encode('client_id:client_secret')' \
    --data config.channel_token_jwks_uri=https://<Keycloak>/auth/realms/demo/protocol/openid-connect/certs \
    --data config.channel_token_request_header=X-JWT-channel-client \
    --data config.channel_token_upstream_header=X-JWT-channel-upstream

    In above example, I use introspection on access_token and validate JWT signature on channel_token. Kong will look for the channel token from X-JWT-channel-client header in the client request and will put re-signed channel token to X-JWT-channel-upstream header on the upstream request.

    I sent my request as below

    1
    2
    3
    4
    curl --request GET \
    --url https://$KONG_PROXY:8443/demo \
    --header 'Authorization: Bearer <Opaque>' \
    --header 'X-JWT-channel-client: <JWT_TOKEN>'
  • Do not validate JWT token, only re-assign token with Kong’s private key

    I don’t encourage you to skip token validation but you can turn off JWT validation when needed. Once it is turned off, Kong will re-sign any JWT token passed in and add it to upstream request.

    1
    2
    3
    4
    5
    6
    7
    curl --request POST \
    --url https://$KONG_ADMIN_API/plugins \
    --header 'Content-Type: application/x-www-form-urlencoded' \
    --data name=jwt-signer \
    --data config.channel_token_optional=true \
    --data config.verify_access_token_signature=false \
    --data config.verify_access_token_expiry=false

That’s all I want to show you today. As usual I can’t/don’t want to cover all plugin parameters but I am hoping this post can help you understanding the basics of this plugin and to start using it.

See you on the next one.