UNPKG

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