UNPKG

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