Skip to content
Merged
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
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

89 changes: 89 additions & 0 deletions src/middleware/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Middlewares for Common Environments

If you want to use Node.js middleware, read main [README.md](../../README.md#createnodemiddlewareapp-options) instead. If you need to implement a handler/middleware for another environment, read this document.

The `middleware` directory contains the generic HTTP handler. Each sub-directory (e.g., [`node`](node)) exposes an HTTP handler/middleware for a specific environment.

```
middleware
├── handle-request.ts
├── on-unhandled-request-default.ts
├── types.ts
├── node/
├── cloudflare/ (to be implemented)
└── deno/ (to be implemented)
```

## Generic HTTP Handler

[`handleRequest`](handle-request.ts) function is an abstract HTTP handler which accepts an `OctokitRequest` and returns an `OctokitResponse` if the request matches any predefined route.

> Different environments (e.g., Node.js, Cloudflare, etc.) exposes different APIs when processing HTTP requests (e.g., [`IncomingMessage`](https://nodejs.org/api/http.html#http_class_http_incomingmessage) for Node.js, [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) for Cloudflare workers, etc.). Two HTTP-related types ([`OctokitRequest` and `OctokitResponse`](./types.ts)) are generalized to make an abstract HTTP handler possible.

To share the behavior and capability with the existing Node.js middleware (and be compatible with [OAuth user authentication strategy in the browser](https://github.com/octokit/auth-oauth-user-client.js)), it is better to implement your HTTP handler/middleware based on `handleRequest` function.

`handleRequest` function takes three parameters:

<table width="100%">
<thead align=left>
<tr>
<th width=150>
name
</th>
<th width=70>
type
</th>
<th>
description
</th>
</tr>
</thead>
<tbody align=left valign=top>
<tr>
<th>
<code>app</code>
</th>
<th>
<code>OAuthApp instance</code>
</th>
<td>
<strong>Required</strong>.
</td>
</tr>
<tr>
<th>
<code>options.pathPrefix</code>
</th>
<th>
<code>string</code>
</th>
<td>

All exposed paths will be prefixed with the provided prefix. Defaults to `"/api/github/oauth"`

</td>
</tr>
<tr>
<th>
<code>request</code>
</th>
<th>
<code>OctokitRequest</code>
</th>
<td>
Generalized HTTP request in `OctokitRequest` type.
</td>
</tr>
</tbody>
</table>

## Adapt for an Environment

Implementing an HTTP handler/middleware for a certain environment involves three steps:

1. Write a function to parse the HTTP request (e.g., `IncomingMessage` in Node.js) into an `OctokitRequest` object. See [`node/parse-request.ts`](node/parse-request.ts) for reference.
2. Write a function to render an `OctokitResponse` object (e.g., as `ServerResponse` in Node.js). See [`node/send-response.ts`](node/send-response.ts) for reference.
3. Expose an HTTP handler/middleware in the dialect of the environment which performs three steps:
1. Parse the HTTP request using (1).
2. Process the `OctokitRequest` object using `handleRequest`. If the request is not handled by `handleRequest` (the request does not match any predefined route), [`onUnhandledRequestDefault`](on-unhandled-request-default.ts) can be used to generate a `404` response consistently.
3. Render the `OctokitResponse` object using (2).
Copy link
Contributor

Choose a reason for hiding this comment

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

thanks for the README, this is great!

178 changes: 87 additions & 91 deletions src/middleware/node/middleware.ts → src/middleware/handle-request.ts
Original file line number Diff line number Diff line change
@@ -1,75 +1,71 @@
// remove type imports from http for Deno compatibility
// see https://github.com/octokit/octokit.js/issues/24#issuecomment-817361886
// import { IncomingMessage, ServerResponse } from "http";
type IncomingMessage = any;
type ServerResponse = any;
import { OAuthApp } from "../index";
import { OctokitRequest, OctokitResponse, HandlerOptions } from "./types";
import { ClientType, Options } from "../types";
// @ts-ignore - requires esModuleInterop flag
import fromEntries from "fromentries";

import { parseRequest } from "./parse-request";

import { OAuthApp } from "../../index";
import { MiddlewareOptions } from "./types";
import { Options, ClientType } from "../../types";

export async function middleware(
export async function handleRequest(
app: OAuthApp<Options<ClientType>>,
options: Required<MiddlewareOptions>,
request: IncomingMessage,
response: ServerResponse,
next?: Function
) {
// request.url mayb include ?query parameters which we don't want for `route`
{ pathPrefix = "/api/github/oauth" }: HandlerOptions,
request: OctokitRequest
): Promise<OctokitResponse | null> {
// request.url may include ?query parameters which we don't want for `route`
// hence the workaround using new URL()
const { pathname } = new URL(request.url as string, "http://localhost");
const route = [request.method, pathname].join(" ");
const routes = {
getLogin: `GET ${options.pathPrefix}/login`,
getCallback: `GET ${options.pathPrefix}/callback`,
createToken: `POST ${options.pathPrefix}/token`,
getToken: `GET ${options.pathPrefix}/token`,
patchToken: `PATCH ${options.pathPrefix}/token`,
patchRefreshToken: `PATCH ${options.pathPrefix}/refresh-token`,
scopeToken: `POST ${options.pathPrefix}/token/scoped`,
deleteToken: `DELETE ${options.pathPrefix}/token`,
deleteGrant: `DELETE ${options.pathPrefix}/grant`,
getLogin: `GET ${pathPrefix}/login`,
getCallback: `GET ${pathPrefix}/callback`,
createToken: `POST ${pathPrefix}/token`,
getToken: `GET ${pathPrefix}/token`,
patchToken: `PATCH ${pathPrefix}/token`,
patchRefreshToken: `PATCH ${pathPrefix}/refresh-token`,
scopeToken: `POST ${pathPrefix}/token/scoped`,
deleteToken: `DELETE ${pathPrefix}/token`,
deleteGrant: `DELETE ${pathPrefix}/grant`,
};

// handle unknown routes
if (!Object.values(routes).includes(route)) {
const isExpressMiddleware = typeof next === "function";
if (isExpressMiddleware) {
// @ts-ignore `next` must be a function as we check two lines above
return next();
} else {
return options.onUnhandledRequest(request, response);
}
return null;
}

let parsedRequest;
let json: any;
try {
parsedRequest = await parseRequest(request);
const text = await request.text();
json = text ? JSON.parse(text) : {};
} catch (error) {
response.writeHead(400, {
"content-type": "application/json",
});
return response.end(
JSON.stringify({
return {
status: 400,
headers: { "content-type": "application/json" },
text: JSON.stringify({
error: "[@octokit/oauth-app] request error",
})
);
}),
};
}
const { headers, query, body = {} } = parsedRequest;
const { searchParams } = new URL(request.url as string, "http://localhost");
const query = fromEntries(searchParams) as {
state?: string;
scopes?: string;
code?: string;
redirectUrl?: string;
allowSignup?: string;
error?: string;
error_description?: string;
error_url?: string;
};
const headers = request.headers as { authorization?: string };

try {
if (route === routes.getLogin) {
const { url } = app.getWebFlowAuthorizationUrl({
state: query.state,
scopes: query.scopes?.split(","),
allowSignup: query.allowSignup,
scopes: query.scopes ? query.scopes.split(",") : undefined,
allowSignup: query.allowSignup !== "false",
redirectUrl: query.redirectUrl,
});

response.writeHead(302, { location: url });
return response.end();
return { status: 302, headers: { location: url } };
}

if (route === routes.getCallback) {
Expand All @@ -91,18 +87,19 @@ export async function middleware(
code: query.code,
});

response.writeHead(200, {
"content-type": "text/html",
});
response.write(`<h1>Token created successfull</h1>
return {
status: 200,
headers: {
"content-type": "text/html",
},
text: `<h1>Token created successfull</h1>

<p>Your token is: <strong>${token}</strong>. Copy it now as it cannot be shown again.</p>`);

return response.end();
<p>Your token is: <strong>${token}</strong>. Copy it now as it cannot be shown again.</p>`,
};
}

if (route === routes.createToken) {
const { state: oauthState, code, redirectUrl } = body;
const { state: oauthState, code, redirectUrl } = json;

if (!oauthState || !code) {
throw new Error(
Expand All @@ -118,11 +115,11 @@ export async function middleware(
redirectUrl,
});

response.writeHead(201, {
"content-type": "application/json",
});

return response.end(JSON.stringify({ token, scopes }));
return {
status: 201,
headers: { "content-type": "application/json" },
text: JSON.stringify({ token, scopes }),
};
}

if (route === routes.getToken) {
Expand All @@ -138,10 +135,11 @@ export async function middleware(
token,
});

response.writeHead(200, {
"content-type": "application/json",
});
return response.end(JSON.stringify(result));
return {
status: 200,
headers: { "content-type": "application/json" },
text: JSON.stringify(result),
};
}

if (route === routes.patchToken) {
Expand All @@ -157,10 +155,11 @@ export async function middleware(
token,
});

response.writeHead(200, {
"content-type": "application/json",
});
return response.end(JSON.stringify(result));
return {
status: 200,
headers: { "content-type": "application/json" },
text: JSON.stringify(result),
};
}

if (route === routes.patchRefreshToken) {
Expand All @@ -172,7 +171,7 @@ export async function middleware(
);
}

const { refreshToken } = body;
const { refreshToken } = json;

if (!refreshToken) {
throw new Error(
Expand All @@ -182,10 +181,11 @@ export async function middleware(

const result = await app.refreshToken({ refreshToken });

response.writeHead(200, {
"content-type": "application/json",
});
return response.end(JSON.stringify(result));
return {
status: 200,
headers: { "content-type": "application/json" },
text: JSON.stringify(result),
};
}

if (route === routes.scopeToken) {
Expand All @@ -199,13 +199,14 @@ export async function middleware(

const result = await app.scopeToken({
token,
...body,
...json,
});

response.writeHead(200, {
"content-type": "application/json",
});
return response.end(JSON.stringify(result));
return {
status: 200,
headers: { "content-type": "application/json" },
text: JSON.stringify(result),
};
}

if (route === routes.deleteToken) {
Expand All @@ -221,8 +222,7 @@ export async function middleware(
token,
});

response.writeHead(204);
return response.end();
return { status: 204 };
}

// route === routes.deleteGrant
Expand All @@ -238,16 +238,12 @@ export async function middleware(
token,
});

response.writeHead(204);
return response.end();
return { status: 204 };
} catch (error) {
response.writeHead(400, {
"content-type": "application/json",
});
response.end(
JSON.stringify({
error: error.message,
})
);
return {
status: 400,
headers: { "content-type": "application/json" },
text: JSON.stringify({ error: error.message }),
};
}
}
Loading