WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content

Commit 0e02281

Browse files
committed
Allow for scope descriptions
This makes a small change to the code introduced in #849 to allow for models to be used as OAuth scopes. Without this check, an object like `{value: "foo", description: "bar"}` gets wrapped in an additional `value` key. When the OpenAPI3 emitter tries to print out the `securitySchemes`, it sees `{value: {...}}` and produces a scope with value `[object Object]` and no description. This also introduces a base `SecurityScheme` model that allows for more specific type checking on the `@useAuth` decorator. We need to remove an `openapi3` test that checks for an error when providing an unsupported auth type. Now the compiler enforces that the auth type is `AuthType`, and `openapi3` supports all of the types in `AuthType`, it should not be possible to trigger this condition in TSP.
1 parent 63739c4 commit 0e02281

File tree

6 files changed

+91
-29
lines changed

6 files changed

+91
-29
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
changeKind: fix
3+
packages:
4+
- "@typespec/http"
5+
---
6+
7+
Allow for OAuth2 scopes to be properly specified with descriptions

packages/http/lib/auth.tsp

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
namespace TypeSpec.Http;
22

3+
model SecurityScheme {
4+
type: AuthType;
5+
}
6+
37
@doc("Authentication type")
48
enum AuthType {
59
@doc("HTTP")
@@ -27,7 +31,7 @@ enum AuthType {
2731
* ```
2832
*/
2933
@doc("")
30-
model BasicAuth {
34+
model BasicAuth extends SecurityScheme {
3135
@doc("Http authentication")
3236
type: AuthType.http;
3337

@@ -44,7 +48,7 @@ model BasicAuth {
4448
* ```
4549
*/
4650
@doc("")
47-
model BearerAuth {
51+
model BearerAuth extends SecurityScheme {
4852
@doc("Http authentication")
4953
type: AuthType.http;
5054

@@ -89,7 +93,7 @@ enum ApiKeyLocation {
8993
* @template Name The name of the API key
9094
*/
9195
@doc("")
92-
model ApiKeyAuth<Location extends ApiKeyLocation, Name extends string> {
96+
model ApiKeyAuth<Location extends ApiKeyLocation, Name extends string> extends SecurityScheme {
9397
@doc("API key authentication")
9498
type: AuthType.apiKey;
9599

@@ -100,6 +104,16 @@ model ApiKeyAuth<Location extends ApiKeyLocation, Name extends string> {
100104
name: Name;
101105
}
102106

107+
model OAuth2Scope {
108+
@doc("The scope identifier")
109+
value: string;
110+
111+
@doc("A description of the scope")
112+
description?: string;
113+
}
114+
115+
alias ScopeList = string[] | OAuth2Scope[];
116+
103117
/**
104118
* OAuth 2.0 is an authorization protocol that gives an API client limited access to user data on a web server.
105119
*
@@ -111,7 +125,7 @@ model ApiKeyAuth<Location extends ApiKeyLocation, Name extends string> {
111125
* @template Scopes The list of OAuth2 scopes, which are common for every flow from `Flows`. This list is combined with the scopes defined in specific OAuth2 flows.
112126
*/
113127
@doc("")
114-
model OAuth2Auth<Flows extends OAuth2Flow[], Scopes extends string[] = []> {
128+
model OAuth2Auth<Flows extends OAuth2Flow[], Scopes extends ScopeList = []> extends SecurityScheme {
115129
@doc("OAuth2 authentication")
116130
type: AuthType.oauth2;
117131

@@ -154,7 +168,7 @@ model AuthorizationCodeFlow {
154168
refreshUrl?: string;
155169

156170
@doc("list of scopes for the credential")
157-
scopes?: string[];
171+
scopes?: ScopeList;
158172
}
159173

160174
@doc("Implicit flow")
@@ -169,7 +183,7 @@ model ImplicitFlow {
169183
refreshUrl?: string;
170184

171185
@doc("list of scopes for the credential")
172-
scopes?: string[];
186+
scopes?: ScopeList;
173187
}
174188

175189
@doc("Resource Owner Password flow")
@@ -184,7 +198,7 @@ model PasswordFlow {
184198
refreshUrl?: string;
185199

186200
@doc("list of scopes for the credential")
187-
scopes?: string[];
201+
scopes?: ScopeList;
188202
}
189203

190204
@doc("Client credentials flow")
@@ -199,7 +213,7 @@ model ClientCredentialsFlow {
199213
refreshUrl?: string;
200214

201215
@doc("list of scopes for the credential")
202-
scopes?: string[];
216+
scopes?: ScopeList;
203217
}
204218

205219
/**
@@ -212,7 +226,7 @@ model ClientCredentialsFlow {
212226
* https://server.com/.well-known/openid-configuration
213227
* ```
214228
*/
215-
model OpenIdConnectAuth<ConnectUrl extends string> {
229+
model OpenIdConnectAuth<ConnectUrl extends string> extends SecurityScheme {
216230
/** Auth type */
217231
type: AuthType.openIdConnect;
218232

@@ -225,6 +239,6 @@ model OpenIdConnectAuth<ConnectUrl extends string> {
225239
* It might be useful when overriding authentication on interface of operation level.
226240
*/
227241
@doc("")
228-
model NoAuth {
242+
model NoAuth extends SecurityScheme {
229243
type: AuthType.noAuth;
230244
}

packages/http/lib/decorators.tsp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,10 @@ extern dec server(
374374
* namespace PetStore;
375375
* ```
376376
*/
377-
extern dec useAuth(target: Namespace | Interface | Operation, auth: {} | Union | {}[]);
377+
extern dec useAuth(
378+
target: Namespace | Interface | Operation,
379+
auth: SecurityScheme | Union | SecurityScheme[]
380+
);
378381

379382
/**
380383
* Defines the relative route URI template for the target operation as defined by [RFC 6570](https://datatracker.ietf.org/doc/html/rfc6570#section-3.2.3)

packages/http/src/decorators.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ import {
5252
HttpStatusCodeRange,
5353
HttpStatusCodes,
5454
HttpVerb,
55+
OAuth2Flow,
56+
OAuth2Scope,
57+
Oauth2Auth,
5558
PathParameterOptions,
5659
QueryParameterOptions,
5760
} from "./types.js";
@@ -621,7 +624,11 @@ function extractHttpAuthentication(
621624
];
622625
}
623626

624-
function extractOAuth2Auth(modelType: Model, data: any): HttpAuth {
627+
function normalizeScope(scope: string | OAuth2Scope): OAuth2Scope {
628+
return typeof scope === "string" ? { value: scope } : scope;
629+
}
630+
631+
function extractOAuth2Auth(modelType: Model, data: any): Oauth2Auth<OAuth2Flow[]> {
625632
// Validation of OAuth2Flow models in this function is minimal because the
626633
// type system already validates whether the model represents a flow
627634
// configuration. This code merely avoids runtime errors.
@@ -630,16 +637,19 @@ function extractOAuth2Auth(modelType: Model, data: any): HttpAuth {
630637
? data.flows
631638
: [];
632639

633-
const defaultScopes = Array.isArray(data.defaultScopes) ? data.defaultScopes : [];
640+
const defaultScopes: Array<string | OAuth2Scope> = Array.isArray(data.defaultScopes)
641+
? data.defaultScopes
642+
: [];
643+
634644
return {
635645
id: data.id,
636646
type: data.type,
637647
model: modelType,
638-
flows: flows.map((flow: any) => {
639-
const scopes: Array<string> = flow.scopes ? flow.scopes : defaultScopes;
648+
flows: flows.map((flow: OAuth2Flow) => {
649+
const scopes = flow.scopes ? flow.scopes : defaultScopes;
640650
return {
641651
...flow,
642-
scopes: scopes.map((x: string) => ({ value: x })),
652+
scopes: scopes.map(normalizeScope),
643653
};
644654
}),
645655
};

packages/http/test/http-decorators.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -897,6 +897,47 @@ describe("http: decorators", () => {
897897
});
898898
});
899899

900+
it("can specify OAuth2 with object scopes", async () => {
901+
const { Foo, program } = await Tester.compile(t.code`
902+
model MyFlow {
903+
type: OAuth2FlowType.implicit;
904+
authorizationUrl: "https://api.example.com/oauth2/authorize";
905+
refreshUrl: "https://api.example.com/oauth2/refresh";
906+
scopes: [
907+
{value: "read", description: "read data"},
908+
{value: "write", description: "write data"},
909+
];
910+
}
911+
@useAuth(OAuth2Auth<[MyFlow]>)
912+
namespace ${t.namespace("Foo")} {}
913+
`);
914+
915+
expect(getAuthentication(program, Foo)).toEqual({
916+
options: [
917+
{
918+
schemes: [
919+
{
920+
id: "OAuth2Auth",
921+
type: "oauth2",
922+
flows: [
923+
{
924+
type: "implicit",
925+
authorizationUrl: "https://api.example.com/oauth2/authorize",
926+
refreshUrl: "https://api.example.com/oauth2/refresh",
927+
scopes: [
928+
{ value: "read", description: "read data" },
929+
{ value: "write", description: "write data" },
930+
],
931+
},
932+
],
933+
model: expect.objectContaining({ kind: "Model" }),
934+
},
935+
],
936+
},
937+
],
938+
});
939+
});
940+
900941
it("can specify OAuth2 with scopes, which are default for every flow", async () => {
901942
const { Foo, program } = await Tester.compile(t.code`
902943
alias MyAuth<T extends string[]> = OAuth2Auth<Flows=[{

packages/openapi3/test/security.test.ts

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -247,19 +247,6 @@ worksFor(supportedVersions, ({ diagnoseOpenApiFor, openApiFor }) => {
247247
deepStrictEqual(res.security, [{ OpenIdConnectAuth: [] }]);
248248
});
249249

250-
it("set a unsupported auth", async () => {
251-
const diagnostics = await diagnoseOpenApiFor(
252-
`
253-
@service
254-
@useAuth({})
255-
namespace MyService {}
256-
`,
257-
);
258-
expectDiagnostics(diagnostics, {
259-
code: "@typespec/openapi3/unsupported-auth",
260-
});
261-
});
262-
263250
it("can specify custom auth name with description", async () => {
264251
const res = await openApiFor(
265252
`

0 commit comments

Comments
 (0)