001/*
002 * (C) Copyright 2006-2018 Nuxeo (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Antoine Taillefer
018 */
019package org.nuxeo.ecm.tokenauth.service;
020
021import java.io.Serializable;
022import java.security.Principal;
023import java.util.Calendar;
024import java.util.Collections;
025import java.util.HashMap;
026import java.util.Map;
027import java.util.UUID;
028
029import javax.servlet.http.HttpServletRequest;
030
031import org.apache.commons.lang3.StringUtils;
032import org.apache.commons.logging.Log;
033import org.apache.commons.logging.LogFactory;
034import org.nuxeo.ecm.core.api.DocumentModel;
035import org.nuxeo.ecm.core.api.DocumentModelList;
036import org.nuxeo.ecm.core.api.NuxeoException;
037import org.nuxeo.ecm.core.api.NuxeoPrincipal;
038import org.nuxeo.ecm.directory.BaseSession;
039import org.nuxeo.ecm.directory.Session;
040import org.nuxeo.ecm.directory.api.DirectoryService;
041import org.nuxeo.ecm.platform.ui.web.auth.service.AuthenticationPluginDescriptor;
042import org.nuxeo.ecm.platform.ui.web.auth.service.PluggableAuthenticationService;
043import org.nuxeo.ecm.platform.ui.web.auth.token.TokenAuthenticator;
044import org.nuxeo.ecm.tokenauth.TokenAuthenticationException;
045import org.nuxeo.runtime.api.Framework;
046
047/**
048 * Default implementation of the {@link TokenAuthenticationService}.
049 * <p>
050 * The token is generated by the {@link UUID#randomUUID()} method which guarantees its uniqueness. The storage back-end
051 * is a SQL Directory.
052 *
053 * @author Antoine Taillefer (ataillefer@nuxeo.com)
054 * @since 5.7
055 */
056public class TokenAuthenticationServiceImpl implements TokenAuthenticationService {
057
058    private static final long serialVersionUID = 35041039370298705L;
059
060    private static final Log log = LogFactory.getLog(TokenAuthenticationServiceImpl.class);
061
062    protected static final String DIRECTORY_NAME = "authTokens";
063
064    protected static final String DIRECTORY_SCHEMA = "authtoken";
065
066    protected static final String USERNAME_FIELD = "userName";
067
068    protected static final String TOKEN_FIELD = "token";
069
070    protected static final String APPLICATION_NAME_FIELD = "applicationName";
071
072    protected static final String DEVICE_ID_FIELD = "deviceId";
073
074    protected static final String DEVICE_DESCRIPTION_FIELD = "deviceDescription";
075
076    protected static final String PERMISSION_FIELD = "permission";
077
078    protected static final String CREATION_DATE_FIELD = "creationDate";
079
080    @Override
081    public String acquireToken(String userName, String applicationName, String deviceId, String deviceDescription,
082            String permission) throws TokenAuthenticationException {
083
084        // Look for a token bound to the (userName,
085        // applicationName, deviceId) triplet, if it exists return it,
086        // else generate a unique one
087        String token = getToken(userName, applicationName, deviceId);
088        if (token != null) {
089            return token;
090        }
091
092        // Check required parameters (userName, applicationName and deviceId are
093        // already checked in #getToken)
094        if (StringUtils.isEmpty(permission)) {
095            throw new TokenAuthenticationException(
096                    "The permission parameter is mandatory to acquire an authentication token.");
097        }
098
099        return Framework.doPrivileged(() -> {
100            // Open directory session
101            try (Session session = Framework.getService(DirectoryService.class).open(DIRECTORY_NAME)) {
102                // Generate random token, store the binding and return the token
103                UUID uuid = UUID.randomUUID();
104                String newToken = uuid.toString();
105
106                final DocumentModel entry = getBareAuthTokenModel(Framework.getService(DirectoryService.class));
107                entry.setProperty(DIRECTORY_SCHEMA, TOKEN_FIELD, newToken);
108                entry.setProperty(DIRECTORY_SCHEMA, USERNAME_FIELD, userName);
109                entry.setProperty(DIRECTORY_SCHEMA, APPLICATION_NAME_FIELD, applicationName);
110                entry.setProperty(DIRECTORY_SCHEMA, DEVICE_ID_FIELD, deviceId);
111                if (!StringUtils.isEmpty(deviceDescription)) {
112                    entry.setProperty(DIRECTORY_SCHEMA, DEVICE_DESCRIPTION_FIELD, deviceDescription);
113                }
114                entry.setProperty(DIRECTORY_SCHEMA, PERMISSION_FIELD, permission);
115                Calendar creationDate = Calendar.getInstance();
116                creationDate.setTimeInMillis(System.currentTimeMillis());
117                entry.setProperty(DIRECTORY_SCHEMA, CREATION_DATE_FIELD, creationDate);
118                session.createEntry(entry);
119
120                log.debug(String.format(
121                        "Generated unique token for the (userName, applicationName, deviceId) triplet: ('%s', '%s', '%s'), returning it.",
122                        userName, applicationName, deviceId));
123                return newToken;
124
125            }
126        });
127    }
128
129    @Override
130    public String acquireToken(HttpServletRequest request) throws TokenAuthenticationException {
131        Principal principal = request.getUserPrincipal();
132        if (principal == null) {
133            return null;
134        }
135
136        // Don't provide token for anonymous user unless 'allowAnonymous' parameter is explicitly set to true in
137        // the authentication plugin configuration
138        if (principal instanceof NuxeoPrincipal && ((NuxeoPrincipal) principal).isAnonymous()) {
139            PluggableAuthenticationService authenticationService = Framework.getService(
140                    PluggableAuthenticationService.class);
141            AuthenticationPluginDescriptor tokenAuthPluginDesc = authenticationService.getDescriptor("TOKEN_AUTH");
142            if (tokenAuthPluginDesc == null || !(Boolean.parseBoolean(
143                    tokenAuthPluginDesc.getParameters().get(TokenAuthenticator.ALLOW_ANONYMOUS_KEY)))) {
144                return null;
145            }
146        }
147
148        String userName = principal.getName();
149        String applicationName = request.getParameter("applicationName");
150        String deviceId = request.getParameter("deviceId");
151        String deviceDescription = request.getParameter("deviceDescription");
152        String permission = request.getParameter("permission");
153
154        return acquireToken(userName, applicationName, deviceId, deviceDescription, permission);
155    }
156
157    @Override
158    public String getToken(String userName, String applicationName, String deviceId)
159            throws TokenAuthenticationException {
160
161        if (StringUtils.isEmpty(userName) || StringUtils.isEmpty(applicationName) || StringUtils.isEmpty(deviceId)) {
162            throw new TokenAuthenticationException(
163                    "The following parameters are mandatory to get an authentication token: userName, applicationName, deviceId.");
164        }
165
166        return Framework.doPrivileged(() -> {
167            // Open directory session
168            try (Session session = Framework.getService(DirectoryService.class).open(DIRECTORY_NAME)) {
169                // Look for a token bound to the (userName,
170                // applicationName, deviceId) triplet, if it exists return it,
171                // else return null
172                final Map<String, Serializable> filter = new HashMap<>();
173                filter.put(USERNAME_FIELD, userName);
174                filter.put(APPLICATION_NAME_FIELD, applicationName);
175                filter.put(DEVICE_ID_FIELD, deviceId);
176                DocumentModelList tokens = session.query(filter);
177                if (!tokens.isEmpty()) {
178                    // Multiple tokens found for the same triplet, this is
179                    // inconsistent
180                    if (tokens.size() > 1) {
181                        throw new NuxeoException(String.format(
182                                "Found multiple tokens for the (userName, applicationName, deviceId) triplet: ('%s', '%s', '%s'), this is inconsistent.",
183                                userName, applicationName, deviceId));
184                    }
185                    // Return token
186                    log.debug(String.format(
187                            "Found token for the (userName, applicationName, deviceId) triplet: ('%s', '%s', '%s'), returning it.",
188                            userName, applicationName, deviceId));
189                    DocumentModel tokenModel = tokens.get(0);
190                    return tokenModel.getId();
191                }
192
193                log.debug(String.format(
194                        "No token found for the (userName, applicationName, deviceId) triplet: ('%s', '%s', '%s'), returning null.",
195                        userName, applicationName, deviceId));
196                return null;
197            }
198        });
199    }
200
201    @Override
202    public String getUserName(final String token) {
203        return Framework.doPrivileged(() -> {
204            try (Session session = Framework.getService(DirectoryService.class).open(DIRECTORY_NAME)) {
205                DocumentModel entry = session.getEntry(token);
206                if (entry == null) {
207                    log.debug(String.format("Found no user name bound to the token: '%s', returning null.", token));
208                    return null;
209                }
210                log.debug(String.format("Found a user name bound to the token: '%s', returning it.", token));
211                return (String) entry.getProperty(DIRECTORY_SCHEMA, USERNAME_FIELD);
212
213            }
214        });
215    }
216
217    @Override
218    public void revokeToken(final String token) {
219        Framework.doPrivileged(() -> {
220            try (Session session = Framework.getService(DirectoryService.class).open(DIRECTORY_NAME)) {
221                session.deleteEntry(token);
222                log.info(String.format("Deleted token: '%s' from the back-end.", token));
223            }
224        });
225    }
226
227    @Override
228    public DocumentModelList getTokenBindings(String userName) {
229        return getTokenBindings(userName, null);
230    }
231
232    @Override
233    public DocumentModelList getTokenBindings(String userName, String applicationName) {
234        return Framework.doPrivileged(() -> {
235            try (Session session = Framework.getService(DirectoryService.class).open(DIRECTORY_NAME)) {
236                final Map<String, Serializable> filter = new HashMap<>();
237                filter.put(USERNAME_FIELD, userName);
238                if (applicationName != null) {
239                    filter.put(APPLICATION_NAME_FIELD, applicationName);
240                }
241                final Map<String, String> orderBy = new HashMap<>();
242                orderBy.put(CREATION_DATE_FIELD, "desc");
243                return session.query(filter, Collections.emptySet(), orderBy);
244            }
245        });
246    }
247
248    protected DocumentModel getBareAuthTokenModel(DirectoryService directoryService) {
249
250        String directorySchema = directoryService.getDirectorySchema(DIRECTORY_NAME);
251        return BaseSession.createEntryModel(null, directorySchema, null, null);
252    }
253
254}