package org.swellrt.server.box.servlet;
import java.io.IOException;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.security.Principal;
import java.security.cert.X509Certificate;
import javax.inject.Singleton;
import javax.naming.InvalidNameException;
import javax.naming.ldap.LdapName;
import javax.naming.ldap.Rdn;
import javax.security.auth.Subject;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.login.Configuration;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;
import javax.security.auth.x500.X500Principal;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.io.IOUtils;
import org.waveprotocol.box.server.authentication.HttpRequestBasedCallbackHandler;
import org.waveprotocol.box.server.authentication.ParticipantPrincipal;
import org.waveprotocol.box.server.authentication.SessionManager;
import org.waveprotocol.box.server.persistence.AccountStore;
import org.waveprotocol.box.server.persistence.PersistenceException;
import org.waveprotocol.box.server.util.RegistrationUtil;
import org.waveprotocol.wave.model.id.WaveIdentifiers;
import org.waveprotocol.wave.model.wave.InvalidParticipantAddress;
import org.waveprotocol.wave.model.wave.ParticipantId;
import org.waveprotocol.wave.util.logging.Log;
import com.google.common.base.Preconditions;
import com.google.gson.JsonParseException;
import com.google.inject.Inject;
import com.typesafe.config.Config;
/**
* A servlet for authenticating a user's password and giving them a token via a
* cookie.
*
* Login
*
* POST /auth { id : <ParticipantId>, password : <Password>, remember : <boolean> (optional) }
*
* Login (Anonymous)
*
* POST /auth { id : "_anonymous_", password : "_anonymous_" }
*
* Resume
*
* POST /auth { index : <user_session_index> (optional) }
*
* Listing existing users in session
*
* GET /auth { }
*
* Close session
*
* DELETE /auth {id : <ParticipantId> }
*
* Original code taken from {@AuthenticationServlet}.
*
* @author Pablo Ojanguren (pablojan@gmail.com)
*
*/
@Singleton
public class AuthenticationService extends BaseService {
public static class AuthenticationServiceData extends ServiceData {
public String id;
public String password;
public String status;
public boolean remember;
public int index;
public AuthenticationServiceData() {
}
public AuthenticationServiceData(String status) {
this.status = status;
}
}
// The Object ID of the PKCS #9 email address stored in the client
// certificate.
// Source:
// http://www.rsa.com/products/bsafe/documentation/sslc251html/group__AD__COMMON__OIDS.html
private static final String OID_EMAIL = "1.2.840.113549.1.9.1";
private static final Log LOG = Log.get(AuthenticationService.class);
private final AccountStore accountStore;
private final Configuration configuration;
private final String domain;
private final boolean isClientAuthEnabled;
private final String clientAuthCertDomain;
private boolean failedClientAuth;
@Inject
public AuthenticationService(AccountStore accountStore, Configuration configuration,
SessionManager sessionManager, Config config) {
super(sessionManager);
this.accountStore = accountStore;
this.configuration = configuration;
this.domain = config.getString("core.wave_server_domain");
this.isClientAuthEnabled = config.getBoolean("security.enable_clientauth");
this.clientAuthCertDomain = config.getString("security.clientauth_cert_domain").toLowerCase();
}
@Override
public void execute(HttpServletRequest request, HttpServletResponse response) throws IOException {
try {
if (request.getMethod().equals("POST"))
doPost(request, response);
else if (request.getMethod().equals("GET"))
doGet(request, response);
else if (request.getMethod().equals("DELETE"))
doDelete(request, response);
} catch (PersistenceException e) {
sendResponseError(response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
RC_INTERNAL_SERVER_ERROR);
LOG.warning(e.getMessage(), e);
}
}
private LoginContext login(String address, String password) throws IOException, LoginException {
Subject subject = new Subject();
CallbackHandler callbackHandler = new HttpRequestBasedCallbackHandler(address, password);
LoginContext context = new LoginContext("Wave", subject, callbackHandler, configuration);
// If authentication fails, login() will throw a LoginException.
context.login();
return context;
}
/**
* The POST request should have all the fields required for authentication.
*
* @throws PersistenceException
*/
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException,
PersistenceException {
req.setCharacterEncoding("UTF-8");
LoginContext context = null;
Subject subject;
ParticipantId loggedInAddress = null;
if (isClientAuthEnabled) {
boolean skipClientAuth = false;
try {
X509Certificate[] certs =
(X509Certificate[]) req.getAttribute("javax.servlet.request.X509Certificate");
if (certs == null) {
failedClientAuth = true;
skipClientAuth = true;
}
if (!skipClientAuth) {
failedClientAuth = false;
subject = new Subject();
for (X509Certificate cert : certs) {
X500Principal principal = cert.getSubjectX500Principal();
subject.getPrincipals().add(principal);
}
loggedInAddress = getLoggedInUser(subject);
doLogin(req, resp, loggedInAddress, false);
return;
}
} catch (InvalidParticipantAddress e1) {
sendResponseError(resp, HttpServletResponse.SC_FORBIDDEN, RC_INVALID_ACCOUNT_ID_SYNTAX);
return;
}
}
AuthenticationServiceData authData = new AuthenticationServiceData();
try {
authData = getRequestServiceData(req);
} catch (JsonParseException e) {
sendResponseError(resp, HttpServletResponse.SC_BAD_REQUEST, RC_INVALID_JSON_SYNTAX);
return;
}
if (authData.has("id") && authData.id != null) {
if (!ParticipantId.isAnonymousName(authData.id)) {
try {
String password = (authData.has("password") && authData.password != null ? authData.password : "");
context = login(authData.id, password);
subject = context.getSubject();
loggedInAddress = getLoggedInUser(subject);
doLogin(req, resp, loggedInAddress, authData.has("remember") ? authData.remember : false);
} catch (LoginException e) {
sendResponseError(resp, HttpServletResponse.SC_FORBIDDEN, RC_LOGIN_FAILED);
return;
} catch (InvalidParticipantAddress e1) {
sendResponseError(resp, HttpServletResponse.SC_FORBIDDEN, RC_INVALID_ACCOUNT_ID_SYNTAX);
return;
}
} else {
doLogin(req, resp, ParticipantId.anonymousOfUnsafe(domain), false);
}
} else {
doResume(req, resp, authData.has("index") ? authData.index : -1);
}
}
protected AccountService.AccountServiceData getAccountData(HttpServletRequest req, ParticipantId participantId) throws PersistenceException {
AccountService.AccountServiceData accountData;
if (!participantId.isAnonymous())
accountData =
AccountService.toServiceData(ServiceUtils.getUrlBuilder(req),
accountStore.getAccount(participantId).asHuman());
else
accountData = new AccountService.AccountServiceData(participantId.getAddress());
accountData.sessionId = sessionManager.getSessionId(req);
accountData.transientSessionId = sessionManager.getTransientSessionId(req);
accountData.domain = domain;
return accountData;
}
protected void doLogin(HttpServletRequest req, HttpServletResponse resp, ParticipantId participantId, boolean keepLogin) throws IOException, PersistenceException {
if (participantId.isAnonymous()) {
participantId = ParticipantId.anonymousOfUnsafe(sessionManager.getSessionId(req), domain);
keepLogin = false;
}
sessionManager.login(req, participantId, keepLogin);
LOG.info("Authenticated user " + participantId);
sendResponse(resp, getAccountData(req, participantId));
}
protected void doResume(HttpServletRequest req, HttpServletResponse resp, int userSessionIndex) throws IOException, PersistenceException {
ParticipantId participantId = sessionManager.resume(req, userSessionIndex);
if (participantId == null) {
sendResponseError(resp, HttpServletResponse.SC_FORBIDDEN, RC_LOGIN_FAILED);
} else {
LOG.info("Authenticated user " + participantId);
sendResponse(resp, getAccountData(req, participantId));
}
}
/**
* DELETE a session
*
* @param req
* @param resp
* @throws IOException
*/
protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws IOException {
String[] pathTokens = SwellRtServlet.getCleanPathInfo(req).split("/");
String participantToken = pathTokens.length > 2 ? pathTokens[2] : null;
boolean wasDelete = false;
if (participantToken != null && !participantToken.isEmpty()) {
ParticipantId participantId;
try {
participantId = ParticipantId.of(participantToken);
} catch (InvalidParticipantAddress e) {
sendResponseError(resp, HttpServletResponse.SC_BAD_REQUEST, RC_INVALID_ACCOUNT_ID_SYNTAX);
return;
}
wasDelete = sessionManager.logout(req, participantId);
}
if (wasDelete) {
sendResponse(resp, new AuthenticationServiceData("SESSION_CLOSED"));
return;
} else {
sendResponseError(resp, HttpServletResponse.SC_BAD_REQUEST, RC_ACCOUNT_NOT_LOGGED_IN);
return;
}
}
/**
* Get the participant id of the given subject.
*
* The subject is searched for compatible principals. When other
* authentication types are added, this method will need to be updated to
* support their principal types.
*
* @throws InvalidParticipantAddress The subject's address is invalid
*/
private ParticipantId getLoggedInUser(Subject subject) throws InvalidParticipantAddress {
String address = null;
for (Principal p : subject.getPrincipals()) {
// TODO(josephg): When we support other authentication types (LDAP, etc),
// this method will need to read the address portion out of the other
// principal types.
if (p instanceof ParticipantPrincipal) {
address = ((ParticipantPrincipal) p).getName();
break;
} else if (p instanceof X500Principal) {
return attemptClientCertificateLogin((X500Principal) p);
}
}
return address == null ? null : ParticipantId.of(address);
}
/**
* Attempts to authenticate the user using their client certificate.
*
* Retrieves the email from their certificate, using it as the wave username.
* If the user doesn't exist and registration is enabled, it will
* automatically create an account before continuing. Otherwise it will simply
* check if the account exists and authenticate based on that.
*
* @throws RuntimeException The encoding of the email is unsupported on this
* system
* @throws InvalidParticipantAddress The email address doesn't correspond to
* an account
*/
private ParticipantId attemptClientCertificateLogin(X500Principal p) throws RuntimeException,
InvalidParticipantAddress {
String distinguishedName = p.getName();
try {
LdapName ldapName = new LdapName(distinguishedName);
for (Rdn rdn : ldapName.getRdns()) {
if (rdn.getType().equals(OID_EMAIL)) {
String email = decodeEmailFromCertificate((byte[]) rdn.getValue());
if (email.endsWith("@" + clientAuthCertDomain)) {
// Check we decoded the string correctly.
Preconditions.checkState(WaveIdentifiers.isValidIdentifier(email),
"The decoded email is not a valid wave identifier");
ParticipantId id = ParticipantId.of(email);
if (!RegistrationUtil.doesAccountExist(accountStore, id)) {
// if (!isRegistrationDisabled) {
// if (!RegistrationUtil.createAccountIfMissing(accountStore, id, null, welcomeBot)) {
// return null;
// }
// } else {
// throw new InvalidNameException(
// "User doesn't already exist, and registration disabled by administrator");
// }
throw new InvalidNameException(
"User doesn't already exist");
}
return id;
}
}
}
} catch (UnsupportedEncodingException ex) {
throw new RuntimeException(ex);
} catch (InvalidNameException ex) {
throw new InvalidParticipantAddress(distinguishedName,
"Certificate does not contain a valid distinguished name");
}
return null;
}
/**
* Decodes the user email from the X.509 certificate.
*
* Email address is assumed to be valid in ASCII, and less than 128 characters
* long
*
* @param encoded Output from rdn.getValue(). 1st byte is the tag, second is
* the length.
* @return The decoded email in ASCII
* @throws UnsupportedEncodingException The email address wasn't in ASCII
*/
private String decodeEmailFromCertificate(byte[] encoded) throws UnsupportedEncodingException {
// Check for < 130, since first 2 bytes are taken up as stated above.
Preconditions.checkState(encoded.length < 130, "The email address is longer than expected");
return new String(encoded, 2, encoded.length - 2, "ascii");
}
/**
* GET, return a list of active users within the http session
*
* @throws PersistenceException
*/
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException,
PersistenceException {
sendResponse(resp, sessionManager.getSessionUsersIndex(req));
}
protected AuthenticationServiceData getRequestServiceData(HttpServletRequest request)
throws IOException, JsonParseException {
StringWriter writer = new StringWriter();
IOUtils.copy(request.getInputStream(), writer, Charset.forName("UTF-8"));
String json = writer.toString();
if (json == null)
throw new JsonParseException("Null JSON message");
return (AuthenticationServiceData) ServiceData.fromJson(json,
AuthenticationServiceData.class);
}
}