JWS and JWT policies overview

This page applies to Apigee and Apigee hybrid.

View Apigee Edge documentation.

This topic provides general information about JWT (JSON Web Token) and JWS (JSON Web Signature) and the Apigee JWS/JWT policies that may be of interest to Apigee proxy developers.

Introduction

Both JWS and JWT are commonly used to share claims or assertions between connected applications. The JWS/JWT policies enable Apigee API proxies to:

In the latter two cases, the policy also sets flow variables. This allows subsequent policies in the Apigee flow to inspect the claims in the JWT or JWS, and to make decisions based on those claims, or to propagate that information to the backend services.

When using the VerifyJWS or VerifyJWT policies, an invalid JWS/JWT will be rejected and will result in an error condition. Similarly, when using the DecodeJWS or DecodeJWT policies, a malformed JWS/JWT will result in an error condition.

Videos

Watch a short video for a quick introduction to JWT. While this video is specific to generating a JWT, many of the concepts are the same for JWS.

What a short video to learn more about the JWT structure.

Use cases

You can use the JWS/JWT policies to:

  • Generate a new JWS/JWT on either the proxy or target endpoint sides of an Apigee proxy. For example, you might create a proxy request flow that generates a JWS/JWT and returns it to a client. Or, you might design a proxy so that it generates a JWS/JWT on the target request flow, and attaches it to the request sent to the target. The JWS/JWT and its claims would then be available to enable backend services to apply further security processing.
  • Verify and extract claims from a JWS/JWT obtained from inbound client requests, from target service responses, from Service Callout policy responses, or from other sources. For a signed JWS/JWT, Apigee will use one of the RSA, ECDSA, or HMAC algorithms to verify the signature, whether the JWS/JWT was generated by Apigee or a third party. For an encrypted JWT, Apigee will decrypt the JWT, using one of any of the supported JWA encryption algorithms (see IETF RFC 7518).
  • Decode a JWS/JWT. Decoding is most useful when used in concert with the Verify JWS/JWT policy, in cases where the value of a claim (JWT) or header (JWS/JWT) within the JWS/JWT must be known before verifying a signed JWS/JWT or decrypting an encrypted JWT.

Parts of a JWS/JWT

A signed JWS/JWT encodes information in three parts separated by dots:

Header.Payload.Signature
  • The Generate JWS/JWT policy creates all three parts.
  • The Verify JWS/JWT policy examines all three parts.
  • The DecodeJWS policy examines the header only. The DecodeJWT policy examines the header and payload only.

A JWS also supports a detached form that omits the payload from the JWS:

Header..Signature

With a detached JWS, the payload is sent separately from the JWS. You use the <DetachedContent> element of the Verify JWS policy to specify the raw, unencoded JWS payload. The Verify JWS policy then verifies the JWS by using the header and signature in the JWS and the payload specified by the <DetachedContent> element.

An encrypted JWT encodes information in five parts separated by dots:

Header.Key.InitializationVector.Payload.AuthenticationTag

The GenerateJWT and VerifyJWT policies will create or examine all of those parts. The DecodeJWT can examine only the un-encrpted header.

To learn more about tokens and how they are encoded, and signed or encrypted, see the relevant standards documents:

Differences between JWS and JWT

You can use either a JWT or JWS to share claims or assertions between connected applications. The major difference between the two is the representation of the payload:

  • JWT
    • The payload is always a JSON object.
    • The payload is always attached to the JWT.
    • The typ header of the token is always set to JWT.
    • The payload may be either signed or encrypted.
  • JWS
    • The payload can be represented by any format, such as a JSON object, byte stream, octet stream, and others.
    • The payload does not have to be attached to the JWS.
    • The header and payload are always signed. JWS does not support encryption.

Because the JWT format always uses a JSON object to represent the payload, the Apigee GenerateJWT and VerifyJWT policies have built in support to handle common Registered Claim Names, such as aud, iss, sub, and others. That means you can use elements of the GenerateJWT policy to set these claims in the payload, and elements of the VerifyJWT policy to verify their values. See the Registered Claim Names section of the JWT specification for more.

Along with supporting certain Registered Claim Names, the GenerateJWT policy directly supports adding claims with arbitrary names to the payload or header of the JWT. Each claim is a simple name/value pair, where the value can be of type number, boolean, string, map, or array.

When using GenerateJWS, you supply a context variable that represents the payload. Because a JWS can use any data representation for the payload, there is no concept of "payload claim" in a JWS, and the GenerateJWS policy does not support adding claims with arbitrary names to the payload. It is possible to use the GenerateJWS policy to add claims with arbitrary names to the header of the JWS. Also, the JWS policies support a detached payload, where the JWS omits the payload. A detached payload lets you send the JWS and payload separately. Using a detached payload can be more space efficient especially for large payloads, and is required by several security standards.

Signing versus Encryption

Apigee can generate signed or encrypted JWT. Choose signed JWT when the payload need not be secret, but it's important to provide integrity and non-repudiation guarantees to readers. A signed JWT assures readers that the payload is has not changed since the JWT was signed and that the JWT was signed by the holder of the private key. Choose encrypted JWT when the payload should be secret. An encrypted JWT provides confidentiality for the payload, as only the appropriate key holder can decrypt it.

It's possible to use both encrypted and signed JWT together, especially when the encrypted JWT uses an asymmetric cryptography algorithm (RSA, ECDSA). In that case, the identity of the producer of that JWT cannot be determined, because the encryption key is public. To address that problem, combine signing with encryption. A typical pattern is as follows:

  • Sign a payload to produce a JWS or signed JWT.
  • Encrypt the signed result to produce an encrypted JWT.

Embedding a signed payload within an encrypted JWT using this approach provides both non-repudiation and confidentiality guarantees. Apigee policies can produce and decode & verify such combinations.

Signature algorithms

For signed JWT, the JWS/JWT Verification and JWS/JWT Generation policies support RSA, RSASSA-PSS, ECDSA, and HMAC algorithms, using SHA2 checksums of bit strength 256, 384, or 512. The DecodeJWS and DecodeJWT policies work regardless of the algorithm that was used to sign the JWS/JWT.

HMAC algorithms

The HMAC algorithms relies on a shared secret, known as the secret key, for creating the signature (also known as signing the JWS/JWT) and for verifying the signature.

The minimum length of the secret key depends on the bit strength of the algorithm:

  • HS256: 32 bytes minimum key length
  • HS384: 48 bytes minimum key length
  • HS512: 64 bytes minimum key length

RSA algorithms

The RSA algorithms use a public/private key pair for the cryptographic signature. The JWA specification uses the monikers RS256, RS384, and RS512 to represent these options. With RSA signatures, the signing party uses an RSA private key to sign the JWS/JWT, and the verifying party uses the matching RSA public key to verify the signature on the JWS/JWT. There are no size requirements on the keys.

RSASSA-PSS algorithms

The RSASSA-PSS algorithms are an update to the RSA algorithms, and use the monikers PS256, PS384, and PS512. Like the RS* variants, RSASSA-PSS uses an RSA public/private key pair for the cryptographic signature. The format, mechanism, and size limits of the key is the same as for the RS* algorithms.

ECDSA algorithms

The Elliptic Curve Digital Signature Algorithm (ECDSA) set of algorithms are elliptic-curve cryptography algorithms with a P-256, P-384, or P-521 curve. When you use ECDSA algorithms, the algorithm determines the type of public and private key you must specify:

Algorithm Curve Key requirement
ES256 P-256 A key generated from the P-256 curve (also known as secp256r1 or prime256v1)
ES384 P-384 A key generated from the P-384 curve (also known as secp384r1)
ES512 P-521 A key generated from P-521 curve (also known as secp521r1)

Encryption algorithms

When using GenerateJWT and VerifyJWT to handle encrypted JWT, the policies support these algorithms:

  • dir
  • RSA-OAEP-256
  • A128KW, A192KW, A256KW
  • A128GCMKW, A192GCMKW, A256GCMKW
  • PBES2-HS256+A128KW, PBES2-HS384+A192KW, PBES2-HS512+A256KW
  • ECDH-ES, ECDH-ES+A128KW, ECDH-ES+A192KW, ECDH-ES+A256KW

Keys and Key Representations

The JOSE standards, covering JWS, signed and encrypted JWT, and more, describe how to use cryptographic keys to sign or encrypt information. The foundational elements for any cryptographic operation include the algorithm, and the key. Different algorithms require different key types, and in some cases, different key sizes.

Symmetric algorithms, such as the HS* family for signing, or the A128KW algorithm for encryption, require symmetric or shared keys: the same key is used for signing and verifying, or the same key is used for encrypting and decrypting. Asymmetric algorithms, such as the RS*, PS*, and ES* algorithms for signing, or the ECDH* algorithms for encrypting, use key pairs - there is a public key and a private key, and they are matched. In signing, the signer uses the private key to sign, and any party can use the public key to verify the signature. In encrypting, the encrypter uses the public key to encrypt, and the decrypter uses the private key to decrypt. Public keys, as the name suggests, are publicly shareable, and need not be kept secret.

There are different ways to serialize cryptographic keys into text format. Apigee policies accept keys serialized in various forms: PEM encoded-form, JWKS form, or for shared keys, UTF-8 encoded or base64 encoded form.

PEM Format

For public or private keys, it is common to use PEM encoding, which is defined in IETF RFC 7468. Here is an example of a private key represented in PEM format:

-----BEGIN PRIVATE KEY-----
MIGEAgEAMBAGByqGSM49AgEGBSuBBAAKBG0wawIBAQQgVcB/UNPxalR9zDYAjQIf
jojUDiQuGnSJrFEEzZPT/92hRANCAASc7UJtgnF/abqWM60T3XNJEzBv5ez9TdwK
H0M6xpM2q+53wmsN/eYLdgtjgBd3DBmHtPilCkiFICXyaA8z9LkJ
-----END PRIVATE KEY-----

There are similar PEM formats for public keys, or encrypted private keys.

JWKS Format

A JSON Web Key (JWK) is a JSON data structure that represents a single cryptographic key. A JSON Web Key Set (JWKS) is a JSON structure that represents a set of JWKs. JWK and JWKS are described in RFC7517. See JWKS examples at Appendix A. Example JSON Web Key Sets.

JWKS is intended to allow any party to represent a set of keys in a standard format. A key use-case is sharing public keys in a standard manner, via an HTTP endpoint that delivers data in JWKS format. When a company or system that generates signed JWS or JWT, such as an Identity Provider, publishes their public keys, any system or application that can read the public keys, can verify signatures generated by the signing party. Conversely, any system or app that wants to encrypt data that should be read only by a particular party or company, can retrieve the public keys belonging to that party or company, and generate an encrypted JWT for that purpose.

RFC7517 describes the JWKS key elements for each key type, such as RSA or EC. These elements should always be present:

  • kty - The key type, such as RSA or EC.
  • kid - the key ID. This can be any arbitrary unique string value; there should be no duplicates within a single key set. If the inbound JWT bears a key ID which is present in the set of JWKS, then the VerifyJWS or VerifyJWT policy will use the correct public key to verify the JWS/JWT signature.

Following are examples of optional elements and their values:

  • alg: The key algorithm. It must match the signing algorithm in the JWS/JWT.
  • use: The intended use of the key. Typical values are "sig" for signing and verification, or "enc" for encryption and decryption.

The following JWKS (originally retrieved from https://www.googleapis.com/oauth2/v3/certs, but out of date now) includes the required elements and values, and would be usable by Apigee:

{
     "keys":[
        {
           "kty":"RSA",
           "alg":"RS256",
           "use":"sig",
           "kid":"ca04df587b5a7cead80abee9ea8dcf7586a78e01",
           "n":"iXn-WmrwLLBa-QDiToBozpu4Y4ThKdwORWFXQa9I75pKOvPUjUjE2Bk05TUSt7-V7KDjCq0_Nkd-X9rMRV5LKgCa0_F8YgI30QS3bUm9orFryrdOc65PUIVFVxIwMZuGDY1hj6HEJVWIr0CZdcgNIll06BasclckkUK4O-Eh7MaQrqb646ghFlG3zlgk9b2duHbDOq3s39ICPinRQWC6NqTYfqg7E8GN_NLY9srUCc_MswuUfMJ2cKT6edrhLuIwIj_74YGkpOwilr2VswKsvJ7dcoiJxheKYvKDKtZFkbKrWETTJSGX2Xeh0DFB0lqbKLVvqkM2lFU2Qx1OgtTnrw",
           "e":"AQAB"
        },
        {
            "kty":"EC",
            "alg":"ES256",
            "use":"enc",
            "kid":"k05TUSt7-V7KDjCq0_N"
            "crv":"P-256",
            "x":"Xej56MungXuFZwmk_xccvsMpCtXmqhvEEMCmHyAmKF0",
            "y":"Bozpu4Y4ThKdwORWFXQa9I75pKOvPUjUjE2Bk05TUSt",
        }
     ]
  }
  

Specifying keys to the JWS and JWT policies

Whether you are generating or verifying JWS or JWT, you need to provide a key for use within the cryptographic operations.

When generating a signed JWT, you need to provide a key that can produce the signature.

  • In the case of an RS*, PS*, or ES* signing algorithm, all of which use asymmetric keys, you must provide the private key to generate the signature.
  • In the case of an HS* algorithm, you need to provide the symmetric key that will be used when generating the signature.

When you verify a signed JWS/JWT, you need to provide a key that can verify the signature.

  • In the case of an RS*, PS*, or ES* signing algorithm, you must provide the public key that is associated with the private key that was originally used to sign the token.
  • In the case of an HS* algorithm, you need to provide the same symmetric key that was used to sign the JWS or JWT.

You have two options for providing the keys to the JWS and JWT policies:

  • Provide the key value directly (typically via a context variable), or
  • Provide the key indirectly, via a kid and a JWKS. You can specify the JWKS directly, or indirectly via an http URL at which Apigee can retrieve the JWKS.

The JWKS URL option is typically used only as a source of public keys that are usable with asymmetric algorithms, because JWKS URLs will typically be public.

The following examples illustrate ways you can supply keys directly in various scenarios.

  • Generate a JWT signed with the HS256 algorithm. The required key in this case is a symmetric key. This policy provides a context variable that contains the base64url-encoded secret key.

    <GenerateJWT name='gen-138'>
      <Algorithm>HS256</Algorithm>
      <IgnoreUnresolvedVariables>false</IgnoreUnresolvedVariables>
      <SecretKey encoding='base64url'>
        <Value ref='private.secretkey'/>
        <Id ref='variable-containing-desired-keyid'/>
      </SecretKey>
      . . .
      <OutputVariable>output_variable_name</OutputVariable>
    </GenerateJWT>
  • Verify a JWT signed with the HS256 algorithm. The required key in this case is a symmetric key. As in the above example, this policy provides a context variable that contains the base64url-encoded secret key.

    <VerifyJWT name='verify-138'>
      <Algorithm>HS256</Algorithm>
      <IgnoreUnresolvedVariables>false</IgnoreUnresolvedVariables>
      <SecretKey encoding='base64url'>
        <Value ref='private.secretkey'/>
      </SecretKey>
      . . .
      <OutputVariable>output_variable_name</OutputVariable>
    </VerifyJWT>
  • Verify a JWT that has been signed with the PS256 algorithm. The required key in this case is a public RSA key. This policy provides a context variable that contains the PEM-encoded public key.

    <VerifyJWT name='JWT-001'>
      <Algorithm>PS256</Algorithm>
      <IgnoreUnresolvedVariables>false</IgnoreUnresolvedVariables>
      <PublicKey>
        <Value ref='variable-containing-pem-encoded-public-key'/>
      </PublicKey>
      . . .
    </VerifyJWT>
  • Generate a JWT signed with the PS256 algorithm. The required key in this case is a private RSA key. This policy provides a context variable that contains the PEM-encoded private key.

    <GenerateJWT name='JWT-002'>
      <Algorithm>PS256</Algorithm>
      <IgnoreUnresolvedVariables>false</IgnoreUnresolvedVariables>
      <PrivateKey>
        <Value ref='private.variable-containing-pem-encoded-private-key'/>
      </PrivateKey>
       . . .
    </GenerateJWT>

JWKS as a key source when verifying a JWS or signed JWT

When a system or app generates a JWS/JWT, typically the system or app inserts a Key Identifier (the kid claim) into the JWS/JWT header. The key tells any reader of the JWS/JWT which key is required to verify the signature on the signed JWS/JWT, or decrypt the encrypted JWT.

As an example, suppose an issuer signs a JWT with a private key. The "Key ID" identifies the matching public key that must be used to verify the JWT. The list of public keys is typically available at some well-known endpoint, such as the Google Identity endpoint or the Firebase Authentication endpoint. Other providers will have their own public endpoints that publish keys in JWKS format.

When using Apigee to verify a JWS or signed JWT with public keys that are shared via a JWKS endpoint, you have some options:

  • Option 1: In the policy configuration, specify the JWKS endpoint uri in the <PublicKey/JWKS> element. For example, for the VerifyJWT policy:

    <VerifyJWT name="JWT-Verify-RS256">
      <Algorithm>RS256</Algorithm>
      <Source>json.jwt</Source>
      <IgnoreUnresolvedVariables>false</IgnoreUnresolvedVariables>
      <PublicKey>
          <JWKS uri="https://www.googleapis.com/oauth2/v3/certs"/>
      </PublicKey>
      . . .
    </VerifyJWT>
    

    In this case, Apigee will:

    1. Examine the JWS/JWT header to find the signing algorithm (alg), such as RS256, and reject the inbound JWT if that algorithm does not match the algorithm configured in the policy.
    2. Retrieve the list of keys with their IDs from the specified JWKS endpoint, or from an internal cache if this JWKS endpoint has been used before.
    3. Examine the JWS/JWT header to find the Key ID (kid). If the inbound JWT does not include a key ID (kid) in the header, then the mapping of kid to verification-key is not possible, and Apigee will throw a fault.
    4. Extract from the JWKS, the JWK with the key ID noted in the JWS/JWT header. Throw a fault if a key with that key ID is not present.
    5. Check that the algorithm for the JWK matches the algorithm specified in the policy configuration. Reject the verification and throw a fault if the algorithms do not match.
    6. Use that public key to verify the signature on the JWS/JWT. Reject the verification and throw a fault if the signature does not verify.
  • Option 2: In the policy configuration, specify a variable that holds the JWKS endpoint uri in the <PublicKey/JWKS> element.

    For example, for the VerifyJWT policy:

    <VerifyJWT name="JWT-Verify-RS256">
      <Algorithm>RS256</Algorithm>
      <Source>json.jwt</Source>
      <IgnoreUnresolvedVariables>false</IgnoreUnresolvedVariables>
      <PublicKey>
        <JWKS uriRef="variable-containing-a-uri"/>
      </PublicKey>
      . . .
    </VerifyJWT>
    

    In this case, Apigee will perform the same steps as above, except that Apigee will retrieve the JWKS not from a hard-coded URI, but from the uri specified in the variable referenced by the uriRef attribute. Caching still applies.

  • Option 3: In the policy configuration, specify a variable that holds the hard-coded JWKS data in the <PublicKey/JWKS> element.

    For example, for the VerifyJWT policy:

    <VerifyJWT name="JWT-Verify-RS256">
      <Algorithm>RS256</Algorithm>
      <Source>json.jwt</Source>
      <IgnoreUnresolvedVariables>false</IgnoreUnresolvedVariables>
      <PublicKey>
        <JWKS ref="variable-that-holds-a-jwks"/>
      </PublicKey>
      . . .
    </VerifyJWT>
    

    In this case, Apigee will perform the same steps as above, except that Apigee will retrieve the JWKS not from a URI, but from the context variable that you specify in the ref attribute. Typically you would have loaded this context variable from a ServiceCallout, a KVM, or a properties file associated to the proxy.

JWKS as a key source when generating an encrypted JWT

When you generate an encrypted JWT via an asymmetric algorithm (RSA-OAEP-256 or any of the ECDH-* variants) you use the public key to encrypt. You have options for providing the key to the GenerateJWT policy

A typical approach is to specify in the policy configuration the JWKS endpoint uri in the <PublicKey/JWKS> element. For example:

<GenerateJWT name="GJWT-1">
  <Algorithms>
    <Key>RSA-OAEP-256</Key>
    <Content>A128GCM</Content>
  </Algorithms>
  <IgnoreUnresolvedVariables>false</IgnoreUnresolvedVariables>
  <PublicKey>
    <JWKS uri='https://www.example.com/.well-known/jwks.json'/>
    <Id ref='variable-containing-desired-keyid'/>
  </PublicKey>
    . . .
</GenerateJWT>

In this case, Apigee will:

  1. Assemble the un-encoded payload and header for the JWT based on the policy configuration.
  2. Retrieve the list of keys with their IDs from the specified JWKS endpoint, or from an internal cache if this JWKS endpoint has been used before. Currently the cache TTL is 5 minutes.
  3. Extract the JWK with the key ID noted in the PublicKey/Id element from the JWKS. Throw a fault if a key with that key ID is not present.
  4. Check that the algorithm for the JWK matches the algorithm specified in the policy configuration. Throw a fault if the algorithms do not match.
  5. Generate a random sequence to be used as the content-encryption key.
  6. Use the selected public key to encrypt the content encryption key.
  7. Use the content encryption key to encrypt the payload.
  8. Finally, assemble all the parts into a serialized encrypted JWT.

An alternative is to use the uriRef attribute to specify a variable that holds the URI of a JWKS endpoint. For example:

<GenerateJWT name="GJWT-1">
  <Algorithms>
    <Key>RSA-OAEP-256</Key>
    <Content>A128GCM</Content>
  </Algorithms>
  <IgnoreUnresolvedVariables>false</IgnoreUnresolvedVariables>
  <PublicKey>
    <JWKS uriRef='variable-containing-jwks-uri'/>
    <Id ref='variable-containing-desired-keyid'/>
  </PublicKey>
  . . .
</GenerateJWT>

In this case, Apigee will perform the same steps as above, except that Apigee will retrieve the JWKS from the uri specified in the variable referenced by the uriRef attribute, instead of a hard-coded URI. Apigee will read the JWKS from an internal cache if this JWKS endpoint has been used before.