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}