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