An OAuth2 securityScheme that passes validation can still produce a generated SDK that never actually authenticates. The cause is almost never a spec error — swagger-cli and redocly lint both catch missing required OAuth Flow fields cleanly. The real problem is what codegen tools don't do: neither openapi-generator's typescript-fetch nor typescript-axios templates implement the authorization-code, client-credentials, or any other OAuth2 flow. They generate a callback slot and leave the actual token exchange to you — and if you don't wire it up, requests silently go out with no Authorization header. Reproduced with real tools on 2026-09-21.
How do you reproduce the error?
Start with an authorizationCode flow missing the required tokenUrl:
components:
securitySchemes:
oauth2:
type: oauth2
flows:
authorizationCode:
authorizationUrl: https://auth.example.com/authorize
scopes:
read:messages: Read messages
# tokenUrl missing
swagger-cli validate v4.0.4 buries the real error in AJV's oneOf noise across all four security scheme types:
Swagger schema validation failed.
#/components/securitySchemes/oauth2 must have required property '$ref'
#/components/securitySchemes/oauth2 must NOT have additional properties
#/components/securitySchemes/oauth2/type must be equal to one of the allowed values
#/components/securitySchemes/oauth2/flows/authorizationCode must have required property 'tokenUrl'
#/components/securitySchemes/oauth2 must have required property 'openIdConnectUrl'
#/components/securitySchemes/oauth2 must match exactly one schema in oneOf
redocly lint v2.53.3 names it directly:
[3] api.yaml:19:9 at #/components/securitySchemes/oauth2/flows/authorizationCode
The field `tokenUrl` must be present on this level.
Add tokenUrl and both tools pass the spec clean. That's the easy half of the problem. The harder half shows up after codegen — with a fully valid spec.
What actually happens when you generate an SDK from a valid OAuth2 spec
Take a fully valid spec: an authorizationCode flow with authorizationUrl, tokenUrl, and scopes all present, securing a GET /messages operation. Running openapi-generator-cli generate -g typescript-fetch against it produces this in DefaultApi.ts:
if (this.configuration && this.configuration.accessToken) {
// oauth required
headerParameters["Authorization"] = await this.configuration.accessToken("oauth2", ["read:messages"]);
}
And typescript-axios (same spec, -g typescript-axios) produces the equivalent:
// authentication oauth2 required
// oauth required
await setOAuthToObject(localVarHeaderParameter, "oauth2", ["read:messages"], configuration)
Both generators read authorizationUrl, tokenUrl, and scopes from the spec only to know that a header is needed and which scope names to hand to your own callback — Configuration.accessToken. Neither one generates the redirect to authorizationUrl, the code-for-token exchange against tokenUrl, PKCE handling, or token refresh. If configuration.accessToken is never set, the if branch above is simply falsy: no error, no warning, the request goes out with no Authorization header at all.
Why does this happen?
Per the OpenAPI 3.1.1 specification's OAuth Flow Object, authorizationUrl is required for implicit and authorizationCode flows, tokenUrl is required for password, clientCredentials, and authorizationCode, and scopes is required for every flow — but these fields are declarative metadata for humans and docs tools, not instructions a code generator executes. An OAuth2 flow involves a redirect, user consent, a token endpoint round trip, and often PKCE or refresh-token rotation — none of which is expressible as a single typed method call the way an API key header or HTTP bearer token is. Generators treat apiKey and http schemes as "attach this value to this header," which is trivial to generate. OAuth2 is a multi-step protocol that has to run somewhere — usually a browser redirect or a backend token service — and no mainstream OpenAPI codegen tool ships that logic. This is spec-accurate, not a generator bug: OpenAPI documents the flow's endpoints, not its execution.
How do you fix it?
You can't make codegen implement OAuth2 for you, but you can make the gap visible and safe:
# Document the scheme fully — this doesn't change codegen behavior,
# but it's what a human or an SDK consumer reads to know what to build.
components:
securitySchemes:
oauth2:
type: oauth2
description: >
Clients must obtain an access token via the authorizationCode flow
and supply it through Configuration.accessToken. This SDK does not
perform the OAuth2 flow itself.
flows:
authorizationCode:
authorizationUrl: https://auth.example.com/authorize
tokenUrl: https://auth.example.com/token
scopes:
read:messages: Read messages
// Consumer code — required, not optional, despite compiling without it
const config = new Configuration({
accessToken: async () => await getTokenFromYourAuthLibrary(),
});
The fix isn't a YAML change — it's treating configuration.accessToken as a required wiring step in your onboarding docs and integration tests, since nothing in the generated types or the build forces it.
How different tools react
| Tool | Missing tokenUrl in spec |
Valid spec, accessToken never configured |
|---|---|---|
swagger-cli validate (AJV) |
Fails, buried in oneOf noise |
N/A — not a spec error |
redocly lint (semantic linter) |
Fails with exact rule and path | N/A — not a spec error |
openapi-generator (typescript-fetch, typescript-axios) |
N/A — generation still runs | Compiles clean; requests silently omit Authorization |
| Runtime | N/A | No error — server returns 401, easy to mistake for a server-side bug |
Same pattern for clientCredentials and password flows
The behavior is identical for the other flow types — clientCredentials and password only require tokenUrl and scopes (no authorizationUrl, since there's no user redirect), but the generated code still just calls configuration.accessToken(name, scopes) and expects you to supply a working token, this time typically from a machine-to-machine token exchange you implement yourself. Skipping authorizationUrl for these flows doesn't change how the generated client behaves; it only changes what a docs page or an OAuth2 client library built against the spec would need.
Sourced validates OAuth Flow Object completeness on every spec push and flags security schemes in its compatibility report, so a missing tokenUrl or an OAuth2 scheme with no working token wiring shows up before it reaches a customer's generated client. Create hosted docs from your repo or start a free report to see your security schemes checked against a real spec.
FAQ
Will a generated SDK ever implement the OAuth2 flow itself?
Not with openapi-generator's TypeScript templates as of 2026 — both typescript-fetch and typescript-axios delegate to a Configuration.accessToken callback you must supply. Some language-specific generators (server stubs, certain Java/Python clients) handle more of the flow, but treat that as generator-specific, not a guarantee.
Does an invalid OAuth2 securityScheme ever reach codegen without erroring?
No — openapi-generator runs its own validation pass before generating and will refuse a spec with a structurally broken security scheme. The gap this post covers is different: a fully valid scheme that codegen can't turn into a working flow, not an invalid one that slips through.
Why does swagger-cli produce so much noise for one missing field?
securitySchemes entries validate against a oneOf of four scheme types (apiKey, http, oauth2, openIdConnect) in the OpenAPI meta-schema. AJV reports every failed branch, not just the one you meant, which is why one missing tokenUrl produces six or more lines. redocly lint's semantic rules check the OAuth Flow Object directly and report only the real problem.
What's the safest way to test that OAuth2 auth actually works after generating an SDK?
Write an integration test that calls a real authenticated endpoint using the generated client with a real accessToken callback wired up, and confirm it fails (401) when that callback is omitted. A clean tsc build proves nothing about auth — the callback is optional at the type level.
Do API key or HTTP bearer schemes have the same problem?
No. apiKey and http (basic/bearer) schemes generate directly into a header, query param, or cookie because they're single-value credentials with no protocol to run — that's exactly what makes them straightforward to codegen and OAuth2 fundamentally different.
Should I avoid documenting OAuth2 in my OpenAPI spec since codegen can't implement it?
No — document it fully. The spec's OAuth Flow Object is still the source of truth your docs, your team, and any OAuth2-aware tooling (Postman, Insomnia, some API gateways) read to configure the flow correctly, even though your generated TypeScript SDK won't run it automatically.