aboutsummaryrefslogtreecommitdiff
path: root/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/PasswordResetEndpoint.cs
blob: 60c99e38dfa033badeed2c91161e3401ea458044 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
/*
* Copyright (c) 2024 Vaughn Nugent
* 
* Library: VNLib
* Package: VNLib.Plugins.Essentials.Accounts
* File: PasswordResetEndpoint.cs 
*
* PasswordResetEndpoint.cs is part of VNLib.Plugins.Essentials.Accounts which is part of the larger 
* VNLib collection of libraries and utilities.
*
* VNLib.Plugins.Essentials.Accounts is free software: you can redistribute it and/or modify 
* it under the terms of the GNU Affero General Public License as 
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* VNLib.Plugins.Essentials.Accounts is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program.  If not, see https://www.gnu.org/licenses/.
*/

using System;
using System.Net;
using System.Threading.Tasks;
using System.Text.Json.Serialization;

using FluentValidation;

using VNLib.Utils;
using VNLib.Utils.Memory;
using VNLib.Plugins.Essentials.Users;
using VNLib.Plugins.Essentials.Extensions;
using VNLib.Plugins.Essentials.Endpoints;
using VNLib.Plugins.Essentials.Accounts.MFA;
using VNLib.Plugins.Extensions.Validation;
using VNLib.Plugins.Extensions.Loading;
using VNLib.Plugins.Extensions.Loading.Users;

namespace VNLib.Plugins.Essentials.Accounts.Endpoints
{

    /*
     * SECURITY NOTES:
     * 
     * If no MFA configuration is loaded for this plugin, users will
     * be permitted to change passwords without thier 2nd factor. 
     * 
     * This decision was made to allow users with MFA enabled from a previous
     * config to change their passwords rather than deny them the ability.
     */

    /// <summary>
    /// Password reset for user's that are logged in and know 
    /// their passwords to reset their MFA methods
    /// </summary>
    [ConfigurationName("password_endpoint")]
    internal sealed class PasswordChangeEndpoint : ProtectedWebEndpoint
    {
        private readonly IUserManager Users;
        private readonly MFAConfig mFAConfig;
        private readonly IValidator<PasswordResetMesage> ResetMessValidator;

        public PasswordChangeEndpoint(PluginBase pbase, IConfigScope config)
        {
            string? path = config["path"].GetString();
            InitPathAndLog(path, pbase.Log);

            Users = pbase.GetOrCreateSingleton<UserManager>();
            ResetMessValidator = GetMessageValidator();
            mFAConfig = pbase.GetConfigElement<MFAConfig>();
        }

        private static IValidator<PasswordResetMesage> GetMessageValidator()
        {
            InlineValidator<PasswordResetMesage> rules = new();

            rules.RuleFor(static pw => pw.Current)
                .NotEmpty()
                .WithMessage("You must specify your current password")
                .Length(8, 100);

            //Use centralized password validator for new passwords
            rules.RuleFor(static pw => pw.NewPassword)
                .NotEmpty()
                .NotEqual(static pm => pm.Current)
                .WithMessage("Your new password may not equal your new current password")
                .SetValidator(AccountValidations.PasswordValidator);

            return rules;
        }

        protected override async ValueTask<VfReturnType> PostAsync(HttpEntity entity)
        {
            ValErrWebMessage webm = new();

            //get the request body
            using PasswordResetMesage? pwReset = await entity.GetJsonFromFileAsync<PasswordResetMesage>();

            if (webm.Assert(pwReset != null, "No request specified"))
            {
                return VirtualClose(entity, webm, HttpStatusCode.BadRequest);
            }

            //Validate
            if(!ResetMessValidator.Validate(pwReset, webm))
            {
                return VirtualOk(entity, webm);
            }

            //get the user's entry in the table
            using IUser? user = await Users.GetUserFromIDAsync(entity.Session.UserID, entity.EventCancellation);

            if(webm.Assert(user != null, "An error has occured, please log-out and try again"))
            {
                return VirtualOk(entity, webm);
            }

            //Make sure the account's origin is a local profile
            if (webm.Assert(user.IsLocalAccount(), "External accounts cannot be modified"))
            {
                return VirtualOk(entity, webm);
            }

            //Validate the user's current password
            ERRNO isPassValid = await Users.ValidatePasswordAsync(user, pwReset.Current!, PassValidateFlags.None, entity.EventCancellation);

            //Verify the user's old password
            if (webm.Assert(isPassValid > 0, "Please check your current password"))
            {
                return VirtualOk(entity, webm);
            }

            //Check if totp is enabled
            if (mFAConfig.TOTPEnabled && user.MFATotpEnabled())
            {
                //TOTP code is required
                if (webm.Assert(pwReset.TotpCode.HasValue, "TOTP is enabled on this user account, you must enter your TOTP code."))
                {
                    return VirtualOk(entity, webm);
                }

                //Veriy totp code
                bool verified = mFAConfig.VerifyTOTP(user, pwReset.TotpCode.Value);

                if (webm.Assert(verified, "Please check your TOTP code and try again"))
                {
                    return VirtualOk(entity, webm);
                }

                //continue
            }

            //Update the user's password
            if (await Users.UpdatePasswordAsync(user, pwReset.NewPassword!, entity.EventCancellation) == 1)
            {
                //error
                webm.Result = "Your password could not be updated";
                return VirtualOk(entity, webm);
            }

            //Publish to user database
            await user.ReleaseAsync(entity.EventCancellation);

            //delete the user's MFA entry so they can re-enable it
            webm.Result = "Your password has been updated";
            webm.Success = true;
            return VirtualOk(entity, webm);
        }

        private sealed class PasswordResetMesage : PrivateStringManager
        {
            public PasswordResetMesage() : base(2)
            {
            }

            [JsonPropertyName("current")]
            public string? Current
            {
                get => this[0];
                set => this[0] = value;
            }

            [JsonPropertyName("new_password")]
            public string? NewPassword
            {
                get => this[1];
                set => this[1] = value;
            }

            [JsonPropertyName("totp_code")]
            public uint? TotpCode { get; set; }
        }
    }
}