Switch branches/tags
Nothing to show
Find file History
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
..
Failed to load latest commit information.
src
LICENSE
README.md
pom.xml

README.md

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 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 JWT looks like this:

eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.
eyJzdWIiOiJ1cm46NzVFNzBBRjYtQjQ2OC00QkNFLxxxxxx.
D1OlHFCXAF4DPF7TfOphJ7AzpUOXHh7owZF

It's three parts, each separated by a dot. The indivdual parts are each base64-encoded payloads. They represent:

  • the header
  • the JWT body
  • the signature

The JWT body (payload), 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 above indicates a subject, audience, issuer, expiry, issued-at time, and JTI or unique identifier of the token. These are all "standard" claims for a JWT. The final thing included in the body is a "scope" claim, which is non-standard. All of this is signed, and a receiver can then validate the signature value against that payload, to determine whether the JWT should be considered valid.

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.apigee.callout.jwtsigned.JwtCreatorCallout</ClassName>
      <ResourceURL>java://apigee-edge-callout-jwt-signed-1.0.15.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

Jars available in Edge:

  • Apigee Edge expressions v1.0
  • Apigee Edge message-flow v1.0
  • Apache commons lang v2.6 - String and Date utilities
  • Apache commons codec 1.7 - Base64 and Hex codecs
  • not-yet-commons-ssl v0.3.9 - RSA private/public crypto

Jars not available in Edge:

  • Nimbus JOSE JWT v3.10
  • json-smart v1.3
  • Google Guava 18.0 (for collections utilities and cache)
  • Apache commons lang3 v3.4 - FastDateFormat

All these jars must be available on the classpath for the compile to succeed. The build.sh script should download all of these files for you, automatically.

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 two callout classes, one to generate a JWT and one to validate and parse a JWT. How the JWT 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.apigee.callout.jwtsigned.JwtCreatorCallout</ClassName>
    <ResourceURL>java://apigee-edge-callout-jwt-signed-1.0.15.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.

Starting in v1.0.13 of the callout, 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 may specify an explicit not-before time by providing a value that is the number of seconds since epoch, or a timestring 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

examples:

      <Property name="not-before">1508537209</Property>
      <Property name="not-before">2017-08-14T11:00:21.269-0700</Property>

As per the JWT specification (RFC 7519), subject, issuer, audience, and id are all optional claims. If you include them, they'll be inserted as claims into the generated JWT. If you don't include them, 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.apigee.callout.jwtsigned.JwtCreatorCallout</ClassName>
    <ResourceURL>java://apigee-edge-callout-jwt-signed-1.0.15.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.

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.apigee.callout.jwtsigned.JwtCreatorCallout</ClassName>
    <ResourceURL>java://apigee-edge-callout-jwt-signed-1.0.15.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.apigee.callout.jwtsigned.JwtCreatorCallout</ClassName>
    <ResourceURL>java://apigee-edge-callout-jwt-signed-1.0.15.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 clims, including a compound (JSON) claim

If you wish to embed claims that are themselves JSON hashes, you can do so by using a specially-named claim:

  <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>

      <!-- 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"/>

      <!-- Property names that begin with claim_json_ are parsed as json -->
      <Property name="claim_json_account">{"allocations":[4,"seven",false],"verified":true,"id":1234}</Property>
      <Property name="claim_json_attributes">{variable_name_here}</Property>

    </Properties>

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

Parsing and 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.apigee.callout.jwtsigned.JwtParserCallout</ClassName>
    <ResourceURL>java://apigee-edge-callout-jwt-signed-1.0.15.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>

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.apigee.callout.jwtsigned.JwtParserCallout</ClassName>
    <ResourceURL>java://apigee-edge-callout-jwt-signed-1.0.15.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.

Parsing and Verifying a JWT - RS256

To parse and verify a RS256 JWT, then you need to 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.apigee.callout.jwtsigned.JwtParserCallout</ClassName>
    <ResourceURL>java://apigee-edge-callout-jwt-signed-1.0.15.jar</ResourceURL>
  </JavaCallout>

By default, the Parser 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 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.apigee.callout.jwtsigned.JwtParserCallout</ClassName>
    <ResourceURL>java://apigee-edge-callout-jwt-signed-1.0.15.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 JwtParserCallout, 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.

Parse a JWT, and Verify specific claims

To verify specific claims in the JWT, 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.apigee.callout.jwtsigned.JwtParserCallout</ClassName>
    <ResourceURL>java://apigee-edge-callout-jwt-signed-1.0.15.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.

Parsing and Verifying a JWT - RS256 - pemfile

You can also specify the public key as a named file resource in the jar. To do this, you need to recompile the jar with your desired pemfile contained within it. The class looks for the file in the jarfile under the /resources directory. The configuration looks like this:

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

      <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>

    </Properties>

    <ClassName>com.apigee.callout.jwtsigned.JwtParserCallout</ClassName>
    <ResourceURL>java://apigee-edge-callout-jwt-signed-1.0.15.jar</ResourceURL>
  </JavaCallout>

Parsing and Verifying a JWT - RS256 - certificate

You can also specify a serialized X509 certificate which contains the public key.

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

      <!-- certificate used only for algorithm = RS256 -->
      <Property name="certificate">
      -----BEGIN CERTIFICATE-----
      MIIC4jCCAcqgAwIBAgIQ.....aKLWSqMhozdhXsIIKvJQ==
      -----END CERTIFICATE-----
      </Property>

      <!-- claims to verify -->
      <Property name="claim_iss">https://sts.windows.net/fa2613dd-1c7b-469b-8f92-88cd26856240/</Property>
      <Property name="claim_ver">1.0</Property>

    </Properties>

    <ClassName>com.apigee.callout.jwtsigned.JwtParserCallout</ClassName>
    <ResourceURL>java://apigee-edge-callout-jwt-signed-1.0.15.jar</ResourceURL>
  </JavaCallout>

This particular example verifies the issuer is a given URL from windows.net. This is what Azure Active Directory uses when generating JWT. (This URL is unique to the Active Directory instance, so it is not re-usable when verifying your own AAD-generated tokens.)

Parsing and Verifying a JWT - RS256 - using modulus + exponent

Suppose you have not the PEM-representation of the public key, and not a certificate, but the modulus and public exponent of the RSA key, in base64 encoded format. In this case you can specify the public key with those values, using the modulus and exponent properties:

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

    <!-- 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>

    <!-- claims to verify -->
    <Property name="claim_iss">http://server.example.com</Property>
    <Property name="claim_aud">s6BhdRkqt3</Property>

  </Properties>

  <ClassName>com.apigee.callout.jwtsigned.JwtParserCallout</ClassName>
  <ResourceURL>java://apigee-edge-callout-jwt-signed-1.0.15.jar</ResourceURL>
</JavaCallout>

This is useful when, for example, verifying keys that have been issued by Salesforce.com, which publishes its keys in JWK form, with modulus and exponent. The modulus and exponent should be in base64 format. The policy will eliminate any whitespace in these strings. They can be URL-safe base64 or non-URL-safe base64.

You can also specify these values statically in the configuration file.

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

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

If you specify more than one of {A,B,C,D} the callout will use the first one it finds. It's not the order in which the properties appear in the file; it's the order described here.

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-edge-callout-jwt-signed-1.0.15.jar to your apiproxy/resources/java directory.

    Also copy from the target/lib directory, these depedencies:

    • nimbus-jose-jwt-3.10.jar
    • bcprov-jdk15on-1.58.jar
    • commons-lang3-3.4.jar
    • bcpkix-jdk15on-1.58.jar
    • not-yet-commons-ssl-0.3.11.jar
    • json-smart-1.3.jar
    • guava-18.0.jar

License

This project and all the code contained within is Copyright 2017-2018 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