Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 

jwt_signed callout

This directory contains the Java source code and Java jars required to compile a Java callout for Apigee Edge that does generation and parsing / validation of signed JWT. It also can generate and verify signed JWS. It has support for limited algorithms: HS256, RS256, PS256. This callout uses the Nimbus library for JOSE.

You do not need to build this Java code in order to use the JWT Generator or Verifier callout. The callout will work, with the pre-built JAR file. Find the pre-built JAR file in (the API Proxy subdirectory)[../apiproxy/resources/java].

You may wish to modify this code for your own purposes. In that case, you can modify the Java code, re-build, then copy that JAR into the appropriate apiproxy/resources/java directory for your API Proxy.

What Good is This?

Suppose you need to generate a JWT in response to an API call, or a series of API calls (for example, as part of an Open ID Connect flow). You could use this callout within an Apigee Edge API Proxy to generate a JWT.

Suppose a caller presents a JWT that was generated by an external system - like Google ID, or Azure AD, or Paypal, etc. You could use this callout within an Apigee Edge API Proxy to validate that JWT.

Reminder: What's a JWT?

A JWT is just a payload of JSON, with claims about something or someone. The claims state the identity of the someone/something, the identity of the token issuer, the time the token was issued, the time the token expires, and maybe some other information about the person or token holder. A typical signed JWT looks like this:

eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.
eyJzdWIiOiJ1cm46NzVFNzBBRjYtQjQ2OC00QkNFLxxxxxx.
D1OlHFCXAF4DPF7TfOphJ7AzpUOXHh7owZF

(newlines added for clarity) It's three parts, each separated by a dot. The indivdual parts are each base64url-encoded payloads. They represent:

  • the header
  • the JWT body
  • the signature

The JWT body (payload), base64url-decoded, might look like this:

{
  "sub": "api-key-might-go-here-78B13CD0-CEFD-4F6A-BB76",
  "aud": "https://api.mycompany.com/oauth2/token",
  "iss": "https://mycompany.net",
  "exp": 1471902991,
  "iat": 1471902961,
  "nbf": 1471902961,
  "jti": "2e8a36bc-5c14-4105-837f-3245abc03027",
  "scope": "https://www.example.com/accounts.readonly"
}

The payload can actually be any JSON. The JWT spec defines a set of particular claims to have well-defined meanings. These "registered" claim names include iss, sub, aud, exp, nbf, iat, and jti, to indicate issuer, ubject, audience, expiry, not-before time, issued-at time, and JTI or unique identifier of the token. The "scope" claim shown above is not a registered claim name, which means it can mean anything the issuer wants it to mean. This payload, plus the JWT Header, is signed; then the JWT is formed by the dot-concatenation of the base64url-encoded version of the three parts: header, payload signature.

Someone who receives that JWT can then cryptographically verify the signature value against the payload and header, to determine whether the claims within the JWT should be trusted.

What's a JWS?

A signed JWT is a special case of JWS. Whereas in a JWT, the payload must be JSON and there are registered names of some of the JSON properties, in a JWS, the payload is any bytestream. It need not be JSON, and there is no special meaning attributed to the properties if it is JSON. You could use JWS to sign an XML document, for example, or a .png image. A JWS is structured just like a JWT - the dot-concatenation of three base64url-encoded parts: header, payload, and signature. The key difference is the middle-part cannot necessarily be base64-decoded into a JSON string.

Using the Jar

You do not need to build the JAR in order to use it. To use it:

  1. Include the Java callout policy in your apiproxy/resources/policies directory. The configuration should look like this:

    <JavaCallout name="JavaJwtHandler" >
      <DisplayName>Java JWT Creator</DisplayName>
      <Properties>...</Properties>
      <ClassName>com.google.apigee.callout.jwtsigned.JwtCreatorCallout</ClassName>
      <ResourceURL>java://apigee-callout-jwt-signed-1.0.21.jar</ResourceURL>
    </JavaCallout>
  2. Deploy your API Proxy, using pushapi, importAndDeploy.js, the Import-EdgeApi cmdlet for Powershell, or another similar tool.

For some examples of how to configure the callout, see the related api proxy bundle.

Dependencies

Maven will download all of these dependencies for you. If for some reason you want to download these dependencies manually, you can visit https://mvnrepository.com .

Configuring the Callout Policy

There are four callout classes, all in the com.google.apigee.callout.jwtsigned package:

class name description
JwtCreatorCallout create a signed JWT, using HS256, RS256, or PS256 for the algorithm
JwtVerifierCallout parse and verify a signed JWT that uses HS256, RS256, or PS256
JwsCreatorCallout create a signed JWS with any string as the payload, using HS256, RS256, or PS256
JwsVerifierCallout parse and verify a signed JWS that uses HS256, RS256, or PS256 for the algorithm

How the JWT (or JWS) is generated or validated, respectively, depends on configuration information you specify for the callout, in the form of properties on the policy. Some examples follow.

Generate a JWT using HS256

  <JavaCallout name='JavaCallout-JWT-Create'>
    <DisplayName>JavaCallout-JWT-Create</DisplayName>
    <Properties>
      <Property name="algorithm">HS256</Property>
      <!-- the key is likely the client_secret -->
      <Property name="secret-key">{organization.name}</Property>
      <!-- claims -->
      <Property name="subject">{apiproxy.name}</Property>
      <Property name="issuer">http://dinochiesa.net</Property>
      <Property name="audience">{desired_jwt_audience}</Property>
      <Property name="expiresIn">86400</Property> <!-- in seconds -->
      <Property name="continueOnError">false</Property>
    </Properties>

    <ClassName>com.google.apigee.callout.jwtsigned.JwtCreatorCallout</ClassName>
    <ResourceURL>java://apigee-callout-jwt-signed-1.0.21.jar</ResourceURL>
  </JavaCallout>

This configuration of the policy causes Edge to create a JWT with these standard claims:

  • subject (sub)
  • audience (aud)
  • issuer (iss)
  • issued-at (iat)
  • expiration (exp)

It uses HMAC-SHA256 for signing.

Because there is no "id" Property included in the configuration, the "jti" claim is not included.

The values for the properties can be specified as string values, or as variables to de-reference, when placed inside curly braces.

It emits the dot-separated JWT into the variable named jwt_jwt

There is no way to explicitly set the "issued at" (iat) time. The iat time automatically gets the value accurately indicating when the JWT is generated.

You can set a not-before (nbf) time, to the same time the JWT was issued, by including this property:

      <Property name="not-before"/>

You can configure the policy to compute a not-before relative to "now" using a time-span expression, like 10m to indicate 10 minutes from now, 3h to indicate 3 hours from now. It looks like this:

   <!-- three minutes from now -->
   <Property name="not-before">3m</Property>

You can also specify an explicit not-before time as number of seconds since epoch:

   <Property name="not-before">1601582274</Property>

And you can specify an explicit not-before as a string, in one of these forms:

  • ISO-8601: 2017-08-14T11:00:21.269-0700
  • RFC-3339: 2017-08-14T11:00:21-07:00
  • RFC 1123: Mon, 14 Aug 2017 11:00:21 PDT
  • RFC 850: Monday, 14-Aug-17 11:00:21 PDT
  • ANSI-C: Mon Aug 14 11:00:21 2017
   <Property name="not-before">2017-08-14T11:00:21-07:00</Property>

Properties supported by JwtCreatorCallout

property description
issuer set "iss" claim in payload
algorithm set "alg" in header
audience set "aud" claim in payload
subject set "sub" claim in payload
id set "jti" claim in payload
kid set "kid" in header
secret-key specify the secret key to use for HS256 signing. Used only when algorithm=HS256.
private-key specify the PEM-encoded RSA private key. Used for algorithm=RS256 or PS256
private-key-password the password for the private key, if there is one.
claim_xxx set an arbitrary claim in the JWT. The xxx will be the name of the claim in the JWT, and must not be a registered claim name.
json-payload a JSON payload to use for the JWT; can use this in lieu of specifying individual claims.

As per the JWT specification (IETF RFC 7519), all claims are optional. If you include Property elements for subject, issuer, audience, and id, or if you specify json-payload property with any of those claims, they'll be inserted as claims into the generated JWT. If you don't include that configuration, the generated JWT won't include those claims. In either case, the JWT is valid.

The continueOnError property is optional. If present, and the value is "true", then the policy will not return a Fault when there is a policy error for any reason. This is mostly useful when parsing and verifying a JWT. Using this property, you can instruct the policy to not cause the flow to enter fault status, but only set appropriate context variables, if the JWT is expired, if the time is before the not-before claim, if the time is before the issued-at time, if the required claims are not present, or if the signature does not verify.

Generate a JWT using RS256

To generate a key signed with RS256, you can specify the private RSA key inside the policy configuration, like this:

  <JavaCallout name='JavaCallout-JWT-Create-RS256-2' >
    <DisplayName>JavaCallout-JWT-Create-RS256-2</DisplayName>
    <Properties>
      <Property name="algorithm">RS256</Property>

      <!-- private-key and private-key-password used only for algorithm = RS256 -->
      <Property name="private-key">
      -----BEGIN PRIVATE KEY-----
      Proc-Type: 4,ENCRYPTED
      DEK-Info: DES-EDE3-CBC,049E6103F40FBE84

      EZVWs5v4FoRrFdK+YbpjCmW0KoHUmBAW7XLvS+vK3BdSM2Yx/hPhDO9URCVl9Oar
      ApEZC1CxzsyRfvKDtiKWfQKdYKLccl8pA4Jj0sCxVgL4MBFDNDDEau4vRfXBv2EF
      ....
      7ZOF1UXVaoldDs+izZo5biVF/NNIBtg2FkZd4hh/cFlF1PV+M5+5mA==
      -----END PRIVATE KEY-----
      </Property>

      <!-- The password value for the private key should not be hardcoded.
        Put it in the Encrypted KVM, and reference a variable here. -->
      <Property name="private-key-password">{private.privkey_password}</Property>

      <!-- this value goes into the JWT header to identify the
        key with which the JWT is signed. To support key rotation. -->
      <Property name="kid">{key_id}</Property>

      <!-- standard claims -->
      <Property name="subject">{subject_uuid}</Property>
      <Property name="issuer">https://mycompany.net</Property>
      <Property name="audience">Optional-String-or-URI</Property>
      <Property name="expiresIn">86400</Property> <!-- in seconds -->
      <Property name="id"/> <!-- an ID will be dynamically generated -->

      <!-- custom claims to inject into the JWT -->
      <Property name="claim_primarylanguage">English</Property>
      <Property name="claim_shoesize">8.5</Property>

    </Properties>

    <ClassName>com.google.apigee.callout.jwtsigned.JwtCreatorCallout</ClassName>
    <ResourceURL>java://apigee-callout-jwt-signed-1.0.21.jar</ResourceURL>
  </JavaCallout>

The resulting JWT is signed with RSA, using the designated private-key. The payload looks like this:

{
  "sub": "urn:75E70AF6-B468-4BCE-B096-88F13D6DB03F",
  "aud": ["Optional-String-or-URI"],
  "iss": "https://mycompany.net",
  "exp": 1471902991,
  "iat": 1471902961,
  "nbf": 1471902961,
  "jti": "2e8a36bc-5c14-4105-837f-3245abc03027",
  "primarylanguage" : "English",
  "shoesize" : 8.5
}

The private key should be in pkcs8 format. You can produce a keypair in the correct format with this set of shell commands:

openssl genrsa -out private.pem 2048
openssl rsa -in private.pem -outform PEM -pubout -out public.pem
openssl pkcs8 -topk8 -inform pem -in private.pem -outform pem -nocrypt -out private-pkcs8.pem

The private key need not be encrypted. If it is, obviously you need to specify the private-key-password. That password can be (should be!) a variable - specify it in curly braces in that case. You should retrieve it from secure storage before invoking this policy.

To use PS256, just set the algorithm appropriately:

    <Properties>
      <Property name="algorithm">PS256</Property>

Generate a JWS using RS256

  <JavaCallout name='JavaCallout-JWS-Create-RS256' >
    <DisplayName>JavaCallout-JWS-Create-RS256</DisplayName>
    <Properties>
      <Property name="algorithm">RS256</Property>

      <!-- private-key and private-key-password used only for algorithm = RS256 -->
      <Property name="private-key">
      -----BEGIN PRIVATE KEY-----
      MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDkgKKKutQu7T8z
      ww0qWXHGEASVZZlhZ/bLdn2BAsmvpfyj8V40LDgKQUbX2d56aYmeYzENAINrLKUX
      ApEZC1CxzsyRfvKDtiKWfQKdYKLccl8pA4Jj0sCxVgL4MBFDNDDEau4vRfXBv2EF
      ....
      7ZOF1UXVaoldDs+izZo5biVF/NNIBtg2FkZd4hh/cFlF1PV+M5+5mA==
      -----END PRIVATE KEY-----
      </Property>

      <!-- this value goes into the JWT header to identify the
        key with which the JWT is signed. To support key rotation. -->
      <Property name="kid">{key_id}</Property>

      <Property name="payload">{request.content}</Property>
    </Properties>

    <ClassName>com.google.apigee.callout.jwtsigned.JwsCreatorCallout</ClassName>
    <ResourceURL>java://apigee-callout-jwt-signed-1.0.21.jar</ResourceURL>
  </JavaCallout>

You can also ask for detached content for the JWS, by specifying detach-content boolean as true:

    <Properties>
       ...
      <Property name="detach-content">true</Property>
       ...

Generate a JWT using RS256 - specify PEM file as resource in JAR

You can also specify the PEM as a named file resource that is bundled in the jar itself. To do this, you need to recompile the jar with your desired pemfile(s) contained within it. The class looks for the file in the jarfile under the /resources directory. Follow the example of the existing pem files. (Note: you will want to remove the existing PEM files from the JAR, as they are useful only for the examples given, and they should not be used in your own production deployment of the JWT policy) The configuration when using pem files bundled this way looks like this:

  <JavaCallout name='JavaCallout-JWT-Create'>
    <DisplayName>JavaCallout-JWT-Create</DisplayName>
    <Properties>
      <Property name="algorithm">RS256</Property>

      <!-- pemfile + private-key-password} used only for algorithm = RS256 -->
      <Property name="pemfile">private.pem</Property>
      <Property name="private-key-password">{var.that.contains.password.here}</Property>

      <!-- claims to inject into the JWT -->
      <Property name="subject">{apiproxy.name}</Property>
      <Property name="issuer">http://dinochiesa.net</Property>
      <Property name="audience">{context.var.that.contains.audience.name}</Property>
      <Property name="expiresIn">86400</Property> <!-- in seconds -->
      <Property name="id"/>
    </Properties>

    <ClassName>com.google.apigee.callout.jwtsigned.JwtCreatorCallout</ClassName>
    <ResourceURL>java://apigee-callout-jwt-signed-1.0.21.jar</ResourceURL>
  </JavaCallout>

The pemfile need not be encrypted. If it is, obviously you need to specify the password in the configuration of the policy. You can generate encrypted keypairs using the command-line openssl tool, like this:

openssl genrsa -des3 -out private-encrypted.pem 2048

This callout has been tested with Triple-DES (des3) and with AES256-CBC (-aes256) encrypted PEM files.

The PEM file(s) must be in PEM PKCS8 format, not DER format. (You can convert keys between various formats using the openssl command line tool). The class looks for the file in the jarfile under the /resources directory.

Generating a JWT with custom claims

If you wish to embed other claims into the JWT, you can do so by using the Properties elements, like this:

  <JavaCallout name='JavaCallout-JWT-Create'>
    <DisplayName>JavaCallout-JWT-Create</DisplayName>
    <Properties>
      <Property name="algorithm">RS256</Property>

      <!-- pemfile + private-key-password} used only for algorithm = RS256 -->
      <Property name="pemfile">private.pem</Property>
      <Property name="private-key-password">deecee123</Property>

      <!-- standard claims to embed -->
      <Property name="subject">{user_name}</Property>
      <Property name="issuer">http://apigee.net/{apiproxy.name}</Property>
      <Property name="audience">Optional-String-or-URI</Property>
      <Property name="expiresIn">86400</Property> <!-- in seconds -->
      <Property name="id"/>

      <!-- custom claims to embed in the JWT. -->
      <!-- Property names must begin with claim_ . -->
      <Property name="claim_shoesize">{user_shoesize}</Property>
      <Property name="claim_gender">{user_gender}</Property>

    </Properties>

    <ClassName>com.google.apigee.callout.jwtsigned.JwtCreatorCallout</ClassName>
    <ResourceURL>java://apigee-callout-jwt-signed-1.0.21.jar</ResourceURL>
  </JavaCallout>

The value of either standard or custom claims can be fixed strings or references to context variables - strings wrapped in curly braces.

If you would like to embed an array claim in the JWT, then you should use a variable reference, like so:

      <Property name="claim_api_products">{api_products_list}</Property>

And the context variable api_products_list should resolve to a String[].

Generate a JWT with custom claims from raw JSON

You can also use the json-payload property to just specify claims to include:

  <JavaCallout name='JavaCallout-JWT-Create'>
    <DisplayName>JavaCallout-JWT-Create</DisplayName>
    <Properties>
      <Property name="algorithm">RS256</Property>

      <!-- pemfile + private-key-password} used only for algorithm = RS256 -->
      <Property name="pemfile">private.pem</Property>
      <Property name="private-key-password">{private.pempassphrase}</Property>

      <Property name="expiresIn">1h</Property>

      <Property name="json-payload">{
      "sub" : "{user_name}",
      "iss" : "https://apigee.net/{apiproxy.name}",
      "aud" : "Optional-String-or-URI",
      "custom-claim" : { "foo": "bar", "quan": 123}
      }
      </Property>
    </Properties>
    <ClassName>com.google.apigee.callout.jwtsigned.JwtCreatorCallout</ClassName>
    <ResourceURL>java://apigee-callout-jwt-signed-1.0.21.jar</ResourceURL>
  </JavaCallout>

Verifying a JWT - HS256

For parsing and verifying a JWT, you need to specify a different Java class. Configure it like so for HS256:

  <JavaCallout name='JavaCallout-JWT-Parse'>
    <DisplayName>JavaCallout-JWT-Parse</DisplayName>
    <Properties>
      <Property name="algorithm">HS256</Property>

      <Property name="jwt">{request.formparam.jwt}</Property>

      <!-- name of var that holds the shared key (likely the client_secret) -->
      <Property name="secret-key">{organization.name}</Property>

    </Properties>

    <ClassName>com.google.apigee.callout.jwtsigned.JwtVerifierCallout</ClassName>
    <ResourceURL>java://apigee-callout-jwt-signed-1.0.21.jar</ResourceURL>
  </JavaCallout>

This class accepts a signed JWT in dot-separated format, verifies the signature with the specified key, and then parses the resulting claims.

It sets these context variables:

  jwt_jwt - the jwt string you passed in
  jwt_claims - a json-formatted string of all claims
  jwt_issuer
  jwt_jti
  jwt_audience
  jwt_subject
  jwt_issueTime
  jwt_issueTimeFormatted ("yyyy-MM-dd'T'HH:mm:ss.SSSZ")
  jwt_hasExpiry  (true/false)
  jwt_expirationTime
  jwt_expirationTimeFormatted
  jwt_secondsRemaining
  jwt_timeRemainingFormatted   (HH:mm:ss.xxx)
  jwt_signed (true/false indicating if JWT is signed)
  jwt_verified (true/false indicating if signature has been verified)
  jwt_isExpired  (true/false)
  jwt_isValid  (true/false)
  jwt_reason - human explanation for the reason a JWT is not valid

The "Formatted" versions of the times are for diagnostic or display purposes. It's easier to understand a time when displayed that way.

The isValid indicates whether the JWT should be honored - true if and only if the signature verifies and the times are valid, and all the required claims match.

Regarding the times: The policy checks the iat, nbf, and exp times, if they are present. You can tell the policy to ignore the iat claim - in other words tell the policy to not validate that iat was in the past - by using this property:

      <Property name="ignore-issued-at">true</Property>

Properties supported by JwtVerifierCallout

property description
algorithm require that the specified "alg" appear in the header
claim_xxx verify the claim "xxx" (replace with whatever you like) has the given value in the payload
secret-key specify the Symmetric key. Used for algorithm=HS256
public-key specify the PEM-encoded RSA public key. Used for algorithm=RS256 or PS256

Let's talk about Verification

The policy may return SUCCESS or ABORT - in other words it may succeed, or it may put the proxy into Fault processing. Faults occur only case of an un-foreseeable runtime error, or when there is an incorrect configuration. Examples of incorrect configuration:

  • if you specify algorithm=RS256 but do not specify a certificate or public-key with which to perform the validation.
  • if you specify algorithm=HS256 but do not specify a secret-key.
  • if you do not specify a jwt property

In all other cases, the callout will return SUCCESS, even if the signature does not verify properly, or if it is expired, and so on. SUCCESS indicates that the policy has completed its check, it does not indicate that the policy found the provided JWT to satisfy the configured constraints. For this reason, api proxy logic should check for the presence and value of variables like jwt_isValid, jwt_isExpired, and jwt_verified.

It is possible for a JWT to be signed and verified but not valid, according to the configured claims you are enforcing. If the JWT signature is not verifiable, then the JWT will also be not valid (jwt_isValid = false).

Parsing without Verifying - HS256

For parsing without verifying a JWT, you can specify wantVerify = false.

  <JavaCallout name='JavaCallout-JWT-Parse'>
    <DisplayName>JavaCallout-JWT-Parse</DisplayName>
    <Properties>
      <Property name="algorithm">HS256</Property>
      <Property name="wantVerify">false</Property>
      <Property name="jwt">{request.formparam.jwt}</Property>
    </Properties>

    <ClassName>com.google.apigee.callout.jwtsigned.JwtVerifierCallout</ClassName>
    <ResourceURL>java://apigee-callout-jwt-signed-1.0.21.jar</ResourceURL>
  </JavaCallout>

This will simply parse the JWT and set the appropriate context variables from the payload, without verifying the signature. wantVerify defaults to true.

Verifying and Decoding a JWT - RS256

To verify and decode a RS256 JWT, you can use a configuration like this:

  <JavaCallout name='JavaCallout-JWT-Parse-RS256-2'>
    <DisplayName>JavaCallout-JWT-Parse-RS256-2</DisplayName>
    <Properties>
      <Property name="algorithm">RS256</Property>
      <Property name="jwt">{request.formparam.jwt}</Property>
      <Property name="timeAllowance">30000</Property>

      <!-- public-key used only for algorithm = RS256 -->
      <Property name="public-key">
      -----BEGIN PUBLIC KEY-----
      MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtxlohiBDbI/jejs5WLKe
      Vpb4SCNM9puY+poGkgMkurPRAUROvjCUYm2g9vXiFQl+ZKfZ2BolfnEYIXXVJjUm
      zzaX9lBnYK/v9GQz1i2zrxOnSRfhhYEb7F8tvvKWMChK3tArrOXUDdOp2YUZBY2b
      sl1iBDkc5ul/UgtjhHntA0r2FcUE4kEj2lwU1di9EzJv7sdE/YKPrPtFoNoxmthI
      OvvEC45QxfNJ6OwpqgSOyKFwE230x8UPKmgGDQmED3PNrio3PlcM0XONDtgBewL0
      3+OgERo/6JcZbs4CtORrpPxpJd6kvBiDgG07pUxMNKC2EbQGxkXer4bvlyqLiVzt
      bwIDAQAB
      -----END PUBLIC KEY-----
      </Property>

      <!-- claims to verify. Can include custom claims. -->
      <Property name="claim_iss">http://dinochiesa.net</Property>
      <Property name="claim_shoesize">8.5</Property>

      <Property name="continueOnError">false</Property>

    </Properties>

    <ClassName>com.google.apigee.callout.jwtsigned.JwtVerifierCallout</ClassName>
    <ResourceURL>java://apigee-callout-jwt-signed-1.0.21.jar</ResourceURL>
  </JavaCallout>

By default, the Verifier callout, whether using HS256 or RS256, verifies that the nbf (not-before) and exp (expiry) claims are valid - in other words the JWT is within it's documented valid time range. By default the Parser allows a 1s skew for iat, exp and nbf claims. You can modify this with an additional property, as shown above, "timeAllowance". This is useful if the time on the issuing system is skewed from the time on the validating system. Set this value in milliseconds. In the example above, the value 30000 means that a JWT with a nbf time that is less than 30 seconds in the future will be treated as valid. Similarly a JWT with an exp which is less than 30 seconds in the past will also be treated as valid. Use a negative value (eg, -1) to completely disable validity checks on nbf and exp.

Beyond times, you may wish to verify other arbitrary claims on the JWT. At this time the only supported check is for string equivalence. So you may verify the issuer, the audience, or the value of any custom custom claim (either public/registered, or private). If the claim in the JWT is an array, the check verifies that the value provided is present in the array. For example, consider this JWT:

{
  "sub": "urn:75E70AF6-B468-4BCE-B096-88F13D6DB03F",
  "aud": "https://api.example.com/oauth2/token",
  "iss": "422720CE-6690-463E-9C5A-275423594FE7",
  "exp": 1471902991,
  "iat": 1471902961,
  "jti": "2e8a36bc-5c14-4105-837f-3245abc03027",
  "products" : [ "A", "B", "C"]
}

Then, you could verify the presence of ONE of the values of the products array, with a configuration like this:

  <JavaCallout name='JavaCallout-JWT-Parse-RS256-2'>
    <DisplayName>JavaCallout-JWT-Parse-RS256-3</DisplayName>
    <Properties>
      <Property name="algorithm">RS256</Property>
      <Property name="jwt">{request.formparam.jwt}</Property>
      <Property name="timeAllowance">30000</Property>
      <Property name="public-key">...</Property>

      <Property name="claim_products">A</Property>

    </Properties>

    <ClassName>com.google.apigee.callout.jwtsigned.JwtVerifierCallout</ClassName>
    <ResourceURL>java://apigee-callout-jwt-signed-1.0.21.jar</ResourceURL>
  </JavaCallout>

Regarding audience - the spec states that the audience is an array of strings. The parser class validates that the audience value you pass here (as a string) is present as one of the elements in that array. Currently there is no way to verify that the JWT is directed to more than one audience. To do so, you could invoke the Callout twice, with different configurations.

Alteratively, AFTER invoking the JwtVerifierCallout, compare the context variable 'jwt_claim_aud' to the result of array.join('|'). In other words, if you want to verify A, B, and C, then compare jwt_claim_aud to 'A|B|C' . The ordering in the JWT matters. To disregard ordering, you'd need to use a JavaScript to parse the jwt_claim_aud and check for each expected element.

As described previously, you can use the continueOnError property to instruct the policy to return Success, and not enter a fault, if the JWT verification does not succeed for any reason - times, signature, claims, well-formedness, etc.

Verifying a JWS - RS256

To verify a RS256-signed JWS, use a configuration like this:

  <JavaCallout name='JavaCallout-JWS-Verify-RS256'>
    <DisplayName>JavaCallout-JWS-Verify-RS256</DisplayName>
    <Properties>
      <Property name="algorithm">RS256</Property>
      <Property name="jws">{request.formparam.jws}</Property>

      <!-- public-key used only for algorithm = RS256 -->
      <Property name="public-key">
      -----BEGIN PUBLIC KEY-----
      MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtxlohiBDbI/jejs5WLKe
      Vpb4SCNM9puY+poGkgMkurPRAUROvjCUYm2g9vXiFQl+ZKfZ2BolfnEYIXXVJjUm
      zzaX9lBnYK/v9GQz1i2zrxOnSRfhhYEb7F8tvvKWMChK3tArrOXUDdOp2YUZBY2b
      sl1iBDkc5ul/UgtjhHntA0r2FcUE4kEj2lwU1di9EzJv7sdE/YKPrPtFoNoxmthI
      OvvEC45QxfNJ6OwpqgSOyKFwE230x8UPKmgGDQmED3PNrio3PlcM0XONDtgBewL0
      3+OgERo/6JcZbs4CtORrpPxpJd6kvBiDgG07pUxMNKC2EbQGxkXer4bvlyqLiVzt
      bwIDAQAB
      -----END PUBLIC KEY-----
      </Property>
    </Properties>

    <ClassName>com.google.apigee.callout.jwtsigned.JwsVerifierCallout</ClassName>
    <ResourceURL>java://apigee-callout-jwt-signed-1.0.21.jar</ResourceURL>
  </JavaCallout>

You can also verify a JWS that uses detached content by specifying the detached-content property. This should specify a string value that has been signed.

    <Properties>
       ...
      <Property name="detached-content">{request.content}</Property>
       ...

Verify the signature on a JWT, and also Verify specific claims

To verify specific claims in the JWT after verifying the signature, use additional properties. Do this by specifying Property elements with name attributes that begin with claim_ :

  <JavaCallout name='JavaCallout-JWT-Parse'>
    <DisplayName>JavaCallout-JWT-Parse</DisplayName>
    <Properties>
      <Property name="algorithm">RS256</Property>

      <!-- name of var that holds the jwt -->
      <Property name="jwt">{request.formparam.jwt}</Property>

      <!-- name of the pemfile. This must be a resource in the JAR!  -->
      <Property name="pemfile">rsa-public.pem</Property>

      <!-- specific claims to verify, and their required values. -->
      <Property name="claim_sub">A6EE23332295D597</Property>
      <Property name="claim_aud">http://example.com/everyone</Property>
      <Property name="claim_iss">urn://edge.apigee.com/jwt</Property>
      <Property name="claim_shoesize">9</Property>

    </Properties>

    <ClassName>com.google.apigee.callout.jwtsigned.JwtVerifierCallout</ClassName>
    <ResourceURL>java://apigee-callout-jwt-signed-1.0.21.jar</ResourceURL>
  </JavaCallout>

All the context variables described above are also set in this scenario.

As above, the isValid variable indicates whether the JWT should be honored. In this case, though, it is true if and only if the times are valid AND if all of the claims listed as required in the configuration are present in the JWT, and their respective values are equal to the values provided in the elements.

To specify required claims, you must use the claim names as used within the JSON-serialized JWT. Hence "claim_sub", "claim_jti", and "claim_iss", not "claim_subject" and "claim_issuer".

Verifying specific claims works whether the algorithm is HS256 or RS256.

If you do not include such claims to verify, then the callout doesn't check claims at all. You may wish to include checks for claims in the Edge flow, after the callout returns. To do that, you can reference the context variables, set by the policy.

Key sources for Verifying a JWT signed with RS256 and PS256

There are different ways to specify the public key to use, to verify the signature on the JWT. Each one uses a different property to specify that value to the callout.

  • public-key - specify the PEM-encoded public key directly.

        <Property name="public-key">
        -----BEGIN PUBLIC KEY-----
        MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtxlohiBDbI/jejs5WLKe
        Vpb4SCNM9puY+poGkgMkurPRAUROvjCUYm2g9vXiFQl+ZKfZ2BolfnEYIXXVJjUm
        zzaX9lBnYK/v9GQz1i2zrxOnSRfhhYEb7F8tvvKWMChK3tArrOXUDdOp2YUZBY2b
        sl1iBDkc5ul/UgtjhHntA0r2FcUE4kEj2lwU1di9EzJv7sdE/YKPrPtFoNoxmthI
        OvvEC45QxfNJ6OwpqgSOyKFwE230x8UPKmgGDQmED3PNrio3PlcM0XONDtgBewL0
        3+OgERo/6JcZbs4CtORrpPxpJd6kvBiDgG07pUxMNKC2EbQGxkXer4bvlyqLiVzt
        bwIDAQAB
        -----END PUBLIC KEY-----
        </Property>
  • pemfile - specify the name of a resource which is compiled into the JAR file.

       <!-- name of the pemfile. This must be a resource in the JAR. -->
       <Property name="pemfile">rsa-public.pem</Property>
  • certificate

        <!-- certificate used only for algorithm = RS256 -->
        <Property name="certificate">
        -----BEGIN CERTIFICATE-----
        MIIC4jCCAcqgAwIBAgIQ.....aKLWSqMhozdhXsIIKvJQ==
        -----END CERTIFICATE-----
        </Property>
  • jwks-uri - string specifying the JWKS URI.

      <Property name="jwks-uri">https://www.googleapis.com/oauth2/v3/certs</Property>
    

    When you use this, the JWKS will be cached for 10 minutes.

  • modulus and exponent - you need to specify two properties for this one.

      <!-- these properties are used only for algorithm = RS256 -->
      <Property name="modulus">{context.var.containing.modulus}</Property>
      <Property name="exponent">{context.var.containing.public.exponent}</Property>
    

The order of precedence the callout uses for determining the public key is this:

A. jwks-uri B. public-key C. modulus and exponent D. certificate E. pemfile

If you specify more than one of the above, the callout will use the first one it finds. It's not the order in which the properties appear in the configuration file; it's the order described here.

The way you specify the public key does not affect the way the callout verifies the signature or claims in a JWT or JWS. It only affects how the public key is retrieved.

Some comments about Performance

Performance of this policy will vary depending on many factors: the machine (CPU, memory) that supports the message processor, the other things running on the machine, the other traffic being handled by the message processor, and so on.

In my tests, it takes between 4ms and 12ms to generate a HS256-signed JWT on the Trial (free) version of hosted Apigee Edge. Caching the MACSigner in the Java code optimizes that. When the key is in cache, HS256 signing takes <1ms. Verifying signatures with HS256 takes about 1ms, with caching.

The signers and verifiers for RS256 are also cached, as of 2016 March 20. I haven't measured verification or creation of RS256-signed JWT. The cache will make a difference only at high load.

Runtime Errors

When verifying a JWT, you may see one of the following errors:

Error Reason Explanation
the signature could not be verified. If using RS256, the certificate or public keydoes not match the private key used to sign the token. Or, if using HS256, the secret key does not match the secret key used to sign the token. Or, the token has been modified after having been signed. For example a claim in the JWT was added or removed after signing, or an existing claim was modified after signing.
notBeforeTime is in the future the not-before-time (nbf) claim on the token is in the future. This means the issuer intended that the token should not yet be used.
the token is expired the expiry (exp) claim on the token is in the past. This means the issuer intended that the token should not be used past that time.
there is a mismatch in a claim One of the claims to be verified did not match what was found in the token.
audience violation None of the audience values on token token match the audience given in the policy configuration
Algorithm mismatch the token is signed with an algorithm that does not match what is provided in the policy configuration

Building the Jar

To build the binary JAR yourself, follow these instructions.

  1. unpack (if you can read this, you've already done that).

  2. build the binary with Apache maven. You need to first install it, and then you can:

    mvn clean package
    

    This will also run all relevant tests.

    If during the running of tests, you see an error like this in output:

        org.bouncycastle.openssl.EncryptionException: exception using cipher - please check password and data.
    

    ...it's probably because you don't have the unlimited strength ciphers installed for the JDK. Install that, and re-build.

  3. maven will copy all the required jar files to your apiproxy/resources/java directory. If for some reason your project directory is not set up properly, you can do this manually. copy target/apigee-callout-jwt-signed-1.0.21.jar to your apiproxy/resources/java directory, as well as all the dependencies.

License

This project and all the code contained within is Copyright 2017-2020 Google Inc, and is licensed under the Apache 2.0 Source license.

Limitations

  • This callout does not support JWT with encrypted claim sets.
  • This callout does not support EC algorithms