Changeset 7408
- Timestamp:
- Oct 6, 2017, 11:37:18 AM (5 years ago)
- Location:
- trunk
- Files:
-
- 3 added
- 9 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/src/core/common-queries.xml
r7312 r7408 3932 3932 </description> 3933 3933 </query> 3934 3935 <query id="GET_USER_DEVICE" type="HQL"> 3936 <sql> 3937 SELECT dev 3938 FROM UserDeviceData dev 3939 WHERE dev.user = :userId 3940 AND dev.client = :clientId 3941 AND dev.token = :token 3942 </sql> 3943 <description> 3944 HQL query that selects a device for a given user and client 3945 with a given device token. 3946 </description> 3947 </query> 3934 3948 3935 3949 </predefined-queries> -
trunk/src/core/net/sf/basedb/core/SessionControl.java
r7381 r7408 38 38 import net.sf.basedb.core.data.UserClientSettingData; 39 39 import net.sf.basedb.core.data.UserDefaultSettingData; 40 import net.sf.basedb.core.data.UserDeviceData; 40 41 import net.sf.basedb.core.hibernate.TypeWrapper; 41 42 import net.sf.basedb.core.data.ClientDefaultSettingData; 42 43 import net.sf.basedb.core.data.ContextData; 43 44 import net.sf.basedb.core.data.ContextIndex; 45 import net.sf.basedb.util.EmailUtil; 44 46 import net.sf.basedb.util.Enumeration; 47 import net.sf.basedb.util.MD5; 45 48 import net.sf.basedb.util.extensions.ExtensionsInvoker; 46 49 import net.sf.basedb.util.extensions.Registry; … … 57 60 import java.util.Locale; 58 61 import java.util.Set; 62 import java.util.UUID; 59 63 import java.util.Map; 60 64 import java.util.HashMap; 61 65 import java.util.HashSet; 62 66 import java.util.WeakHashMap; 67 63 68 import java.util.Collections; 64 69 import java.util.List; … … 133 138 private LoginInfo loginInfo; 134 139 140 /** 141 Temporary login information before a device is verified. 142 */ 143 private UnverifiedDeviceInfo unverifiedDeviceInfo; 144 135 145 /** 136 146 Map for storing current contexts. … … 302 312 return externalClientId; 303 313 } 314 315 /** 316 Get the id of the <code>UserDevice</code> in use. Use 317 {@link UserDevice#getById(DbControl, int)} to get the {@link UserDevice} object. 318 @return Device id as an int, 0 if the device is unknown 319 @since 3.12 320 */ 321 public int getDeviceId() 322 { 323 updateLastAccess(); 324 return loginInfo == null ? 0 : loginInfo.deviceId; 325 } 304 326 305 327 … … 384 406 org.hibernate.Session session = null; 385 407 org.hibernate.Transaction tx = null; 408 409 UserDeviceData device = null; 410 AuthenticatedUser authUser = null; 411 UserData user = null; 386 412 try 387 413 { … … 390 416 tx = HibernateUtil.newTransaction(session); 391 417 392 AuthenticatedUserauthUser = verifyUserExternal(session, loginRequest);418 authUser = verifyUserExternal(session, loginRequest); 393 419 if (authUser == null) 394 420 { … … 397 423 } 398 424 425 // The login was ok so far... check device verification 426 device = verifyDevice(session, loginRequest, authUser); 427 user = HibernateUtil.loadData(session, UserData.class, authUser.getInternalId()); 428 429 // A null value means that either device verification is disabled 430 // An existing device means that it is already verified 431 LoginInfo li = null; 432 if (device == null || device.getId() != 0) 433 { 434 // All is ok, finalize the login 435 li = createLoginInfo(session, user, loginRequest.getComment(), false, authUser.getAuthenticationMethod(), device == null ? 0 : device.getId()); 436 } 437 HibernateUtil.commit(tx); 438 439 if (li != null) 440 { 441 currentContexts.clear(); 442 allowedClients.clear(); 443 loginInfo = li; 444 } 445 } 446 catch (InterruptedException ex) 447 { 448 throw new LoginException("Login aborted"); 449 } 450 catch (BaseException ex) 451 { 452 if (tx != null) HibernateUtil.rollback(tx); 453 throw ex; 454 } 455 finally 456 { 457 if (session != null) HibernateUtil.close(session); 458 } 459 460 if (device != null && device.getId() == 0) 461 { 462 // Device verification is enabled and the user is using an unverified device 463 // Send verification code to the user and keep the information we have so far 464 // Once the user has got the verification code it is expected that the 465 // verifyDevice(String, boolean) method is called 466 467 UnverifiedDeviceInfo udi = new UnverifiedDeviceInfo(); 468 // Prepare the information that is needed to verify the device 469 udi.device = device; 470 udi.loginRequest = loginRequest; 471 udi.authenticatedUser = authUser; 472 udi.verificationCode = MD5.leftPad(Integer.toString((int)(Math.random()*1000000)), '0', 6); 473 474 udi.message = "A verification code has been sent to your registered email address: " + 475 "<b>" + user.getEmail() + "</b>\n" + 476 "Please enter the verification code in the form below to continue with the login."; 477 478 udi.message += "\n\nDEBUG!!! The verification code for device '" + udi.getDeviceToken() + "' is: " + udi.verificationCode; 479 unverifiedDeviceInfo = udi; 480 481 throw new DeviceNotVerifiedException(); 482 } 483 } 484 485 /** 486 Get information about a device that is currently waiting to 487 be verified. If this method returns a non-null value a verification 488 has been sent to the user via email. This code should be used as 489 a parameter when calling {@link #verifyDevice(String, boolean)}. 490 491 @return Information about the unverified device, or null if device verification 492 is not needed 493 @since 3.12 494 */ 495 public UnverifiedDeviceInfo getUnverifiedDeviceInfo() 496 { 497 return unverifiedDeviceInfo; 498 } 499 500 /** 501 Call this method to verify a device. If device verification is not needed 502 at this moment, an IllegalStateException is thrown. If the verification 503 fails the login process must be re-started from the beginning. There is no 504 second try. If the verification is successful, the user will be logged in 505 after this method completes. 506 507 @param code The verification code that was sent (by email) to the user 508 @param rememberDevice A flag indicating if the device should be remembered. 509 If not, the device needs to be verified again the next time it is used 510 @since 3.12 511 @see #getUnverifiedDeviceInfo() 512 */ 513 public synchronized void verifyDevice(String code, boolean rememberDevice) 514 { 515 if (unverifiedDeviceInfo == null) 516 { 517 throw new IllegalStateException("No device is waiting for verification."); 518 } 519 org.hibernate.Session session = null; 520 org.hibernate.Transaction tx = null; 521 try 522 { 523 // Check the verification code! 524 if (!unverifiedDeviceInfo.verificationCode.equals(code)) 525 { 526 // Not correct. Login must be re-started 527 throw new LoginException("The verification code was not correct."); 528 } 529 530 // The verification code was correct, finalize the login 531 session = HibernateUtil.newSession(); 532 tx = HibernateUtil.newTransaction(session); 533 534 LoginRequest loginRequest = unverifiedDeviceInfo.loginRequest; 535 AuthenticatedUser authUser = unverifiedDeviceInfo.authenticatedUser; 536 537 int deviceId = 0; 538 if (rememberDevice) 539 { 540 HibernateUtil.saveData(session, unverifiedDeviceInfo.device); 541 deviceId = unverifiedDeviceInfo.device.getId(); 542 } 543 399 544 UserData user = HibernateUtil.loadData(session, UserData.class, authUser.getInternalId()); 545 LoginInfo li = createLoginInfo(session, user, loginRequest.getComment(), false, authUser.getAuthenticationMethod(), deviceId); 546 HibernateUtil.commit(tx); 400 547 401 LoginInfo li = createLoginInfo(session, user, loginRequest.getComment(), false, authUser.getAuthenticationMethod());402 HibernateUtil.commit(tx);403 548 currentContexts.clear(); 404 549 allowedClients.clear(); 405 550 loginInfo = li; 406 551 } 407 catch (InterruptedException ex)408 {409 throw new LoginException("Login aborted");410 }411 catch (BaseException ex)412 {413 if (tx != null) HibernateUtil.rollback(tx);414 throw ex;415 }416 552 finally 417 553 { 554 unverifiedDeviceInfo = null; 418 555 if (session != null) HibernateUtil.close(session); 419 556 } 420 557 } 421 422 558 423 559 /** … … 608 744 609 745 /** 746 Verify the device the user is using. The verification is done if the 747 current client application supports it and if the user has enabled 748 device verification. If the device is found to be unverified a 749 UnverifiedLoginInfo instance is created and returned. 750 751 @return An {@link UnverifiedLoginInfo} object if device verification is 752 supported and needed, null to continue with normal login 753 @since 3.12 754 */ 755 private UserDeviceData verifyDevice(org.hibernate.Session session, LoginRequest loginRequest, AuthenticatedUser authUser) 756 { 757 // No email = no device verification 758 if (!EmailUtil.isEnabled()) return null; 759 760 // Check if the client application supports device verification 761 ClientData client = getClientId() != 0 ? HibernateUtil.loadData(session, ClientData.class, getClientId()) : null; 762 if (client == null || !client.getSupportsDeviceVerification()) return null; 763 764 // Check if the user has enabled device verification 765 UserData user = HibernateUtil.loadData(session, UserData.class, authUser.getInternalId()); 766 if (!user.getUseDeviceVerification()) return null; 767 768 // Check the submitted deviceToken 769 String deviceToken = loginRequest.getDeviceToken(); 770 String userAgent = loginRequest.getAttribute("user-agent"); 771 UserDeviceData device = null; 772 773 if (deviceToken != null) 774 { 775 org.hibernate.query.Query<UserDeviceData> query = HibernateUtil.getPredefinedQuery(session, 776 "GET_USER_DEVICE", UserDeviceData.class); 777 /* 778 SELECT dev 779 FROM UserDeviceData dev 780 WHERE dev.user = :userId 781 AND dev.client = :clientId 782 AND dev.token = :token 783 */ 784 query.setParameter("userId", authUser.getInternalId(), TypeWrapper.H_INTEGER); 785 query.setParameter("clientId", clientId, TypeWrapper.H_INTEGER); 786 query.setParameter("token", deviceToken, TypeWrapper.H_STRING); 787 device = HibernateUtil.loadData(query); 788 789 if (device != null) 790 { 791 // This device is already verified 792 // We update the user agent string since it may be different due to version upgrade 793 if (userAgent != null) device.setUserAgent(userAgent); 794 // And the lastUsed date 795 device.setLastUsed(new Date()); 796 } 797 } 798 799 if (device == null) 800 { 801 // The user is using an unverified device 802 if (deviceToken != null) 803 { 804 // If the submitted deviceToken is already stored in the database (for any other user/client) 805 // we accept it as a possible valid device (that still needs to be verified for this 806 // particular user) 807 org.hibernate.query.Query<Long> query = HibernateUtil.createQuery(session, 808 "SELECT count(*) FROM UserDeviceData dev WHERE dev.token = :token", Long.class); 809 query.setParameter("token", deviceToken, TypeWrapper.H_STRING); 810 if (HibernateUtil.loadData(query) == 0) 811 { 812 // Not found, so we generate a new deviceToken 813 deviceToken = null; 814 } 815 } 816 if (deviceToken == null) deviceToken = UUID.randomUUID().toString(); 817 818 // Create the new device (but we do not save it until it has been verified!) 819 Date now = new Date(); 820 device = new UserDeviceData(); 821 device.setName("New device"); 822 device.setUser(user); 823 device.setClient(client); 824 device.setEntryDate(now); 825 device.setLastUsed(now); 826 device.setToken(deviceToken); 827 device.setUserAgent(userAgent); 828 } 829 return device; 830 } 831 832 833 /** 610 834 Log in as another user or create a clone of the currently logged in user's session. 611 835 If this call is successful, you will get a new <code>SessionControl</code> object which … … 639 863 // Load user data 640 864 UserData userData = HibernateUtil.loadData(session, UserData.class, userId); 641 LoginInfo li = createLoginInfo(session, userData, comment, true, getAuthenticationMethod() );865 LoginInfo li = createLoginInfo(session, userData, comment, true, getAuthenticationMethod(), getDeviceId()); 642 866 HibernateUtil.commit(tx); 643 867 SessionControl impersonated = Application.newSessionControl(getExternalClientId(), getRemoteId(), null); … … 741 965 Create a LoginInfo object and load all information that it needs. 742 966 */ 743 private LoginInfo createLoginInfo(org.hibernate.Session session, UserData userData, String comment, boolean impersonated, AuthenticationMethod authenticationMethod )967 private LoginInfo createLoginInfo(org.hibernate.Session session, UserData userData, String comment, boolean impersonated, AuthenticationMethod authenticationMethod, int deviceId) 744 968 throws BaseException 745 969 { … … 765 989 } 766 990 } 991 UserDeviceData deviceData = null; 992 li.deviceId = deviceId; 993 if (deviceId != 0) 994 { 995 deviceData = HibernateUtil.loadData(session, UserDeviceData.class, deviceId); 996 } 767 997 768 998 // Load settings … … 777 1007 sessionData.setUser(userData); 778 1008 sessionData.setClient(clientData); 1009 sessionData.setDevice(deviceData); 779 1010 sessionData.setLoginTime(new Date()); 780 1011 sessionData.setLoginComment(comment); … … 2412 2643 private AuthenticationMethod authenticationMethod; 2413 2644 2645 2646 /** 2647 The id of the {@link UserDevice} in use. 2648 @since 3.12 2649 */ 2650 private int deviceId; 2651 2414 2652 /** 2415 2653 The id of the {@link ProjectData} object of the active project. … … 2458 2696 this.userId = parent.userId; 2459 2697 this.userLogin = parent.userLogin; 2698 this.deviceId = parent.deviceId; 2460 2699 this.activeProjectId = parent.activeProjectId; 2461 2700 this.projectKeyId = parent.projectKeyId; … … 2467 2706 2468 2707 } 2708 2709 /** 2710 Class for storing temporary device information for a user 2711 that has been authenticated but before a device has been 2712 verified. 2713 2714 @since 3.12 2715 */ 2716 public static class UnverifiedDeviceInfo 2717 { 2718 UserDeviceData device; 2719 LoginRequest loginRequest; 2720 AuthenticatedUser authenticatedUser; 2721 2722 String verificationCode; 2723 String message; 2724 2725 /** 2726 Get the token for this device. If the verification is successful 2727 (see {@link SessionControl#verifyDevice(String, boolean)} the 2728 client must store this token and submit it together with the login 2729 information the next time the user is logging in. 2730 */ 2731 public String getDeviceToken() 2732 { 2733 return device.getToken(); 2734 } 2735 2736 /** 2737 Get a message to display for the user on the form where the 2738 verification code should be entered. 2739 */ 2740 public String getVerificationSentMessage() 2741 { 2742 return message; 2743 } 2744 2745 /** 2746 The preferred setting for the "rememberDevice" parameter when 2747 verifying the device. 2748 */ 2749 public boolean getDefaultRememberDevice() 2750 { 2751 return true; 2752 } 2753 } 2754 2469 2755 2470 2756 /** -
trunk/src/core/net/sf/basedb/core/authentication/LoginRequest.java
r6427 r7408 38 38 private String login; 39 39 private String password; 40 private String deviceToken; 40 41 private String comment; 41 42 … … 55 56 56 57 /** 58 Create a login request with login + password + deviceToken 59 @since 3.12 60 */ 61 public LoginRequest(String login, String password, String deviceToken) 62 { 63 this.login = login; 64 this.password = password; 65 this.deviceToken = deviceToken; 66 } 67 68 /** 57 69 Create a login request with user id + password 58 70 */ … … 62 74 this.password = password; 63 75 } 76 77 /** 78 Create a login request with user id + password + deviceToken 79 @since 3.12 80 */ 81 public LoginRequest(int userId, String password, String deviceToken) 82 { 83 this.userId = userId; 84 this.password = password; 85 this.deviceToken = deviceToken; 86 } 87 64 88 65 89 /** … … 112 136 113 137 /** 138 Set the deviceToken to use. 139 @since 3.12 140 */ 141 public void setDeviceToken(String deviceToken) 142 { 143 this.deviceToken = deviceToken; 144 } 145 146 /** 147 Get the deviceToken to use. 148 @since 3.12 149 */ 150 public String getDeviceToken() 151 { 152 return deviceToken; 153 } 154 155 /** 114 156 Set the comment to use. 115 157 */ -
trunk/www/exception/not_logged_in.jsp
r7114 r7408 101 101 <input type="hidden" name="redirect" value="<%=redirect%>"> 102 102 <input type="hidden" name="useAutoStartPage" value="0"> 103 <input type="hidden" name="deviceToken" value=""> 103 104 104 105 <table style="margin: auto; width: 700px; margin-top:5em; "> -
trunk/www/login.js
r7352 r7408 198 198 199 199 if (frm.target) Dialogs.openPopup('', frm.target, 300, 200); 200 // Set the deviceToken if we have it in local storage 201 if (frm.deviceToken) 202 { 203 var deviceToken = App.getLocal('deviceToken'); 204 if (deviceToken) frm.deviceToken.value = deviceToken; 205 } 200 206 if (!pUseLastLogin) 201 207 { -
trunk/www/login.jsp
r7295 r7408 59 59 String errorTitle = null; 60 60 String errorMessage = null; 61 String message = null; 61 62 String login = request.getParameter("login"); 62 63 … … 64 65 { 65 66 String password = request.getParameter("password"); 67 String deviceToken = Values.getStringOrNull(request.getParameter("deviceToken")); 66 68 try 67 69 { 68 70 if (sc.isLoggedIn()) sc.logout(); 69 LoginRequest loginRequest = new LoginRequest(login, password); 71 LoginRequest loginRequest = new LoginRequest(login, password, deviceToken); 72 loginRequest.setAttribute("user-agent", request.getHeader("User-Agent")); 70 73 sc.login(loginRequest); 71 74 useAutoStartPage = Values.getBoolean(request.getParameter("useAutoStartPage")); 72 75 } 76 catch (DeviceNotVerifiedException ex) 77 { 78 message = "Device not verified"; 79 redirect = root + "verify_device.jsp?ID=" + sc.getId(); 80 } 73 81 catch (LoginException ex) 74 82 { … … 89 97 { 90 98 errorTitle = "Permission denied"; 99 errorMessage = ex.getMessage(); 100 } 101 } 102 else if ("VerifyDevice".equals(cmd)) 103 { 104 String verificationCode = request.getParameter("verificationCode"); 105 boolean rememberDevice = Values.getBoolean(request.getParameter("rememberDevice")); 106 try 107 { 108 sc.verifyDevice(verificationCode, rememberDevice); 109 } 110 catch (LoginException ex) 111 { 112 errorTitle = "Login failed"; 91 113 errorMessage = ex.getMessage(); 92 114 } … … 190 212 else 191 213 { 214 if (message == null) message = "Login successful"; 192 215 if (redirect == null) 193 216 { 194 response.sendRedirect(root+"common/close_popup.jsp?message= Login+successful&refresh_opener=1");217 response.sendRedirect(root+"common/close_popup.jsp?message="+HTML.urlEncode(message)+"&refresh_opener=1"); 195 218 } 196 219 else 197 220 { 198 response.sendRedirect(root+"common/close_popup.jsp?message= Login+successful&redirect_opener="+HTML.urlEncode(redirect));221 response.sendRedirect(root+"common/close_popup.jsp?message="+HTML.urlEncode(message)+"&redirect_opener="+HTML.urlEncode(redirect)); 199 222 } 200 223 } -
trunk/www/main.jsp
r7394 r7408 112 112 <input type="hidden" name="ID" value="<%=ID%>"> 113 113 <input type="hidden" name="useAutoStartPage" value="1"> 114 <input type="hidden" name="deviceToken" value=""> 114 115 115 116 <table style="margin: auto; width: 700px;"> -
trunk/www/switch.jsp
r7114 r7408 93 93 <input type="hidden" name="redirect" value=""> 94 94 <input type="hidden" name="useAutoStartPage" value="0"> 95 <input type="hidden" name="deviceToken" value=""> 95 96 96 97 <div class="content"> -
trunk/www/views/devices/list_devices.jsp
r7407 r7408 323 323 if (devices != null) 324 324 { 325 int currentDeviceId = sc.getDeviceId(); 325 326 while (devices.hasNext()) 326 327 { … … 363 364 clazz="icons" 364 365 visible="<%=mode.hasIcons()%>" 365 > </tbl:header> 366 ><base:icon 367 image="star.png" 368 tooltip="This is the current device" 369 visible="<%=itemId == currentDeviceId%>" 370 /> </tbl:header> 366 371 <tbl:cell column="name"><div 367 372 class="link table-item"
Note: See TracChangeset
for help on using the changeset viewer.