Skip to content

Log truncated hash of refresh token param for refresh_token OAuth flow

What does this MR do and why?

  • For all POST /oauth/token requests for the refresh_token grant type, when a refresh_token param is present, log a truncated hash of its value.
  • Only log the first 10 characters of the computed hash .
    • This supports our use case while keeping info logged to a minimum.
  • I'm appending refresh_token_hash to the payload.metadata key, but this gets flattened by middleware and becomes a top-level log attribute.
    • I'm appending to metadata to be consistent with the existing log augmentation going on in this controller.

Rollout

I'm being ultra cautious and putting this behind a feature flag. The reason is the /oauth/token endpoint controls all OAuth token creation so downtime or anything unexpected could have severe negative impact. The code is not complicated but there is an external system involved (logging).

I think this is defensible given the published "when to use a FF" criteria but if there are strong opinions against, I'm interested in hearing them.

References

Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/523854

How to set up and validate locally

  1. Visit local GDK and create an OAuth application https://<gdk_url>/-/user_settings/applications
  2. Note the application ID, secret, and redirect URI.

First, we'll validate that the flow does not log refresh_token_hash when the feature flag is disabled (the default).

Step 1: Set Environment Variables

Set up your OAuth application credentials:

export CLIENT_ID="your_application_id_here"
export CLIENT_SECRET="your_application_secret_here"
export REDIRECT_URI="your_redirect_uri_here"
export GITLAB_URL="your_gdk_url_here"

Step 2: Get Authorization Code

  1. Visit the authorization URL in your browser:

    echo "Visit: ${GITLAB_URL}/oauth/authorize?client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URI}&response_type=code&scope=api+read_user"
  2. Authorize the application when prompted in the browser

  3. Copy the authorization code from the redirect URL parameter (e.g., ?code=abc123...)

  4. Set the authorization code:

    export AUTH_CODE="your_authorization_code_here"

Step 3: Exchange Authorization Code for Tokens

Make a cURL request to get the initial access and refresh tokens:

curl -X POST "${GITLAB_URL}/oauth/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "grant_type=authorization_code" \
  --data-urlencode "client_id=${CLIENT_ID}" \
  --data-urlencode "client_secret=${CLIENT_SECRET}" \
  --data-urlencode "redirect_uri=${REDIRECT_URI}" \
  --data-urlencode "code=${AUTH_CODE}" \
  | jq '.'

Expected Response:

{
  "access_token": "abc123...",
  "refresh_token": "def456...",
  "token_type": "Bearer",
  "expires_in": 7200
}
  1. Extract and save the refresh token:
    export REFRESH_TOKEN="your_refresh_token_from_response"

Step 4: Calculate Expected Hash

Calculate the expected SHA256 hash of your refresh token (first 10 characters):

# Calculate full hash
FULL_HASH=$(echo -n "$REFRESH_TOKEN" | shasum -a 256 | cut -d' ' -f1)
EXPECTED_HASH_PREFIX=${FULL_HASH:0:10}

echo "Full refresh token: $REFRESH_TOKEN"
echo "Full SHA256 hash: $FULL_HASH"
echo "Expected hash prefix (first 10 chars): $EXPECTED_HASH_PREFIX"

Step 5: Start Log Monitoring

In a separate terminal, start monitoring the GitLab logs for refresh token hash entries:

# Monitor logs for refresh token hash entries
tail -f log/development_json.log | grep refresh_token_hash

Step 6: Make Refresh Token Request

Make a cURL request to refresh the access token:

curl -X POST "${GITLAB_URL}/oauth/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "grant_type=refresh_token" \
  --data-urlencode "client_id=${CLIENT_ID}" \
  --data-urlencode "client_secret=${CLIENT_SECRET}" \
  --data-urlencode "redirect_uri=${REDIRECT_URI}" \
  --data-urlencode "refresh_token=${REFRESH_TOKEN}" \
  | jq '.'

Expected Response:

{
  "access_token": "new_access_token...",
  "refresh_token": "new_refresh_token...",
  "token_type": "Bearer",
  "expires_in": 7200
}

Step 7: Verify no log entry

  1. Check log monitoring terminal and validate a log was produced, but with no refresh_token_hash entry.

Step 8: Enable feature flag

  1. Get a Rails console and enable the feature flag
gdk rails c
> Feature.enable(:log_refresh_token_hash)
> true

Step 9: Repeat the refresh_token request

  1. Reset the REFRESH_TOKEN environment variable according to the most recent response from the server:
export REFRESH_TOKEN=<refresh_token_from_last_response>
  1. Repeat Step 6 and make another refresh_token request.

Step 10: Verify Logging

  1. Check the log monitoring terminal for entries containing refresh_token_hash

  2. Expected log entry format:

    {"time":"2024-01-01T12:00:00.000Z","severity":"INFO","message":"OAuth token refresh","refresh_token_hash":"abc1234567"}
  3. Observe there is a refresh_token_hash key logged and the value is a truncated SHA256 hash as expected.

MR acceptance checklist

Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.

Related to #523854

Edited by Jason Knabl

Merge request reports

Loading