/*******************************************************************************
* Copyright (c) 2013, 2014 Lectorius, Inc.
* Authors:
* Vijay Pandurangan (vijayp@mitro.co)
* Evan Jones (ej@mitro.co)
* Adam Hilss (ahilss@mitro.co)
*
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* You can contact the authors at inbound@mitro.co.
*******************************************************************************/
package co.mitro.core.servlets;
import java.io.IOException;
import java.io.StringWriter;
import java.sql.SQLException;
import java.util.concurrent.TimeUnit;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import co.mitro.core.crypto.KeyInterfaces.KeyFactory;
import co.mitro.core.exceptions.MitroServletException;
import co.mitro.core.server.Manager;
import co.mitro.core.server.ManagerFactory;
import co.mitro.core.server.Templates;
import co.mitro.core.server.data.DBAudit;
import co.mitro.core.server.data.DBIdentity;
import co.mitro.core.server.data.RPC;
import co.mitro.twofactor.TwoFactorSigningService;
import com.github.mustachejava.Mustache;
import com.google.common.collect.ImmutableMap;
import com.google.gson.Gson;
@WebServlet("/user/VerifyDevice")
public class VerifyDeviceServlet extends HttpServlet {
private static final Gson gson = new Gson();
private static final Logger logger = LoggerFactory.getLogger(VerifyDeviceServlet.class);
private static final long serialVersionUID = 1L;
/** Destination where successful requests are redirected. */
public static final String SUCCESS_DESTINATION = "/verified-device.html";
/** How long a device token is valid, which must permit long email delays. */
static final long VALIDITY_TIMEOUT_MS = TimeUnit.HOURS.toMillis(12);
private ManagerFactory managerFactory;
private Mustache errorPageTemplate;
private Mustache errorMessageTemplate;
public VerifyDeviceServlet(ManagerFactory managerFactory, KeyFactory keyFactory) {
this.managerFactory = managerFactory;
// Crash on startup if secrets aren't loaded
TwoFactorSigningService.checkInitialized();
errorPageTemplate = Templates.compile("error.mustache");
errorMessageTemplate = Templates.compile("verifydeviceservlet.mustache");
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String token = request.getParameter("token");
String tokenSignature = request.getParameter("token_signature");
RPC.LoginToken signedToken = gson.fromJson(token,
RPC.LoginToken.class);
// TODO: Remove user parameter: it isn't signed so it can't be trusted!
if (request.getParameter("user") != null) {
if (!request.getParameter("user").equals(signedToken.email)) {
throw new ServletException("Invalid parameters");
}
}
boolean correctSignature = TwoFactorSigningService.verifySignature(token, tokenSignature);
// TODO: even with the signature we should check that it came from the past?
boolean correctTimeStamp = TwoFactorSigningService.verifyTimestamp(
signedToken.timestampMs, VALIDITY_TIMEOUT_MS);
if (!(correctSignature && correctTimeStamp)) {
// both could be wrong, but incorrect signatures are more "important" (shouldn't happen)
if (!correctSignature) {
logger.error("Invalid signature for token: {} signature: {}", token, tokenSignature);
} else {
assert !correctTimeStamp;
logger.warn("Token timed out. timestampMs: {} expired time: {} now: {} ",
signedToken.timestampMs, signedToken.timestampMs + VALIDITY_TIMEOUT_MS,
System.currentTimeMillis());
}
// Generate the error message
ImmutableMap<String, Boolean> template = ImmutableMap.of(
"correctTimestamp", correctTimeStamp,
"correctSignature", correctSignature);
StringWriter writer = new StringWriter();
errorMessageTemplate.execute(writer, template);
String errorMessage = writer.toString();
// Render the error page with the message
ImmutableMap<String, String> templatePage = ImmutableMap.of(
"errorMessage", errorMessage);
errorPageTemplate.execute(response.getWriter(), templatePage);
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
return;
}
try (Manager mgr = managerFactory.newManager()) {
DBIdentity u = DBIdentity.getIdentityForUserName(mgr, signedToken.email);
mgr.setOperationName("VERIFY_DEVICE");
String deviceKey = GetMyDeviceKey.maybeGetOrCreateDeviceKey(
mgr, u, signedToken.deviceId, false, "UNKNOWN");
if (deviceKey == null) {
logger.error("Failed for device {} user {}: get/create device key failed",
signedToken.deviceId, u.getName());
throw new MitroServletException("failed to verify device");
}
if (!u.isVerified()) {
mgr.setRequestor(u, signedToken.deviceId);
logger.info("marking user {} as verified", u.getName());
mgr.addAuditLog(DBAudit.ACTION.AUTHORIZE_NEW_DEVICE, u, null, null, null,
null);
u.setVerified(true);
mgr.identityDao.update(u);
}
mgr.commitTransaction();
logger.info("accepting device {} for user {}", signedToken.deviceId, u.getName());
response.sendRedirect(SUCCESS_DESTINATION);
} catch (SQLException|MitroServletException e) {
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid token");
throw new ServletException(e);
}
}
}