/*
* (C) Copyright 2006-2016 Nuxeo SA (http://nuxeo.com/) and others.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Contributors:
* Antoine Taillefer
*/
package org.nuxeo.ecm.tokenauth.service;
import java.io.Serializable;
import java.security.Principal;
import java.util.Calendar;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuxeo.ecm.core.api.DocumentModel;
import org.nuxeo.ecm.core.api.DocumentModelList;
import org.nuxeo.ecm.core.api.NuxeoException;
import org.nuxeo.ecm.core.api.NuxeoPrincipal;
import org.nuxeo.ecm.directory.BaseSession;
import org.nuxeo.ecm.directory.Session;
import org.nuxeo.ecm.directory.api.DirectoryService;
import org.nuxeo.ecm.platform.ui.web.auth.service.AuthenticationPluginDescriptor;
import org.nuxeo.ecm.platform.ui.web.auth.service.PluggableAuthenticationService;
import org.nuxeo.ecm.platform.ui.web.auth.token.TokenAuthenticator;
import org.nuxeo.ecm.tokenauth.TokenAuthenticationException;
import org.nuxeo.runtime.api.Framework;
/**
* Default implementation of the {@link TokenAuthenticationService}.
* <p>
* The token is generated by the {@link UUID#randomUUID()} method which guarantees its uniqueness. The storage back-end
* is a SQL Directory.
*
* @author Antoine Taillefer (ataillefer@nuxeo.com)
* @since 5.7
*/
public class TokenAuthenticationServiceImpl implements TokenAuthenticationService {
private static final long serialVersionUID = 35041039370298705L;
private static final Log log = LogFactory.getLog(TokenAuthenticationServiceImpl.class);
protected static final String DIRECTORY_NAME = "authTokens";
protected static final String DIRECTORY_SCHEMA = "authtoken";
protected static final String USERNAME_FIELD = "userName";
protected static final String TOKEN_FIELD = "token";
protected static final String APPLICATION_NAME_FIELD = "applicationName";
protected static final String DEVICE_ID_FIELD = "deviceId";
protected static final String DEVICE_DESCRIPTION_FIELD = "deviceDescription";
protected static final String PERMISSION_FIELD = "permission";
protected static final String CREATION_DATE_FIELD = "creationDate";
@Override
public String acquireToken(String userName, String applicationName, String deviceId, String deviceDescription,
String permission) throws TokenAuthenticationException {
// Look for a token bound to the (userName,
// applicationName, deviceId) triplet, if it exists return it,
// else generate a unique one
String token = getToken(userName, applicationName, deviceId);
if (token != null) {
return token;
}
// Check required parameters (userName, applicationName and deviceId are
// already checked in #getToken)
if (StringUtils.isEmpty(permission)) {
throw new TokenAuthenticationException(
"The permission parameter is mandatory to acquire an authentication token.");
}
// Log in as system user
LoginContext lc;
try {
lc = Framework.login();
} catch (LoginException e) {
throw new NuxeoException("Cannot log in as system user", e);
}
try {
// Open directory session
try (Session session = Framework.getService(DirectoryService.class).open(DIRECTORY_NAME)) {
// Generate random token, store the binding and return the token
UUID uuid = UUID.randomUUID();
token = uuid.toString();
final DocumentModel entry = getBareAuthTokenModel(Framework.getService(DirectoryService.class));
entry.setProperty(DIRECTORY_SCHEMA, TOKEN_FIELD, token);
entry.setProperty(DIRECTORY_SCHEMA, USERNAME_FIELD, userName);
entry.setProperty(DIRECTORY_SCHEMA, APPLICATION_NAME_FIELD, applicationName);
entry.setProperty(DIRECTORY_SCHEMA, DEVICE_ID_FIELD, deviceId);
if (!StringUtils.isEmpty(deviceDescription)) {
entry.setProperty(DIRECTORY_SCHEMA, DEVICE_DESCRIPTION_FIELD, deviceDescription);
}
entry.setProperty(DIRECTORY_SCHEMA, PERMISSION_FIELD, permission);
Calendar creationDate = Calendar.getInstance();
creationDate.setTimeInMillis(System.currentTimeMillis());
entry.setProperty(DIRECTORY_SCHEMA, CREATION_DATE_FIELD, creationDate);
session.createEntry(entry);
log.debug(String.format(
"Generated unique token for the (userName, applicationName, deviceId) triplet: ('%s', '%s', '%s'), returning it.",
userName, applicationName, deviceId));
return token;
}
} finally {
try {
// Login context may be null in tests
if (lc != null) {
lc.logout();
}
} catch (LoginException e) {
throw new NuxeoException("Cannot log out system user", e);
}
}
}
@Override
public String acquireToken(HttpServletRequest request) throws TokenAuthenticationException {
Principal principal = request.getUserPrincipal();
if (principal == null) {
return null;
}
// Don't provide token for anonymous user unless 'allowAnonymous' parameter is explicitly set to true in
// the authentication plugin configuration
if (principal instanceof NuxeoPrincipal && ((NuxeoPrincipal) principal).isAnonymous()) {
PluggableAuthenticationService authenticationService = (PluggableAuthenticationService) Framework.getRuntime()
.getComponent(
PluggableAuthenticationService.NAME);
AuthenticationPluginDescriptor tokenAuthPluginDesc = authenticationService.getDescriptor("TOKEN_AUTH");
if (tokenAuthPluginDesc == null || !(Boolean.valueOf(
tokenAuthPluginDesc.getParameters().get(TokenAuthenticator.ALLOW_ANONYMOUS_KEY)))) {
return null;
}
}
String userName = principal.getName();
String applicationName = request.getParameter("applicationName");
String deviceId = request.getParameter("deviceId");
String deviceDescription = request.getParameter("deviceDescription");
String permission = request.getParameter("permission");
return acquireToken(userName, applicationName, deviceId, deviceDescription, permission);
}
@Override
public String getToken(String userName, String applicationName, String deviceId)
throws TokenAuthenticationException {
if (StringUtils.isEmpty(userName) || StringUtils.isEmpty(applicationName) || StringUtils.isEmpty(deviceId)) {
throw new TokenAuthenticationException(
"The following parameters are mandatory to get an authentication token: userName, applicationName, deviceId.");
}
// Log in as system user
LoginContext lc;
try {
lc = Framework.login();
} catch (LoginException e) {
throw new NuxeoException("Cannot log in as system user", e);
}
try {
// Open directory session
try (Session session = Framework.getService(DirectoryService.class).open(DIRECTORY_NAME)) {
// Look for a token bound to the (userName,
// applicationName, deviceId) triplet, if it exists return it,
// else return null
final Map<String, Serializable> filter = new HashMap<String, Serializable>();
filter.put(USERNAME_FIELD, userName);
filter.put(APPLICATION_NAME_FIELD, applicationName);
filter.put(DEVICE_ID_FIELD, deviceId);
DocumentModelList tokens = session.query(filter);
if (!tokens.isEmpty()) {
// Multiple tokens found for the same triplet, this is
// inconsistent
if (tokens.size() > 1) {
throw new NuxeoException(String.format(
"Found multiple tokens for the (userName, applicationName, deviceId) triplet: ('%s', '%s', '%s'), this is inconsistent.",
userName, applicationName, deviceId));
}
// Return token
log.debug(String.format(
"Found token for the (userName, applicationName, deviceId) triplet: ('%s', '%s', '%s'), returning it.",
userName, applicationName, deviceId));
DocumentModel tokenModel = tokens.get(0);
return tokenModel.getId();
}
log.debug(String.format(
"No token found for the (userName, applicationName, deviceId) triplet: ('%s', '%s', '%s'), returning null.",
userName, applicationName, deviceId));
return null;
}
} finally {
try {
// Login context may be null in tests
if (lc != null) {
lc.logout();
}
} catch (LoginException e) {
throw new NuxeoException("Cannot log out system user", e);
}
}
}
@Override
public String getUserName(final String token) {
// Log in as system user
LoginContext lc;
try {
lc = Framework.login();
} catch (LoginException e) {
throw new NuxeoException("Cannot log in as system user", e);
}
try {
try (Session session = Framework.getService(DirectoryService.class).open(DIRECTORY_NAME)) {
DocumentModel entry = session.getEntry(token);
if (entry == null) {
log.debug(String.format("Found no user name bound to the token: '%s', returning null.", token));
return null;
}
log.debug(String.format("Found a user name bound to the token: '%s', returning it.", token));
return (String) entry.getProperty(DIRECTORY_SCHEMA, USERNAME_FIELD);
}
} finally {
try {
// Login context may be null in tests
if (lc != null) {
lc.logout();
}
} catch (LoginException e) {
throw new NuxeoException("Cannot log out system user", e);
}
}
}
@Override
public void revokeToken(final String token) {
// Log in as system user
LoginContext lc;
try {
lc = Framework.login();
} catch (LoginException e) {
throw new NuxeoException("Cannot log in as system user", e);
}
try {
try (Session session = Framework.getService(DirectoryService.class).open(DIRECTORY_NAME)) {
session.deleteEntry(token);
log.info(String.format("Deleted token: '%s' from the back-end.", token));
}
} finally {
try {
// Login context may be null in tests
if (lc != null) {
lc.logout();
}
} catch (LoginException e) {
throw new NuxeoException("Cannot log out system user", e);
}
}
}
@Override
public DocumentModelList getTokenBindings(String userName) {
return getTokenBindings(userName, null);
}
@Override
public DocumentModelList getTokenBindings(String userName, String applicationName) {
// Log in as system user
LoginContext lc;
try {
lc = Framework.login();
} catch (LoginException e) {
throw new NuxeoException("Cannot log in as system user", e);
}
try {
try (Session session = Framework.getService(DirectoryService.class).open(DIRECTORY_NAME)) {
final Map<String, Serializable> filter = new HashMap<String, Serializable>();
filter.put(USERNAME_FIELD, userName);
if (applicationName != null) {
filter.put(APPLICATION_NAME_FIELD, applicationName);
}
final Map<String, String> orderBy = new HashMap<String, String>();
orderBy.put(CREATION_DATE_FIELD, "desc");
return session.query(filter, Collections.emptySet(), orderBy);
}
} finally {
try {
// Login context may be null in tests
if (lc != null) {
lc.logout();
}
} catch (LoginException e) {
throw new NuxeoException("Cannot log out system user", e);
}
}
}
protected DocumentModel getBareAuthTokenModel(DirectoryService directoryService) {
String directorySchema = directoryService.getDirectorySchema(DIRECTORY_NAME);
return BaseSession.createEntryModel(null, directorySchema, null, null);
}
}