OAuth Testing Guide for PKCE Tokens and Secure Sign In

Design practical OAuth tests for public web and native apps covering S256 PKCE, redirects, OIDC validation, token rotation, revocation, and recovery.

OAuth Testing cover showing a public app exchanging an authorization code with PKCE protection

A successful sign in can hide a callback that accepts the wrong transaction or an API that accepts the wrong token.

OAuth testing should establish who can exchange a code, which API accepts the resulting token, and what happens when that permission expires. A browser arriving at the dashboard proves little about those boundaries. Build assertions around rejected requests and recovery as carefully as the successful journey.

The scope here is OAuth 2.0 authorization code with S256 PKCE for public browser and native clients. Every product scenario below is hypothetical and describes intended tests, without claiming experimental results. Run negative checks only in authorized staging environments with synthetic accounts and isolated registrations. Never replay customer tokens or probe another application's production endpoints.

Separate authorization from authentication

Identify the responsibility of each token

Build separate assertions for delegated API access and the identity used to create an application session. An OAuth access token authorizes resource access; an OpenID Connect ID token carries authentication claims for the client to validate. The OpenID Connect Core introduction defines that relationship. Receiving an access token alone does not establish that the client validated an authenticated identity.

In an example scheduling app, the access token permits reading appointments at the scheduling API. An ID token identifies the signed in person to the client. Test that supplying the ID token to the API does not grant access under your API's access token policy. Separately verify that the client creates a session only after its OIDC validation succeeds.

Use the identity provider's trusted configuration to validate ID token issuer, client audience, and expiration under OIDC's validation rules. For a client configured to verify signatures, test an untrusted signing key and a disallowed algorithm. Add wrong issuer, wrong client audience, and expired token fixtures. Assert that each leaves the application signed out and cannot attach the returned identity to an existing account. Keep signature validation policy explicit because Core permits TLS server validation in place of signature checking for tokens received directly from the token endpoint in this flow.

Public clients cannot preserve a shared client secret as confidential. A value bundled into JavaScript or a native binary does not change that classification. RFC 6749 section 2.1 distinguishes public and confidential clients. A web architecture with a confidential backend needs additional tests for that backend's credentials and session boundary.

Build a repeatable OAuth testing workflow

OAuth testing workflow labeled Define the flow, Challenge tokens, and Check recovery
Define the flow, challenge tokens, and check recovery using isolated accounts and observable assertions.

Tie each assertion to an observable boundary

Define the flow

Record the client registration, trusted issuer, redirect URI, requested scopes, and intended resource server. Identify where the verifier and transaction context live during browser navigation or app suspension. Document whether OIDC and refresh tokens are enabled. For OIDC, assert that the outgoing scope includes openid, as required by Core's authentication request rules. Test a configuration that accidentally omits it, without assuming every provider returns the same error; behavior without that scope is unspecified by OIDC.

Load configuration from the intended issuer. RFC 8414 requires the metadata issuer to match the issuer used to retrieve it; its metadata also defines the optional supported PKCE methods field. Test a mismatched issuer fixture and verify that configuration is rejected. Missing advertised methods require a documented way to establish server support, rather than an assumption that PKCE is unavailable.

Challenge tokens

Start with one successful transaction through the actual client adapter. Then vary one input per case and assert the rejection at its owning boundary. Keep client callback validation, token endpoint checks, and resource server authorization separate so a failure identifies the responsible component.

For an example staging client, associate every case with a fresh transaction identifier. Retain sanitized request shapes, expected errors, and whether an API side effect occurred. Exclude sensitive data such as raw codes, verifiers, and tokens from CI artifacts. A failure message should identify a wrong audience fixture without printing the credential that carried it.

Check recovery

Exercise consent denial, interrupted navigation, expiration, and unavailable token services. Assert that the client leaves pending state consistently and offers a usable next action. A recovered screen must not conceal an unauthorized write, a duplicate request, or an account switch caused by stale transaction data.

Use unit tests for deterministic parsing and state transitions, integration tests against the staging issuer for protocol behavior, and a small browser or device journey suite for navigation. The Playwright end to end testing guide covers browser journey design. Provider mocks can drive rare failures, but their acceptance rules cannot establish what the actual issuer enforces.

Verify S256 PKCE and callback binding

Test the verifier as transaction data

RFC 9700 section 2.1.1 requires public clients to use PKCE. RFC 7636 specifies a fresh cryptographically random verifier for each authorization request, containing 43 to 128 unreserved characters. S256 hashes its ASCII representation and encodes the digest using base64url without padding. Verify that an S256 capable client never falls back to the plain transformation; RFC 7636 makes S256 mandatory for that client.

Test challenge generation with the RFC's published vector and boundary fixtures. Inspect the outgoing request for an explicit S256 method. Exercise the token exchange with a missing verifier and a different verifier; expect rejection and no issued credentials. Keep each negative case independent because a rejected exchange may consume its code under provider policy.

Also remove code_challenge from an authorization request in staging. Where PKCE is mandatory, expect authorization rejection. If the server issues an unbound code under another supported policy, redemption with code_verifier must fail. This checks the downgrade protection in RFC 9700; testing only codes that already have a challenge misses this boundary.

In an example app with two overlapping login attempts, retain separate verifier records. Complete the callbacks in reverse order and check that each resolves only its own transaction. Also redeem a code successfully, then attempt reuse. RFC 6749 section 4.1.2 requires denial of repeated code use and recommends revoking tokens previously issued from that code when possible.

Make the CSRF defense explicit

Under RFC 9700 section 2.1, a client needs a CSRF defense for its redirect flow. Establishing that the server supports PKCE allows the client to use that mechanism for CSRF protection. OIDC nonce can also provide protection; otherwise, one time state tokens securely bound to the user agent MUST be used. Record which mechanism your implementation actually validates.

When state is used, test missing, changed, expired, and previously consumed values against the client callback handler. Preserve the originating browser or app context in the fixture. A random string that is never compared with stored transaction data provides no useful assertion. For OIDC requests containing nonce, require the returned ID token's nonce to match, following Core section 3.1.3.7.

Check redirect registration and native navigation

Keep redirect comparisons exact

For the ordinary public client flow covered here, RFC 9700 requires exact string matching against registered redirects, with the native loopback port exception. Its redirect countermeasures also allow trusting redirect values when request origin and integrity are verified, for example through authenticated JAR or PAR; assess that separate design if used. Test unregistered paths, added query parameters, and changed scheme or host. Assert rejection without navigation to the invalid destination. Include a positive case for every legitimately registered callback so restrictive settings do not break supported clients.

For native loopback redirects, RFC 8252 section 7.3 requires authorization servers to allow any requested port for loopback IP redirect URIs. An example desktop client should succeed with two available ephemeral ports while an altered callback path fails. This exception does not permit arbitrary hosts or relaxed matching for ordinary web redirects.

Exercise the external user agent

RFC 8252 section 8.12 prohibits embedded user agents for native authorization requests. Use an external user agent, including an appropriate system browser authentication session. Test return to the app after cancellation, suspension, and a cold launch. Verify that unsupported or stale callbacks cannot complete a new pending transaction.

An example mobile release can pass adapter tests while its operating system callback registration points at the wrong build. Include installation and browser return on real supported devices. Keep passkey enrollment and credential recovery coverage in the separate passkey testing guide; those checks do not establish OAuth code exchange correctness.

Validate access at the resource server

Reject credentials that do not authorize the request

For deployments using the JWT access token profile, RFC 9068 section 4 defines validation of token type, issuer, audience, signature, and expiration. Test those checks through the API middleware with controlled issuer fixtures. Decoding a JWT and reading its claims does not validate it. Keep accepted algorithms in trusted configuration and test rejection of an unexpected algorithm.

In an example appointment API, issue a valid staging token intended for a separate reporting API and expect rejection. Give another fixture read scope and attempt an appointment update; assert both denial and unchanged data. With a controlled clock, exercise expiration before and after the configured skew allowance. State that allowance in the test so changing machine time cannot silently widen it.

Opaque access tokens need a different fixture strategy. RFC 7662 defines introspection's active indicator and relevant authorization metadata. Test inactive responses, unavailable introspection, and cache expiration under the application's documented policy. Check audience and scope where supplied and required by that policy. Do not treat a readable token shape as evidence of validity.

RFC 6750 specifies the Bearer authorization header method. Assert that the real client sends credentials to the intended API and that telemetry redacts that header. Inspect redirects and retry code for accidental forwarding to another origin. Use synthetic credentials when checking browser history, screenshots, and captured network traces for leakage.

Exercise refresh revocation and logout independently

Cover rotation and concurrent refresh

RFC 9700 section 2.2.2 requires public client refresh tokens to use sender constraint or rotation. Its rotation mechanism invalidates the previous token and revokes the active refresh token when reuse reveals a breach. Choose tests that match the configured mechanism.

In an example rotation fixture, refresh once, save the replacement securely, then present the old value in isolation. Check the provider's reuse response and subsequent recovery using its documented policy, including any retry grace behavior. Separately launch two authorized API calls that both encounter expiry. Assert that client refresh coordination preserves the replacement credential and does not retry writes twice.

Test a lost refresh response using a controlled transport failure. The server may have rotated the token even though the client saw a timeout. Define a bounded retry policy and a path back to authorization when recovery fails. Record actual staging behavior as evidence; do not infer it from a mock returning the error you configured.

Distinguish permission removal from session termination

RFC 7009 returns HTTP 200 for both successful revocation and an invalid submitted token. Therefore, assert subsequent credential behavior as well as the revocation response. Document propagation delay and cascading revocation policy. Avoid promising that revoking one refresh token immediately makes every previously issued access token unusable at every API.

Local logout clears the application's session and credentials. OIDC provider logout is a separate browser interaction specified by RP initiated logout. Test registered logout return destinations and local cleanup even when the provider is unavailable. Then sign in again and observe whether the provider still has a session, according to the intended product behavior.

Create a BetaRoast feedback round with safe staging accounts and a mission such as canceling consent, returning to the app, and signing in again. Ask whether recovery instructions and account identity are understandable. BetaRoast supplies human feedback on that experience; protocol enforcement and token validation remain responsibilities of your engineering tests.

Frequently asked questions

Does PKCE authenticate a public client?

PKCE binds code redemption to possession of a transaction verifier. It does not turn a distributed application into a confidential client or make a bundled secret trustworthy.

Should every OAuth test require both state and nonce?

Choose assertions for the implemented CSRF defense and protocol. State binds callback context when used. Nonce applies to OIDC and needs ID token validation. Neither protects a flow merely by appearing in an outgoing URL.

Can a successful logout prove tokens were revoked?

No. Verify local session cleanup, provider logout, and token revocation separately. The API's token validation and cache policy determine what it rejects after revocation.

How much can a mocked authorization server prove?

It can exercise deterministic client behavior and failure handling. Run separate integration checks against the actual staging issuer to establish registration, PKCE enforcement, refresh policy, and browser or device return behavior.

THE BOTTOM LINE

Exercise authorization code flows through the real client, enforce S256 PKCE and transaction binding, and verify token rejection at the API. Test refresh recovery, revocation, and logout separately. Combine isolated protocol checks with human feedback on consent and recovery through BetaRoast.

PUT IT INTO PRACTICE

Make your next beta count.

Define the mission, recruit matching testers, and turn feedback from real devices into a better release.

Start a Roast

Technical guidance can change. Verify current official documentation for your platform and tools before release.