A client ID is a public label and a client secret is a password for an application, yet teams routinely protect the first like a secret and spread the second like a label. Every OAuth 2.0 and OpenID Connect integration starts with this pair: an identifier the authorisation server issues to the application, and, for some applications, a shared secret the application uses to prove who it is at the token endpoint. Search demand for the two terms is high because the difference is genuinely confusing, and because the secret keeps turning up in mobile apps, browser code, Git history and support tickets. This guide explains what each value is, which applications should have a secret at all, how to store, rotate and retire secrets, and how to respond when one leaks. It is written for developers, identity engineers and secrets management owners; the deeper flow mechanics are in the linked OAuth guides.
Key takeaways
- The client ID is not a secret. RFC 6749 says it is exposed to the resource owner and must not be used alone for client authentication, so never build security on keeping it hidden.
- The client secret is a credential for a non-human identity. Whoever holds it can act as the application at the token endpoint, with every permission the application has been granted.
- Public clients should not rely on a secret. Mobile, desktop, CLI and single-page apps cannot keep one confidential; they should use authorisation code with PKCE, and servers should not accept an embedded secret as proof of identity.
- Prefer no shared secret at all. RFC 9700 recommends asymmetric client authentication (mTLS or private_key_jwt), and workload identity federation or managed identities remove the stored credential entirely.
- Rotation needs overlap, and leaks need speed. Use the two-secret overlap most providers support, record expiry dates, and treat a leaked secret as an incident: rotate, revoke issued tokens and review what the client did.
What a client ID and a client secret actually are
In OAuth 2.0 the "client" is the application asking for access, not the user. When you register an application with an authorisation server (Microsoft Entra ID, Google Cloud, Okta, Auth0, GitHub, Salesforce and so on) it is given a client identifier, and, if it is registered as a confidential client, one or more client secrets.
| Property | Client ID | Client secret |
|---|---|---|
| Purpose | Identifies which registered application is making the request | Proves the caller really is that application (client authentication) |
| Secrecy | Not secret. RFC 6749 Section 2.2 says it is exposed to the resource owner | Secret. Equivalent to a password for the application |
| Where it appears | Authorisation URLs, browser redirects, mobile app bundles, logs, consent screens | Only in server-side calls to the token endpoint (and introspection or revocation endpoints) |
| Uniqueness | Unique to the authorisation server that issued it | Unique per client ID; RFC 7591 says it should also be unique across instances sharing one client ID |
| Lifetime | Life of the registration | Should expire; RFC 7591 returns client_secret_expires_at, where 0 means it never expires |
| If exposed | Low impact on its own; enables phishing that impersonates the app and lets anyone start a flow with your app's name | High impact: attacker can obtain tokens as the app, including app-only tokens with no user present |
Two practical consequences follow. First, hiding the client ID buys nothing, but it is still useful in detection, because it is the key you search logs by. Second, the client secret belongs in the same governance as service account passwords and API keys: an owner, an expiry, a storage location and a rotation procedure. See the API Keys vs OAuth Tokens Guide for how the secret differs from the access tokens it is exchanged for.
Confidential and public clients: who should have a secret
RFC 6749 Section 2.1 defines two client types. A confidential client can keep its credentials secret, typically because it runs on a server you control. A public client cannot, and the RFC gives installed native applications and browser-based applications as examples. It also says a client with both a server component and a browser component should normally be registered as two separate clients.
| Application type | Client type | Secret? | What to use instead |
|---|---|---|---|
| Server-side web app (backend renders pages, holds tokens) | Confidential | Yes, or better an asymmetric method | private_key_jwt or mTLS where the server supports it |
| Backend service, scheduled job, integration (no user) | Confidential | Often, but avoid where possible | Workload identity federation, managed identity, or private_key_jwt with the client credentials grant |
| Single-page app running in the browser | Public | No | Authorisation code with PKCE, ideally behind a backend-for-frontend that is itself a confidential client |
| Mobile or desktop app | Public | No (any embedded value is extractable) | Authorisation code with PKCE in the system browser (RFC 8252) |
| CLI or headless device | Public | No | PKCE with a loopback redirect, or the device authorisation grant where there is no browser |
RFC 8252 (OAuth 2.0 for Native Apps) is explicit. Section 8.4 says native apps are classified as public clients unless each installed instance receives its own secret, for example through dynamic client registration. Section 8.5 says a secret distributed inside an app should not be treated as a confidential secret, that it is NOT RECOMMENDED for authorisation servers to require a shared secret from public native app clients, and that a server which still uses one must not accept it as proof of the client's identity. Section 6 requires public native app clients to implement PKCE.
Some providers still issue a secret to public apps. GitHub's OAuth app guidance, for example, acknowledges that a native or single-page app cannot secure the secret and will have to ship it in code, and tells developers to use PKCE and to be cautious about trusting tokens from such apps because anyone can reuse the client ID. Treat any secret shipped this way as public information. See the PKCE Implementation guide and the OAuth for Native and Mobile Apps Guide.
How the secret is sent, and better alternatives
RFC 6749 Section 2.3.1 defines the client password method. The server must support HTTP Basic authentication, with the form-encoded client ID as the username and the secret as the password. It may also accept client_id and client_secret as request-body parameters, but the RFC says sending them in the body is NOT RECOMMENDED and should be limited to clients that cannot use Basic. The parameters must never appear in the request URI, TLS is required, and the endpoint must be protected against brute force. RFC 7591 names these two methods client_secret_basic (the default) and client_secret_post; none means a public client with no secret.
| Method (token_endpoint_auth_method) | What the server stores | Replay and leak exposure | When to choose it |
|---|---|---|---|
client_secret_basic / client_secret_post | The shared secret (or a hash of it) | Secret travels with every token request; a copy anywhere works anywhere | Only where nothing better is supported; keep lifetimes short |
client_secret_jwt | The shared secret, used as an HMAC key | Secret no longer sent, but both sides hold it, so a server-side leak still exposes it | Rarely worth it; little gain over Basic |
private_key_jwt | Public key or JWKS URL | Private key never leaves the client; signed assertions are short-lived | Default choice for confidential clients that can manage a key |
tls_client_auth / self_signed_tls_client_auth (RFC 8705) | Certificate subject or public key | Key held in TLS stack or HSM; can also bind tokens to the certificate | High-assurance APIs, financial-grade profiles, service meshes |
| Workload identity federation / managed identity | Trust in an external issuer | No long-lived credential to leak | Workloads in cloud platforms or CI systems that issue their own identity tokens |
RFC 9700, the OAuth 2.0 Security Best Current Practice, says authorisation servers should enforce client authentication where feasible and that it is RECOMMENDED to use asymmetric cryptography, such as mutual TLS or signed JWTs (private_key_jwt), because the server then does not need to store sensitive symmetric keys. Microsoft gives the same direction for Entra ID: it recommends certificates over client secrets before production, and its migration guidance points to managed identities and federated identity credentials so that no secret is stored at all. See the JWT Client Authentication (private_key_jwt) Guide, the mTLS Guide and the Workload Identity Federation Guide.
Provider behaviour worth knowing
Secret handling differs between platforms, and those differences drive your rotation runbooks.
| Platform | Relevant behaviour (from vendor documentation) | Operational implication |
|---|---|---|
| Google Cloud OAuth clients | Secrets are hashed; the full value can be viewed or downloaded only once at creation, after which only the last four characters show. Google announced this in April 2025, applying it to new clients from June 2025 and to existing clients from November 2025. Maximum two secrets per client. Clients unused for six months are deleted automatically after a 30-day email warning. | Capture the secret straight into a secrets manager at creation; plan for "lost secret means rotate"; watch for deleted_client errors on rarely used integrations. |
| Microsoft Entra ID app registrations | Supports secrets, certificates and federated credentials; Microsoft recommends certificates over secrets and recommends moving to managed identities or federated credentials. Application management policies can block new password credentials or limit their lifetime (premium licensing). | Enforce a tenant policy on secret lifetime and creation; inventory app registrations with password credentials. See the Azure Entra Client Secrets Security Guide. |
| Okta | Generating a new client secret leaves the original ACTIVE so both work in parallel; old secrets are deactivated (INACTIVE, can be reactivated) and then deleted. Okta advises against secrets for native apps and recommends PKCE; it describes signing keys as giving substantially higher security. | Script rotation through the Apps API: create, deploy, test, deactivate, validate, delete. |
| GitHub OAuth apps | Guidance is to store the secret in a key vault or encrypted server secret; on compromise, generate a new secret, update the app and delete the old one. Device flow should only be enabled for constrained environments because it lacks redirect URIs and can be abused for phishing. | Audit which OAuth apps your organisation owns and who can reset their secrets. See How to audit GitHub Apps and OAuth apps in an organisation. |
Storing, issuing and rotating client secrets
Treat each confidential client as a non-human identity with a lifecycle. A sensible baseline:
- Ownership: every client registration has a named owning team, a business purpose and a link to the system that uses it. Registrations without an owner are the first to be forgotten and the last to be rotated.
- Storage: secrets live in a secrets manager or key vault and are injected at runtime. Not in source code, container images, CI variables visible to every job, wiki pages, tickets or chat. Google's guidance says the same: never commit them to version control or share them by email or forums.
- Lifetime: set an expiry on every secret. As an NHI Mgmt Group recommendation, 90 to 180 days for secrets that cannot yet be replaced, and no secret that never expires (a
client_secret_expires_atof 0 in dynamic registration is a finding). - One secret, one consumer: do not share a client registration between environments or services. Separate clients for dev, test and production make revocation surgical and logs readable.
- Least privilege: a leaked secret is only as bad as the scopes and application permissions behind it. Review granted scopes when you rotate. See the OAuth Scope Design Guide.
Rotation uses overlap. Google, Okta and Entra all allow a second secret to be active while the first is still valid, which gives a zero-downtime procedure:
- Create a new secret on the client registration and write it directly into the secrets manager (never through a human clipboard where avoidable).
- Deploy the consuming service so it reads the new value; confirm token requests succeed with it.
- Search code, configuration, pipelines and logs to confirm the old value is no longer referenced.
- Disable or deactivate the old secret where the platform supports it, and watch for
invalid_clienterrors. Disabling is reversible on Google and Okta, which is your rollback. - After a quiet period, delete the old secret and record the new expiry date in your inventory.
Rotation failures are a common cause of outages; see the Secret Rotation Outage Troubleshooting Guide and the OAuth invalid_client and unauthorized_client Troubleshooting Guide.
When a client secret leaks
A leaked client secret lets an attacker request tokens as your application. For clients allowed the client credentials grant, that means app-only access with no user, no MFA and often tenant-wide permissions. Respond as you would to a leaked privileged password:
- Scope: identify the client ID, the authorisation server, the grant types the client may use and the permissions it holds.
- Rotate: issue a new secret, deploy it, then disable and delete the leaked one. Rotation does not invalidate access tokens already issued, so also revoke refresh tokens and, where the platform allows, active sessions for that client. See the OAuth Token Introspection and Revocation Guide.
- Review activity: pull token issuance and API activity for the client ID from the exposure time onwards, looking for new source IPs, unusual scopes or bulk data access.
- Remove the source: purge the secret from Git history, images or logs, and find out how it got there.
- Fix the cause: move the client to private_key_jwt, federation or a managed identity if it is a backend, or to PKCE without a secret if it is a public client.
The OneLogin API flaw (CVE-2025-59363) shows why secrets stored by a platform are themselves a target: an API endpoint returned OIDC client secrets to anyone holding an API key. The Commvault Metallic breach exposed Microsoft 365 app secrets held by a SaaS backup provider on behalf of customers, which forced customers to rotate secrets they did not directly control. See the How to respond to a GitHub secret scanning alert runbook for the repository side.
Detection and measurement
Most authorisation servers log token requests with the client ID, grant type, source IP and outcome. Use them:
- Source change: token requests for a confidential client from IP ranges or networks outside its known hosting.
- Failed client authentication: bursts of
invalid_clienterrors can mean brute force, a stale secret after rotation, or an attacker testing a leaked value. - Grant drift: a client that normally uses authorisation code suddenly using client credentials, or new scopes being requested.
- Credential changes: new secrets added to an existing registration, especially high-privilege ones, are a known persistence technique; alert on them.
- Secret scanning hits: repository and pipeline scanning for OAuth secret patterns. See the Git Secret Scanning Guide.
Useful metrics:
- Percentage of confidential clients using asymmetric or federated authentication rather than a shared secret.
- Number of client secrets with no expiry, or expiry beyond your policy maximum.
- Number of public clients (SPA, mobile, CLI) still registered with a secret.
- Median time from secret leak detection to rotation and token revocation.
- Percentage of client registrations with a named owner, and number unused for 90 days.
Common failure patterns
- Secret in the mobile app or JavaScript bundle. Anything shipped to users is extractable, and RFC 8252 says it should not be treated as confidential. Use PKCE with no secret.
- Treating the client ID as sensitive and the secret as config. Teams redact client IDs from screenshots but paste secrets into tickets, .env files and Terraform state.
- Never-expiring secrets with tenant-wide permissions. A single forgotten secret on a high-privilege app registration becomes a durable backdoor; adding a new secret to an existing app is a common persistence method after compromise.
- One client registration shared across environments. A leak in a test environment then grants production access, and rotation breaks everything at once.
- Rotation without revocation. Changing the secret but leaving refresh tokens valid lets an attacker keep access. See the How to detect use of a stolen OAuth refresh token guide.
- Secrets held by a third party with no inventory. SaaS and backup providers often hold secrets for your tenant; when they are breached, you need to know which clients to rotate.
Framework mapping
| Framework | Reference | How it applies |
|---|---|---|
| NIST SP 800-53 Rev. 5 | IA-5 Authenticator Management | Lifetime, protection and change of client secrets |
| NIST SP 800-53 Rev. 5 | IA-9 Service Identification and Authentication | Authenticating applications and services before they get tokens |
| NIST SP 800-53 Rev. 5 | SC-12 Cryptographic Key Establishment and Management; AC-6 Least Privilege | Keys for private_key_jwt or mTLS; scopes and permissions behind each client |
| NIST CSF 2.0 | PR.AA-01 | Identities and credentials for services are managed |
| ISO/IEC 27001:2022 Annex A | 5.17 Authentication information; 8.24 Use of cryptography | Handling and protecting secrets and keys |
| MITRE ATT&CK | T1552.001 Credentials In Files; T1528 Steal Application Access Token; T1550.001 Application Access Token; T1078.004 Cloud Accounts | How client secrets and the tokens they produce are stolen and abused |
Practitioner checklist
- Inventory every OAuth client registration across identity providers and SaaS platforms, with owner, grant types, permissions and secret expiry.
- Remove secrets from all public clients (SPA, mobile, desktop, CLI) and require PKCE.
- Move backend clients to private_key_jwt, mTLS, managed identity or workload identity federation, highest privilege first.
- Set and enforce a maximum secret lifetime with platform policy where available; no non-expiring secrets.
- Store remaining secrets only in a secrets manager and inject them at runtime.
- Use separate client registrations per environment and per consuming service.
- Document a two-secret rotation procedure with a tested rollback (disable before delete).
- Alert on new secrets added to existing registrations and on token requests from unexpected networks.
- Include token revocation and activity review in the leaked-secret runbook.
Standards and references
- IETF: RFC 6749 The OAuth 2.0 Authorization Framework
- IETF: RFC 9700 Best Current Practice for OAuth 2.0 Security
- IETF: RFC 8252 OAuth 2.0 for Native Apps
- IETF: RFC 7591 OAuth 2.0 Dynamic Client Registration Protocol
- Microsoft Learn: Migrate applications away from secret-based authentication
- Google Cloud: Manage OAuth clients
- Okta Developer: Client secret rotation and key management
- GitHub Docs: Best practices for creating an OAuth app
Related NHI Mgmt Group resources: Machine-to-Machine OAuth (Client Credentials) Guide · OAuth 2.0 Security Best Current Practice (RFC 9700) Guide · Azure Entra Client Secrets Security Guide · Leaked secret breaches