← Back to team overview

dhis2-devs team mailing list archive

[Branch ~dhis2-devs-core/dhis2/trunk] Rev 13326: implemented user account expiration

 

------------------------------------------------------------
revno: 13326
committer: Morten Olav Hansen <mortenoh@xxxxxxxxx>
branch nick: dhis2
timestamp: Thu 2013-12-19 12:50:58 +0100
message:
  implemented user account expiration
added:
  dhis-2/dhis-web/dhis-web-commons-resources/src/main/webapp/dhis-web-commons/javascripts/useraccount/expired.js
  dhis-2/dhis-web/dhis-web-commons-resources/src/main/webapp/dhis-web-commons/useraccount/expired.vm
  dhis-2/dhis-web/dhis-web-commons/src/main/java/org/hisp/dhis/security/CustomExceptionMappingAuthenticationFailureHandler.java
  dhis-2/dhis-web/dhis-web-commons/src/main/java/org/hisp/dhis/useraccount/action/ExpiredAccountAction.java
modified:
  dhis-2/dhis-web/dhis-web-api/src/main/java/org/hisp/dhis/api/controller/AccountController.java
  dhis-2/dhis-web/dhis-web-commons/src/main/resources/META-INF/dhis/beans.xml
  dhis-2/dhis-web/dhis-web-commons/src/main/resources/META-INF/dhis/security.xml
  dhis-2/dhis-web/dhis-web-commons/src/main/resources/dhis-web-commons.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-web/dhis-web-api/src/main/java/org/hisp/dhis/api/controller/AccountController.java'
--- dhis-2/dhis-web/dhis-web-api/src/main/java/org/hisp/dhis/api/controller/AccountController.java	2013-08-23 16:00:30 +0000
+++ dhis-2/dhis-web/dhis-web-api/src/main/java/org/hisp/dhis/api/controller/AccountController.java	2013-12-19 11:50:58 +0000
@@ -28,13 +28,7 @@
  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
 
-import java.util.Collection;
-import java.util.HashSet;
-
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
-
+import com.fasterxml.jackson.databind.ObjectMapper;
 import org.apache.commons.lang.StringUtils;
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
@@ -65,6 +59,17 @@
 import org.springframework.web.bind.annotation.ResponseBody;
 import org.springframework.web.client.RestTemplate;
 
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
 /**
  * @author Lars Helge Overland
  */
@@ -73,35 +78,37 @@
 public class AccountController
 {
     private static final Log log = LogFactory.getLog( AccountController.class );
-    
+
     private static final String RECAPTCHA_VERIFY_URL = "https://www.google.com/recaptcha/api/verify";;
     protected static final String PUB_KEY = "6LcM6tcSAAAAANwYsFp--0SYtcnze_WdYn8XwMMk";
     private static final String KEY = "6LcM6tcSAAAAAFnHo1f3lLstk3rZv3EVinNROfRq";
     private static final String TRUE = "true";
     private static final String SPLIT = "\n";
     private static final int MAX_LENGTH = 80;
-    
+
     @Autowired
     private RestTemplate restTemplate;
-    
+
     @Autowired
     private UserService userService;
-    
+
     @Autowired
     private AuthenticationManager authenticationManager;
-    
+
     @Autowired
     private ConfigurationService configurationService;
-    
+
     @Autowired
     private PasswordManager passwordManager;
-    
+
     @Autowired
     private SecurityService securityService;
-    
+
     @Autowired
     private SystemSettingManager systemSettingManager;
-    
+
+    private ObjectMapper objectMapper = new ObjectMapper();
+
     @RequestMapping( value = "/recovery", method = RequestMethod.POST, produces = ContextUtils.CONTENT_TYPE_TEXT )
     public @ResponseBody String recoverAccount(
         @RequestParam String username,
@@ -109,15 +116,15 @@
         HttpServletResponse response )
     {
         String rootPath = ContextUtils.getContextPath( request );
-        
+
         if ( !systemSettingManager.accountRecoveryEnabled() )
         {
             response.setStatus( HttpServletResponse.SC_CONFLICT );
             return "Account recovery is not enabled";
         }
-        
+
         boolean recover = securityService.sendRestoreMessage( username, rootPath );
-        
+
         if ( !recover )
         {
             response.setStatus( HttpServletResponse.SC_CONFLICT );
@@ -125,11 +132,11 @@
         }
 
         log.info( "Recovery message sent for user: " + username );
-        
+
         response.setStatus( HttpServletResponse.SC_OK );
         return "Recovery message sent";
     }
-    
+
     @RequestMapping( value = "/restore", method = RequestMethod.POST, produces = ContextUtils.CONTENT_TYPE_TEXT )
     public @ResponseBody String restoreAccount(
         @RequestParam String username,
@@ -137,42 +144,42 @@
         @RequestParam String code,
         @RequestParam String password,
         HttpServletRequest request,
-        HttpServletResponse response )        
+        HttpServletResponse response )
     {
         if ( !systemSettingManager.accountRecoveryEnabled() )
         {
             response.setStatus( HttpServletResponse.SC_CONFLICT );
             return "Account recovery is not enabled";
         }
-        
+
         if ( password == null || !ValidationUtils.passwordIsValid( password ) )
         {
             response.setStatus( HttpServletResponse.SC_BAD_REQUEST );
             return "Password is not specified or invalid";
         }
-        
+
         if ( password.trim().equals( username.trim() ) )
         {
             response.setStatus( HttpServletResponse.SC_BAD_REQUEST );
             return "Password cannot be equal to username";
         }
-        
+
         boolean restore = securityService.restore( username, token, code, password );
-        
+
         if ( !restore )
         {
             response.setStatus( HttpServletResponse.SC_BAD_REQUEST );
             return "Account could not be restored";
-        }        
+        }
 
         log.info( "Account restored for user: " + username );
-        
+
         response.setStatus( HttpServletResponse.SC_OK );
         return "Account restored";
     }
-    
+
     @RequestMapping( method = RequestMethod.POST, produces = ContextUtils.CONTENT_TYPE_TEXT )
-    public @ResponseBody String createAccount( 
+    public @ResponseBody String createAccount(
         @RequestParam String username,
         @RequestParam String firstName,
         @RequestParam String surname,
@@ -186,17 +193,17 @@
         HttpServletResponse response )
     {
         boolean allowed = configurationService.getConfiguration().selfRegistrationAllowed();
-        
+
         if ( !allowed )
         {
             response.setStatus( HttpServletResponse.SC_BAD_REQUEST );
             return "User self registration is not allowed";
         }
-        
+
         // ---------------------------------------------------------------------
         // Trim input
         // ---------------------------------------------------------------------
-        
+
         username = StringUtils.trimToNull( username );
         firstName = StringUtils.trimToNull( firstName );
         surname = StringUtils.trimToNull( surname );
@@ -210,21 +217,21 @@
         // ---------------------------------------------------------------------
         // Validate input, return 400 if invalid
         // ---------------------------------------------------------------------
-        
+
         if ( username == null || username.trim().length() > MAX_LENGTH )
         {
             response.setStatus( HttpServletResponse.SC_BAD_REQUEST );
             return "User name is not specified or invalid";
         }
-        
+
         UserCredentials credentials = userService.getUserCredentialsByUsername( username );
-        
+
         if ( credentials != null )
         {
             response.setStatus( HttpServletResponse.SC_BAD_REQUEST );
             return "User name is alread taken";
         }
-        
+
         if ( firstName == null || firstName.trim().length() > MAX_LENGTH )
         {
             response.setStatus( HttpServletResponse.SC_BAD_REQUEST );
@@ -242,19 +249,19 @@
             response.setStatus( HttpServletResponse.SC_BAD_REQUEST );
             return "Password is not specified or invalid";
         }
-        
+
         if ( password.trim().equals( username.trim() ) )
         {
             response.setStatus( HttpServletResponse.SC_BAD_REQUEST );
             return "Password cannot be equal to username";
         }
-        
+
         if ( email == null || !ValidationUtils.emailIsValid( email ) )
         {
             response.setStatus( HttpServletResponse.SC_BAD_REQUEST );
             return "Email is not specified or invalid";
         }
-        
+
         if ( phoneNumber == null || phoneNumber.trim().length() > 30 )
         {
             response.setStatus( HttpServletResponse.SC_BAD_REQUEST );
@@ -278,11 +285,11 @@
             response.setStatus( HttpServletResponse.SC_BAD_REQUEST );
             return "Recaptcha response must be specified";
         }
-        
+
         // ---------------------------------------------------------------------
         // Check result from API, return 500 if not
         // ---------------------------------------------------------------------
-        
+
         String[] results = checkRecaptcha( KEY, request.getRemoteAddr(), recapChallenge, recapResponse );
 
         if ( results == null || results.length == 0 )
@@ -294,10 +301,10 @@
         // ---------------------------------------------------------------------
         // Check if verification was successful, return 400 if not
         // ---------------------------------------------------------------------
-        
+
         if ( !TRUE.equalsIgnoreCase( results[0] ) )
-        {            
-            log.info( "Recaptcha failed with code: " + ( results.length > 0 ? results[1] : "" ) );
+        {
+            log.info( "Recaptcha failed with code: " + (results.length > 0 ? results[1] : "") );
 
             response.setStatus( HttpServletResponse.SC_BAD_REQUEST );
             return "The characters you entered did not match the word verification, try again";
@@ -306,10 +313,10 @@
         // ---------------------------------------------------------------------
         // Create and save user, return 201
         // ---------------------------------------------------------------------
-        
+
         UserAuthorityGroup userRole = configurationService.getConfiguration().getSelfRegistrationRole();
         OrganisationUnit orgUnit = configurationService.getConfiguration().getSelfRegistrationOrgUnit();
-        
+
         User user = new User();
         user.setFirstName( firstName );
         user.setSurname( surname );
@@ -317,38 +324,84 @@
         user.setPhoneNumber( phoneNumber );
         user.setEmployer( employer );
         user.getOrganisationUnits().add( orgUnit );
-        
+
         credentials = new UserCredentials();
         credentials.setUsername( username );
         credentials.setPassword( passwordManager.encodePassword( username, password ) );
         credentials.setSelfRegistered( true );
         credentials.setUser( user );
         credentials.getUserAuthorityGroups().add( userRole );
-        
+
         user.setUserCredentials( credentials );
-        
+
         userService.addUser( user );
         userService.addUserCredentials( credentials );
 
         authenticate( username, password, userRole, request );
-        
+
         log.info( "Created user with username: " + username );
-        
+
         response.setStatus( HttpServletResponse.SC_CREATED );
         return "Account created";
     }
-    
+
+    @RequestMapping( method = RequestMethod.PUT, produces = ContextUtils.CONTENT_TYPE_TEXT )
+    public @ResponseBody String updatePassword(
+        @RequestParam String oldPassword,
+        @RequestParam String password,
+        HttpServletRequest request,
+        HttpServletResponse response ) throws IOException
+    {
+        String username = (String) request.getSession().getAttribute( "username" );
+        UserCredentials credentials = userService.getUserCredentialsByUsername( username );
+
+        Map<String, String> result = new HashMap<String, String>();
+        result.put( "status", "OK" );
+
+        if ( userService.credentialsNonExpired( credentials ) )
+        {
+            response.setStatus( HttpServletResponse.SC_BAD_REQUEST );
+            result.put( "status", "NON_EXPIRED" );
+            result.put( "message", "Account is not expired, redirecting to login." );
+
+            return objectMapper.writeValueAsString( result );
+        }
+
+        String oldPasswordEncoded = passwordManager.encodePassword( username, oldPassword );
+
+        if ( !credentials.getPassword().equals( oldPasswordEncoded ) )
+        {
+            response.setStatus( HttpServletResponse.SC_BAD_REQUEST );
+            result.put( "status", "NON_MATCHING_PASSWORD" );
+            result.put( "message", "Old password is wrong, please correct and try again." );
+
+            return objectMapper.writeValueAsString( result );
+        }
+
+        String passwordEncoded = passwordManager.encodePassword( username, password );
+
+        credentials.setPassword( passwordEncoded );
+        credentials.setPasswordLastUpdated( new Date() );
+        userService.updateUserCredentials( credentials );
+
+        authenticate( username, password, getAuthorities( credentials.getUserAuthorityGroups() ), request );
+
+        result.put( "message", "Account was updated." );
+
+        return objectMapper.writeValueAsString( result );
+    }
+
     @RequestMapping( value = "/username", method = RequestMethod.GET, produces = ContextUtils.CONTENT_TYPE_JSON )
     public @ResponseBody String validateUserName( @RequestParam String username )
     {
         boolean valid = username != null && userService.getUserCredentialsByUsername( username ) == null;
-        
+
         // Custom code required because of our hacked jQuery validation
-        
+
         return valid ? "{ \"response\": \"success\", \"message\": \"\" }" :
             "{ \"response\": \"error\", \"message\": \"Username is already taken\" }";
     }
-    
+
     // ---------------------------------------------------------------------
     // Supportive methods
     // ---------------------------------------------------------------------
@@ -356,7 +409,7 @@
     private String[] checkRecaptcha( String privateKey, String remoteIp, String challenge, String response )
     {
         MultiValueMap<String, String> params = new LinkedMultiValueMap<String, String>();
-        
+
         params.add( "privatekey", privateKey );
         params.add( "remoteip", remoteIp );
         params.add( "challenge", challenge );
@@ -365,33 +418,59 @@
         String result = restTemplate.postForObject( RECAPTCHA_VERIFY_URL, params, String.class );
 
         log.info( "Recaptcha result: " + result );
-        
+
         return result != null ? result.split( SPLIT ) : null;
     }
-    
+
+    private void authenticate( String username, String rawPassword, Collection<GrantedAuthority> authorities, HttpServletRequest request )
+    {
+        UsernamePasswordAuthenticationToken token =
+            new UsernamePasswordAuthenticationToken( username, rawPassword, authorities );
+
+        Authentication auth = authenticationManager.authenticate( token );
+
+        SecurityContextHolder.getContext().setAuthentication( auth );
+
+        HttpSession session = request.getSession();
+
+        session.setAttribute( "SPRING_SECURITY_CONTEXT", SecurityContextHolder.getContext() );
+    }
+
     private void authenticate( String username, String rawPassword, UserAuthorityGroup userRole, HttpServletRequest request )
     {
-        UsernamePasswordAuthenticationToken token = 
+        UsernamePasswordAuthenticationToken token =
             new UsernamePasswordAuthenticationToken( username, rawPassword, getAuthorities( userRole ) );
 
         Authentication auth = authenticationManager.authenticate( token );
-        
+
         SecurityContextHolder.getContext().setAuthentication( auth );
 
         HttpSession session = request.getSession();
-        
+
         session.setAttribute( "SPRING_SECURITY_CONTEXT", SecurityContextHolder.getContext() );
     }
-    
+
+    private Collection<GrantedAuthority> getAuthorities( Set<UserAuthorityGroup> userRoles )
+    {
+        Collection<GrantedAuthority> auths = new HashSet<GrantedAuthority>();
+
+        for ( UserAuthorityGroup userRole : userRoles )
+        {
+            auths.addAll( getAuthorities( userRole ) );
+        }
+
+        return auths;
+    }
+
     private Collection<GrantedAuthority> getAuthorities( UserAuthorityGroup userRole )
     {
         Collection<GrantedAuthority> auths = new HashSet<GrantedAuthority>();
-        
+
         for ( String auth : userRole.getAuthorities() )
         {
             auths.add( new SimpleGrantedAuthority( auth ) );
         }
-        
+
         return auths;
     }
 }

=== added file 'dhis-2/dhis-web/dhis-web-commons-resources/src/main/webapp/dhis-web-commons/javascripts/useraccount/expired.js'
--- dhis-2/dhis-web/dhis-web-commons-resources/src/main/webapp/dhis-web-commons/javascripts/useraccount/expired.js	1970-01-01 00:00:00 +0000
+++ dhis-2/dhis-web/dhis-web-commons-resources/src/main/webapp/dhis-web-commons/javascripts/useraccount/expired.js	2013-12-19 11:50:58 +0000
@@ -0,0 +1,79 @@
+/*
+ * Copyright (c) 2004-2013, 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.
+ */
+
+var validationRules = {
+	rules: {
+		oldPassword: {
+			required: true
+		},
+		password: {
+			required: true,
+			rangelength: [ 8, 80 ],
+			password: true,
+			notequalto : "#oldPassword"
+		},
+		retypePassword: {
+			required: true,
+			equalTo: "#password"
+		}
+	}
+};
+
+$( document ).ready( function() {
+	$( "#accountForm" ).validate( {
+		rules: validationRules.rules,
+		submitHandler: accountSubmitHandler,
+		errorPlacement: function( error, element ) {
+			element.parent( "td" ).append( "<br>" ).append( error );
+		}
+	} );
+} );
+
+function accountSubmitHandler()
+{
+	$( "#submitButton" ).attr( "disabled", "disabled" );
+
+	$.ajax( {
+		url: '../../api/account',
+		data: $( "#accountForm" ).serialize(),
+		type: 'put',
+		success: function( data ) {
+			window.location.href = "../../dhis-web-commons-about/redirect.action";
+		},
+		error: function( jqXHR, textStatus, errorThrown ) {
+            var data = JSON.parse(jqXHR.responseText);
+
+            if( data.status === 'NON_EXPIRED' ) {
+                window.location.href = "login.action";
+            }
+
+            $( "#messageSpan" ).show().text( data.message );
+			$( "#submitButton" ).removeAttr( "disabled" );
+		}
+	} );
+}

=== added file 'dhis-2/dhis-web/dhis-web-commons-resources/src/main/webapp/dhis-web-commons/useraccount/expired.vm'
--- dhis-2/dhis-web/dhis-web-commons-resources/src/main/webapp/dhis-web-commons/useraccount/expired.vm	1970-01-01 00:00:00 +0000
+++ dhis-2/dhis-web/dhis-web-commons-resources/src/main/webapp/dhis-web-commons/useraccount/expired.vm	2013-12-19 11:50:58 +0000
@@ -0,0 +1,62 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+    <title>$encoder.htmlEncode( $applicationTitle )</title>
+    <script type="text/javascript" src="../javascripts/jQuery/jquery.min.js"></script>
+    <script type="text/javascript" src="../javascripts/jQuery/jquery.validate.js"></script>
+    <script type="text/javascript" src="../javascripts/jQuery/jquery.validate.ext.js"></script>
+    <script type="text/javascript" src="../javascripts/useraccount/expired.js"></script>
+    <script type="text/javascript" src="../i18nJavaScript.action"></script>
+    <link type="text/css" rel="stylesheet" href="../css/account.css">
+</head>
+<body>
+
+<div id="accountHeader"></div>
+
+<div id="accountContainer">
+
+<div id="bannerArea"><a href="http://dhis2.org";><img src="../security/logo_front.png" style="border:none"></a></div>
+
+<div id="accountInput">
+
+<h3><span id="create_new_account">$i18n.getString( "change_password" )</span></h3>
+
+<form id="accountForm">
+
+<table>
+    <tr>
+        <td><label id="label_username" for="username">$i18n.getString( "user_name" )</label></td>
+        <td><input type="text" id="username" name="username" value="$username" disabled autocomplete}="off"></td>
+    </tr>
+    <tr>
+        <td><label id="label_oldpassword" for="oldPassword">$i18n.getString( "old_password" )</label></td>
+        <td><input type="password" id="oldPassword" name="oldPassword" autocomplete="off" placeholder="$i18n.getString( 'password_hint' )"></td>
+    </tr>
+    <tr>
+        <td><label id="label_password" for="password">$i18n.getString( "new_password" )</label></td>
+        <td><input type="password" id="password" name="password" autocomplete="off" placeholder="$i18n.getString( 'password_hint' )"></td>
+    </tr>
+    <tr>
+        <td><label id="label_retypePassword" for="retypePassword">$i18n.getString( "confirm_password" )</label></td>
+        <td><input type="password" id="retypePassword" name="retypePassword" autocomplete="off"></td>
+    </tr>
+
+    <tr>
+   		<td></td>
+   		<td><label id="messageSpan" class="error" style="display:none"></label></td>
+   	</tr>
+
+    <tr>
+    	<td></td>
+    	<td><input id="submitButton" type="submit" value="$i18n.getString( 'save' )" style="width:10em"></td>
+    </tr>
+</table>
+
+</form>
+
+</div>
+
+</div>
+
+</body>
+</html>

=== added file 'dhis-2/dhis-web/dhis-web-commons/src/main/java/org/hisp/dhis/security/CustomExceptionMappingAuthenticationFailureHandler.java'
--- dhis-2/dhis-web/dhis-web-commons/src/main/java/org/hisp/dhis/security/CustomExceptionMappingAuthenticationFailureHandler.java	1970-01-01 00:00:00 +0000
+++ dhis-2/dhis-web/dhis-web-commons/src/main/java/org/hisp/dhis/security/CustomExceptionMappingAuthenticationFailureHandler.java	2013-12-19 11:50:58 +0000
@@ -0,0 +1,51 @@
+package org.hisp.dhis.security;
+
+/*
+ * Copyright (c) 2004-2013, 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.springframework.security.core.AuthenticationException;
+import org.springframework.security.web.authentication.ExceptionMappingAuthenticationFailureHandler;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ * @author Morten Olav Hansen <mortenoh@xxxxxxxxx>
+ */
+public class CustomExceptionMappingAuthenticationFailureHandler extends ExceptionMappingAuthenticationFailureHandler
+{
+    @Override
+    public void onAuthenticationFailure( HttpServletRequest request, HttpServletResponse response, AuthenticationException exception ) throws IOException, ServletException
+    {
+        request.getSession().setAttribute( "username", request.getParameter( "j_username" ) );
+
+        super.onAuthenticationFailure( request, response, exception );
+    }
+}

=== added file 'dhis-2/dhis-web/dhis-web-commons/src/main/java/org/hisp/dhis/useraccount/action/ExpiredAccountAction.java'
--- dhis-2/dhis-web/dhis-web-commons/src/main/java/org/hisp/dhis/useraccount/action/ExpiredAccountAction.java	1970-01-01 00:00:00 +0000
+++ dhis-2/dhis-web/dhis-web-commons/src/main/java/org/hisp/dhis/useraccount/action/ExpiredAccountAction.java	2013-12-19 11:50:58 +0000
@@ -0,0 +1,84 @@
+package org.hisp.dhis.useraccount.action;
+
+/*
+ * Copyright (c) 2004-2013, 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 com.opensymphony.xwork2.Action;
+import org.apache.struts2.ServletActionContext;
+import org.hisp.dhis.user.UserCredentials;
+import org.hisp.dhis.user.UserService;
+import org.springframework.beans.factory.annotation.Autowired;
+
+/**
+ * @author Morten Olav Hansen <mortenoh@xxxxxxxxx>
+ */
+public class ExpiredAccountAction implements Action
+{
+    // -------------------------------------------------------------------------
+    // Dependencies
+    // -------------------------------------------------------------------------
+
+    private UserService userService;
+
+    @Autowired
+    public void setUserService( UserService userService )
+    {
+        this.userService = userService;
+    }
+
+    // -------------------------------------------------------------------------
+    // Getters and Setters
+    // -------------------------------------------------------------------------
+
+    private String username;
+
+    public String getUsername()
+    {
+        return username;
+    }
+
+    // -------------------------------------------------------------------------
+    // Action Impl
+    // -------------------------------------------------------------------------
+
+    @Override
+    public String execute() throws Exception
+    {
+        username = (String) ServletActionContext.getRequest().getSession().getAttribute( "username" );
+
+        UserCredentials credentials = userService.getUserCredentialsByUsername( username );
+
+        // check that the user is actually expired
+        if ( credentials != null && !userService.credentialsNonExpired( credentials ) )
+        {
+            return SUCCESS;
+        }
+
+        return ERROR;
+    }
+}

=== modified file 'dhis-2/dhis-web/dhis-web-commons/src/main/resources/META-INF/dhis/beans.xml'
--- dhis-2/dhis-web/dhis-web-commons/src/main/resources/META-INF/dhis/beans.xml	2013-12-18 08:26:45 +0000
+++ dhis-2/dhis-web/dhis-web-commons/src/main/resources/META-INF/dhis/beans.xml	2013-12-19 11:50:58 +0000
@@ -551,6 +551,9 @@
 
   <!-- User Account -->
 
+  <bean id="org.hisp.dhis.useraccount.action.ExpiredAccountAction" class="org.hisp.dhis.useraccount.action.ExpiredAccountAction"
+    scope="prototype" />
+
   <bean id="org.hisp.dhis.useraccount.action.GetCurrentUserAction" class="org.hisp.dhis.useraccount.action.GetCurrentUserAction"
     scope="prototype">
     <property name="currentUserService" ref="org.hisp.dhis.user.CurrentUserService" />

=== modified file 'dhis-2/dhis-web/dhis-web-commons/src/main/resources/META-INF/dhis/security.xml'
--- dhis-2/dhis-web/dhis-web-commons/src/main/resources/META-INF/dhis/security.xml	2013-12-18 15:28:59 +0000
+++ dhis-2/dhis-web/dhis-web-commons/src/main/resources/META-INF/dhis/security.xml	2013-12-19 11:50:58 +0000
@@ -45,10 +45,10 @@
   </sec:http>
 
   <bean id="securityExceptionTranslationHandler"
-    class="org.springframework.security.web.authentication.ExceptionMappingAuthenticationFailureHandler">
+    class="org.hisp.dhis.security.CustomExceptionMappingAuthenticationFailureHandler">
     <property name="exceptionMappings">
       <props>
-        <prop key="org.springframework.security.authentication.CredentialsExpiredException">/dhis-web-commons/security/login.action?expired=true</prop>
+        <prop key="org.springframework.security.authentication.CredentialsExpiredException">/dhis-web-commons/security/expired.action</prop>
       </props>
     </property>
     <property name="defaultFailureUrl" value="/dhis-web-commons/security/login.action?failed=true" />

=== modified file 'dhis-2/dhis-web/dhis-web-commons/src/main/resources/dhis-web-commons.xml'
--- dhis-2/dhis-web/dhis-web-commons/src/main/resources/dhis-web-commons.xml	2013-12-18 10:48:39 +0000
+++ dhis-2/dhis-web/dhis-web-commons/src/main/resources/dhis-web-commons.xml	2013-12-19 11:50:58 +0000
@@ -135,6 +135,11 @@
       <result name="error" type="redirect">login.action</result>
     </action>
 
+    <action name="expired" class="org.hisp.dhis.useraccount.action.ExpiredAccountAction">
+      <result name="success" type="velocity">/dhis-web-commons/useraccount/expired.vm</result>
+      <result name="error" type="redirect">login.action</result>
+    </action>
+
     <action name="recovery" class="org.hisp.dhis.useraccount.action.IsAccountRecoveryAllowedAction">
       <result name="success" type="velocity">/dhis-web-commons/useraccount/recovery.vm</result>
       <result name="error" type="redirect">login.action</result>