Add OpenAI project key validity check

What does this MR do and why?

This adds a validity check for OpenAI project keys (starting with prefix sk-proj-).

A user can then tell whether the exposed credential is still live rather than only that it was leaked.

Implementation

This follows ADR-006 and uses the registry directly to map the token straight to the verifier class/client, i.e. OpenaiClient.

The verifier calls GET https://api.openai.com/v1/models (reference) with the token as a Bearer credential.

We map the responses to the following statuses/outcomes:

Response Outcome
200 active
401 with invalid_api_key inactive
any other 401 unknown
429 RateLimitError
500, 502, 503, 504 NetworkError
anything else unknown

Reporting a live token as dead is worse than reporting nothing, so unmapped statuses are always unknown.

Why the status code is not enough

OpenAI answers 401 for four documented reasons, and only one of them means the key is no good. From its error code reference:

Documented 401 What it indicates
Incorrect API key provided The key is not valid
Invalid Authentication "Ensure the correct API key and requesting organization are being used"
You must be a member of an organization to use the API The account belongs to no organization
IP not authorized The request IP is outside the organization's allowlist

The last three describe a live key that this particular request cannot use, so treating a bare 401 as revoked would report a live credential as dead. The verifier reads the error body instead and resolves inactive only for the documented invalid-key code:

{ "error": { "message": "Incorrect API key provided: sk-proj-*******key...",
             "type": "invalid_request_error", "code": "invalid_api_key" } }

A revoked key returns that same code, confirmed against the live API with a real key before and after revoking it. Every other 401, including one carrying no code at all, resolves to unknown: the official Python client models code as an optional free-form string with no enumeration, so absent or unfamiliar codes are expected rather than exceptional.

Stack Position

We have 8 MRs for all new validity checks generated during Sec Challenge #6, with the base targeting master.

Each MR in the stack adds exactly one validity check: the client class, its spec, the registry entry, and the rate limit rule.

Stack (in review order)
# Check Token type
1 GitHub PAT Github Personal Access Token
2 OpenAI project key OpenAiProjectKey
3 Anthropic API key anthropic_key
4 Slack access tokens Slack token
5 Stripe live secret key StripeLiveSecretKey
6 Datadog API key DataDogAPIKey
7 SendGrid API token Sendgrid API token
8 Heroku API key Heroku API Key

Notes

How to set up and validate locally

  1. Run the specs:

    bundle exec rspec ee/spec/lib/security/secret_detection/partner_tokens/openai_client_spec.rb \
      ee/spec/services/security/secret_detection/partner_tokens/registry_spec.rb
  2. Confirm the registry resolves the token type and that the rate limit rule exists:

    Security::SecretDetection::PartnerTokens::Registry.client_for('OpenAiProjectKey')
    # => #<Security::SecretDetection::PartnerTokens::OpenaiClient>
    Gitlab::ApplicationRateLimiter.period_for(:partner_openai_api)
    # => 60 seconds
  3. Optional, with a real credential — a revoked token should come back inactive and a live one active:

    Security::SecretDetection::PartnerTokens::Registry
      .client_for('OpenAiProjectKey').verify_token(ENV['TOKEN']).status

MR acceptance checklist

I have evaluated this MR against the MR acceptance checklist.

Edited by Ahmed Hemdan

Merge request reports

Loading
Loading