001/*
002 * (C) Copyright 2006-2016 Nuxeo SA (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.security.auth.login.LoginContext;
030import javax.security.auth.login.LoginException;
031import javax.servlet.http.HttpServletRequest;
032
033import org.apache.commons.lang.StringUtils;
034import org.apache.commons.logging.Log;
035import org.apache.commons.logging.LogFactory;
036import org.nuxeo.ecm.core.api.DocumentModel;
037import org.nuxeo.ecm.core.api.DocumentModelList;
038import org.nuxeo.ecm.core.api.NuxeoException;
039import org.nuxeo.ecm.core.api.NuxeoPrincipal;
040import org.nuxeo.ecm.directory.BaseSession;
041import org.nuxeo.ecm.directory.Session;
042import org.nuxeo.ecm.directory.api.DirectoryService;
043import org.nuxeo.ecm.platform.ui.web.auth.service.AuthenticationPluginDescriptor;
044import org.nuxeo.ecm.platform.ui.web.auth.service.PluggableAuthenticationService;
045import org.nuxeo.ecm.platform.ui.web.auth.token.TokenAuthenticator;
046import org.nuxeo.ecm.tokenauth.TokenAuthenticationException;
047import org.nuxeo.runtime.api.Framework;
048
049/**
050 * Default implementation of the {@link TokenAuthenticationService}.
051 * <p>
052 * The token is generated by the {@link UUID#randomUUID()} method which guarantees its uniqueness. The storage back-end
053 * is a SQL Directory.
054 *
055 * @author Antoine Taillefer (ataillefer@nuxeo.com)
056 * @since 5.7
057 */
058public class TokenAuthenticationServiceImpl implements TokenAuthenticationService {
059
060    private static final long serialVersionUID = 35041039370298705L;
061
062    private static final Log log = LogFactory.getLog(TokenAuthenticationServiceImpl.class);
063
064    protected static final String DIRECTORY_NAME = "authTokens";
065
066    protected static final String DIRECTORY_SCHEMA = "authtoken";
067
068    protected static final String USERNAME_FIELD = "userName";
069
070    protected static final String TOKEN_FIELD = "token";
071
072    protected static final String APPLICATION_NAME_FIELD = "applicationName";
073
074    protected static final String DEVICE_ID_FIELD = "deviceId";
075
076    protected static final String DEVICE_DESCRIPTION_FIELD = "deviceDescription";
077
078    protected static final String PERMISSION_FIELD = "permission";
079
080    protected static final String CREATION_DATE_FIELD = "creationDate";
081
082    @Override
083    public String acquireToken(String userName, String applicationName, String deviceId, String deviceDescription,
084            String permission) throws TokenAuthenticationException {
085
086        // Look for a token bound to the (userName,
087        // applicationName, deviceId) triplet, if it exists return it,
088        // else generate a unique one
089        String token = getToken(userName, applicationName, deviceId);
090        if (token != null) {
091            return token;
092        }
093
094        // Check required parameters (userName, applicationName and deviceId are
095        // already checked in #getToken)
096        if (StringUtils.isEmpty(permission)) {
097            throw new TokenAuthenticationException(
098                    "The permission parameter is mandatory to acquire an authentication token.");
099        }
100
101        // Log in as system user
102        LoginContext lc;
103        try {
104            lc = Framework.login();
105        } catch (LoginException e) {
106            throw new NuxeoException("Cannot log in as system user", e);
107        }
108        try {
109            // Open directory session
110            try (Session session = Framework.getService(DirectoryService.class).open(DIRECTORY_NAME)) {
111                // Generate random token, store the binding and return the token
112                UUID uuid = UUID.randomUUID();
113                token = uuid.toString();
114
115                final DocumentModel entry = getBareAuthTokenModel(Framework.getService(DirectoryService.class));
116                entry.setProperty(DIRECTORY_SCHEMA, TOKEN_FIELD, token);
117                entry.setProperty(DIRECTORY_SCHEMA, USERNAME_FIELD, userName);
118                entry.setProperty(DIRECTORY_SCHEMA, APPLICATION_NAME_FIELD, applicationName);
119                entry.setProperty(DIRECTORY_SCHEMA, DEVICE_ID_FIELD, deviceId);
120                if (!StringUtils.isEmpty(deviceDescription)) {
121                    entry.setProperty(DIRECTORY_SCHEMA, DEVICE_DESCRIPTION_FIELD, deviceDescription);
122                }
123                entry.setProperty(DIRECTORY_SCHEMA, PERMISSION_FIELD, permission);
124                Calendar creationDate = Calendar.getInstance();
125                creationDate.setTimeInMillis(System.currentTimeMillis());
126                entry.setProperty(DIRECTORY_SCHEMA, CREATION_DATE_FIELD, creationDate);
127                session.createEntry(entry);
128
129                log.debug(String.format(
130                        "Generated unique token for the (userName, applicationName, deviceId) triplet: ('%s', '%s', '%s'), returning it.",
131                        userName, applicationName, deviceId));
132                return token;
133
134            }
135        } finally {
136            try {
137                // Login context may be null in tests
138                if (lc != null) {
139                    lc.logout();
140                }
141            } catch (LoginException e) {
142                throw new NuxeoException("Cannot log out system user", e);
143            }
144        }
145    }
146
147    @Override
148    public String acquireToken(HttpServletRequest request) throws TokenAuthenticationException {
149        Principal principal = request.getUserPrincipal();
150        if (principal == null) {
151            return null;
152        }
153
154        // Don't provide token for anonymous user unless 'allowAnonymous' parameter is explicitly set to true in
155        // the authentication plugin configuration
156        if (principal instanceof NuxeoPrincipal && ((NuxeoPrincipal) principal).isAnonymous()) {
157            PluggableAuthenticationService authenticationService = (PluggableAuthenticationService) Framework.getRuntime()
158                                                                                                             .getComponent(
159                                                                                                                     PluggableAuthenticationService.NAME);
160            AuthenticationPluginDescriptor tokenAuthPluginDesc = authenticationService.getDescriptor("TOKEN_AUTH");
161            if (tokenAuthPluginDesc == null || !(Boolean.valueOf(
162                    tokenAuthPluginDesc.getParameters().get(TokenAuthenticator.ALLOW_ANONYMOUS_KEY)))) {
163                return null;
164            }
165        }
166
167        String userName = principal.getName();
168        String applicationName = request.getParameter("applicationName");
169        String deviceId = request.getParameter("deviceId");
170        String deviceDescription = request.getParameter("deviceDescription");
171        String permission = request.getParameter("permission");
172
173        return acquireToken(userName, applicationName, deviceId, deviceDescription, permission);
174    }
175
176    @Override
177    public String getToken(String userName, String applicationName, String deviceId)
178            throws TokenAuthenticationException {
179
180        if (StringUtils.isEmpty(userName) || StringUtils.isEmpty(applicationName) || StringUtils.isEmpty(deviceId)) {
181            throw new TokenAuthenticationException(
182                    "The following parameters are mandatory to get an authentication token: userName, applicationName, deviceId.");
183        }
184
185        // Log in as system user
186        LoginContext lc;
187        try {
188            lc = Framework.login();
189        } catch (LoginException e) {
190            throw new NuxeoException("Cannot log in as system user", e);
191        }
192        try {
193            // Open directory session
194            try (Session session = Framework.getService(DirectoryService.class).open(DIRECTORY_NAME)) {
195                // Look for a token bound to the (userName,
196                // applicationName, deviceId) triplet, if it exists return it,
197                // else return null
198                final Map<String, Serializable> filter = new HashMap<String, Serializable>();
199                filter.put(USERNAME_FIELD, userName);
200                filter.put(APPLICATION_NAME_FIELD, applicationName);
201                filter.put(DEVICE_ID_FIELD, deviceId);
202                DocumentModelList tokens = session.query(filter);
203                if (!tokens.isEmpty()) {
204                    // Multiple tokens found for the same triplet, this is
205                    // inconsistent
206                    if (tokens.size() > 1) {
207                        throw new NuxeoException(String.format(
208                                "Found multiple tokens for the (userName, applicationName, deviceId) triplet: ('%s', '%s', '%s'), this is inconsistent.",
209                                userName, applicationName, deviceId));
210                    }
211                    // Return token
212                    log.debug(String.format(
213                            "Found token for the (userName, applicationName, deviceId) triplet: ('%s', '%s', '%s'), returning it.",
214                            userName, applicationName, deviceId));
215                    DocumentModel tokenModel = tokens.get(0);
216                    return tokenModel.getId();
217                }
218
219                log.debug(String.format(
220                        "No token found for the (userName, applicationName, deviceId) triplet: ('%s', '%s', '%s'), returning null.",
221                        userName, applicationName, deviceId));
222                return null;
223            }
224        } finally {
225            try {
226                // Login context may be null in tests
227                if (lc != null) {
228                    lc.logout();
229                }
230            } catch (LoginException e) {
231                throw new NuxeoException("Cannot log out system user", e);
232            }
233        }
234    }
235
236    @Override
237    public String getUserName(final String token) {
238
239        // Log in as system user
240        LoginContext lc;
241        try {
242            lc = Framework.login();
243        } catch (LoginException e) {
244            throw new NuxeoException("Cannot log in as system user", e);
245        }
246        try {
247            try (Session session = Framework.getService(DirectoryService.class).open(DIRECTORY_NAME)) {
248                DocumentModel entry = session.getEntry(token);
249                if (entry == null) {
250                    log.debug(String.format("Found no user name bound to the token: '%s', returning null.", token));
251                    return null;
252                }
253                log.debug(String.format("Found a user name bound to the token: '%s', returning it.", token));
254                return (String) entry.getProperty(DIRECTORY_SCHEMA, USERNAME_FIELD);
255
256            }
257        } finally {
258            try {
259                // Login context may be null in tests
260                if (lc != null) {
261                    lc.logout();
262                }
263            } catch (LoginException e) {
264                throw new NuxeoException("Cannot log out system user", e);
265            }
266        }
267    }
268
269    @Override
270    public void revokeToken(final String token) {
271
272        // Log in as system user
273        LoginContext lc;
274        try {
275            lc = Framework.login();
276        } catch (LoginException e) {
277            throw new NuxeoException("Cannot log in as system user", e);
278        }
279        try {
280            try (Session session = Framework.getService(DirectoryService.class).open(DIRECTORY_NAME)) {
281                session.deleteEntry(token);
282                log.info(String.format("Deleted token: '%s' from the back-end.", token));
283            }
284        } finally {
285            try {
286                // Login context may be null in tests
287                if (lc != null) {
288                    lc.logout();
289                }
290            } catch (LoginException e) {
291                throw new NuxeoException("Cannot log out system user", e);
292            }
293        }
294    }
295
296    @Override
297    public DocumentModelList getTokenBindings(String userName) {
298        return getTokenBindings(userName, null);
299    }
300
301    @Override
302    public DocumentModelList getTokenBindings(String userName, String applicationName) {
303
304        // Log in as system user
305        LoginContext lc;
306        try {
307            lc = Framework.login();
308        } catch (LoginException e) {
309            throw new NuxeoException("Cannot log in as system user", e);
310        }
311        try {
312            try (Session session = Framework.getService(DirectoryService.class).open(DIRECTORY_NAME)) {
313                final Map<String, Serializable> filter = new HashMap<String, Serializable>();
314                filter.put(USERNAME_FIELD, userName);
315                if (applicationName != null) {
316                    filter.put(APPLICATION_NAME_FIELD, applicationName);
317                }
318                final Map<String, String> orderBy = new HashMap<String, String>();
319                orderBy.put(CREATION_DATE_FIELD, "desc");
320                return session.query(filter, Collections.emptySet(), orderBy);
321            }
322        } finally {
323            try {
324                // Login context may be null in tests
325                if (lc != null) {
326                    lc.logout();
327                }
328            } catch (LoginException e) {
329                throw new NuxeoException("Cannot log out system user", e);
330            }
331        }
332    }
333
334    protected DocumentModel getBareAuthTokenModel(DirectoryService directoryService) {
335
336        String directorySchema = directoryService.getDirectorySchema(DIRECTORY_NAME);
337        return BaseSession.createEntryModel(null, directorySchema, null, null);
338    }
339
340}