1 | import body from "koa-body";
|
2 | import { v4 } from "uuid";
|
3 | import createPlaygroundMiddleware from "graphql-playground-middleware-koa";
|
4 | import Router, { IRouterOptions } from "koa-router";
|
5 | import { errorHandler, execute } from "graphql-api-koa";
|
6 | import { Context as KoaContext, Next as KoaNext } from "koa";
|
7 | import { parse } from "auth-header";
|
8 | import { Pool } from "pg";
|
9 |
|
10 | import x from "./x";
|
11 | import oauth2 from "./oauth2";
|
12 | import { Config, assertConfig } from "./Config";
|
13 | import { Context } from "./Context";
|
14 | import { createSchema } from "./graphql";
|
15 | import { fromBasic, fromBearer } from "./util/getAuthorization";
|
16 | import { StrategyCollection } from "./StrategyCollection";
|
17 | import { UnsupportedMediaTypeError } from "./errors";
|
18 | import { createAuthXExplanations } from "./explanations";
|
19 | import { DataLoaderExecutor } from "./loader";
|
20 |
|
21 | export * from "./x";
|
22 | export * from "./errors";
|
23 | export * from "./loader";
|
24 | export * from "./model";
|
25 | export * from "./graphql";
|
26 | export * from "./Strategy";
|
27 | export * from "./StrategyCollection";
|
28 | export * from "./Config";
|
29 | export * from "./Context";
|
30 | export * from "./util/validateIdFormat";
|
31 |
|
32 |
|
33 |
|
34 |
|
35 | type AuthXMiddleware = any;
|
36 |
|
37 | export 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 |
|
51 | this.pool = new Pool(config.pg);
|
52 |
|
53 |
|
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 |
|
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 |
|
76 |
|
77 |
|
78 | await authorization.invoke(tx, {
|
79 | id: v4(),
|
80 | format: "basic",
|
81 | createdAt: new Date()
|
82 | });
|
83 | }
|
84 |
|
85 |
|
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 |
|
95 |
|
96 | }
|
97 |
|
98 |
|
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 |
|
122 |
|
123 |
|
124 |
|
125 | this.post(
|
126 | "/graphql",
|
127 |
|
128 | errorHandler(),
|
129 |
|
130 | contextMiddleware as AuthXMiddleware,
|
131 |
|
132 |
|
133 |
|
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 |
|
160 |
|
161 |
|
162 | this.all("/graphiql", createPlaygroundMiddleware({ endpoint: "/graphql" }));
|
163 |
|
164 |
|
165 |
|
166 |
|
167 |
|
168 |
|
169 |
|
170 |
|
171 |
|
172 |
|
173 |
|
174 |
|
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 |
|
185 | export default AuthX;
|