Operation level authentication and scopes (#2901)
Hi! 🖖🏻 This PR resolves #2624 by implementing the [design doc](https://gist.github.com/timotheeguerin/56690786e61a436710dd647de9febc0f), but in its initial form: - `@useAuth` can now be applied not only to service namespace, but to interfaces and operations as well. Its arguments override all authentication, which was set for enclosing scopes. - OAuth2 scopes can now be set at operation level (though, the code doing this in OpenAPI emitter is a bit clunky). - New `NoAuth` authentication option allows to declare optional authentication (`NoAuth | AnyOtherAuth`) or override authentication to none in nested scopes. This implementation does not introduce new `@authScopes` decorator as design doc comments suggest, and here's why: 1. It does not compose well with `@useAuth` at operation level. For example ``` ... @useAuth(BasicAuth) @authScopes(MyOauth2, ["read"]) op gogo(): void ``` Should that be equivalent to `BasicAuth | MyOauth2`, or to `[BasicAuth, MyOauth2]`? 2. Introducing new decorator would increase complexity, but (imho) it would not reduce the amount of boilerplate: ``` alias MyOAuth2 = OAuth2Auth<{ ... }>; @useAuth(MyOAuth2) @authAcopes(MyOauth2, ["read"]) @service namepsace Foo; ``` vs ``` model MyOAuth2Flow<T extends string[]> { ... }; alias MyOauth2<T extends string[]> = Oauth2Auth<[MyOauth2Flow[T]]> @useAuth(MyOAuth2<["read"]>) @service namepsace Foo ``` I would be happy to hear any feedback and apply suggested changes. And thanks for a convenient development setup and thorough test coverage! --------- Co-authored-by: Timothee Guerin <timothee.guerin@outlook.com>
This commit is contained in:
Родитель
aa6e53f4ab
Коммит
d2d397cb67
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking
|
||||
changeKind: feature
|
||||
packages:
|
||||
- "@typespec/http"
|
||||
---
|
||||
|
||||
Add ability to sepcify authentication and different scopes per operation
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking
|
||||
changeKind: internal
|
||||
packages:
|
||||
- "@typespec/openapi3"
|
||||
---
|
||||
|
||||
Add support for per operation authentication and scopes
|
|
@ -174,6 +174,15 @@ The URL of the requested resource has been changed permanently. The new URL is g
|
|||
model TypeSpec.Http.MovedResponse
|
||||
```
|
||||
|
||||
### `NoAuth` {#TypeSpec.Http.NoAuth}
|
||||
|
||||
This authentication option signifies that API is not secured at all.
|
||||
It might be useful when overriding authentication on interface of operation level.
|
||||
|
||||
```typespec
|
||||
model TypeSpec.Http.NoAuth
|
||||
```
|
||||
|
||||
### `NoContentResponse` {#TypeSpec.Http.NoContentResponse}
|
||||
|
||||
There is no content to send for this request, but the headers may be useful.
|
||||
|
@ -207,14 +216,15 @@ For that purpose, an OAuth 2.0 server issues access tokens that the client appli
|
|||
For more information about OAuth 2.0, see oauth.net and RFC 6749.
|
||||
|
||||
```typespec
|
||||
model TypeSpec.Http.OAuth2Auth<Flows>
|
||||
model TypeSpec.Http.OAuth2Auth<Flows, Scopes>
|
||||
```
|
||||
|
||||
#### Template Parameters
|
||||
|
||||
| Name | Description |
|
||||
| ----- | ---------------------------------- |
|
||||
| Flows | The list of supported OAuth2 flows |
|
||||
| Name | Description |
|
||||
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| Flows | The list of supported OAuth2 flows |
|
||||
| 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. |
|
||||
|
||||
### `OkResponse` {#TypeSpec.Http.OkResponse}
|
||||
|
||||
|
|
|
@ -402,7 +402,7 @@ op create(): {@statusCode: 201 | 202}
|
|||
|
||||
### `@useAuth` {#@TypeSpec.Http.useAuth}
|
||||
|
||||
Specify this service authentication. See the [documentation in the Http library](https://typespec.io/docs/libraries/http/authentication) for full details.
|
||||
Specify authentication for a whole service or specific methods. See the [documentation in the Http library](https://typespec.io/docs/libraries/http/authentication) for full details.
|
||||
|
||||
```typespec
|
||||
@TypeSpec.Http.useAuth(auth: {} | Union | {}[])
|
||||
|
@ -410,7 +410,7 @@ Specify this service authentication. See the [documentation in the Http library]
|
|||
|
||||
#### Target
|
||||
|
||||
`Namespace`
|
||||
`union Namespace | Interface | Operation`
|
||||
|
||||
#### Parameters
|
||||
|
||||
|
|
|
@ -69,6 +69,7 @@ npm install --save-peer @typespec/http
|
|||
- [`ImplicitFlow`](./data-types.md#TypeSpec.Http.ImplicitFlow)
|
||||
- [`LocationHeader`](./data-types.md#TypeSpec.Http.LocationHeader)
|
||||
- [`MovedResponse`](./data-types.md#TypeSpec.Http.MovedResponse)
|
||||
- [`NoAuth`](./data-types.md#TypeSpec.Http.NoAuth)
|
||||
- [`NoContentResponse`](./data-types.md#TypeSpec.Http.NoContentResponse)
|
||||
- [`NotFoundResponse`](./data-types.md#TypeSpec.Http.NotFoundResponse)
|
||||
- [`NotModifiedResponse`](./data-types.md#TypeSpec.Http.NotModifiedResponse)
|
||||
|
|
|
@ -447,7 +447,7 @@ op create(): {@statusCode: 201 | 202}
|
|||
|
||||
#### `@useAuth`
|
||||
|
||||
Specify this service authentication. See the [documentation in the Http library](https://typespec.io/docs/libraries/http/authentication) for full details.
|
||||
Specify authentication for a whole service or specific methods. See the [documentation in the Http library](https://typespec.io/docs/libraries/http/authentication) for full details.
|
||||
|
||||
```typespec
|
||||
@TypeSpec.Http.useAuth(auth: {} | Union | {}[])
|
||||
|
@ -455,7 +455,7 @@ Specify this service authentication. See the [documentation in the Http library]
|
|||
|
||||
##### Target
|
||||
|
||||
`Namespace`
|
||||
`union Namespace | Interface | Operation`
|
||||
|
||||
##### Parameters
|
||||
|
||||
|
|
|
@ -13,6 +13,9 @@ enum AuthType {
|
|||
|
||||
@doc("OpenID connect")
|
||||
openIdConnect,
|
||||
|
||||
@doc("Empty auth")
|
||||
noAuth,
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -105,14 +108,18 @@ model ApiKeyAuth<Location extends ApiKeyLocation, Name extends string> {
|
|||
* For more information about OAuth 2.0, see oauth.net and RFC 6749.
|
||||
*
|
||||
* @template Flows The list of supported OAuth2 flows
|
||||
* @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[]> {
|
||||
model OAuth2Auth<Flows extends OAuth2Flow[], Scopes extends string[] = []> {
|
||||
@doc("OAuth2 authentication")
|
||||
type: AuthType.oauth2;
|
||||
|
||||
@doc("Supported OAuth2 flows")
|
||||
flows: Flows;
|
||||
|
||||
@doc("Oauth2 scopes of every flow. Overridden by scope definitions in specific flows")
|
||||
defaultScopes: Scopes;
|
||||
}
|
||||
|
||||
@doc("Describes the OAuth2 flow type")
|
||||
|
@ -147,7 +154,7 @@ model AuthorizationCodeFlow {
|
|||
refreshUrl?: string;
|
||||
|
||||
@doc("list of scopes for the credential")
|
||||
scopes: string[];
|
||||
scopes?: string[];
|
||||
}
|
||||
|
||||
@doc("Implicit flow")
|
||||
|
@ -162,7 +169,7 @@ model ImplicitFlow {
|
|||
refreshUrl?: string;
|
||||
|
||||
@doc("list of scopes for the credential")
|
||||
scopes: string[];
|
||||
scopes?: string[];
|
||||
}
|
||||
|
||||
@doc("Resource Owner Password flow")
|
||||
|
@ -177,7 +184,7 @@ model PasswordFlow {
|
|||
refreshUrl?: string;
|
||||
|
||||
@doc("list of scopes for the credential")
|
||||
scopes: string[];
|
||||
scopes?: string[];
|
||||
}
|
||||
|
||||
@doc("Client credentials flow")
|
||||
|
@ -192,7 +199,7 @@ model ClientCredentialsFlow {
|
|||
refreshUrl?: string;
|
||||
|
||||
@doc("list of scopes for the credential")
|
||||
scopes: string[];
|
||||
scopes?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -212,3 +219,12 @@ model OpenIdConnectAuth<ConnectUrl extends valueof string> {
|
|||
/** Connect url. It can be specified relative to the server URL */
|
||||
openIdConnectUrl: ConnectUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* This authentication option signifies that API is not secured at all.
|
||||
* It might be useful when overriding authentication on interface of operation level.
|
||||
*/
|
||||
@doc("")
|
||||
model NoAuth {
|
||||
type: AuthType.noAuth;
|
||||
}
|
||||
|
|
|
@ -202,7 +202,7 @@ extern dec server(
|
|||
);
|
||||
|
||||
/**
|
||||
* Specify this service authentication. See the [documentation in the Http library](https://typespec.io/docs/libraries/http/authentication) for full details.
|
||||
* Specify authentication for a whole service or specific methods. See the [documentation in the Http library](https://typespec.io/docs/libraries/http/authentication) for full details.
|
||||
*
|
||||
* @param auth Authentication configuration. Can be a single security scheme, a union(either option is valid authentication) or a tuple (must use all authentication together)
|
||||
* @example
|
||||
|
@ -213,7 +213,7 @@ extern dec server(
|
|||
* namespace PetStore;
|
||||
* ```
|
||||
*/
|
||||
extern dec useAuth(target: Namespace, auth: {} | Union | {}[]);
|
||||
extern dec useAuth(target: Namespace | Interface | Operation, auth: {} | Union | {}[]);
|
||||
|
||||
/**
|
||||
* Specify if inapplicable metadata should be included in the payload for the given entity.
|
||||
|
|
|
@ -0,0 +1,185 @@
|
|||
import { Operation, Program } from "@typespec/compiler";
|
||||
import { deepClone, deepEquals } from "@typespec/compiler/utils";
|
||||
import { HttpStateKeys } from "./lib.js";
|
||||
import {
|
||||
Authentication,
|
||||
HttpAuth,
|
||||
HttpService,
|
||||
NoAuth,
|
||||
OAuth2Flow,
|
||||
OAuth2Scope,
|
||||
Oauth2Auth,
|
||||
} from "./types.js";
|
||||
|
||||
export function getAuthenticationForOperation(
|
||||
program: Program,
|
||||
operation: Operation
|
||||
): Authentication | undefined {
|
||||
const operationAuth = program.stateMap(HttpStateKeys.authentication).get(operation);
|
||||
if (operationAuth === undefined && operation.interface !== undefined) {
|
||||
const interfaceAuth = program.stateMap(HttpStateKeys.authentication).get(operation.interface);
|
||||
return interfaceAuth;
|
||||
}
|
||||
return operationAuth;
|
||||
}
|
||||
|
||||
export type HttpAuthRef = AnyHttpAuthRef | OAuth2HttpAuthRef | NoHttpAuthRef;
|
||||
|
||||
export interface AnyHttpAuthRef {
|
||||
readonly kind: "any";
|
||||
readonly auth: HttpAuth;
|
||||
}
|
||||
|
||||
export interface NoHttpAuthRef {
|
||||
readonly kind: "noAuth";
|
||||
readonly auth: NoAuth;
|
||||
}
|
||||
|
||||
/* Holder of this reference needs only a `scopes` subset of all scopes defined at `auth` */
|
||||
export interface OAuth2HttpAuthRef {
|
||||
readonly kind: "oauth2";
|
||||
readonly auth: Oauth2Auth<OAuth2Flow[]>;
|
||||
readonly scopes: string[];
|
||||
}
|
||||
|
||||
export interface AuthenticationReference {
|
||||
/**
|
||||
* Either one of those options can be used independently to authenticate.
|
||||
*/
|
||||
readonly options: AuthenticationOptionReference[];
|
||||
}
|
||||
|
||||
export interface AuthenticationOptionReference {
|
||||
/**
|
||||
* For this authentication option all the given auth have to be used together.
|
||||
*/
|
||||
readonly all: HttpAuthRef[];
|
||||
}
|
||||
|
||||
export interface HttpServiceAuthentication {
|
||||
/**
|
||||
* All the authentication schemes used in this service.
|
||||
* Some might only be used in certain operations.
|
||||
*/
|
||||
readonly schemes: HttpAuth[];
|
||||
|
||||
/**
|
||||
* Default authentication for operations in this service.
|
||||
*/
|
||||
readonly defaultAuth: AuthenticationReference;
|
||||
|
||||
/**
|
||||
* Authentication overrides for individual operations.
|
||||
*/
|
||||
readonly operationsAuth: Map<Operation, AuthenticationReference>;
|
||||
}
|
||||
|
||||
export function resolveAuthentication(service: HttpService): HttpServiceAuthentication {
|
||||
let schemes: Record<string, HttpAuth> = {};
|
||||
let defaultAuth: AuthenticationReference = { options: [] };
|
||||
const operationsAuth: Map<Operation, AuthenticationReference> = new Map();
|
||||
|
||||
if (service.authentication) {
|
||||
const { newServiceSchemes, authOptions } = gatherAuth(service.authentication, {});
|
||||
schemes = newServiceSchemes;
|
||||
defaultAuth = authOptions;
|
||||
}
|
||||
|
||||
for (const op of service.operations) {
|
||||
if (op.authentication) {
|
||||
const { newServiceSchemes, authOptions } = gatherAuth(op.authentication, schemes);
|
||||
schemes = newServiceSchemes;
|
||||
operationsAuth.set(op.operation, authOptions);
|
||||
}
|
||||
}
|
||||
|
||||
return { schemes: Object.values(schemes), defaultAuth, operationsAuth };
|
||||
}
|
||||
|
||||
function gatherAuth(
|
||||
authentication: Authentication,
|
||||
serviceSchemes: Record<string, HttpAuth>
|
||||
): {
|
||||
newServiceSchemes: Record<string, HttpAuth>;
|
||||
authOptions: AuthenticationReference;
|
||||
} {
|
||||
const newServiceSchemes: Record<string, HttpAuth> = serviceSchemes;
|
||||
const authOptions: AuthenticationReference = { options: [] };
|
||||
for (const option of authentication.options) {
|
||||
const authOption: AuthenticationOptionReference = { all: [] };
|
||||
for (const optionScheme of option.schemes) {
|
||||
const serviceScheme = serviceSchemes[optionScheme.id];
|
||||
let newServiceScheme = optionScheme;
|
||||
if (serviceScheme) {
|
||||
// If we've seen a different scheme by this id,
|
||||
// Make sure to not overwrite it
|
||||
if (!authsAreEqual(serviceScheme, optionScheme)) {
|
||||
while (serviceSchemes[newServiceScheme.id]) {
|
||||
newServiceScheme.id = newServiceScheme.id + "_";
|
||||
}
|
||||
}
|
||||
// Merging scopes when encountering the same Oauth2 scheme
|
||||
else if (serviceScheme.type === "oauth2" && optionScheme.type === "oauth2") {
|
||||
const x = mergeOAuthScopes(serviceScheme, optionScheme);
|
||||
newServiceScheme = x;
|
||||
}
|
||||
}
|
||||
const httpAuthRef = makeHttpAuthRef(optionScheme, newServiceScheme);
|
||||
newServiceSchemes[newServiceScheme.id] = newServiceScheme;
|
||||
authOption.all.push(httpAuthRef);
|
||||
}
|
||||
authOptions.options.push(authOption);
|
||||
}
|
||||
return { newServiceSchemes, authOptions };
|
||||
}
|
||||
|
||||
function makeHttpAuthRef(local: HttpAuth, reference: HttpAuth): HttpAuthRef {
|
||||
if (reference.type === "oauth2" && local.type === "oauth2") {
|
||||
const scopes: string[] = [];
|
||||
for (const flow of local.flows) {
|
||||
scopes.push(...flow.scopes.map((x) => x.value));
|
||||
}
|
||||
return { kind: "oauth2", auth: reference, scopes: scopes };
|
||||
} else if (reference.type === "noAuth") {
|
||||
return { kind: "noAuth", auth: reference };
|
||||
} else {
|
||||
return { kind: "any", auth: reference };
|
||||
}
|
||||
}
|
||||
|
||||
function mergeOAuthScopes<Flows extends OAuth2Flow[]>(
|
||||
scheme1: Oauth2Auth<Flows>,
|
||||
scheme2: Oauth2Auth<Flows>
|
||||
): Oauth2Auth<Flows> {
|
||||
const flows = deepClone(scheme1.flows);
|
||||
flows.forEach((flow1, i) => {
|
||||
const flow2 = scheme2.flows[i];
|
||||
const scopes = Array.from(new Set(flow1.scopes.concat(flow2.scopes)));
|
||||
flows[i].scopes = scopes;
|
||||
});
|
||||
return {
|
||||
...scheme1,
|
||||
flows,
|
||||
};
|
||||
}
|
||||
|
||||
function setOauth2Scopes<Flows extends OAuth2Flow[]>(
|
||||
scheme: Oauth2Auth<Flows>,
|
||||
scopes: OAuth2Scope[]
|
||||
): Oauth2Auth<Flows> {
|
||||
const flows: Flows = deepClone(scheme.flows);
|
||||
flows.forEach((flow) => {
|
||||
flow.scopes = scopes;
|
||||
});
|
||||
return {
|
||||
...scheme,
|
||||
flows,
|
||||
};
|
||||
}
|
||||
|
||||
function authsAreEqual(scheme1: HttpAuth, scheme2: HttpAuth): boolean {
|
||||
if (scheme1.type === "oauth2" && scheme2.type === "oauth2") {
|
||||
return deepEquals(setOauth2Scopes(scheme1, []), setOauth2Scopes(scheme2, []));
|
||||
}
|
||||
return deepEquals(scheme1, scheme2);
|
||||
}
|
|
@ -2,6 +2,7 @@ import {
|
|||
DecoratorContext,
|
||||
Diagnostic,
|
||||
DiagnosticTarget,
|
||||
Interface,
|
||||
Model,
|
||||
ModelProperty,
|
||||
Namespace,
|
||||
|
@ -25,6 +26,7 @@ import { HttpStateKeys, createDiagnostic, reportDiagnostic } from "./lib.js";
|
|||
import { setRoute, setSharedRoute } from "./route.js";
|
||||
import { getStatusCodesFromType } from "./status-codes.js";
|
||||
import {
|
||||
Authentication,
|
||||
AuthenticationOption,
|
||||
HeaderFieldOptions,
|
||||
HttpAuth,
|
||||
|
@ -33,7 +35,6 @@ import {
|
|||
HttpVerb,
|
||||
PathParameterOptions,
|
||||
QueryParameterOptions,
|
||||
ServiceAuthentication,
|
||||
} from "./types.js";
|
||||
import { extractParamsFromPath } from "./utils.js";
|
||||
|
||||
|
@ -432,28 +433,28 @@ setTypeSpecNamespace("Private", $plainData);
|
|||
|
||||
export function $useAuth(
|
||||
context: DecoratorContext,
|
||||
serviceNamespace: Namespace,
|
||||
entity: Namespace | Interface | Operation,
|
||||
authConfig: Model | Union | Tuple
|
||||
) {
|
||||
const [auth, diagnostics] = extractServiceAuthentication(context.program, authConfig);
|
||||
const [auth, diagnostics] = extractAuthentication(context.program, authConfig);
|
||||
if (diagnostics.length > 0) context.program.reportDiagnostics(diagnostics);
|
||||
if (auth !== undefined) {
|
||||
setAuthentication(context.program, serviceNamespace, auth);
|
||||
setAuthentication(context.program, entity, auth);
|
||||
}
|
||||
}
|
||||
|
||||
export function setAuthentication(
|
||||
program: Program,
|
||||
serviceNamespace: Namespace,
|
||||
auth: ServiceAuthentication
|
||||
entity: Namespace | Interface | Operation,
|
||||
auth: Authentication
|
||||
) {
|
||||
program.stateMap(HttpStateKeys.authentication).set(serviceNamespace, auth);
|
||||
program.stateMap(HttpStateKeys.authentication).set(entity, auth);
|
||||
}
|
||||
|
||||
function extractServiceAuthentication(
|
||||
function extractAuthentication(
|
||||
program: Program,
|
||||
type: Model | Union | Tuple
|
||||
): [ServiceAuthentication | undefined, readonly Diagnostic[]] {
|
||||
): [Authentication | undefined, readonly Diagnostic[]] {
|
||||
const diagnostics = createDiagnosticCollector();
|
||||
|
||||
switch (type.kind) {
|
||||
|
@ -473,7 +474,7 @@ function extractHttpAuthenticationOptions(
|
|||
program: Program,
|
||||
tuple: Union,
|
||||
diagnosticTarget: DiagnosticTarget
|
||||
): [ServiceAuthentication, readonly Diagnostic[]] {
|
||||
): [Authentication, readonly Diagnostic[]] {
|
||||
const options: AuthenticationOption[] = [];
|
||||
const diagnostics = createDiagnosticCollector();
|
||||
for (const variant of tuple.variants.values()) {
|
||||
|
@ -565,12 +566,16 @@ function extractOAuth2Auth(data: any): HttpAuth {
|
|||
Array.isArray(data.flows) && data.flows.every((x: any) => typeof x === "object")
|
||||
? data.flows
|
||||
: [];
|
||||
|
||||
const defaultScopes = Array.isArray(data.defaultScopes) ? data.defaultScopes : [];
|
||||
return {
|
||||
...data,
|
||||
id: data.id,
|
||||
type: data.type,
|
||||
flows: flows.map((flow: any) => {
|
||||
const scopes: Array<string> = flow.scopes ? flow.scopes : defaultScopes;
|
||||
return {
|
||||
...flow,
|
||||
scopes: (flow.scopes || []).map((x: string) => ({ value: x })),
|
||||
scopes: scopes.map((x: string) => ({ value: x })),
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
@ -578,9 +583,9 @@ function extractOAuth2Auth(data: any): HttpAuth {
|
|||
|
||||
export function getAuthentication(
|
||||
program: Program,
|
||||
namespace: Namespace
|
||||
): ServiceAuthentication | undefined {
|
||||
return program.stateMap(HttpStateKeys.authentication).get(namespace);
|
||||
entity: Namespace | Interface | Operation
|
||||
): Authentication | undefined {
|
||||
return program.stateMap(HttpStateKeys.authentication).get(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
export { $lib } from "./lib.js";
|
||||
export { $linter } from "./linter.js";
|
||||
|
||||
export * from "./auth.js";
|
||||
export * from "./content-types.js";
|
||||
export * from "./decorators.js";
|
||||
export * from "./metadata.js";
|
||||
|
|
|
@ -15,6 +15,8 @@ import {
|
|||
Program,
|
||||
SyntaxKind,
|
||||
} from "@typespec/compiler";
|
||||
import { getAuthenticationForOperation } from "./auth.js";
|
||||
import { getAuthentication } from "./decorators.js";
|
||||
import { createDiagnostic, reportDiagnostic } from "./lib.js";
|
||||
import { getResponsesForOperation } from "./responses.js";
|
||||
import { isSharedRoute, resolvePathAndParameters } from "./route.js";
|
||||
|
@ -95,6 +97,7 @@ export function getHttpService(
|
|||
},
|
||||
})
|
||||
);
|
||||
const authentication = getAuthentication(program, serviceNamespace);
|
||||
|
||||
validateProgram(program, diagnostics);
|
||||
validateRouteUnique(program, diagnostics, httpOperations);
|
||||
|
@ -102,6 +105,7 @@ export function getHttpService(
|
|||
const service: HttpService = {
|
||||
namespace: serviceNamespace,
|
||||
operations: httpOperations,
|
||||
authentication: authentication,
|
||||
};
|
||||
return diagnostics.wrap(service);
|
||||
}
|
||||
|
@ -213,6 +217,7 @@ function getHttpOperationInternal(
|
|||
resolvePathAndParameters(program, operation, overloading, options ?? {})
|
||||
);
|
||||
const responses = diagnostics.pipe(getResponsesForOperation(program, operation));
|
||||
const authentication = getAuthenticationForOperation(program, operation);
|
||||
|
||||
const httpOperation: HttpOperation = {
|
||||
path: route.path,
|
||||
|
@ -220,8 +225,9 @@ function getHttpOperationInternal(
|
|||
verb: route.parameters.verb,
|
||||
container: operation.interface ?? operation.namespace ?? program.getGlobalNamespaceType(),
|
||||
parameters: route.parameters,
|
||||
operation,
|
||||
responses,
|
||||
operation,
|
||||
authentication,
|
||||
};
|
||||
Object.assign(httpOperationRef, httpOperation);
|
||||
|
||||
|
|
|
@ -16,7 +16,11 @@ export type OperationDetails = HttpOperation;
|
|||
|
||||
export type HttpVerb = "get" | "put" | "post" | "patch" | "delete" | "head";
|
||||
|
||||
export interface ServiceAuthentication {
|
||||
/** @deprecated use Authentication */
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
type ServiceAuthentication = Authentication;
|
||||
|
||||
export interface Authentication {
|
||||
/**
|
||||
* Either one of those options can be used independently to authenticate.
|
||||
*/
|
||||
|
@ -35,7 +39,8 @@ export type HttpAuth =
|
|||
| BearerAuth
|
||||
| ApiKeyAuth<ApiKeyLocation, string>
|
||||
| Oauth2Auth<OAuth2Flow[]>
|
||||
| OpenIDConnectAuth;
|
||||
| OpenIDConnectAuth
|
||||
| NoAuth;
|
||||
|
||||
export interface HttpAuthBase {
|
||||
/**
|
||||
|
@ -184,6 +189,14 @@ export interface OpenIDConnectAuth extends HttpAuthBase {
|
|||
openIdConnectUrl: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This authentication option signifies that API is not secured at all.
|
||||
* It might be useful when overriding authentication on interface of operation level.
|
||||
*/
|
||||
export interface NoAuth extends HttpAuthBase {
|
||||
type: "noAuth";
|
||||
}
|
||||
|
||||
export type OperationContainer = Namespace | Interface;
|
||||
|
||||
export type OperationVerbSelector = (
|
||||
|
@ -287,6 +300,7 @@ export interface HttpOperationParameters {
|
|||
export interface HttpService {
|
||||
namespace: Namespace;
|
||||
operations: HttpOperation[];
|
||||
authentication?: Authentication;
|
||||
}
|
||||
|
||||
export interface HttpOperation {
|
||||
|
@ -325,6 +339,11 @@ export interface HttpOperation {
|
|||
*/
|
||||
operation: Operation;
|
||||
|
||||
/**
|
||||
* Operation authentication. Overrides HttpService authentication
|
||||
*/
|
||||
authentication?: Authentication;
|
||||
|
||||
/**
|
||||
* Overload this operation
|
||||
*/
|
||||
|
|
|
@ -623,20 +623,6 @@ describe("http: decorators", () => {
|
|||
});
|
||||
|
||||
describe("@useAuth", () => {
|
||||
it("emit diagnostics when @useAuth is not used on namespace", async () => {
|
||||
const diagnostics = await runner.diagnose(`
|
||||
@useAuth(BasicAuth) op test(): string;
|
||||
`);
|
||||
|
||||
expectDiagnostics(diagnostics, [
|
||||
{
|
||||
code: "decorator-wrong-target",
|
||||
message:
|
||||
"Cannot apply @useAuth decorator to test since it is not assignable to Namespace",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("emit diagnostics when config is not a model, tuple or union", async () => {
|
||||
const diagnostics = await runner.diagnose(`
|
||||
@useAuth(anOp)
|
||||
|
@ -792,6 +778,60 @@ describe("http: decorators", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("can specify OAuth2 with scopes, which are default for every flow", async () => {
|
||||
const { Foo } = (await runner.compile(`
|
||||
alias MyAuth<T extends string[]> = OAuth2Auth<Flows=[{
|
||||
type: OAuth2FlowType.implicit;
|
||||
authorizationUrl: "https://api.example.com/oauth2/authorize";
|
||||
refreshUrl: "https://api.example.com/oauth2/refresh";
|
||||
}], Scopes=T>;
|
||||
|
||||
@useAuth(MyAuth<["read", "write"]>)
|
||||
@test namespace Foo {}
|
||||
`)) as { Foo: Namespace };
|
||||
|
||||
deepStrictEqual(getAuthentication(runner.program, Foo), {
|
||||
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" }, { value: "write" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("can specify NoAuth", async () => {
|
||||
const { Foo } = (await runner.compile(`
|
||||
@useAuth(NoAuth)
|
||||
@test namespace Foo {}
|
||||
`)) as { Foo: Namespace };
|
||||
|
||||
deepStrictEqual(getAuthentication(runner.program, Foo), {
|
||||
options: [
|
||||
{
|
||||
schemes: [
|
||||
{
|
||||
id: "NoAuth",
|
||||
type: "noAuth",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("can specify multiple auth options", async () => {
|
||||
const { Foo } = (await runner.compile(`
|
||||
@useAuth(BasicAuth | BearerAuth)
|
||||
|
@ -853,6 +893,50 @@ describe("http: decorators", () => {
|
|||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("can override auth schemes on interface", async () => {
|
||||
const { Foo } = (await runner.compile(`
|
||||
alias ServiceKeyAuth = ApiKeyAuth<ApiKeyLocation.header, "X-API-KEY">;
|
||||
@useAuth(ServiceKeyAuth)
|
||||
@test namespace Foo {
|
||||
@useAuth(BasicAuth | BearerAuth)
|
||||
interface Bar { }
|
||||
}
|
||||
`)) as { Foo: Namespace };
|
||||
|
||||
deepStrictEqual(getAuthentication(runner.program, Foo.interfaces.get("Bar")!), {
|
||||
options: [
|
||||
{
|
||||
schemes: [{ id: "BasicAuth", type: "http", scheme: "basic" }],
|
||||
},
|
||||
{
|
||||
schemes: [{ id: "BearerAuth", type: "http", scheme: "bearer" }],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("can override auth schemes on operation", async () => {
|
||||
const { Foo } = (await runner.compile(`
|
||||
alias ServiceKeyAuth = ApiKeyAuth<ApiKeyLocation.header, "X-API-KEY">;
|
||||
@useAuth(ServiceKeyAuth)
|
||||
@test namespace Foo {
|
||||
@useAuth([BasicAuth, BearerAuth])
|
||||
op bar(): void;
|
||||
}
|
||||
`)) as { Foo: Namespace };
|
||||
|
||||
deepStrictEqual(getAuthentication(runner.program, Foo.operations.get("bar")!), {
|
||||
options: [
|
||||
{
|
||||
schemes: [
|
||||
{ id: "BasicAuth", type: "http", scheme: "basic" },
|
||||
{ id: "BearerAuth", type: "http", scheme: "bearer" },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("@visibility", () => {
|
||||
|
|
|
@ -50,8 +50,10 @@ import {
|
|||
import { AssetEmitter, createAssetEmitter, EmitEntity } from "@typespec/compiler/emitter-framework";
|
||||
import {} from "@typespec/compiler/utils";
|
||||
import {
|
||||
Authentication,
|
||||
AuthenticationOptionReference,
|
||||
AuthenticationReference,
|
||||
createMetadataInfo,
|
||||
getAuthentication,
|
||||
getHttpService,
|
||||
getServers,
|
||||
getStatusCodeDescription,
|
||||
|
@ -72,8 +74,8 @@ import {
|
|||
MetadataInfo,
|
||||
QueryParameterOptions,
|
||||
reportIfNoRoutes,
|
||||
resolveAuthentication,
|
||||
resolveRequestVisibility,
|
||||
ServiceAuthentication,
|
||||
Visibility,
|
||||
} from "@typespec/http";
|
||||
import {
|
||||
|
@ -274,7 +276,12 @@ function createOAPIEmitter(
|
|||
}
|
||||
}
|
||||
|
||||
function initializeEmitter(service: Service, version?: string) {
|
||||
function initializeEmitter(
|
||||
service: Service,
|
||||
allHttpAuthentications: HttpAuth[],
|
||||
defaultAuth: AuthenticationReference,
|
||||
version?: string
|
||||
) {
|
||||
currentService = service;
|
||||
metadataInfo = createMetadataInfo(program, {
|
||||
canonicalVisibility: Visibility.Read,
|
||||
|
@ -294,7 +301,8 @@ function createOAPIEmitter(
|
|||
} as any
|
||||
);
|
||||
|
||||
const auth = processAuth(service.type);
|
||||
const securitySchemes = getOpenAPISecuritySchemes(allHttpAuthentications);
|
||||
const security = getOpenAPISecurity(defaultAuth);
|
||||
|
||||
root = {
|
||||
openapi: "3.0.0",
|
||||
|
@ -307,14 +315,14 @@ function createOAPIEmitter(
|
|||
externalDocs: getExternalDocs(program, service.type),
|
||||
tags: [],
|
||||
paths: {},
|
||||
security: auth?.security,
|
||||
security: security.length > 0 ? security : undefined,
|
||||
components: {
|
||||
parameters: {},
|
||||
requestBodies: {},
|
||||
responses: {},
|
||||
schemas: {},
|
||||
examples: {},
|
||||
securitySchemes: auth?.securitySchemes ?? {},
|
||||
securitySchemes: securitySchemes,
|
||||
},
|
||||
};
|
||||
diagnostics = createDiagnosticCollector();
|
||||
|
@ -578,6 +586,7 @@ function createOAPIEmitter(
|
|||
verb: HttpVerb;
|
||||
parameters: HttpOperationParameters;
|
||||
bodies: HttpOperationRequestBody[] | undefined;
|
||||
authentication?: Authentication;
|
||||
responses: Map<string, HttpOperationResponse[]>;
|
||||
operations: Operation[];
|
||||
}
|
||||
|
@ -682,6 +691,7 @@ function createOAPIEmitter(
|
|||
parameters: [],
|
||||
},
|
||||
bodies: undefined,
|
||||
authentication: operations[0].authentication,
|
||||
responses: new Map<string, HttpOperationResponse[]>(),
|
||||
};
|
||||
for (const [paramName, ops] of paramMap) {
|
||||
|
@ -729,16 +739,20 @@ function createOAPIEmitter(
|
|||
service: Service,
|
||||
version?: string
|
||||
): Promise<[OpenAPI3Document, Readonly<Diagnostic[]>] | undefined> {
|
||||
initializeEmitter(service, version);
|
||||
try {
|
||||
const httpService = ignoreDiagnostics(getHttpService(program, service.type));
|
||||
const auth = resolveAuthentication(httpService);
|
||||
|
||||
initializeEmitter(service, auth.schemes, auth.defaultAuth, version);
|
||||
reportIfNoRoutes(program, httpService.operations);
|
||||
|
||||
for (const op of resolveOperations(httpService.operations)) {
|
||||
if ((op as SharedHttpOperation).kind === "shared") {
|
||||
emitSharedOperation(op as SharedHttpOperation);
|
||||
const opAuth = auth.operationsAuth.get((op as SharedHttpOperation).operations[0]);
|
||||
emitSharedOperation(op as SharedHttpOperation, opAuth);
|
||||
} else {
|
||||
emitOperation(op as HttpOperation);
|
||||
const opAuth = auth.operationsAuth.get((op as HttpOperation).operation);
|
||||
emitOperation(op as HttpOperation, opAuth);
|
||||
}
|
||||
}
|
||||
emitParameters();
|
||||
|
@ -781,7 +795,10 @@ function createOAPIEmitter(
|
|||
}
|
||||
}
|
||||
|
||||
function emitSharedOperation(shared: SharedHttpOperation): void {
|
||||
function emitSharedOperation(
|
||||
shared: SharedHttpOperation,
|
||||
authReference?: AuthenticationReference
|
||||
): void {
|
||||
const { path: fullPath, verb: verb, operations: ops } = shared;
|
||||
if (!root.paths[fullPath]) {
|
||||
root.paths[fullPath] = {};
|
||||
|
@ -845,9 +862,12 @@ function createOAPIEmitter(
|
|||
}
|
||||
attachExtensions(program, op, currentEndpoint);
|
||||
}
|
||||
if (authReference) {
|
||||
emitSecurity(authReference);
|
||||
}
|
||||
}
|
||||
|
||||
function emitOperation(operation: HttpOperation): void {
|
||||
function emitOperation(operation: HttpOperation, authReference?: AuthenticationReference): void {
|
||||
const { path: fullPath, operation: op, verb, parameters } = operation;
|
||||
// If path contains a query string, issue msg and don't emit this endpoint
|
||||
if (fullPath.indexOf("?") > 0) {
|
||||
|
@ -877,10 +897,14 @@ function createOAPIEmitter(
|
|||
currentEndpoint.description = getDoc(program, operation.operation);
|
||||
currentEndpoint.parameters = [];
|
||||
currentEndpoint.responses = {};
|
||||
|
||||
const visibility = resolveRequestVisibility(program, operation.operation, verb);
|
||||
emitEndpointParameters(parameters.parameters, visibility);
|
||||
emitRequestBody(parameters.body, visibility);
|
||||
emitResponses(operation.responses);
|
||||
if (authReference) {
|
||||
emitSecurity(authReference);
|
||||
}
|
||||
if (isDeprecated(program, op)) {
|
||||
currentEndpoint.deprecated = true;
|
||||
}
|
||||
|
@ -1599,53 +1623,52 @@ function createOAPIEmitter(
|
|||
}
|
||||
}
|
||||
|
||||
function processAuth(serviceNamespace: Namespace):
|
||||
| {
|
||||
securitySchemes: Record<string, OpenAPI3SecurityScheme>;
|
||||
security: Record<string, string[]>[];
|
||||
function getOpenAPISecuritySchemes(
|
||||
httpAuthentications: HttpAuth[]
|
||||
): Record<string, OpenAPI3SecurityScheme> {
|
||||
const schemes: Record<string, OpenAPI3SecurityScheme> = {};
|
||||
for (const httpAuth of httpAuthentications) {
|
||||
const scheme = getOpenAPI3Scheme(httpAuth);
|
||||
if (scheme) {
|
||||
schemes[httpAuth.id] = scheme;
|
||||
}
|
||||
| undefined {
|
||||
const authentication = getAuthentication(program, serviceNamespace);
|
||||
if (authentication) {
|
||||
return processServiceAuthentication(authentication);
|
||||
}
|
||||
return undefined;
|
||||
return schemes;
|
||||
}
|
||||
|
||||
function processServiceAuthentication(authentication: ServiceAuthentication): {
|
||||
securitySchemes: Record<string, OpenAPI3SecurityScheme>;
|
||||
security: Record<string, string[]>[];
|
||||
} {
|
||||
const oaiSchemes: Record<string, OpenAPI3SecurityScheme> = {};
|
||||
const security: Record<string, string[]>[] = [];
|
||||
for (const option of authentication.options) {
|
||||
const oai3SecurityOption: Record<string, string[]> = {};
|
||||
for (const scheme of option.schemes) {
|
||||
const result = getOpenAPI3Scheme(scheme);
|
||||
if (result) {
|
||||
oaiSchemes[scheme.id] = result.scheme;
|
||||
oai3SecurityOption[scheme.id] = result.scopes;
|
||||
function getOpenAPISecurity(authReference: AuthenticationReference) {
|
||||
const security = authReference.options.map((authOption: AuthenticationOptionReference) => {
|
||||
const securityOption: Record<string, string[]> = {};
|
||||
for (const httpAuthRef of authOption.all) {
|
||||
switch (httpAuthRef.kind) {
|
||||
case "noAuth":
|
||||
// should emit "{}" as a security option https://github.com/OAI/OpenAPI-Specification/issues/14#issuecomment-297457320
|
||||
continue;
|
||||
case "oauth2":
|
||||
securityOption[httpAuthRef.auth.id] = httpAuthRef.scopes;
|
||||
continue;
|
||||
default:
|
||||
securityOption[httpAuthRef.auth.id] = [];
|
||||
}
|
||||
}
|
||||
security.push(oai3SecurityOption);
|
||||
}
|
||||
return { securitySchemes: oaiSchemes, security };
|
||||
return securityOption;
|
||||
});
|
||||
return security;
|
||||
}
|
||||
|
||||
function getOpenAPI3Scheme(
|
||||
auth: HttpAuth
|
||||
): { scheme: OpenAPI3SecurityScheme; scopes: string[] } | undefined {
|
||||
function emitSecurity(authReference: AuthenticationReference) {
|
||||
const security = getOpenAPISecurity(authReference);
|
||||
if (security.length > 0) {
|
||||
currentEndpoint.security = security;
|
||||
}
|
||||
}
|
||||
|
||||
function getOpenAPI3Scheme(auth: HttpAuth): OpenAPI3SecurityScheme | undefined {
|
||||
switch (auth.type) {
|
||||
case "http":
|
||||
return {
|
||||
scheme: { type: "http", scheme: auth.scheme, description: auth.description },
|
||||
scopes: [],
|
||||
};
|
||||
return { type: "http", scheme: auth.scheme, description: auth.description };
|
||||
case "apiKey":
|
||||
return {
|
||||
scheme: { type: "apiKey", in: auth.in, name: auth.name, description: auth.description },
|
||||
scopes: [],
|
||||
};
|
||||
return { type: "apiKey", in: auth.in, name: auth.name, description: auth.description };
|
||||
case "oauth2":
|
||||
const flows: OpenAPI3OAuthFlows = {};
|
||||
const scopes: string[] = [];
|
||||
|
@ -1658,16 +1681,15 @@ function createOAPIEmitter(
|
|||
scopes: Object.fromEntries(flow.scopes.map((x) => [x.value, x.description ?? ""])),
|
||||
};
|
||||
}
|
||||
return { scheme: { type: "oauth2", flows, description: auth.description }, scopes };
|
||||
return { type: "oauth2", flows, description: auth.description };
|
||||
case "openIdConnect":
|
||||
return {
|
||||
scheme: {
|
||||
type: "openIdConnect",
|
||||
openIdConnectUrl: auth.openIdConnectUrl,
|
||||
description: auth.description,
|
||||
},
|
||||
scopes: [],
|
||||
type: "openIdConnect",
|
||||
openIdConnectUrl: auth.openIdConnectUrl,
|
||||
description: auth.description,
|
||||
};
|
||||
case "noAuth":
|
||||
return undefined;
|
||||
default:
|
||||
diagnostics.add(
|
||||
createDiagnostic({
|
||||
|
|
|
@ -662,6 +662,7 @@ export type OpenAPI3Operation = Extensions & {
|
|||
requestBody?: any;
|
||||
parameters: OpenAPI3Parameter[];
|
||||
deprecated?: boolean;
|
||||
security?: Record<string, string[]>[];
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
|
|
|
@ -161,4 +161,144 @@ describe("openapi3: security", () => {
|
|||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("can override security on methods of interface", async () => {
|
||||
const res = await openApiFor(
|
||||
`
|
||||
namespace Test;
|
||||
alias ServiceKeyAuth = ApiKeyAuth<ApiKeyLocation.header, "X-API-KEY">;
|
||||
|
||||
@service
|
||||
@useAuth(ServiceKeyAuth)
|
||||
@route("/my-service")
|
||||
namespace MyService {
|
||||
@route("/file")
|
||||
@useAuth(ServiceKeyAuth | ApiKeyAuth<ApiKeyLocation.query, "token">)
|
||||
interface FileManagement {
|
||||
@route("/download")
|
||||
op download(fileId: string): bytes;
|
||||
}
|
||||
}
|
||||
`
|
||||
);
|
||||
deepStrictEqual(res.components.securitySchemes, {
|
||||
ApiKeyAuth: {
|
||||
in: "header",
|
||||
name: "X-API-KEY",
|
||||
type: "apiKey",
|
||||
},
|
||||
ApiKeyAuth_: {
|
||||
in: "query",
|
||||
name: "token",
|
||||
type: "apiKey",
|
||||
},
|
||||
});
|
||||
deepStrictEqual(res.security, [
|
||||
{
|
||||
ApiKeyAuth: [],
|
||||
},
|
||||
]);
|
||||
deepStrictEqual(res.paths["/my-service/file/download"]["post"].security, [
|
||||
{
|
||||
ApiKeyAuth: [],
|
||||
},
|
||||
{
|
||||
ApiKeyAuth_: [],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("can override security on methods of operation", async () => {
|
||||
const res = await openApiFor(
|
||||
`
|
||||
namespace Test;
|
||||
|
||||
alias ServiceKeyAuth = ApiKeyAuth<ApiKeyLocation.header, "X-API-KEY">;
|
||||
|
||||
@service
|
||||
@useAuth(ServiceKeyAuth)
|
||||
@route("/my-service")
|
||||
namespace MyService {
|
||||
@useAuth(NoAuth | ServiceKeyAuth | ApiKeyAuth<ApiKeyLocation.query, "token">)
|
||||
@route("download")
|
||||
op download(fileId: string): bytes;
|
||||
}
|
||||
`
|
||||
);
|
||||
deepStrictEqual(res.components.securitySchemes, {
|
||||
ApiKeyAuth: {
|
||||
in: "header",
|
||||
name: "X-API-KEY",
|
||||
type: "apiKey",
|
||||
},
|
||||
ApiKeyAuth_: {
|
||||
in: "query",
|
||||
name: "token",
|
||||
type: "apiKey",
|
||||
},
|
||||
});
|
||||
deepStrictEqual(res.security, [
|
||||
{
|
||||
ApiKeyAuth: [],
|
||||
},
|
||||
]);
|
||||
deepStrictEqual(res.paths["/my-service/download"]["post"].security, [
|
||||
{},
|
||||
{
|
||||
ApiKeyAuth: [],
|
||||
},
|
||||
{
|
||||
ApiKeyAuth_: [],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("can override Oauth2 scopes on operation", async () => {
|
||||
const res = await openApiFor(
|
||||
`
|
||||
namespace Test;
|
||||
|
||||
alias MyOauth<T extends string[]> = OAuth2Auth<Flows=[{
|
||||
type: OAuth2FlowType.implicit;
|
||||
authorizationUrl: "https://api.example.com/oauth2/authorize";
|
||||
refreshUrl: "https://api.example.com/oauth2/refresh";
|
||||
}], Scopes=T>;
|
||||
|
||||
@route("/my-service")
|
||||
@useAuth(MyOauth<["read", "write"]>)
|
||||
@service
|
||||
namespace MyService {
|
||||
@route("/delete")
|
||||
@useAuth(MyOauth<["delete"]>)
|
||||
@post op delete(): void;
|
||||
}
|
||||
`
|
||||
);
|
||||
deepStrictEqual(res.components.securitySchemes, {
|
||||
OAuth2Auth: {
|
||||
type: "oauth2",
|
||||
flows: {
|
||||
implicit: {
|
||||
authorizationUrl: "https://api.example.com/oauth2/authorize",
|
||||
refreshUrl: "https://api.example.com/oauth2/refresh",
|
||||
scopes: {
|
||||
read: "",
|
||||
write: "",
|
||||
delete: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
deepStrictEqual(res.security, [
|
||||
{
|
||||
OAuth2Auth: ["read", "write"],
|
||||
},
|
||||
]);
|
||||
deepStrictEqual(res.paths["/my-service/delete"]["post"].security, [
|
||||
{
|
||||
OAuth2Auth: ["delete"],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
import "@typespec/rest";
|
||||
|
||||
using TypeSpec.Http;
|
||||
|
||||
@service({
|
||||
title: "Authenticated service with interface override",
|
||||
})
|
||||
@useAuth(BearerAuth)
|
||||
namespace TypeSpec.InterfaceAuth;
|
||||
|
||||
// requires BearerAuth
|
||||
@route("/one")
|
||||
op one(): void;
|
||||
|
||||
@useAuth(BasicAuth)
|
||||
interface Sample {
|
||||
// requires BasicAuth
|
||||
@route("/two")
|
||||
two(): void;
|
||||
}
|
|
@ -1,24 +1,3 @@
|
|||
import "@typespec/rest";
|
||||
|
||||
using TypeSpec.Http;
|
||||
|
||||
@service({
|
||||
title: "Authenticated service",
|
||||
})
|
||||
@useAuth(
|
||||
// Here authentication can either be a
|
||||
// - ApiKey AND Basic Auth together
|
||||
// - Bearer token
|
||||
// - OAuth2
|
||||
BearerAuth | [ApiKeyAuth<ApiKeyLocation.header, "x-my-header">, BasicAuth] | OAuth2Auth<[MyFlow]>
|
||||
)
|
||||
namespace TypeSpec.Samples;
|
||||
|
||||
model MyFlow {
|
||||
type: OAuth2FlowType.implicit;
|
||||
authorizationUrl: "https://api.example.com/oauth2/authorize";
|
||||
refreshUrl: "https://api.example.com/oauth2/refresh";
|
||||
scopes: ["read", "write"];
|
||||
}
|
||||
|
||||
op test(): string;
|
||||
import "./interface-auth.tsp";
|
||||
import "./operation-auth.tsp";
|
||||
import "./service-auth.tsp";
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
import "@typespec/rest";
|
||||
|
||||
using TypeSpec.Http;
|
||||
|
||||
@service({
|
||||
title: "Authenticated service with method override",
|
||||
})
|
||||
@useAuth(BearerAuth | MyAuth<["read", "write"]>)
|
||||
namespace TypeSpec.OperationAuth;
|
||||
|
||||
alias MyAuth<Scopes extends string[]> = OAuth2Auth<
|
||||
Flows = [
|
||||
{
|
||||
type: OAuth2FlowType.implicit;
|
||||
authorizationUrl: "https://api.example.com/oauth2/authorize";
|
||||
refreshUrl: "https://api.example.com/oauth2/refresh";
|
||||
}
|
||||
],
|
||||
Scopes = Scopes
|
||||
>;
|
||||
|
||||
// requires BearerAuth | MyAuth<["read", "write"]>
|
||||
@route("/one")
|
||||
op one(): void;
|
||||
|
||||
// requires optional ApiKeyAuth
|
||||
@useAuth(NoAuth | ApiKeyAuth<ApiKeyLocation.header, "x-my-header">)
|
||||
@route("/two")
|
||||
op two(): void;
|
||||
|
||||
// requires MyAuth<"delete">
|
||||
@useAuth(MyAuth<["delete"]>)
|
||||
@route("/three")
|
||||
op three(): void;
|
|
@ -0,0 +1,24 @@
|
|||
import "@typespec/rest";
|
||||
|
||||
using TypeSpec.Http;
|
||||
|
||||
@service({
|
||||
title: "Authenticated service",
|
||||
})
|
||||
@useAuth(
|
||||
// Here authentication can either be a
|
||||
// - ApiKey AND Basic Auth together
|
||||
// - Bearer token
|
||||
// - OAuth2
|
||||
BearerAuth | [ApiKeyAuth<ApiKeyLocation.header, "x-my-header">, BasicAuth] | OAuth2Auth<[MyFlow]>
|
||||
)
|
||||
namespace TypeSpec.ServiceAuth;
|
||||
|
||||
model MyFlow {
|
||||
type: OAuth2FlowType.implicit;
|
||||
authorizationUrl: "https://api.example.com/oauth2/authorize";
|
||||
refreshUrl: "https://api.example.com/oauth2/refresh";
|
||||
scopes: ["read", "write"];
|
||||
}
|
||||
|
||||
op test(): string;
|
|
@ -0,0 +1,32 @@
|
|||
openapi: 3.0.0
|
||||
info:
|
||||
title: Authenticated service with interface override
|
||||
version: 0000-00-00
|
||||
tags: []
|
||||
paths:
|
||||
/one:
|
||||
get:
|
||||
operationId: one
|
||||
parameters: []
|
||||
responses:
|
||||
'204':
|
||||
description: 'There is no content to send for this request, but the headers may be useful. '
|
||||
/two:
|
||||
get:
|
||||
operationId: Sample_two
|
||||
parameters: []
|
||||
responses:
|
||||
'204':
|
||||
description: 'There is no content to send for this request, but the headers may be useful. '
|
||||
security:
|
||||
- BasicAuth: []
|
||||
security:
|
||||
- BearerAuth: []
|
||||
components:
|
||||
securitySchemes:
|
||||
BearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
BasicAuth:
|
||||
type: http
|
||||
scheme: basic
|
|
@ -0,0 +1,57 @@
|
|||
openapi: 3.0.0
|
||||
info:
|
||||
title: Authenticated service with method override
|
||||
version: 0000-00-00
|
||||
tags: []
|
||||
paths:
|
||||
/one:
|
||||
get:
|
||||
operationId: one
|
||||
parameters: []
|
||||
responses:
|
||||
'204':
|
||||
description: 'There is no content to send for this request, but the headers may be useful. '
|
||||
/three:
|
||||
get:
|
||||
operationId: three
|
||||
parameters: []
|
||||
responses:
|
||||
'204':
|
||||
description: 'There is no content to send for this request, but the headers may be useful. '
|
||||
security:
|
||||
- OAuth2Auth:
|
||||
- delete
|
||||
/two:
|
||||
get:
|
||||
operationId: two
|
||||
parameters: []
|
||||
responses:
|
||||
'204':
|
||||
description: 'There is no content to send for this request, but the headers may be useful. '
|
||||
security:
|
||||
- {}
|
||||
- ApiKeyAuth: []
|
||||
security:
|
||||
- BearerAuth: []
|
||||
- OAuth2Auth:
|
||||
- read
|
||||
- write
|
||||
components:
|
||||
securitySchemes:
|
||||
BearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
OAuth2Auth:
|
||||
type: oauth2
|
||||
flows:
|
||||
implicit:
|
||||
authorizationUrl: https://api.example.com/oauth2/authorize
|
||||
refreshUrl: https://api.example.com/oauth2/refresh
|
||||
scopes:
|
||||
read: ''
|
||||
write: ''
|
||||
delete: ''
|
||||
ApiKeyAuth:
|
||||
type: apiKey
|
||||
in: header
|
||||
name: x-my-header
|
Загрузка…
Ссылка в новой задаче