|
2 | 2 | type IAdminForth, |
3 | 3 | type IHttpServer, |
4 | 4 | BeforeLoginAttemptFunction, |
| 5 | + AfterLoginFailedFunction, |
| 6 | + LoginFailureReason, |
5 | 7 | BeforeLoginConfirmationFunction, |
6 | 8 | BeforeLogoutFunction, |
7 | 9 | AdminForthResource, |
@@ -598,6 +600,21 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI { |
598 | 600 | return null; |
599 | 601 | } |
600 | 602 |
|
| 603 | + /** |
| 604 | + * Runs afterLoginFailed hooks. Hooks can't change the 401 response, they are called for logging only. |
| 605 | + */ |
| 606 | + async processAfterLoginFail ed(username: string, pk: string | null, reason: LoginFailureReason, extra: HttpExtra) { |
| 607 | + const afterLoginFailed =this.adminforth.config.auth.afterLoginFailed as (AfterLoginFailedFunction[] | undefined); |
| 608 | + |
| 609 | + for (const hook of listify(afterLoginFailed)) { |
| 610 | + try { |
| 611 | + await hook({ username, pk, reason, adminforth: this.adminforth, extra }); |
| 612 | + } catch (e) { |
| 613 | + afLogger.error(`Error in afterLoginFailed hook, continuing with failed login response: ${e}`); |
| 614 | + } |
| 615 | + } |
| 616 | + } |
| 617 | + |
601 | 618 | async processLoginCallbacks(adminUser: AdminUser, toReturn: { redirectTo?: string, allowedLogin:boolean, error?: string }, response: any, extra: HttpExtra, sessionDuration?: string) { |
602 | 619 | const beforeLoginConfirmation = this.adminforth.config.auth.beforeLoginConfirmation as (BeforeLoginConfirmationFunction[] | undefined); |
603 | 620 |
|
@@ -672,14 +689,14 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI { |
672 | 689 | const userResource = this.adminforth.config.resources.find((res) => res.resourceId === this.adminforth.config.auth.usersResourceId); |
673 | 690 | const usernameColumn = userResource.columns.find((col) => col.name === this.adminforth.config.auth.usernameField); |
674 | 691 | const normalizedUsername = normalizeColumnValue(usernameColumn, username); |
| 692 | + const extra: HttpExtra = { body, headers, query, cookies, requestUrl, response }; |
675 | 693 |
|
676 | 694 | // hooks are called before any lookup in database, so rejected attempt looks the same |
677 | 695 | // for existing and non-existing users (e.g. captcha check should not leak valid credentials) |
678 | | - const loginAttemptError = await this.processBeforeLoginAttempt(normalizedUsername, { |
679 | | - body, headers, query, cookies, requestUrl, response |
680 | | - }, tr); |
| 696 | + const loginAttemptError = await this.processBeforeLoginAttempt(normalizedUsername, extra, tr); |
681 | 697 | if (loginAttemptError) { |
682 | 698 | response.setStatus(401); |
| 699 | + await this.processAfterLoginFailed(normalizedUsername, null, 'rejected', extra); |
683 | 700 | return loginAttemptError; |
684 | 701 | } |
685 | 702 |
|
@@ -709,41 +726,42 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI { |
709 | 726 |
|
710 | 727 | if (!userRecord) { |
711 | 728 | response.setStatus(401); |
| 729 | + await this.processAfterLoginFailed(normalizedUsername, null, 'user_not_found', extra); |
712 | 730 | return { error: INVALID_MESSAGE }; |
713 | 731 | } |
714 | 732 |
|
| 733 | + const pk = userRecord[userResource.columns.find((col) => col.primaryKey).name]; |
715 | 734 | const passwordHash = userRecord[this.adminforth.config.auth.passwordHashField]; |
716 | 735 | const valid = await AdminForthAuth.verifyPassword(password, passwordHash); |
717 | 736 | if (valid) { |
718 | | - adminUser = { |
| 737 | + adminUser = { |
719 | 738 | dbUser: userRecord, |
720 | | - pk: userRecord[userResource.columns.find((col) => col.primaryKey).name], |
| 739 | + pk, |
721 | 740 | username: normalizedUsername, |
722 | 741 | }; |
723 | 742 |
|
724 | | - const expireInDuration = rememberMe |
| 743 | + const expireInDuration = rememberMe |
725 | 744 | ? (this.adminforth.config.auth.rememberMeDuration || '30d') |
726 | 745 | : '1d'; |
727 | 746 |
|
728 | | - await this.processLoginCallbacks(adminUser, toReturn, response, { |
729 | | - body, headers, query, cookies, requestUrl, response |
730 | | - }, expireInDuration); |
| 747 | + await this.processLoginCallbacks(adminUser, toReturn, response, extra, expireInDuration); |
731 | 748 |
|
732 | 749 | if (toReturn.allowedLogin) { |
733 | | - |
| 750 | + |
734 | 751 | await this.adminforth.auth.setAuthCookie({ |
735 | 752 | expireInDuration, |
736 | 753 | response, |
737 | 754 | username: normalizedUsername, |
738 | | - pk: userRecord[userResource.columns.find((col) => col.primaryKey).name], |
739 | | - extra: { body, headers, query, cookies, requestUrl, response }, |
| 755 | + pk, |
| 756 | + extra, |
740 | 757 | }); |
741 | | - } |
| 758 | + } |
742 | 759 | } else { |
743 | 760 | response.setStatus(401); |
| 761 | + await this.processAfterLoginFailed(normalizedUsername, pk, 'invalid_password', extra); |
744 | 762 | return { error: INVALID_MESSAGE }; |
745 | 763 | } |
746 | | - |
| 764 | + |
747 | 765 |
|
748 | 766 | return toReturn; |
749 | 767 | } |
|
0 commit comments