/* * (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); } }