← Back to team overview

dhis2-devs team mailing list archive

[Branch ~dhis2-devs-core/dhis2/trunk] Rev 8751: Server side implementation of user account restore function

 

------------------------------------------------------------
revno: 8751
committer: Lars Helge Øverland <larshelge@xxxxxxxxx>
branch nick: dhis2
timestamp: Mon 2012-10-29 17:34:57 +0300
message:
  Server side implementation of user account restore function
added:
  dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/security/DefaultSecurityService.java
  dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/security/SecurityService.java
  dhis-2/dhis-services/dhis-service-core/src/main/resources/restore_message1.vm
  dhis-2/dhis-services/dhis-service-core/src/main/resources/restore_message2.vm
  dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/security/
  dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/security/SecurityServiceTest.java
modified:
  dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/CodeGenerator.java
  dhis-2/dhis-api/src/main/java/org/hisp/dhis/user/UserCredentials.java
  dhis-2/dhis-services/dhis-service-core/src/main/resources/META-INF/dhis/security.xml
  dhis-2/dhis-services/dhis-service-core/src/main/resources/org/hisp/dhis/user/hibernate/UserCredentials.hbm.xml


--
lp:dhis2
https://code.launchpad.net/~dhis2-devs-core/dhis2/trunk

Your team DHIS 2 developers is subscribed to branch lp:dhis2.
To unsubscribe from this branch go to https://code.launchpad.net/~dhis2-devs-core/dhis2/trunk/+edit-subscription
=== modified file 'dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/CodeGenerator.java'
--- dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/CodeGenerator.java	2011-11-25 18:32:06 +0000
+++ dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/CodeGenerator.java	2012-10-29 14:34:57 +0000
@@ -43,21 +43,34 @@
     public static final int CODESIZE = 11;
 
     /**
-     * Generates a pseudo random string using the allowed characters
+     * Generates a pseudo random string using the allowed characters. Code is
+     * 11 characters long.
      * 
-     * @return the Code
+     * @param codeSize the number of characters in the code.
+     * @return the code.
      */
     public static String generateCode()
     {
+        return generateCode( CODESIZE );
+    }
+        
+    /**
+     * Generates a pseudo random string using the allowed characters.
+     * 
+     * @param codeSize the number of characters in the code.
+     * @return the code.
+     */
+    public static String generateCode( int codeSize )
+    {
         // Using the system default algorithm and seed
         SecureRandom sr = new SecureRandom();
 
-        char[] randomChars = new char[CODESIZE];
+        char[] randomChars = new char[codeSize];
         
         // first char should be a letter
         randomChars[0] = letters.charAt( sr.nextInt( letters.length() ) );
         
-        for ( int i = 1; i < CODESIZE; ++i )
+        for ( int i = 1; i < codeSize; ++i )
         {
             randomChars[i] = allowedChars.charAt( sr.nextInt( NUMBER_OF_CODEPOINTS ) );
         }

=== modified file 'dhis-2/dhis-api/src/main/java/org/hisp/dhis/user/UserCredentials.java'
--- dhis-2/dhis-api/src/main/java/org/hisp/dhis/user/UserCredentials.java	2012-10-07 19:07:53 +0000
+++ dhis-2/dhis-api/src/main/java/org/hisp/dhis/user/UserCredentials.java	2012-10-29 14:34:57 +0000
@@ -27,24 +27,25 @@
  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
 
+import java.io.Serializable;
+import java.util.Collection;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.hisp.dhis.common.BaseIdentifiableObject;
+import org.hisp.dhis.common.Dxf2Namespace;
+import org.hisp.dhis.common.IdentifiableObjectUtils;
+import org.hisp.dhis.common.view.DetailedView;
+import org.hisp.dhis.common.view.ExportView;
+import org.hisp.dhis.dataset.DataSet;
+
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.annotation.JsonView;
 import com.fasterxml.jackson.databind.annotation.JsonSerialize;
 import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
 import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
 import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
-import org.hisp.dhis.common.BaseIdentifiableObject;
-import org.hisp.dhis.common.Dxf2Namespace;
-import org.hisp.dhis.common.IdentifiableObjectUtils;
-import org.hisp.dhis.common.view.DetailedView;
-import org.hisp.dhis.common.view.ExportView;
-import org.hisp.dhis.dataset.DataSet;
-
-import java.io.Serializable;
-import java.util.Collection;
-import java.util.Date;
-import java.util.HashSet;
-import java.util.Set;
 
 /**
  * @author Nguyen Hong Duc
@@ -71,13 +72,34 @@
     private String username;
 
     /**
-     * Required.
+     * Required. Will be stored as a hash.
      */
     private String password;
 
+    /**
+     * Set of user roles.
+     */
     private Set<UserAuthorityGroup> userAuthorityGroups = new HashSet<UserAuthorityGroup>();
 
+    /**
+     * Date of the user's last login.
+     */
     private Date lastLogin;
+    
+    /**
+     * The token used for a user account restore. Will be stored as a hash.
+     */
+    private String restoreToken;
+
+    /**
+     * The code used for a user account restore. Will be stored as a hash.
+     */
+    private String restoreCode;
+    
+    /**
+     * The timestamp representing when the restore window expires.
+     */
+    private Date restoreExpiry;
 
     // -------------------------------------------------------------------------
     // Logic
@@ -200,6 +222,39 @@
         return user != null ? user.getName() : username;
     }
 
+    /**
+     * Tests whether the given input arguments can perform a valid restore of the
+     * user account for these credentials. Returns false if any of the input arguments
+     * are null, or any of the properties on the credentials are null. Returns false
+     * if the expiry date arguement is after the expiry date of the credentials.
+     * Returns false if any of the given token or code arguments are not equal to
+     * the respective properties the the credentials. Returns true otherwise.
+     * 
+     * @param token the restore token.
+     * @param code the restore code.
+     * @param expiry the expiry date.
+     * @return true or false.
+     */
+    public boolean canRestore( String token, String code, Date date )
+    {
+        if ( this.restoreToken == null || this.restoreCode == null || this.restoreExpiry == null )
+        {
+            return false;
+        }
+        
+        if ( token == null || code == null || date == null )
+        {
+            return false;
+        }
+        
+        if ( date.after( this.restoreExpiry ) )
+        {
+            return false;
+        }
+        
+        return token.equals( this.restoreToken ) && code.equals( this.restoreCode );
+    }
+    
     // -------------------------------------------------------------------------
     // hashCode and equals
     // -------------------------------------------------------------------------
@@ -313,4 +368,34 @@
     {
         this.lastLogin = lastLogin;
     }
+
+    public String getRestoreToken()
+    {
+        return restoreToken;
+    }
+
+    public void setRestoreToken( String restoreToken )
+    {
+        this.restoreToken = restoreToken;
+    }
+
+    public String getRestoreCode()
+    {
+        return restoreCode;
+    }
+
+    public void setRestoreCode( String restoreCode )
+    {
+        this.restoreCode = restoreCode;
+    }
+
+    public Date getRestoreExpiry()
+    {
+        return restoreExpiry;
+    }
+
+    public void setRestoreExpiry( Date restoreExpiry )
+    {
+        this.restoreExpiry = restoreExpiry;
+    }
 }

=== added file 'dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/security/DefaultSecurityService.java'
--- dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/security/DefaultSecurityService.java	1970-01-01 00:00:00 +0000
+++ dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/security/DefaultSecurityService.java	2012-10-29 14:34:57 +0000
@@ -0,0 +1,190 @@
+package org.hisp.dhis.security;
+
+/*
+ * Copyright (c) 2004-2012, University of Oslo
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * * Redistributions of source code must retain the above copyright notice, this
+ *   list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ *   this list of conditions and the following disclaimer in the documentation
+ *   and/or other materials provided with the distribution.
+ * * Neither the name of the HISP project nor the names of its contributors may
+ *   be used to endorse or promote products derived from this software without
+ *   specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+import java.util.Calendar;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.hisp.dhis.common.CodeGenerator;
+import org.hisp.dhis.message.MessageSender;
+import org.hisp.dhis.period.Cal;
+import org.hisp.dhis.system.velocity.VelocityManager;
+import org.hisp.dhis.user.User;
+import org.hisp.dhis.user.UserCredentials;
+import org.hisp.dhis.user.UserService;
+
+/**
+ * @author Lars Helge Overland
+ */
+public class DefaultSecurityService
+    implements SecurityService
+{
+    private static final String RESTORE_PATH = "/dhis-web-commons/security/restore.action";
+
+    // -------------------------------------------------------------------------
+    // Dependencies
+    // -------------------------------------------------------------------------
+
+    private PasswordManager passwordManager;
+
+    public void setPasswordManager( PasswordManager passwordManager )
+    {
+        this.passwordManager = passwordManager;
+    }
+
+    private MessageSender emailMessageSender;
+
+    public void setEmailMessageSender( MessageSender emailMessageSender )
+    {
+        this.emailMessageSender = emailMessageSender;
+    }
+
+    private UserService userService;
+
+    public void setUserService( UserService userService )
+    {
+        this.userService = userService;
+    }
+
+    // -------------------------------------------------------------------------
+    // SecurityService implementation
+    // -------------------------------------------------------------------------
+
+    public boolean sendRestoreMessage( String username, String rootPath )
+    {
+        if ( username == null || rootPath == null )
+        {
+            return false;
+        }
+        
+        UserCredentials credentials = userService.getUserCredentialsByUsername( username );
+        
+        if ( credentials == null )
+        {
+            return false;
+        }
+        
+        // TODO check if email is configured
+        
+        String[] result = initRestore( credentials );
+        
+        Set<User> users = new HashSet<User>();
+        users.add( credentials.getUser() );
+        
+        Map<String, String> vars = new HashMap<String, String>();
+        vars.put( "rootPath", rootPath );
+        vars.put( "restorePath", rootPath + RESTORE_PATH );
+        vars.put( "token", result[0] );
+        vars.put( "code", result[1] );
+        vars.put( "username", username );
+        
+        String text1 = new VelocityManager().render( vars, "restore_message1.vm" );
+        String text2 = new VelocityManager().render( vars, "restore_message2.vm" );
+        
+        emailMessageSender.sendMessage( "User account restore confirmation (message 1 of 2)", text1, null, users );
+        emailMessageSender.sendMessage( "User account restore confirmation (message 2 of 2)", text2, null, users );
+        
+        return true;
+    }
+
+    public String[] initRestore( UserCredentials credentials )
+    {
+        String token = CodeGenerator.generateCode( 40 );
+        String code = CodeGenerator.generateCode( 15 );
+        
+        String hashedToken = passwordManager.encodePassword( credentials.getUsername(), token );
+        String hashedCode = passwordManager.encodePassword( credentials.getUsername(), code );
+        
+        Date expiry = new Cal().now().add( Calendar.HOUR_OF_DAY, 1 ).time();
+        
+        credentials.setRestoreToken( hashedToken );
+        credentials.setRestoreCode( hashedCode );
+        credentials.setRestoreExpiry( expiry );
+
+        userService.updateUserCredentials( credentials );
+        
+        String[] result = { token, code };
+        return result;
+    }
+    
+    public boolean restore( String username, String token, String code, String newPassword )
+    {
+        if ( username == null || token == null || code == null || newPassword == null )
+        {
+            return false;
+        }
+
+        UserCredentials credentials = userService.getUserCredentialsByUsername( username );
+        
+        if ( credentials == null )
+        {
+            return false;
+        }
+        
+        token = passwordManager.encodePassword( username, token );
+        code = passwordManager.encodePassword( username, code );
+        
+        Date date = new Cal().now().time();
+
+        if ( !credentials.canRestore( token, code, date ) )
+        {
+            return false;
+        }
+        
+        newPassword = passwordManager.encodePassword( username, newPassword );
+        
+        credentials.setPassword( newPassword );
+        
+        userService.updateUserCredentials( credentials );
+        
+        return true;
+    }
+    
+    public boolean verifyToken( String username, String token )
+    {
+        if ( username == null || token == null )
+        {
+            return false;
+        }
+
+        UserCredentials credentials = userService.getUserCredentialsByUsername( username );
+        
+        if ( credentials == null || credentials.getRestoreToken() == null )
+        {
+            return false;
+        }
+        
+        token = passwordManager.encodePassword( username, token );
+        
+        return credentials.getRestoreToken().equals( token );
+    }
+}

=== added file 'dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/security/SecurityService.java'
--- dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/security/SecurityService.java	1970-01-01 00:00:00 +0000
+++ dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/security/SecurityService.java	2012-10-29 14:34:57 +0000
@@ -0,0 +1,87 @@
+package org.hisp.dhis.security;
+
+/*
+ * Copyright (c) 2004-2012, University of Oslo
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * * Redistributions of source code must retain the above copyright notice, this
+ *   list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ *   this list of conditions and the following disclaimer in the documentation
+ *   and/or other materials provided with the distribution.
+ * * Neither the name of the HISP project nor the names of its contributors may
+ *   be used to endorse or promote products derived from this software without
+ *   specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+import org.hisp.dhis.user.UserCredentials;
+
+/**
+ * @author Lars Helge Overland
+ */
+public interface SecurityService
+{
+    /**
+     * Will invoke the initiateRestore method and dispatch email messages with
+     * restore information to the user.
+     * 
+     * @param username the user name of the user to send restore messages.
+     * @param rootPath the root path of the request.
+     * @return false if any of the arguments are null or if the user credentials
+     *         identified by the user name does not exist, true otherwise.
+     */
+    boolean sendRestoreMessage( String username, String rootPath );
+    
+    /**
+     * Will populate the restoreToken and restoreCode property of the given
+     * credentials with a hashed version of auto-generated values. Will set the
+     * restoreExpiry property with a date time one hour from now. Changes will be
+     * persisted.
+     * 
+     * @param credentials the user credentials.
+     * @return an array where index 0 is the clear-text token and index 1 the 
+     *         clear-text code.
+     */
+    String[] initRestore( UserCredentials credentials );
+    
+    /**
+     * Tests whether the given token and code are valid for the given user name.
+     * If true, it will update the user credentials identified by the given user
+     * name with the new password. In order to succeed, the given token and code
+     * must match the ones on the credentials, and the current date must be before
+     * the expiry date time of the credentials.
+     * 
+     * @param username the user name.
+     * @param token the token.
+     * @param code the code.
+     * @param newPassword the proposed new password.
+     * @return true or false.
+     */
+    boolean restore( String username, String token, String code, String newPassword );
+    
+    /**
+     * Tests whether the given token in combination with the given user name is
+     * valid, i.e. whether the hashed version of the token matches the one on the
+     * user credentials identified by the given user name.
+     * 
+     * @param username the user name.
+     * @param token the token.
+     * @return false if any of the arguments are null or if the user credentials
+     *         identified by the user name does not exist, true if the arguments
+     *         are valid.
+     */
+    boolean verifyToken( String username, String token );
+}

=== modified file 'dhis-2/dhis-services/dhis-service-core/src/main/resources/META-INF/dhis/security.xml'
--- dhis-2/dhis-services/dhis-service-core/src/main/resources/META-INF/dhis/security.xml	2012-03-05 14:43:22 +0000
+++ dhis-2/dhis-services/dhis-service-core/src/main/resources/META-INF/dhis/security.xml	2012-10-29 14:34:57 +0000
@@ -16,6 +16,12 @@
     <property name="usernameSaltSource" ref="usernameSaltSource" />
   </bean>
 
+  <bean id="org.hisp.dhis.security.SecurityService" class="org.hisp.dhis.security.DefaultSecurityService">
+    <property name="passwordManager" ref="org.hisp.dhis.security.PasswordManager" />
+    <property name="emailMessageSender" ref="emailMessageSender" />
+    <property name="userService" ref="org.hisp.dhis.user.UserService" />
+  </bean>
+
   <sec:authentication-manager alias="authenticationManager">
     <sec:authentication-provider user-service-ref="userDetailsService">
       <sec:password-encoder hash="md5">

=== modified file 'dhis-2/dhis-services/dhis-service-core/src/main/resources/org/hisp/dhis/user/hibernate/UserCredentials.hbm.xml'
--- dhis-2/dhis-services/dhis-service-core/src/main/resources/org/hisp/dhis/user/hibernate/UserCredentials.hbm.xml	2011-06-11 19:51:41 +0000
+++ dhis-2/dhis-services/dhis-service-core/src/main/resources/org/hisp/dhis/user/hibernate/UserCredentials.hbm.xml	2012-10-29 14:34:57 +0000
@@ -30,7 +30,13 @@
       <many-to-many column="userroleid" class="org.hisp.dhis.user.UserAuthorityGroup" foreign-key="fk_userrolemembers_userroleid" />
     </set>
 	
-	<property name="lastLogin"/>
+	<property name="lastLogin" />
+
+    <property name="restoreToken" />
+    
+    <property name="restoreCode" />
+    
+    <property name="restoreExpiry" type="timestamp" />    
 
   </class>
 </hibernate-mapping>

=== added file 'dhis-2/dhis-services/dhis-service-core/src/main/resources/restore_message1.vm'
--- dhis-2/dhis-services/dhis-service-core/src/main/resources/restore_message1.vm	1970-01-01 00:00:00 +0000
+++ dhis-2/dhis-services/dhis-service-core/src/main/resources/restore_message1.vm	2012-10-29 14:34:57 +0000
@@ -0,0 +1,7 @@
+Someone, probably you, have asked us to restore your useraccount at ${basePath}. 
+You have been sent two emails. This is the first email of those two. Please follow
+the link below this text. In the next step you will be asked to enter a code 
+which is sent to you in the other email. You must complete the restore within 1 
+hour.
+
+${restorePath}?username=${username}&token=${token}

=== added file 'dhis-2/dhis-services/dhis-service-core/src/main/resources/restore_message2.vm'
--- dhis-2/dhis-services/dhis-service-core/src/main/resources/restore_message2.vm	1970-01-01 00:00:00 +0000
+++ dhis-2/dhis-services/dhis-service-core/src/main/resources/restore_message2.vm	2012-10-29 14:34:57 +0000
@@ -0,0 +1,7 @@
+Someone, probably you, have asked us to restore your useraccount at ${basePath}. 
+You have been sent two emails. This is the second email of those two. Please
+read the first email and follow the instructions. If you already have done that,
+please use the code below to complete the account restore form. You must complete 
+the restore within 1 hour.
+
+${code}

=== added directory 'dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/security'
=== added file 'dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/security/SecurityServiceTest.java'
--- dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/security/SecurityServiceTest.java	1970-01-01 00:00:00 +0000
+++ dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/security/SecurityServiceTest.java	2012-10-29 14:34:57 +0000
@@ -0,0 +1,94 @@
+package org.hisp.dhis.security;
+
+/*
+ * Copyright (c) 2004-2012, University of Oslo
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * * Redistributions of source code must retain the above copyright notice, this
+ *   list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ *   this list of conditions and the following disclaimer in the documentation
+ *   and/or other materials provided with the distribution.
+ * * Neither the name of the HISP project nor the names of its contributors may
+ *   be used to endorse or promote products derived from this software without
+ *   specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+import static junit.framework.Assert.*;
+
+import org.hisp.dhis.DhisSpringTest;
+import org.hisp.dhis.user.User;
+import org.hisp.dhis.user.UserCredentials;
+import org.hisp.dhis.user.UserService;
+import org.junit.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+
+/**
+ * @author Lars Helge Overland
+ */
+public class SecurityServiceTest
+    extends DhisSpringTest
+{
+    private UserCredentials credentials;
+    
+    @Autowired
+    private UserService userService; 
+    
+    @Autowired
+    private PasswordManager passwordManager;
+    
+    @Autowired
+    private SecurityService securityService;
+    
+    @Override
+    public void setUpTest()
+    {        
+        credentials = new UserCredentials();
+        credentials.setUsername( "johndoe" );
+        credentials.setPassword( "" );
+        
+        User user = createUser( 'A' );
+        user.setUserCredentials( credentials );
+        credentials.setUser( user );
+        userService.addUserCredentials( credentials );
+    }
+    
+    @Test
+    public void testRestore()
+    {
+        String[] result = securityService.initRestore( credentials );
+        
+        assertNotNull( result[0] );
+        assertNotNull( result[1] );
+        assertNotNull( credentials.getRestoreToken() );
+        assertNotNull( credentials.getRestoreCode() );
+        assertNotNull( credentials.getRestoreExpiry() );
+        
+        boolean verified = securityService.verifyToken( credentials.getUsername(), result[0] );
+        
+        assertTrue( verified );
+        
+        String password = "NewPassword1";
+        
+        boolean restored = securityService.restore( credentials.getUsername(), result[0], result[1], password );
+        
+        assertTrue( restored );
+        
+        String hashedPassword = passwordManager.encodePassword( credentials.getUsername(), password );
+        
+        assertEquals( hashedPassword, credentials.getPassword() );
+    }
+}