​How can I validate “scope” with the JWT Policies?

Not applicable

I like to validate a JWT in Apigee. As I don’t have access to Java Callout I tried the new build in VerifyJWT Policy. It works to verify the token with a jwks as well as the Issuer but I would like to verify that the correct scopes are included in the token and I found two issues:

Cases 1: The token has more claims than needed for the API resource e.g:

{
"iss": "https://d10l.eu.auth0.com/",
"sub": "auth0|5a70e4e394059f5e7527d6b1",
"aud": [
"https://api.d10l.de",
"https://d10l.eu.auth0.com/userinfo"
],
"iat": 1518429498,
"exp": 1518436698,
"azp": "94YJaDlR5QDpaS7Em6aC02_gj6kA1Q_G",
"scope": "openid profile https://api.d10l.de/products:write https://api.d10l.de/products:read"
}

And I want to check that ‘https://api.d10l.de/products:read` is include. It fails because it does a complete string comparison and fails if three are additional scopes in the token:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<VerifyJWT name="Verify-JWT">
<Algorithm>RS256</Algorithm>
<Source>inbound.jwt</Source>
<PublicKey>
<JWKS ref="cached.auth0.jwks"/>
</PublicKey>
<Issuer>https://d10l.eu.auth0.com/</Issuer>
<AdditionalClaims>
<Claim name="scope">http://api.d10l.de/products:read</Claim>
</AdditionalClaims>
</VerifyJWT>

Cases 2: The API can be access with multiple scopes e.g. `http://api.d10l.de/products:read` or ` http://api.d10l.de/products:admin` would be ok. Something like

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<VerifyJWT name="Verify-JWT">
<Algorithm>RS256</Algorithm>
<Source>inbound.jwt</Source>
<PublicKey>
<JWKS ref="cached.auth0.jwks"/>
</PublicKey>
<Issuer>https://d10l.eu.auth0.com/</Issuer>
<AdditionalClaims>
<Claim name="scope">[http://api.d10l.de/products:read, http://api.d10l.de/products:admin] </Claim>
</AdditionalClaims>
</VerifyJWT>

Again I only can do a simple string comparison while I would need a construct that allows to check that either of the scopes are inside the field.

Is there a way to configure those features?

Thanks Dennis

3 6 6,877
6 REPLIES 6

Today, you cannot configure the VerifyJWT policy to perform a "partial match" on a string claim.

For example, you cannot configure the policy to check for the substring "xxxxx" (or any substring) in a claim named "scope".

In the case that the scope claim holds a string value that is a set of substrings separated by whitespace, the claim value is still a simple string.

YOU CAN configure a condition outside the VerifyJWT policy, to check for a partial match on one of the claim values. When VerifyJWT runs, it sets variables containing the values of the various claims. Supposing a JWT payload like this:

{
  "iss": "https://d10l.eu.auth0.com/",
  "sub": "auth0|5a70e4e394059f5e7527d6b1",
  "aud": [
    "https://api.d10l.de",
    "https://d10l.eu.auth0.com/userinfo"
  ],
  "iat": 1518429498,
  "exp": 1518436698,
  "azp": "94YJaDlR5QDpaS7Em6aC02_gj6kA1Q_G",
  "scope": "openid profile https://api.d10l.de/products:write https://api.d10l.de/products:read"
}

You could use this Verify policy to check audience and issuer and subject if you like:

<VerifyJWT name="Verify-JWT-2">
    <Algorithm>HS256</Algorithm>
    <Source>request.formparam.jwt</Source>
    <SecretKey>
        <Value ref="private.secret_key"/>
    </SecretKey>
    <Subject>auth0|5a70e4e394059f5e7527d6b1</Subject>
    <Issuer>https://d10l.eu.auth0.com/</Issuer>
    <Audience>https://api.d10l.de</Audience>
</VerifyJWT>

(NB: The audience is treated specially as the claim value is potentially an array. This policy config checks that the specified audience is one of the elements of the array.)

THEN, immediately after that policy, you could use a regex check, and raise a fault if it fails. Like this:

            <Request>
                <Step>
                    <Name>Verify-JWT-2</Name>
                </Step>
                <Step>
                    <Condition>NOT (jwt.Verify-JWT-2.claim.scope ~~ ".*\bhttps://api.d10l.de/products:read\b.*")</Condition>
                    <Name>RF-MissingScope</Name>
                </Step>
            </Request>

That little bit of hieroglyphics with the double twiddle is a regex match. The pattern says "look for the string https://api.d10l.de/products:read bounded by non-word characters (like space or end-of-string or beginning-of-string)".

The ~~ operator in Apigee Edge matches on the entire string, so you need the .* at the beginning and the end of the pattern to slurp up any potential other characters.

If the claim really IS an array value, then you can do the checking inside the policy. Specifically, if the JWT looks like this:

{
  "aud": ["https://api.d10l.de", "https://api.d10l.de"],
  "sub": "auth0|5a70e4e394059f5e7527d6b1",
  "nbf": 1518751495,
  "azp": "94YJaDlR5QDpaS7Em6aC02_gj6kA1Q_G",
  "scope": ["openid", "profile", "https://api.d10l.de/products:write", "https://api.d10l.de/products:read"],
  "iss": "https://d10l.eu.auth0.com/",
  "exp": 1518780295,
  "iat": 1518751495
}

...then you can verify the presence of any a specific string as a member of the scope claim using this policy configuration:

<VerifyJWT async="false" continueOnError="false" enabled="true" name="Verify-JWT-1">
    <Algorithm>HS256</Algorithm>
    <Source>request.formparam.jwt</Source>
    <SecretKey>
        <Value ref="private.secret_key"/>
    </SecretKey>
    <Subject>auth0|5a70e4e394059f5e7527d6b1</Subject>
    <Issuer>https://d10l.eu.auth0.com/</Issuer>
    <Audience>https://api.d10l.de</Audience>
    <AdditionalClaims>
        <Claim name="scope" type="string" array="true">https://api.d10l.de/products:read</Claim>
    </AdditionalClaims>
</VerifyJWT>

And in that case you don't need an extra Condition and RaiseFault.

Clear?

ps: I'm not sure if it would be a good idea to allow a regex match capability on the Claim string. That's an intriguing possibility. What I mean is:

<VerifyJWT name="Verify-JWT">
  <Algorithm>RS256</Algorithm>
  <Source>inbound.jwt</Source>
  <PublicKey>
    <JWKS ref="cached.auth0.jwks"/>
  </PublicKey>
  <Issuer>https://d10l.eu.auth0.com/</Issuer>
  <AdditionalClaims>
    <Claim name="scope" regex='true'>.*\bhttp://api.d10l.de/products:read\b.*</Claim>
  </AdditionalClaims>
</VerifyJWT>

And it would use the regex in the TEXT node of the Claim element to compare to the value. This would work only when the type of the claim is string. It really wouldn't be that difficult to implement. I wonder if it would be useful . IT would allow you to skip the Condition and RaiseFault , when the claim value is a space-separated string , rather than an actual array.

Hi @Dino-at-Google,

Just what I wanted, or so I thought. It doesn't appear to work.

In my case, I want to validate one or all of the scopes, even if they may be in a different order.

Given a JWT like this:

{
  "sub": "application-access",
  "aud": "audience.com",
  "scope": [
    "email",
    "openid",
    "profile"
  ],
  "iss": "apigee.net",
  "exp": 1573678330,
  "iat": 1573677730,
  "jti": "e22543ad-4ac0-47c0-af33-3eaef9832f5d"
}

Verify JWT works with the original scope using:

<VerifyJWT name="Verify-JWT-Created">
...
    <AdditionalClaims>
        <Claim name="scope" type="string" array="true">email,openid,profile</Claim>
    </AdditionalClaims>
</VerifyJWT>

But when I use just "profile" or "profile,openid,email" it does not.

<VerifyJWT name="Verify-JWT-Profile">
...
    <AdditionalClaims>
        <Claim name="scope" type="string" array="true">profile</Claim>
    </AdditionalClaims>
</VerifyJWT>

I get a the following error:

Invalid Claim: policy(Verify-JWT-Profile) claim(scope)

I've attached a simple test proxy. I can use a Condition and Raise Fault as a work around as you suggested.

Am I missing something?

jwt-rev5-2019-11-13.zip

Hmm, probably you're not missing anything.

Based on your observations, I've erred in my description of the validation of array claims in the policy. I've looked again: VerifyJWT does not check for "partial" match of an array. As you're observing, it checks for full matches. And the sort order must be correct! (The aud (audience) claim behaves differently; it checks for partial match)

Thanks for the confirmation.

Not applicable

Hi @Dino, thanks for the answer. I will check your solution, probably I can improve the developer experience by creating a shared flow that is parameterised from a variable in the flow.

About the regex on the claims, I guess this would make the experience better in many cases. Interesting would be the performance consideration if the regex match is implemented in the policy directly would this have a better performance then using two policies. As the token validation will be probably one of the most used parts in all APIs.