Resource library

QA How-To

Testing OAuth 2.0 authorization code flow (2026)

Learn testing OAuth 2.0 authorization code flow with PKCE, state, redirects, token exchange, negative security cases, and practical CI-ready Node.js tests.

22 min read | 3,523 words

TL;DR

Testing OAuth 2.0 authorization code flow requires more than confirming login. Validate the authorization request, transaction-bound state and PKCE S256 values, exact callback handling, one-time code exchange, token claims or introspection, scope enforcement, replay rejection, and secure browser behavior across client, authorization server, and resource server.

Key Takeaways

  • Test the browser redirect, callback validation, back-channel code exchange, and resource access as separate trust boundaries.
  • Use transaction-specific PKCE with S256, and verify wrong, missing, reused, and downgraded verifier cases.
  • Require exact registered redirect URI behavior and reject open redirect or issuer mix-up paths.
  • Treat authorization codes as short-lived, single-use values bound to client and redirect context.
  • Verify state, issuer, scopes, consent, and token audience according to the client's OAuth or OpenID Connect profile.
  • Automate provider-independent protocol cases locally, then keep a small real-provider browser suite in an isolated tenant.

Testing OAuth 2.0 authorization code flow means validating a multi-party security protocol, not merely checking that a sign-in page appears. A complete suite follows the browser redirect, authorization response, client callback, back-channel code exchange, and protected resource call while proving that PKCE, transaction binding, redirect validation, code replay prevention, scopes, and token handling fail safely.

The 2026 baseline should incorporate OAuth 2.0 Security Best Current Practice, RFC 9700. Public clients must use PKCE, confidential clients should also use it, authorization servers must support PKCE, and S256 is the appropriate current challenge method. OAuth grants delegated authorization. If the application also authenticates the user, test the OpenID Connect layer, including ID Token validation and nonce, separately.

This guide gives protocol-level tests that can run without automating a real user's password and a small browser strategy for a staging identity provider. Use the API error handling and negative testing guide for consistent error assertions and the API test data management guide for isolated clients, users, and grants.

TL;DR

Boundary Positive proof Critical negative proof
Authorization request Correct endpoint, client, redirect, scope, state, PKCE S256 No secret or verifier in browser URL
Authorization response Code and transaction value accepted once Wrong state, wrong issuer, error response, injected code rejected
Token request Code exchanged over TLS with exact redirect and verifier Wrong verifier, reused code, wrong client, redirect mismatch
Token response Expected type, scope, lifetime fields, and profile validation Malformed response or excessive scope rejected
Resource server Token accepted only for intended audience and scopes Missing, expired, wrong-audience, or under-scoped token rejected
Browser client Safe redirect and callback cleanup Open redirect, history leakage, session mix-up prevented

Do not log authorization codes, client secrets, access tokens, refresh tokens, ID Tokens, or PKCE verifiers. Test reports should use generated transaction IDs and redacted fingerprints.

1. Testing OAuth 2.0 authorization code flow as a sequence

The flow involves a resource owner, a user agent, an OAuth client, an authorization server, and a resource server. The client creates a transaction, stores a state binding and PKCE verifier, then redirects the browser to the authorization endpoint with response_type=code, client ID, registered redirect URI, requested scope, state, code challenge, and code_challenge_method=S256.

After authentication and consent, the authorization server redirects the browser to the client's callback with a short-lived code and the returned transaction value. The client validates the response, then exchanges the code directly at the token endpoint using form encoding. The browser must not perform the token exchange. A confidential client authenticates according to its registered method; a public client has no client secret. The token is then used at the resource server.

Model each boundary independently:

  1. Client creates unpredictable, one-time transaction material.
  2. Authorization server validates the request and user decision.
  3. Client validates the callback before exchanging anything.
  4. Token endpoint validates code, client, redirect context, PKCE, and code lifetime.
  5. Client validates the token response for its protocol profile.
  6. Resource server validates access token authority and permissions.

A browser happy path can pass while any one of those controls is broken. For example, an application can land on its dashboard even if state is ignored, code replay succeeds, or the resource server accepts a token for another audience. Keep explicit assertions at the party that owns each rule.

2. Use the 2026 OAuth security baseline

RFC 6749 defines the framework, while RFC 9700 updates the security baseline from operational experience. Test against the deployed profile and current best practice, not an old tutorial that sends secrets from a single-page application or uses the implicit grant.

Control Expected 2026 behavior Test signal
PKCE Public clients must use it, confidential clients should use it S256 challenge is transaction-specific and enforced
Redirect matching Exact registered URI matching, with the specified native localhost exception Look-alike, subpath, query, and open redirect variants fail
Authorization code Short-lived, single-use, bound to client and redirect context Replay and cross-client exchange fail
Browser tokens Authorization response uses code, not an access token in URL No access token in query or fragment
Issuer binding Client identifies the expected authorization server Wrong iss or mixed endpoints fail where applicable
Sender constraint Consider mTLS or DPoP for profiles that require it Stolen token cannot be replayed by another sender

PKCE is not a substitute for every client control. Keep state when the client uses it for CSRF binding or application context, and test it as a one-time value bound to the initiating browser session. If an OpenID Connect client uses nonce, validate it in the ID Token according to that profile. Do not assume OAuth access tokens prove user authentication.

TLS is required for network endpoints outside narrowly defined native loopback cases. Never weaken certificate validation in an end-to-end test and call the flow secure. Use a trusted test certificate and the same issuer and endpoint discovery path as production.

Test the controls as a system. A client can generate excellent PKCE values, but security still fails if the token endpoint ignores the verifier.

3. Verify metadata, client registration, and endpoint separation

Start with static and discovery configuration. Assert the configured issuer exactly matches the trusted issuer, authorization and token endpoints use HTTPS, the redirect URI is registered exactly, and the client type matches credential handling. If OAuth Authorization Server Metadata is used, fetch it from the issuer-derived location and validate its issuer and endpoints before use.

Check code_challenge_methods_supported when published and require S256 for this client. Verify supported response types include code, and inspect token endpoint authentication methods for a confidential client. Do not dynamically trust an arbitrary metadata URL supplied by a user or authorization response.

Separate endpoint behaviors. The browser navigates to the authorization endpoint. The backend sends a form-encoded POST to the token endpoint. A resource API receives the access token. Confusing these roles creates defects such as sending a client secret in a browser URL, posting user credentials from automation to an authorization endpoint, or treating an access token as an ID Token.

Registration tests should cover redirect URIs that differ by scheme, host case rules, port, path, trailing slash, query, encoded characters, fragments, and attacker-controlled suffixes. Expected matching is exact except the standards-defined native loopback port handling. A callback such as https://client.example.test/callback.attacker must not match https://client.example.test/callback.

If several authorization servers are supported, test mix-up defenses. Endpoint selection, issuer validation, and callback transaction state must keep one provider's code away from another provider's token endpoint. RFC 9207 defines an authorization response issuer parameter that some profiles use for this purpose.

4. Generate and test state plus PKCE S256 correctly

A PKCE verifier is transaction-specific, high entropy, 43 to 128 permitted characters, and retained by the client. With S256, the challenge is the unpadded base64url encoding of the SHA-256 digest of the ASCII verifier. The verifier goes only to the token endpoint, never in the authorization request.

The following Node.js module uses current platform cryptography and URL APIs. Save it as oauth-flow.test.mjs; later code blocks extend the same file.

import http from 'node:http';
import test from 'node:test';
import assert from 'node:assert/strict';
import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';

function randomBase64Url(byteLength) {
  return randomBytes(byteLength).toString('base64url');
}

function pkceChallenge(verifier) {
  return createHash('sha256').update(verifier, 'ascii').digest('base64url');
}

function createTransaction() {
  const verifier = randomBase64Url(32);
  return {
    state: randomBase64Url(24),
    verifier,
    challenge: pkceChallenge(verifier),
    used: false
  };
}

function buildAuthorizationUrl({ issuer, clientId, redirectUri, scope, transaction }) {
  const url = new URL('/authorize', issuer);
  url.search = new URLSearchParams({
    response_type: 'code',
    client_id: clientId,
    redirect_uri: redirectUri,
    scope,
    state: transaction.state,
    code_challenge: transaction.challenge,
    code_challenge_method: 'S256'
  });
  return url;
}

function sameSecret(left, right) {
  const a = Buffer.from(left);
  const b = Buffer.from(right);
  return a.length === b.length && timingSafeEqual(a, b);
}

Test the RFC 7636 S256 example and the browser-facing request:

test('creates the RFC 7636 S256 challenge', () => {
  const verifier = 'dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk';
  assert.equal(pkceChallenge(verifier), 'E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM');
});

test('authorization URL contains challenge but no verifier or secret', () => {
  const transaction = createTransaction();
  const url = buildAuthorizationUrl({
    issuer: 'https://issuer.example.test',
    clientId: 'qa-client',
    redirectUri: 'https://client.example.test/callback',
    scope: 'orders:read',
    transaction
  });

  assert.equal(url.searchParams.get('response_type'), 'code');
  assert.equal(url.searchParams.get('code_challenge_method'), 'S256');
  assert.equal(url.searchParams.get('code_challenge'), transaction.challenge);
  assert.equal(url.searchParams.has('code_verifier'), false);
  assert.equal(url.searchParams.has('client_secret'), false);
  assert.notEqual(url.searchParams.get('state'), transaction.verifier);
});

Also test that two transactions never reuse state, verifier, or challenge. Do not print those values on failure. A deterministic RFC vector tests encoding correctness; random generation tests should assert format, length, uniqueness over a small sample, and use of an approved cryptographic source without inventing a probabilistic security benchmark.

5. Test the authorization response and callback transaction

The callback must handle success and error responses before exchanging a code. Verify the scheme, host, and path are the registered callback route. Parse query or form-post parameters according to the configured response mode. Reject missing, mismatched, replayed, or session-unbound state. Where the profile uses an authorization response issuer, compare it with the expected issuer.

Add this minimal callback validator to the runnable file:

function consumeCallback(callbackUrl, transaction, expectedIssuer) {
  if (transaction.used) throw new Error('transaction already used');
  const callback = new URL(callbackUrl);
  const returnedState = callback.searchParams.get('state') ?? '';
  if (!sameSecret(returnedState, transaction.state)) {
    throw new Error('state mismatch');
  }
  const returnedIssuer = callback.searchParams.get('iss');
  if (returnedIssuer && returnedIssuer !== expectedIssuer) {
    throw new Error('issuer mismatch');
  }

  const error = callback.searchParams.get('error');
  if (error) {
    transaction.used = true;
    return { error, description: callback.searchParams.get('error_description') };
  }

  const code = callback.searchParams.get('code');
  if (!code) throw new Error('authorization code missing');
  transaction.used = true;
  return { code };
}

test('accepts one callback and rejects replay', () => {
  const transaction = createTransaction();
  const issuer = 'https://issuer.example.test';
  const callback = new URL('https://client.example.test/callback');
  callback.searchParams.set('code', 'test-code');
  callback.searchParams.set('state', transaction.state);
  callback.searchParams.set('iss', issuer);

  assert.deepEqual(consumeCallback(callback, transaction, issuer), { code: 'test-code' });
  assert.throws(() => consumeCallback(callback, transaction, issuer), /already used/);
});

test('rejects a callback from another browser transaction', () => {
  const transaction = createTransaction();
  const callback = new URL('https://client.example.test/callback');
  callback.searchParams.set('code', 'injected-code');
  callback.searchParams.set('state', 'attacker-state');
  assert.throws(
    () => consumeCallback(callback, transaction, 'https://issuer.example.test'),
    /state mismatch/
  );
});

In a real client, bind the stored transaction to the initiating browser session and expire it. Consume it after the first valid callback, even if the subsequent token exchange fails, unless the client has a carefully designed recovery rule. Never redirect to a URL taken directly from untrusted state. Store an internal destination identifier or validate destinations against a strict allowlist to prevent open redirects.

Cover access_denied, malformed error combinations, unknown parameters, duplicated parameters, and very long values. The client should show a safe user outcome without reflecting untrusted descriptions as executable markup.

6. Test the back-channel code exchange

The token request is an HTTPS POST with application/x-www-form-urlencoded data. It includes grant_type=authorization_code, code, redirect URI when required by the original request, client ID according to the profile, and PKCE verifier. Confidential clients authenticate using their registered token endpoint method. Never put a client secret in a public client or browser bundle.

Add this exchange function to the same module:

async function exchangeCode({ tokenEndpoint, clientId, redirectUri, code, verifier }) {
  const body = new URLSearchParams({
    grant_type: 'authorization_code',
    client_id: clientId,
    redirect_uri: redirectUri,
    code,
    code_verifier: verifier
  });
  const response = await fetch(tokenEndpoint, {
    method: 'POST',
    headers: { 'content-type': 'application/x-www-form-urlencoded' },
    body
  });
  const payload = await response.json();
  if (!response.ok) {
    throw new Error(`token exchange failed: ${payload.error ?? response.status}`);
  }
  return payload;
}

This self-contained test uses a local token endpoint to verify the actual request encoding without exposing a real secret:

test('exchanges code with the matching PKCE verifier', async (t) => {
  const transaction = createTransaction();
  const server = http.createServer(async (request, response) => {
    let raw = '';
    for await (const chunk of request) raw += chunk;
    const form = new URLSearchParams(raw);
    assert.equal(request.method, 'POST');
    assert.match(request.headers['content-type'], /^application\/x-www-form-urlencoded/);
    assert.equal(form.get('grant_type'), 'authorization_code');
    assert.equal(form.get('code'), 'single-use-code');
    assert.equal(pkceChallenge(form.get('code_verifier')), transaction.challenge);
    response.writeHead(200, { 'content-type': 'application/json' });
    response.end(JSON.stringify({
      access_token: 'redacted-test-token',
      token_type: 'Bearer',
      expires_in: 300,
      scope: 'orders:read'
    }));
  });

  await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
  t.after(() => new Promise((resolve) => server.close(resolve)));
  const { port } = server.address();

  const token = await exchangeCode({
    tokenEndpoint: `http://127.0.0.1:${port}/token`,
    clientId: 'qa-client',
    redirectUri: 'https://client.example.test/callback',
    code: 'single-use-code',
    verifier: transaction.verifier
  });
  assert.equal(token.token_type, 'Bearer');
  assert.equal(token.scope, 'orders:read');
});

The local endpoint is a protocol fixture, not a substitute for testing the real provider. Against the authorization server, verify wrong verifier, omitted verifier, wrong redirect URI, wrong client, expired code, and second exchange all fail with the defined OAuth error, typically invalid_grant where specified. Confirm a token is not issued or left active after code-reuse detection according to provider policy.

7. Build a security-focused negative matrix

Negative cases should change one security binding at a time so the rejecting party is clear. Keep codes and transactions fresh unless replay is the scenario. A reused code combined with a wrong verifier gives an ambiguous result because either check can reject first.

Attack or defect Mutation Expected control
CSRF or login injection Callback has another transaction's state Client rejects before token call
Code interception Attacker exchanges code with wrong verifier Token endpoint rejects
PKCE downgrade Verifier appears for a code issued without challenge Authorization server rejects per BCP
Code replay Exchange same code twice Second exchange fails
Redirect manipulation Add path, query, host suffix, or open redirect Authorization request is rejected
Client mix-up Exchange code under another client Token endpoint rejects
Issuer mix-up Callback issuer differs from transaction Client rejects
Scope escalation Request or token contains unapproved scope Consent or server policy constrains it

Also test duplicated request parameters because parsers can choose first or last values differently. The authorization server should handle malformed, missing, and repeated parameters according to its profile without redirecting to an untrusted URI. Error redirects must not be sent to an invalid redirect URI, because that could leak error and transaction data.

Check browser leakage. The authorization request may appear in history, but it must not contain the verifier or secret. The callback briefly contains a code, so the client should process it promptly and navigate to a clean URL. Callback pages should avoid third-party resources that could receive the URL in a referrer. Verify security headers and log redaction.

Test open redirectors on both client and authorization server domains. A validated redirect URI that immediately forwards to an arbitrary destination can defeat exact matching.

8. Validate scopes, consent, accounts, sessions, and tokens

Test the smallest requested scope, multiple scopes, declined optional scope, unsupported scope, and an attempted escalation. Compare the granted scope in the token response or provider-specific introspection with what the resource server enforces. A client must not assume every requested scope was granted.

Consent cases depend on provider policy. Cover first consent, remembered consent, denial, changed scopes, revoked grant, and an administrator-controlled restriction. Verify the user-facing text identifies the correct client and permissions without automating around genuine consent protections. Use dedicated test clients and users in an isolated tenant.

Account and session cases include no authorization-server session, an existing session, multiple eligible accounts, forced reauthentication where supported, logged-out client with logged-in provider, and client session switching. Ensure one browser's transaction cannot complete in another browser unless the product explicitly supports a handoff protocol.

For opaque access tokens, validate through the resource server or introspection according to the architecture. For JWT access tokens, the resource server must validate signature, issuer, audience, time claims, and permissions using the correct profile. The client should not invent authorization by merely decoding a JWT.

OpenID Connect adds an ID Token and identity-specific checks: signature, issuer, audience, authorized party where applicable, expiry, nonce, and other profile claims. An access token is for a resource server; an ID Token communicates authentication to the client. Keep the test oracles separate so a token-type confusion defect is visible.

9. Test refresh, revocation, resource access, and logout boundaries

The authorization code flow is only the start of a session lifecycle. If a refresh token is issued, test its scope, client binding, storage, rotation, expiry, reuse detection, and revocation according to provider policy. Public clients require stronger protection because they cannot keep a client secret. Do not assume every provider issues refresh tokens or uses the same rotation behavior.

Call a protected resource with the access token and assert the intended audience and scope succeed. Then try missing token, malformed scheme, expired token, revoked token, wrong audience, wrong issuer, and insufficient scope. The resource server should return its documented OAuth error without revealing token contents. This is where authorization is actually enforced.

Revoking consent or the grant should affect refresh and resource behavior according to documented propagation timing. Use bounded polling if propagation is asynchronous. A test should not sleep for an arbitrary period and then pass without showing when revocation became effective.

Logout is not defined by core OAuth. Client logout, authorization-server session logout, and OpenID Connect relying-party initiated logout are different operations. Test only the mechanisms the product implements. Signing out of the client may leave the provider session active, which can cause an immediate silent or low-friction sign-in next time. That can be correct but must match user expectations and policy.

After logout, verify local tokens and transaction material are removed from the intended storage, browser navigation is safe, and back-button behavior does not expose authenticated content. Do not print storage contents in reports.

10. Automate browser flows without brittle credential scripts

Use browser automation for the user-visible redirect, consent, callback, and final application state. Keep protocol mutations in API and component tests, where they are faster and safer. A real provider may change login markup, add risk challenges, require MFA, or block automation, so use a supported test tenant and provider-approved strategy. Never attempt to bypass anti-bot or MFA protections.

Prefer a dedicated test user and prearranged authentication state only when it still exercises the authorization transaction under test. If you seed a provider session, clearly state that credential-entry UI is out of scope while redirect, consent, callback, and code exchange remain in scope. Maintain at least one controlled test for a fresh session if the provider supports it.

Capture navigation URLs only after redacting code and state. Intercepting the callback can help assert parameter presence, but do not reuse the captured code outside its intended transaction. For popups, use event-based browser APIs rather than fixed waits. The Playwright popup and new tab handling guide shows stable page coordination patterns.

High-value browser assertions include the trusted authorization-server origin, correct client name and requested permissions, denial behavior, return to the exact callback origin, removal of sensitive query values after processing, final signed-in or connected state, and safe handling of the back button.

Do not make the full security suite depend on the provider UI. A local authorization-server fixture or provider sandbox can exercise callback and exchange permutations, while a small real-provider smoke test catches registration, issuer, cookie, TLS, and integration drift.

11. Scale testing OAuth 2.0 authorization code flow in CI

Run deterministic client unit tests on every change: PKCE vectors, transaction uniqueness, authorization URL construction, callback validation, error mapping, token request encoding, and log redaction. Component tests should use a local authorization-server fixture to enforce exact redirect, code lifetime, one-time use, client binding, and wrong-verifier rejection.

Integration tests should use an isolated provider tenant with dedicated clients, exact registered callbacks, least-privilege users, and resettable grants. Keep one browser happy path, denial, changed scope, wrong state injection at the client, and resource authorization path. Schedule disruptive cases such as revocation propagation, key rotation, provider outage, and concurrent account sessions.

Secrets belong in the CI secret store and should be available only to jobs that need them. Public clients must not be given a secret just for testing. Restrict test redirect URIs to controlled HTTPS hosts, except legitimate native loopback profiles. Delete obsolete clients and rotate compromised test credentials.

Artifacts should show step, issuer, client identifier, redirect URI, requested and granted scope, redacted transaction fingerprint, HTTP status, OAuth error, and correlation ID. They must not contain raw codes or tokens. Configure HTTP tracing to redact form fields and authorization headers before enabling it.

Use unique browser sessions and transactions per parallel worker. Never share a code or PKCE verifier. Clean up local sessions, provider grants where appropriate, and created resource data. An API automation framework in Python or another established stack can host provider-independent tests, but keep protocol values generated through cryptographic platform APIs.

Interview Questions and Answers

Q: What are the main checkpoints in an authorization code flow test?

I validate the authorization request, browser redirect, callback transaction, back-channel token request, token response profile, and protected resource. At each checkpoint I test both the successful binding and one controlled mismatch such as wrong state, verifier, redirect, client, issuer, audience, or scope.

Q: Why is PKCE necessary?

PKCE binds the authorization request to the token exchange using a transaction-specific verifier. An intercepted code is insufficient without that verifier. The token endpoint must enforce the original S256 challenge, so client generation and server enforcement both need tests.

Q: What is the difference between state and nonce?

state binds an OAuth request and callback, commonly for CSRF protection and client context. OpenID Connect nonce binds the authentication request to the ID Token and helps prevent replay or code injection in that profile. They are not interchangeable data fields, and PKCE has its own code-binding role.

Q: How do you test redirect URI security?

I register one exact URI, then vary scheme, host, port, path, slash, query, encoding, and attacker suffix one at a time. Invalid values must be rejected without redirecting sensitive data. I also test that the valid callback is not an open redirector.

Q: How do you test authorization code replay?

I obtain one fresh code, exchange it successfully, then submit the same code again under otherwise identical parameters. The second request must fail, and I verify any provider-specific token revocation behavior associated with reuse detection.

Q: Can OAuth be tested entirely through APIs?

Protocol components can be tested deeply through fixtures and direct token endpoint calls, but the authorization endpoint is a user-agent redirect flow. I keep a small browser suite for origin, consent, callback, cookies, and navigation while testing most security mutations below the UI.

Q: What should never appear in OAuth test logs?

Raw authorization codes, access tokens, refresh tokens, ID Tokens, client secrets, PKCE verifiers, and sensitive state contents should be redacted. I log correlation IDs, safe client metadata, scopes, step names, statuses, and non-secret fingerprints instead.

Common Mistakes

  • Treating a successful login screen as proof that the OAuth protocol is secure.
  • Using the implicit grant or placing access tokens in browser redirect URLs.
  • Omitting PKCE, using plain, reusing a verifier, or testing only client generation without server enforcement.
  • Putting the PKCE verifier or client secret in the authorization URL.
  • Accepting redirect URI prefixes, wildcard-like subpaths, or open redirectors.
  • Reusing state or failing to bind it to the initiating browser transaction.
  • Exchanging a code before validating callback state and issuer context.
  • Assuming an access token authenticates a user instead of testing OpenID Connect where identity is needed.
  • Decoding a JWT without validating signature, issuer, audience, time, and profile rules.
  • Retrying an authorization code exchange after an ambiguous failure without considering single-use behavior.
  • Logging codes, tokens, secrets, or verifiers in CI traces and screenshots.
  • Building every negative test through a fragile provider login UI.

Conclusion

Testing OAuth 2.0 authorization code flow is a transaction-binding exercise across browser and back-channel boundaries. Use PKCE S256, exact redirect validation, one-time state and codes, issuer-aware callback handling, secure token exchange, and resource-level scope enforcement. Test every control where it is enforced, not only where it is generated.

Begin with a deterministic local suite for URL construction, callback rejection, PKCE verification, and code replay. Then add one isolated real-provider browser path and one protected-resource check. This layered design provides security depth without making every regression depend on external login markup.

Interview Questions and Answers

Walk me through your OAuth authorization code flow test strategy.

I split the flow into authorization request, authorization response, token exchange, token validation, and resource access. I automate each binding positively and negatively, especially state, PKCE, redirect URI, client, issuer, code lifetime, audience, and scope. Most protocol tests use a deterministic fixture, with a small isolated browser suite against the real provider.

How do PKCE and state differ?

PKCE proves that the party exchanging the code holds transaction-specific secret material associated with the authorization request. State binds the callback to client-side browser transaction context and is commonly used for CSRF protection or application context. I test both according to the client's profile and never put the verifier into the browser URL.

What would you test at the token endpoint?

I test form encoding, registered client authentication, exact redirect context, valid PKCE, code lifetime, and one-time use. Negative cases change one binding at a time: wrong verifier, client, redirect, expired code, or replay. I also validate stable OAuth errors and confirm no token is issued on failure.

How do you test OAuth securely in CI?

I use isolated clients and users, least-privilege scopes, exact test callbacks, secret-store injection, and strict artifact redaction. Transactions are unique per worker. Provider-independent cases run locally, while a small provider suite covers registration, TLS, cookies, consent, callback, and resource access.

Why is exact redirect URI matching important?

Loose matching can send an authorization code to an attacker-controlled path, host, or open redirector. I mutate each URI component and require invalid requests to fail without redirecting sensitive data. I separately verify the legitimate callback cannot forward to arbitrary destinations.

How do you test token audience and scope?

I call the intended resource with the correct token, then try a token for another audience and one missing the required scope. The resource server must reject them according to its profile. I compare granted scopes with requested scopes instead of assuming consent granted everything.

What is a common mistake when testing OpenID Connect on top of OAuth?

Teams often treat any JWT or access token as proof of identity. I validate the ID Token using OpenID Connect rules, including signature, issuer, audience, time, and nonce, and keep access-token authorization checks at the resource server. Token types must not be interchangeable.

Frequently Asked Questions

How do you test OAuth 2.0 authorization code flow?

Validate the authorization URL, browser redirect, state and issuer callback checks, PKCE-bound token exchange, token response, and protected resource. Add negative cases for wrong values, expiry, replay, redirect manipulation, scope, and authorization.

Is PKCE required for authorization code flow in 2026?

OAuth 2.0 Security Best Current Practice requires public clients to use PKCE and recommends it for confidential clients, while authorization servers must support it. New tests should use and enforce the S256 challenge method.

What PKCE cases should I automate?

Test a valid S256 verifier, wrong verifier, missing verifier, reused verifier across transactions, omitted challenge, downgrade behavior, malformed length or characters, and code replay. Also verify the verifier never appears in the browser authorization request.

How do I test the OAuth state parameter?

Generate it per transaction, bind it to the initiating browser session, and accept it once. Test missing, mismatched, replayed, duplicated, expired, and cross-session values, and ensure untrusted state cannot create an open redirect.

Should OAuth login tests automate passwords and MFA?

Only use provider-approved automation in an isolated test tenant. Prefer a dedicated test session when credential entry is not the behavior under test, keep a small fresh-session test where supported, and never bypass anti-bot or MFA controls.

How do I test authorization code replay?

Exchange one fresh code successfully and submit it a second time with otherwise identical valid parameters. The second exchange must fail, and the test should verify any documented revocation or audit behavior triggered by reuse.

What is the difference between OAuth and OpenID Connect testing?

OAuth tests delegated access and access-token enforcement. OpenID Connect adds user authentication through an ID Token and requires profile checks such as issuer, audience, signature, expiry, and nonce, so those oracles should be tested separately.

Related Guides