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 = (PluggableAuthenticationService) Framework.getRuntime()
140                                                                                                             .getComponent(
141                                                                                                                     PluggableAuthenticationService.NAME);
142            AuthenticationPluginDescriptor tokenAuthPluginDesc = authenticationService.getDescriptor("TOKEN_AUTH");
143            if (tokenAuthPluginDesc == null || !(Boolean.parseBoolean(
144                    tokenAuthPluginDesc.getParameters().get(TokenAuthenticator.ALLOW_ANONYMOUS_KEY)))) {
145                return null;
146            }
147        }
148
149        String userName = principal.getName();
150        String applicationName = request.getParameter("applicationName");
151        String deviceId = request.getParameter("deviceId");
152        String deviceDescription = request.getParameter("deviceDescription");
153        String permission = request.getParameter("permission");
154
155        return acquireToken(userName, applicationName, deviceId, deviceDescription, permission);
156    }
157
158    @Override
159    public String getToken(String userName, String applicationName, String deviceId)
160            throws TokenAuthenticationException {
161
162        if (StringUtils.isEmpty(userName) || StringUtils.isEmpty(applicationName) || StringUtils.isEmpty(deviceId)) {
163            throw new TokenAuthenticationException(
164                    "The following parameters are mandatory to get an authentication token: userName, applicationName, deviceId.");
165        }
166
167        return Framework.doPrivileged(() -> {
168            // Open directory session
169            try (Session session = Framework.getService(DirectoryService.class).open(DIRECTORY_NAME)) {
170                // Look for a token bound to the (userName,
171                // applicationName, deviceId) triplet, if it exists return it,
172                // else return null
173                final Map<String, Serializable> filter = new HashMap<>();
174                filter.put(USERNAME_FIELD, userName);
175                filter.put(APPLICATION_NAME_FIELD, applicationName);
176                filter.put(DEVICE_ID_FIELD, deviceId);
177                DocumentModelList tokens = session.query(filter);
178                if (!tokens.isEmpty()) {
179                    // Multiple tokens found for the same triplet, this is
180                    // inconsistent
181                    if (tokens.size() > 1) {
182                        throw new NuxeoException(String.format(
183                                "Found multiple tokens for the (userName, applicationName, deviceId) triplet: ('%s', '%s', '%s'), this is inconsistent.",
184                                userName, applicationName, deviceId));
185                    }
186                    // Return token
187                    log.debug(String.format(
188                            "Found token for the (userName, applicationName, deviceId) triplet: ('%s', '%s', '%s'), returning it.",
189                            userName, applicationName, deviceId));
190                    DocumentModel tokenModel = tokens.get(0);
191                    return tokenModel.getId();
192                }
193
194                log.debug(String.format(
195                        "No token found for the (userName, applicationName, deviceId) triplet: ('%s', '%s', '%s'), returning null.",
196                        userName, applicationName, deviceId));
197                return null;
198            }
199        });
200    }
201
202    @Override
203    public String getUserName(final String token) {
204        return Framework.doPrivileged(() -> {
205            try (Session session = Framework.getService(DirectoryService.class).open(DIRECTORY_NAME)) {
206                DocumentModel entry = session.getEntry(token);
207                if (entry == null) {
208                    log.debug(String.format("Found no user name bound to the token: '%s', returning null.", token));
209                    return null;
210                }
211                log.debug(String.format("Found a user name bound to the token: '%s', returning it.", token));
212                return (String) entry.getProperty(DIRECTORY_SCHEMA, USERNAME_FIELD);
213
214            }
215        });
216    }
217
218    @Override
219    public void revokeToken(final String token) {
220        Framework.doPrivileged(() -> {
221            try (Session session = Framework.getService(DirectoryService.class).open(DIRECTORY_NAME)) {
222                session.deleteEntry(token);
223                log.info(String.format("Deleted token: '%s' from the back-end.", token));
224            }
225        });
226    }
227
228    @Override
229    public DocumentModelList getTokenBindings(String userName) {
230        return getTokenBindings(userName, null);
231    }
232
233    @Override
234    public DocumentModelList getTokenBindings(String userName, String applicationName) {
235        return Framework.doPrivileged(() -> {
236            try (Session session = Framework.getService(DirectoryService.class).open(DIRECTORY_NAME)) {
237                final Map<String, Serializable> filter = new HashMap<>();
238                filter.put(USERNAME_FIELD, userName);
239                if (applicationName != null) {
240                    filter.put(APPLICATION_NAME_FIELD, applicationName);
241                }
242                final Map<String, String> orderBy = new HashMap<>();
243                orderBy.put(CREATION_DATE_FIELD, "desc");
244                return session.query(filter, Collections.emptySet(), orderBy);
245            }
246        });
247    }
248
249    protected DocumentModel getBareAuthTokenModel(DirectoryService directoryService) {
250
251        String directorySchema = directoryService.getDirectorySchema(DIRECTORY_NAME);
252        return BaseSession.createEntryModel(null, directorySchema, null, null);
253    }
254
255}