Project

General

Profile

Download (13.1 KB) Statistics
| Branch: | Tag: | Revision:
1
/**
2
* Copyright (C) 2021 EDIT
3
* European Distributed Institute of Taxonomy
4
* http://www.e-taxonomy.eu
5
*
6
* The contents of this file are subject to the Mozilla Public License Version 1.1
7
* See LICENSE.TXT at the top of this package for the full license terms.
8
*/
9
package eu.etaxonomy.cdm.api.service.security;
10

    
11
import static org.junit.Assert.assertEquals;
12
import static org.junit.Assert.assertFalse;
13
import static org.junit.Assert.assertNotNull;
14
import static org.junit.Assert.assertTrue;
15

    
16
import java.io.FileNotFoundException;
17
import java.time.Duration;
18
import java.util.concurrent.CountDownLatch;
19
import java.util.regex.Matcher;
20
import java.util.regex.Pattern;
21

    
22
import javax.mail.internet.MimeMessage;
23

    
24
import org.apache.logging.log4j.Level;
25
import org.apache.logging.log4j.LogManager;
26
import org.apache.logging.log4j.Logger;
27
import org.junit.After;
28
import org.junit.Before;
29
import org.junit.Test;
30
import org.springframework.mail.javamail.JavaMailSender;
31
import org.springframework.util.concurrent.ListenableFuture;
32
import org.subethamail.wiser.Wiser;
33
import org.subethamail.wiser.WiserMessage;
34
import org.unitils.dbunit.annotation.DataSet;
35
import org.unitils.spring.annotation.SpringBeanByName;
36
import org.unitils.spring.annotation.SpringBeanByType;
37

    
38
import eu.etaxonomy.cdm.api.security.AbstractRequestTokenStore;
39
import eu.etaxonomy.cdm.api.security.IAbstractRequestTokenStore;
40
import eu.etaxonomy.cdm.api.security.PasswordResetRequest;
41
import eu.etaxonomy.cdm.api.service.IUserService;
42
import eu.etaxonomy.cdm.common.LogUtils;
43
import eu.etaxonomy.cdm.model.permission.User;
44
import eu.etaxonomy.cdm.test.unitils.CleanSweepInsertLoadStrategy;
45

    
46
/**
47
 * @author a.kohlbecker
48
 * @since Nov 8, 2021
49
 */
50
public class PasswordResetServiceTest extends eu.etaxonomy.cdm.test.integration.CdmTransactionalIntegrationTest {
51

    
52
    private static final Logger logger = LogManager.getLogger(PasswordResetServiceTest.class);
53

    
54
    private static final double maxRequestRate = 4.0;
55

    
56
    private static final int rateLimiterTimeout = 200;
57
    private static final String userName = "pwdResetTestUser";
58
    private static final String userPWD = "super_SECURE_123";
59
    private static final String newPWD = "NEW_123_new_456";
60
    private static final String userEmail = "pwdResetTestUser@cybertaxonomy.test";
61

    
62
    private static String base64UrlSaveCharClass = "[a-zA-Z0-9\\-_]";
63

    
64
    private static final String requestFormUrlTemplate = "http://cybertaxonomy.test/passwordReset?userName={%s}&sessID=f8d8sf8dsf";
65

    
66
    @SpringBeanByType
67
    private IUserService userService;
68

    
69
    @SpringBeanByType
70
    private IPasswordResetService passwordResetService;
71

    
72
    @SpringBeanByName
73
    private IAbstractRequestTokenStore<PasswordResetRequest, User> passwordResetTokenStore;
74

    
75
    @SpringBeanByType
76
    private JavaMailSender emailSender;
77

    
78
    private Wiser wiser = null;
79

    
80
    private CountDownLatch resetTokenSendSignal;
81
    private CountDownLatch resetTokenSendSignal2;
82
    private CountDownLatch passwordChangedSignal;
83
    private Throwable assyncError = null;
84

    
85
    @Before
86
    public void startEmailServer() {
87
        // Integer smtpPort = env.getProperty(SendEmailConfigurer.PORT, Integer.class);
88
        wiser = new Wiser();
89
        wiser.setPort(2500); // must be the same as configured for SendEmailConfigurer.PORT
90
        wiser.start();
91
        logger.debug("Wiser email server started");
92
    }
93

    
94

    
95
    @Before
96
    public void createUser() {
97
        User user = User.NewInstance(userName, userPWD);
98
        user.setEmailAddress(userEmail);
99
        userService.save(user);
100
        commitAndStartNewTransaction();
101
        // printDataSet(System.err, "User");
102
    }
103

    
104
    @Before
105
    public void setLogLevel() {
106
        LogUtils.setLevel(logger, Level.DEBUG);
107
    }
108

    
109
    @Before
110
    public void resetPasswordResetService() throws InterruptedException {
111
//        LogUtils.setLevel(PasswordResetRequest.class, Level.TRACE);
112
        // speed up testing
113
        passwordResetService.setRateLimiterTimeout(Duration.ofMillis(rateLimiterTimeout));
114
        passwordResetService.setRate(maxRequestRate);
115
        // pause long enough to avoid conflicts
116
        long sleepTime = Math.round(1000 / maxRequestRate) + rateLimiterTimeout;
117
        Thread.sleep(sleepTime);
118
    }
119

    
120
    @Before
121
    public void resetAsyncVars() {
122
        assyncError = null;
123
        resetTokenSendSignal = null;
124
        resetTokenSendSignal2 = null;
125
        passwordChangedSignal = null;
126
    }
127

    
128
    @After
129
    public void removeUser() {
130
        userService.deleteUser(userName);
131
        userService.getSession().flush();
132
        commitAndStartNewTransaction();
133
    }
134

    
135
    @After
136
    public void stopEmailServer() {
137
        wiser.stop();
138
    }
139

    
140
    @Test
141
    @DataSet(loadStrategy = CleanSweepInsertLoadStrategy.class, value="/eu/etaxonomy/cdm/database/ClearDBDataSet.xml")
142
    public void testSuccessfulEmailReset() throws Throwable {
143

    
144
        logger.debug("testSuccessfulEmailReset() ...");
145

    
146
        // printDataSet(System.err, "UserAccount");
147

    
148
        resetTokenSendSignal = new CountDownLatch(1);
149
        passwordChangedSignal = new CountDownLatch(1);
150

    
151
        ListenableFuture<Boolean> emailResetFuture = passwordResetService.emailResetToken(userName, requestFormUrlTemplate);
152
        emailResetFuture.addCallback(
153
                requestSuccessVal -> {
154
                    resetTokenSendSignal.countDown();
155
                }, futureException -> {
156
                    assyncError = futureException;
157
                    resetTokenSendSignal.countDown();
158
                });
159

    
160
        // -- wait for passwordResetService.emailResetToken() to complete
161
        resetTokenSendSignal.await();
162

    
163
        if(assyncError != null) {
164
            throw assyncError;
165
        }
166

    
167
        assertNotNull(emailResetFuture.get());
168
        assertEquals(1, wiser.getMessages().size());
169

    
170
        // -- read email message
171
        WiserMessage requestMessage = wiser.getMessages().get(0);
172
        MimeMessage requestMimeMessage = requestMessage.getMimeMessage();
173
        assertEquals(UserAccountEmailTemplates.RESET_REQUEST_EMAIL_SUBJECT_TEMPLATE.replace("${userName}", userName), requestMimeMessage.getSubject());
174

    
175
        String messageContent = requestMimeMessage.getContent().toString();
176
        // -- extract token
177
        Pattern pattern = Pattern.compile("=\\{(" + base64UrlSaveCharClass + "+)\\}");
178
        Matcher m = pattern.matcher(messageContent);
179
        assertTrue(m.find());
180
        assertEquals(AbstractRequestTokenStore.TOKEN_LENGTH + 17, m.group(1).length());
181

    
182
        // -- change password
183
        ListenableFuture<Boolean> resetPasswordFuture = passwordResetService.resetPassword( m.group(1), newPWD);
184
        resetPasswordFuture.addCallback(requestSuccessVal -> {
185
            passwordChangedSignal.countDown();
186
        }, futureException -> {
187
            assyncError =  futureException;
188
            passwordChangedSignal.countDown();
189
        });
190
        // -- wait for passwordResetService.resetPassword to complete
191
        passwordChangedSignal.await();
192

    
193
        assertTrue(resetPasswordFuture.get());
194
        assertEquals(2, wiser.getMessages().size());
195
        WiserMessage successMessage = wiser.getMessages().get(1);
196
        MimeMessage successMimeMessage = successMessage.getMimeMessage();
197
        assertEquals(UserAccountEmailTemplates.RESET_SUCCESS_EMAIL_SUBJECT_TEMPLATE.replace("${userName}", userName), successMimeMessage.getSubject());
198
    }
199

    
200
    @Test
201
    @DataSet(loadStrategy = CleanSweepInsertLoadStrategy.class, value="/eu/etaxonomy/cdm/database/ClearDBDataSet.xml")
202
    public void emailResetTokenRateLimiterTest() throws Throwable {
203

    
204
//      LogUtils.setLevel(PasswordResetRequest.class, Level.TRACE);
205
//      LogUtils.setLevel(PasswordResetService.class, Level.TRACE);
206
//      LogUtils.setLevel(AccountSelfManagementService.class, Level.TRACE);
207

    
208
        resetTokenSendSignal = new CountDownLatch(1);
209
        resetTokenSendSignal2 = new CountDownLatch(1);
210
        CountDownLatch resetTokenSendSignal3 = new CountDownLatch(1);
211

    
212
        passwordResetService.setRate(0.2);  //allow request every 5s
213
        passwordResetService.setRateLimiterTimeout(Duration.ofMillis(1)); // as short as possible to allow the fist call to be successful
214

    
215
        logger.debug("1. request");
216
        ListenableFuture<Boolean> emailResetFuture = passwordResetService.emailResetToken(userName, requestFormUrlTemplate);
217
        emailResetFuture.addCallback(
218
                requestSuccessVal -> {
219
                    logger.debug("success 1");
220
                    resetTokenSendSignal.countDown();
221
                }, futureException -> {
222
                    logger.debug("error 1");
223
                    assyncError = futureException;
224
                    resetTokenSendSignal.countDown();
225
                });
226

    
227
        logger.debug("2. request");
228
        //wait 2 seconds to definitely have the first request as first
229
        try {Thread.sleep(2000);} catch (InterruptedException e1) {}
230
        ListenableFuture<Boolean> emailResetFuture2 = passwordResetService.emailResetToken(userName, requestFormUrlTemplate);
231
        emailResetFuture2.addCallback(
232
                requestSuccessVal -> {
233
                    logger.debug("success 2");
234
                    resetTokenSendSignal2.countDown();
235
                }, futureException -> {
236
                    logger.debug("error 2");
237
                    assyncError = futureException;
238
                    resetTokenSendSignal2.countDown();
239
                }
240
        );
241

    
242
        logger.debug("3. request");
243
        //wait another 4 seconds to totally wait 6s and therefore be allowed to request another token again
244
        try {Thread.sleep(4000);} catch (InterruptedException e1) {}
245
        ListenableFuture<Boolean> emailResetFuture3 = passwordResetService.emailResetToken(userName, requestFormUrlTemplate);
246
        emailResetFuture3.addCallback(
247
                requestSuccessVal -> {
248
                    logger.debug("success 3");
249
                    resetTokenSendSignal3.countDown();
250
                }, futureException -> {
251
                    logger.debug("error 3");
252
                    assyncError = futureException;
253
                    resetTokenSendSignal3.countDown();
254
                }
255
        );
256

    
257
        // -- wait for passwordResetService.emailResetToken() to complete
258
        resetTokenSendSignal.await();
259
        resetTokenSendSignal2.await();
260
        resetTokenSendSignal3.await();
261

    
262
        logger.debug("all completed, testing assertions");
263

    
264
        if(assyncError != null) {
265
            throw assyncError; // an error should not have been thrown
266
        }
267
        assertTrue("First request should have been successful", emailResetFuture.get());
268
        assertFalse("Second request should have been rejected", emailResetFuture2.get());
269
        assertTrue("Third request should have been successful again", emailResetFuture.get());
270
        //sleep 5 seconds to "cleanup" rate limiter before next test
271
        try {Thread.sleep(5000);} catch (InterruptedException e1) {}
272
    }
273

    
274
    @Test
275
    @DataSet(loadStrategy = CleanSweepInsertLoadStrategy.class, value="/eu/etaxonomy/cdm/database/ClearDBDataSet.xml")
276
    public void emailResetToken_ivalidUserNameTest() throws Throwable {
277

    
278
        logger.debug("emailResetToken_ivalidUserNameTest() ...");
279

    
280
        resetTokenSendSignal = new CountDownLatch(1);
281

    
282
        passwordResetService.setRateLimiterTimeout(Duration.ofMillis(1)); // as should as possible to allow the fist call to be successful (with 1ns the fist call fails!)
283
        ListenableFuture<Boolean> emailResetFuture = passwordResetService.emailResetToken("iDoNotExist", requestFormUrlTemplate);
284
        emailResetFuture.addCallback(
285
                requestSuccessVal -> {
286
                    resetTokenSendSignal.countDown();
287
                }, futureException -> {
288
                    assyncError = futureException;
289
                    resetTokenSendSignal.countDown();
290
                }
291
        );
292

    
293
        // -- wait for passwordResetService.emailResetToken() to complete
294
        resetTokenSendSignal.await();
295

    
296
        if(assyncError != null) {
297
            throw assyncError; // emailResetToken must be agnostic of the existence of user names
298
        }
299
        assertTrue("The request should look like succesful even if the user does not exist.", emailResetFuture.get());
300
    }
301

    
302
    @Test
303
    @DataSet(loadStrategy = CleanSweepInsertLoadStrategy.class, value="/eu/etaxonomy/cdm/database/ClearDBDataSet.xml")
304
    public void testInvalidToken() throws Throwable {
305

    
306
        logger.debug("testInvalidToken() ...");
307

    
308
        passwordChangedSignal = new CountDownLatch(1);
309

    
310
        // -- change password
311
        ListenableFuture<Boolean> resetPasswordFuture = passwordResetService.resetPassword( "IUER9843URIO--INVALID-TOKEN--UWEUR89EUWWEOIR", newPWD);
312
        resetPasswordFuture.addCallback(requestSuccessVal -> {
313
            passwordChangedSignal.countDown();
314
        }, futureException -> {
315
            assyncError =  futureException;
316
            passwordChangedSignal.countDown();
317
        });
318
        // -- wait for passwordResetService.resetPassword to complete
319
        passwordChangedSignal.await();
320

    
321
        assertNotNull(assyncError);
322
        assertEquals(AccountSelfManagementException.class, assyncError.getClass());
323
        assertEquals(0, wiser.getMessages().size());
324
    }
325

    
326
    @Override
327
    public void createTestDataSet() throws FileNotFoundException {}
328
}
(2-2/2)