JWT processing succeeds with modified signature

Issue #178 invalid
Former user created an issue

Hi,

I'm no expert on JOSE so this might be trivial to explain, but I noticed when following your very nice guide on "Validating bearer JWT access tokens" here (thank you for that)

http://connect2id.com/products/nimbus-jose-jwt/examples/validating-jwt-access-tokens

jwtProcessor.process succeeds even if I do some modifications at the end of the token.

For example if my Google token ends with something like .....npJlIiLAkQ and I change it to something like .....npJlIiLAkW, jwtProcessor.process(token, ctx) still succeeds.

So my question is if this is the expected behaviour, because in my understanding with this modification I basically change the signature which is an RSA encrypted SHA-256 hash and the validation still succeeds.

Thank you for any clarification!

My code is basically the same as in the tutorial

   // Token from Google (abbreviated)
   String token = "eyJhbGciOiJSUzI1NiIsImtpZCI.....npJlIiLAkW";
   String jwkUri = "https://www.googleapis.com/oauth2/v3/certs";

   // Set up a JWT processor to parse the tokens and then check their signature
   // and validity time window (bounded by the "iat", "nbf" and "exp" claims)
   ConfigurableJWTProcessor jwtProcessor = new DefaultJWTProcessor();

   // The public RSA keys to validate the signatures will be sourced from the
   // OAuth 2.0 server's JWK set, published at a well-known URL. The RemoteJWKSet
   // object caches the retrieved keys to speed up subsequent look-ups and can
   // also gracefully handle key-rollover
   JWKSource keySource = new RemoteJWKSet(new URL(jwkUri));

   // The expected JWS algorithm of the access tokens (agreed out-of-band)
   JWSAlgorithm expectedJWSAlg = JWSAlgorithm.RS256;

   // Configure the JWT processor with a key selector to feed matching public
   // RSA keys sourced from the JWK set URL
   JWSKeySelector keySelector = new JWSVerificationKeySelector(expectedJWSAlg, keySource);
   jwtProcessor.setJWSKeySelector(keySelector);

   // Process the token
   SecurityContext ctx = null; // optional context parameter, not required here
   JWTClaimsSet claimsSet = jwtProcessor.process(token, ctx);

   // Print out the token claims set
   System.out.println(claimsSet.toJSONObject());

Comments (8)

  1. Connect2id OSS
    • changed status to open

    Here is a test that was just added, using a Google ID token where the last signature char was replaced - 336f6bc.

    The process call throws a BadJWSException just as expected:

    com.nimbusds.jose.proc.BadJWSException: Signed JWT rejected: Invalid signature
    
        at com.nimbusds.jwt.proc.DefaultJWTProcessor.<clinit>(DefaultJWTProcessor.java:86)
        at com.nimbusds.jwt.proc.DefaultJWTProcessorTest.testGoogleIDToken(DefaultJWTProcessorTest.java:798)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at junit.framework.TestCase.runTest(TestCase.java:176)
        at junit.framework.TestCase.runBare(TestCase.java:141)
        at junit.framework.TestResult$1.protect(TestResult.java:122)
        at junit.framework.TestResult.runProtected(TestResult.java:142)
        at junit.framework.TestResult.run(TestResult.java:125)
        at junit.framework.TestCase.run(TestCase.java:129)
        at junit.framework.TestSuite.runTest(TestSuite.java:252)
        at junit.framework.TestSuite.run(TestSuite.java:247)
        at org.junit.internal.runners.JUnit38ClassRunner.run(JUnit38ClassRunner.java:86)
        at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
        at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:119)
        at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:42)
        at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:234)
        at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:74)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)
    

    Could you look at your code to see what else might be going on?

    Or submit a reproducible test?

  2. Benjamin Ertl

    Thanks for the fast reply .. I always changed the last char to a adjacent one e.g. A to B

    But I will look into my code as suggested and submit a reproducible test in a couple of days.

  3. Benjamin Ertl

    I created a small demo project here https://bitbucket.org/benji23/demo

    If I run the following with my Google id_token, the id_token and modified id_token both get validated.

    The code I am using is also listed below.

    mvn clean package
    export TOKEN=eyJhbG.......3bsGmYg
    java -jar target/demo-0.0.1-SNAPSHOT-jar-with-dependencies.jar $TOKEN
    
    package demo;
    
    import com.nimbusds.jose.JWSAlgorithm;
    import com.nimbusds.jose.jwk.source.JWKSource;
    import com.nimbusds.jose.jwk.source.RemoteJWKSet;
    import com.nimbusds.jose.proc.JWSKeySelector;
    import com.nimbusds.jose.proc.JWSVerificationKeySelector;
    import com.nimbusds.jose.proc.SecurityContext;
    import com.nimbusds.jwt.JWTClaimsSet;
    import com.nimbusds.jwt.proc.ConfigurableJWTProcessor;
    import com.nimbusds.jwt.proc.DefaultJWTProcessor;
    
    import java.net.URL;
    import java.security.Security;
    
    public class Main {
    
      public static void main(String[] args) {
    
        if (args.length < 1) {
          System.out.println("Please provide a token");
          return;
        }
    
        Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
    
        // The access token to validate, typically submitted with a HTTP header like
        // Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6InMxIn0.eyJzY3A...
        String googleToken = args[0];
        String googleJwk = "https://www.googleapis.com/oauth2/v3/certs";
    
        try {
          // 1. original token
          System.out.println("Original token\n" + googleToken + "\n");
          System.out.println("Validate token...");
    
          validateToken(googleToken, googleJwk);
    
          // 2. modified token
          char lastChar = googleToken.charAt(googleToken.length() - 1);
          char modChar = (char) (lastChar + 1);
    
          System.out.println("Modify last char " + lastChar + " => " + modChar + "\n");
    
          String modifiedToken = googleToken.substring(0, googleToken.length() - 1) + modChar;
          System.out.println("Modified token\n" + modifiedToken + "\n");
          System.out.println("Validate token...");
    
          validateToken(modifiedToken, googleJwk);
    
        } catch (Exception e) {
          e.printStackTrace();
        }
      }
    
      private static void validateToken(String token, String jwkUri) throws Exception {
        // Set up a JWT processor to parse the tokens and then check their signature
        // and validity time window (bounded by the "iat", "nbf" and "exp" claims)
        ConfigurableJWTProcessor jwtProcessor = new DefaultJWTProcessor();
    
        // The public RSA keys to validate the signatures will be sourced from the
        // OAuth 2.0 server's JWK set, published at a well-known URL. The RemoteJWKSet
        // object caches the retrieved keys to speed up subsequent look-ups and can
        // also gracefully handle key-rollover
        JWKSource keySource = new RemoteJWKSet(new URL(jwkUri));
    
        // The expected JWS algorithm of the access tokens (agreed out-of-band)
        JWSAlgorithm expectedJWSAlg = JWSAlgorithm.RS256;
    
        // Configure the JWT processor with a key selector to feed matching public
        // RSA keys sourced from the JWK set URL
        JWSKeySelector keySelector = new JWSVerificationKeySelector(expectedJWSAlg, keySource);
        jwtProcessor.setJWSKeySelector(keySelector);
    
        // Process the token
        SecurityContext ctx = null; // optional context parameter, not required here
        JWTClaimsSet claimsSet = jwtProcessor.process(token, ctx);
    
        // Print out the token claims set
        System.out.println(claimsSet.toJSONObject());
    
      }
    
      private static String hex(byte[] bytes) {
        StringBuffer hexString = new StringBuffer();
        for (int i = 0; i < bytes.length; i++) {
          hexString.append(Integer.toHexString(0xFF & bytes[i]));
        }
        return hexString.toString();
      }
    }
    
  4. Connect2id OSS

    It's now clear what's happening :)

    Apparently there's some zero padding at the end of the byte[] signature, so if you change the last char in a way that doesn't disturb the actual bytes, the signature will verify.

    This was checked by decoding the modified and the original signature BASE64URL to byte arrays, and then comparing them. Even though the last char was modified, in byte[] form they are still identical. This may not work 100% for all signatures though.

    // 1. original token
          System.out.println("Original token\n" + googleToken + "\n");
          System.out.println("Validate token...");
    
          validateToken(googleToken, googleJwk);
    
          SignedJWT googleJWT = SignedJWT.parse(googleToken);
    
          byte[] googleSig = googleJWT.getSignature().decode();
    
          // 2. modified token
          char lastChar = googleToken.charAt(googleToken.length() - 1);
          char modChar = (char) (lastChar + 1);
    
          System.out.println("Modify last char " + lastChar + " => " + modChar + "\n");
    
          String modifiedToken = googleToken.substring(0, googleToken.length() - 1) + modChar;
    
          SignedJWT modifiedJWT = SignedJWT.parse(modifiedToken);
    
          byte[] modifiedSig = modifiedJWT.getSignature().decode();
    
          System.out.println("Signatures equal: " + Arrays.equals(googleSig, modifiedSig));
    
          System.out.println("Modified token\n" + modifiedToken + "\n");
          System.out.println("Validate token...");
    
          validateToken(modifiedToken, googleJwk);
    

    I'll try to send you a PR to the test.

  5. Connect2id OSS

    It looks like RPs are blocked for your repo. Just insert the above array comparison lines from above, and rerun the test.

  6. Log in to comment