Uploaded image for project: 'Red Hat build of Keycloak'
  1. Red Hat build of Keycloak
  2. RHBK-4017

DPoP proof replay check doesn't consider clock skew [GHI#43505]

XMLWordPrintable

    • False
    • Hide

      None

      Show
      None
    • False

      Before reporting an issue

      [x] I have read and understood the above terms for submitting issues, and I understand that my issue may be closed without action if I do not follow them.

      Area

      oidc

      Describe the bug

      DPoP proofs are checked for DPoPIsActiveCheck and DPoPReplayCheck conditions in org.keycloak.services.util.DPoPUtil.

      The first check takes into account both lifetime (10 seconds) and clockSkew (15 seconds), as documented in Allow a DPoP Proof by considering a clock skew for smooth interoperability

      The problem is that second check has not been updated accordingly to take clockSkew into account, so there is a 15 seconds window that causes hashString to be removed from the singleUseCache because of resulting negative time in putIfAbsent

      
      

      public boolean test(DPoP t) throws DPoPVerificationException {
      SingleUseObjectProvider singleUseCache = session.singleUseObjects();
      byte[] hash = HashUtils.hash("SHA1", (t.getId() + "\n" + t.getHttpUri()).getBytes());
      String hashString = Hex.encodeHexString(hash);
      if (!singleUseCache.putIfAbsent(hashString, (int)(t.getIat() + lifetime - Time.currentTime())))

      { throw new DPoPVerificationException(t, "DPoP proof has already been used"); }
      return true;
      }
      
      


      h3. Version

      26.4.0

      h3. Regression

      [ ] The issue is a regression

      h3. Expected behavior

      A 'replayed' DPoP proof must be checked for not being already used for additional clockSkew time

      h3. Actual behavior

      Submitting the same DPoP proof multiple times using the appropriate timing can lead to original proof being removed from cache (negative time computed), so attacker can use stolen proof at least one more time to replay.

      h3. How to Reproduce?

      I'm using oauth2c to simulate OAuth2 flow with pkce and dpop in the same Keycloak dev machine (resulting clock skew is 0 seconds in this test)
      export AUTHORIZATION_SERVER=my-keycloak-dev-machine
      

      # Simulate with oauth2c the Auth Code Flow and capture Refresh Token from Keycloak response
      
      export REFRESH_TOKEN=`oauth2c "$AUTHORIZATION_SERVER/.well-known/openid-configuration" \
        --client-id pkce-dpop-client \
        --response-types code \
        --response-mode query \
        --grant-type authorization_code \
        --auth-method none \
        --scopes openid,email,offline_access \
        --signing-key jwks.json \
        --pkce \
        --dpop | jq -r .refresh_token`
      

      # Simulate with oauth2c a refresh token request using captured token from previous call, then capture DPoP proof from Keycloak response
      
      export DPOP=`script -q -c 'oauth2c "$AUTHORIZATION_SERVER/.well-known/openid-configuration" \
        --client-id pkce-dpop-client \
        --grant-type refresh_token \
        --auth-method none \
        --refresh-token $REFRESH_TOKEN \
        --signing-key jwks.json \
        --dpop' | sed 's/\x1B\[[0-9;]\{1,\}[A-Za-z]//g' | sed -n 's/.*Dpop: \(.*\)/\1/p'`
      

      # Manually simulate refresh token request using the same refresh token and the same DPoP proof captured from previus call
      
      watch -n 1 'curl -v -k --request POST "$AUTHORIZATION_SERVER/$TOKEN_METHOD_ENDPOINT" \
        --header "Content-Type: application/x-www-form-urlencoded" \
        --header "DPoP:$DPOP" \
        --data-urlencode "grant_type=refresh_token" \
        --data-urlencode "client_id=pkce-dpop-client" \
        --data-urlencode "refresh_token=$REFRESH_TOKEN"'
      


      Watch ouput of curl command, first responses are 'DPoP proof has already been used' which is ok, but after 10 seconds the DPoP exists from the cache and attacker can issue a new refresh token

      h3. Anything else?

      Fix should be quite simple, just add (at least) clockSkew in time computation
      
      

      if (!singleUseCache.putIfAbsent(hashString, (int)(t.getIat() + lifetime + clockSkew - Time.currentTime()))) { throw new DPoPVerificationException(t, "DPoP proof has already been used"); }
      
      

              Unassigned Unassigned
              pvlha Pavel Vlha
              Keycloak Core Clients
              Votes:
              0 Vote for this issue
              Watchers:
              1 Start watching this issue

                Created:
                Updated:
                Resolved: