Changeset 7408


Ignore:
Timestamp:
Oct 6, 2017, 11:37:18 AM (5 years ago)
Author:
Nicklas Nordborg
Message:

References #2097: Implement support for device verification

The major part of device verification should now be implemented. If the web application has a stored token it is submitted with the login information (LoginRequest.setDeviceToken()). The SessionControl.login() method will check if the device is known or not.

If not, a DeviceNotVerifiedException is thrown and the user is taken to the verify_device.jsp page. The code should be sent by email but is currently only display on that page (to be fixed!). If the verification code is correct, information about the device is stored in the database so that the user can be allowed access immediately the next time.

Location:
trunk
Files:
3 added
9 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/core/common-queries.xml

    r7312 r7408  
    39323932    </description>
    39333933  </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>
    39343948
    39353949</predefined-queries>
  • trunk/src/core/net/sf/basedb/core/SessionControl.java

    r7381 r7408  
    3838import net.sf.basedb.core.data.UserClientSettingData;
    3939import net.sf.basedb.core.data.UserDefaultSettingData;
     40import net.sf.basedb.core.data.UserDeviceData;
    4041import net.sf.basedb.core.hibernate.TypeWrapper;
    4142import net.sf.basedb.core.data.ClientDefaultSettingData;
    4243import net.sf.basedb.core.data.ContextData;
    4344import net.sf.basedb.core.data.ContextIndex;
     45import net.sf.basedb.util.EmailUtil;
    4446import net.sf.basedb.util.Enumeration;
     47import net.sf.basedb.util.MD5;
    4548import net.sf.basedb.util.extensions.ExtensionsInvoker;
    4649import net.sf.basedb.util.extensions.Registry;
     
    5760import java.util.Locale;
    5861import java.util.Set;
     62import java.util.UUID;
    5963import java.util.Map;
    6064import java.util.HashMap;
    6165import java.util.HashSet;
    6266import java.util.WeakHashMap;
     67
    6368import java.util.Collections;
    6469import java.util.List;
     
    133138  private LoginInfo loginInfo;
    134139 
     140  /**
     141    Temporary login information before a device is verified.
     142  */
     143  private UnverifiedDeviceInfo unverifiedDeviceInfo;
     144
    135145  /**
    136146    Map for storing current contexts.
     
    302312    return externalClientId;
    303313  }
     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  }
    304326
    305327 
     
    384406    org.hibernate.Session session = null;
    385407    org.hibernate.Transaction tx = null;
     408   
     409    UserDeviceData device = null;
     410    AuthenticatedUser authUser = null;
     411    UserData user = null;
    386412    try
    387413    {
     
    390416      tx = HibernateUtil.newTransaction(session);
    391417
    392       AuthenticatedUser authUser = verifyUserExternal(session, loginRequest);
     418      authUser = verifyUserExternal(session, loginRequest);
    393419      if (authUser == null)
    394420      {
     
    397423      }
    398424     
     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     
    399544      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);
    400547     
    401       LoginInfo li = createLoginInfo(session, user, loginRequest.getComment(), false, authUser.getAuthenticationMethod());
    402       HibernateUtil.commit(tx);
    403548      currentContexts.clear();
    404549      allowedClients.clear();
    405550      loginInfo = li;
    406551    }
    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     }
    416552    finally
    417553    {
     554      unverifiedDeviceInfo = null;
    418555      if (session != null) HibernateUtil.close(session);
    419556    }
    420557  }
    421  
    422558
    423559  /**
     
    608744
    609745  /**
     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  /**
    610834    Log in as another user or create a clone of the currently logged in user's session.
    611835    If this call is successful, you will get a new <code>SessionControl</code> object which
     
    639863      // Load user data
    640864      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());
    642866      HibernateUtil.commit(tx);
    643867      SessionControl impersonated = Application.newSessionControl(getExternalClientId(), getRemoteId(), null);
     
    741965    Create a LoginInfo object and load all information that it needs.
    742966  */
    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)
    744968    throws BaseException
    745969  {
     
    765989      }
    766990    }
     991    UserDeviceData deviceData = null;
     992    li.deviceId = deviceId;
     993    if (deviceId != 0)
     994    {
     995      deviceData = HibernateUtil.loadData(session, UserDeviceData.class, deviceId);
     996    }
    767997
    768998    // Load settings
     
    7771007    sessionData.setUser(userData);
    7781008    sessionData.setClient(clientData);
     1009    sessionData.setDevice(deviceData);
    7791010    sessionData.setLoginTime(new Date());
    7801011    sessionData.setLoginComment(comment);
     
    24122643    private AuthenticationMethod authenticationMethod;
    24132644   
     2645   
     2646    /**
     2647      The id of the {@link UserDevice} in use.
     2648      @since 3.12
     2649    */
     2650    private int deviceId;
     2651   
    24142652    /**
    24152653      The id of the {@link ProjectData} object of the active project.
     
    24582696      this.userId = parent.userId;
    24592697      this.userLogin = parent.userLogin;
     2698      this.deviceId = parent.deviceId;
    24602699      this.activeProjectId = parent.activeProjectId;
    24612700      this.projectKeyId = parent.projectKeyId;
     
    24672706   
    24682707  }
     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
    24692755 
    24702756  /**
  • trunk/src/core/net/sf/basedb/core/authentication/LoginRequest.java

    r6427 r7408  
    3838  private String login;
    3939  private String password;
     40  private String deviceToken;
    4041  private String comment;
    4142 
     
    5556 
    5657  /**
     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  /**
    5769    Create a login request with user id + password
    5870  */
     
    6274    this.password = password;
    6375  }
     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
    6488 
    6589  /**
     
    112136 
    113137  /**
     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  /**
    114156    Set the comment to use.
    115157  */
  • trunk/www/exception/not_logged_in.jsp

    r7114 r7408  
    101101      <input type="hidden" name="redirect" value="<%=redirect%>">
    102102      <input type="hidden" name="useAutoStartPage" value="0">
     103      <input type="hidden" name="deviceToken" value="">
    103104 
    104105      <table style="margin: auto; width: 700px; margin-top:5em; ">
  • trunk/www/login.js

    r7352 r7408  
    198198   
    199199    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    }
    200206    if (!pUseLastLogin)
    201207    {
  • trunk/www/login.jsp

    r7295 r7408  
    5959  String errorTitle = null;
    6060  String errorMessage = null;
     61  String message = null;
    6162  String login = request.getParameter("login");
    6263   
     
    6465  {
    6566    String password = request.getParameter("password");
     67    String deviceToken = Values.getStringOrNull(request.getParameter("deviceToken"));
    6668    try
    6769    {
    6870      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"));
    7073      sc.login(loginRequest);
    7174      useAutoStartPage = Values.getBoolean(request.getParameter("useAutoStartPage"));
    7275    }
     76    catch (DeviceNotVerifiedException ex)
     77    {
     78      message = "Device not verified";
     79      redirect = root + "verify_device.jsp?ID=" + sc.getId();
     80    }
    7381    catch (LoginException ex)
    7482    {
     
    8997    {
    9098      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";
    91113      errorMessage = ex.getMessage();
    92114    }
     
    190212    else
    191213    {
     214      if (message == null) message = "Login successful";
    192215      if (redirect == null)
    193216      {
    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");
    195218      }
    196219      else
    197220      {
    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));
    199222      }
    200223    }
  • trunk/www/main.jsp

    r7394 r7408  
    112112    <input type="hidden" name="ID" value="<%=ID%>">
    113113    <input type="hidden" name="useAutoStartPage" value="1">
     114    <input type="hidden" name="deviceToken" value="">
    114115   
    115116    <table style="margin: auto; width: 700px;">
  • trunk/www/switch.jsp

    r7114 r7408  
    9393    <input type="hidden" name="redirect" value="">
    9494    <input type="hidden" name="useAutoStartPage" value="0">
     95    <input type="hidden" name="deviceToken" value="">
    9596 
    9697    <div class="content">
  • trunk/www/views/devices/list_devices.jsp

    r7407 r7408  
    323323          if (devices != null)
    324324          {
     325            int currentDeviceId = sc.getDeviceId();
    325326            while (devices.hasNext())
    326327            {
     
    363364                  clazz="icons"
    364365                  visible="<%=mode.hasIcons()%>"
    365                   >&nbsp;</tbl:header>
     366                  ><base:icon
     367                    image="star.png"
     368                    tooltip="This is the current device"
     369                    visible="<%=itemId == currentDeviceId%>"
     370                  />&nbsp;</tbl:header>
    366371                <tbl:cell column="name"><div
    367372                  class="link table-item"
Note: See TracChangeset for help on using the changeset viewer.