Skip to content
Open
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/http"
---

Allow for OAuth2 scopes to be properly specified with descriptions
20 changes: 15 additions & 5 deletions packages/http/lib/auth.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,16 @@ model ApiKeyAuth<Location extends ApiKeyLocation, Name extends string> {
name: Name;
}

model OAuth2Scope {
@doc("The scope identifier")
value: string;

@doc("A description of the scope")
description?: string;
}

alias ScopeList = string[] | OAuth2Scope[];

/**
* OAuth 2.0 is an authorization protocol that gives an API client limited access to user data on a web server.
*
Expand All @@ -111,7 +121,7 @@ model ApiKeyAuth<Location extends ApiKeyLocation, Name extends string> {
* @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.
*/
@doc("")
model OAuth2Auth<Flows extends OAuth2Flow[], Scopes extends string[] = []> {
model OAuth2Auth<Flows extends OAuth2Flow[], Scopes extends ScopeList = []> {
@doc("OAuth2 authentication")
type: AuthType.oauth2;

Expand Down Expand Up @@ -154,7 +164,7 @@ model AuthorizationCodeFlow {
refreshUrl?: string;

@doc("list of scopes for the credential")
scopes?: string[];
scopes?: ScopeList;
}

@doc("Implicit flow")
Expand All @@ -169,7 +179,7 @@ model ImplicitFlow {
refreshUrl?: string;

@doc("list of scopes for the credential")
scopes?: string[];
scopes?: ScopeList;
}

@doc("Resource Owner Password flow")
Expand All @@ -184,7 +194,7 @@ model PasswordFlow {
refreshUrl?: string;

@doc("list of scopes for the credential")
scopes?: string[];
scopes?: ScopeList;
}

@doc("Client credentials flow")
Expand All @@ -199,7 +209,7 @@ model ClientCredentialsFlow {
refreshUrl?: string;

@doc("list of scopes for the credential")
scopes?: string[];
scopes?: ScopeList;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some previous idea here was to allow an enum instead what do you think

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean that instead of using OAuth2Scope, you would define scopes as e.g.

enum MyScopes {
  Read: "Read public data",
  Write: "Write private data",
}

?

If so, I like it. I like that it gives you a convenient way to reference individual scopes.

Are there any other places where we employ enums this way (enum value is documentation / description)?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah exactly, we do for the version enum where the value is the api version(though that also could be left to interpretation)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good; I'll make that change.

}

/**
Expand Down
20 changes: 15 additions & 5 deletions packages/http/src/decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ import {
HttpStatusCodeRange,
HttpStatusCodes,
HttpVerb,
OAuth2Flow,
OAuth2Scope,
Oauth2Auth,
PathParameterOptions,
QueryParameterOptions,
} from "./types.js";
Expand Down Expand Up @@ -621,7 +624,11 @@ function extractHttpAuthentication(
];
}

function extractOAuth2Auth(modelType: Model, data: any): HttpAuth {
function normalizeScope(scope: string | OAuth2Scope): OAuth2Scope {
return typeof scope === "string" ? { value: scope } : scope;
}

function extractOAuth2Auth(modelType: Model, data: any): Oauth2Auth<OAuth2Flow[]> {
// Validation of OAuth2Flow models in this function is minimal because the
// type system already validates whether the model represents a flow
// configuration. This code merely avoids runtime errors.
Expand All @@ -630,16 +637,19 @@ function extractOAuth2Auth(modelType: Model, data: any): HttpAuth {
? data.flows
: [];

const defaultScopes = Array.isArray(data.defaultScopes) ? data.defaultScopes : [];
const defaultScopes: Array<string | OAuth2Scope> = Array.isArray(data.defaultScopes)
? data.defaultScopes
: [];

return {
id: data.id,
type: data.type,
model: modelType,
flows: flows.map((flow: any) => {
const scopes: Array<string> = flow.scopes ? flow.scopes : defaultScopes;
flows: flows.map((flow: OAuth2Flow) => {
const scopes = flow.scopes ? flow.scopes : defaultScopes;
return {
...flow,
scopes: scopes.map((x: string) => ({ value: x })),
scopes: scopes.map(normalizeScope),
};
}),
};
Expand Down
41 changes: 41 additions & 0 deletions packages/http/test/http-decorators.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -897,6 +897,47 @@ describe("http: decorators", () => {
});
});

it("can specify OAuth2 with object scopes", async () => {
const { Foo, program } = await Tester.compile(t.code`
model MyFlow {
type: OAuth2FlowType.implicit;
authorizationUrl: "https://api.example.com/oauth2/authorize";
refreshUrl: "https://api.example.com/oauth2/refresh";
scopes: [
{value: "read", description: "read data"},
{value: "write", description: "write data"},
];
}
@useAuth(OAuth2Auth<[MyFlow]>)
namespace ${t.namespace("Foo")} {}
`);

expect(getAuthentication(program, Foo)).toEqual({
options: [
{
schemes: [
{
id: "OAuth2Auth",
type: "oauth2",
flows: [
{
type: "implicit",
authorizationUrl: "https://api.example.com/oauth2/authorize",
refreshUrl: "https://api.example.com/oauth2/refresh",
scopes: [
{ value: "read", description: "read data" },
{ value: "write", description: "write data" },
],
},
],
model: expect.objectContaining({ kind: "Model" }),
},
],
},
],
});
});

it("can specify OAuth2 with scopes, which are default for every flow", async () => {
const { Foo, program } = await Tester.compile(t.code`
alias MyAuth<T extends string[]> = OAuth2Auth<Flows=[{
Expand Down
Loading