33 min read

Build an OAuth Authorization Code and PKCE Test Lab

Create a provider-agnostic OAuth test lab for PKCE, state, redirect URIs, token exchange failures, and public HTTPS callbacks with Localtonet.

OAuth authorization code lab showing browser redirects, PKCE validation, and token exchange.
The lab separates front-channel authorization redirects from the back-channel token exchange.
Developer Guides ยท OAuth PKCE Testing ยท Localtonet ยท 2026

Validate the protocol before debugging the provider

OAuth failures often look like provider problems even when the real defect is in redirect URI construction, transaction state, PKCE verification, callback handling, or the token exchange. This guide builds a provider-agnostic test lab around a local application and a controllable mock authorization server. We will verify the complete authorization code flow locally, add deliberate negative tests, and then use a Localtonet HTTP tunnel when the callback must be reachable at a public HTTPS address. The result is a repeatable test matrix that can separate protocol defects from framework, browser, and provider-specific behavior.

๐Ÿ”’ State, PKCE, and one-time code validation ๐ŸŒ Public HTTPS callbacks through an HTTP tunnel โšก Repeatable positive and negative test cases

What an OAuth authorization code and PKCE lab should test

A useful OAuth test lab does more than return a successful token response. It reproduces the security boundaries of an authorization code flow and lets us deliberately violate each boundary. The application must create an authorization transaction, send the browser to an authorization endpoint, receive a callback, validate that callback, and exchange the authorization code at a token endpoint. The mock authorization server must enforce the same relationships that a real provider is expected to enforce.

PKCE, short for Proof Key for Code Exchange, adds a per-transaction secret called the code_verifier. The application derives a code_challenge from that verifier and includes the challenge in the authorization request. Later, it sends the original verifier during the token exchange. The authorization server recomputes the challenge and rejects the exchange when the two do not match.

This is separate from the state parameter. State correlates the browser callback with a login transaction created by the application and is commonly used as part of cross-site request forgery protection. PKCE binds the authorization code to the client instance that started the flow. A robust application validates both rather than treating one as a replacement for the other.

๐Ÿงญ Authorization request Verify the endpoint, response type, client identifier, redirect URI, scope, state, PKCE challenge, and challenge method produced by the application.
๐Ÿ” Browser callback Confirm that the application accepts the expected code and state while rejecting missing, altered, duplicated, expired, and provider-error responses.
๐Ÿ” PKCE exchange Confirm that the token endpoint validates the verifier against the challenge stored with the authorization code.
๐ŸŽŸ๏ธ Authorization code lifecycle Treat codes as short-lived, client-bound, redirect-bound, and single-use credentials rather than reusable test constants.
๐Ÿงช Controlled failures Generate one failure at a time so that state, redirect, PKCE, client authentication, and token parsing defects remain distinguishable.
๐ŸŒ Public callback reachability Add a Localtonet HTTP tunnel only after local verification, giving the browser-facing callback a public HTTPS address without inbound router port forwarding.

Separate OAuth from OpenID Connect

OAuth grants access to protected resources. OpenID Connect adds an identity layer, including an ID token and provider discovery conventions. An OAuth-only token response does not have to contain an id_token. If the application under test expects an ID token, issuer metadata, a JSON Web Key Set, nonce validation, or signed-token verification, the lab is testing OpenID Connect behavior in addition to OAuth.

Keep those assertions in a separate part of the test suite. First prove that authorization request construction, callback state validation, PKCE, code consumption, and token error handling work. Then add identity-token validation using keys and claims intentionally configured for the lab. This separation makes failures much easier to diagnose.

Decide what is public and what remains local

The browser-facing callback is the component that may need a public address. The mock authorization server and token endpoint can usually remain local when the application can reach them directly. Exposing every lab component creates unnecessary attack surface and makes the test less representative of the intended boundary.

With Localtonet, the client application running on the machine establishes an outbound connection to our relay. An HTTP tunnel points to a local IP address and port and provides a public HTTPS address. No inbound router port forwarding, public IP address, firewall change, or VPN setup is required. The tunnel is available only while the selected device is connected and the tunnel is running.

The public URL is not the local service address

During public callback testing, the authorization request must carry the public HTTPS callback URI. The Localtonet tunnel still targets the local listener internally. Do not send the provider a localhost redirect URI and expect the browser to substitute the public address.

Prerequisites and lab boundaries

This guide is intentionally independent of a programming framework and mock-server package. Use a mock authorization server that lets the test control registered redirect URIs, client type, PKCE policy, code lifetime, token responses, and error responses. If a package hides those controls, it may be useful for demonstrations but not for protocol conformance testing.

You need two logical services. They may run as separate processes or as isolated components in one test harness:

  • Application under test: owns the login initiation route, callback route, transaction storage, token exchange logic, and post-login session behavior.
  • Mock authorization server: owns the authorization endpoint, temporary authorization-code records, token endpoint, client registration, and deterministic error injection.

You also need a browser for the redirect path, an HTTP inspection method that does not record secrets, and a Localtonet client on the machine that can reach the callback service when public HTTPS testing is required. Obtain the device authentication token and available relay server or region from the current Localtonet dashboard. Tokens are device-specific and must not be copied into source code, screenshots, test output, or this guide.

Lab value Where it belongs Validation rule
Client identifier Application configuration and mock client registration The token request must refer to the client for which the code was issued.
Redirect URI Mock registration, authorization request, and token exchange where required Use an exact, consistent value. Scheme, host, port, path, and relevant string representation must not drift between stages.
State Application transaction record and authorization request The returned value must match the outstanding transaction and must not be accepted without that transaction.
Code verifier Application transaction record only until token exchange Do not put it in the authorization URL, browser history, or normal logs.
Code challenge Authorization request and mock code record For S256, compare it with the base64url-encoded SHA-256 digest derived from the submitted verifier.
Authorization code Mock code store, callback query, and token request Make it short-lived, client-bound, redirect-bound, and unusable after a successful exchange.
Access or refresh token Token response and protected application storage Never expose the complete value in routine logs, URLs, assertion messages, or browser-visible errors.

Choose the client model deliberately

A public client cannot safely hold a client secret. Native applications and browser-based applications are common examples, although the exact architecture matters. These clients rely on PKCE rather than pretending that a bundled or browser-delivered value is confidential.

A confidential server-side client can authenticate to the token endpoint using its registered method. PKCE is still useful, but it does not eliminate the client-authentication rules assigned to that client. Configure the mock to match the client type being tested. Do not silently accept any secret, and do not add a client secret to a public-client test merely to make the exchange pass.

Use synthetic identities and synthetic credentials

A public tunnel makes the targeted local HTTP service reachable from the internet. Run the lab with test-only users, mock tokens, isolated data, and no production client secrets. Do not expose a developer server that also contains administrative routes, debug consoles, source maps with secrets, database tools, or unrelated local applications.

Build the provider-agnostic test lab

Provider-agnostic OAuth PKCE sequence from verifier generation through token exchange.
The code verifier remains local until the application exchanges the authorization code.

The following sequence defines behavior rather than tying the lab to a particular package. Endpoint paths are examples chosen by the lab owner. They are not Localtonet settings or universal provider defaults. If an existing application already uses different paths, retain those paths and apply the same validation rules.

1

Define one test client and its redirect URIs

Register a synthetic client identifier in the mock server. Mark it public or confidential according to the application architecture, require PKCE with S256 for the PKCE suite, and allow only explicit callback URIs. Start with a loopback callback for local verification. Add the Localtonet HTTPS callback later as a separate registered value rather than weakening redirect validation.

2

Create an authorization transaction

When login begins, generate an unpredictable state value and an independent high-entropy PKCE verifier. Store both in a short-lived transaction associated with the initiating browser session. Record only the fields needed to complete the transaction, such as creation time, intended post-login destination, redirect URI, verifier, and whether the transaction has been consumed.

3

Derive the PKCE challenge

For S256, hash the ASCII representation of the verifier with SHA-256, then base64url-encode the digest without padding. Do not use ordinary base64 unchanged because its alphabet and padding differ. The verifier itself remains in protected transaction storage until the token exchange.

4

Construct the authorization request

Send the browser to the mock authorization endpoint with response_type=code, the test client identifier, the exact redirect URI, requested scope, state, code challenge, and code_challenge_method=S256. Build the query using a URL encoder rather than string concatenation.

5

Issue a bound authorization code

After the mock approves the synthetic user, generate a new unpredictable code. Store a server-side record containing the client identifier, redirect URI, challenge, challenge method, issue time, expiry, approved scopes, test subject, and consumption status. Redirect the browser to the registered callback with the code and the original state.

6

Validate the callback before exchanging the code

Detect provider error parameters first, require the expected callback shape, locate the outstanding transaction, and compare state using a safe equality operation. Reject missing, unknown, expired, previously consumed, or mismatched transactions. Do not create a user session merely because a query string contains a code.

7

Exchange the code at the token endpoint

Send a form-encoded token request containing grant_type=authorization_code, the code, the same redirect URI, the client identifier where required, and the original code verifier. Apply the configured client-authentication method only for a client that is registered to use it.

8

Consume the code and finish the transaction

The mock validates the code record, client, redirect URI, expiry, consumption status, and PKCE proof. On success, consume the code atomically before returning synthetic tokens. The application validates the response, stores tokens according to its security model, removes the temporary transaction, and creates the expected application session.

Recommended transaction model

A transaction is temporary security state, not a general user session. Keeping it separate prevents stale login attempts from polluting the authenticated session and makes concurrent login attempts testable. The conceptual record can look like this:

{
  "transaction_id": "random-reference",
  "state_digest": "digest-or-protected-value",
  "pkce_verifier": "protected-temporary-value",
  "redirect_uri": "http://127.0.0.1:PORT/auth/callback",
  "created_at": "test-clock-value",
  "expires_at": "test-clock-value",
  "consumed": false,
  "return_path": "/account"
}

The example uses placeholders deliberately. Choose the local port and storage implementation from the application configuration rather than copying an invented default. If the transaction is stored in a browser cookie, protect its integrity and confidentiality according to the application framework. If it is stored server-side, give the browser only an opaque transaction reference.

PKCE generation model

A PKCE verifier is a high-entropy string between 43 and 128 characters using the permitted unreserved character set. A straightforward approach is to generate 32 cryptographically secure random bytes and encode them with unpadded base64url, which produces a 43-character verifier. The challenge calculation can be represented as:

verifier  = BASE64URL_WITHOUT_PADDING(SECURE_RANDOM_BYTES(32))
challenge = BASE64URL_WITHOUT_PADDING(SHA256(ASCII(verifier)))

Generate state independently. Reusing the verifier as state couples two security controls and can expose the verifier through the authorization URL. State normally appears in the browser redirect, while the verifier should not leave protected application storage until the direct token request.

Mock authorization endpoint behavior

The mock authorization endpoint should validate at least the response type, client identifier, registered redirect URI, PKCE challenge, and challenge method before issuing a code. It should support deterministic approval and denial paths. For browser testing, a minimal consent page can expose buttons such as approve and deny, but the visual login experience is not the primary target of this lab.

Preserve query encoding carefully. A redirect URI is itself a URI carried as a query parameter, so it must be encoded as a parameter value exactly once by the request-building layer. Tests should compare the decoded semantic parameter and also retain coverage for the final serialized URL. Double encoding and accidental decoding are common causes of callback mismatches.

Mock token endpoint behavior

The token endpoint should accept the media type and client-authentication method expected by the application. It must not validate only the authorization code string. It should retrieve the code record and confirm every binding before issuing tokens. A successful test response can use opaque synthetic token values if downstream JWT behavior is not being tested.

Make code consumption atomic. Two concurrent exchanges using the same code must not both succeed. In an in-memory mock, implement the check and state change in one critical section. In a database-backed mock, use a transaction or conditional update. A test that sends simultaneous exchanges can reveal a race that sequential tests miss.

Verify the complete flow locally first

A public tunnel should not be the first debugging tool. Verify that both services start, the browser can reach them, the application creates a valid transaction, the mock issues a bound code, and the token exchange succeeds entirely on the local machine. This establishes a known-good baseline before hostname, scheme, proxy, or public callback behavior is introduced.

Inspect the authorization redirect

Begin login without automatically following the first redirect. Inspect the response location and parse it as a URL. A successful construction test should answer all of these questions:

  • Does the URL point to the intended mock authorization endpoint?
  • Is response_type exactly code?
  • Does the client identifier match the mock registration?
  • Does the redirect URI exactly match the local registered callback?
  • Is state present, unpredictable, and associated with a stored transaction?
  • Are the expected scopes represented without relying on their display order?
  • Are code_challenge and code_challenge_method=S256 present?
  • Is the verifier absent from the URL?

Run the login initiation twice and confirm that both state and verifier change. Concurrent transactions should not overwrite each other unless the application explicitly allows only one active login attempt and safely invalidates the previous transaction.

Complete one successful browser flow

Follow the redirect to the mock, approve the synthetic user, and allow the browser to return to the local callback. Confirm that the application validates state before making the token request. At the token endpoint, capture structured test diagnostics showing that the client, redirect URI, code status, expiry, and PKCE proof were checked, but do not capture the raw verifier, code, or returned tokens.

After success, verify both sides of the lifecycle. The mock code record should be consumed, and the application transaction should be removed or marked unusable. The browser should receive the expected post-login redirect and a session appropriate to the application. A second request to the callback with the same code must not recreate or duplicate the session.

Verify callback error handling

An authorization server can redirect back with an error instead of a code. The application should recognize the error response, validate state when it is returned, avoid calling the token endpoint, clear or close the associated transaction, and present a safe message. Do not place raw provider descriptions into an HTML response without output encoding.

Test the absence of required values as well as malformed values. A callback with neither code nor error is not a successful flow. A callback with both should be treated cautiously and rejected rather than allowing ambiguous input to choose an unintended branch.

Use a controllable test clock where practical

Waiting in real time makes expiry tests slow and unreliable. If the application and mock can receive a test clock, advance it beyond transaction or code expiry and assert the result immediately. Keep production clock handling separate from test-only controls.

Run a deliberate OAuth and PKCE failure matrix

OAuth failure matrix comparing valid requests with state, redirect, verifier, and code errors.
Controlled failures isolate the validation rule responsible for each rejected OAuth request.

Negative tests are the main reason to maintain a controllable mock server. Change one input at a time, assert the layer that rejects it, and assert that no authenticated session or usable token is created. An HTTP error alone is not enough if the application has already persisted partial authentication state.

Test case Expected rejection point What to assert
Missing state in callback Application callback handler No token exchange, no session, and the event is recorded without sensitive query values.
Unknown or altered state Application callback handler The callback is rejected and cannot attach to another browser's transaction.
Expired transaction Application callback handler The application requires a fresh login rather than exchanging the code.
Unregistered redirect URI Mock authorization endpoint No code is issued and the mock does not redirect sensitive data to the unregistered destination.
Different redirect URI during exchange Mock token endpoint The code is not exchanged for tokens.
Missing PKCE challenge Mock authorization endpoint A client configured to require PKCE cannot start a downgraded flow.
Unsupported challenge method Mock authorization endpoint The request fails instead of silently changing to a weaker method.
Missing code verifier Mock token endpoint No token response is issued for a PKCE-bound code.
Incorrect code verifier Mock token endpoint Challenge comparison fails and no tokens are created.
Verifier encoded incorrectly Mock token endpoint Standard base64 padding or alphabet mistakes are detected rather than normalized unexpectedly.
Expired authorization code Mock token endpoint The application handles the token error and does not establish a session.
Authorization code replay after success Mock token endpoint The second exchange fails even if every other field is correct.
Code issued to another client Mock token endpoint Client binding prevents exchange by the application under test.
Provider denial response Application callback handler No token call occurs, temporary state is handled safely, and the user sees a non-sensitive message.
Malformed token response Application token-response parser No partial session is created and parsing errors do not disclose response secrets.
Token endpoint timeout Application exchange layer The failure is bounded, observable, and does not cause uncontrolled replay.

Test exact redirect URI matching

Redirect URI defects can be subtle. Test differences in scheme, hostname, port, path, trailing slash, and character case where relevant. Also test an added query string and an encoded path difference. The safe lab behavior is explicit registration and exact comparison, not prefix matching or substring matching.

Never redirect to a submitted URI before validating it. If the authorization request contains an unregistered destination, the mock should display or return a local error instead of redirecting the browser to that destination with error details. Otherwise, the test server itself becomes an open redirect.

Test state as a transaction, not only as a string

String equality is necessary but not sufficient. State should resolve to a live transaction belonging to the browser context that initiated the flow. Test a valid state copied from one browser session into another. Test a state from an old completed transaction. Test two simultaneous login attempts completed in the opposite order.

Define consumption behavior clearly. A successful callback should not leave the transaction reusable. For provider denial, malformed responses, and recoverable network failures, choose and document whether a retry is possible. The important property is that the behavior is intentional and cannot be exploited to attach an unexpected callback to a new session.

Test token endpoint failures without hiding them

OAuth token errors are machine-readable protocol responses, but production providers vary in how much diagnostic detail they return. The application should branch on the documented error field and HTTP outcome rather than matching a human-readable description. The user-facing message should remain generic, while internal telemetry records a safe failure category and correlation identifier.

Include transport failures, invalid content types, non-JSON responses, incomplete JSON, unexpected token types, and missing required fields. If refresh tokens are in scope, build a separate refresh suite covering rotation, expiry, invalid grants, and concurrent use according to the provider contract. Do not assume that every provider returns a refresh token or that refresh behavior is part of a basic authorization code grant.

Add a public HTTPS callback with Localtonet

Public OAuth HTTPS callback routed through Localtonet to a localhost callback service.
The public HTTPS endpoint forwards the authorization callback to the private local test application.

Move to public callback testing only after the local flow is reliable. This stage is useful when a real browser, remote test device, webhook-like provider redirect, or provider configuration requires a publicly reachable HTTPS callback. It also exposes assumptions about external scheme and host detection that a localhost-only test may not reveal.

An HTTP tunnel is the appropriate Localtonet tunnel family for a browser callback served by a local web application. HTTP and File Server tunnels can use a Random Sub Domain, Custom Sub Domain, or Custom Domain process type, and all three serve content at a public HTTPS address. Availability can vary by current product configuration or plan, so use only the options displayed in your dashboard. Exact custom-domain DNS instructions should be taken from the current documentation rather than guessed.

1

Install and run the Localtonet client

Install the Localtonet application for the operating system on the device that can reach the local callback service, then run it. Use the current download and installation instructions for that operating system because client packaging can change.

2

Authenticate or select the client device

Use the device-specific authentication token through the supported Localtonet workflow and select the device that will run the tunnel. Keep the token out of application configuration, repositories, terminal captures, browser recordings, and logs.

3

Select an available relay server

Choose a relay server or region from the values currently available in the dashboard. Do not hardcode a server code copied from another environment because available values can vary.

4

Create an HTTP tunnel to the callback service

Select the appropriate HTTP process type and configure the local IP address and port on which the application under test is listening. Target the callback application, not the mock authorization server, unless the test explicitly requires the mock itself to be public. Confirm that the Localtonet client device can reach the chosen local target.

5

Start the tunnel

Creating a tunnel does not make it active. Press Start and confirm that the selected device remains connected and the tunnel is running.

6

Use the assigned HTTPS callback URI

Copy the assigned public HTTPS address, append the application's real callback path, and register that exact URI in the mock authorization server. Configure the application to place the same public URI in the authorization request and token exchange where required. Then run the browser flow from the beginning so the transaction is created for the public callback.

The current configuration workflow is also available in our Localtonet HTTP tunnel documentation. Use the dashboard and documentation for current field names, client installation details, server choices, and option availability.

Verify the public route before running OAuth

Request a harmless health endpoint or a non-sensitive callback readiness page through the public HTTPS address. Confirm that the request reaches the intended local process. A generic application home page is not enough if a reverse proxy or route mapping sends the callback path elsewhere.

Next, initiate login from the application and inspect the authorization request. Its redirect_uri must now contain the public HTTPS scheme, public hostname, optional explicit port if applicable, and exact callback path. The mock registration must contain precisely the same value. Do not repair a mismatch by allowing wildcard paths or arbitrary hosts.

Account for external origin handling

Some applications construct absolute URLs from the incoming request, while others use an explicit external base URL. The correct choice depends on the framework and deployment model. The provider-agnostic assertion is simple: the application must generate the intended public HTTPS callback and must not trust arbitrary client-supplied host information.

If the application generates a localhost callback even when reached through the public URL, configure its documented external-origin or trusted-proxy behavior. Those settings are framework-specific, so there is no universal environment variable or header setting that can be safely copied into this guide. Restrict any proxy trust configuration to the deployment path being tested rather than trusting forwarded headers from every source.

A tunnel exposes the targeted service, not only the callback concept

Review every route served on the selected local IP and port. Disable development exception pages and debug endpoints, require authorization for unrelated routes, and avoid placing the mock administration interface on the tunneled listener. Stop or delete the tunnel when public testing is complete.

Safe logging, automation, and routine operation

Log outcomes, not credentials

OAuth troubleshooting needs correlation, but raw payload logging can turn a test system into a credential archive. Query strings may contain authorization codes and state. Form bodies may contain code verifiers, client secrets, refresh tokens, or codes. Token responses may contain access tokens and identity data. Redact at the structured logging layer before serialization rather than trying to clean an already-written log file.

Data Safe diagnostic approach Avoid
State Log a test correlation identifier or a short one-way fingerprint Full state values in access logs or error pages
Authorization code Log issued, accepted, expired, or replayed status with a record identifier The code itself or the complete callback URL
PKCE verifier Log whether it was present and whether challenge verification passed Raw verifier, derived challenge pairs, or token request bodies
Client secret Log the configured authentication method and pass or fail result Headers, form bodies, process environments, or fixture snapshots containing the secret
Access and refresh tokens Log token type, expected metadata, and safe test correlation Complete tokens, decoded personal claims, or reusable refresh credentials
Redirect URI Log the normalized test configuration when it contains no secrets Blindly logging the complete incoming callback URL and query string

Configure the web server and reverse proxy to avoid query-string logging on the callback route when possible. Test failure messages should identify categories such as state_mismatch, pkce_failed, redirect_mismatch, or code_replayed. They should not embed the rejected value.

Make each test independent

Generate a fresh transaction and authorization code for every case. Reset mock storage between tests or namespace records by a unique test run. A failure test that depends on a code created by a previous test is fragile and can conceal lifecycle bugs. Parallel test workers should not share a fixed state, verifier, code, or browser cookie jar.

Keep positive and negative fixtures explicit. A test named for a wrong verifier should modify only the verifier. If it also changes the redirect URI, the failure no longer proves which check worked. Where practical, have the mock expose an in-process audit record to the test runner, not a public debug endpoint. The audit can state which validation gate rejected the request without exposing secrets over HTTP.

Manage callback URL stability

A generated public hostname is convenient for temporary testing, but any change requires updating both the provider registration and application configuration. Where a stable callback is required and a supported process type is available in the dashboard, use an appropriate selected subdomain or custom domain workflow. Do not assume a particular option is included in every plan.

Treat callback configuration as an environment-specific value. Development, shared testing, staging, and production should not silently reuse one registration. A test suite should assert its expected callback before opening the browser so a stale hostname fails early rather than sending a code to an unintended endpoint.

End the test session cleanly

Stop the browser automation, remove synthetic sessions, stop the mock server, and stop the application listener if it is not otherwise needed. Then stop or delete the Localtonet tunnel. Because a tunnel remains available only while its selected device is connected and the tunnel is running, verify the tunnel status rather than assuming that closing a browser ended public access.

Remove temporary client registrations and public callback URIs from shared mock environments. Rotate any test credential that was accidentally printed or captured. If a real provider was used for final compatibility testing, revoke test grants and tokens according to that provider's supported process.

Troubleshooting common OAuth lab failures

The provider reports a redirect URI mismatch

Capture the redirect URI configured in the mock, the decoded redirect_uri parameter in the authorization request, and the value sent during the token exchange. Compare scheme, hostname, port, path, trailing slash, query, and encoding. Do not compare only what the browser address bar visually displays.

If public HTTPS is in use, make sure the application is not emitting its local http://127.0.0.1 address. Also confirm that the public callback was added as a distinct registered URI. Avoid solving the issue with wildcard redirects because that removes the exact property the lab is intended to test.

State fails only when several tabs are open

The application may store one global state value in the session and overwrite it whenever login starts. Use transaction-specific storage capable of representing concurrent attempts, or intentionally invalidate older attempts and test that behavior. Also check cookie scope, session rotation, and whether the public and local hostnames are creating separate browser cookie contexts.

PKCE works with a fixed verifier but fails with generated values

Inspect the generation and encoding operations without logging the verifier itself in normal output. Confirm the verifier uses permitted characters and length, the SHA-256 input is the verifier's ASCII bytes, and the challenge uses unpadded base64url. Standard base64 may introduce +, /, and =, which should not appear in an S256 challenge.

Also verify that the application retrieves the verifier from the same transaction identified by state. A valid verifier from another simultaneous login attempt must fail against the current authorization code.

The local callback works but the public callback returns an error

Test the public callback path with a harmless request and confirm it reaches the correct listener. Verify that the Localtonet client is connected and the tunnel was started, since creating the tunnel alone is not sufficient. Recheck the local IP and port from the tunnel configuration and make sure the service is reachable from the device running the Localtonet client.

If the service is reached but it rejects the request, investigate application host validation, secure-cookie behavior, external URL configuration, and trusted proxy rules using the application's own documentation. Do not disable host validation or globally trust forwarding headers merely to pass the test.

The callback succeeds but the token exchange fails

Compare the token request with the code record. Check the grant type, code, client identifier, redirect URI, verifier, client-authentication method, content type, and code expiry. A successful callback validates the browser transaction, but it does not prove that the server-to-server exchange is correctly constructed.

Ensure that a public client is not sending a fabricated secret and that a confidential client is using the authentication method expected by the mock. Inspect safe structured diagnostics from the mock rather than dumping request headers and bodies.

A replayed authorization code succeeds

The mock may be marking the code consumed after returning the token response or performing the check and update as separate unsynchronized operations. Consume the code atomically before issuing tokens. Add both a sequential replay test and a concurrent exchange test.

The application should also avoid retrying a token exchange blindly after an ambiguous network failure. A retry may reach a token endpoint that already consumed the code. Define bounded error handling and restart the authorization flow when the final exchange state cannot be established safely.

Browser history or server logs contain sensitive values

Authorization codes and state naturally appear in the callback query, so the callback handler should process them promptly and redirect to a clean post-login URL. Configure access logging to omit or redact query strings for that route. Never put access tokens or refresh tokens into a browser URL.

If sensitive test values were captured, remove the affected logs from normal distribution, invalidate reusable credentials, and correct logging before continuing. Synthetic values reduce impact, but safe handling should match the production design.

Frequently asked questions

Can PKCE replace the OAuth state parameter?

No. They protect different relationships. PKCE binds the authorization code to the client instance holding the verifier. State correlates the callback with an outstanding browser transaction and is commonly part of CSRF protection. A robust authorization code implementation tests both.

Does an OAuth public client need a client secret in the test lab?

A public client cannot safely protect a client secret, so the lab should not invent one merely to make the flow pass. Configure the mock for the real client type and require PKCE. A confidential server-side client can use its registered token-endpoint authentication method in addition to PKCE.

Must the redirect URI in the token exchange match the authorization request?

When the redirect URI was included in the authorization request, the exchange should use the same value as required by the flow. The mock should bind the code to that value and reject a mismatch. Keep the registered URI, authorization request, and token request consistent.

Do I need to expose the mock authorization server through Localtonet?

Usually not. If the browser and application can reach the mock locally, only the application callback may need a public HTTPS address. Exposing the mock adds attack surface and is unnecessary unless a remote participant in the test must reach it.

Why should the flow work locally before starting the tunnel?

A local baseline proves that transaction storage, state, PKCE, code issuance, callback handling, and token exchange are functional. If the public flow then fails, the investigation can focus on the external URL, routing, host handling, cookies, or proxy-aware application configuration.

Can I reuse one authorization code across several tests?

No. Each test should receive a fresh code bound to its own client, redirect URI, PKCE challenge, and lifecycle. A code that has been exchanged successfully must not work again. Reusing a fixed code prevents meaningful replay and concurrency testing.

Is an ID token always part of the authorization code flow?

No. An ID token is an OpenID Connect concept, not a required OAuth token response field. Add issuer, audience, signature, nonce, and claim validation tests only when the application is also implementing OpenID Connect.

Does creating a Localtonet tunnel make it immediately available?

No. Creating the configuration does not mean the tunnel is running. Start it with the Start button and keep the selected client device connected. Stop or delete the tunnel when the public callback test is finished.

Test your OAuth callback with Localtonet

Once the authorization code and PKCE flow passes locally, use a Localtonet HTTP tunnel to give the browser-facing callback a public HTTPS address without inbound router port forwarding. Keep the lab isolated, register the exact callback URI, run the negative test matrix, and stop the tunnel when testing is complete.

Get Started Free โ†’

Localtonet is a secure multi-protocol tunneling and proxy platform designed to expose localhost, devices, private services, and AI agents to the public internet supporting HTTP/HTTPS tunnels, TCP/UDP forwarding, mobile proxy infrastructure, file server publishing, latency-optimized game connectivity, and developer-ready AI agent endpoint exposure from a single unified control plane.

support