A client secret protects an OAuth client only when the credential remains inside an environment controlled by the application owner. A backend server can usually provide that protection. JavaScript delivered to a browser, a mobile application package, a desktop executable, and a browser extension cannot.
Placing a client secret in one of these public applications does not make the application confidential. A user or attacker can inspect the source code, application bundle, source maps, network requests, memory, or compiled package and recover the credential.
The correct solution is not to hide the secret more effectively. It is to choose an OAuth architecture that does not depend on a secret the application cannot protect.
For public clients, the normal starting point is the Authorization Code flow with Proof Key for Code Exchange, commonly called PKCE. For applications that handle sensitive information or privileged operations, a backend-for-frontend can keep OAuth tokens outside the browser altogether.
Last reviewed: July 16, 2026. OAuth standards, identity-provider requirements, browser behavior, and library recommendations may change. Confirm the final implementation against your provider’s current documentation.
OAuth Is Not a Login Protocol by Itself
OAuth is primarily an authorization framework. It allows an application to obtain limited access to a protected resource without receiving the user’s password.
If the application needs to sign users in and establish their identity, use OpenID Connect on top of OAuth. OpenID Connect introduces concepts such as ID tokens, user identity claims, and the openid scope.
| Requirement | Use | Purpose |
|---|---|---|
| Access an API | OAuth 2.0 | Obtain limited authorization to access protected resources |
| Sign a user in | OpenID Connect | Establish the authenticated user’s identity |
| Application-only access | OAuth Client Credentials | Allow one protected backend service to access another |
Do not use an OAuth access token as proof of identity unless the provider explicitly defines that behavior. The client should not assume that every access token is a JWT or that its claims are intended for the client application.
Public Clients and Confidential Clients
OAuth distinguishes clients by their ability to protect credentials.
A confidential client runs in a controlled environment and can authenticate securely to the authorization server. A public client cannot reliably protect credentials distributed with the application.
| Application | Client Type | Can Protect a Secret? | Recommended Direction |
|---|---|---|---|
| Server-rendered web application | Confidential | Yes, when kept exclusively on the server | Authorization Code flow with server-side token exchange and PKCE |
| Backend-for-frontend | Confidential backend | Yes, inside the backend | Store tokens server-side and give the browser a protected session cookie |
| Browser SPA | Public | No | Authorization Code with PKCE, or use a BFF for stronger isolation |
| Mobile application | Public | No | Authorization Code with PKCE through the system browser |
| Desktop application | Public | No | Authorization Code with PKCE and an approved redirect method |
| Browser extension | Public | No | Provider-supported public-client flow with PKCE |
| Machine-to-machine backend | Confidential | Yes | Client Credentials flow from protected server infrastructure |
Choose the Architecture Before Writing OAuth Code
The most important decision is not which OAuth library to install. It is where tokens will be acquired, stored, and used.
The backend completes OAuth, stores access and refresh tokens, and creates a cookie-based session. OAuth tokens are not directly exposed to browser JavaScript.
The backend protects the refresh token but may provide short-lived access tokens to the browser. This reduces some risks but still exposes access tokens to the frontend.
The browser acts as a public OAuth client and handles tokens directly. Use Authorization Code with PKCE and design carefully for malicious JavaScript and token exposure.
The mobile or desktop app uses PKCE and opens the authorization request in the user’s system browser or another approved external user-agent.
Which Architecture Should You Choose?
- Prefer a backend-for-frontend for administrative dashboards, financial operations, private customer records, and other sensitive applications.
- Consider a token-mediating backend when the browser must call resource servers directly but refresh tokens should remain on the server.
- Use a browser-only public client when a backend is impractical and the risks are acceptable.
- Use a native public client for installed mobile and desktop software.
A BFF reduces direct token exposure, but it does not make browser security irrelevant. Malicious JavaScript may still make authenticated requests through the user’s active session. Content Security Policy, dependency control, output encoding, CSRF protection, and secure application development remain necessary.
How PKCE Replaces Dependence on a Public Client Secret
PKCE uses a new temporary value for every authorization attempt.
The client generates a high-entropy code_verifier. It then calculates a code_challenge from that verifier and sends the challenge in the authorization request.
After authorization, the client receives a short-lived authorization code. To exchange that code for tokens, it must send the original verifier. A party that intercepts only the authorization code cannot complete the exchange without the verifier.
PKCE does not turn a public client into a confidential client. It protects the authorization-code exchange. It does not prevent malicious JavaScript from using or stealing tokens after the application receives them.
Step 1: Register the Correct OAuth Client Type
Create separate client registrations for platforms with different security properties.
Do not use the same OAuth client registration for all of the following:
- A confidential web server application.
- A browser SPA.
- An Android application.
- An iOS application.
- A desktop application.
- Development, staging, and production environments.
Separate registrations allow each platform to have appropriate redirect URIs, consent settings, credential rules, token policies, and incident response.
Step 2: Register Exact Redirect URIs
The authorization server returns the user to a pre-registered redirect URI. Loose redirect validation may allow authorization codes or tokens to reach an attacker-controlled destination.
Register the complete URI whenever the platform permits it:
https://app.example.com/oauth/callback
Avoid broad wildcard patterns such as:
https://*.example.com/*
https://app.example.com/*
https://example.com/redirect?next=ANY_DESTINATION
Native desktop applications may use a loopback address with a dynamically selected port when supported by the provider, such as:
http://127.0.0.1:51004/oauth/callback
The redirect endpoint should process only expected OAuth responses. Do not turn it into a general-purpose open redirect.
Step 3: Generate a Verifier and an S256 Challenge
New implementations should use the S256 challenge method. The verifier must be generated with a cryptographically secure source of randomness and must never be logged or included in analytics.
The following browser example is illustrative. A maintained OAuth or OpenID Connect library is preferable for production systems.
function toBase64Url(bytes) {
const binary = Array.from(bytes, byte =>
String.fromCharCode(byte)
).join("");
return btoa(binary)
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
}
async function createPkceValues() {
const randomBytes = crypto.getRandomValues(new Uint8Array(32));
const codeVerifier = toBase64Url(randomBytes);
const verifierBytes = new TextEncoder().encode(codeVerifier);
const digest = await crypto.subtle.digest("SHA-256", verifierBytes);
const codeChallenge = toBase64Url(new Uint8Array(digest));
return {
codeVerifier,
codeChallenge
};
}
The verifier is temporary and belongs only to the current authorization transaction. Delete it after completing or abandoning the flow.
Step 4: Build the Authorization Request
A typical public-client request includes:
response_type=code- The public
client_id - An exact
redirect_uri - Minimal required scopes
- The PKCE
code_challenge code_challenge_method=S256- Transaction protection such as an appropriate
statevalue - An OpenID Connect
noncewhen required by the selected flow or library
GET https://identity.example.com/authorize
?response_type=code
&client_id=public-web-client
&redirect_uri=https%3A%2F%2Fapp.example.com%2Foauth%2Fcallback
&scope=openid%20profile%20api.read
&code_challenge=GENERATED_CHALLENGE
&code_challenge_method=S256
&state=UNIQUE_TRANSACTION_VALUE
&nonce=UNIQUE_OPENID_CONNECT_VALUE
The authorization request should open as a top-level browser navigation. Native applications should use the system browser or an approved external browser component rather than an embedded WebView.
PKCE, State, and Nonce Are Not Identical
| Value | Main Purpose | Required Action |
|---|---|---|
| PKCE verifier | Protect the authorization code against interception and injection | Send the matching verifier during the token exchange |
| State | Bind the response to the transaction and support CSRF protection | Generate a unique value and compare it after redirect |
| OIDC nonce | Bind an ID token to the authentication request and reduce replay risk | Validate the matching claim when the flow uses it |
Provider-enforced PKCE can also contribute to CSRF protection in properly configured flows. A practical implementation should use a trusted library and follow its transaction-handling requirements rather than removing state or nonce without understanding the consequences.
Step 5: Validate the Authorization Response
The redirect endpoint should not accept every request containing a parameter named code.
Before continuing:
- Confirm that the response belongs to an authorization transaction initiated by the application.
- Compare the returned
statewith the stored transaction value when state is used. - Reject missing, expired, duplicated, or unexpected transaction data.
- Confirm the expected authorization-server issuer when the provider supports issuer identification.
- Do not place authorization codes in logs, analytics events, support messages, or error-monitoring payloads.
- Display a safe error when the provider returns an OAuth error response.
Authorization codes should be short-lived and used only once. Do not store them as long-term application data.
Step 6: Exchange the Code Without a Client Secret
A public client sends its authorization code and the original verifier to the token endpoint. It does not pretend to authenticate with a static secret.
POST /token HTTP/1.1
Host: identity.example.com
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&client_id=public-web-client
&code=RETURNED_AUTHORIZATION_CODE
&redirect_uri=https%3A%2F%2Fapp.example.com%2Foauth%2Fcallback
&code_verifier=ORIGINAL_CODE_VERIFIER
The authorization server recalculates the challenge from the submitted verifier. If it does not match the challenge from the initial request, the exchange must fail.
Browser-only applications can perform this request only when the provider intentionally permits the public browser client to access its token endpoint, normally through an appropriate CORS policy.
Step 7: Validate Tokens in the Correct Component
The resource server, normally the API, is responsible for deciding whether an access token authorizes a request.
Depending on the provider and token format, this may require:
- Validating a cryptographic signature using trusted provider keys.
- Confirming the issuer.
- Confirming the intended audience or resource.
- Checking expiration and other time-related conditions.
- Checking required scopes or authorization details.
- Using the provider’s token introspection endpoint for opaque tokens.
- Rejecting tokens created for another API, client, tenant, or environment.
Do not assume that decoding a JWT means it is valid. Decoding reveals its contents; validation verifies whether it can be trusted.
If OpenID Connect is used, the client must separately validate the ID token according to the provider and library requirements. An ID token is intended for the client and should not normally be sent to an API as an access token.
Worked Example: Sensitive Dashboard Using a BFF
Consider a business dashboard that displays invoices, customer records, and account administration controls. Exposing refresh tokens to JavaScript would create unnecessary risk.
A backend-for-frontend can use this structure:
- The browser requests the BFF login endpoint.
- The BFF creates the authorization transaction and redirects the browser to the identity provider.
- The identity provider redirects the authorization code back to the BFF.
- The BFF exchanges the code from its protected server environment.
- The BFF stores access and refresh tokens in a server-side session.
- The browser receives only an opaque session cookie.
- The browser calls the BFF, and the BFF calls the approved API with the access token.
Cookie Requirements
The session cookie should normally use:
Secureso it is sent only over HTTPS.HttpOnlyso normal JavaScript cannot read it.- An appropriate
SameSitepolicy. - A restricted domain and path.
- A limited lifetime aligned with the server session.
- Rotation after authentication and other sensitive transitions.
Cookie-based sessions also require CSRF protection. Depending on the architecture, this may include SameSite controls, CSRF tokens, Origin or Referer validation, and restrictions on accepted content types and methods.
Worked Example: Browser SPA Using PKCE Directly
Consider a lower-risk SPA that reads a user’s project list from an API and cannot use a backend.
A safer implementation would:
- Register the SPA as a public client.
- Register its exact HTTPS redirect URI.
- Generate new PKCE and transaction values for each authorization attempt.
- Use Authorization Code with
S256. - Request only the scopes needed to read projects.
- Exchange the code without a client secret.
- Keep access tokens short-lived.
- Avoid persistent refresh tokens unless the provider implements the required protection.
- Apply strict frontend security and dependency controls.
This approach avoids embedding a client secret, but the SPA still executes in a browser environment. If malicious JavaScript runs in the application’s origin, it may access tokens, call APIs as the user, or initiate new authorization flows.
Where Should Tokens Be Stored?
No browser storage choice completely solves the risk of malicious same-origin JavaScript. Storage changes the exposure and persistence of a token, but application security remains essential.
| Location | Benefit | Risk or Limitation | Typical Fit |
|---|---|---|---|
| BFF server-side session | OAuth tokens are not directly exposed to browser JavaScript | Requires backend security, session management, and CSRF defenses | Sensitive browser applications |
| Browser memory | Tokens disappear when the execution context ends | Tokens remain usable by malicious code in the active context and do not survive reloads | Short-lived browser sessions |
| Web Worker or Service Worker | Can isolate some token operations from the main application context | Does not prevent malicious code from abusing the active session or starting a new flow | Advanced browser architectures |
| localStorage | Simple and persistent across page reloads | Readable by same-origin JavaScript and exposed for a longer period | Avoid for high-value or long-lived tokens when safer designs are available |
| Native secure storage | Uses operating-system facilities designed for protected application data | Security depends on the platform, device state, and implementation | Mobile and desktop applications |
| Backend secret manager | Central access control, auditing, and credential rotation | The server and deployment pipeline must still be secured | Confidential clients and backend credentials |
Refresh Tokens Require Additional Protection
Refresh tokens can obtain new access tokens and may remain useful longer than an individual access token. Their exposure can therefore create a longer-lived compromise.
When an authorization server issues refresh tokens to public clients, it should detect replay through one of these protections:
- Refresh token rotation: every successful refresh returns a new refresh token and invalidates the previous one.
- Sender-constrained refresh tokens: the token is cryptographically bound to a particular client instance.
The authorization server should also apply appropriate expiration, revocation, scope, and resource restrictions. Confirm whether your provider supports these protections before requesting offline access from a public client.
Rotation reduces risk but does not solve every browser compromise. Malicious JavaScript that remains active may continuously capture the latest token or initiate a new authorization flow.
Optional Advanced Protection with DPoP
Demonstrating Proof of Possession, or DPoP, can bind an access token to a cryptographic key controlled by the client. Unlike a normal bearer token, copying the token alone is not sufficient to use it.
DPoP may reduce the impact of simple token exfiltration when the authorization server and resource server support it correctly. However, it does not eliminate malicious-script risk. Code running inside the application may still use the key through the legitimate browser context or start a new flow with an attacker-controlled key.
Treat DPoP as an additional layer, not as a replacement for PKCE, XSS prevention, limited scopes, short lifetimes, and appropriate architecture.
Protect Client Secrets on a Backend
A confidential client may use a secret or another supported client-authentication method, but the credential must remain inside protected server infrastructure.
Do not place backend credentials in:
- Frontend JavaScript or bundled environment variables.
- Mobile or desktop application packages.
- Public Git repositories.
- Docker images distributed to untrusted environments.
- CI/CD logs.
- Error messages or monitoring events.
- Support tickets, chat channels, or shared documents.
- Example code published in documentation.
Use a secret-management service or appropriately protected server configuration. Restrict who and what can read the credential, audit access, and rotate it after suspected exposure.
Development, staging, and production should use separate OAuth clients and separate credentials.
Frontend Security Still Matters
Removing the client secret does not make a SPA safe from cross-site scripting. Malicious JavaScript running in the application’s origin may act with the same browser privileges as the legitimate application.
Important defenses include:
- Context-appropriate output encoding.
- A restrictive Content Security Policy.
- Avoiding unsafe HTML insertion.
- Reviewing and minimizing third-party scripts.
- Using Subresource Integrity where applicable.
- Keeping frameworks and dependencies updated.
- Protecting build and deployment pipelines.
- Restricting token scope and lifetime.
- Avoiding sensitive data in browser-accessible logs and analytics.
CORS is not an authorization mechanism. It controls which browser origins may read certain cross-origin responses, but APIs must still validate access tokens and enforce authorization on every protected operation.
Common OAuth Mistakes
| Mistake | Possible Result | Safer Direction |
|---|---|---|
| Putting a secret in JavaScript | Anyone can copy and reuse the credential | Register a public client and use PKCE, or move OAuth to a backend |
| Using the Implicit flow | Tokens may be exposed through browser redirects and other leakage paths | Use Authorization Code with PKCE |
| Using PKCE with plain challenge | The verifier may be exposed through the initial request | Use the S256 challenge method |
| Allowing wildcard redirects | Authorization responses may reach an unintended destination | Register and compare exact redirect URIs |
| Using embedded WebViews for native login | The application may inspect credentials and page content | Use the system browser or an approved external user-agent |
| Requesting every scope | A stolen token receives unnecessary power | Request only the permissions required by the active feature |
| Logging codes or tokens | Credentials may spread to monitoring and support systems | Redact OAuth credentials from logs and telemetry |
| Decoding a JWT without validating it | Forged or wrongly issued tokens may be trusted | Validate signature, issuer, audience, expiration, and authorization |
| Using the Password Grant | The client handles user credentials and bypasses safer authentication features | Use a redirect-based Authorization Code flow |
Production Readiness Checklist
When to Request a Security Review
Professional review is particularly important when OAuth protects:
- Money movement or payment administration.
- Healthcare or regulated records.
- Account recovery and identity verification.
- Administrative or support functions.
- Multi-tenant business systems.
- Enterprise federation and single sign-on.
- Highly privileged API scopes.
- Native applications using custom redirect schemes.
- Complex logout, token exchange, delegation, or impersonation workflows.
The review should cover more than whether login succeeds. It should examine redirect handling, client registration, token validation, scopes, refresh behavior, sessions, CSRF, XSS exposure, API authorization, logout, revocation, logging, and incident response.
Conclusion
Implementing OAuth without exposing client secrets begins with accepting that browsers, mobile apps, desktop apps, and extensions are public clients. A static credential shipped with those applications cannot provide reliable client authentication.
Use Authorization Code with PKCE and the S256 challenge method for public clients. Register exact redirect URIs, limit scopes, validate every authorization transaction, and protect tokens according to the application’s architecture.
For sensitive browser applications, a backend-for-frontend can keep OAuth access and refresh tokens on the server and give the browser only a protected session cookie. This reduces direct token exposure but still requires strong session, CSRF, XSS, dependency, and backend security.
The objective is not to conceal a credential inside public code. It is to design a flow that remains secure without depending on that impossible guarantee.
Frequently Asked Questions
Can I hide a client secret using environment variables?
Does PKCE make a SPA a confidential client?
Should a SPA store tokens in localStorage?
Do I need state when using PKCE?
Should mobile applications include a client secret?
Is a BFF always required?
Official References
- RFC 9700: Best Current Practice for OAuth 2.0 Security
- RFC 7636: Proof Key for Code Exchange by OAuth Public Clients
- RFC 8252: OAuth 2.0 for Native Apps
- RFC 9449: OAuth 2.0 Demonstrating Proof of Possession
- RFC 8414: OAuth 2.0 Authorization Server Metadata
- RFC 7662: OAuth 2.0 Token Introspection
- IETF Internet-Draft: OAuth 2.0 for Browser-Based Applications — work in progress
- OpenID Connect Core 1.0

The Linqdev Editorial Team researches, reviews, and publishes practical content about cloud architecture, API security, DevOps workflows, and scalable software systems. Our guides are prepared using official documentation, reliable technical sources, and careful editorial review to provide clear, accurate, and useful information for developers and technology professionals.




