001/*
002 * (C) Copyright 2006-20012 Nuxeo SA (http://nuxeo.com/) and contributors.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 *     Antoine Taillefer
016 */
017package org.nuxeo.ecm.tokenauth.service;
018
019import java.io.Serializable;
020import java.util.Calendar;
021import java.util.Collections;
022import java.util.HashMap;
023import java.util.Map;
024import java.util.UUID;
025
026import javax.security.auth.login.LoginContext;
027import javax.security.auth.login.LoginException;
028
029import org.apache.commons.lang.StringUtils;
030import org.apache.commons.logging.Log;
031import org.apache.commons.logging.LogFactory;
032import org.nuxeo.ecm.core.api.DocumentModel;
033import org.nuxeo.ecm.core.api.DocumentModelList;
034import org.nuxeo.ecm.core.api.NuxeoException;
035import org.nuxeo.ecm.directory.BaseSession;
036import org.nuxeo.ecm.directory.Session;
037import org.nuxeo.ecm.directory.api.DirectoryService;
038import org.nuxeo.ecm.tokenauth.TokenAuthenticationException;
039import org.nuxeo.runtime.api.Framework;
040
041/**
042 * Default implementation of the {@link TokenAuthenticationService}.
043 * <p>
044 * The token is generated by the {@link UUID#randomUUID()} method which guarantees its uniqueness. The storage back-end
045 * is a SQL Directory.
046 *
047 * @author Antoine Taillefer (ataillefer@nuxeo.com)
048 * @since 5.7
049 */
050public class TokenAuthenticationServiceImpl implements TokenAuthenticationService {
051
052    private static final long serialVersionUID = 35041039370298705L;
053
054    private static final Log log = LogFactory.getLog(TokenAuthenticationServiceImpl.class);
055
056    protected static final String DIRECTORY_NAME = "authTokens";
057
058    protected static final String DIRECTORY_SCHEMA = "authtoken";
059
060    protected static final String USERNAME_FIELD = "userName";
061
062    protected static final String TOKEN_FIELD = "token";
063
064    protected static final String APPLICATION_NAME_FIELD = "applicationName";
065
066    protected static final String DEVICE_ID_FIELD = "deviceId";
067
068    protected static final String DEVICE_DESCRIPTION_FIELD = "deviceDescription";
069
070    protected static final String PERMISSION_FIELD = "permission";
071
072    protected static final String CREATION_DATE_FIELD = "creationDate";
073
074    @Override
075    public String acquireToken(String userName, String applicationName, String deviceId, String deviceDescription,
076            String permission) throws TokenAuthenticationException {
077
078        // Look for a token bound to the (userName,
079        // applicationName, deviceId) triplet, if it exists return it,
080        // else generate a unique one
081        String token = getToken(userName, applicationName, deviceId);
082        if (token != null) {
083            return token;
084        }
085
086        // Check required parameters (userName, applicationName and deviceId are
087        // already checked in #getToken)
088        if (StringUtils.isEmpty(permission)) {
089            throw new TokenAuthenticationException(
090                    "The permission parameter is mandatory to acquire an authentication token.");
091        }
092
093        // Log in as system user
094        LoginContext lc;
095        try {
096            lc = Framework.login();
097        } catch (LoginException e) {
098            throw new NuxeoException("Cannot log in as system user", e);
099        }
100        try {
101            // Open directory session
102            try (Session session = Framework.getService(DirectoryService.class).open(DIRECTORY_NAME)) {
103                // Generate random token, store the binding and return the token
104                UUID uuid = UUID.randomUUID();
105                token = uuid.toString();
106
107                final DocumentModel entry = getBareAuthTokenModel(Framework.getService(DirectoryService.class));
108                entry.setProperty(DIRECTORY_SCHEMA, TOKEN_FIELD, token);
109                entry.setProperty(DIRECTORY_SCHEMA, USERNAME_FIELD, userName);
110                entry.setProperty(DIRECTORY_SCHEMA, APPLICATION_NAME_FIELD, applicationName);
111                entry.setProperty(DIRECTORY_SCHEMA, DEVICE_ID_FIELD, deviceId);
112                if (!StringUtils.isEmpty(deviceDescription)) {
113                    entry.setProperty(DIRECTORY_SCHEMA, DEVICE_DESCRIPTION_FIELD, deviceDescription);
114                }
115                entry.setProperty(DIRECTORY_SCHEMA, PERMISSION_FIELD, permission);
116                Calendar creationDate = Calendar.getInstance();
117                creationDate.setTimeInMillis(System.currentTimeMillis());
118                entry.setProperty(DIRECTORY_SCHEMA, CREATION_DATE_FIELD, creationDate);
119                session.createEntry(entry);
120
121                log.debug(String.format(
122                        "Generated unique token for the (userName, applicationName, deviceId) triplet: ('%s', '%s', '%s'), returning it.",
123                        userName, applicationName, deviceId));
124                return token;
125
126            }
127        } finally {
128            try {
129                // Login context may be null in tests
130                if (lc != null) {
131                    lc.logout();
132                }
133            } catch (LoginException e) {
134                throw new NuxeoException("Cannot log out system user", e);
135            }
136        }
137    }
138
139    @Override
140    public String getToken(String userName, String applicationName, String deviceId)
141            throws TokenAuthenticationException {
142
143        if (StringUtils.isEmpty(userName) || StringUtils.isEmpty(applicationName) || StringUtils.isEmpty(deviceId)) {
144            throw new TokenAuthenticationException(
145                    "The following parameters are mandatory to get an authentication token: userName, applicationName, deviceId.");
146        }
147
148        // Log in as system user
149        LoginContext lc;
150        try {
151            lc = Framework.login();
152        } catch (LoginException e) {
153            throw new NuxeoException("Cannot log in as system user", e);
154        }
155        try {
156            // Open directory session
157            try (Session session = Framework.getService(DirectoryService.class).open(DIRECTORY_NAME)) {
158                // Look for a token bound to the (userName,
159                // applicationName, deviceId) triplet, if it exists return it,
160                // else return null
161                final Map<String, Serializable> filter = new HashMap<String, Serializable>();
162                filter.put(USERNAME_FIELD, userName);
163                filter.put(APPLICATION_NAME_FIELD, applicationName);
164                filter.put(DEVICE_ID_FIELD, deviceId);
165                DocumentModelList tokens = session.query(filter);
166                if (!tokens.isEmpty()) {
167                    // Multiple tokens found for the same triplet, this is
168                    // inconsistent
169                    if (tokens.size() > 1) {
170                        throw new NuxeoException(String.format(
171                                "Found multiple tokens for the (userName, applicationName, deviceId) triplet: ('%s', '%s', '%s'), this is inconsistent.",
172                                userName, applicationName, deviceId));
173                    }
174                    // Return token
175                    log.debug(String.format(
176                            "Found token for the (userName, applicationName, deviceId) triplet: ('%s', '%s', '%s'), returning it.",
177                            userName, applicationName, deviceId));
178                    DocumentModel tokenModel = tokens.get(0);
179                    return tokenModel.getId();
180                }
181
182                log.debug(String.format(
183                        "No token found for the (userName, applicationName, deviceId) triplet: ('%s', '%s', '%s'), returning null.",
184                        userName, applicationName, deviceId));
185                return null;
186            }
187        } finally {
188            try {
189                // Login context may be null in tests
190                if (lc != null) {
191                    lc.logout();
192                }
193            } catch (LoginException e) {
194                throw new NuxeoException("Cannot log out system user", e);
195            }
196        }
197    }
198
199    @Override
200    public String getUserName(final String token) {
201
202        // Log in as system user
203        LoginContext lc;
204        try {
205            lc = Framework.login();
206        } catch (LoginException e) {
207            throw new NuxeoException("Cannot log in as system user", e);
208        }
209        try {
210            try (Session session = Framework.getService(DirectoryService.class).open(DIRECTORY_NAME)) {
211                DocumentModel entry = session.getEntry(token);
212                if (entry == null) {
213                    log.debug(String.format("Found no user name bound to the token: '%s', returning null.", token));
214                    return null;
215                }
216                log.debug(String.format("Found a user name bound to the token: '%s', returning it.", token));
217                return (String) entry.getProperty(DIRECTORY_SCHEMA, USERNAME_FIELD);
218
219            }
220        } finally {
221            try {
222                // Login context may be null in tests
223                if (lc != null) {
224                    lc.logout();
225                }
226            } catch (LoginException e) {
227                throw new NuxeoException("Cannot log out system user", e);
228            }
229        }
230    }
231
232    @Override
233    public void revokeToken(final String token) {
234
235        // Log in as system user
236        LoginContext lc;
237        try {
238            lc = Framework.login();
239        } catch (LoginException e) {
240            throw new NuxeoException("Cannot log in as system user", e);
241        }
242        try {
243            try (Session session = Framework.getService(DirectoryService.class).open(DIRECTORY_NAME)) {
244                session.deleteEntry(token);
245                log.info(String.format("Deleted token: '%s' from the back-end.", token));
246            }
247        } finally {
248            try {
249                // Login context may be null in tests
250                if (lc != null) {
251                    lc.logout();
252                }
253            } catch (LoginException e) {
254                throw new NuxeoException("Cannot log out system user", e);
255            }
256        }
257    }
258
259    @Override
260    public DocumentModelList getTokenBindings(String userName) {
261
262        // Log in as system user
263        LoginContext lc;
264        try {
265            lc = Framework.login();
266        } catch (LoginException e) {
267            throw new NuxeoException("Cannot log in as system user", e);
268        }
269        try {
270            try (Session session = Framework.getService(DirectoryService.class).open(DIRECTORY_NAME)) {
271                final Map<String, Serializable> filter = new HashMap<String, Serializable>();
272                filter.put(USERNAME_FIELD, userName);
273                final Map<String, String> orderBy = new HashMap<String, String>();
274                orderBy.put(CREATION_DATE_FIELD, "desc");
275                return session.query(filter, Collections.<String> emptySet(), orderBy);
276            }
277        } finally {
278            try {
279                // Login context may be null in tests
280                if (lc != null) {
281                    lc.logout();
282                }
283            } catch (LoginException e) {
284                throw new NuxeoException("Cannot log out system user", e);
285            }
286        }
287    }
288
289    protected DocumentModel getBareAuthTokenModel(DirectoryService directoryService) {
290
291        String directorySchema = directoryService.getDirectorySchema(DIRECTORY_NAME);
292        return BaseSession.createEntryModel(null, directorySchema, null, null);
293    }
294
295}