Skip to content

fix: OAuth flow validation contradicts the specification - #10

Merged
f3l1x merged 5 commits into
contributte:masterfrom
AdamFiser:fix/oauth-flow-validation
Aug 1, 2026
Merged

fix: OAuth flow validation contradicts the specification#10
f3l1x merged 5 commits into
contributte:masterfrom
AdamFiser:fix/oauth-flow-validation

Conversation

@AdamFiser

Copy link
Copy Markdown
Contributor

Problem

The library rejects OAuth security schemes that the specification permits — including the official OAuth Flow Object Example from the specification itself:

TypeError: OAuthFlow::__construct(): Argument #2 ($tokenUrl) must be of type string, null given

Three defects cause this. All three contradict tables that are identical in OAS 3.0.4 and 3.1.1, so 3.0 documents are affected too.

1. OAuthFlow treated every field as mandatory. Only scopes is REQUIRED; for the URLs the "Applies To" column narrows the rest:

Field Required? Applies to
authorizationUrl REQUIRED implicit, authorizationCode only
tokenUrl REQUIRED password, clientCredentials, authorizationCode only
refreshUrl not required any oauth2
scopes REQUIRED any oauth2 — "The map MAY be empty"

All four were declared string and read unconditionally, so an implicit flow — which has no token endpoint by definition — could not be represented at all.

2. oauth2 required all four flow types at once. The OAuth Flows Object marks no field as REQUIRED, yet a scheme supporting only authorizationCode was rejected with Attribute "flows" is missing required key "implicit".

3. bearerFormat was required for http/bearer schemes. The specification calls it a hint "primarily for documentation purposes". It is not REQUIRED, yet omitting it threw.

Changes

  • OAuthFlow — the three URL fields become nullable and toArray() emits only what is set. scopes stays mandatory, read unguarded like the required fields of Info, Server and Tag.
  • SecurityScheme::setFlows() — accepts any non-empty subset of known flow types and validates each flow's URLs against its own type: implicit needs authorizationUrl, password and clientCredentials need tokenUrl, authorizationCode needs both, refreshUrl is never required. Unknown flow keys are still rejected; an oauth2 scheme with no flows at all is still invalid.
  • SecurityScheme::setBearerFormat() — validation removed.
  • Named FLOW_* constants added, following the existing TYPE_*/IN_* pattern. FLOWS keeps its value and order.

Per-flow validation lives in SecurityScheme because only it knows which flow type a given OAuthFlow represents.

Two limits are deliberate: the "Applies to" column is enforced in one direction only (a flow carrying a URL it does not need is still accepted, since tightening that breaks the existing testRequired provider), and flows: {} still throws even though an empty map validates against the official schema — both are BC decisions of their own.

Backward compatibility

getAuthorizationUrl(), getTokenUrl() and getRefreshUrl() now return ?string, so callers feeding them into a string parameter need a null check. Unavoidable — the fields are genuinely optional. Everything else is source-compatible: constructor parameter order is unchanged and all four arguments now have defaults.

Tests

Eleven tests added, each failing against the unmodified code. Two tests that asserted the removed behaviour were replaced (testMissingBearerFormattestBearerFormatIsOptional; testMissingFlowtestSingleFlowIsAccepted + testUnknownFlowTypeIsRejected). testMissingFlows is retained — that case is still invalid. The specification's OAuth Flow Object Example is added as a round-trip fixture, verified to fail against the pre-fix code.

The OAuth Flow Object marks only scopes as REQUIRED. authorizationUrl
applies to implicit and authorizationCode, tokenUrl to password,
clientCredentials and authorizationCode, and refreshUrl to none of them.

Treating all four as mandatory made an implicit flow fail with a TypeError
on the official example from the specification.
The specification calls bearerFormat 'a hint to the client [...] primarily
for documentation purposes'. It is not REQUIRED, so a bearer scheme without
it is valid and must be accepted.
The OAuth Flows Object marks no field as REQUIRED, so defining only the
flows an API actually supports is valid. Requiring all four rejected almost
every real-world document, including the specification's own example.

Unknown flow keys are still rejected, and an oauth2 scheme with no flows
at all remains invalid.
Each flow type has its own required fields: implicit needs
authorizationUrl, password and clientCredentials need tokenUrl, and
authorizationCode needs both. refreshUrl is never required.

SecurityScheme performs this check because it knows the flow type of each
entry, which OAuthFlow itself does not.
This document is the OAuth Flow Object Example from OAS 3.1.1. Before the
preceding fixes it could not be parsed at all.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants