Skip to content

Commit

Permalink
Add support for openIdConnect auth scheme (microsoft#2811)
Browse files Browse the repository at this point in the history
  • Loading branch information
timotheeguerin authored Feb 5, 2024
1 parent fd4fdfb commit 8ed1d82
Show file tree
Hide file tree
Showing 11 changed files with 132 additions and 19 deletions.
5 changes: 5 additions & 0 deletions .changeset/light-countries-cross.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@typespec/http": patch
---

Fix: OpenIDConnect types not exposed on the TypeScript types
5 changes: 5 additions & 0 deletions .changeset/shiny-crabs-guess.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@typespec/openapi3": patch
---

Add support for OpenIdConnect auth scheme
1 change: 1 addition & 0 deletions cspell.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ words:
- nostdlib
- npmjs
- oapi
- OIDC
- oneof
- onig
- onigasm
Expand Down
21 changes: 21 additions & 0 deletions docs/libraries/http/reference/data-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,27 @@ The request has succeeded.
model TypeSpec.Http.OkResponse
```

### `OpenIdConnectAuth` {#TypeSpec.Http.OpenIdConnectAuth}

OpenID Connect (OIDC) is an identity layer built on top of the OAuth 2.0 protocol and supported by some OAuth 2.0 providers, such as Google and Azure Active Directory.
It defines a sign-in flow that enables a client application to authenticate a user, and to obtain information (or "claims") about that user, such as the user name, email, and so on.
User identity information is encoded in a secure JSON Web Token (JWT), called ID token.
OpenID Connect defines a discovery mechanism, called OpenID Connect Discovery, where an OpenID server publishes its metadata at a well-known URL, typically

```http
https://server.com/.well-known/openid-configuration
```

```typespec
model TypeSpec.Http.OpenIdConnectAuth<ConnectUrl>
```

#### Template Parameters

| Name | Description |
| ---------- | ----------- |
| ConnectUrl | |

### `PasswordFlow` {#TypeSpec.Http.PasswordFlow}

Resource Owner Password flow
Expand Down
1 change: 1 addition & 0 deletions docs/libraries/http/reference/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ npm install --save-peer @typespec/http
- [`NotModifiedResponse`](./data-types.md#TypeSpec.Http.NotModifiedResponse)
- [`OAuth2Auth`](./data-types.md#TypeSpec.Http.OAuth2Auth)
- [`OkResponse`](./data-types.md#TypeSpec.Http.OkResponse)
- [`OpenIdConnectAuth`](./data-types.md#TypeSpec.Http.OpenIdConnectAuth)
- [`PasswordFlow`](./data-types.md#TypeSpec.Http.PasswordFlow)
- [`PlainData`](./data-types.md#TypeSpec.Http.PlainData)
- [`QueryOptions`](./data-types.md#TypeSpec.Http.QueryOptions)
Expand Down
18 changes: 18 additions & 0 deletions packages/http/lib/auth.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -194,3 +194,21 @@ model ClientCredentialsFlow {
@doc("list of scopes for the credential")
scopes: string[];
}

/**
* OpenID Connect (OIDC) is an identity layer built on top of the OAuth 2.0 protocol and supported by some OAuth 2.0 providers, such as Google and Azure Active Directory.
* It defines a sign-in flow that enables a client application to authenticate a user, and to obtain information (or "claims") about that user, such as the user name, email, and so on.
* User identity information is encoded in a secure JSON Web Token (JWT), called ID token.
* OpenID Connect defines a discovery mechanism, called OpenID Connect Discovery, where an OpenID server publishes its metadata at a well-known URL, typically
*
* ```http
* https://server.com/.well-known/openid-configuration
* ```
*/
model OpenIdConnectAuth<ConnectUrl extends valueof string> {
/** Auth type */
type: AuthType.openIdConnect;

/** Connect url. It can be specified relative to the server URL */
openIdConnectUrl: ConnectUrl;
}
1 change: 1 addition & 0 deletions packages/http/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"watch": "tsc -p . --watch",
"lint-typespec-library": "tsp compile . --warn-as-error --import @typespec/library-linter --no-emit",
"test": "vitest run",
"test:watch": "vitest -w",
"test:ui": "vitest --ui",
"test-official": "vitest run --coverage --reporter=junit --reporter=default --no-file-parallelism",
"lint": "eslint . --ext .ts --max-warnings=0",
Expand Down
18 changes: 17 additions & 1 deletion packages/http/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ export type HttpAuth =
| BasicAuth
| BearerAuth
| ApiKeyAuth<ApiKeyLocation, string>
| Oauth2Auth<OAuth2Flow[]>;
| Oauth2Auth<OAuth2Flow[]>
| OpenIDConnectAuth;

export interface HttpAuthBase {
/**
Expand Down Expand Up @@ -168,6 +169,21 @@ export interface OAuth2Scope {
description?: string;
}

/**
* OpenID Connect (OIDC) is an identity layer built on top of the OAuth 2.0 protocol and supported by some OAuth 2.0 providers, such as Google and Azure Active Directory.
* It defines a sign-in flow that enables a client application to authenticate a user, and to obtain information (or "claims") about that user, such as the user name, email, and so on.
* User identity information is encoded in a secure JSON Web Token (JWT), called ID token.
* OpenID Connect defines a discovery mechanism, called OpenID Connect Discovery, where an OpenID server publishes its metadata at a well-known URL, typically
*
* ```http
* https://server.com/.well-known/openid-configuration
* ```
*/
export interface OpenIDConnectAuth extends HttpAuthBase {
type: "openIdConnect";
openIdConnectUrl: string;
}

export type OperationContainer = Namespace | Interface;

export type OperationVerbSelector = (
Expand Down
6 changes: 6 additions & 0 deletions packages/openapi3/src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,12 @@ export const libDef = {
default: paramMessage`Status code range '${"start"} to '${"end"}' is not supported. OpenAPI 3.0 can only represent range 1XX, 2XX, 3XX, 4XX and 5XX. Example: \`@minValue(400) @maxValue(499)\` for 4XX.`,
},
},
"unsupported-auth": {
severity: "warning",
messages: {
default: paramMessage`Authentication "${"authType"}" is not a known authentication by the openapi3 emitter, it will be ignored.`,
},
},
},
emitter: {
options: EmitterOptionsSchema as JSONSchemaType<OpenAPI3EmitterOptions>,
Expand Down
55 changes: 38 additions & 17 deletions packages/openapi3/src/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,9 +169,9 @@ function createOAPIEmitter(
let schemaEmitter: AssetEmitter<OpenAPI3Schema, OpenAPI3EmitterOptions>;

let root: OpenAPI3Document;

let currentService: Service;
// Get the service namespace string for use in name shortening
let serviceNamespace: string | undefined;
let serviceNamespaceName: string | undefined;
let currentPath: any;
let currentEndpoint: OpenAPI3Operation;

Expand All @@ -194,13 +194,14 @@ function createOAPIEmitter(
// shorten type names by removing TypeSpec and service namespace
namespaceFilter(ns) {
const name = getNamespaceFullName(ns);
return name !== serviceNamespace;
return name !== serviceNamespaceName;
},
};

return { emitOpenAPI };

function initializeEmitter(service: Service, version?: string) {
currentService = service;
metadataInfo = createMetadataInfo(program, {
canonicalVisibility: Visibility.Read,
canShareProperty: (p) => isReadonlyProperty(program, p),
Expand Down Expand Up @@ -247,7 +248,7 @@ function createOAPIEmitter(
root.servers = resolveServers(servers);
}

serviceNamespace = getNamespaceFullName(service.type);
serviceNamespaceName = getNamespaceFullName(service.type);
currentPath = root.paths;

params = new Map();
Expand Down Expand Up @@ -957,7 +958,7 @@ function createOAPIEmitter(
}
contentType = contentType === "application/json" ? undefined : contentType;
return schemaEmitter.emitType(type, {
referenceContext: { visibility, serviceNamespaceName: serviceNamespace, contentType },
referenceContext: { visibility, serviceNamespaceName: serviceNamespaceName, contentType },
}) as any;
}

Expand Down Expand Up @@ -1538,24 +1539,31 @@ function createOAPIEmitter(
for (const option of authentication.options) {
const oai3SecurityOption: Record<string, string[]> = {};
for (const scheme of option.schemes) {
const [oaiScheme, scopes] = getOpenAPI3Scheme(scheme);
oaiSchemes[scheme.id] = oaiScheme;
oai3SecurityOption[scheme.id] = scopes;
const result = getOpenAPI3Scheme(scheme);
if (result) {
oaiSchemes[scheme.id] = result.scheme;
oai3SecurityOption[scheme.id] = result.scopes;
}
}
security.push(oai3SecurityOption);
}
return { securitySchemes: oaiSchemes, security };
}

function getOpenAPI3Scheme(auth: HttpAuth): [OpenAPI3SecurityScheme, string[]] {
function getOpenAPI3Scheme(
auth: HttpAuth
): { scheme: OpenAPI3SecurityScheme; scopes: string[] } | undefined {
switch (auth.type) {
case "http":
return [{ type: "http", scheme: auth.scheme, description: auth.description }, []];
return {
scheme: { type: "http", scheme: auth.scheme, description: auth.description },
scopes: [],
};
case "apiKey":
return [
{ type: "apiKey", in: auth.in, name: auth.name, description: auth.description },
[],
];
return {
scheme: { type: "apiKey", in: auth.in, name: auth.name, description: auth.description },
scopes: [],
};
case "oauth2":
const flows: OpenAPI3OAuthFlows = {};
const scopes: string[] = [];
Expand All @@ -1568,10 +1576,23 @@ function createOAPIEmitter(
scopes: Object.fromEntries(flow.scopes.map((x) => [x.value, x.description ?? ""])),
};
}
return [{ type: "oauth2", flows, description: auth.description }, scopes];
return { scheme: { type: "oauth2", flows, description: auth.description }, scopes };
case "openIdConnect":
return {
scheme: {
type: "openIdConnect",
openIdConnectUrl: auth.openIdConnectUrl,
description: auth.description,
},
scopes: [],
};
default:
const _assertNever: never = auth;
compilerAssert(false, "Unreachable");
reportDiagnostic(program, {
code: "unsupported-auth",
format: { authType: (auth as any).type },
target: currentService.type,
});
return undefined;
}
}
}
Expand Down
20 changes: 19 additions & 1 deletion packages/openapi3/test/security.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { deepStrictEqual } from "assert";
import { describe, it } from "vitest";
import { describe, expect, it } from "vitest";
import { openApiFor } from "./test-host.js";

describe("openapi3: security", () => {
Expand Down Expand Up @@ -89,6 +89,24 @@ describe("openapi3: security", () => {
deepStrictEqual(res.security, [{ OAuth2Auth: ["read", "write"] }]);
});

it("set openId auth", async () => {
const res = await openApiFor(
`
@service
@useAuth(OpenIdConnectAuth<"https://api.example.com/openid">)
namespace MyService {}
`
);
expect(res.components.securitySchemes).toEqual({
OpenIdConnectAuth: {
type: "openIdConnect",
openIdConnectUrl: "https://api.example.com/openid",
description: expect.stringMatching(/^OpenID Connect/),
},
});
deepStrictEqual(res.security, [{ OpenIdConnectAuth: [] }]);
});

it("can specify custom auth name with description", async () => {
const res = await openApiFor(
`
Expand Down

0 comments on commit 8ed1d82

Please sign in to comment.