Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: fix
packages:
- "@typespec/openapi3"
---

Fix custom auth scheme models leaking into `components.schemas` when declared inside the service namespace. They are now emitted only under `components.securitySchemes` as expected.
6 changes: 6 additions & 0 deletions packages/openapi3/src/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1780,10 +1780,16 @@ function createOAPIEmitter(
}

function processUnreferencedSchemas() {
const authSchemeModels = new Set<Type>(serviceAuth.schemes.map((s) => s.model));
const addSchema = (type: Type) => {
if (isOrExtendsHttpFile(program, type)) {
return;
}
if (authSchemeModels.has(type)) {
// Auth scheme models are emitted under components.securitySchemes
// and should not also appear as payload schemas in components.schemas.
return;
}
if (
visibilityUsage.isUnreachable(type) &&
!paramModels.has(type) &&
Expand Down
64 changes: 64 additions & 0 deletions packages/openapi3/test/security.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -589,4 +589,68 @@ worksFor(supportedVersions, ({ diagnoseOpenApiFor, openApiFor }) => {
},
]);
});

it("does not emit a custom auth scheme model under components.schemas", async () => {
Comment thread
timotheeguerin marked this conversation as resolved.
// A custom auth scheme model declared inside the service namespace
// belongs only in `components.securitySchemes`. Previously
// `processUnreferencedSchemas` also emitted it under
// `components.schemas` because no payload references it, which
// caused downstream validators to reject auth-only attributes
// (e.g. `bearerFormat`) that propagated to the schemas-side copy.
const res = await openApiFor(
`
@useAuth(customBearer)
@service
namespace MyService;

model customBearer {
type: AuthType.http;
scheme: "bearer";
}

@route("/ping")
op ping(): { @statusCode _: 200; ok: boolean };
`,
);
deepStrictEqual(res.components.securitySchemes, {
customBearer: {
type: "http",
scheme: "bearer",
},
});
expect(res.components.schemas?.customBearer).toBeUndefined();
});

it("still emits an auth scheme model under components.schemas if referenced by an operation", async () => {
// The filter in `processUnreferencedSchemas` only skips auth scheme
// models that are otherwise unreachable. If the same model is also
// referenced from a payload (e.g. returned by an operation), it must
// continue to appear under `components.schemas` so the operation can
// $ref it, while still being emitted under `components.securitySchemes`.
const res = await openApiFor(
`
@useAuth(customBearer)
@service
namespace MyService;

model customBearer {
type: AuthType.http;
scheme: "bearer";
}

@route("/echo")
op echo(): customBearer;
`,
);
deepStrictEqual(res.components.securitySchemes, {
customBearer: {
type: "http",
scheme: "bearer",
},
});
expect(res.components.schemas?.customBearer).toBeDefined();
deepStrictEqual(res.paths["/echo"]["get"].responses["200"].content["application/json"].schema, {
$ref: "#/components/schemas/customBearer",
});
});
});
Loading