Skip to content

Commit b44b8d4

Browse files
Feature: Add service authentication support (microsoft#795)
* Authentication base * Implement auth on http side * Authentication * use unique name * Add tests * Add docs * Merge with main * Wip * Export more things * Export more things * Allow description * Allow description * Missing desc in openapi3 * Missing desc in openapi3 * Fix formatting * Fix syntax error after merge Co-authored-by: David Wilson <[email protected]>
1 parent 829d555 commit b44b8d4

File tree

18 files changed

+1065
-19
lines changed

18 files changed

+1065
-19
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"changes": [
3+
{
4+
"packageName": "@cadl-lang/compiler",
5+
"comment": "Allow extracting value from enums",
6+
"type": "minor"
7+
}
8+
],
9+
"packageName": "@cadl-lang/compiler"
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"changes": [
3+
{
4+
"packageName": "@cadl-lang/openapi3",
5+
"comment": "Use authentication configured via `@useAuth` http decorator",
6+
"type": "minor"
7+
}
8+
],
9+
"packageName": "@cadl-lang/openapi3"
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"changes": [
3+
{
4+
"packageName": "@cadl-lang/rest",
5+
"comment": "Add new `@useAuth` decorator providing support to define service authentication",
6+
"type": "minor"
7+
}
8+
],
9+
"packageName": "@cadl-lang/rest"
10+
}

packages/compiler/core/decorator-utils.ts

+2
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,8 @@ function cadlTypeToJsonInternal(
355355
case "Boolean":
356356
case "Number":
357357
return [cadlType.value, []];
358+
case "EnumMember":
359+
return [cadlType.value ?? cadlType.name, []];
358360
case "Tuple": {
359361
const result = [];
360362
for (const [index, type] of cadlType.values.entries()) {

packages/openapi3/src/openapi.ts

+69-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
checkIfServiceNamespace,
3+
compilerAssert,
34
emitFile,
45
EmitOptionsFor,
56
EnumMemberType,
@@ -54,29 +55,34 @@ import {
5455
import { Discriminator, getDiscriminator, http } from "@cadl-lang/rest";
5556
import {
5657
getAllRoutes,
58+
getAuthentication,
5759
getContentTypes,
5860
getHeaderFieldName,
5961
getPathParamName,
6062
getQueryParamName,
6163
getStatusCodeDescription,
64+
HttpAuth,
6265
HttpOperationParameter,
6366
HttpOperationParameters,
6467
HttpOperationResponse,
6568
isStatusCode,
6669
OperationDetails,
6770
reportIfNoRoutes,
71+
ServiceAuthentication,
6872
} from "@cadl-lang/rest/http";
6973
import { buildVersionProjections } from "@cadl-lang/versioning";
7074
import { getOneOf, getRef } from "./decorators.js";
7175
import { OpenAPI3EmitterOptions, OpenAPILibrary, reportDiagnostic } from "./lib.js";
7276
import {
7377
OpenAPI3Discriminator,
7478
OpenAPI3Document,
79+
OpenAPI3OAuthFlows,
7580
OpenAPI3Operation,
7681
OpenAPI3Parameter,
7782
OpenAPI3ParameterType,
7883
OpenAPI3Schema,
7984
OpenAPI3SchemaProperty,
85+
OpenAPI3SecurityScheme,
8086
OpenAPI3Server,
8187
OpenAPI3ServerVariable,
8288
} from "./types.js";
@@ -212,6 +218,8 @@ function createOAPIEmitter(program: Program, options: ResolvedOpenAPI3EmitterOpt
212218
return { emitOpenAPI };
213219

214220
function initializeEmitter(serviceNamespaceType: NamespaceType, version?: string) {
221+
const auth = processAuth(serviceNamespaceType);
222+
215223
root = {
216224
openapi: "3.0.0",
217225
info: {
@@ -222,13 +230,14 @@ function createOAPIEmitter(program: Program, options: ResolvedOpenAPI3EmitterOpt
222230
externalDocs: getExternalDocs(program, serviceNamespaceType),
223231
tags: [],
224232
paths: {},
233+
security: auth?.security,
225234
components: {
226235
parameters: {},
227236
requestBodies: {},
228237
responses: {},
229238
schemas: {},
230239
examples: {},
231-
securitySchemes: {},
240+
securitySchemes: auth?.securitySchemes ?? {},
232241
},
233242
};
234243
const servers = http.getServers(program, serviceNamespaceType);
@@ -1305,6 +1314,65 @@ function createOAPIEmitter(program: Program, options: ResolvedOpenAPI3EmitterOpt
13051314
return { type: "string", format: "duration" };
13061315
}
13071316
}
1317+
1318+
function processAuth(serviceNamespace: NamespaceType):
1319+
| {
1320+
securitySchemes: Record<string, OpenAPI3SecurityScheme>;
1321+
security: Record<string, string[]>[];
1322+
}
1323+
| undefined {
1324+
const authentication = getAuthentication(program, serviceNamespace);
1325+
if (authentication) {
1326+
return processServiceAuthentication(authentication);
1327+
}
1328+
return undefined;
1329+
}
1330+
1331+
function processServiceAuthentication(authentication: ServiceAuthentication): {
1332+
securitySchemes: Record<string, OpenAPI3SecurityScheme>;
1333+
security: Record<string, string[]>[];
1334+
} {
1335+
const oaiSchemes: Record<string, OpenAPI3SecurityScheme> = {};
1336+
const security: Record<string, string[]>[] = [];
1337+
for (const option of authentication.options) {
1338+
const oai3SecurityOption: Record<string, string[]> = {};
1339+
for (const scheme of option.schemes) {
1340+
const [oaiScheme, scopes] = getOpenAPI3Scheme(scheme);
1341+
oaiSchemes[scheme.id] = oaiScheme;
1342+
oai3SecurityOption[scheme.id] = scopes;
1343+
}
1344+
security.push(oai3SecurityOption);
1345+
}
1346+
return { securitySchemes: oaiSchemes, security };
1347+
}
1348+
1349+
function getOpenAPI3Scheme(auth: HttpAuth): [OpenAPI3SecurityScheme, string[]] {
1350+
switch (auth.type) {
1351+
case "http":
1352+
return [{ type: "http", scheme: auth.scheme, description: auth.description }, []];
1353+
case "apiKey":
1354+
return [
1355+
{ type: "apiKey", in: auth.in, name: auth.name, description: auth.description },
1356+
[],
1357+
];
1358+
case "oauth2":
1359+
const flows: OpenAPI3OAuthFlows = {};
1360+
const scopes: string[] = [];
1361+
for (const flow of auth.flows) {
1362+
scopes.push(...flow.scopes);
1363+
flows[flow.type] = {
1364+
authorizationUrl: (flow as any).authorizationUrl,
1365+
tokenUrl: (flow as any).tokenUrl,
1366+
refreshUrl: flow.refreshUrl,
1367+
scopes: Object.fromEntries(flow.scopes.map((x: string) => [x, ""])),
1368+
};
1369+
}
1370+
return [{ type: "oauth2", flows, description: auth.description }, scopes];
1371+
default:
1372+
const _assertNever: never = auth;
1373+
compilerAssert(false, "Unreachable");
1374+
}
1375+
}
13081376
}
13091377

13101378
function prettierOutput(output: string) {

packages/openapi3/src/types.ts

+3
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ export interface OpenAPI3Document extends Extensions {
3434

3535
/** An element to hold various schemas for the specification. */
3636
components?: OpenAPI3Components;
37+
38+
/** A declaration of which security mechanisms can be used across the API. The list of values includes alternative security requirement objects that can be used. Only one of the security requirement objects need to be satisfied to authorize a request. Individual operations can override this definition. */
39+
security?: Record<string, string[]>[];
3740
}
3841

3942
export interface OpenAPI3Info extends Extensions {
+145
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import { deepStrictEqual } from "assert";
2+
import { openApiFor } from "./test-host.js";
3+
4+
describe("openapi3: security", () => {
5+
it("set a basic auth", async () => {
6+
const res = await openApiFor(
7+
`
8+
@serviceTitle("My service")
9+
@useAuth(BasicAuth)
10+
namespace MyService {}
11+
`
12+
);
13+
deepStrictEqual(res.components.securitySchemes, {
14+
BasicAuth: {
15+
type: "http",
16+
scheme: "basic",
17+
},
18+
});
19+
deepStrictEqual(res.security, [{ BasicAuth: [] }]);
20+
});
21+
22+
it("set a bearer auth", async () => {
23+
const res = await openApiFor(
24+
`
25+
@serviceTitle("My service")
26+
@useAuth(BearerAuth)
27+
namespace MyService {}
28+
`
29+
);
30+
deepStrictEqual(res.components.securitySchemes, {
31+
BearerAuth: {
32+
type: "http",
33+
scheme: "bearer",
34+
},
35+
});
36+
deepStrictEqual(res.security, [{ BearerAuth: [] }]);
37+
});
38+
39+
it("set a ApiKeyAuth ", async () => {
40+
const res = await openApiFor(
41+
`
42+
@serviceTitle("My service")
43+
@useAuth(ApiKeyAuth<ApiKeyLocation.header, "x-my-header">)
44+
namespace MyService {}
45+
`
46+
);
47+
deepStrictEqual(res.components.securitySchemes, {
48+
ApiKeyAuth: {
49+
type: "apiKey",
50+
in: "header",
51+
name: "x-my-header",
52+
},
53+
});
54+
deepStrictEqual(res.security, [{ ApiKeyAuth: [] }]);
55+
});
56+
57+
it("set a oauth2 auth", async () => {
58+
const res = await openApiFor(
59+
`
60+
@serviceTitle("My service")
61+
62+
@useAuth(OAuth2Auth<[MyFlow]>)
63+
namespace MyService {
64+
model MyFlow {
65+
type: OAuth2FlowType.implicit;
66+
authorizationUrl: "https://api.example.com/oauth2/authorize";
67+
refreshUrl: "https://api.example.com/oauth2/refresh";
68+
scopes: ["read", "write"];
69+
}
70+
}
71+
`
72+
);
73+
deepStrictEqual(res.components.securitySchemes, {
74+
OAuth2Auth: {
75+
type: "oauth2",
76+
flows: {
77+
implicit: {
78+
authorizationUrl: "https://api.example.com/oauth2/authorize",
79+
refreshUrl: "https://api.example.com/oauth2/refresh",
80+
scopes: {
81+
read: "",
82+
write: "",
83+
},
84+
},
85+
},
86+
},
87+
});
88+
deepStrictEqual(res.security, [{ OAuth2Auth: ["read", "write"] }]);
89+
});
90+
91+
it("can specify custom auth name with description", async () => {
92+
const res = await openApiFor(
93+
`
94+
@serviceTitle("My service")
95+
@useAuth(MyAuth)
96+
@test namespace Foo {
97+
@doc("My custom basic auth")
98+
model MyAuth is BasicAuth;
99+
}
100+
`
101+
);
102+
deepStrictEqual(res.components.securitySchemes, {
103+
MyAuth: {
104+
type: "http",
105+
scheme: "basic",
106+
description: "My custom basic auth",
107+
},
108+
});
109+
deepStrictEqual(res.security, [{ MyAuth: [] }]);
110+
});
111+
112+
it("can use multiple auth", async () => {
113+
const res = await openApiFor(
114+
`
115+
@serviceTitle("My service")
116+
@useAuth(BearerAuth | [ApiKeyAuth<ApiKeyLocation.header, "x-my-header">, BasicAuth])
117+
namespace MyService {}
118+
`
119+
);
120+
deepStrictEqual(res.components.securitySchemes, {
121+
ApiKeyAuth: {
122+
in: "header",
123+
name: "x-my-header",
124+
type: "apiKey",
125+
},
126+
BasicAuth: {
127+
scheme: "basic",
128+
type: "http",
129+
},
130+
BearerAuth: {
131+
scheme: "bearer",
132+
type: "http",
133+
},
134+
});
135+
deepStrictEqual(res.security, [
136+
{
137+
BearerAuth: [],
138+
},
139+
{
140+
ApiKeyAuth: [],
141+
BasicAuth: [],
142+
},
143+
]);
144+
});
145+
});

packages/rest/README.md

+29-17
Original file line numberDiff line numberDiff line change
@@ -25,27 +25,38 @@ See [Rest section in the tutorial](https://github.com/microsoft/cadl/blob/main/d
2525

2626
`@cadl-lang/rest` library defines of the following artifacts:
2727

28-
- [Models](#models)
29-
- [Decorators](#decorators)
30-
- [Interfaces](#interfaces)
28+
- [Cadl HTTP/Rest Library](#cadl-httprest-library)
29+
- [Install](#install)
30+
- [Usage](#usage)
31+
- [Library Tour](#library-tour)
32+
- [Models](#models)
33+
- [Decorators](#decorators)
34+
- [Interfaces](#interfaces)
35+
- [See also](#see-also)
3136

3237
## Models
3338

3439
- ### HTTP namespace
35-
| Model | Notes |
36-
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
37-
| LocationHeader | Location header |
38-
| Response&lt;Status> | &lt;Status> is numerical status code. |
39-
| OkResponse&lt;T> | Response&lt;200> with T as the response body model type. |
40-
| CreatedResponse | Response&lt;201> |
41-
| AcceptedResponse | Response&lt;202> |
42-
| NoContentResponse | Response&lt;204> |
43-
| MovedResponse | Response&lt;301> with LocationHeader for redirected URL |
44-
| NotModifiedResponse | Response&lt;304> |
45-
| UnauthorizedResponse | Response&lt;401> |
46-
| NotFoundResponse | Response&lt;404> |
47-
| ConflictResponse | Response&lt;409> |
48-
| PlainData&lt;T> | Produces a new model with the same properties as T, but with @query, @header, @body, and @path decorators removed from all properties. |
40+
41+
| Model | Notes |
42+
| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
43+
| LocationHeader | Location header |
44+
| Response&lt;Status> | &lt;Status> is numerical status code. |
45+
| OkResponse&lt;T> | Response&lt;200> with T as the response body model type. |
46+
| CreatedResponse | Response&lt;201> |
47+
| AcceptedResponse | Response&lt;202> |
48+
| NoContentResponse | Response&lt;204> |
49+
| MovedResponse | Response&lt;301> with LocationHeader for redirected URL |
50+
| NotModifiedResponse | Response&lt;304> |
51+
| UnauthorizedResponse | Response&lt;401> |
52+
| NotFoundResponse | Response&lt;404> |
53+
| ConflictResponse | Response&lt;409> |
54+
| PlainData&lt;T> | Produces a new model with the same properties as T, but with @query, @header, @body, and @path decorators removed from all properties. |
55+
| BasicAuth | Configure `basic` authentication with @useAuth |
56+
| BearerAuth | Configure `bearer` authentication with @useAuth |
57+
| ApiKeyAuth<TLocation, TName> | Configure `apiKey` authentication with @useAuth |
58+
| OAuth2Auth<TFlows> | Configure `oauth2` authentication with @useAuth |
59+
4960
- ### REST namespace
5061
| Model | Notes |
5162
| ------------------------------------------ | ----------------------------------------------------------------------------------------------------------- |
@@ -78,6 +89,7 @@ The `@cadl-lang/rest` library defines the following decorators in `Cadl.Http` na
7889
| @path | model properties and operation parameters | indicating the properties are in request path. |
7990
| @statusCode | model properties and operation parameters | indicating the property is the return status code. Only one allowed per model. |
8091
| @server | namespace | Configure the server url for the service. |
92+
| @useAuth | namespace | Configure the service authentication. |
8193

8294
- ### REST namespace
8395

0 commit comments

Comments
 (0)