Uploaded image for project: 'Keycloak'
  1. Keycloak
  2. KEYCLOAK-8856

"state" parameter decode failure due to UUID vs. Base64 format mismatch

    Details

    • Steps to Reproduce:
      1. rm /tmp/cookies.txt
      2. curl -c /tmp/cookies.txt -L -v -u newb:newb 'http://localhost:3000/example'
    • Docs QE Status:
      NEW
    • QE Status:
      NEW

      Description

      Given this environment I'm trying to test Basic Auth using curl in the following manner, and subsequent outcome.

      $ rm /tmp/cookies.txt
      $ curl -c /tmp/cookies.txt -L -v -u newb:newb 'http://localhost:3000/example'
      

      This causes curl to automatically follow all the redirects, keep track of the cookies, and provide an Authorization: Basic ... header. This triggers an initial issue in Gatekeeper where it blindly assumed that any Authorization header must have a bearer token, which leads to a _unable to parse access token

      {"error": "malformed JWS, only 1 segments"}

      _ error. The below patch fixes this so this is not the main issue here.

      diff --git a/session.go b/session.go
      index b2e916d..6890b2e 100644
      --- a/session.go
      +++ b/session.go
      @@ -93,7 +93,9 @@ func getTokenInBearer(req *http.Request) (string, error) {
              }
      
              items := strings.Split(token, " ")
      -       if len(items) != 2 {
      +       if items[0] != "Bearer" {
      +               return "", ErrSessionNotFound
      +       } else if len(items) != 2 {
                      return "", ErrInvalidSession
              }
      

      With this patch in place, or by not using the -L option for curl and carefully supplying the Basic auth credentials only with the request that really needs them, I get further. Now this series of steps happens:

      1. GET http://localhost:3000/example
      2. --> 307 redirect to Location: /oauth/authorize?state=3e2781fb-0b54-4c97-af78-6e7410a8e6e3
        1. OAuth_Token_Request_State cookie set to same UUID value.
      3. GET http://localhost:3000/oauth/authorize?state=3e2781fb-0b54-4c97-af78-6e7410a8e6e3
      4. --> 307 redirect to Location: http://localhost:9797/auth/realms/DEMO/protocol/openid-connect/auth?client_id=example&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Foauth%2Fcallback&response_type=code&scope=vpn-user+openid+email+profile&state=3e2781fb-0b54-4c97-af78-6e7410a8e6e3
      5. GET http://localhost:9797/auth/realms/DEMO/protocol/openid-connect/auth?client_id=example&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Foauth%2Fcallback&response_type=code&scope=vpn-user+openid+email+profile&state=3e2781fb-0b54-4c97-af78-6e7410a8e6e3
      6. --> 302 redirect to Location: http://localhost:9797/auth/realms/DEMO/login-actions/authenticate?client_id=example&tab_id=svXq7J0S3tA&auth_session_id=854e1a48-4f78-4bf6-a87b-5704d983ac1a
        1. AUTH_SESSION_ID cookie set.
        2. KC_RESTART cookie set.
      7. GET http://localhost:9797/auth/realms/DEMO/login-actions/authenticate?client_id=example&tab_id=svXq7J0S3tA&auth_session_id=854e1a48-4f78-4bf6-a87b-5704d983ac1a
      8. --> 302 redirect to Location: http://localhost:3000/oauth/callback?state=3e2781fb-0b54-4c97-af78-6e7410a8e6e3&session_state=854e1a48-4f78-4bf6-a87b-5704d983ac1a&code=eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0..7k7JSMsbbziP6QYhD1WXSw.7c7lcyal4Rwpbi0_K64DJz4L3TDzNnlMdMQExTQKzqwe1vWWimbpOSlsmstNXds_KdfhW9q88b8vr4IKfIZUDa_q6Fhe01ZD75YoNUjHAmlwPpT2a6de3tXx_KaEkOaEZNcqTGN9ukN24qDYXJIlYD38VVwlHkgZsnOC8v5YJDCLrdsY4nJskAaQiw2DKPtCkq2b1ufY5bK9xPHErx9pg-5vvwonk8H5RnCo4QHkXlV8RPRf6m45xM_r9rOHIM_s.EXL0uOQbnZk4EP_lu1JvNQ
        1. KC_RESTART cookie set to empty string.
        2. KEYCLOAK_IDENTITY cookie set.
        3. KEYCLOAK_SESSION cookie set.
        4. KEYCLOAK_REMEMBER_ME cookie set to empty string.
      9. GET http://localhost:3000/oauth/callback?state=3e2781fb-0b54-4c97-af78-6e7410a8e6e3&session_state=854e1a48-4f78-4bf6-a87b-5704d983ac1a&code=eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0..7k7JSMsbbziP6QYhD1WXSw.7c7lcyal4Rwpbi0_K64DJz4L3TDzNnlMdMQExTQKzqwe1vWWimbpOSlsmstNXds_KdfhW9q88b8vr4IKfIZUDa_q6Fhe01ZD75YoNUjHAmlwPpT2a6de3tXx_KaEkOaEZNcqTGN9ukN24qDYXJIlYD38VVwlHkgZsnOC8v5YJDCLrdsY4nJskAaQiw2DKPtCkq2b1ufY5bK9xPHErx9pg-5vvwonk8H5RnCo4QHkXlV8RPRf6m45xM_r9rOHIM_s.EXL0uOQbnZk4EP_lu1JvNQ
      10. 307 redirect to Location: /
        1. kc-access cookie set.
        2. kc-state cookie set.
      11. GET http://localhost:3000/
        1. --> 200 OK, but this is not the original "/example" request URL

      Actual bug

      The oauthCallbackHandler in handlers.go contains this snippet at the very end of the function:

          // step: decode the state variable
          state := "/"
          if req.URL.Query().Get("state") != "" {
              decoded, err := base64.StdEncoding.DecodeString(req.URL.Query().Get("state"))
              if err != nil {
                  r.log.Warn("unable to decode the state parameter",
                      zap.String("state", req.URL.Query().Get("state")),
                      zap.Error(err))
              } else {
                  state = string(decoded)
              }
          }
          if r.config.BaseURI != "" {
              // assuming state starts with slash
              state = r.config.BaseURI + state
          }
      
          r.redirectToURL(state, w, req, http.StatusTemporaryRedirect)
      }
      

      This assumes that the "state" query parameter should contain a base64 encoded URL but looking at the above sequence of redirects the redirect to /oauth/callback has a "state" parameter that is (still) a UUID. Hence the base64 decoding fails and I get redirected to "/" rather than the original URL. I have not been able to pinpoint where/how gatekeeper manages the original url here. I see the unable to decode the state parameter warning with "illegal base64 data at input byte 8" as error message.

      Related conflict/discrepancy

      In getRedirectionURL in handlers.go there is this snippet which assumes that "state" is a UUID that can be compared to the OAuth_Token_Request_State cookie based on this commit. Maybe that commit or something related to those tickets caused a regression here, or at least there seems to be some conflict of interest in what the "state" query parameter should be.

      commit 82f61a2fd5aaf0368465bdd2a7e919fe8a1eeeb4
      Author: Bruno Oliveira da Silva <bruno@abstractj.org>
      Date: Fri Oct 26 19:03:56 2018 -0300

      OAuth2 state parameter

      • [KEYCLOAK-8669] Gatekeeper state parameter generated doesn’t have
        enough entropy
      • [KEYCLOAK-8667] CSRF vulnerability in Gatekeeper due to the lack of
        checks for "state" parameter

        Gliffy Diagrams

          Attachments

            Issue Links

              Activity

                People

                • Assignee:
                  abstractj Bruno Oliveira da Silva
                  Reporter:
                  bergner Marcus Bergner
                • Votes:
                  0 Vote for this issue
                  Watchers:
                  6 Start watching this issue

                  Dates

                  • Created:
                    Updated:
                    Resolved: