UNPKG

26.6 kBPlain TextView Raw
1import { v4 } from "uuid";
2import { Pool } from "pg";
3import jwt from "jsonwebtoken";
4import { randomBytes } from "crypto";
5import { Context } from "./Context";
6import { Client, Grant, Authorization } from "./model";
7import { NotFoundError } from "./errors";
8import { createV2AuthXScope } from "./util/scopes";
9import { DataLoaderExecutor } from "./loader";
10import { inject, isEqual, isValidScopeTemplate } from "@authx/scopes";
11import { Context as KoaContext } from "koa";
12import x from "./x";
13
14async function assertPermissions(
15 realm: string,
16 executor: DataLoaderExecutor,
17 grant: Grant,
18 values: {
19 currentUserId: string | null;
20 currentGrantId: string | null;
21 currentClientId: string | null;
22 currentAuthorizationId: string | null;
23 }
24): Promise<void> {
25 if (
26 // Check that we have every relevant user scope:
27 !(await grant.can(
28 executor,
29 values,
30 createV2AuthXScope(
31 realm,
32 {
33 type: "user",
34 userId: grant.userId
35 },
36 {
37 basic: "r"
38 }
39 )
40 )) ||
41 // Check that we have every relevant grant scope:
42 !(await grant.can(
43 executor,
44 values,
45 createV2AuthXScope(
46 realm,
47 {
48 type: "grant",
49 clientId: grant.clientId,
50 grantId: grant.id,
51 userId: grant.userId
52 },
53 {
54 basic: "r",
55 scopes: "",
56 secrets: ""
57 }
58 )
59 )) ||
60 !(await grant.can(
61 executor,
62 values,
63 createV2AuthXScope(
64 realm,
65 {
66 type: "grant",
67 clientId: grant.clientId,
68 grantId: grant.id,
69 userId: grant.userId
70 },
71 {
72 basic: "r",
73 scopes: "",
74 secrets: "r"
75 }
76 )
77 )) ||
78 !(await grant.can(
79 executor,
80 values,
81 createV2AuthXScope(
82 realm,
83 {
84 type: "grant",
85 clientId: grant.clientId,
86 grantId: grant.id,
87 userId: grant.userId
88 },
89 {
90 basic: "r",
91 scopes: "r",
92 secrets: ""
93 }
94 )
95 )) ||
96 !(await grant.can(
97 executor,
98 values,
99 createV2AuthXScope(
100 realm,
101 {
102 type: "grant",
103 clientId: grant.clientId,
104 grantId: grant.id,
105 userId: grant.userId
106 },
107 {
108 basic: "w",
109 scopes: "",
110 secrets: "w"
111 }
112 )
113 )) ||
114 // Check that we have every relevant authorization scope:
115 !(await grant.can(
116 executor,
117 values,
118 createV2AuthXScope(
119 realm,
120 {
121 type: "authorization",
122 authorizationId: "*",
123 clientId: grant.clientId,
124 grantId: grant.id,
125 userId: grant.userId
126 },
127 {
128 basic: "r",
129 scopes: "",
130 secrets: ""
131 }
132 )
133 )) ||
134 !(await grant.can(
135 executor,
136 values,
137 createV2AuthXScope(
138 realm,
139 {
140 type: "authorization",
141 authorizationId: "*",
142 clientId: grant.clientId,
143 grantId: grant.id,
144 userId: grant.userId
145 },
146 {
147 basic: "r",
148 scopes: "r",
149 secrets: ""
150 }
151 )
152 )) ||
153 !(await grant.can(
154 executor,
155 values,
156 createV2AuthXScope(
157 realm,
158 {
159 type: "authorization",
160 authorizationId: "*",
161 clientId: grant.clientId,
162 grantId: grant.id,
163 userId: grant.userId
164 },
165 {
166 basic: "r",
167 scopes: "",
168 secrets: "r"
169 }
170 )
171 )) ||
172 !(await grant.can(
173 executor,
174 values,
175 createV2AuthXScope(
176 realm,
177 {
178 type: "authorization",
179 authorizationId: "*",
180 clientId: grant.clientId,
181 grantId: grant.id,
182 userId: grant.userId
183 },
184 {
185 basic: "w",
186 scopes: "",
187 secrets: ""
188 }
189 )
190 )) ||
191 !(await grant.can(
192 executor,
193 values,
194 createV2AuthXScope(
195 realm,
196 {
197 type: "authorization",
198 authorizationId: "*",
199 clientId: grant.clientId,
200 grantId: grant.id,
201 userId: grant.userId
202 },
203 {
204 basic: "w",
205 scopes: "w",
206 secrets: ""
207 }
208 )
209 )) ||
210 !(await grant.can(
211 executor,
212 values,
213 createV2AuthXScope(
214 realm,
215 {
216 type: "authorization",
217 authorizationId: "*",
218 clientId: grant.clientId,
219 grantId: grant.id,
220 userId: grant.userId
221 },
222 {
223 basic: "w",
224 scopes: "",
225 secrets: "w"
226 }
227 )
228 ))
229 ) {
230 throw new OAuthError(
231 "invalid_grant",
232 "The grant contains insufficient permission for OAuth.",
233 undefined,
234 grant.clientId,
235 403
236 );
237 }
238}
239
240class OAuthError extends Error {
241 public code: string;
242 public uri: null | string;
243 public clientId?: string;
244 public statusCode: number;
245
246 public constructor(
247 code: string,
248 message?: string,
249 uri?: string,
250 clientId?: string,
251 statusCode: number = 400
252 ) {
253 super(typeof message === "undefined" ? code : message);
254 this.code = code;
255 this.uri = uri || null;
256 this.clientId = clientId;
257 this.statusCode = statusCode;
258 }
259}
260
261// Loop through an iterable of grant secrets and select the secret that was most
262// recently issued
263function getRefreshToken(secrets: Iterable<string>): null | string {
264 let refreshToken = null;
265 let max = 0;
266 for (const secret of secrets) {
267 const issuedString = Buffer.from(secret, "base64")
268 .toString("utf8")
269 .split(":")[1];
270
271 const issuedNumber = parseInt(issuedString);
272 if (issuedString && issuedNumber && issuedNumber > max) {
273 refreshToken = secret;
274 max = issuedNumber;
275 }
276 }
277
278 return refreshToken;
279}
280
281async function oAuth2Middleware(
282 ctx: KoaContext & { [x]: Context }
283): Promise<void> {
284 ctx.response.set("Cache-Control", "no-store");
285 ctx.response.set("Pragma", "no-cache");
286
287 const {
288 executor,
289 realm,
290 jwtValidityDuration,
291 codeValidityDuration,
292 privateKey
293 } = ctx[x];
294
295 const strategies = executor.strategies;
296 const pool = executor.connection;
297 if (!(pool instanceof Pool)) {
298 throw new Error(
299 "INVARIANT: The executor connection is expected to be an instance of Pool."
300 );
301 }
302
303 try {
304 const tx = await pool.connect();
305 try {
306 // Make sure this transaction is used for queries made by the executor.
307 const executor = new DataLoaderExecutor(tx, strategies);
308
309 // Make sure the body is an object.
310 const request: {
311 grant_type?: unknown;
312 client_id?: unknown;
313 client_secret?: unknown;
314 code?: unknown;
315 nonce?: unknown;
316 refresh_token?: unknown;
317 scope?: unknown;
318 } = (ctx.request as any).body;
319 if (!request || typeof request !== "object") {
320 throw new OAuthError(
321 "invalid_request",
322 "The request body must be a JSON object."
323 );
324 }
325
326 const grantType: undefined | string =
327 typeof request.grant_type === "string" ? request.grant_type : undefined;
328
329 // Authorization Code
330 // ==================
331 if (grantType === "authorization_code") {
332 try {
333 tx.query("BEGIN DEFERRABLE");
334 const now = Math.floor(Date.now() / 1000);
335 const paramsClientId: undefined | string =
336 typeof request.client_id === "string"
337 ? request.client_id
338 : undefined;
339 const paramsClientSecret: undefined | string =
340 typeof request.client_secret === "string"
341 ? request.client_secret
342 : undefined;
343 const paramsCode: undefined | string =
344 typeof request.code === "string" ? request.code : undefined;
345 const paramsNonce: undefined | string =
346 typeof request.nonce === "string" ? request.nonce : undefined;
347
348 if (!paramsClientId || !paramsClientSecret || !paramsCode) {
349 throw new OAuthError(
350 "invalid_request",
351 "The request body must include fields `client_id`, `client_secret`, and `code`.",
352 undefined,
353 paramsClientId
354 );
355 }
356
357 // Authenticate the client with its secret.
358 let client;
359 try {
360 client = await Client.read(executor, paramsClientId);
361 } catch (error) {
362 if (!(error instanceof NotFoundError)) throw error;
363 throw new OAuthError(
364 "invalid_client",
365 undefined,
366 undefined,
367 paramsClientId
368 );
369 }
370
371 if (!client.secrets.has(paramsClientSecret)) {
372 throw new OAuthError(
373 "invalid_client",
374 undefined,
375 undefined,
376 paramsClientId
377 );
378 }
379
380 if (!client.enabled) {
381 throw new OAuthError(
382 "unauthorized_client",
383 undefined,
384 undefined,
385 paramsClientId
386 );
387 }
388
389 // Invoke the client.
390 await client.invoke(executor, {
391 id: v4(),
392 createdAt: new Date()
393 });
394
395 // Decode and validate the authorization code.
396 const [grantId, issuedAt, nonce] = Buffer.from(paramsCode, "base64")
397 .toString("utf8")
398 .split(":");
399
400 if (!grantId || !issuedAt || !nonce) {
401 throw new OAuthError(
402 "invalid_grant",
403 "The authorization code is malformed.",
404 undefined,
405 paramsClientId
406 );
407 }
408
409 if (parseInt(issuedAt, 10) + codeValidityDuration < now) {
410 throw new OAuthError(
411 "invalid_grant",
412 "The authorization code is expired.",
413 undefined,
414 paramsClientId
415 );
416 }
417
418 // Fetch the grant.
419 let grant;
420 try {
421 grant = await Grant.read(executor, grantId);
422 } catch (error) {
423 if (!(error instanceof NotFoundError)) throw error;
424 throw new OAuthError(
425 "invalid_grant",
426 "The authorization code is invalid.",
427 undefined,
428 paramsClientId
429 );
430 }
431
432 if (!grant.enabled) {
433 throw new OAuthError(
434 "invalid_grant",
435 "The authorization code is invalid.",
436 undefined,
437 paramsClientId
438 );
439 }
440
441 if (!grant.codes.has(paramsCode)) {
442 throw new OAuthError(
443 "invalid_grant",
444 "The authorization code is invalid.",
445 undefined,
446 paramsClientId
447 );
448 }
449
450 // Invoke the grant.
451 await grant.invoke(executor, {
452 id: v4(),
453 createdAt: new Date()
454 });
455
456 // Fetch the user.
457 const user = await grant.user(executor);
458 if (!user.enabled) {
459 throw new OAuthError(
460 "invalid_grant",
461 "The authorization code is invalid.",
462 undefined,
463 paramsClientId
464 );
465 }
466
467 const requestedScopes = grant.scopes;
468
469 const values = {
470 currentUserId: grant.userId,
471 currentGrantId: grant.id,
472 currentClientId: grant.clientId,
473 currentAuthorizationId: null
474 };
475
476 // Make sure we have the necessary access.
477 await assertPermissions(realm, executor, grant, values);
478
479 // Get all enabled authorizations of this grant.
480 const authorizations = (await grant.authorizations(executor)).filter(
481 t => t.enabled
482 );
483
484 // Look for an existing active authorization for this grant with all
485 // grant scopes.
486 const possibleRootAuthorizations = authorizations.filter(t =>
487 isEqual("**:**:**", t.scopes)
488 );
489
490 let rootAuthorization: Authorization;
491 if (possibleRootAuthorizations.length) {
492 // Use an existing authorization.
493 ctx[x].authorization = rootAuthorization =
494 possibleRootAuthorizations[0];
495 } else {
496 // Create a new authorization.
497 const authorizationId = v4();
498 ctx[
499 x
500 ].authorization = rootAuthorization = await Authorization.write(
501 tx,
502 {
503 id: authorizationId,
504 enabled: true,
505 userId: user.id,
506 grantId: grant.id,
507 secret: randomBytes(16).toString("hex"),
508 scopes: ["**:**:**"]
509 },
510 {
511 recordId: v4(),
512 createdByAuthorizationId: authorizationId,
513 createdByCredentialId: null,
514 createdAt: new Date()
515 }
516 );
517 }
518
519 // Look for an existing active authorization for this grant with the
520 // requested scopes.
521 const possibleRequestedAuthorizations = authorizations.filter(t =>
522 isEqual(requestedScopes, t.scopes)
523 );
524
525 let requestedAuthorization: Authorization;
526 if (possibleRequestedAuthorizations.length) {
527 // Use an existing authorization.
528 requestedAuthorization = possibleRequestedAuthorizations[0];
529 } else {
530 // Create a new authorization.
531 requestedAuthorization = await Authorization.write(
532 tx,
533 {
534 id: v4(),
535 enabled: true,
536 userId: user.id,
537 grantId: grant.id,
538 secret: randomBytes(16).toString("hex"),
539 scopes: requestedScopes
540 },
541 {
542 recordId: v4(),
543 createdByAuthorizationId: rootAuthorization.id,
544 createdByCredentialId: null,
545 createdAt: new Date()
546 }
547 );
548 }
549
550 // Remove the authorization code we used, and prune any others that
551 // have expired.
552 const codes = [...grant.codes].filter(code => {
553 const issued = Buffer.from(code, "base64")
554 .toString("utf8")
555 .split(":")[1];
556 return (
557 code !== paramsCode &&
558 issued &&
559 parseInt(issued) + codeValidityDuration > now
560 );
561 });
562
563 grant = await Grant.write(
564 tx,
565 {
566 ...grant,
567 codes
568 },
569 {
570 recordId: v4(),
571 createdByAuthorizationId: rootAuthorization.id,
572 createdAt: new Date()
573 }
574 );
575
576 const scopes = await requestedAuthorization.access(executor);
577 const tokenId = v4();
578 await requestedAuthorization.invoke(executor, {
579 id: tokenId,
580 format: "bearer",
581 createdAt: new Date()
582 });
583
584 const body = {
585 /* eslint-disable camelcase */
586 token_type: "bearer",
587 access_token: jwt.sign(
588 {
589 aid: requestedAuthorization.id,
590 scopes,
591 nonce: paramsNonce
592 },
593 privateKey,
594 {
595 jwtid: tokenId,
596 algorithm: "RS512",
597 expiresIn: jwtValidityDuration,
598 audience: client.id,
599 subject: user.id,
600 issuer: realm
601 }
602 ),
603 refresh_token: getRefreshToken(grant.secrets),
604 expires_in: jwtValidityDuration,
605 scope: scopes.join(" ")
606 /* eslint-enable camelcase */
607 };
608
609 await tx.query("COMMIT");
610
611 ctx.response.body = body;
612 return;
613 } catch (error) {
614 await tx.query("ROLLBACK");
615 throw error;
616 }
617 }
618
619 // Refresh Token
620 // =============
621 if (grantType === "refresh_token") {
622 try {
623 tx.query("BEGIN DEFERRABLE");
624 const paramsClientId: undefined | string =
625 typeof request.client_id === "string"
626 ? request.client_id
627 : undefined;
628 const paramsClientSecret: undefined | string =
629 typeof request.client_secret === "string"
630 ? request.client_secret
631 : undefined;
632 const paramsRefreshToken: undefined | string =
633 typeof request.refresh_token === "string"
634 ? request.refresh_token
635 : undefined;
636 const paramsScope: undefined | string =
637 typeof request.scope === "string" ? request.scope : undefined;
638 const paramsNonce: undefined | string =
639 typeof request.nonce === "string" ? request.nonce : undefined;
640 if (!paramsClientId || !paramsClientSecret || !paramsRefreshToken) {
641 throw new OAuthError(
642 "invalid_request",
643 "The request body must include fields `client_id`, `client_secret`, and `refresh_token`.",
644 undefined,
645 paramsClientId
646 );
647 }
648
649 const requestedScopeTemplates = paramsScope
650 ? paramsScope.split(" ")
651 : [];
652 if (
653 paramsScope &&
654 !requestedScopeTemplates.every(isValidScopeTemplate)
655 ) {
656 throw new OAuthError(
657 "invalid_scope",
658 undefined,
659 undefined,
660 paramsClientId
661 );
662 }
663
664 // Authenticate the client with its secret.
665 let client;
666 try {
667 client = await Client.read(executor, paramsClientId);
668 } catch (error) {
669 if (!(error instanceof NotFoundError)) throw error;
670 throw new OAuthError(
671 "invalid_client",
672 undefined,
673 undefined,
674 paramsClientId
675 );
676 }
677
678 if (!client.secrets.has(paramsClientSecret)) {
679 throw new OAuthError(
680 "invalid_client",
681 undefined,
682 undefined,
683 paramsClientId
684 );
685 }
686
687 if (!client.enabled) {
688 throw new OAuthError(
689 "unauthorized_client",
690 undefined,
691 undefined,
692 paramsClientId
693 );
694 }
695
696 // Invoke the client.
697 await client.invoke(tx, {
698 id: v4(),
699 createdAt: new Date()
700 });
701
702 // Decode and validate the authorization code.
703 const [grantId, issuedAt, nonce] = Buffer.from(
704 paramsRefreshToken,
705 "base64"
706 )
707 .toString("utf8")
708 .split(":");
709
710 if (!grantId || !issuedAt || !nonce) {
711 throw new OAuthError(
712 "invalid_grant",
713 "Invalid authorization code.",
714 undefined,
715 paramsClientId
716 );
717 }
718
719 // Fetch the grant.
720 let grant: Grant;
721 try {
722 grant = await Grant.read(executor, grantId);
723 } catch (error) {
724 if (!(error instanceof NotFoundError)) throw error;
725 throw new OAuthError(
726 "invalid_grant",
727 "Invalid authorization code.",
728 undefined,
729 paramsClientId
730 );
731 }
732
733 if (!grant.enabled) {
734 throw new OAuthError(
735 "invalid_grant",
736 "Invalid authorization code.",
737 undefined,
738 paramsClientId
739 );
740 }
741
742 if (grant.clientId !== client.id) {
743 throw new OAuthError(
744 "invalid_grant",
745 "Invalid authorization code.",
746 undefined,
747 paramsClientId
748 );
749 }
750
751 if (!grant.secrets.has(paramsRefreshToken)) {
752 throw new OAuthError(
753 "invalid_grant",
754 "Invalid authorization code.",
755 undefined,
756 paramsClientId
757 );
758 }
759
760 // Invoke the grant.
761 await grant.invoke(executor, {
762 id: v4(),
763 createdAt: new Date()
764 });
765
766 // Fetch the user.
767 const user = await grant.user(executor);
768 if (!user.enabled) {
769 throw new OAuthError(
770 "invalid_grant",
771 "Invalid authorization code.",
772 undefined,
773 paramsClientId
774 );
775 }
776
777 // Make sure we have the necessary access.
778 await assertPermissions(realm, executor, grant, {
779 currentUserId: grant.userId,
780 currentGrantId: grant.id,
781 currentClientId: grant.clientId,
782 currentAuthorizationId: null
783 });
784
785 // Get all enabled authorizations of this grant.
786 const authorizations = (await grant.authorizations(executor)).filter(
787 t => t.enabled
788 );
789
790 // Look for an existing active authorization for this grant with all
791 // grant scopes.
792 const possibleRootAuthorizations = authorizations.filter(t =>
793 isEqual("**:**:**", t.scopes)
794 );
795
796 let rootAuthorization: Authorization;
797 if (possibleRootAuthorizations.length) {
798 // Use an existing authorization.
799 ctx[x].authorization = rootAuthorization =
800 possibleRootAuthorizations[0];
801 } else {
802 // Create a new authorization.
803 const authorizationId = v4();
804 ctx[
805 x
806 ].authorization = rootAuthorization = await Authorization.write(
807 tx,
808 {
809 id: authorizationId,
810 enabled: true,
811 userId: user.id,
812 grantId: grant.id,
813 secret: randomBytes(16).toString("hex"),
814 scopes: ["**:**:**"]
815 },
816 {
817 recordId: v4(),
818 createdByAuthorizationId: authorizationId,
819 createdByCredentialId: null,
820 createdAt: new Date()
821 }
822 );
823 }
824
825 // Look for an existing active authorization for this grant with the
826 // requested scopes.
827 const possibleRequestedAuthorizations = authorizations.filter(t =>
828 isEqual(
829 inject(requestedScopeTemplates, {
830 /* eslint-disable camelcase */
831 current_user_id: grant.userId ?? null,
832 current_grant_id: grant.id ?? null,
833 current_client_id: grant.clientId ?? null,
834 current_authorization_id: t.id ?? null
835 /* eslint-enable camelcase */
836 }),
837 t.scopes
838 )
839 );
840
841 let requestedAuthorization: Authorization;
842
843 if (possibleRequestedAuthorizations.length) {
844 // Use an existing authorization.
845 requestedAuthorization = possibleRequestedAuthorizations[0];
846 } else {
847 // Create a new authorization.
848 const authorizationId = v4();
849 requestedAuthorization = await Authorization.write(
850 tx,
851 {
852 id: authorizationId,
853 enabled: true,
854 userId: user.id,
855 grantId: grant.id,
856 secret: randomBytes(16).toString("hex"),
857 scopes: inject(requestedScopeTemplates, {
858 /* eslint-disable camelcase */
859 current_user_id: grant.userId ?? null,
860 current_grant_id: grant.id ?? null,
861 current_client_id: grant.clientId ?? null,
862 current_authorization_id: authorizationId ?? null
863 /* eslint-enable camelcase */
864 })
865 },
866 {
867 recordId: v4(),
868 createdByAuthorizationId: rootAuthorization.id,
869 createdByCredentialId: null,
870 createdAt: new Date()
871 }
872 );
873 }
874
875 const scopes = await requestedAuthorization.access(executor);
876
877 const tokenId = v4();
878 await requestedAuthorization.invoke(executor, {
879 id: tokenId,
880 format: "bearer",
881 createdAt: new Date()
882 });
883
884 const body = {
885 /* eslint-disable camelcase */
886 token_type: "bearer",
887 access_token: jwt.sign(
888 {
889 aid: requestedAuthorization.id,
890 scopes,
891 nonce: paramsNonce
892 },
893 privateKey,
894 {
895 jwtid: tokenId,
896 algorithm: "RS512",
897 expiresIn: jwtValidityDuration,
898 audience: client.id,
899 subject: user.id,
900 issuer: realm
901 }
902 ),
903 refresh_token: getRefreshToken(grant.secrets),
904 expires_in: jwtValidityDuration,
905 scope: scopes.join(" ")
906 /* eslint-enabme camelcase */
907 };
908
909 await tx.query("COMMIT");
910 ctx.response.body = body;
911 return;
912 } catch (error) {
913 await tx.query("ROLLBACK");
914 throw error;
915 }
916 }
917
918 // Unsupported Grant Type
919 throw new OAuthError("unsupported_grant_type");
920 } finally {
921 tx.release();
922 }
923 } catch (error) {
924 if (!(error instanceof OAuthError)) throw error;
925 const body: {
926 error: string;
927 error_message?: string;
928 error_uri?: string;
929 } = { error: error.code };
930
931 if (error.message !== error.code) {
932 // eslint-disable-next-line camelcase
933 body.error_message = error.message;
934 }
935
936 if (error.uri) {
937 // eslint-disable-next-line camelcase
938 body.error_uri = error.uri;
939 }
940
941 ctx.response.status = error.statusCode;
942 ctx.response.body = body;
943 ctx.app.emit("error", error, ctx);
944 }
945}
946
947export default oAuth2Middleware;