extensions/net.sf.basedb.otp/trunk/src/net/sf/basedb/otp/OtpServlet.java

Code
Comments
Other
Rev Date Author Line
4850 14 Jun 18 nicklas 1 package net.sf.basedb.otp;
4850 14 Jun 18 nicklas 2
4850 14 Jun 18 nicklas 3 import java.awt.image.BufferedImage;
4850 14 Jun 18 nicklas 4 import java.io.ByteArrayOutputStream;
4850 14 Jun 18 nicklas 5 import java.io.IOException;
4850 14 Jun 18 nicklas 6 import java.util.Base64;
4852 15 Jun 18 nicklas 7 import java.util.Collections;
4852 15 Jun 18 nicklas 8 import java.util.Map;
4852 15 Jun 18 nicklas 9 import java.util.WeakHashMap;
4850 14 Jun 18 nicklas 10
4850 14 Jun 18 nicklas 11 import javax.imageio.ImageIO;
4851 14 Jun 18 nicklas 12 import javax.security.auth.login.LoginException;
4850 14 Jun 18 nicklas 13 import javax.servlet.ServletException;
4850 14 Jun 18 nicklas 14 import javax.servlet.http.HttpServlet;
4850 14 Jun 18 nicklas 15 import javax.servlet.http.HttpServletRequest;
4850 14 Jun 18 nicklas 16 import javax.servlet.http.HttpServletResponse;
4850 14 Jun 18 nicklas 17
4850 14 Jun 18 nicklas 18 import org.json.simple.JSONObject;
4850 14 Jun 18 nicklas 19
4850 14 Jun 18 nicklas 20 import com.warrenstrange.googleauth.GoogleAuthenticator;
4850 14 Jun 18 nicklas 21 import com.warrenstrange.googleauth.GoogleAuthenticatorKey;
4850 14 Jun 18 nicklas 22 import com.warrenstrange.googleauth.GoogleAuthenticatorQRGenerator;
4850 14 Jun 18 nicklas 23
4850 14 Jun 18 nicklas 24 import io.nayuki.qrcodegen.QrCode;
4850 14 Jun 18 nicklas 25 import net.sf.basedb.clients.web.Base;
4850 14 Jun 18 nicklas 26 import net.sf.basedb.core.Application;
4857 18 Jun 18 nicklas 27 import net.sf.basedb.core.DbControl;
4850 14 Jun 18 nicklas 28 import net.sf.basedb.core.SessionControl;
4857 18 Jun 18 nicklas 29 import net.sf.basedb.core.User;
4851 14 Jun 18 nicklas 30 import net.sf.basedb.core.authentication.LoginRequest;
4850 14 Jun 18 nicklas 31 import net.sf.basedb.util.Values;
4850 14 Jun 18 nicklas 32 import net.sf.basedb.util.error.ThrowableUtil;
4850 14 Jun 18 nicklas 33
4850 14 Jun 18 nicklas 34
4850 14 Jun 18 nicklas 35
4850 14 Jun 18 nicklas 36 /**
4850 14 Jun 18 nicklas 37   Servlet class for performing OTP-related tasks
4850 14 Jun 18 nicklas 38   for the web interface.
4850 14 Jun 18 nicklas 39
4850 14 Jun 18 nicklas 40   @author nicklas
4850 14 Jun 18 nicklas 41   @since 1.0
4850 14 Jun 18 nicklas 42 */
4850 14 Jun 18 nicklas 43 public class OtpServlet 
4850 14 Jun 18 nicklas 44   extends HttpServlet 
4850 14 Jun 18 nicklas 45 {
4850 14 Jun 18 nicklas 46
4850 14 Jun 18 nicklas 47   private static final long serialVersionUID = -2684685679966855942L;
4850 14 Jun 18 nicklas 48
4852 15 Jun 18 nicklas 49   private static final Map<SessionControl, String> temporaryKeys = 
4852 15 Jun 18 nicklas 50     Collections.synchronizedMap(new WeakHashMap<>());
4851 14 Jun 18 nicklas 51   
4850 14 Jun 18 nicklas 52   public OtpServlet()
4850 14 Jun 18 nicklas 53   {}
4850 14 Jun 18 nicklas 54
4850 14 Jun 18 nicklas 55   @Override
4850 14 Jun 18 nicklas 56   protected void doGet(HttpServletRequest req, HttpServletResponse resp)
4850 14 Jun 18 nicklas 57     throws ServletException, IOException 
4850 14 Jun 18 nicklas 58   {
4850 14 Jun 18 nicklas 59     final String ID = req.getParameter("ID");
4850 14 Jun 18 nicklas 60     final String cmd = req.getParameter("cmd");
4850 14 Jun 18 nicklas 61
4851 14 Jun 18 nicklas 62     JsonUtil.setJsonResponseHeaders(resp);
4850 14 Jun 18 nicklas 63     
4850 14 Jun 18 nicklas 64     JSONObject json = new JSONObject();
4850 14 Jun 18 nicklas 65     json.put("status", "ok");
4923 10 Aug 18 nicklas 66     
4923 10 Aug 18 nicklas 67     DbControl dc = null;
4850 14 Jun 18 nicklas 68     try
4850 14 Jun 18 nicklas 69     {
4850 14 Jun 18 nicklas 70       
4850 14 Jun 18 nicklas 71       if ("CreateQRCode".equals(cmd))
4850 14 Jun 18 nicklas 72       {
4923 10 Aug 18 nicklas 73         final SessionControl sc = Application.getSessionControl(ID, Base.WEBCLIENT_ID, req.getRemoteAddr(), false);
4923 10 Aug 18 nicklas 74         
4850 14 Jun 18 nicklas 75         String username = Values.getStringOrNull(req.getParameter("username"));
4850 14 Jun 18 nicklas 76         if (username == null)
4850 14 Jun 18 nicklas 77         {
4850 14 Jun 18 nicklas 78           throw new NullPointerException("A username must be specified");
4850 14 Jun 18 nicklas 79         }
4850 14 Jun 18 nicklas 80         
4850 14 Jun 18 nicklas 81         // Generate a random key (with twice the default length = 160 bits)
4850 14 Jun 18 nicklas 82         GoogleAuthenticator gAuth = new GoogleAuthenticator();
4850 14 Jun 18 nicklas 83         String key1 = gAuth.createCredentials().getKey();
4850 14 Jun 18 nicklas 84         String key2 = gAuth.createCredentials().getKey();
4850 14 Jun 18 nicklas 85         GoogleAuthenticatorKey gKey = new GoogleAuthenticatorKey.Builder(key1+key2).build();
4850 14 Jun 18 nicklas 86
4850 14 Jun 18 nicklas 87         // Use host name + path which should give the URL to the BASE server
4850 14 Jun 18 nicklas 88         String issuer = Application.getHostName() + req.getContextPath();
4850 14 Jun 18 nicklas 89         String totpURL = GoogleAuthenticatorQRGenerator.getOtpAuthTotpURL(issuer, username, gKey);
4850 14 Jun 18 nicklas 90       
4850 14 Jun 18 nicklas 91         // Create the QR code
4850 14 Jun 18 nicklas 92         QrCode qrCode = QrCode.encodeText(totpURL, QrCode.Ecc.LOW);
4850 14 Jun 18 nicklas 93
4850 14 Jun 18 nicklas 94         // And get it as a png image
4850 14 Jun 18 nicklas 95         BufferedImage img = qrCode.toImage(3, 0);
4850 14 Jun 18 nicklas 96         ByteArrayOutputStream png = new ByteArrayOutputStream(1024);
4850 14 Jun 18 nicklas 97         ImageIO.write(img, "png", png);
4850 14 Jun 18 nicklas 98         
4850 14 Jun 18 nicklas 99         // Encode it as Base64 to let the web client use <img src="data:image/png;base64,...">
4850 14 Jun 18 nicklas 100         String pngInBase64 = Base64.getEncoder().encodeToString(png.toByteArray());
4850 14 Jun 18 nicklas 101         json.put("image", pngInBase64);
4851 14 Jun 18 nicklas 102         
4852 15 Jun 18 nicklas 103         temporaryKeys.put(sc, gKey.getKey());
4923 10 Aug 18 nicklas 104       }
4923 10 Aug 18 nicklas 105       else if ("DisplayQRCode".equals(cmd))
4923 10 Aug 18 nicklas 106       {
4923 10 Aug 18 nicklas 107         final SessionControl sc = Application.getSessionControl(ID, Base.WEBCLIENT_ID, req.getRemoteAddr(), true);
4923 10 Aug 18 nicklas 108         String otp = Values.getStringOrNull(req.getParameter("otp"));
4923 10 Aug 18 nicklas 109         // Check that the OTP is ok
4923 10 Aug 18 nicklas 110         if (otp == null || !otp.matches("\\d{6}"))
4923 10 Aug 18 nicklas 111         {
4923 10 Aug 18 nicklas 112           // It seems like the user did not enter a 6-digit code
4923 10 Aug 18 nicklas 113           throw new IllegalArgumentException("The one-time passcode must have 6 digits.");
4923 10 Aug 18 nicklas 114         }
4852 15 Jun 18 nicklas 115         
4923 10 Aug 18 nicklas 116         dc = sc.newDbControl();
4923 10 Aug 18 nicklas 117         User user = User.getById(dc, sc.getLoggedInUserId());
4923 10 Aug 18 nicklas 118         String username = user.getLogin();
4923 10 Aug 18 nicklas 119         
4923 10 Aug 18 nicklas 120         // De-crypt the OTP secret key
4923 10 Aug 18 nicklas 121         String otpKeyEncrypted = (String)user.getExtended("otpSecretKey");
4923 10 Aug 18 nicklas 122         if (otpKeyEncrypted == null)
4923 10 Aug 18 nicklas 123         {
4923 10 Aug 18 nicklas 124           throw new NullPointerException("OTP has not been enabled for user '" + username + "'");
4923 10 Aug 18 nicklas 125         }
4923 10 Aug 18 nicklas 126         String otpKey = CryptUtil.decrypt(otpKeyEncrypted, user.getId());
4923 10 Aug 18 nicklas 127         
4923 10 Aug 18 nicklas 128         // Validate the OTP value
4923 10 Aug 18 nicklas 129         GoogleAuthenticator gAuth = new GoogleAuthenticator();
4923 10 Aug 18 nicklas 130         if (!gAuth.authorize(otpKey, Values.getInt(otp)))
4923 10 Aug 18 nicklas 131         {
4923 10 Aug 18 nicklas 132           throw new IllegalArgumentException("Invalid one-time passcode. Please try again.");
4923 10 Aug 18 nicklas 133         }
4924 13 Aug 18 nicklas 134         if (!UsedOtpCodes.INSTANCE.useCode(user.getId(), otp))
4924 13 Aug 18 nicklas 135         {
4924 13 Aug 18 nicklas 136           throw new IllegalArgumentException("The one-time passcode has already been used.");
4924 13 Aug 18 nicklas 137         }
4923 10 Aug 18 nicklas 138         
4923 10 Aug 18 nicklas 139         // Generate the URI that should be encoded in the QR code
4923 10 Aug 18 nicklas 140         GoogleAuthenticatorKey gKey = new GoogleAuthenticatorKey.Builder(otpKey).build();
4923 10 Aug 18 nicklas 141         // Use host name + path which should give the URL to the BASE server
4923 10 Aug 18 nicklas 142         String issuer = Application.getHostName() + req.getContextPath();
4923 10 Aug 18 nicklas 143         String totpURL = GoogleAuthenticatorQRGenerator.getOtpAuthTotpURL(issuer, username, gKey);
4923 10 Aug 18 nicklas 144       
4923 10 Aug 18 nicklas 145         // Create the QR code
4923 10 Aug 18 nicklas 146         QrCode qrCode = QrCode.encodeText(totpURL, QrCode.Ecc.LOW);
4923 10 Aug 18 nicklas 147         
4923 10 Aug 18 nicklas 148         // And get it as a png image
4923 10 Aug 18 nicklas 149         BufferedImage img = qrCode.toImage(3, 0);
4923 10 Aug 18 nicklas 150         ByteArrayOutputStream png = new ByteArrayOutputStream(1024);
4923 10 Aug 18 nicklas 151         ImageIO.write(img, "png", png);
4923 10 Aug 18 nicklas 152         
4923 10 Aug 18 nicklas 153         // Encode it as Base64 to let the web client use <img src="data:image/png;base64,...">
4923 10 Aug 18 nicklas 154         String pngInBase64 = Base64.getEncoder().encodeToString(png.toByteArray());
4923 10 Aug 18 nicklas 155         json.put("image", pngInBase64);
4924 13 Aug 18 nicklas 156         json.put("issuer", issuer);
4924 13 Aug 18 nicklas 157         json.put("username", username);
4850 14 Jun 18 nicklas 158       }
4850 14 Jun 18 nicklas 159     }
4850 14 Jun 18 nicklas 160     catch (Throwable t)
4850 14 Jun 18 nicklas 161     {
4850 14 Jun 18 nicklas 162       t.printStackTrace(System.out);
4850 14 Jun 18 nicklas 163       json.clear();
4850 14 Jun 18 nicklas 164       json.put("status", "error");
4850 14 Jun 18 nicklas 165       json.put("message", t.getMessage());
4850 14 Jun 18 nicklas 166       json.put("stacktrace", ThrowableUtil.stackTraceToString(t));
4850 14 Jun 18 nicklas 167     }
4850 14 Jun 18 nicklas 168     finally
4850 14 Jun 18 nicklas 169     {
4923 10 Aug 18 nicklas 170       if (dc != null) dc.close();
4850 14 Jun 18 nicklas 171       json.writeJSONString(resp.getWriter());
4850 14 Jun 18 nicklas 172     }
4850 14 Jun 18 nicklas 173   }
4850 14 Jun 18 nicklas 174
4851 14 Jun 18 nicklas 175   @Override
4851 14 Jun 18 nicklas 176   protected void doPost(HttpServletRequest req, HttpServletResponse resp)
4851 14 Jun 18 nicklas 177     throws ServletException, IOException 
4851 14 Jun 18 nicklas 178   {
4851 14 Jun 18 nicklas 179     final String ID = req.getParameter("ID");
4851 14 Jun 18 nicklas 180     final String cmd = req.getParameter("cmd");
4850 14 Jun 18 nicklas 181
4851 14 Jun 18 nicklas 182     JsonUtil.setJsonResponseHeaders(resp);
4851 14 Jun 18 nicklas 183     
4851 14 Jun 18 nicklas 184     JSONObject json = new JSONObject();
4851 14 Jun 18 nicklas 185     json.put("status", "ok");
4851 14 Jun 18 nicklas 186     
4857 18 Jun 18 nicklas 187     DbControl dc = null;
4851 14 Jun 18 nicklas 188     try
4851 14 Jun 18 nicklas 189     {
4851 14 Jun 18 nicklas 190       final SessionControl sc = Application.getSessionControl(ID, Base.WEBCLIENT_ID, req.getRemoteAddr(), false);
4851 14 Jun 18 nicklas 191       
4851 14 Jun 18 nicklas 192       if ("SaveOTPSetup".equals(cmd))
4851 14 Jun 18 nicklas 193       {
4851 14 Jun 18 nicklas 194         JSONObject jsonReq = JsonUtil.parseRequest(req);
4851 14 Jun 18 nicklas 195         
4851 14 Jun 18 nicklas 196         String username = (String)jsonReq.get("username");
4851 14 Jun 18 nicklas 197         String password = (String)jsonReq.get("password");
4851 14 Jun 18 nicklas 198         String otp = (String)jsonReq.get("otp");
4857 18 Jun 18 nicklas 199         String newPassword = (String)jsonReq.get("newPassword");
4851 14 Jun 18 nicklas 200         
4851 14 Jun 18 nicklas 201         // Check that the OTP is ok and valid
4851 14 Jun 18 nicklas 202         if (otp == null || !otp.matches("\\d{6}"))
4851 14 Jun 18 nicklas 203         {
4851 14 Jun 18 nicklas 204           // It seems like the user did not enter a 6-digit code
4851 14 Jun 18 nicklas 205           throw new LoginException("The one-time passcode must have 6 digits.");
4851 14 Jun 18 nicklas 206         }
4852 15 Jun 18 nicklas 207         
4852 15 Jun 18 nicklas 208         String secretKey = temporaryKeys.get(sc);
4852 15 Jun 18 nicklas 209         if (secretKey == null)
4852 15 Jun 18 nicklas 210         {
4852 15 Jun 18 nicklas 211           throw new NullPointerException("Could not find the OTP secret key. Please restart the wizard and try again.");
4852 15 Jun 18 nicklas 212         }
4852 15 Jun 18 nicklas 213         
4851 14 Jun 18 nicklas 214         GoogleAuthenticator gAuth = new GoogleAuthenticator();
4852 15 Jun 18 nicklas 215         if (!gAuth.authorize(secretKey, Values.getInt(otp)))
4851 14 Jun 18 nicklas 216         {
4851 14 Jun 18 nicklas 217           throw new LoginException("Invalid one-time passcode. Please try again.");
4851 14 Jun 18 nicklas 218         }
4850 14 Jun 18 nicklas 219
4851 14 Jun 18 nicklas 220         // Generate a login request and set mode=SETUP_MODE
4851 14 Jun 18 nicklas 221         // This will trigger SetupOtpAuthenticationManger to store the secret key
4851 14 Jun 18 nicklas 222         LoginRequest login = new LoginRequest(username, password);
4851 14 Jun 18 nicklas 223         login.setComment("Setting up OTP");
5153 28 Nov 18 nicklas 224         login.setAttribute("login-form", Otp.SETUP_MODE);
4852 15 Jun 18 nicklas 225         login.setAttribute("otpSecretKey", secretKey);
4851 14 Jun 18 nicklas 226         sc.login(login);
4857 18 Jun 18 nicklas 227         
4857 18 Jun 18 nicklas 228         String message = "OTP setup completed successfully.";
4857 18 Jun 18 nicklas 229         if (newPassword != null)
4857 18 Jun 18 nicklas 230         {
4857 18 Jun 18 nicklas 231           try
4857 18 Jun 18 nicklas 232           {
4857 18 Jun 18 nicklas 233             dc = sc.newDbControl();
4857 18 Jun 18 nicklas 234             User user = User.getById(dc, sc.getLoggedInUserId());
4857 18 Jun 18 nicklas 235             user.setPassword(newPassword);
4857 18 Jun 18 nicklas 236             dc.commit();
4857 18 Jun 18 nicklas 237             message = "OTP setup completed successfully and the password has been changed.";
4857 18 Jun 18 nicklas 238           }
4857 18 Jun 18 nicklas 239           catch (Exception ex)
4857 18 Jun 18 nicklas 240           {
4857 18 Jun 18 nicklas 241             message = "OTP setup completed successfully, but the password could not be changed (" + ex.getMessage() + ").";
4857 18 Jun 18 nicklas 242           }
4857 18 Jun 18 nicklas 243         }
4857 18 Jun 18 nicklas 244         json.put("message", message);
4857 18 Jun 18 nicklas 245         
4852 15 Jun 18 nicklas 246         temporaryKeys.remove(sc);
4851 14 Jun 18 nicklas 247         sc.logout();
4851 14 Jun 18 nicklas 248       }
4851 14 Jun 18 nicklas 249       
4851 14 Jun 18 nicklas 250     }
4851 14 Jun 18 nicklas 251     catch (Throwable t)
4851 14 Jun 18 nicklas 252     {
4851 14 Jun 18 nicklas 253       t.printStackTrace(System.out);
4851 14 Jun 18 nicklas 254       json.clear();
4851 14 Jun 18 nicklas 255       json.put("status", "error");
4851 14 Jun 18 nicklas 256       json.put("message", t.getMessage());
4851 14 Jun 18 nicklas 257       json.put("stacktrace", ThrowableUtil.stackTraceToString(t));
4851 14 Jun 18 nicklas 258     }
4851 14 Jun 18 nicklas 259     finally
4851 14 Jun 18 nicklas 260     {
4851 14 Jun 18 nicklas 261       json.writeJSONString(resp.getWriter());
4857 18 Jun 18 nicklas 262       if (dc != null) dc.close();
4851 14 Jun 18 nicklas 263     }
4851 14 Jun 18 nicklas 264   }
4851 14 Jun 18 nicklas 265
4851 14 Jun 18 nicklas 266
4850 14 Jun 18 nicklas 267 }