OAuth2 / OIDC discovery playbook for authorized testing
CTF or authorized-lab work only: no real-world targets, credentials or findings. Original in GrantZero604/security-writeups on GitHub, CC BY 4.0.
TL;DR. Map an OAuth2 or OpenID Connect flow and its trust decisions before touching a single parameter. Most high-impact OAuth bugs (the ones that end in account takeover) come from weak
redirect_urivalidation, a missing or unverifiedstate, or a public client with no PKCE. Only the third is usually visible in the discovery document; the first two are runtime behaviors you have to confirm by testing. What discovery gives you is where to look.
When to use this
Section titled “When to use this”Use this playbook at the start of testing any application that signs users in through OAuth2 or OpenID Connect: read-only reconnaissance first, then methodical testing of the flow. Sections 1 to 3 map the provider and the flow without changing anything, sections 4 to 6 are the tests that map points you to, section 7 covers what sits just past discovery, and section 8 covers reporting.
Scope and conduct. Everything below is for systems you are authorized to test: a bug bounty program’s in-scope hosts, a client engagement with a signed contract, or your own lab. Use your own test accounts, never a real user’s. Keep request rates within the program’s limits. Never attempt code theft, token replay against production, or use of a discovered client secret — if you find one, report it, don’t exercise it.
1. Map the provider — metadata first
Section titled “1. Map the provider — metadata first”Fetch the discovery document and read it as a specification of the server’s own behavior. It will tell you more in one request than an hour of clicking:
curl -s "https://TARGET/.well-known/openid-configuration" -o oidc.json
# Some Entra ID and Keycloak deployments use a tenant- or realm-specific issuer:curl -s "https://TARGET/realms/REALM/.well-known/openid-configuration"curl -s "https://TARGET/.well-known/oauth-authorization-server"Each field maps to a test:
| Field | Why it matters |
|---|---|
issuer |
Must match the token’s iss; a mismatch is confused-deputy territory |
authorization_endpoint / token_endpoint |
Where authorization and code exchange happen |
jwks_uri |
Key set used for id_token signature validation |
end_session_endpoint |
Logout — check post_logout_redirect_uri handling |
registration_endpoint |
Dynamic client registration; open DCR means anyone can register a client |
grant_types_supported |
Is password (ROPC), implicit or client_credentials enabled? |
response_types_supported |
code versus token / id_token — implicit and hybrid flows |
code_challenge_methods_supported |
Is S256 present, absent, or is plain the only option? |
token_endpoint_auth_methods_supported |
none in this list means the server supports public clients; the per-client token_endpoint_auth_method is what makes a given client public |
scopes_supported / claims_supported |
Scope escalation and claim over-sharing |
request_parameter_supported |
JAR support — signed request objects |
frontchannel_logout_supported |
Front-channel and iframe logout surface |
If the metadata endpoint 404s, fall back to the application’s own login redirect —
/oauth2/authorize, /authorize, /connect/authorize — and its JavaScript. Those
become your source of truth.
2. Fingerprint the provider
Section titled “2. Fingerprint the provider”Correlate signals across the login page HTML and JS, redirect Location headers,
response headers, cookies, and the shape of well-known paths:
- Entra ID (Azure AD) — host
login.microsoftonline.com, paths/common|organizations|<tenant>/oauth2/v2.0/authorize, andx-ms-request-id/x-ms-ests-serverresponse headers. An unauthenticated authorize request setsbuid,esctx,fpcandstsservicecookie; theESTSAUTHfamily appears only after sign-in, so don’t expect it during passive discovery. Watch forclient_infoandclaimsparameters on the authorize URL. - Okta —
*.okta.com,/oauth2/default/v1/authorize,/oauth2/v1/token, cookiesokta-oauth-state,okta-oauth-nonce,DT, and anx-okta-request-idheader. - Keycloak —
/realms/<realm>/protocol/openid-connect/auth|token|userinfo, admin console at/admin/, cookiesAUTH_SESSION_ID,KEYCLOAK_IDENTITY,KC_RESTART. - Auth0 —
login.<tenant>or*.auth0.com,cdn.auth0.com,lock.js, cookiesauth0(session) anddid(device id), and aconnection=parameter on the authorize URL. Transaction state usually lives in web storage ascom.auth0.auth.<state>rather than in a cookie. - Duende IdentityServer — cookies
idsrv,idsrv.session, login at/Account/Login, endpoints/connect/authorize|token|userinfo; the error page names “Duende IdentityServer” (older builds: “IdentityServer4”). - Amazon Cognito — hosted UI at
*.auth.<region>.amazoncognito.com/oauth2/authorize|token,cognito-idp.<region>.amazonaws.com, JWKS athttps://cognito-idp.<region>.amazonaws.com/<pool>/.well-known/jwks.json, andidentity_provider/client_idparameters.
The fingerprint only tells you which defaults are likely. The metadata and the observed flow decide.
3. Classify the flow
Section titled “3. Classify the flow”authorization_code—/authorizereturns?code=, which the client exchanges at/token. The question that matters: is the exchange server-side, using a secret, or in-page from a public client?authorization_code+ PKCE —/authorizecarriescode_challengeandcode_challenge_method;/tokencarriescode_verifier.S256only is the strong configuration. A public client without PKCE is exposed to code interception.- implicit / hybrid —
response_type=tokenorid_token(orcode id_token) puts tokens in the URL fragment, exposing them to browser history, in-page JavaScript and anything else with DOM access. Fragments are never transmitted to the server, so they appear in neither server logs nor theRefererheader — which closes off some leak paths, but also means server-side logs will not show you that exposure happened. - password / ROPC — the client collects the password directly and posts
grant_type=password. Bad practice on its own; the impact depends on whether MFA and risk-based checks are bypassed along with it. client_credentials/refresh_token— machine-to-machine and long-lived sessions. Check whether refresh tokens rotate, and whether an old one still works after rotation.
Determine which grants are actually enabled by comparing the metadata against a normal login. Don’t brute-force the token endpoint to find out.
4. PKCE — presence and implications
Section titled “4. PKCE — presence and implications”code_challenge_methods_supported: ["S256"]is the good case — but verify the client actually sends it. Advertised support and real use are different things.["plain"], or the field missing entirely, is weak. On mobile and SPA clients an attacker who captures the redirect can redeem the authorization code.- PKCE is not a substitute for exact
redirect_urivalidation. Its relationship tostateis more nuanced than it is often taught: RFC 9700, the OAuth 2.0 Security Best Current Practice, holds that PKCE also provides CSRF protection, so a client may legitimately rely on it instead ofstate. Don’t assume a missingstateis deliberate, though — confirm the client is sending and checking something.
5. Common misconfigurations
Section titled “5. Common misconfigurations”Test one variable at a time. Changing two things and seeing a different result tells you nothing about which one mattered.
Redirect handling — one of the largest sources of account takeover:
- Does
redirect_urirequire an exact match? Try path append (/cb/../evil), a trailing slash, a case change, an explicit default port,httpversushttps, and an extra subdomain. - Open-redirect chaining: an allowed origin that itself contains a redirect.
- Parser confusion:
https://[email protected],https://allowed.com.evil.com,//evil.com,https:evil.com, backslashes,%2f, CRLF, and Unicode or dot normalization. - Parameter pollution — two
redirect_urivalues — andresponse_modeoverrides. - Fragment smuggling: injecting
#or?into the allowed URI.
State and nonce:
- Is
statepresent, per-session random, and actually verified on return? A static or missingstateenables login CSRF and account-linking attacks. Presence is not verification — change it and see whether the flow still completes. - Is
noncepresent forid_tokenflows and bound to the session? A replayable nonce means a stolenid_tokencan be accepted.
Response type and mode:
response_mode=querycan leakcodeortokenintoRefererheaders, proxy logs and analytics pipelines.response_mode=form_postauto-submitting to an unvalidated URI.
Token endpoint client authentication:
token_endpoint_auth_method=nonemeans a public client. Confirm no secret is embedded in the JavaScript bundle anyway — it happens.- A
client_secretin an SPA or mobile bundle, orclient_secret_postwhere a public client should be usingnoneplus PKCE.
Logout:
post_logout_redirect_uriandid_token_hint— is the redirect validated? An open redirect here chains into token theft or convincing phishing.
Account binding and scopes:
login_hint,prompt=noneanddomain_hintcan enable account or tenant enumeration.- Over-broad
scopeacceptance, consent bypass, and account linking on an unverified email address. That last one leads to pre-account-takeover: an attacker registers a local account against an email they don’t control, and when the real owner later signs in through a federated provider, the service links that login to the attacker’s pre-existing account rather than creating a new one.
6. Server-side code exchange
Section titled “6. Server-side code exchange”- Where is the code exchanged — a backend
/callback(good) or in the browser (exposed)? - Is the code single-use, short-lived, and bound to both
client_idandredirect_uri? Try redeeming it twice. - Is the session that started the flow the same one that redeems it? If not, that’s login CSRF.
- Do
access_tokenorrefresh_tokenvalues end up in URLs, logs, or cookies scoped more broadly than they need to be? - For OIDC, is the
id_tokenfully validated server-side —iss,aud,exp,nonceand signature? Partial validation is common and is worth testing individually.
7. Adjacent to discovery — worth a look once the flow is mapped
Section titled “7. Adjacent to discovery — worth a look once the flow is mapped”This playbook stops at mapping the flow. Three areas sit just past that boundary and are easy to reach from what discovery already told you:
- IdP mix-up (multi-IdP deployments). Where an application supports several
identity providers, an attacker may be able to get a code issued by one and redeemed
against another. RFC 9207 adds an
issparameter to the authorization response specifically to defeat this — check whether the AS sends it and whether the client validates it. Relevant any time you noted more than oneissuer. - SSRF via
request_uri(JAR by reference). If the server advertises support for request objects by reference, it will fetch a URL you supply. Classic server-side request forgery, reachable from therequest_parameter_supportedandrequest_uri_parameter_supportedmetadata fields. Pushed Authorization Requests (PAR,pushed_authorization_request_endpoint) are worth checking in the same pass. id_tokensignature attacks. Section 6 says validate the signature; the ways that validation breaks arealg: none, RS256→HS256 key confusion (signing with the public key as an HMAC secret), andkidorjkuinjection pointing at a key set you control. Thejwks_uriyou recorded in step 1 is the reference point for all three.
8. Reporting
Section titled “8. Reporting”- Lead with impact — account takeover, cross-tenant access, privilege gain — not with the parameter name. A triager reads the first sentence and decides how much attention the rest gets.
- Show the exact request and response, and name the boundary that broke. Redact real tokens.
- Score the impact you actually demonstrated, and state the theoretical maximum separately and explicitly. Conflating the two is the fastest way to lose a triager’s trust.
grantzero · OAuth2 / OIDC Discovery Playbook