UNPKG

5.76 kBPlain TextView Raw
1import body from "koa-body";
2import { v4 } from "uuid";
3import createPlaygroundMiddleware from "graphql-playground-middleware-koa";
4import Router, { IRouterOptions } from "koa-router";
5import { errorHandler, execute } from "graphql-api-koa";
6import { Context as KoaContext, Next as KoaNext } from "koa";
7import { parse } from "auth-header";
8import { Pool } from "pg";
9
10import x from "./x";
11import oauth2 from "./oauth2";
12import { Config, assertConfig } from "./Config";
13import { Context } from "./Context";
14import { createSchema } from "./graphql";
15import { fromBasic, fromBearer } from "./util/getAuthorization";
16import { StrategyCollection } from "./StrategyCollection";
17import { UnsupportedMediaTypeError } from "./errors";
18import { createAuthXExplanations } from "./explanations";
19import { DataLoaderExecutor } from "./loader";
20
21export * from "./x";
22export * from "./errors";
23export * from "./loader";
24export * from "./model";
25export * from "./graphql";
26export * from "./Strategy";
27export * from "./StrategyCollection";
28export * from "./Config";
29export * from "./Context";
30export * from "./util/validateIdFormat";
31
32// FIXME: The newest types from @types/koa fail to match perfectly valid
33// overloads for chained middleware. I haven't had a chance to really dive into
34// what's going on, so for now we are going to cast through any.
35type AuthXMiddleware = any; // Middleware<any, KoaContext & { [x]: Context }>
36
37export class AuthX extends Router<any, { [x]: Context }> {
38 public readonly pool: Pool;
39 public constructor(config: Config & IRouterOptions) {
40 assertConfig(config);
41 super(config);
42
43 const explanations = createAuthXExplanations({ [config.realm]: "AuthX" });
44
45 const strategies =
46 config.strategies instanceof StrategyCollection
47 ? config.strategies
48 : new StrategyCollection(config.strategies);
49
50 // create a database pool
51 this.pool = new Pool(config.pg);
52
53 // define the context middleware
54 const contextMiddleware = async (
55 ctx: KoaContext & { [x]: Context },
56 next: KoaNext
57 ): Promise<void> => {
58 const tx = await this.pool.connect();
59 try {
60 let authorization = null;
61
62 const auth = ctx.request.header.authorization
63 ? parse(ctx.request.header.authorization)
64 : null;
65
66 // HTTP Basic Authorization
67 const basic =
68 auth && auth.scheme === "Basic" && typeof auth.token === "string"
69 ? auth.token
70 : null;
71
72 if (basic) {
73 authorization = await fromBasic(tx, basic);
74
75 // Invoke the authorization. Because the resource validates basic
76 // tokens by making a GraphQL request here, each request can be
77 // considered an invocation.
78 await authorization.invoke(tx, {
79 id: v4(),
80 format: "basic",
81 createdAt: new Date()
82 });
83 }
84
85 // Bearer Token Authorization
86 const bearer =
87 auth && auth.scheme === "Bearer" && typeof auth.token === "string"
88 ? auth.token
89 : null;
90
91 if (bearer) {
92 authorization = await fromBearer(tx, config.publicKeys, bearer);
93
94 // There is no need to invoke this authorization here, since it was
95 // invoked when the bearer token was generated.
96 }
97
98 // An authorization header exists, but did not match a known format.
99 if (ctx.request.header.authorization && !authorization) {
100 throw new Error(
101 "An authorization header must be of either HTTP Basic or Bearer format."
102 );
103 }
104
105 const context: Context = {
106 ...ctx[x],
107 ...config,
108 authorization,
109 explanations: explanations,
110 executor: new DataLoaderExecutor(this.pool, strategies)
111 };
112
113 ctx[x] = context;
114 } finally {
115 tx.release();
116 }
117
118 await next();
119 };
120
121 // GraphQL
122 // =======
123 // The GraphQL endpoint is the primary API for interacting with AuthX.
124
125 this.post(
126 "/graphql",
127
128 errorHandler(),
129
130 contextMiddleware as AuthXMiddleware,
131
132 // The GraphQL endpoint only accepts JSON. This helps protect against CSRF
133 // attacks that send urlenceded data via HTML forms.
134 async (ctx, next) => {
135 if (!ctx.is("json"))
136 throw new UnsupportedMediaTypeError(
137 "Requests to the AuthX GraphQL endpoint MUST specify a Content-Type of `application/json`."
138 );
139
140 await next();
141 },
142
143 body({ multipart: false, urlencoded: false, text: false, json: true }),
144
145 execute({
146 schema: config.processSchema
147 ? (config.processSchema(createSchema(strategies)) as any)
148 : (createSchema(strategies) as any),
149 override: (ctx: any) => {
150 const contextValue: Context = ctx[x];
151
152 return {
153 contextValue
154 };
155 }
156 })
157 );
158
159 // GraphiQL
160 // ========
161 // This is a graphical (get it, graph-i-QL) interface to the AuthX API.
162 this.all("/graphiql", createPlaygroundMiddleware({ endpoint: "/graphql" }));
163
164 // OAuth
165 // =====
166 // The core AuthX library supports the following OAuth2 grant types:
167 //
168 // - `authorization_code`
169 // - `refresh_token`
170 //
171 // Because it involves presentation elements, the core AuthX library does
172 // **not** implement the `code` grant type. Instead, a compatible reference
173 // implementation of this flow is provided by the `authx-interface` NPM
174 // package.
175
176 this.post(
177 "/",
178 contextMiddleware as AuthXMiddleware,
179 body({ multipart: false, urlencoded: true, text: false, json: true }),
180 oauth2 as AuthXMiddleware
181 );
182 }
183}
184
185export default AuthX;